Compare commits

...

6 Commits

Author SHA1 Message Date
Jon Chery bbabd2dc0a feat(P03): core agent flesh — VerifierAgent, ResearcherAgent, TesterAgent intrinsic logic 2026-05-29 18:08:38 +00:00
Jon Chery 99df4fe4e2 feat(P02): orchestrator enrichment — GitAgentContext, multi-phase, error recovery, timer cleanup, TEST stage
---ci---
phase: 2
milestone: v0.6
status: execute
decisions:
  - id: D-001
    decision: Pass GitAgentContext to agents instead of bare AgentContext
    rationale: Agents need git-native context (gitContext, gitBranch, ciFiles, milestone) to operate autonomously
    confidence: 0.95
  - id: D-002
    decision: Implement multi-phase iteration with totalPhases derived from ROADMAP.md
    rationale: Milestones can span multiple phases; orchestrator must advance through all of them
    confidence: 0.90
  - id: D-003
    decision: Add executeStageWithRecovery with retry + plan revision + escalation
    rationale: Robust error recovery requires multiple fallback levels before giving up
    confidence: 0.85
  - id: D-004
    decision: Add timer-to-escalation mapping in EscalationProtocol for proper cleanup
    rationale: resolveEscalation must clearTimeout for the corresponding timer to prevent resource leaks
    confidence: 0.90
  - id: D-005
    decision: Add dispose() to EscalationProtocol called in orchestrator finally block
    rationale: Ensures all timers are cleaned up on orchestrator exit regardless of outcome
    confidence: 0.95
  - id: D-006
    decision: Add mechanical TEST stage fallback running npm test via execSync
    rationale: When no backend is available, tests can still be run mechanically
    confidence: 0.85
---/ci---
2026-05-29 18:05:36 +00:00
Jon Chery 8527df24b3 fix(P01): rename ci-files.test.ts → ciagent-files.test.ts
---ci---
project: ci
phase: 1
milestone: v0.7
status: execute
requirements:
  covered: [RENAME-01, RENAME-02, RENAME-03, RENAME-04, RENAME-05, RENAME-06, RENAME-07, RENAME-08, RENAME-09, RENAME-10, RENAME-11, RENAME-12]
---/ci---

All 12 RENAME requirements covered. 31 test suites, 370 tests passing.
2026-05-29 18:03:31 +00:00
Jon Chery 4a58aa1657 refactor(rebrand): rename & rebrand CI → CIAgent across all source and test files
- Type renames: CIConfig → CIAgentConfig, DEFAULT_CI_CONFIG → DEFAULT_CIAGENT_CONFIG
- Type renames: CiMetadata → CIAgentMetadata, ParsedCiCommit → ParsedCIAgentCommit
- Function renames: initCI → initCIAgent, isCIInitialized → isCIAgentInitialized
- Function renames: extractCiBlock → extractCIAgentBlock, parseCiBlock → parseCIAgentBlock
- Class renames: CiFiles → CIAgentFiles
- Import paths: ci-files.js → ciagent-files.js
- Directory paths: .ci/ → .ciagent/ across all source and test files
- Check names: ".ci directory exists" → ".ciagent directory exists"
- Check names: "CI config valid" → "CIAgent config valid"
- Temp dir names: ci-*-test- → ciagent-*-test-
- CLI examples: "ci init" → "ciagent init"
- Fix deepMerge infinite recursion bug in config.ts
- ---ci---/---/ci--- block markers preserved unchanged
- All 31 test suites, 370 tests passing

---ci---
phase: 1
milestone: v0.5
plan: 07
task: 07-01-01
status: execute
---/ci---
2026-05-29 18:01:13 +00:00
Jon Chery e31afe3b59 docs(rebrand): rename & rebrand CI → CIAgent across all documentation, templates, and scripts
- README.md: title, project name, CLI commands, .ci/ → .ciagent/, ci-files → ciagent-files, CI Modification → CIAgent Modification
- AGENTS.md: title, project name, architecture tree, agent count (18→19), test count (25→31 suites, 218→370 tests), version (0.4.0→0.6.0), ci-files → ciagent-files, CIConfig → CIAgentConfig, CiMetadata → CIAgentMetadata, .ci/ → .ciagent/
- templates/DECISIONS.md: .ci/audit/ → .ciagent/audit/, ci audit → ciagent audit
- scripts/postinstall.js: CI postinstall → CIAgent postinstall
- scripts/install.sh: CI → CIAgent, ci-init → ciagent-init, INSTALL COMPLETE banner
- opencode/ci/workflows/*.md (11 files): .ci/ → .ciagent/, CI → CIAgent project name, ci-command → ciagent-command usage lines
- opencode/ci/references/*.md (5 files): .ci/ → .ciagent/, CI → CIAgent project name, ci-files → ciagent-files references
- opencode/ci/contexts/*.md (3 files): .ci/ → .ciagent/, CI → CIAgent project name
- opencode/agents/ci-*.md (18 files): .ci/ → .ciagent/, CI → CIAgent project name
- opencode/command/ci-*.md (11 files): CI → CIAgent project name

Preserved: ---ci---/---/ci--- markers, opencode/ci/ dir paths, ci-*.md filenames, ci listProjects()/ci setActiveProject() API names, repo URLs

---ci---
phase: 1
milestone: v0.6
plan: 01-01
task: 01-01-01
status: execute
---/ci---
2026-05-29 17:58:48 +00:00
Jon Chery ab6af144b7 feat(P06): 3-tier versioning, branch hierarchy enforcement, ARCHITECTURE-PLAN synthesis
---ci---
phase: 6
milestone: v0.5
status: complete
decisions:
  - id: D-006
    decision: Research as intermediate work product
    rationale: Conclusions update .ci/ files; full research doc intentionally not preserved
    confidence: 0.90
  - id: D-007
    decision: Branch hierarchy enforcement: main > milestone > phase
    rationale: Prevents out-of-order merges and semantically wrong tags
    confidence: 0.92
  - id: D-008
    decision: 3-tier versioning: NFR/feature/schema-breaking
    rationale: Patch per phase (NFR/feature) or minor per phase (schema-breaking); milestone gets minor (feature) or major (schema-breaking)
    confidence: 0.95
requirements:
  covered: [VER-06, BRANCH-01, BRANCH-02, ARCH-01]
---/ci---

- Synthesize ARCHITECTURE-PLAN.md into .ci/ci/ARCHITECTURE.md (expanded 51→230 lines)
- Add D-006/D-007/D-008 to .ci/ci/PROJECT.md key decisions table
- Delete ARCHITECTURE-PLAN.md after synthesis
- Rewrite ship.md with 3-tier versioning model + branch hierarchy merge flows
- Rewrite branch-strategy.md with 3-tier versioning + branch hierarchy + version validation
- Add MilestoneType to config types
- Replace isNfrMilestone() with getMilestoneType() returning nfr|feature|schema-breaking
- Add validateMergeOrder(), mergeMilestoneBranch(), computeMilestoneTag() to GitBranch
- Add computeShipVersion(), validateVersionOrder(), resolveMergeTarget() to ship command
- Remove hardcoded v0.5. from error-recovery rollback
- Create .githooks/pre-push for semver ordering + branch hierarchy validation
- Add 15 new tests (370 total, all passing)
2026-05-29 17:18:10 +00:00
112 changed files with 3049 additions and 1048 deletions
+80
View File
@@ -0,0 +1,80 @@
#!/bin/bash
# CI pre-push hook: enforce versioning and branching rules
# Install: git config core.hooksPath .githooks
zero="0000000000000000000000000000000000000000"
while read local_ref local_oid remote_ref remote_oid; do
if [ "$local_oid" = "$zero" ]; then
continue
fi
# Check pushed tags
if echo "$local_ref" | grep -qE "^refs/tags/"; then
tag_name=$(echo "$local_ref" | sed 's|^refs/tags/||')
# Validate semver format
if echo "$tag_name" | grep -qE "^v[0-9]+\.[0-9]+\.[0-9]+$"; then
tag_major=$(echo "$tag_name" | sed 's/v\([0-9]*\)\.[0-9]*\.[0-9]*/\1/')
tag_minor=$(echo "$tag_name" | sed 's/v[0-9]*\.\([0-9]*\)\.[0-9]*/\1/')
tag_patch=$(echo "$tag_name" | sed 's/v[0-9]*\.[0-9]*\.\([0-9]*\)/\1/')
# Check for semver ordering violations
for existing_tag in $(git tag -l "v${tag_major}.${tag_minor}.*" 2>/dev/null); do
if [ "$existing_tag" = "$tag_name" ]; then
continue
fi
existing_patch=$(echo "$existing_tag" | sed 's/v[0-9]*\.[0-9]*\.\([0-9]*\)/\1/')
if [ "$existing_patch" -ge "$tag_patch" ] && [ "$tag_patch" -le "$existing_patch" ]; then
echo "ERROR: Tag $tag_name is not greater than existing tag $existing_tag"
echo " Milestone tags must be the NEXT version (e.g., v0.6.0 after v0.5.1-5, NOT v0.5.0)"
exit 1
fi
done
# Check for milestone-tags-below-phase-tags
# If this is a .0 tag (milestone), verify no .N tags exist with higher patch
if [ "$tag_patch" = "0" ]; then
for existing_tag in $(git tag -l "v${tag_major}.${tag_minor}.*" 2>/dev/null); do
existing_patch=$(echo "$existing_tag" | sed 's/v[0-9]*\.[0-9]*\.\([0-9]*\)/\1/')
if [ "$existing_patch" -gt 0 ] && [ "$existing_patch" -gt "$tag_patch" ]; then
echo "ERROR: Milestone tag $tag_name is below existing phase tags (e.g., $existing_tag)"
echo " Feature milestone completion must be tagged as v${tag_major}.$(($tag_minor + 1)).0, not v${tag_major}.${tag_minor}.0"
exit 1
fi
done
fi
fi
fi
# Check branch merges: reject direct-to-main pushes if milestone branch exists
if echo "$local_ref" | grep -qE "^refs/heads/main$"; then
milestone_branches=$(git branch -r 2>/dev/null | grep 'milestone/v' | grep -v ':$' || true)
if [ -n "$milestone_branches" ]; then
# Allow if this is a merge commit from a milestone branch
merge_parents=$(git cat-file -p "$local_oid" 2>/dev/null | grep "^parent" | wc -l)
if [ "$merge_parents" -lt 2 ]; then
# Not a merge commit — check if there are active milestone branches
active_milestones=""
for mb in $milestone_branches; do
clean_name=$(echo "$mb" | sed 's|^[^/]*/||' | tr -d ' ')
merged=$(git branch -r --merged origin/main 2>/dev/null | grep "$clean_name" || true)
if [ -z "$merged" ]; then
active_milestones="$active_milestones $clean_name"
fi
done
if [ -n "$active_milestones" ]; then
echo "WARNING: Pushing directly to main while active milestone branches exist:"
for ms in $active_milestones; do
echo " - $ms"
done
echo " Phase branches should merge into the milestone branch first."
# Warning only — not blocking. The code-level enforcement in git-branch.ts
# is the hard gate; this hook is a safety net.
fi
fi
fi
fi
done
exit 0
+34 -34
View File
@@ -1,4 +1,4 @@
# AGENTS.md — CI Project Guidelines
# AGENTS.md — CIAgent Project Guidelines
## Build & Run Commands
@@ -9,16 +9,16 @@
## Project Overview
CI (Continuous Intelligence) is a fully autonomous AI-driven software engineering harness. It receives a specification, resolves ambiguities through a single Clarify phase, then executes the full pipeline (research → plan → execute → verify) autonomously, escalating only when it cannot safely proceed alone.
CIAgent (Continuous Intelligence) is a fully autonomous AI-driven software engineering harness. It receives a specification, resolves ambiguities through a single Clarify phase, then executes the full pipeline (research → plan → execute → verify) autonomously, escalating only when it cannot safely proceed alone.
## Architecture
```
src/
agents/ # 18 agent implementations (persona loaders delegating to backends)
agents/ # 19 agent implementations (persona loaders delegating to backends)
backends/ # Intelligence backend layer
types.ts # IntelligenceBackend, BackendRequest, BackendResult, BackendConfigSection
tool-registry.ts # CI-owned tool implementations (readFile, writeFile, editFile, runBash, glob, grep)
tool-registry.ts # CIAgent-owned tool implementations (readFile, writeFile, editFile, runBash, glob, grep)
ollama-base.ts # Abstract base for Ollama backends (shared tool loop, prompt construction)
ollama-local.ts # OllamaLocalBackend (localhost:11434)
ollama-cloud.ts # OllamaCloudBackend (remote endpoint, auth, rate limiting)
@@ -26,21 +26,21 @@ src/
index.ts # Backend registry + auto-detection
cli/ # Commander.js CLI (commands.ts, index.ts)
core/ # Core engine components
artifacts.ts # Legacy .ci/ artifact management (retained for backward compat)
audit.ts # Legacy audit trail in .ci/audit/ (retained for backward compat)
ci-files.ts # .ci/ long-lived reference file management (PROJECT.md, ROADMAP.md, etc.)
artifacts.ts # Legacy .ciagent/ artifact management (retained for backward compat)
audit.ts # Legacy audit trail in .ciagent/audit/ (retained for backward compat)
ciagent-files.ts # .ciagent/ long-lived reference file management (PROJECT.md, ROADMAP.md, etc.)
clarify.ts # Clarify phase: question generation, default acceptance
commit-builder.ts # Structured commit message generation (---ci--- YAML blocks)
commit-parser.ts # ---ci--- YAML block extraction and parsing
config.ts # .ci/config.json load/save/init
config.ts # .ciagent/config.json load/save/init
decision-engine.ts # Bounded rationality: commits decisions as git artifacts
error-recovery.ts # Retry, plan revision, rollback logic
escalation.ts # Escalation protocol: commits escalations as git artifacts
git-branch.ts # Branch lifecycle: phase/NN-slug, milestone/vX.X-slug
git-context.ts # Project state reconstruction from git log + branches
types/ # Type definitions
commit-meta.ts # CiMetadata, CommitDecision, CommitEscalation, ParsedCiCommit
config.ts # CIConfig, AutonomyLevel, ModelProfile, DEFAULT_CI_CONFIG (includes backend)
commit-meta.ts # CIAgentMetadata, CommitDecision, CommitEscalation, ParsedCIAgentCommit
config.ts # CIAgentConfig, AutonomyLevel, ModelProfile, DEFAULT_CIAGENT_CONFIG (includes backend)
decisions.ts # Decision, ConfidenceLevel, DecisionCategory
escalation.ts # Escalation, EscalationType, EscalationResolution
clarify.ts # ClarifyQuestion, ClarifyResult
@@ -53,7 +53,7 @@ src/
security.ts # Layer 3: regex-based threat pattern scanning (no STRIDE analysis yet)
quality.ts # Layer 4: regex-based code quality checks (no multi-persona review yet)
index.ts # Public API exports
version.ts # VERSION = "0.4.0"
version.ts # VERSION = "0.6.0"
templates/ # Template files (config.json, DECISIONS.md, specification.md)
```
@@ -62,9 +62,9 @@ templates/ # Template files (config.json, DECISIONS.md, specification.md
- **Autonomy levels**: `full` (no HITL after clarify), `supervised` (escalate on gates + verification failures), `guided` (escalate on every decision gate)
- **Decision confidence thresholds**: High (>0.85) auto-decide and log; Medium (0.600.85) auto-decide with assumption logging; Low (<0.60) escalate to human
- **Escalation timeout**: Default 5 minutes, then auto-proceeds with recommended option. Set to `0` to require human, `-1` to always auto-proceed
- **18 agents** purpose-built for CI, all configured for autonomous operation. OrchestratorAgent is CI-specific
- **Git-native context**: The git log IS the project memory. Agent's first impulse to gather context is `git log` + `git branch`, not file reads. Dynamic state (decisions, escalations, lessons, compounding) lives in `---ci---` YAML blocks in commit messages. `.ci/` holds only long-lived reference docs (PROJECT.md, ARCHITECTURE.md, ROADMAP.md, REQUIREMENTS.md, config.json).
- **Artifact compatibility**: CI no longer writes `.planning/` schema. Dynamic state is derived from git history. `.ci/` files follow a CI-native schema.
- **19 agents** purpose-built for CIAgent, all configured for autonomous operation. OrchestratorAgent is CIAgent-specific
- **Git-native context**: The git log IS the project memory. Agent's first impulse to gather context is `git log` + `git branch`, not file reads. Dynamic state (decisions, escalations, lessons, compounding) lives in `---ci---` YAML blocks in commit messages. `.ciagent/` holds only long-lived reference docs (PROJECT.md, ARCHITECTURE.md, ROADMAP.md, REQUIREMENTS.md, config.json).
- **Artifact compatibility**: CIAgent no longer writes `.planning/` schema. `.ciagent/` files follow a CIAgent-native schema.
## Code Conventions
@@ -73,7 +73,7 @@ templates/ # Template files (config.json, DECISIONS.md, specification.md
- **Agent pattern**: All agents extend `BaseAgent` with `name` (AgentName), `description`, `workflow`, and `execute(context: AgentContext): Promise<AgentResult>`. Agents delegate to `context.backend` when available, fail honestly when not.
- **No runtime validation library**: Uses plain TypeScript types, not Zod schemas (Zod is a dependency but types are hand-defined)
- **File I/O**: Use `src/utils/file.ts` helpers (`writeFile`, `readFile`, `ensureDir`, `readJSON`, `writeJSON`) instead of raw `fs` calls in agent/business logic
- **Config**: `CIConfig` type and `DEFAULT_CI_CONFIG` in `src/types/config.ts` — always merge partial configs with defaults
- **Config**: `CIAgentConfig` type and `DEFAULT_CIAGENT_CONFIG` in `src/types/config.ts` — always merge partial configs with defaults
- **Error handling**: Agents return `{ success: false, error: string }` rather than throwing
- **No comments in code**: Follow existing pattern — agent files have no comments
- **Naming**: `camelCase` for functions/variables, `PascalCase` for classes/types/interfaces, `kebab-case` for file names
@@ -91,20 +91,20 @@ Each stage is executed by `OrchestratorAgent.executeStage()`. The orchestrator d
```
IntelligenceBackend (unified interface)
├── LLMBackend (CI runs tool loop, provides tools, constructs prompts)
├── LLMBackend (CIAgent runs tool loop, provides tools, constructs prompts)
│ ├── OllamaLocalBackend (localhost:11434, no auth)
│ ├── OllamaCloudBackend (remote endpoint, API key, rate limits)
│ └── (future: OpenAI, Anthropic, Gemini, etc.)
└── AgentBackend (agent runs own tool loop, CI sends request)
└── AgentBackend (agent runs own tool loop, CIAgent sends request)
├── OpencodeBackend (opencode --non-interactive)
└── (future: Codex, Claude Code, Hermes, etc.)
```
- **LLM backends**: CI constructs system prompts from persona.md + workflow.md, defines tool schemas, runs the tool-call loop via `ToolRegistry`, and parses structured JSON output
- **Agent backends**: CI serializes `BackendRequest`, invokes the agent, and parses JSON `BackendResult` from stdout
- **LLM backends**: CIAgent constructs system prompts from persona.md + workflow.md, defines tool schemas, runs the tool-call loop via `ToolRegistry`, and parses structured JSON output
- **Agent backends**: CIAgent serializes `BackendRequest`, invokes the agent, and parses JSON `BackendResult` from stdout
- **Auto-detection** (provider: "auto"): tries opencode → ollama-local → ollama-cloud → fails with instructions
- **Per-command override**: `ci run --backend ollama-local` forces a specific backend
- **Config**: `backend` section in `.ci/config.json` with provider, fallback, agent_backends, llm_backends
- **Per-command override**: `ciagent run --backend ollama-local` forces a specific backend
- **Config**: `backend` section in `.ciagent/config.json` with provider, fallback, agent_backends, llm_backends
## Agent Modification Rules (from PRD)
@@ -131,17 +131,17 @@ IntelligenceBackend (unified interface)
- Test framework: Jest with ts-jest
- Test file pattern: `**/*.test.ts` in `src/`
- Run: `npm run test`
- 25 test suites, 218 tests covering types, core, git-native, verification, and utility modules
- 31 test suites, 370 tests covering types, core, git-native, verification, and utility modules
- Tests use temp directories (os.mkdtempSync) and clean up after each test
- Module resolution in jest uses moduleNameMapper to strip `.js` extensions
## Important Files
- `.ci/config.json` — Project-level CI configuration (autonomy, parallelization, verification, security, git)
- `.ci/PROJECT.md` — Vision, core value, requirements, constraints, key decisions table
- `.ci/ARCHITECTURE.md` — System architecture, component boundaries, data flow
- `.ci/ROADMAP.md` — Phase breakdown, milestone mapping, success criteria, progress table
- `.ci/REQUIREMENTS.md` — v1/v2 requirements with REQ-IDs and traceability matrix
- `.ciagent/config.json` — Project-level CIAgent configuration (autonomy, parallelization, verification, security, git)
- `.ciagent/PROJECT.md` — Vision, core value, requirements, constraints, key decisions table
- `.ciagent/ARCHITECTURE.md` — System architecture, component boundaries, data flow
- `.ciagent/ROADMAP.md` — Phase breakdown, milestone mapping, success criteria, progress table
- `.ciagent/REQUIREMENTS.md` — v1/v2 requirements with REQ-IDs and traceability matrix
- Git log — Primary project memory: decisions, escalations, lessons, compounding, verification results
- Branch structure — `phase/NN-slug` (active/complete) and `milestone/vX.X-slug` branches
@@ -191,16 +191,16 @@ IntelligenceBackend (unified interface)
## Current State
- **v0.4.0**: Backends module (OllamaLocal, OllamaCloud, Opencode), learnship references removed, verification layers migrated from .planning/ to .ci/
- **New modules**: commit-parser (`---ci---` YAML block extraction/parsing), commit-builder (structured commit message generation), git-context (project state reconstruction from git log + branches), git-branch (phase/milestone branch lifecycle), ci-files (`.ci/` long-lived reference file management)
- **Commit schema**: Every CI-generated commit contains a `---ci---` YAML block with phase, milestone, status, decisions, escalations, requirements, lessons, and compound metadata
- **v0.6.0**: Backends module (OllamaLocal, OllamaCloud, Opencode), learnship references removed, verification layers migrated from .planning/ to .ciagent/
- **New modules**: commit-parser (`---ci---` YAML block extraction/parsing), commit-builder (structured commit message generation), git-context (project state reconstruction from git log + branches), git-branch (phase/milestone branch lifecycle), ciagent-files (`.ciagent/` long-lived reference file management)
- **Commit schema**: Every CIAgent-generated commit contains a `---ci---` YAML block with phase, milestone, status, decisions, escalations, requirements, lessons, and compound metadata
- **Branch strategy**: `phase/NN-slug` and `milestone/vX.X-slug` branches encode project structure; merged = complete, active = in progress
- **Core engine rewrites**: DecisionEngine generates commit messages (not audit JSON), EscalationProtocol commits escalations as git artifacts, OrchestratorAgent uses git log as first impulse
- **Removed**: `.ci/audit/` directory (audit trail is git log), `.planning/` directory (dynamic state derived from git history)
- **`.ci/` contents**: `config.json`, `PROJECT.md`, `ARCHITECTURE.md`, `ROADMAP.md`, `REQUIREMENTS.md` — long-lived reference docs updated with discipline
- **Removed**: `.ciagent/audit/` directory (audit trail is git log), `.planning/` directory (dynamic state derived from git history)
- **`.ciagent/` contents**: `config.json`, `PROJECT.md`, `ARCHITECTURE.md`, `ROADMAP.md`, `REQUIREMENTS.md` — long-lived reference docs updated with discipline
- **Reconstruction test**: An agent with only commit message access can reconstruct project state (phase, decisions, requirements coverage, lessons, escalations)
- **Verification layers**: All 4 layers implemented — structural, behavioral, security (STRIDE), quality
- **Verification layers**: All 4 layers implemented — structural, behavioral, security, quality
- **CLI**: All 11 commands wired up (`init`, `run`, `quick`, `debug`, `verify`, `review`, `status`, `audit`, `clarify`, `rollback`, `ship`)
- **Agent implementations**: Persona loaders that delegate to active backend. Fail honestly when no backend is available (no more fake success).
- **Intelligence backends**: OllamaLocal (LLM, localhost), OllamaCloud (LLM, remote), Opencode (Agent, --non-interactive). Auto-detection: opencode → ollama-local → ollama-cloud.
- **Tests**: 27 test suites covering types, config, decision-engine, escalation, clarify, commit-parser, commit-builder, git-context, git-branch, ci-files, all 4 verification layers, file utils, backends, tool-registry
- **Tests**: 31 test suites, 370 tests covering types, config, decision-engine, escalation, clarify, commit-parser, commit-builder, git-context, git-branch, ciagent-files, all 4 verification layers, file utils, backends, tool-registry
+36 -36
View File
@@ -1,10 +1,10 @@
# CI — Continuous Intelligence
# CIAgent — Continuous Intelligence
Fully autonomous, git-native AI-driven software engineering harness.
## Overview
CI (Continuous Intelligence) is an autonomous-first software engineering harness that eliminates human-in-the-loop overhead while preserving the rigor of guided development. It receives a specification, resolves ambiguities through a single Clarify phase, then executes the full pipeline — research, plan, execute, verify — autonomously.
CIAgent (Continuous Intelligence) is an autonomous-first software engineering harness that eliminates human-in-the-loop overhead while preserving the rigor of guided development. It receives a specification, resolves ambiguities through a single Clarify phase, then executes the full pipeline — research, plan, execute, verify — autonomously.
**The git log IS the project memory.** Every decision, escalation, lesson learned, and verification result is encoded in commit messages using structured `---ci---` YAML blocks. An agent's first impulse to gather context is `git log`, not file reads. Another agent with access to only commit messages (no code, no diffs) can reconstruct the project state completely.
@@ -14,7 +14,7 @@ From source (package not yet published to npm):
```bash
git clone https://git.cloudinit.dev/continuous-intelligence/ci.git
cd ci
cd ciagent
npm install
npm run build
npm link
@@ -24,45 +24,45 @@ npm link
```bash
# Initialize from inline specification
ci init "Build a REST API for task management"
ciagent init "Build a REST API for task management"
# Initialize from a specification file
ci init --spec ./specs/my-project.md
ciagent init --spec ./specs/my-project.md
# Run the full autonomous pipeline
ci run --all
ciagent run --all
# Run a specific phase
ci run research
ci run plan
ci run execute
ci run verify
ciagent run research
ciagent run plan
ciagent run execute
ciagent run verify
# Execute an ad-hoc task
ci quick "Add authentication middleware"
ciagent quick "Add authentication middleware"
# Check project status (reads from git log + branches)
ci status
ciagent status
# Review autonomous decisions (extracted from git log ---ci--- blocks)
ci audit
ci audit --verbose
ciagent audit
ciagent audit --verbose
# Debug an issue
ci debug "Tests failing on CI"
ciagent debug "Tests failing on CI"
# Rollback a phase
ci rollback 1
ciagent rollback 1
# Ship a phase (verify, security, commit, tag)
ci ship 1
ciagent ship 1
```
## Git-Native Architecture (v0.2.0)
### The Commit Schema
Every CI-generated commit contains a `---ci---` YAML block with structured metadata:
Every CIAgent-generated commit contains a `---ci---` YAML block with structured metadata:
```
feat(P01-01-02): create user registration endpoint
@@ -92,11 +92,11 @@ requirements:
| Where | What | Why |
|-------|------|-----|
| `.ci/config.json` | Autonomy, thresholds, git strategy | Controls system behavior before any commits exist |
| `.ci/PROJECT.md` | Vision, core value, requirements, constraints, key decisions table | Long-lived strategic reference |
| `.ci/ARCHITECTURE.md` | System architecture, component boundaries, data flow | Long-lived technical reference |
| `.ci/ROADMAP.md` | Phase breakdown, milestone mapping, success criteria | Long-lived planning reference |
| `.ci/REQUIREMENTS.md` | v1/v2 requirements with REQ-IDs and traceability | Long-lived requirements reference |
| `.ciagent/config.json` | Autonomy, thresholds, git strategy | Controls system behavior before any commits exist |
| `.ciagent/PROJECT.md` | Vision, core value, requirements, constraints, key decisions table | Long-lived strategic reference |
| `.ciagent/ARCHITECTURE.md` | System architecture, component boundaries, data flow | Long-lived technical reference |
| `.ciagent/ROADMAP.md` | Phase breakdown, milestone mapping, success criteria | Long-lived planning reference |
| `.ciagent/REQUIREMENTS.md` | v1/v2 requirements with REQ-IDs and traceability | Long-lived requirements reference |
| **Git commit bodies** | Decisions, escalations, lessons, compounds, verification results | Dynamic event stream — the audit trail |
| **Git branches** | Phase/milestone status | `phase/NN-slug` and `milestone/vX.X-slug` encode project structure |
@@ -121,17 +121,17 @@ An agent starting a session gathers context in this order:
1. `git log --oneline -20` — recent activity
2. `git branch -a` — phase/milestone structure
3. `git log -1 --format="%b"` — latest `---ci---` block
4. `.ci/config.json` — autonomy + thresholds
5. `.ci/PROJECT.md` — vision + constraints (when needed)
6. `.ci/ROADMAP.md` — phase plan + success criteria (when needed)
7. `.ci/REQUIREMENTS.md` — REQ-IDs + traceability (when planning)
8. `.ci/ARCHITECTURE.md` — system structure (when researching)
4. `.ciagent/config.json` — autonomy + thresholds
5. `.ciagent/PROJECT.md` — vision + constraints (when needed)
6. `.ciagent/ROADMAP.md` — phase plan + success criteria (when needed)
7. `.ciagent/REQUIREMENTS.md` — REQ-IDs + traceability (when planning)
8. `.ciagent/ARCHITECTURE.md` — system structure (when researching)
Steps 1-3 take <1 second and provide 80% of the context needed.
### The Reconstruction Test
An agent with access to **only commit messages** (no code, no diffs, no `.ci/` files) can reconstruct:
An agent with access to **only commit messages** (no code, no diffs, no `.ciagent/` files) can reconstruct:
| Reconstructable | How |
|---------------|-----|
@@ -148,7 +148,7 @@ An agent with access to **only commit messages** (no code, no diffs, no `.ci/` f
### Commit Types
In addition to conventional commit types, CI uses:
In addition to conventional commit types, CIAgent uses:
| Type | When Used |
|------|-----------|
@@ -168,7 +168,7 @@ In addition to conventional commit types, CI uses:
## Configuration
CI uses `.ci/config.json` for project configuration:
CIAgent uses `.ciagent/config.json` for project configuration:
```json
{
@@ -224,7 +224,7 @@ SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → VERIFY → COMPLETE
| `commit-builder` | Structured commit message generation for all commit types |
| `git-context` | Project state reconstruction from `git log` + `git branch` |
| `git-branch` | Phase/milestone branch lifecycle management |
| `ci-files` | `.ci/` long-lived reference file management with update discipline |
| `ciagent-files` | `.ciagent/` long-lived reference file management with update discipline |
### Decision Engine
@@ -237,7 +237,7 @@ Decisions are committed to git as `decision` type commits. The audit trail is `g
### 18 Agents
| Agent | Role | CI Modification |
| Agent | Role | CIAgent Modification |
|-------|------|----------------|
| orchestrator | Pipeline controller | Git-first context loading, `---ci---` commit generation |
| planner | Plan creation | Never sets `autonomous: false` |
@@ -280,7 +280,7 @@ Build a REST API for task management.
## Escalation Protocol
When CI cannot proceed autonomously:
When CIAgent cannot proceed autonomously:
1. **Irreversible Action**: Deploy, delete, merge to protected branch
2. **Verification Failure**: Tests pass but functional verification fails
@@ -298,10 +298,10 @@ Each escalation is committed as an `escalation` type commit. Resolved escalation
## Differences from Learnship
| Dimension | Learnship | CI |
| Dimension | Learnship | CIAgent |
|-----------|-----------|-----|
| Project memory | `.planning/` directory files (legacy) | Git log + `---ci---` commit blocks |
| Audit trail | `.ci/audit/*.json` files (legacy) | `git log --grep="decisions:"` |
| Audit trail | `.ciagent/audit/*.json` files (legacy) | `git log --grep="decisions:"` |
| State management | `STATE.md` + `STATE.md.json` (legacy) | Reconstructed from git on demand |
| Phase discovery | Read `.planning/phases/` directory (legacy) | `git branch -a \| grep phase/` |
| Human Interactions | 19+/lifecycle | 1-2/lifecycle |
+9 -9
View File
@@ -1,5 +1,5 @@
---
description: Stress-tests CI proposals through product and engineering lenses using forcing questions. Binding verdicts — only escalates when confidence < 0.60.
description: Stress-tests CIAgent proposals through product and engineering lenses using forcing questions. Binding verdicts — only escalates when confidence < 0.60.
color: "#FFA500"
tools:
read: true
@@ -9,28 +9,28 @@ tools:
---
<role>
You are a CI challenger. You stress-test proposals through product and engineering lenses using forcing questions that expose weak assumptions.
You are a CIAgent challenger. You stress-test proposals through product and engineering lenses using forcing questions that expose weak assumptions.
CI challengers produce binding verdicts. Only escalate when confidence < 0.60. If confident the proposal is sound, it proceeds. If confident it needs rework, it is sent back.
CIAgent challengers produce binding verdicts. Only escalate when confidence < 0.60. If confident the proposal is sound, it proceeds. If confident it needs rework, it is sent back.
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before challenging, load context from git first:
1. Run `git log --max-count=30` for recent decisions and project history
2. Use GitContext.getDecisions(currentPhase) for phase decisions
3. Read `.ci/PROJECT.md` for project vision and constraints
4. Read `.ci/ARCHITECTURE.md` for component boundaries
3. Read `.ciagent/PROJECT.md` for project vision and constraints
4. Read `.ciagent/ARCHITECTURE.md` for component boundaries
5. Use GitContext.getCompounds() for compound learnings
</project_context>
@@ -44,7 +44,7 @@ Read the proposal and all git context. Extract settled decisions that should not
For assigned lens (product or engineering):
1. Select 3-5 forcing questions most relevant to the proposal
2. Answer each based on evidence from git history and .ci/ files
2. Answer each based on evidence from git history and .ciagent/ files
3. Note confidence level for each answer
### Product Lens Questions
+7 -7
View File
@@ -1,5 +1,5 @@
---
description: Reviews CI code changes through a specific persona lens (correctness, testing, security, performance, maintainability, adversarial). Auto-applies P0 fixes. Flags P1+ for post-hoc review.
description: Reviews CIAgent code changes through a specific persona lens (correctness, testing, security, performance, maintainability, adversarial). Auto-applies P0 fixes. Flags P1+ for post-hoc review.
color: "#FF69B4"
tools:
read: true
@@ -10,20 +10,20 @@ tools:
---
<role>
You are a CI code reviewer. You review code changes through a specific persona lens, finding issues by severity and confidence.
You are a CIAgent code reviewer. You review code changes through a specific persona lens, finding issues by severity and confidence.
CI code reviewers auto-apply P0 fixes. P1+ issues are flagged for post-hoc review via `git log --grep="review"`.
CIAgent code reviewers auto-apply P0 fixes. P1+ issues are flagged for post-hoc review via `git log --grep="review"`.
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before reviewing, load context from git first:
@@ -31,7 +31,7 @@ Before reviewing, load context from git first:
1. Run `git log --max-count=10` for recent changes
2. Run `git diff HEAD~3` to see the changes being reviewed
3. Use GitContext.getDecisions() for design decisions that explain choices
4. Read `.ci/ARCHITECTURE.md` for component boundaries
4. Read `.ciagent/ARCHITECTURE.md` for component boundaries
5. Read `./AGENTS.md` for project conventions and coding standards
</project_context>
+6 -6
View File
@@ -11,20 +11,20 @@ tools:
---
<role>
You are a CI debugger. You investigate bugs using systematic scientific method — forming hypotheses, testing them against the codebase, and finding the exact root cause.
You are a CIAgent debugger. You investigate bugs using systematic scientific method — forming hypotheses, testing them against the codebase, and finding the exact root cause.
CI debuggers auto-diagnose and auto-fix when confidence > 0.60. Only low-confidence root causes are escalated to human.
CIAgent debuggers auto-diagnose and auto-fix when confidence > 0.60. Only low-confidence root causes are escalated to human.
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before debugging, load context from git first:
@@ -33,7 +33,7 @@ Before debugging, load context from git first:
2. Run `git diff HEAD~5` to see recent file changes
3. Use GitContext.getDecisions() for decisions that may be relevant
4. Read `./AGENTS.md` or `./CLAUDE.md` for project conventions
5. Read `.ci/ARCHITECTURE.md` for component boundaries
5. Read `.ciagent/ARCHITECTURE.md` for component boundaries
</project_context>
<execution_flow>
+7 -7
View File
@@ -1,5 +1,5 @@
---
description: Verifies CI documentation matches the live codebase — catches stale docs, missing sections, incorrect references. Uses git diff to detect code/doc drift.
description: Verifies CIAgent documentation matches the live codebase — catches stale docs, missing sections, incorrect references. Uses git diff to detect code/doc drift.
color: "#F0E68C"
tools:
read: true
@@ -9,7 +9,7 @@ tools:
---
<role>
You are a CI doc verifier. You verify that documentation matches the live codebase by catching stale docs, missing sections, and incorrect references.
You are a CIAgent doc verifier. You verify that documentation matches the live codebase by catching stale docs, missing sections, and incorrect references.
You use git diff and codebase analysis to detect drift between documentation and implementation.
@@ -18,18 +18,18 @@ If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before verifying, load context from git first:
1. Run `git diff HEAD~10` to see recent code changes
2. Run `git log --max-count=20` for recent doc updates
3. Read `.ci/PROJECT.md`, `.ci/ARCHITECTURE.md`, `.ci/REQUIREMENTS.md`, `.ci/ROADMAP.md`
3. Read `.ciagent/PROJECT.md`, `.ciagent/ARCHITECTURE.md`, `.ciagent/REQUIREMENTS.md`, `.ciagent/ROADMAP.md`
4. Read `./AGENTS.md` or `./CLAUDE.md` for project conventions
</project_context>
@@ -37,7 +37,7 @@ Before verifying, load context from git first:
## Step 1: Load Documentation
Read all .ci/ documentation files. Read the codebase for actual state.
Read all .ciagent/ documentation files. Read the codebase for actual state.
## Step 2: Cross-Reference
+8 -8
View File
@@ -1,5 +1,5 @@
---
description: Writes and updates CI project documentation files — grounded in the live codebase, verifies factual claims. Documentation updates are committed with ---ci--- blocks.
description: Writes and updates CIAgent project documentation files — grounded in the live codebase, verifies factual claims. Documentation updates are committed with ---ci--- blocks.
color: "#90EE90"
tools:
read: true
@@ -11,20 +11,20 @@ tools:
---
<role>
You are a CI doc writer. You write and update CI project documentation files, grounded in the live codebase. You verify factual claims against actual code.
You are a CIAgent doc writer. You write and update CIAgent project documentation files, grounded in the live codebase. You verify factual claims against actual code.
Documentation updates are committed with `---ci---` blocks. You update `.ci/` static files (PROJECT.md, ARCHITECTURE.md, ROADMAP.md, REQUIREMENTS.md) with discipline.
Documentation updates are committed with `---ci---` blocks. You update `.ciagent/` static files (PROJECT.md, ARCHITECTURE.md, ROADMAP.md, REQUIREMENTS.md) with discipline.
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before writing, load context from git first:
@@ -32,7 +32,7 @@ Before writing, load context from git first:
1. Run `git log --max-count=20` for recent changes that affect docs
2. Use GitContext.getDecisions() for decisions to document
3. Use GitContext.getRequirementsCoverage() for current coverage
4. Read the existing .ci/ file you're updating
4. Read the existing .ciagent/ file you're updating
5. Read the relevant source code to verify claims
</project_context>
@@ -51,7 +51,7 @@ Before writing any factual claim:
## Step 3: Write/Update Documentation
Use CiFiles methods to write .ci/ files:
Use CiFiles methods to write .ciagent/ files:
- writeProjectMd(project, reason)
- writeArchitectureMd(architecture)
- writeRoadmapMd(roadmap)
+9 -9
View File
@@ -1,5 +1,5 @@
---
description: Executes a single CI plan atomically — one task at a time with per-task commits and ---ci--- blocks. Never pauses for checkpoint. Creates automated verification scripts for traditionally human tasks.
description: Executes a single CIAgent plan atomically — one task at a time with per-task commits and ---ci--- blocks. Never pauses for checkpoint. Creates automated verification scripts for traditionally human tasks.
color: "#FFFF00"
tools:
read: true
@@ -11,20 +11,20 @@ tools:
---
<role>
You are a CI executor. You execute plan tasks atomically — one task at a time, committing after each with `---ci---` blocks.
You are a CIAgent executor. You execute plan tasks atomically — one task at a time, committing after each with `---ci---` blocks.
CI executors NEVER pause for checkpoints. Every task is autonomous. Create automated verification scripts for traditionally human tasks (manual testing, visual inspection, etc.).
CIAgent executors NEVER pause for checkpoints. Every task is autonomous. Create automated verification scripts for traditionally human tasks (manual testing, visual inspection, etc.).
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before executing, load context from git first:
@@ -32,8 +32,8 @@ Before executing, load context from git first:
1. Run `git log --max-count=20` for recent project history
2. Use GitContext.reconstructState() for current phase, milestone, stage
3. Use GitContext.getDecisions(currentPhase) for phase decisions
4. Read `.ci/PROJECT.md` for project constraints
5. Read `.ci/ARCHITECTURE.md` for component boundaries
4. Read `.ciagent/PROJECT.md` for project constraints
5. Read `.ciagent/ARCHITECTURE.md` for component boundaries
6. Read `./AGENTS.md` or `./CLAUDE.md` for project conventions
</project_context>
@@ -41,7 +41,7 @@ Before executing, load context from git first:
## Step 1: Load Context
Read the plan file. Extract wave, files_modified, autonomous (always true in CI), must_haves.
Read the plan file. Extract wave, files_modified, autonomous (always true in CIAgent), must_haves.
Load git context for current state and decisions.
+8 -8
View File
@@ -1,5 +1,5 @@
---
description: Generates codebase-grounded improvement ideas through a specific thinking frame for CI. Uses git history to understand the codebase evolution.
description: Generates codebase-grounded improvement ideas through a specific thinking frame for CIAgent. Uses git history to understand the codebase evolution.
color: "#FFD700"
tools:
read: true
@@ -9,7 +9,7 @@ tools:
---
<role>
You are a CI ideation agent. You generate codebase-grounded improvement ideas through a specific thinking frame. You use git history to understand the codebase evolution and identify improvement opportunities.
You are a CIAgent ideation agent. You generate codebase-grounded improvement ideas through a specific thinking frame. You use git history to understand the codebase evolution and identify improvement opportunities.
You do not implement changes. You produce ideas with rationale for the orchestrator to evaluate and potentially plan.
@@ -18,11 +18,11 @@ If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before ideating, load context from git first:
@@ -31,15 +31,15 @@ Before ideating, load context from git first:
2. Use GitContext.getDecisions() for existing decisions
3. Use GitContext.getCompounds() for compound learnings
4. Use GitContext.getLessons() for lessons that suggest improvements
5. Read `.ci/ARCHITECTURE.md` for component boundaries
6. Read `.ci/REQUIREMENTS.md` for incomplete requirements
5. Read `.ciagent/ARCHITECTURE.md` for component boundaries
6. Read `.ciagent/REQUIREMENTS.md` for incomplete requirements
</project_context>
<execution_flow>
## Step 1: Load Context
Read git history and .ci/ files. Understand the codebase's current state and evolution.
Read git history and .ciagent/ files. Understand the codebase's current state and evolution.
## Step 2: Apply Thinking Frame
+9 -9
View File
@@ -1,5 +1,5 @@
---
description: Orchestrates the full CI pipeline by iterating through pipeline stages, loading context from the git log first, and delegating to specialized agents. The orchestrator is CI-specific — it drives the SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → VERIFY → COMPLETE flow.
description: Orchestrates the full CIAgent pipeline by iterating through pipeline stages, loading context from the git log first, and delegating to specialized agents. The orchestrator is CIAgent-specific — it drives the SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → VERIFY → COMPLETE flow.
color: "#00BFFF"
tools:
read: true
@@ -11,9 +11,9 @@ tools:
---
<role>
You are the CI orchestrator. You drive the full CI pipeline by iterating through pipeline stages, making git-first context loading decisions, and delegating to specialized agents.
You are the CIAgent orchestrator. You drive the full CIAgent pipeline by iterating through pipeline stages, making git-first context loading decisions, and delegating to specialized agents.
CI operates autonomously after the clarify phase. You never pause for human checkpoints unless a decision falls below the confidence threshold or an escalation hook is triggered.
CIAgent operates autonomously after the clarify phase. You never pause for human checkpoints unless a decision falls below the confidence threshold or an escalation hook is triggered.
Your job: Execute stages in order, collect PhaseResult for each, handle errors via ErrorRecovery, and produce a final project outcome.
@@ -22,11 +22,11 @@ If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before any operation, load project context from git first:
@@ -37,9 +37,9 @@ Before any operation, load project context from git first:
4. Use GitContext.getEscalations() for any pending escalations
5. Use GitContext.getRequirementsCoverage() for covered/partial requirements
6. Use GitContext.getLessons() for learned lessons
7. Read `.ci/config.json` for autonomy level and parallelization settings
8. Read `.ci/PROJECT.md` for project vision and constraints
9. Read `.ci/ROADMAP.md` for phase breakdown and success criteria
7. Read `.ciagent/config.json` for autonomy level and parallelization settings
8. Read `.ciagent/PROJECT.md` for project vision and constraints
9. Read `.ciagent/ROADMAP.md` for phase breakdown and success criteria
</project_context>
<execution_flow>
+10 -10
View File
@@ -1,5 +1,5 @@
---
description: Researches how to implement a CI phase well — identifies pitfalls, recommends existing solutions. Uses git history and .ci/ files as primary context sources.
description: Researches how to implement a CIAgent phase well — identifies pitfalls, recommends existing solutions. Uses git history and .ciagent/ files as primary context sources.
color: "#4169E1"
tools:
read: true
@@ -9,20 +9,20 @@ tools:
---
<role>
You are a CI phase researcher. You research how to implement a phase well by identifying pitfalls, recommending existing solutions, and documenting findings.
You are a CIAgent phase researcher. You research how to implement a phase well by identifying pitfalls, recommending existing solutions, and documenting findings.
You use git history and .ci/ files as primary context sources. Research is an intermediate work product — conclusions update .ci/ static files, key findings go in the commit body, decisions go in ---ci--- blocks.
You use git history and .ciagent/ files as primary context sources. Research is an intermediate work product — conclusions update .ciagent/ static files, key findings go in the commit body, decisions go in ---ci--- blocks.
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before researching, load context from git first:
@@ -30,16 +30,16 @@ Before researching, load context from git first:
1. Run `git log --max-count=50` for full project history
2. Use GitContext.getDecisions() for existing decisions
3. Use GitContext.getCompounds() for compound learnings
4. Read `.ci/PROJECT.md` for project vision
5. Read `.ci/REQUIREMENTS.md` for phase requirements
6. Read `.ci/ARCHITECTURE.md` for system design
4. Read `.ciagent/PROJECT.md` for project vision
5. Read `.ciagent/REQUIREMENTS.md` for phase requirements
6. Read `.ciagent/ARCHITECTURE.md` for system design
</project_context>
<execution_flow>
## Step 1: Load Context
Read git history and .ci/ files. Understand the phase goal and requirements.
Read git history and .ciagent/ files. Understand the phase goal and requirements.
## Step 2: Research
+8 -8
View File
@@ -1,5 +1,5 @@
---
description: Verifies CI PLAN.md files for a phase — checks goal coverage, requirement IDs, task completeness, wave correctness, and vertical slice integrity. Uses git context for validation.
description: Verifies CIAgent PLAN.md files for a phase — checks goal coverage, requirement IDs, task completeness, wave correctness, and vertical slice integrity. Uses git context for validation.
color: "#32CD32"
tools:
read: true
@@ -9,7 +9,7 @@ tools:
---
<role>
You are a CI plan checker. You verify PLAN.md files for a phase by checking goal coverage, requirement IDs, task completeness, wave correctness, and vertical slice integrity.
You are a CIAgent plan checker. You verify PLAN.md files for a phase by checking goal coverage, requirement IDs, task completeness, wave correctness, and vertical slice integrity.
You use git context to validate that plans align with existing decisions and don't contradict locked choices.
@@ -18,20 +18,20 @@ If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before checking, load context from git first:
1. Run `git log --max-count=20` for recent decisions affecting this phase
2. Use GitContext.getDecisions() for locked decisions
3. Read `.ci/ROADMAP.md` for phase goal and success criteria
4. Read `.ci/REQUIREMENTS.md` for requirement IDs
5. Read `.ci/ARCHITECTURE.md` for component boundaries
3. Read `.ciagent/ROADMAP.md` for phase goal and success criteria
4. Read `.ciagent/REQUIREMENTS.md` for requirement IDs
5. Read `.ciagent/ARCHITECTURE.md` for component boundaries
</project_context>
<execution_flow>
+10 -10
View File
@@ -1,5 +1,5 @@
---
description: Creates executable plans for a CI phase — decomposes goals into vertical slice tasks with wave-ordered dependency analysis. Never sets autonomous: false. Plans are precise prompts, not documents that become prompts.
description: Creates executable plans for a CIAgent phase — decomposes goals into vertical slice tasks with wave-ordered dependency analysis. Never sets autonomous: false. Plans are precise prompts, not documents that become prompts.
color: "#00FF00"
tools:
read: true
@@ -10,29 +10,29 @@ tools:
---
<role>
You are a CI planner. You create executable plans for a phase by decomposing goals into atomic, independently verifiable tasks with wave-based dependency ordering.
You are a CIAgent planner. You create executable plans for a phase by decomposing goals into atomic, independently verifiable tasks with wave-based dependency ordering.
CI plans NEVER have `autonomous: false`. Every task is autonomous by default. Decompose into verifiable subtasks that an executor can implement without interpretation.
CIAgent plans NEVER have `autonomous: false`. Every task is autonomous by default. Decompose into verifiable subtasks that an executor can implement without interpretation.
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before planning, load context from git first:
1. Run `git log --max-count=50` to see recent decisions and project history
2. Read `.ci/PROJECT.md` for project vision and constraints
3. Read `.ci/REQUIREMENTS.md` for requirement IDs assigned to this phase
4. Read `.ci/ROADMAP.md` for phase goal and success criteria
5. Read `.ci/ARCHITECTURE.md` for component boundaries and build order
2. Read `.ciagent/PROJECT.md` for project vision and constraints
3. Read `.ciagent/REQUIREMENTS.md` for requirement IDs assigned to this phase
4. Read `.ciagent/ROADMAP.md` for phase goal and success criteria
5. Read `.ciagent/ARCHITECTURE.md` for component boundaries and build order
6. Use GitContext.getDecisions(currentPhase) for phase-specific decisions
7. Use GitContext.getLessons() for lessons that affect planning
8. Use GitContext.getCompounds() for compound learnings from past phases
+8 -8
View File
@@ -1,5 +1,5 @@
---
description: Researches the domain ecosystem for a new CI project. Produces reference files that inform roadmap creation. Uses web search and codebase analysis.
description: Researches the domain ecosystem for a new CIAgent project. Produces reference files that inform roadmap creation. Uses web search and codebase analysis.
color: "#4169E1"
tools:
read: true
@@ -9,7 +9,7 @@ tools:
---
<role>
You are a CI project researcher. You research the domain ecosystem for a new CI project, producing reference files that inform roadmap creation.
You are a CIAgent project researcher. You research the domain ecosystem for a new CI project, producing reference files that inform roadmap creation.
You investigate the technology stack, available features, system architecture patterns, and common pitfalls for the domain.
@@ -18,18 +18,18 @@ If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before researching, load context from git first:
1. Run `git log --max-count=20` for any prior project history
2. Read `.ci/PROJECT.md` for project vision (if exists)
3. Read `.ci/config.json` for project settings (if exists)
2. Read `.ciagent/PROJECT.md` for project vision (if exists)
3. Read `.ciagent/config.json` for project settings (if exists)
4. Search the codebase for existing implementations to reuse
</project_context>
@@ -49,7 +49,7 @@ Read the project specification. Understand what the project needs to accomplish.
## Step 3: Produce Reference Files
Update `.ci/` static files with research conclusions:
Update `.ciagent/` static files with research conclusions:
- PROJECT.md: project vision and requirements
- ARCHITECTURE.md: recommended system architecture
- REQUIREMENTS.md: formal requirements with IDs
+12 -12
View File
@@ -1,5 +1,5 @@
---
description: Synthesizes research files for CI into a cohesive summary for roadmap creation. Merges findings from stack, features, architecture, and pitfalls research.
description: Synthesizes research files for CIAgent into a cohesive summary for roadmap creation. Merges findings from stack, features, architecture, and pitfalls research.
color: "#87CEEB"
tools:
read: true
@@ -9,28 +9,28 @@ tools:
---
<role>
You are a CI research synthesizer. You synthesize research files into a cohesive summary for roadmap creation. You merge findings from stack, features, architecture, and pitfalls research.
You are a CIAgent research synthesizer. You synthesize research files into a cohesive summary for roadmap creation. You merge findings from stack, features, architecture, and pitfalls research.
You read git history and .ci/ files to understand what research has already been done, then produce a unified view.
You read git history and .ciagent/ files to understand what research has already been done, then produce a unified view.
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before synthesizing, load context from git first:
1. Run `git log --grep="research" --max-count=20` for prior research commits
2. Read `.ci/PROJECT.md` for project vision
3. Read `.ci/ARCHITECTURE.md` for architecture research
4. Read `.ci/REQUIREMENTS.md` for requirements research
2. Read `.ciagent/PROJECT.md` for project vision
3. Read `.ciagent/ARCHITECTURE.md` for architecture research
4. Read `.ciagent/REQUIREMENTS.md` for requirements research
5. Use GitContext.getDecisions() for research-based decisions
</project_context>
@@ -38,7 +38,7 @@ Before synthesizing, load context from git first:
## Step 1: Load All Research
Read all `.ci/` files and git history for research outputs. Identify the 4 research streams: stack, features, architecture, pitfalls.
Read all `.ciagent/` files and git history for research outputs. Identify the 4 research streams: stack, features, architecture, pitfalls.
## Step 2: Synthesize
@@ -50,11 +50,11 @@ Cross-reference the research streams:
## Step 3: Update .ci/ Files
Update `.ci/` static files with synthesized conclusions. Resolve contradictions by making decisions (logged with confidence).
Update `.ciagent/` static files with synthesized conclusions. Resolve contradictions by making decisions (logged with confidence).
## Step 4: Commit Synthesis
Commit updated .ci/ files with `---ci---` block capturing synthesis decisions.
Commit updated .ciagent/ files with `---ci---` block capturing synthesis decisions.
## Step 5: Return Result
+11 -11
View File
@@ -1,5 +1,5 @@
---
description: Investigates the domain for a CI phase using git history, web search, and codebase analysis. Never flags assumptions for human validation — logs assumptions to decisions with confidence scores.
description: Investigates the domain for a CIAgent phase using git history, web search, and codebase analysis. Never flags assumptions for human validation — logs assumptions to decisions with confidence scores.
color: "#4169E1"
tools:
read: true
@@ -9,20 +9,20 @@ tools:
---
<role>
You are a CI researcher. You investigate the domain for a phase using git history, web search, and codebase analysis.
You are a CIAgent researcher. You investigate the domain for a phase using git history, web search, and codebase analysis.
CI researchers NEVER flag `[ASSUMED]` for human validation. Instead, log assumptions to DecisionEngine with confidence scores. Low-confidence assumptions are escalated through the normal decision flow.
CIAgent researchers NEVER flag `[ASSUMED]` for human validation. Instead, log assumptions to DecisionEngine with confidence scores. Low-confidence assumptions are escalated through the normal decision flow.
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before researching, load context from git first:
@@ -30,16 +30,16 @@ Before researching, load context from git first:
1. Run `git log --max-count=50` for project history and prior research
2. Use GitContext.getDecisions() for existing decisions
3. Use GitContext.getCompounds() for compound learnings from past phases
4. Read `.ci/PROJECT.md` for project vision and constraints
5. Read `.ci/ARCHITECTURE.md` for component boundaries
6. Read `.ci/REQUIREMENTS.md` for requirements assigned to this phase
4. Read `.ciagent/PROJECT.md` for project vision and constraints
5. Read `.ciagent/ARCHITECTURE.md` for component boundaries
6. Read `.ciagent/REQUIREMENTS.md` for requirements assigned to this phase
</project_context>
<execution_flow>
## Step 1: Load Context
Read git history and .ci/ files. Extract phase requirements and existing decisions.
Read git history and .ciagent/ files. Extract phase requirements and existing decisions.
## Step 2: Research Domain
@@ -51,7 +51,7 @@ Read git history and .ci/ files. Extract phase requirements and existing decisio
## Step 3: Commit Findings
Research conclusions update `.ci/` static files. Key findings go in the commit body. Decisions go in `---ci---` blocks:
Research conclusions update `.ciagent/` static files. Key findings go in the commit body. Decisions go in `---ci---` blocks:
```
docs(P##): research [topic]
+10 -10
View File
@@ -1,5 +1,5 @@
---
description: Creates CI project roadmaps with phase breakdown, requirement mapping, success criteria derivation, and coverage validation. Uses git history to understand project context.
description: Creates CIAgent project roadmaps with phase breakdown, requirement mapping, success criteria derivation, and coverage validation. Uses git history to understand project context.
color: "#20B2AA"
tools:
read: true
@@ -10,7 +10,7 @@ tools:
---
<role>
You are a CI roadmapper. You create project roadmaps with phase breakdown, requirement mapping, success criteria derivation, and coverage validation.
You are a CIAgent roadmapper. You create project roadmaps with phase breakdown, requirement mapping, success criteria derivation, and coverage validation.
You use git history to understand the project context and ensure every requirement is mapped to a phase.
@@ -19,27 +19,27 @@ If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before roadmapping, load context from git first:
1. Run `git log --max-count=30` for project history
2. Use GitContext.getDecisions() for existing decisions
3. Read `.ci/PROJECT.md` for project vision and constraints
4. Read `.ci/REQUIREMENTS.md` for all requirements
5. Read `.ci/ARCHITECTURE.md` for component boundaries and build order
3. Read `.ciagent/PROJECT.md` for project vision and constraints
4. Read `.ciagent/REQUIREMENTS.md` for all requirements
5. Read `.ciagent/ARCHITECTURE.md` for component boundaries and build order
</project_context>
<execution_flow>
## Step 1: Load Context
Read git history and .ci/ files. Extract all requirements and architectural constraints.
Read git history and .ciagent/ files. Extract all requirements and architectural constraints.
## Step 2: Break Into Phases
@@ -50,7 +50,7 @@ Read git history and .ci/ files. Extract all requirements and architectural cons
## Step 3: Write ROADMAP.md
Write `.ci/ROADMAP.md` using CiFiles.writeRoadmapMd():
Write `.ciagent/ROADMAP.md` using CiFiles.writeRoadmapMd():
- Overview
- Phase list with status, dependencies, requirements, success criteria
- Phase details section
+9 -9
View File
@@ -1,5 +1,5 @@
---
description: Verifies threat mitigation coverage for a CI phase — reads plan threat data, analyzes codebase for security concerns, classifies threats. Auto-dispositions: low=accept, medium=mitigate, high=escalate. Read-only — does not modify source code.
description: Verifies threat mitigation coverage for a CIAgent phase — reads plan threat data, analyzes codebase for security concerns, classifies threats. Auto-dispositions: low=accept, medium=mitigate, high=escalate. Read-only — does not modify source code.
color: "#FF0000"
tools:
read: true
@@ -9,9 +9,9 @@ tools:
---
<role>
You are a CI security auditor. You verify that security threats identified during planning have been properly mitigated in the implementation.
You are a CIAgent security auditor. You verify that security threats identified during planning have been properly mitigated in the implementation.
CI security auditors auto-disposition threats: low=accept, medium=mitigate, high=escalate. Only high-severity threats with no clear mitigation are escalated to human.
CIAgent security auditors auto-disposition threats: low=accept, medium=mitigate, high=escalate. Only high-severity threats with no clear mitigation are escalated to human.
You are READ-ONLY. Do not modify source code.
@@ -20,11 +20,11 @@ If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before auditing, load context from git first:
@@ -32,15 +32,15 @@ Before auditing, load context from git first:
1. Run `git log --grep="security" --max-count=20` for prior security decisions
2. Use GitContext.getDecisions(currentPhase) for phase decisions
3. Use GitContext.getEscalations() for pending security escalations
4. Read `.ci/config.json` for security enforcement settings
5. Read `.ci/ARCHITECTURE.md` for trust boundaries
4. Read `.ciagent/config.json` for security enforcement settings
5. Read `.ciagent/ARCHITECTURE.md` for trust boundaries
</project_context>
<execution_flow>
## Step 1: Load Context
Read git security history and .ci/ files. Extract trust boundaries and prior threat classifications.
Read git security history and .ciagent/ files. Extract trust boundaries and prior threat classifications.
## Step 2: STRIDE Analysis
+6 -6
View File
@@ -1,5 +1,5 @@
---
description: Analyzes a recently solved CI problem and produces a structured compound learning document. Compound learnings are committed as ---ci--- blocks, not separate files.
description: Analyzes a recently solved CIAgent problem and produces a structured compound learning document. Compound learnings are committed as ---ci--- blocks, not separate files.
color: "#9370DB"
tools:
read: true
@@ -10,7 +10,7 @@ tools:
---
<role>
You are a CI solution writer. You analyze recently solved problems and produce structured compound learning documents. Compound learnings are committed as `---ci---` blocks, not separate files.
You are a CIAgent solution writer. You analyze recently solved problems and produce structured compound learning documents. Compound learnings are committed as `---ci---` blocks, not separate files.
You use git history to understand the problem context and trace the solution path.
@@ -19,11 +19,11 @@ If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before analyzing, load context from git first:
@@ -31,7 +31,7 @@ Before analyzing, load context from git first:
1. Run `git log --max-count=20` for recent problem-solving history
2. Use GitContext.getLessons() for lessons learned
3. Use GitContext.getCompounds() for existing compound learnings (avoid duplicates)
4. Read `.ci/ARCHITECTURE.md` for component context
4. Read `.ciagent/ARCHITECTURE.md` for component context
</project_context>
<execution_flow>
+8 -8
View File
@@ -1,5 +1,5 @@
---
description: Verifies that a CI phase goal was actually achieved after execution — checks must_haves, requirement coverage, and integration links. Never produces human_needed unless truly unverifiable. Generates automated test scripts for unverifiable items.
description: Verifies that a CIAgent phase goal was actually achieved after execution — checks must_haves, requirement coverage, and integration links. Never produces human_needed unless truly unverifiable. Generates automated test scripts for unverifiable items.
color: "#800080"
tools:
read: true
@@ -9,20 +9,20 @@ tools:
---
<role>
You are a CI verifier. You verify that a phase was completed correctly — not just that code was written, but that the phase goal is genuinely achieved.
You are a CIAgent verifier. You verify that a phase was completed correctly — not just that code was written, but that the phase goal is genuinely achieved.
CI verifiers NEVER produce `human_needed` unless something is truly unverifiable. Generate automated test scripts for traditionally human-verified items.
CIAgent verifiers NEVER produce `human_needed` unless something is truly unverifiable. Generate automated test scripts for traditionally human-verified items.
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
</role>
<project_context>
If .ci/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ci/config.json
If .ciagent/config.json has projects[] with length > 0, you are in multi-project mode.
- Read active_project from .ciagent/config.json
- All commits must include `project: <active_project>` in ---ci--- block
- Branch names are prefixed with <slug>/ in multi-project mode
- .ci/ files are in .ci/<slug>/ subdirectories
- .ciagent/ files are in .ciagent/<slug>/ subdirectories
If single-project mode (projects[] empty or absent), use existing conventions.
Before verifying, load context from git first:
@@ -30,8 +30,8 @@ Before verifying, load context from git first:
1. Run `git log --grep="P##" --max-count=50` for all phase commits
2. Use GitContext.reconstructState() for current project state
3. Use GitContext.getRequirementsCoverage() for covered/partial requirements
4. Read `.ci/ROADMAP.md` for phase goal and success criteria
5. Read `.ci/REQUIREMENTS.md` for requirement IDs
4. Read `.ciagent/ROADMAP.md` for phase goal and success criteria
5. Read `.ciagent/REQUIREMENTS.md` for requirement IDs
6. Use GitContext.getCommitsForPhase(currentPhase) for phase commit history
</project_context>
+4 -4
View File
@@ -1,15 +1,15 @@
<dev_context>
Agent output guidance for CI dev mode. Loaded when the orchestrator operates in default (dev) mode.
Agent output guidance for CIAgent dev mode. Loaded when the orchestrator operates in default (dev) mode.
---
## Multi-Project and NFR Versioning
When in multi-project mode (`.ci/config.json` has `projects[]` with length > 0):
When in multi-project mode (`.ciagent/config.json` has `projects[]` with length > 0):
- All commits include `project: <slug>` in `---ci---` block
- Branch names are prefixed with `<slug>/`
- `.ci/` files are in `.ci/<slug>/` subdirectories
- `.ciagent/` files are in `.ciagent/<slug>/` subdirectories
- Project scoping applies to all operations
NFR milestone versioning:
@@ -27,7 +27,7 @@ NFR milestone versioning:
- Working code that compiles and passes tests
- Minimal diff — change only what is necessary
- Commit with `---ci---` blocks for all CI-generated work
- Commit with `---ci---` blocks for all CIAgent-generated work
- Flag side effects or breaking changes immediately
- Surface the next actionable step at the end of every response
+3 -3
View File
@@ -1,6 +1,6 @@
<research_context>
Agent output guidance for CI research mode. Loaded when the orchestrator operates in research mode.
Agent output guidance for CIAgent research mode. Loaded when the orchestrator operates in research mode.
---
@@ -21,12 +21,12 @@ Agent output guidance for CI research mode. Loaded when the orchestrator operate
## Research Output
Research is intermediate work product — conclusions update `.ci/<slug>/` static files (ARCHITECTURE.md, PROJECT.md) and contain:
Research is intermediate work product — conclusions update `.ciagent/<slug>/` static files (ARCHITECTURE.md, PROJECT.md) and contain:
- Key findings in the commit body
- Decisions in the `---ci---` block
- Confidence levels for each recommendation
In multi-project mode, research conclusions update files in `.ci/<slug>/` subdirectories, not the root `.ci/` directory.
In multi-project mode, research conclusions update files in `.ciagent/<slug>/` subdirectories, not the root `.ciagent/` directory.
## Verbosity
+2 -2
View File
@@ -1,12 +1,12 @@
<review_context>
Agent output guidance for CI review mode. Loaded when the orchestrator operates in review mode.
Agent output guidance for CIAgent review mode. Loaded when the orchestrator operates in review mode.
---
## Multi-Project Awareness
When in multi-project mode (`.ci/config.json` has `projects[]` with length > 0):
When in multi-project mode (`.ciagent/config.json` has `projects[]` with length > 0):
- All reviews are scoped to the active project
- Commits include `project: <slug>` in `---ci---` blocks
- Branch names are prefixed with `<slug>/`
+108 -49
View File
@@ -1,6 +1,6 @@
<branch_strategy>
Canonical branch naming and lifecycle conventions for CI. Branches encode project structure — merged branches indicate completed work, active branches indicate work in progress.
Canonical branch naming and lifecycle conventions for CIAgent. Branches encode project structure — merged branches indicate completed work, active branches indicate work in progress.
---
@@ -23,7 +23,7 @@ Canonical branch naming and lifecycle conventions for CI. Branches encode projec
**Lifecycle:**
1. Created at phase start by `GitBranch.createPhaseBranch()`
2. All task commits for the phase land on this branch
3. Merged to main (squash) on phase completion
3. Merged to their milestone branch (or main if no milestone branch) on phase completion
4. Merged = phase complete, active = phase in progress, absent = not started
### Milestone Branches
@@ -42,14 +42,33 @@ Canonical branch naming and lifecycle conventions for CI. Branches encode projec
**Lifecycle:**
1. Created at first phase of milestone by `GitBranch.createMilestoneBranch()`
2. Spans multiple phases within the same milestone
3. Merged to main on milestone completion
4. Merged = milestone complete, active = milestone in progress
3. All phase branches merge into this branch on completion
4. Merged to main on milestone completion
5. Merged = milestone complete, active = milestone in progress
### Hotfix Branches
**Format:** `hotfix/description`
Created for urgent fixes outside the normal phase flow. Merged directly to main.
Created for urgent fixes outside the normal phase flow. Merged directly to main (exception to hierarchy).
## Branch Hierarchy (Enforced)
```text
main ─── milestone/vX.X-slug ─── phase/NN-slug
Rules:
- Phase branches MUST merge into their milestone branch first
- Milestone branches merge into main only after all phase branches are merged
- If no milestone branch exists, phases may merge directly to main
- Hotfix branches merge directly to main (exception)
```
**Validation** is enforced in `GitBranch.mergePhaseBranch()` and `createShipCommand()`:
- Phase → main: rejected if milestone branch exists for this milestone
- Phase → milestone: allowed
- Milestone → main: allowed only after all phase branches are merged
- Hotfix → main: allowed
## Branch Status Inference
@@ -69,71 +88,101 @@ const branches = gitContext.getBranches();
## Merge Strategy
Default: **squash merge** into main.
Default: **squash merge**.
Phase branches squash-merge into their milestone branch. Milestone branches squash-merge into main. This keeps main clean while preserving full development history in the phase branch.
```typescript
gitBranch.mergePhaseBranch("phase/01-git-native-architecture", "main", true);
// Phase → milestone (enforced when milestone branch exists)
gitBranch.mergePhaseBranch("phase/01-git-native-architecture", "milestone/v0.5-honest-baseline", true);
// Milestone → main (after all phases merged)
gitBranch.mergeMilestoneBranch("milestone/v0.5-honest-baseline", "main", true);
```
Squash merge keeps main clean while preserving full development history in the phase branch. Phase branches can be deleted after merge if desired.
Phase branches can be deleted after merge if desired.
## Versioning and Releases
**Every merge to main creates a release. No exceptions.** Versioning maps to project structure:
**Every merge to main creates a release. No exceptions.** Versioning follows a 3-tier model based on milestone type:
| Version Part | When | Example |
|-------------|------|---------|
| **Major** (X.0.0) | Project-level refactor or schema change | `v1.0.0` |
| **Minor** (0.X.0) | Every milestone completion | `v0.3.0` |
| **Patch** (0.0.X) | Every phase completion | `v0.2.3` |
### 3-Tier Versioning Model
### Phase completion (patch release)
| Milestone Type | Condition | Phase release | Milestone release |
|---------------|-----------|---------------|-------------------|
| **NFR** | All phases: fix/chore/docs/perf/refactor/test | Patch (`vX.Y.Z`) | None |
| **Feature** | Any phase is `feat`, no schema break | Patch (`vX.Y.Z`) | Minor — `vX.(Y+1).0` |
| **Schema-breaking** | Refactor/schema break/new direction | Minor — `vX.(Y+N).0` per phase | Major — `v(X+1).0.0` |
**IMPORTANT:** Milestone tags are always the NEXT version, never the base:
- Feature: patches v0.5.1v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
- Schema-breaking: minors v0.3.0, v0.4.0, v0.5.0 → milestone tag is v1.0.0
- NFR: no milestone tag — the milestone is implicit from the patch sequence
Determine milestone type via `getMilestoneType()` which returns `"nfr" | "feature" | "schema-breaking"`.
### Phase completion
**NFR/Feature (patch release):**
```bash
git checkout main
git merge --squash phase/01-git-native-architecture
git commit -m "docs(P01): complete git-native-architecture phase"
git tag -a v0.2.1 -m "v0.2.1: git-native-architecture"
git checkout milestone/v0.5-honest-baseline # or main if no milestone branch
git merge --squash phase/01-quick-wins
git commit -m "docs(P01): complete quick-wins phase"
git tag -a v0.5.1 -m "v0.5.1: quick-wins"
git push origin main --tags
# Create Gitea release for v0.2.1
# Create Gitea release for v0.5.1
```
Phase number within the milestone determines the patch version (1st phase = .1, 2nd phase = .2, etc.)
### Milestone completion (minor release)
**Schema-breaking (minor release per phase):**
```bash
git checkout main
git merge --squash milestone/v0.2-git-native
git commit -m "docs(milestone): complete git-native"
git tag -a v0.2.0 -m "v0.2.0: git-native"
git checkout milestone/v0.5-schema-rewrite
git merge --squash phase/01-core-refactor
git commit -m "docs(P01): complete core-refactor phase"
git tag -a v0.5.0 -m "v0.5.0: core-refactor"
git push origin main --tags
# Create Gitea release for v0.2.0 with full milestone summary
# Create Gitea release for v0.5.0
```
### Major release
Each schema-breaking phase bumps the minor. 1st phase = next available minor, 2nd = minor+1, etc.
When the project undergoes a schema-breaking change (e.g., switching from file-based to git-native architecture), bump the major version. Major releases follow the same merge → tag → release flow.
### Milestone completion
## NFR Milestone Versioning
**Feature (minor release):**
```bash
# All phases already merged into milestone branch
git checkout main
git merge --squash milestone/v0.5-honest-baseline
git commit -m "docs(milestone): complete honest-baseline"
git tag -a v0.6.0 -m "v0.6.0: honest-baseline" # NEXT minor, NOT v0.5.0
git push origin main --tags
# Create Gitea release for v0.6.0 with full milestone summary
```
NFR milestones and feature milestones follow different versioning rules:
**Schema-breaking (major release):**
```bash
# All phases already merged into milestone branch
git checkout main
git merge --squash milestone/v0.5-schema-rewrite
git commit -m "docs(milestone): complete schema-rewrite"
git tag -a v1.0.0 -m "v1.0.0: schema-rewrite" # NEXT major
git push origin main --tags
# Create Gitea release for v1.0.0 with full milestone summary
```
**NFR milestones** — all phases are `fix`, `chore`, `docs`, `perf`, `refactor`, or `test`:
- Each phase gets a progressive patch version (v0.1.1, v0.1.2, v0.1.3)
- No separate milestone tag — the milestone is implicit from the patch sequence
- Example: milestone v0.1 with phases P01 (chore), P02 (test), P03 (perf) → v0.1.1, v0.1.2, v0.1.3
**NFR milestones produce no milestone tag.** The last phase's patch version is the final release.
**Feature milestones** — any phase is `feat`:
- Each phase gets a progressive patch version
- On milestone completion, tag a minor version (e.g., v0.2.0)
- Example: milestone v0.2 with phases P01 (feat), P02 (feat), P03 (fix) → v0.2.1, v0.2.2, v0.2.3 + milestone tag v0.3.0
### Version Validation
Determine milestone type by checking `isNfrMilestone()` which inspects all phase commit types within the milestone.
Before creating any tag:
1. Tag must be strictly greater than all existing tags on the same major.minor line
2. Milestone completion tag must be next minor (feature) or next major (schema-breaking)
3. NEVER create a tag that is semantically below existing phase tags
## Multi-Project Branch Naming
When operating in multi-project mode (`.ci/config.json` has `projects[]` with length > 0):
When operating in multi-project mode (`.ciagent/config.json` has `projects[]` with length > 0):
| Branch Type | Format | Example |
|-------------|--------|---------|
@@ -155,7 +204,7 @@ const milestones = gitBranch.listMilestones();
## Branch Creation Rules
1. Always create phase branches from main (or the current milestone branch)
1. Always create phase branches from the current milestone branch (or main if no milestone branch exists)
2. Never create a branch for a completed phase — it should already be merged
3. Milestone branches span phases — don't create one per phase
4. Use `GitBranch.createPhaseBranch()` to ensure consistent naming
@@ -164,25 +213,35 @@ const milestones = gitBranch.listMilestones();
## Working with Phase Branches
```bash
# Create a phase branch
git checkout -b phase/01-git-native-architecture
# Create a milestone branch first
git checkout main
git checkout -b milestone/v0.5-honest-baseline
# Create a phase branch from the milestone
git checkout -b phase/01-quick-wins
# Commit work with ---ci--- blocks
git commit -m "feat(P01-01-01): implement commit parser
---ci---
phase: 1
milestone: v0.2
milestone: v0.5
plan: 01-01
task: 01-01-01
status: execute
---/ci---"
# Merge on completion
# Merge phase into milestone on completion
git checkout milestone/v0.5-honest-baseline
git merge --squash phase/01-quick-wins
git commit -m "docs(P01): complete quick-wins phase"
git tag -a v0.5.1 -m "v0.5.1: quick-wins"
# After all phases, merge milestone into main
git checkout main
git merge --squash phase/01-git-native-architecture
git commit -m "docs(P01): complete git-native-architecture phase"
git tag -a v0.2.1 -m "v0.2.1: git-native-architecture"
git merge --squash milestone/v0.5-honest-baseline
git commit -m "docs(milestone): complete honest-baseline"
git tag -a v0.6.0 -m "v0.6.0: honest-baseline"
git push origin main --tags
```
+21 -21
View File
@@ -1,15 +1,15 @@
<ci_files_discipline>
How CI manages the `.ci/` directory — long-lived reference documents only. Dynamic state lives in the git log via `---ci---` YAML blocks, not in files.
How CIAgent manages the `.ciagent/` directory — long-lived reference documents only. Dynamic state lives in the git log via `---ci---` YAML blocks, not in files.
---
## Multi-Project Directory Structure
In multi-project mode, `.ci/` uses subdirectories per project:
In multi-project mode, `.ciagent/` uses subdirectories per project:
```
.ci/
.ciagent/
config.json # Registry with projects[] and active_project
<project-slug>/
PROJECT.md
@@ -18,11 +18,11 @@ In multi-project mode, `.ci/` uses subdirectories per project:
REQUIREMENTS.md
```
`.ci/config.json` serves as the registry with `projects[]` (array of project entries) and `active_project` (slug of the currently active project).
`.ciagent/config.json` serves as the registry with `projects[]` (array of project entries) and `active_project` (slug of the currently active project).
**Backward compatibility:** if `.ci/` has flat files (PROJECT.md, ARCHITECTURE.md, etc.) and no project subdirectories, auto-migrate by creating `<default-slug>/` and moving files into it, then updating `config.json` with a single `projects[]` entry.
**Backward compatibility:** if `.ciagent/` has flat files (PROJECT.md, ARCHITECTURE.md, etc.) and no project subdirectories, auto-migrate by creating `<default-slug>/` and moving files into it, then updating `config.json` with a single `projects[]` entry.
## What Lives in `.ci/`
## What Lives in `.ciagent/`
| File | Purpose | Update Frequency |
|------|---------|-------------------|
@@ -32,23 +32,23 @@ In multi-project mode, `.ci/` uses subdirectories per project:
| `<slug>/ROADMAP.md` | Phase breakdown, milestone mapping, success criteria per project | Low (phase transitions) |
| `<slug>/REQUIREMENTS.md` | v1/v2 requirements with REQ-IDs, out of scope, traceability per project | Low (requirement changes) |
## What Does NOT Live in `.ci/`
## What Does NOT Live in `.ciagent/`
These were removed in v0.2.0 and now live in the git log:
| Previous Location | Now In | Access Method |
|-------------------|--------|---------------|
| `.ci/audit/decisions.json` | `---ci---` decisions block | `GitContext.getDecisions()` |
| `.ci/audit/escalations.json` | `---ci---` escalations block | `GitContext.getEscalations()` |
| `.ci/audit/lessons.json` | `---ci---` lessons block | `GitContext.getLessons()` |
| `.ciagent/audit/decisions.json` | `---ci---` decisions block | `GitContext.getDecisions()` |
| `.ciagent/audit/escalations.json` | `---ci---` escalations block | `GitContext.getEscalations()` |
| `.ciagent/audit/lessons.json` | `---ci---` lessons block | `GitContext.getLessons()` |
| `.planning/` directory (removed) | Git log + branches | `GitContext.reconstructState()` |
## CiFiles API
| Method | Returns | Purpose |
|--------|---------|---------|
| `ensureCIDir()` | void | Create `.ci/` if it doesn't exist |
| `isInitialized()` | boolean | Check if `.ci/config.json` exists |
| `ensureCIDir()` | void | Create `.ciagent/` if it doesn't exist |
| `isInitialized()` | boolean | Check if `.ciagent/config.json` exists |
| `readProjectMd()` | ProjectMd \| null | Read project definition |
| `writeProjectMd(project, reason)` | void | Write project definition |
| `readRoadmapMd()` | RoadmapMd \| null | Read roadmap |
@@ -66,7 +66,7 @@ These were removed in v0.2.0 and now live in the git log:
2. **Phase boundaries** — Major updates happen at phase transitions, not during task execution.
3. **Requirements status** — Use `updateRequirementStatus()` for single-status changes, not full rewrites.
4. **Phase status** — Use `updatePhaseStatus()` for phase transitions, not full roadmap rewrites.
5. **Commit after write** — Every `.ci/` file change should be committed immediately with a `---ci---` block.
5. **Commit after write** — Every `.ciagent/` file change should be committed immediately with a `---ci---` block.
## Update Triggers
@@ -157,21 +157,21 @@ interface ArchitectureMd {
## Research and .ci/ File Updates
Research is intermediate work product. Conclusions from research update `.ci/` static files:
Research is intermediate work product. Conclusions from research update `.ciagent/` static files:
- Key findings go in the commit body
- Decisions go in `---ci---` blocks
- Conclusions that change project structure update the appropriate `.ci/<slug>/` files (ARCHITECTURE.md, PROJECT.md, etc.)
- Conclusions that change project structure update the appropriate `.ciagent/<slug>/` files (ARCHITECTURE.md, PROJECT.md, etc.)
Research commits are not final artifacts — they feed into planning and roadmap updates.
## Anti-Patterns
- Never write dynamic state (decisions, escalations, lessons) to `.ci/` files
- Never update `.ci/` files during task execution — update at phase boundaries
- Never write dynamic state (decisions, escalations, lessons) to `.ciagent/` files
- Never update `.ciagent/` files during task execution — update at phase boundaries
- Never skip the `reason` parameter when writing PROJECT.md
- Never commit `.ci/` changes without a `---ci---` block
- Never create new files in `.ci/` without updating this reference document
- Never store counters, timestamps, or session state in `.ci/` files
- Never store research conclusions only in commits — update `.ci/<slug>/` static files with findings
- Never commit `.ciagent/` changes without a `---ci---` block
- Never create new files in `.ciagent/` without updating this reference document
- Never store counters, timestamps, or session state in `.ciagent/` files
- Never store research conclusions only in commits — update `.ciagent/<slug>/` static files with findings
</ci_files_discipline>
+5 -5
View File
@@ -1,6 +1,6 @@
<commit_schema>
Canonical `---ci---` YAML block schema for CI commits. Every CI-generated commit contains a structured YAML block that enables full project state reconstruction from the git log alone.
Canonical `---ci---` YAML block schema for CIAgent commits. Every CIAgent-generated commit contains a structured YAML block that enables full project state reconstruction from the git log alone.
---
@@ -39,7 +39,7 @@ compound: # optional
---/ci---
```
The `project` field is required when in multi-project mode (`.ci/config.json` has `projects[]` with length > 0). In single-project mode, it is optional.
The `project` field is required when in multi-project mode (`.ciagent/config.json` has `projects[]` with length > 0). In single-project mode, it is optional.
Example with project field:
@@ -104,7 +104,7 @@ The `CommitBuilder` class provides typed constructors:
## Reconstruction Guarantee
An agent with access to only commit messages (no code, no diffs, no .ci/ files) can reconstruct:
An agent with access to only commit messages (no code, no diffs, no .ciagent/ files) can reconstruct:
1. **Current phase and milestone** — from the latest commit's `phase` and `milestone` fields
2. **Pipeline stage** — from the latest commit's `status` field
@@ -117,8 +117,8 @@ An agent with access to only commit messages (no code, no diffs, no .ci/ files)
## Anti-Patterns
- Never put CI metadata in code comments — it belongs in commit messages
- Never omit the `---ci---` block from a CI-generated commit
- Never put CIAgent metadata in code comments — it belongs in commit messages
- Never omit the `---ci---` block from a CIAgent-generated commit
- Never store decisions, escalations, or lessons in files — commit them
- Never use a non-standard commit type — use the 14 types above
- Never put freeform text inside the YAML block — use the structured fields
+2 -2
View File
@@ -1,6 +1,6 @@
<decision_engine>
How CI makes decisions and commits them as git artifacts. The DecisionEngine uses bounded rationality with confidence thresholds to auto-decide or escalate.
How CIAgent makes decisions and commits them as git artifacts. The DecisionEngine uses bounded rationality with confidence thresholds to auto-decide or escalate.
---
@@ -99,7 +99,7 @@ Decisions can be project-scoped via the `project` field in `---ci---` blocks. Wh
## Anti-Patterns
- Never write decisions to a `.ci/audit/` file — commit them
- Never write decisions to a `.ciagent/audit/` file — commit them
- Never skip recording a decision, even high-confidence ones
- Never make a decision without listing alternatives
- Never override the confidence threshold without explicit configuration
@@ -1,6 +1,6 @@
<git_context_loading>
How CI agents load project context. The git log IS the project memory — a CI agent's first impulse to gather context is `git log` + `git branch`, not file reads.
How CIAgent agents load project context. The git log IS the project memory — a CIAgent agent's first impulse to gather context is `git log` + `git branch`, not file reads.
---
@@ -8,7 +8,7 @@ How CI agents load project context. The git log IS the project memory — a CI a
**Read the log first, files second.**
The git log contains every decision, escalation, lesson, and compound learning through structured `---ci---` YAML blocks. Files in `.ci/` are long-lived reference documents (PROJECT.md, ARCHITECTURE.md, ROADMAP.md, REQUIREMENTS.md, config.json) that change infrequently.
The git log contains every decision, escalation, lesson, and compound learning through structured `---ci---` YAML blocks. Files in `.ciagent/` are long-lived reference documents (PROJECT.md, ARCHITECTURE.md, ROADMAP.md, REQUIREMENTS.md, config.json) that change infrequently.
## Context Loading Sequence
@@ -19,7 +19,7 @@ The git log contains every decision, escalation, lesson, and compound learning t
5. **Requirements coverage**`GitContext.getRequirementsCoverage()` for covered/partial
6. **Lessons scan**`GitContext.getLessons()` for all learned lessons
7. **Compound learnings**`GitContext.getCompounds()` for cross-phase patterns
8. **File reads** — Only now read `.ci/` files (PROJECT.md, ARCHITECTURE.md, etc.)
8. **File reads** — Only now read `.ciagent/` files (PROJECT.md, ARCHITECTURE.md, etc.)
## GitContext API
@@ -74,19 +74,19 @@ interface ParsedCiCommit {
}
```
Commits without `---ci---` blocks have `ci: null` — these are treated as non-CI commits (e.g., manual edits by the developer).
Commits without `---ci---` blocks have `ci: null` — these are treated as non-CIAgent commits (e.g., manual edits by the developer).
## Phase Context Reset
Between phases, all state is committed to git, then the next phase starts with fresh context from git log — not accumulated conversation history.
**On opencode (subagent support):** spawn a fresh agent for the next phase. The new agent loads context from git log and `.ci/` files only.
**On opencode (subagent support):** spawn a fresh agent for the next phase. The new agent loads context from git log and `.ciagent/` files only.
**On platforms without subagents:** simulated reset — re-read git context from scratch, ignore prior conversation history. Treat the phase boundary as a hard context boundary.
**Checkpoint sequence:**
1. Commit all work from the current phase
2. Update `.ci/` files (ROADMAP.md phase status, REQUIREMENTS.md requirement statuses)
2. Update `.ciagent/` files (ROADMAP.md phase status, REQUIREMENTS.md requirement statuses)
3. Verify `GitContext.reconstructState()` matches expected state
4. Reset context — next phase begins fresh
@@ -112,11 +112,11 @@ When context is limited:
2. `getDecisions(currentPhase)` — current phase decisions only
3. `getRequirementsCoverage()` — aggregate view
4. Skip lessons/compounds unless specifically needed
5. Read `.ci/ROADMAP.md` instead of scanning all phase branches
5. Read `.ciagent/ROADMAP.md` instead of scanning all phase branches
## What NOT to Do
- Never read `.ci/` files before checking the git log
- Never read `.ciagent/` files before checking the git log
- Never parse commit messages manually — use `CommitParser.parseCommitMessage()`
- Never assume the latest commit reflects the current state — check branches
- Never reconstruct state from files when git data is available
+17 -17
View File
@@ -1,18 +1,18 @@
---
description: Audit CI project health — reconstruct state from git log, verify .ci/ files match codebase, check for stale references
description: Audit CIAgent project health — reconstruct state from git log, verify .ciagent/ files match codebase, check for stale references
---
# CI Audit
# CIAgent Audit
Audit the CI project for health issues. Verifies that git log state matches .ci/ files and that the project can be fully reconstructed from commit messages alone.
Audit the CIAgent project for health issues. Verifies that git log state matches .ciagent/ files and that the project can be fully reconstructed from commit messages alone.
**Usage:** `ci-audit`
**Usage:** `ciagent-audit`
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ci/config.json` to determine if multi-project mode is active.
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
If `.ci/config.json` has `projects[]` with length > 0:
If `.ciagent/config.json` has `projects[]` with length > 0:
- Confirm `active_project` is correct for this audit
- If not, set it with `ci setActiveProject(<slug>)`
- Scope audit queries to the active project
@@ -26,16 +26,16 @@ Attempt to reconstruct the full project state from commit messages only:
1. Parse all `---ci---` blocks from git log
2. Reconstruct: current phase, milestone, stage, decisions, escalations, requirements, lessons, compounds
3. Compare reconstructed state with `.ci/` file contents
3. Compare reconstructed state with `.ciagent/` file contents
## Step 2: Check .ci/ File Discipline
For each .ci/ file:
- `.ci/config.json`: valid JSON, required fields present
- `.ci/PROJECT.md`: has required sections (What This Is, Requirements, Constraints, Key Decisions)
- `.ci/ROADMAP.md`: phases match git branches (merged = complete, active = in progress)
- `.ci/REQUIREMENTS.md`: traceability matrix is complete
- `.ci/ARCHITECTURE.md`: components match actual code structure
For each .ciagent/ file:
- `.ciagent/config.json`: valid JSON, required fields present
- `.ciagent/PROJECT.md`: has required sections (What This Is, Requirements, Constraints, Key Decisions)
- `.ciagent/ROADMAP.md`: phases match git branches (merged = complete, active = in progress)
- `.ciagent/REQUIREMENTS.md`: traceability matrix is complete
- `.ciagent/ARCHITECTURE.md`: components match actual code structure
## Step 3: Check Branch Hygiene
@@ -46,20 +46,20 @@ For each .ci/ file:
## Step 4: Check Commit Discipline
- Every CI-generated commit should have a `---ci---` block
- No stale decisions (decisions from >50 commits ago that are still in `.ci/` but not reflected in code)
- No stale decisions (decisions from >50 commits ago that are still in `.ciagent/` but not reflected in code)
- No unresolved escalations older than the escalation timeout
## Step 5: Display Report
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CI ► AUDIT REPORT
CIAgent ► AUDIT REPORT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Reconstruction: [PASS/FAIL] — [details]
.ci/ Files: [N] checked, [issues]
.ciagent/ Files: [N] checked, [issues]
Branches: [N] phase, [N] milestone, [issues]
Commits: [N] CI commits, [N] without ---ci--- blocks
Commits: [N] CIAgent commits, [N] without ---ci--- blocks
[If issues found:]
Issues:
+9 -9
View File
@@ -1,18 +1,18 @@
---
description: Clarify CI project ambiguities — generate questions, accept defaults at full autonomy, present at supervised/guided
description: Clarify CIAgent project ambiguities — generate questions, accept defaults at full autonomy, present at supervised/guided
---
# CI Clarify
# CIAgent Clarify
Run the clarification phase for the current CI project. Generate questions about ambiguities, accept defaults automatically at full autonomy, or present to the user at supervised/guided levels.
Run the clarification phase for the current CIAgent project. Generate questions about ambiguities, accept defaults automatically at full autonomy, or present to the user at supervised/guided levels.
**Usage:** `ci-clarify [phase_number]`
**Usage:** `ciagent-clarify [phase_number]`
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ci/config.json` to determine if multi-project mode is active.
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
If `.ci/config.json` has `projects[]` with length > 0:
If `.ciagent/config.json` has `projects[]` with length > 0:
- Confirm `active_project` is correct for this clarification
- If not, set it with `ci setActiveProject(<slug>)`
- All commit messages must include `project: <slug>` in `---ci---` block
@@ -26,7 +26,7 @@ git log --max-count=20
git branch -a
```
Read `.ci/PROJECT.md` and `.ci/REQUIREMENTS.md` for the specification.
Read `.ciagent/PROJECT.md` and `.ciagent/REQUIREMENTS.md` for the specification.
## Step 2: Identify Ambiguities
@@ -75,8 +75,8 @@ decisions:
## Step 6: Update .ci/ Files
Update `.ci/PROJECT.md` with clarified requirements.
Update `.ci/REQUIREMENTS.md` with refined requirements.
Update `.ciagent/PROJECT.md` with clarified requirements.
Update `.ciagent/REQUIREMENTS.md` with refined requirements.
## Step 7: Report
+5 -5
View File
@@ -1,18 +1,18 @@
---
description: Systematic CI debugging with git context — triage, diagnose root cause, auto-fix or escalate
description: Systematic CIAgent debugging with git context — triage, diagnose root cause, auto-fix or escalate
---
# CI Debug
# CIAgent Debug
Systematic debugging workflow: triage → root cause diagnosis → auto-fix or escalate. Uses git history to find recent changes that may have caused the bug.
**Usage:** `ci-debug [description]`
**Usage:** `ciagent-debug [description]`
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ci/config.json` to determine if multi-project mode is active.
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
If `.ci/config.json` has `projects[]` with length > 0:
If `.ciagent/config.json` has `projects[]` with length > 0:
- Confirm `active_project` is correct for this debug session
- If not, set it with `ci setActiveProject(<slug>)`
- Scope debugging to the active project
+18 -18
View File
@@ -1,21 +1,21 @@
---
description: Initialize a new CI project — specification → clarify → create .ci/ reference files → initial commit
description: Initialize a new CIAgent project — specification → clarify → create .ciagent/ reference files → initial commit
---
# CI Init
# CIAgent Init
Initialize a new CI project with specification parsing, clarification, and .ci/ reference file creation.
Initialize a new CIAgent project with specification parsing, clarification, and .ciagent/ reference file creation.
**Usage:** `ci-init [description]`
**Usage:** `ciagent-init [description]`
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ci/config.json` to determine if multi-project mode is active.
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
If `.ci/config.json` has `projects[]` with length > 0:
If `.ciagent/config.json` has `projects[]` with length > 0:
- Confirm `active_project` is correct for this initialization
- If not, set it with `ci setActiveProject(<slug>)`
- All subsequent operations use `.ci/<slug>/` subdirectories
- All subsequent operations use `.ciagent/<slug>/` subdirectories
- All commit messages must include `project: <slug>` in `---ci---` block
If single-project mode: proceed with existing conventions.
@@ -29,12 +29,12 @@ Verify git is initialized:
If NO_GIT: `git init`
Check if `.ci/config.json` already exists:
Check if `.ciagent/config.json` already exists:
```bash
[ -f .ci/config.json ] && echo "ALREADY_INITIALIZED" || echo "NEW"
[ -f .ciagent/config.json ] && echo "ALREADY_INITIALIZED" || echo "NEW"
```
If ALREADY_INITIALIZED: stop. Use `ci-status` to see project state.
If ALREADY_INITIALIZED: stop. Use `ciagent-status` to see project state.
## Step 2: Parse Specification
@@ -59,15 +59,15 @@ Analyze the specification for ambiguities. For each ambiguity:
Record decisions in the `---ci---` block of the init commit.
## Step 4: Create .ci/ Files
## Step 4: Create .ciagent/ Files
Use CiFiles to create the project structure:
1. `.ci/config.json` — registry with `projects[]` and `active_project`
2. `.ci/<slug>/PROJECT.md` — vision, requirements, constraints, key decisions (or `.ci/PROJECT.md` in single-project mode)
3. `.ci/<slug>/ARCHITECTURE.md` — system architecture (initial, may be incomplete)
4. `.ci/<slug>/ROADMAP.md` — phase breakdown (to be refined by roadmapper)
5. `.ci/<slug>/REQUIREMENTS.md` — formal requirements with REQ-IDs
1. `.ciagent/config.json` — registry with `projects[]` and `active_project`
2. `.ciagent/<slug>/PROJECT.md` — vision, requirements, constraints, key decisions (or `.ciagent/PROJECT.md` in single-project mode)
3. `.ciagent/<slug>/ARCHITECTURE.md` — system architecture (initial, may be incomplete)
4. `.ciagent/<slug>/ROADMAP.md` — phase breakdown (to be refined by roadmapper)
5. `.ciagent/<slug>/REQUIREMENTS.md` — formal requirements with REQ-IDs
`initCI()` accepts `projectSlug` and `projectName` parameters for multi-project initialization.
@@ -105,6 +105,6 @@ Include `project: <slug>` in the `---ci---` block when in multi-project mode.
## Step 7: Done
Report project initialized, .ci/ files created, initial branch created.
Report project initialized, .ciagent/ files created, initial branch created.
Next: `ci-run` to execute the pipeline, or `ci-quick` for ad-hoc tasks.
Next: `ciagent-run` to execute the pipeline, or `ciagent-quick` for ad-hoc tasks.
+7 -7
View File
@@ -1,12 +1,12 @@
---
description: Execute an ad-hoc CI task with full agentic guarantees — git context, ---ci--- commits, optional research and verification
description: Execute an ad-hoc CIAgent task with full agentic guarantees — git context, ---ci--- commits, optional research and verification
---
# CI Quick
# CIAgent Quick
Execute small, ad-hoc tasks with CI guarantees: git context loading, `---ci---` commit blocks, optional research and verification.
Execute small, ad-hoc tasks with CIAgent guarantees: git context loading, `---ci---` commit blocks, optional research and verification.
**Usage:** `ci-quick [description]`
**Usage:** `ciagent-quick [description]`
**Flags:**
- `--research` — spawn a focused research agent before execution
@@ -15,9 +15,9 @@ Execute small, ad-hoc tasks with CI guarantees: git context loading, `---ci---`
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ci/config.json` to determine if multi-project mode is active.
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
If `.ci/config.json` has `projects[]` with length > 0:
If `.ciagent/config.json` has `projects[]` with length > 0:
- Confirm `active_project` is correct
- If not, set it with `ci setActiveProject(<slug>)`
- All commit messages must include `project: <slug>` in `---ci---` block
@@ -37,7 +37,7 @@ git branch -a
Use GitContext.reconstructState() to understand project state.
Check that `.ci/config.json` exists. If missing: stop, run `ci-init` first.
Check that `.ciagent/config.json` exists. If missing: stop, run `ciagent-init` first.
## Step 3: Research (only with `--research` or `--full`)
+5 -5
View File
@@ -1,18 +1,18 @@
---
description: Review CI code changes with multi-persona analysis — auto-apply P0 fixes, flag P1+ for post-hoc review
description: Review CIAgent code changes with multi-persona analysis — auto-apply P0 fixes, flag P1+ for post-hoc review
---
# CI Review
# CIAgent Review
Multi-persona code review workflow. Reviews changes in the current phase, auto-applies P0 fixes, and flags P1+ issues for post-hoc review.
**Usage:** `ci-review [phase_number]`
**Usage:** `ciagent-review [phase_number]`
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ci/config.json` to determine if multi-project mode is active.
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
If `.ci/config.json` has `projects[]` with length > 0:
If `.ciagent/config.json` has `projects[]` with length > 0:
- Confirm `active_project` is correct for this review
- If not, set it with `ci setActiveProject(<slug>)`
- All commit messages must include `project: <slug>` in `---ci---` block
+8 -8
View File
@@ -1,20 +1,20 @@
---
description: Rollback CI phase — revert the last phase or specified phase by resetting to its pre-phase state
description: Rollback CIAgent phase — revert the last phase or specified phase by resetting to its pre-phase state
---
# CI Rollback
# CIAgent Rollback
Rollback a CI phase by reverting to the state before the phase started. Uses git to find the exact commit to reset to.
Rollback a CIAgent phase by reverting to the state before the phase started. Uses git to find the exact commit to reset to.
**Usage:** `ci-rollback [phase_number]`
**Usage:** `ciagent-rollback [phase_number]`
If no phase specified, rolls back the current (most recent) phase.
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ci/config.json` to determine if multi-project mode is active.
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
If `.ci/config.json` has `projects[]` with length > 0:
If `.ciagent/config.json` has `projects[]` with length > 0:
- Confirm `active_project` is correct for this rollback
- If not, set it with `ci setActiveProject(<slug>)`
- Identify project-scoped branches (prefixed with `<slug>/`)
@@ -71,8 +71,8 @@ git reset --hard [rollback_point]
## Step 5: Update State
- Delete the phase branch (if not already removed)
- Update `.ci/REQUIREMENTS.md` — mark phase requirements as blocked
- Update `.ci/ROADMAP.md` — mark phase as not_started
- Update `.ciagent/REQUIREMENTS.md` — mark phase requirements as blocked
- Update `.ciagent/ROADMAP.md` — mark phase as not_started
Commit the rollback:
+14 -14
View File
@@ -1,20 +1,20 @@
---
description: Execute the full CI pipeline — research → plan → execute → verify → complete for the current or specified phase
description: Execute the full CIAgent pipeline — research → plan → execute → verify → complete for the current or specified phase
---
# CI Run
# CIAgent Run
Execute the full CI pipeline from the current stage to completion. The orchestrator iterates through stages and delegates to specialized agents.
Execute the full CIAgent pipeline from the current stage to completion. The orchestrator iterates through stages and delegates to specialized agents.
**Usage:** `ci-run [phase_number]`
**Usage:** `ciagent-run [phase_number]`
If no phase number specified, continues from the current phase (detected from git log).
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ci/config.json` to determine if multi-project mode is active.
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
If `.ci/config.json` has `projects[]` with length > 0:
If `.ciagent/config.json` has `projects[]` with length > 0:
- Confirm `active_project` is correct for this run
- If not, set it with `ci setActiveProject(<slug>)`
- All commit messages must include `project: <slug>` in `---ci---` block
@@ -36,17 +36,17 @@ Determine current state:
## Step 2: Pre-Flight Check
Verify `.ci/config.json` exists. If missing: stop, run `ci-init` first.
Verify `.ciagent/config.json` exists. If missing: stop, run `ciagent-init` first.
Read `.ci/PROJECT.md` and `.ci/ROADMAP.md` for phase goals.
Read `.ciagent/PROJECT.md` and `.ciagent/ROADMAP.md` for phase goals.
## Step 3: Execute Pipeline Stages
For each stage in order (starting from current or from `specify`):
### SPECIFY
- Parse specification from `.ci/PROJECT.md`
- Validate requirements exist in `.ci/REQUIREMENTS.md`
- Parse specification from `.ciagent/PROJECT.md`
- Validate requirements exist in `.ciagent/REQUIREMENTS.md`
- Commit: `docs(init): validate specification`
### CLARIFY
@@ -57,7 +57,7 @@ For each stage in order (starting from current or from `specify`):
### RESEARCH
- Delegate to ci-researcher
- Research domain, ecosystem, prior art
- Update `.ci/` static files with conclusions
- Update `.ciagent/` static files with conclusions
- Commit: `docs(P##): research findings`
### PLAN
@@ -81,8 +81,8 @@ For each stage in order (starting from current or from `specify`):
- Merge phase branch into main (squash)
- Tag with patch version (e.g., `v0.2.3` — 3rd phase in milestone v0.2)
- Create Gitea release for the tag
- Update `.ci/REQUIREMENTS.md` requirement statuses
- Update `.ci/ROADMAP.md` phase status
- Update `.ciagent/REQUIREMENTS.md` requirement statuses
- Update `.ciagent/ROADMAP.md` phase status
- Commit: `docs(P##): complete [phase-name] phase`
Versioning: Major = project-level refactor/schema change, Minor = milestone completion, Patch = every phase.
@@ -92,7 +92,7 @@ Versioning: Major = project-level refactor/schema change, Minor = milestone comp
Between phases, perform a context reset:
1. Commit all work from the current phase
2. Update `.ci/` files (phase status, requirement statuses)
2. Update `.ciagent/` files (phase status, requirement statuses)
3. Verify `GitContext.reconstructState()` matches expected state
4. Reset context: spawn fresh agent (opencode) or re-read git context (platforms without subagents)
5. Next phase begins with fresh context from git log only
+71 -31
View File
@@ -1,27 +1,31 @@
---
description: Ship CI phase or milestone — test, tag, release. Every phase gets a patch release. Every milestone gets a minor (or major) release. Full autopilot.
description: Ship CIAgent phase or milestone — test, tag, release. Every phase and milestone gets a release. Full autopilot.
---
# CI Ship
# CIAgent Ship
Ship a CI phase or milestone. Every ship creates a release — no exceptions.
Ship a CIAgent phase or milestone. Every ship creates a release — no exceptions.
**Versioning rule:**
- **Major** (X.0.0): Project-level refactor or schema changes
- **Minor** (0.X.0): Feature milestone completion only
- **Patch** (0.0.X): Every phase completion
**3-Tier Versioning Model:**
**NFR versioning:**
- NFR milestones (all phases are fix/chore/docs/perf/refactor/test): progressive patch versions only (v0.1.1, v0.1.2, v0.1.3). No minor milestone tag.
- Feature milestones (any feat phase): progressive patch versions per phase + minor milestone tag on completion (e.g., v0.2.0).
| Milestone Type | Condition | Phase release | Milestone release |
|---------------|-----------|---------------|-------------------|
| **NFR** | All phases: fix/chore/docs/perf/refactor/test | Patch (`vX.Y.Z`) | None |
| **Feature** | Any phase is `feat`, no schema break | Patch (`vX.Y.Z`) | Minor — `vX.(Y+1).0` |
| **Schema-breaking** | Refactor/schema break/new direction | Minor — `vX.(Y+N).0` per phase | Major — `v(X+1).0.0` |
**Usage:** `ci-ship [phase_number|milestone]`
**CRITICAL:** Milestone tags are always the NEXT version, never the base:
- Feature: patches v0.5.1v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
- Schema-breaking: minors v0.3.0, v0.4.0, v0.5.0 → milestone tag is v1.0.0
- NFR: no milestone tag — the milestone is implicit from the patch sequence
**Usage:** `ciagent-ship [phase_number|milestone]`
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ci/config.json` to determine if multi-project mode is active.
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
If `.ci/config.json` has `projects[]` with length > 0:
If `.ciagent/config.json` has `projects[]` with length > 0:
- Confirm `active_project` is correct for this ship
- If not, set it with `ci setActiveProject(<slug>)`
- All commit messages must include `project: <slug>` in `---ci---` block
@@ -36,14 +40,14 @@ git log --max-count=10
git branch -a
```
Determine what is being shipped: a single phase (patch release) or an entire milestone (minor/major release).
Determine what is being shipped: a single phase or an entire milestone.
Read `.ci/ROADMAP.md` to determine:
Read `.ciagent/ROADMAP.md` to determine:
- Current milestone version (e.g., `v0.2`)
- Phase number within the milestone
- Whether this is the last phase in the milestone
Read `.ci/config.json` for autonomy level.
Read `.ciagent/config.json` for autonomy level.
## Step 2: Run Tests
@@ -57,22 +61,51 @@ If any fail: iterate autonomously until tests pass. Do NOT ask the user for guid
## Step 3: Compute Version
Determine the release version from what is being shipped. Check `isNfrMilestone()` for versioning behavior:
Determine milestone type by calling `getMilestoneType()` which returns `"nfr" | "feature" | "schema-breaking"`:
| What's shipping | Milestone Type | Version bump | Tag format | Example |
| What's shipping | Milestone Type | Phase release | Milestone release | Example |
|----------------|---------------|-------------|------------|---------|
| Single phase | NFR | Patch | `vX.Y.Z` | `v0.1.3` (3rd NFR phase in milestone v0.1) |
| Single phase | Feature | Patch | `vX.Y.Z` | `v0.2.3` (3rd phase in feature milestone v0.2) |
| Milestone completion | NFR | Patch (last phase) | `vX.Y.Z` | `v0.1.3` (no minor tag) |
| Milestone completion | Feature | Minor | `vX.Y.0` | `v0.3.0` (feature milestone v0.3 complete) |
| Project refactor/schema change | Any | Major | `vX.0.0` | `v1.0.0` (breaking schema) |
| Single phase | NFR | Patch `vX.Y.Z` | N/A | v0.1.3 (3rd NFR phase) |
| Single phase | Feature | Patch `vX.Y.Z` | N/A | v0.2.3 (3rd feature phase) |
| Single phase | Schema-breaking | Minor `vX.(Y+N).0` | N/A | v0.4.0 (2nd schema-breaking phase) |
| Milestone completion | NFR | Patch (last phase) | None | v0.1.3 (no milestone tag) |
| Milestone completion | Feature | Last patch | Minor `vX.(Y+1).0` | v0.3.0 (NOT v0.2.0) |
| Milestone completion | Schema-breaking | Last minor | Major `v(X+1).0.0` | v1.0.0 |
Count completed phases in the current milestone to determine the patch number.
Phase number within the milestone determines the increment:
- NFR/Feature: 1st phase = .1, 2nd = .2, etc. (v0.5.1, v0.5.2)
- Schema-breaking: 1st phase = next minor, 2nd = minor+1, etc. (v0.3.0, v0.4.0)
**Before creating ANY tag, validate:**
1. The tag must be strictly greater than all existing tags on the same major.minor line
2. Milestone completion tag must be the next minor (feature) or next major (schema-breaking)
3. NEVER create a milestone tag that is semantically below existing phase tags (e.g., v0.5.0 when v0.5.1 already exists)
## Step 4: Merge Branch
### Phase ship (patch release)
### Branch hierarchy: main > milestone/vX.X-slug > phase/NN-slug
Phases MUST merge into their milestone branch (or to main if no milestone branch exists). Milestones merge into main only after all phases are complete.
### Phase ship
**If milestone branch exists:**
```bash
git checkout milestone/vX.Y-slug
git merge --squash phase/NN-slug
git commit -m "docs(P##): complete [phase-name] phase
---ci---
phase: [N]
milestone: [vX.Y]
status: complete
requirements:
covered: [REQ-01, REQ-02]
partial: []
---/ci---"
```
**If no milestone branch exists (single-phase milestone):**
```bash
git checkout main
git merge --squash phase/NN-slug
@@ -88,9 +121,10 @@ requirements:
---/ci---"
```
### Milestone ship (minor/major release)
### Milestone ship (after last phase)
```bash
# Verify all phase branches are merged into milestone branch
git checkout main
git merge --squash milestone/vX.Y-slug
git commit -m "docs(milestone): complete [milestone-name]
@@ -109,6 +143,12 @@ git tag -a vX.Y.Z -m "vX.Y.Z: [phase-name or milestone-name]"
git push origin main --tags
```
**Tag format by milestone type:**
- NFR/Feature phase: patch format (`v0.5.1`, `v0.5.2`)
- Schema-breaking phase: minor format (`v0.3.0`, `v0.4.0`)
- Feature milestone: next minor (`v0.6.0`, NOT `v0.5.0`)
- Schema-breaking milestone: next major (`v1.0.0`)
## Step 6: Create Release
**Every ship creates a Gitea release. No exceptions.**
@@ -132,8 +172,8 @@ For milestone releases, include a summary of all phases completed and requiremen
## Step 7: Update .ci/ Files
- Update `.ci/REQUIREMENTS.md` — mark shipped requirements as complete
- Update `.ci/ROADMAP.md` — mark shipped phase as complete
- Update `.ciagent/REQUIREMENTS.md` — mark shipped requirements as complete
- Update `.ciagent/ROADMAP.md` — mark shipped phase as complete
Commit the file updates.
@@ -141,11 +181,11 @@ Commit the file updates.
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CI ► SHIPPED
CIAgent ► SHIPPED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase [N]: [name]
Milestone: [vX.Y]
Milestone: [vX.Y] ([nfr|feature|schema-breaking])
Version: vX.Y.Z
Release: https://git.cloudinit.dev/continuous-intelligence/ci/releases/tag/vX.Y.Z
Status: complete
@@ -158,6 +198,6 @@ Requirements covered: [N]
Commits: [N]
[If milestone complete:]
All phases in milestone v0.2 complete. Milestone released.
All phases in milestone v0.2 complete. Milestone released as vX.Y.Z.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
+11 -11
View File
@@ -1,18 +1,18 @@
---
description: Show CI project status — current phase, milestone, pipeline stage, decisions, escalations, and requirements coverage
description: Show CIAgent project status — current phase, milestone, pipeline stage, decisions, escalations, and requirements coverage
---
# CI Status
# CIAgent Status
Display the current CI project status derived entirely from the git log and .ci/ files.
Display the current CIAgent project status derived entirely from the git log and .ciagent/ files.
**Usage:** `ci-status`
**Usage:** `ciagent-status`
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ci/config.json` to determine if multi-project mode is active.
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
If `.ci/config.json` has `projects[]` with length > 0:
If `.ciagent/config.json` has `projects[]` with length > 0:
- Show project list with active project indicator
- Confirm `active_project` is the project to show status for
- If not, set it with `ci setActiveProject(<slug>)`
@@ -45,15 +45,15 @@ Collect from git log:
## Step 3: Read .ci/ Files
Read:
- `.ci/PROJECT.md` — project name and vision
- `.ci/ROADMAP.md` — phase list with status
- `.ci/config.json` — autonomy level
- `.ciagent/PROJECT.md` — project name and vision
- `.ciagent/ROADMAP.md` — phase list with status
- `.ciagent/config.json` — autonomy level
## Step 4: Display Status
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CI ► STATUS
CIAgent ► STATUS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Project: [name] [If multi-project: (active)]
@@ -79,4 +79,4 @@ Recent commits:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
If no `.ci/` directory exists: report "Project not initialized. Run ci-init first."
If no `.ciagent/` directory exists: report "Project not initialized. Run ciagent-init first."
+6 -6
View File
@@ -1,20 +1,20 @@
---
description: Verify CI project deliverables against requirements — structural, behavioral, security, and quality checks
description: Verify CIAgent project deliverables against requirements — structural, behavioral, security, and quality checks
---
# CI Verify
# CIAgent Verify
Run the CI verification pipeline against the current or specified phase. Four layers: structural, behavioral, security, quality.
Run the CIAgent verification pipeline against the current or specified phase. Four layers: structural, behavioral, security, quality.
**Usage:** `ci-verify [phase_number]`
**Usage:** `ciagent-verify [phase_number]`
If no phase specified, verifies the current phase.
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ci/config.json` to determine if multi-project mode is active.
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
If `.ci/config.json` has `projects[]` with length > 0:
If `.ciagent/config.json` has `projects[]` with length > 0:
- Confirm `active_project` is correct for this verification
- If not, set it with `ci setActiveProject(<slug>)`
- Scope verification to the active project
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Audit CI project health — reconstruct state from git log, verify .ci/ files match codebase, check for stale references
description: Audit CIAgent project health — reconstruct state from git log, verify .ciagent/ files match codebase, check for stale references
tools:
read: true
bash: true
@@ -16,6 +16,6 @@ Arguments: $ARGUMENTS
</context>
<process>
Execute the CI audit workflow end-to-end.
Execute the CIAgent audit workflow end-to-end.
Preserve all workflow gates, validations, checkpoints, and routing.
</process>
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Clarify CI project ambiguities — generate questions, accept defaults at full autonomy, present at supervised/guided
description: Clarify CIAgent project ambiguities — generate questions, accept defaults at full autonomy, present at supervised/guided
argument-hint: "[phase_number]"
tools:
read: true
@@ -21,6 +21,6 @@ Arguments: $ARGUMENTS
</context>
<process>
Execute the CI clarify workflow end-to-end.
Execute the CIAgent clarify workflow end-to-end.
Preserve all workflow gates, validations, checkpoints, and routing.
</process>
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Systematic CI debugging with git context — triage, diagnose root cause, auto-fix or escalate
description: Systematic CIAgent debugging with git context — triage, diagnose root cause, auto-fix or escalate
argument-hint: "[description]"
tools:
read: true
@@ -21,6 +21,6 @@ Arguments: $ARGUMENTS
</context>
<process>
Execute the CI debug workflow end-to-end.
Execute the CIAgent debug workflow end-to-end.
Preserve all workflow gates, validations, checkpoints, and routing.
</process>
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Initialize a new CI project — specification → clarify → create .ci/ reference files → initial commit
description: Initialize a new CIAgent project — specification → clarify → create .ciagent/ reference files → initial commit
argument-hint: "[description]"
tools:
read: true
@@ -21,6 +21,6 @@ Arguments: $ARGUMENTS
</context>
<process>
Execute the CI init workflow end-to-end.
Execute the CIAgent init workflow end-to-end.
Preserve all workflow gates, validations, checkpoints, and routing.
</process>
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Execute an ad-hoc CI task with full agentic guarantees — git context, ---ci--- commits, optional research and verification
description: Execute an ad-hoc CIAgent task with full agentic guarantees — git context, ---ci--- commits, optional research and verification
argument-hint: "[description] [--research] [--verify] [--full]"
tools:
read: true
@@ -21,6 +21,6 @@ Arguments: $ARGUMENTS
</context>
<process>
Execute the CI quick workflow end-to-end.
Execute the CIAgent quick workflow end-to-end.
Preserve all workflow gates, validations, checkpoints, and routing.
</process>
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Review CI code changes with multi-persona analysis — auto-apply P0 fixes, flag P1+ for post-hoc review
description: Review CIAgent code changes with multi-persona analysis — auto-apply P0 fixes, flag P1+ for post-hoc review
argument-hint: "[phase_number]"
tools:
read: true
@@ -20,6 +20,6 @@ Arguments: $ARGUMENTS
</context>
<process>
Execute the CI review workflow end-to-end.
Execute the CIAgent review workflow end-to-end.
Preserve all workflow gates, validations, checkpoints, and routing.
</process>
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Rollback CI phase — revert the last phase or specified phase by resetting to its pre-phase state
description: Rollback CIAgent phase — revert the last phase or specified phase by resetting to its pre-phase state
argument-hint: "[phase_number]"
tools:
read: true
@@ -21,6 +21,6 @@ Arguments: $ARGUMENTS
</context>
<process>
Execute the CI rollback workflow end-to-end.
Execute the CIAgent rollback workflow end-to-end.
Preserve all workflow gates, validations, checkpoints, and routing.
</process>
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Execute the full CI pipeline — research → plan → execute → verify → complete
description: Execute the full CIAgent pipeline — research → plan → execute → verify → complete
argument-hint: "[phase_number]"
tools:
read: true
@@ -21,6 +21,6 @@ Arguments: $ARGUMENTS
</context>
<process>
Execute the CI run workflow end-to-end.
Execute the CIAgent run workflow end-to-end.
Preserve all workflow gates, validations, checkpoints, and routing.
</process>
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Ship CI phase or milestone — test, commit, tag, push, release. Full autopilot: zero HITL after milestone setup
description: Ship CIAgent phase or milestone — test, commit, tag, push, release. Full autopilot: zero HITL after milestone setup
argument-hint: "[phase_number|milestone]"
tools:
read: true
@@ -20,6 +20,6 @@ Arguments: $ARGUMENTS
</context>
<process>
Execute the CI ship workflow end-to-end.
Execute the CIAgent ship workflow end-to-end.
Preserve all workflow gates, validations, checkpoints, and routing.
</process>
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Show CI project status — current phase, milestone, pipeline stage, decisions, escalations, and requirements coverage
description: Show CIAgent project status — current phase, milestone, pipeline stage, decisions, escalations, and requirements coverage
tools:
read: true
bash: true
@@ -16,6 +16,6 @@ Arguments: $ARGUMENTS
</context>
<process>
Execute the CI status workflow end-to-end.
Execute the CIAgent status workflow end-to-end.
Preserve all workflow gates, validations, checkpoints, and routing.
</process>
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Verify CI project deliverables against requirements — structural, behavioral, security, and quality checks
description: Verify CIAgent project deliverables against requirements — structural, behavioral, security, and quality checks
argument-hint: "[phase_number]"
tools:
read: true
@@ -20,6 +20,6 @@ Arguments: $ARGUMENTS
</context>
<process>
Execute the CI verify workflow end-to-end.
Execute the CIAgent verify workflow end-to-end.
Preserve all workflow gates, validations, checkpoints, and routing.
</process>
+6 -5
View File
@@ -1,19 +1,20 @@
{
"name": "@continuous-intelligence/ci",
"version": "0.1.0",
"name": "@continuous-intelligence/ciagent",
"version": "0.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@continuous-intelligence/ci",
"version": "0.1.0",
"name": "@continuous-intelligence/ciagent",
"version": "0.5.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"commander": "^12.1.0",
"zod": "^3.23.0"
},
"bin": {
"ci": "dist/cli/index.js"
"ciagent": "dist/cli/index.js"
},
"devDependencies": {
"@types/jest": "^29.5.0",
+3 -3
View File
@@ -1,11 +1,11 @@
{
"name": "@continuous-intelligence/ci",
"name": "@continuous-intelligence/ciagent",
"version": "0.5.0",
"description": "Fully autonomous AI-driven software engineering harness - Continuous Intelligence",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"ci": "./dist/cli/index.js"
"ciagent": "./dist/cli/index.js"
},
"files": [
"dist/",
@@ -22,7 +22,7 @@
"prepublishOnly": "npm run build",
"install-opencode": "node scripts/postinstall.js"
},
"keywords": ["ci", "autonomous", "ai", "software-engineering", "agent", "multi-project"],
"keywords": ["ciagent", "autonomous", "ai", "software-engineering", "agent", "multi-project"],
"license": "MIT",
"engines": {
"node": ">=18.0.0"
Regular → Executable
+15 -15
View File
@@ -14,9 +14,9 @@ for arg in "$@"; do
--help|-h)
echo "Usage: $(basename "$0") [--uninstall] [--force]"
echo ""
echo "Install CI opencode integration files to ~/.config/opencode/"
echo "Install CIAgent opencode integration files to ~/.config/opencode/"
echo ""
echo " --uninstall Remove CI integration files"
echo " --uninstall Remove CIAgent integration files"
echo " --force Overwrite existing files without prompting"
echo " --help Show this help"
exit 0
@@ -25,24 +25,24 @@ for arg in "$@"; do
done
if [ "$UNINSTALL" = true ]; then
echo "Uninstalling CI opencode integration..."
rm -f "${OPENCODE_DIR}/agents/ci-"*.md 2>/dev/null || true
rm -f "${OPENCODE_DIR}/command/ci-"*.md 2>/dev/null || true
rm -rf "${OPENCODE_DIR}/ci/" 2>/dev/null || true
echo "CI integration files removed."
echo "Uninstalling CIAgent opencode integration..."
rm -f "${OPENCODE_DIR}/agents/ci-"*.md 2>/dev/null || true
rm -f "${OPENCODE_DIR}/command/ci-"*.md 2>/dev/null || true
rm -rf "${OPENCODE_DIR}/ci/" 2>/dev/null || true
echo "CIAgent integration files removed."
echo "Note: opencode.json permissions entry preserved (edit manually if needed)."
exit 0
fi
if [ ! -d "$CI_DIR" ]; then
echo "Error: opencode/ directory not found at ${CI_DIR}"
echo "Ensure you're running from the CI repository root."
echo "Ensure you're running from the CIAgent repository root."
exit 1
fi
echo "Installing CI opencode integration..."
echo "Installing CIAgent opencode integration..."
echo " Source: ${CI_DIR}"
echo " Target: ${OPENCODE_DIR}"
echo ""
@@ -143,15 +143,15 @@ fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " CI ► INSTALL COMPLETE"
echo " CIAgent ► INSTALL COMPLETE"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo " Copied: ${COPIED} files"
echo " Skipped: ${SKIPPED} files"
echo ""
echo " Commands available: ci-init, ci-run, ci-quick, ci-status,"
echo " ci-audit, ci-verify, ci-debug, ci-review, ci-ship,"
echo " ci-rollback, ci-clarify"
echo " Commands available: ciagent-init, ciagent-run, ciagent-quick, ciagent-status,"
echo " ciagent-audit, ciagent-verify, ciagent-debug, ciagent-review, ciagent-ship,"
echo " ciagent-rollback, ciagent-clarify"
echo ""
echo " Run --uninstall to remove."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+6 -6
View File
@@ -52,19 +52,19 @@ function applyTemplate(content, vars) {
function install() {
const pkgDir = getPackageDir();
if (!pkgDir) {
console.log("CI postinstall: Could not determine package directory. Skipping.");
console.log("CIAgent postinstall: Could not determine package directory. Skipping.");
return;
}
const opencodeDir = path.join(pkgDir, "opencode");
if (!fs.existsSync(opencodeDir)) {
console.log("CI postinstall: opencode/ directory not found. Skipping.");
console.log("CIAgent postinstall: opencode/ directory not found. Skipping.");
return;
}
if (!isGlobalInstall()) {
console.log("CI postinstall: Not a global install. Skipping opencode integration.");
console.log(" Run `npx ci-install` or `./scripts/install.sh` to install manually.");
console.log("CIAgent postinstall: Not a global install. Skipping opencode integration.");
console.log(" Run `npx ciagent-install` or `./scripts/install.sh` to install manually.");
return;
}
@@ -132,11 +132,11 @@ function install() {
}
}
console.log(`CI postinstall: ${copied} files installed, ${skipped} skipped.`);
console.log(`CIAgent postinstall: ${copied} files installed, ${skipped} skipped.`);
}
try {
install();
} catch (err) {
console.log("CI postinstall: Non-fatal error:", err.message);
console.log("CIAgent postinstall: Non-fatal error:", err.message);
}
+163 -7
View File
@@ -1,4 +1,21 @@
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
import { execSync } from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
export interface ExecutorResult {
success: boolean;
tasksExecuted: number;
tasksCommitted: number;
testsPassing: boolean;
mustHavesChecked: { name: string; passed: boolean }[];
error?: string;
}
interface MustHaveItem {
name: string;
passed: boolean;
}
export class ExecutorAgent extends BaseAgent {
readonly name = "executor";
@@ -8,21 +25,160 @@ export class ExecutorAgent extends BaseAgent {
async execute(context: AgentContext): Promise<AgentResult> {
const start = Date.now();
this.log("Executing tasks...");
if (context.backend) {
const result = await this.executeViaBackend(
context,
`Execute implementation for stage ${context.stage}, phase ${context.phase}. Specification: ${context.specification}`
);
return { ...result, duration_ms: Date.now() - start };
const taskPrompt = await this.buildBackendTaskPrompt(context);
const backendResult = await this.executeViaBackend(context, taskPrompt);
const verification = await this.verifyExecution(context);
return {
...backendResult,
output: `${backendResult.output}\nVerification: tests=${verification.testsPassing ? "passing" : "failing"}, must-haves checked=${verification.mustHavesChecked.length}`,
duration_ms: Date.now() - start,
};
}
return {
success: false,
output: "Execution requires an intelligence backend. Configure one with: ci init --backend",
output: "Executor requires intelligence backend for code implementation",
artifacts_created: [],
decisions: 0,
escalations: 0,
duration_ms: Date.now() - start,
error: "No intelligence backend available",
error: "Executor requires intelligence backend for code implementation",
};
}
private async buildBackendTaskPrompt(context: AgentContext): Promise<string> {
const parts: string[] = [
`Execute implementation for stage ${context.stage}, phase ${context.phase}.`,
"",
"## Specification",
context.specification || "No specification provided",
];
const planContent = this.readPlanFile(context);
if (planContent) {
parts.push("", "## Plan", planContent);
}
const ciDir = path.join(context.project_path, ".ciagent");
const roadmapPath = path.join(ciDir, "ROADMAP.md");
const archPath = path.join(ciDir, "ARCHITECTURE.md");
if (fs.existsSync(roadmapPath)) {
try {
const roadmap = fs.readFileSync(roadmapPath, "utf-8");
parts.push("", "## Roadmap Context", roadmap.slice(0, 2000));
} catch {}
}
if (fs.existsSync(archPath)) {
try {
const arch = fs.readFileSync(archPath, "utf-8");
parts.push("", "## Architecture Boundaries", arch.slice(0, 2000));
} catch {}
}
parts.push("", "## Execution Rules");
parts.push("- Execute one task at a time");
parts.push("- Commit after each task with ---ci--- block");
parts.push("- Never pause for checkpoints");
parts.push("- Create automated verification for traditionally human tasks");
return parts.join("\n");
}
private readPlanFile(context: AgentContext): string | null {
const planPath = path.join(context.project_path, ".ciagent", "PLAN.md");
try {
if (fs.existsSync(planPath)) {
return fs.readFileSync(planPath, "utf-8");
}
} catch {}
return null;
}
private async verifyExecution(context: AgentContext): Promise<ExecutorResult> {
const mustHavesChecked: MustHaveItem[] = this.checkMustHaves(context);
let testsPassing = false;
let tasksExecuted = 0;
let tasksCommitted = 0;
try {
const logOutput = execSync("git log --max-count=20 --oneline", {
cwd: context.project_path,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}).trim();
const commitLines = logOutput.split("\n").filter(Boolean);
tasksCommitted = commitLines.filter((l) => /feat|fix|test/.test(l)).length;
tasksExecuted = tasksCommitted;
} catch {}
try {
execSync("npm test", {
cwd: context.project_path,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
timeout: 120000,
});
testsPassing = true;
} catch {
testsPassing = false;
}
return {
success: mustHavesChecked.every((m) => m.passed) && testsPassing,
tasksExecuted,
tasksCommitted,
testsPassing,
mustHavesChecked,
};
}
private checkMustHaves(context: AgentContext): MustHaveItem[] {
const planPath = path.join(context.project_path, ".ciagent", "PLAN.md");
const results: MustHaveItem[] = [];
try {
if (!fs.existsSync(planPath)) return results;
const planContent = fs.readFileSync(planPath, "utf-8");
const mustHaveRegex = /-\s*\[x\]\s*(.+)/g;
let match;
while ((match = mustHaveRegex.exec(planContent)) !== null) {
const name = match[1].trim();
const passed = this.verifyMustHaveItem(name, context);
results.push({ name, passed });
}
} catch {}
return results;
}
private verifyMustHaveItem(item: string, context: AgentContext): boolean {
const fileMatch = item.match(/(?:exists|created?|present).*?[\s:]+([^\s]+\.(ts|js|json|md))/i);
if (fileMatch) {
const filePath = path.join(context.project_path, fileMatch[1]);
return fs.existsSync(filePath);
}
const testMatch = item.match(/(?:test|tests?)\s+(?:pass|passing)/i);
if (testMatch) {
try {
execSync("npm test", {
cwd: context.project_path,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
timeout: 120000,
});
return true;
} catch {
return false;
}
}
return true;
}
}
+7 -4
View File
@@ -1,9 +1,9 @@
export { BaseAgent, AgentContext, AgentResult, backendResultToAgentResult } from "./base.js";
export { OrchestratorAgent } from "./orchestrator.js";
export { PlannerAgent } from "./planner.js";
export { ExecutorAgent } from "./executor.js";
export { VerifierAgent } from "./verifier.js";
export { ResearcherAgent } from "./researcher.js";
export { PlannerAgent, PlannerResult } from "./planner.js";
export { ExecutorAgent, ExecutorResult } from "./executor.js";
export { VerifierAgent, VerifierResult } from "./verifier.js";
export { ResearcherAgent, ResearcherResult } from "./researcher.js";
export { ChallengerAgent } from "./challenger.js";
export { SecurityAuditorAgent } from "./security-auditor.js";
export { DebuggerAgent } from "./debugger.js";
@@ -17,6 +17,7 @@ export { ProjectResearcherAgent } from "./project-researcher.js";
export { ResearchSynthesizerAgent } from "./research-synthesizer.js";
export { SolutionWriterAgent } from "./solution-writer.js";
export { PhaseResearcherAgent } from "./phase-researcher.js";
export { TesterAgent, TesterResult } from "./tester.js";
import { AgentName } from "../types/config.js";
import { BaseAgent as BaseAgentType } from "./base.js";
@@ -38,6 +39,7 @@ import { ProjectResearcherAgent } from "./project-researcher.js";
import { ResearchSynthesizerAgent } from "./research-synthesizer.js";
import { SolutionWriterAgent } from "./solution-writer.js";
import { PhaseResearcherAgent } from "./phase-researcher.js";
import { TesterAgent } from "./tester.js";
const agentRegistry: Record<AgentName, () => BaseAgentType> = {
orchestrator: () => new OrchestratorAgent(),
@@ -58,6 +60,7 @@ const agentRegistry: Record<AgentName, () => BaseAgentType> = {
"project-researcher": () => new ProjectResearcherAgent(),
"research-synthesizer": () => new ResearchSynthesizerAgent(),
"solution-writer": () => new SolutionWriterAgent(),
tester: () => new TesterAgent(),
};
export function getAgent(name: AgentName): BaseAgentType {
+253 -52
View File
@@ -4,9 +4,9 @@ import { ClarifyPhase } from "../core/clarify.js";
import { EscalationProtocol, EscalationInput } from "../core/escalation.js";
import { GitContext, ProjectState } from "../core/git-context.js";
import { GitBranch } from "../core/git-branch.js";
import { CiFiles } from "../core/ci-files.js";
import { CIAgentFiles } from "../core/ciagent-files.js";
import { CommitBuilder } from "../core/commit-builder.js";
import { CIConfig, AgentName } from "../types/config.js";
import { CIAgentConfig, AgentName } from "../types/config.js";
import {
PipelineState,
PipelineStage,
@@ -16,40 +16,43 @@ import {
STAGE_ORDER,
} from "../types/pipeline.js";
import { Specification, parseSpecification } from "../types/specification.js";
import { loadConfig, saveConfig, isCIInitialized, initCI } from "../core/config.js";
import { loadConfig, saveConfig, isCIAgentInitialized, initCIAgent } from "../core/config.js";
import { getAgent } from "./index.js";
import { IntelligenceBackend, BackendUnavailableError } from "../backends/types.js";
import { execSync } from "node:child_process";
export interface GitAgentContext extends AgentContext {
gitContext: GitContext;
gitBranch: GitBranch;
ciFiles: CiFiles;
ciFiles: CIAgentFiles;
milestone: string;
}
export class OrchestratorAgent extends BaseAgent {
readonly name: AgentName = "orchestrator";
readonly description = "Top-level autonomous controller that coordinates the full CI pipeline";
readonly description = "Top-level autonomous controller that coordinates the full CIAgent pipeline";
readonly workflow = "run";
private config: CIConfig;
private config: CIAgentConfig;
private pipelineState: PipelineState | null = null;
private decisionEngine: DecisionEngine | null = null;
private escalationProtocol: EscalationProtocol | null = null;
private gitContext: GitContext | null = null;
private gitBranch: GitBranch | null = null;
private ciFiles: CiFiles | null = null;
private ciFiles: CIAgentFiles | null = null;
private currentMilestone: string;
private phaseResults: PhaseResult[] = [];
private totalPhases: number = 1;
private static readonly STAGE_AGENT_MAP: Partial<Record<PipelineStage, AgentName>> = {
research: "researcher",
plan: "planner",
execute: "executor",
test: "tester",
verify: "verifier",
};
constructor(config?: CIConfig) {
constructor(config?: CIAgentConfig) {
super();
this.config = config || loadConfig(process.cwd());
this.currentMilestone = "v1.0";
@@ -57,14 +60,14 @@ export class OrchestratorAgent extends BaseAgent {
async execute(context: AgentContext): Promise<AgentResult> {
const startTime = Date.now();
this.log("Starting CI Orchestrator pipeline (git-native)");
this.log("Starting CIAgent Orchestrator pipeline (git-native)");
try {
this.config = loadConfig(context.project_path);
this.gitContext = new GitContext(context.project_path);
this.gitBranch = new GitBranch(context.project_path);
this.ciFiles = new CiFiles(context.project_path);
this.ciFiles = new CIAgentFiles(context.project_path);
this.ciFiles.ensureCIDir();
const projectState = this.gitContext.reconstructState();
@@ -78,47 +81,66 @@ export class OrchestratorAgent extends BaseAgent {
this.pipelineState.current_stage = projectState.currentStage;
}
this.totalPhases = this.deriveTotalPhases();
this.log(`Total phases in milestone: ${this.totalPhases}`);
this.decisionEngine = new DecisionEngine(this.config, context.project_path, this.currentMilestone);
this.escalationProtocol = new EscalationProtocol(this.config, context.project_path, this.currentMilestone);
for (const stage of STAGE_ORDER) {
this.log(`Entering stage: ${stage}`);
this.pipelineState.current_stage = stage;
this.pipelineState.last_updated = new Date().toISOString();
while (this.pipelineState.current_phase <= this.totalPhases) {
this.log(`Processing phase ${this.pipelineState.current_phase} of ${this.totalPhases}`);
const result = await this.executeStage(stage, context);
for (const stage of STAGE_ORDER) {
this.log(`Entering stage: ${stage}`);
this.pipelineState.current_stage = stage;
this.pipelineState.last_updated = new Date().toISOString();
if (!result.success && stage !== "complete") {
this.pipelineState.errors.push({
stage,
phase: this.pipelineState.current_phase,
message: result.error || "Stage failed",
timestamp: new Date().toISOString(),
retry_count: 0,
resolved: false,
});
const result = await this.executeStageWithRecovery(stage, context);
if (stage === "specify" || stage === "clarify") {
return {
success: false,
output: `Pipeline failed at ${stage}: ${result.error}`,
artifacts_created: this.phaseResults.reduce(
(acc, r) => acc + r.artifacts_created.length,
0
),
decisions: this.phaseResults.reduce(
(acc, r) => acc + r.decisions_made,
0
),
escalations: this.phaseResults.reduce(
(acc, r) => acc + r.escalations_raised,
0
),
duration_ms: Date.now() - startTime,
error: result.error,
};
this.phaseResults.push(result);
this.recordPhaseResult(result);
if (!result.success && stage !== "complete") {
this.pipelineState.errors.push({
stage,
phase: this.pipelineState.current_phase,
message: result.error || "Stage failed",
timestamp: new Date().toISOString(),
retry_count: 0,
resolved: false,
});
if (stage === "specify" || stage === "clarify") {
return {
success: false,
output: `Pipeline failed at ${stage}: ${result.error}`,
artifacts_created: this.phaseResults.reduce(
(acc, r) => acc + r.artifacts_created.length,
0
),
decisions: this.phaseResults.reduce(
(acc, r) => acc + r.decisions_made,
0
),
escalations: this.phaseResults.reduce(
(acc, r) => acc + r.escalations_raised,
0
),
duration_ms: Date.now() - startTime,
error: result.error,
};
}
}
}
if (this.pipelineState.current_phase < this.totalPhases) {
this.performPhaseBoundaryCheckpoint(context);
this.pipelineState.current_phase++;
this.pipelineState.current_stage = "specify";
this.log(`Advancing to phase ${this.pipelineState.current_phase}`);
} else {
break;
}
}
const totalDuration = Date.now() - startTime;
@@ -151,9 +173,159 @@ export class OrchestratorAgent extends BaseAgent {
duration_ms: Date.now() - startTime,
error: err instanceof Error ? err.message : String(err),
};
} finally {
this.escalationProtocol?.dispose();
}
}
private buildGitAgentContext(context: AgentContext): GitAgentContext {
return {
...context,
gitContext: this.gitContext!,
gitBranch: this.gitBranch!,
ciFiles: this.ciFiles!,
milestone: this.currentMilestone,
};
}
private recordPhaseResult(result: PhaseResult): void {
for (const artifact of result.artifacts_created) {
this.log(`Artifact created: ${artifact}`);
}
if (result.decisions_made > 0 && this.decisionEngine) {
this.decisionEngine.makeHighConfidenceDecision(
`Agent reported ${result.decisions_made} decision(s) during ${result.stage}`,
`Decisions recorded from ${result.stage} stage execution`,
"general",
[]
);
}
if (result.escalations_raised > 0 && this.escalationProtocol) {
this.escalationProtocol.escalate({
type: "low_confidence_decision",
phase: String(this.pipelineState!.current_phase),
description: `Agent reported ${result.escalations_raised} escalation(s) during ${result.stage}`,
context: `Stage ${result.stage} raised escalations during execution`,
options: [
{ id: "proceed", label: "Proceed", description: "Continue pipeline execution", recommended: true },
{ id: "halt", label: "Halt", description: "Stop pipeline and await manual review", recommended: false },
],
default_option_id: "proceed",
});
}
}
private deriveTotalPhases(): number {
if (!this.ciFiles) return 1;
const roadmap = this.ciFiles.readRoadmapMd();
if (!roadmap || roadmap.phases.length === 0) return 1;
return roadmap.phases.length;
}
private performPhaseBoundaryCheckpoint(context: AgentContext): void {
this.log(`Phase boundary checkpoint for phase ${this.pipelineState!.current_phase}`);
if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) {
try {
const message = `chore(P${String(this.pipelineState!.current_phase).padStart(2, "0")}): phase boundary checkpoint\n\n---ci---\nphase: ${this.pipelineState!.current_phase}\nmilestone: ${this.currentMilestone}\nstatus: complete\n---/ci---`;
execSync(`git add -A && git commit -m "${message.replace(/"/g, '\\"')}" --allow-empty`, {
cwd: context.project_path,
stdio: "pipe",
});
} catch (err) {
this.warn(`Phase boundary commit failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
if (this.ciFiles) {
this.ciFiles.updatePhaseStatus(this.pipelineState!.current_phase, "complete");
const reqs = this.ciFiles.readRequirementsMd();
if (reqs) {
for (const t of reqs.traceability) {
if (t.phase === this.pipelineState!.current_phase && t.status === "in_progress") {
this.ciFiles.updateRequirementStatus(t.requirement, "complete");
}
}
}
}
if (this.gitContext) {
const verifiedState = this.gitContext.reconstructState();
this.log(`Verified state: phase=${verifiedState.currentPhase}, milestone=${verifiedState.currentMilestone}, stage=${verifiedState.currentStage}`);
}
}
private async executeStageWithRecovery(
stage: PipelineStage,
context: AgentContext
): Promise<PhaseResult> {
try {
const result = await this.executeStage(stage, context);
if (result.success) return result;
} catch (err) {
this.warn(`First attempt failed for ${stage}: ${err instanceof Error ? err.message : String(err)}`);
}
this.log(`Retrying stage ${stage}...`);
try {
const result = await this.executeStage(stage, context);
if (result.success) return result;
} catch (err) {
this.warn(`Retry failed for ${stage}: ${err instanceof Error ? err.message : String(err)}`);
}
if (context.backend) {
this.log(`Attempting plan revision for failed stage ${stage}...`);
try {
const planner = getAgent("planner");
const gitContext = this.buildGitAgentContext(context);
const planResult = await planner.execute({
...gitContext,
specification: `Plan revision needed: stage ${stage} failed twice. Original error context: phase ${this.pipelineState!.current_phase}`,
});
if (planResult.success) {
this.log(`Plan revision succeeded, retrying ${stage} with revised plan...`);
try {
const result = await this.executeStage(stage, context);
if (result.success) return result;
} catch (err) {
this.warn(`Post-revision retry failed for ${stage}: ${err instanceof Error ? err.message : String(err)}`);
}
}
} catch (err) {
this.warn(`Plan revision failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
if (this.escalationProtocol) {
this.escalationProtocol.escalate({
type: "verification_failure",
phase: String(this.pipelineState!.current_phase),
description: `Stage ${stage} failed after retry and plan revision attempts`,
context: `All recovery attempts exhausted for stage ${stage} in phase ${this.pipelineState!.current_phase}`,
options: [
{ id: "skip", label: "Skip stage", description: "Continue pipeline skipping this stage", recommended: true },
{ id: "abort", label: "Abort pipeline", description: "Stop the entire pipeline", recommended: false },
],
default_option_id: "skip",
});
}
return {
phase: this.pipelineState!.current_phase,
stage,
success: false,
artifacts_created: [],
decisions_made: 0,
escalations_raised: 1,
duration_ms: 0,
error: `Stage ${stage} failed after recovery attempts`,
};
}
private async executeStage(
stage: PipelineStage,
context: AgentContext
@@ -165,7 +337,8 @@ export class OrchestratorAgent extends BaseAgent {
this.log(`Delegating ${stage} to ${agentName} agent via backend...`);
try {
const agent = getAgent(agentName);
const result = await agent.execute(context);
const gitContext = this.buildGitAgentContext(context);
const result = await agent.execute(gitContext);
return {
phase: this.pipelineState!.current_phase,
stage,
@@ -207,11 +380,10 @@ export class OrchestratorAgent extends BaseAgent {
});
this.log("Init commit prepared with specification in ---ci--- block");
artifactsCreated.push(".ci/config.json");
artifactsCreated.push(".ciagent/config.json");
if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) {
try {
const { execSync } = await import("node:child_process");
this.ciFiles!.writeProjectMd({
name: spec.objective.slice(0, 30),
coreValue: spec.objective,
@@ -296,10 +468,9 @@ export class OrchestratorAgent extends BaseAgent {
1,
this.currentMilestone,
"initial domain research",
["Research completed. Key findings in .ci/ARCHITECTURE.md and .ci/PROJECT.md updates."]
["Research completed. Key findings in .ciagent/ARCHITECTURE.md and .ciagent/PROJECT.md updates."]
);
try {
const { execSync } = await import("node:child_process");
execSync(`git add -A && git commit -m "${researchCommit.replace(/"/g, '\\"')}" --allow-empty`, {
cwd: context.project_path,
stdio: "pipe",
@@ -310,7 +481,7 @@ export class OrchestratorAgent extends BaseAgent {
}
this.pipelineState!.research_completed = true;
artifactsCreated.push(".ci/ARCHITECTURE.md");
artifactsCreated.push(".ciagent/ARCHITECTURE.md");
break;
}
@@ -342,6 +513,38 @@ export class OrchestratorAgent extends BaseAgent {
this.pipelineState!.execute_completed = true;
break;
case "test": {
this.log("Running tests...");
if (!context.backend) {
this.log("No backend available — running mechanical test fallback via npm test");
try {
const testOutput = execSync("npm test", {
cwd: context.project_path,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
timeout: 120000,
});
this.log("npm test passed");
this.pipelineState!.test_completed = true;
artifactsCreated.push("test-results");
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
this.warn(`npm test failed: ${errMsg}`);
return {
phase: this.pipelineState!.current_phase,
stage: "test",
success: false,
artifacts_created: artifactsCreated,
decisions_made: decisionsMade,
escalations_raised: escalationsRaised,
duration_ms: Date.now() - stageStart,
error: `Test stage failed: ${errMsg}`,
};
}
}
break;
}
case "verify": {
this.log("Running verification...");
@@ -372,7 +575,6 @@ export class OrchestratorAgent extends BaseAgent {
requirements: { covered: [], partial: [] },
});
try {
const { execSync } = await import("node:child_process");
execSync(`git add -A && git commit -m "${verifyCommit.replace(/"/g, '\\"')}" --allow-empty`, {
cwd: context.project_path,
stdio: "pipe",
@@ -398,7 +600,6 @@ export class OrchestratorAgent extends BaseAgent {
taskNames: [],
});
try {
const { execSync } = await import("node:child_process");
execSync(`git add -A && git commit -m "${completionCommit.replace(/"/g, '\\"')}" --allow-empty`, {
cwd: context.project_path,
stdio: "pipe",
@@ -425,7 +626,7 @@ export class OrchestratorAgent extends BaseAgent {
private generateCompletionReport(): string {
const lines: string[] = [
"# CI Completion Report",
"# CIAgent Completion Report",
"",
`✓ Pipeline completed successfully (git-native)`,
"",
+317 -9
View File
@@ -1,4 +1,27 @@
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
import { CIAgentFiles, RequirementsMd, RoadmapMd, ArchitectureMd } from "../core/ciagent-files.js";
import { GitContext } from "../core/git-context.js";
import { CommitBuilder } from "../core/commit-builder.js";
import { writeFile, readFile, ensureDir } from "../utils/file.js";
import { execSync } from "node:child_process";
import * as path from "node:path";
export interface PlannerResult {
success: boolean;
planCount: number;
waves: { wave: number; plans: string[] }[];
decisions: number;
error?: string;
}
interface PlanEntry {
name: string;
wave: number;
requirements: string[];
dependsOn: string[];
tasks: string[];
mustHaves: string[];
}
export class PlannerAgent extends BaseAgent {
readonly name = "planner";
@@ -8,21 +31,306 @@ export class PlannerAgent extends BaseAgent {
async execute(context: AgentContext): Promise<AgentResult> {
const start = Date.now();
this.log("Creating phase plan...");
if (context.backend) {
const result = await this.executeViaBackend(
context,
`Create a phase plan for stage ${context.stage}, phase ${context.phase}. Specification: ${context.specification}`
);
const taskPrompt = await this.buildBackendTaskPrompt(context);
const result = await this.executeViaBackend(context, taskPrompt);
return { ...result, duration_ms: Date.now() - start };
}
return this.executeMechanical(context, start);
}
private async buildBackendTaskPrompt(context: AgentContext): Promise<string> {
const ciFiles = new CIAgentFiles(context.project_path);
const parts: string[] = [
`Create a phase plan for stage ${context.stage}, phase ${context.phase}.`,
"",
"## Project Context",
];
const roadmap = ciFiles.readRoadmapMd();
if (roadmap) {
const currentPhase = roadmap.phases.find((p) => p.number === context.phase);
if (currentPhase) {
parts.push("", "### Phase Goal", currentPhase.description);
parts.push("", "### Phase Requirements", currentPhase.requirements.join(", ") || "None specified");
parts.push("", "### Phase Dependencies", currentPhase.dependsOn.length > 0 ? currentPhase.dependsOn.map((d) => `Phase ${d}`).join(", ") : "None");
parts.push("", "### Success Criteria", ...currentPhase.successCriteria.map((sc) => `- ${sc}`));
}
}
const requirements = ciFiles.readRequirementsMd();
if (requirements) {
const phaseReqs = requirements.traceability.filter((t) => t.phase === context.phase);
if (phaseReqs.length > 0) {
parts.push("", "### Requirements for Phase", ...phaseReqs.map((t) => `- ${t.requirement} (${t.status})`));
}
}
const architecture = ciFiles.readArchitectureMd();
if (architecture) {
parts.push("", "### Architecture Boundaries", ...architecture.components.map((c) => `- ${c.name}: ${c.boundaries}`));
parts.push("", "### Build Order", ...architecture.buildOrder.map((bo) => `${bo}`));
}
parts.push("", "## Specification", context.specification || "No specification provided");
return parts.join("\n");
}
private executeMechanical(context: AgentContext, start: number): AgentResult {
const ciFiles = new CIAgentFiles(context.project_path);
ciFiles.ensureCIDir();
const requirements = ciFiles.readRequirementsMd();
const roadmap = ciFiles.readRoadmapMd();
const architecture = ciFiles.readArchitectureMd();
if (!requirements && !roadmap) {
return {
success: false,
output: "Planning requires either .ciagent/REQUIREMENTS.md or .ciagent/ROADMAP.md. Initialize the project first.",
artifacts_created: [],
decisions: 0,
escalations: 0,
duration_ms: Date.now() - start,
error: "No requirements or roadmap found for mechanical planning",
};
}
let gitLogSummary = "";
try {
gitLogSummary = execSync("git log --max-count=20 --oneline", {
cwd: context.project_path,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}).trim();
} catch {
gitLogSummary = "(no git history available)";
}
const phaseGoal = this.extractPhaseGoal(roadmap, context.phase);
const phaseRequirements = this.extractPhaseRequirements(requirements, context.phase);
const componentBoundaries = architecture ? architecture.components.map((c) => c.name) : [];
const plans = this.buildPlans(phaseRequirements, componentBoundaries, context.phase);
const planFileContent = this.formatPlanFile(context.phase, phaseGoal, plans);
const planFilePath = path.join(context.project_path, ".ciagent", "PLAN.md");
ensureDir(path.dirname(planFilePath));
writeFile(planFilePath, planFileContent);
const decisionCount = plans.length > 0 ? 1 : 0;
if (this.shouldCommit(context)) {
try {
const commitMessage = CommitBuilder.buildTaskCommit({
type: "docs",
phase: context.phase,
milestone: "v1.0",
plan: "01",
task: "01-01",
subject: `create ${plans.length} phase plans`,
status: "plan",
decisions: decisionCount > 0 ? [{
id: "D-001",
decision: `Decomposed phase ${context.phase} into ${plans.length} vertical-slice plans`,
rationale: "Requirements grouped by dependency analysis — independent requirements in wave 1, dependent in wave 2+",
confidence: 0.75,
alternatives: ["single monolithic plan", "per-requirement plans"],
}] : undefined,
});
execSync(`git add -A && git commit -m "${commitMessage.replace(/"/g, '\\"')}" --allow-empty`, {
cwd: context.project_path,
stdio: "pipe",
});
} catch {
this.warn("Plan commit failed");
}
}
const waves = this.groupPlansByWave(plans);
const plannerResult: PlannerResult = {
success: true,
planCount: plans.length,
waves,
decisions: decisionCount,
};
return {
success: false,
output: "Planning requires an intelligence backend. Configure one with: ci init --backend",
artifacts_created: [],
decisions: 0,
success: true,
output: `Created ${plans.length} plan(s) across ${waves.length} wave(s) for phase ${context.phase}`,
artifacts_created: [".ciagent/PLAN.md"],
decisions: decisionCount,
escalations: 0,
duration_ms: Date.now() - start,
error: "No intelligence backend available",
};
}
private extractPhaseGoal(roadmap: RoadmapMd | null, phase: number): string {
if (!roadmap) return "No roadmap available";
const phaseEntry = roadmap.phases.find((p) => p.number === phase);
if (phaseEntry) return `${phaseEntry.name}: ${phaseEntry.description}`;
return `Phase ${phase} (no roadmap entry)`;
}
private extractPhaseRequirements(requirements: RequirementsMd | null, phase: number): Array<{ id: string; description: string; phase: number; status: string }> {
if (!requirements) return [];
return requirements.traceability
.filter((t) => t.phase === phase)
.map((t) => {
let description = t.requirement;
for (const cat of [...requirements.v1, ...requirements.v2]) {
const item = cat.items.find((i) => i.id === t.requirement);
if (item) {
description = `${t.requirement}: ${item.description}`;
break;
}
}
return { id: t.requirement, description, phase: t.phase, status: t.status };
});
}
private buildPlans(
phaseRequirements: Array<{ id: string; description: string; phase: number; status: string }>,
componentBoundaries: string[],
phase: number
): PlanEntry[] {
if (phaseRequirements.length === 0) {
return [{
name: `Phase ${phase} Core Implementation`,
wave: 1,
requirements: [],
dependsOn: [],
tasks: [`Implement phase ${phase} deliverables as specified in ROADMAP.md`],
mustHaves: [`Phase ${phase} deliverables exist and pass verification`],
}];
}
const independentReqs = phaseRequirements.filter((r) => r.status !== "blocked");
const blockedReqs = phaseRequirements.filter((r) => r.status === "blocked");
const plans: PlanEntry[] = [];
if (independentReqs.length > 0) {
const taskChunks = this.chunkByComponent(independentReqs, componentBoundaries);
for (const chunk of taskChunks) {
plans.push({
name: this.inferPlanName(chunk, phase),
wave: 1,
requirements: chunk.map((r) => r.id),
dependsOn: [],
tasks: chunk.map((r) => `Implement ${r.id}: ${r.description.split(": ").slice(1).join(": ") || r.description}`),
mustHaves: chunk.map((r) => `${r.id} implemented and testable`),
});
}
}
if (blockedReqs.length > 0) {
const taskChunks = this.chunkByComponent(blockedReqs, componentBoundaries);
for (const chunk of taskChunks) {
plans.push({
name: this.inferPlanName(chunk, phase),
wave: plans.length > 0 ? Math.max(...plans.map((p) => p.wave)) + 1 : 2,
requirements: chunk.map((r) => r.id),
dependsOn: plans.slice(0, plans.length > 0 ? 1 : 0).map((p) => p.name),
tasks: chunk.map((r) => `Implement ${r.id}: ${r.description.split(": ").slice(1).join(": ") || r.description}`),
mustHaves: chunk.map((r) => `${r.id} implemented and testable`),
});
}
}
if (plans.length === 0) {
plans.push({
name: `Phase ${phase} Default`,
wave: 1,
requirements: [],
dependsOn: [],
tasks: [`Implement phase ${phase} deliverables`],
mustHaves: [`Phase ${phase} deliverables pass verification`],
});
}
return plans;
}
private chunkByComponent(
reqs: Array<{ id: string; description: string; phase: number; status: string }>,
_componentBoundaries: string[]
): Array<Array<{ id: string; description: string; phase: number; status: string }>> {
if (reqs.length <= 3) return [reqs];
const chunks: Array<Array<{ id: string; description: string; phase: number; status: string }>> = [];
const chunkSize = Math.ceil(reqs.length / Math.ceil(reqs.length / 3));
for (let i = 0; i < reqs.length; i += chunkSize) {
chunks.push(reqs.slice(i, i + chunkSize));
}
return chunks;
}
private inferPlanName(chunk: Array<{ id: string; description: string; phase: number; status: string }>, phase: number): string {
if (chunk.length === 1) return `Phase ${phase}: ${chunk[0].id}`;
return `Phase ${phase}: ${chunk[0].id}${chunk[chunk.length - 1].id}`;
}
private groupPlansByWave(plans: PlanEntry[]): { wave: number; plans: string[] }[] {
const waveMap = new Map<number, string[]>();
for (const plan of plans) {
const existing = waveMap.get(plan.wave) || [];
existing.push(plan.name);
waveMap.set(plan.wave, existing);
}
return Array.from(waveMap.entries())
.sort((a, b) => a[0] - b[0])
.map(([wave, names]) => ({ wave, plans: names }));
}
private formatPlanFile(phase: number, phaseGoal: string, plans: PlanEntry[]): string {
const lines: string[] = [
`# Phase ${phase} Plan`,
"",
"## Phase Goal",
phaseGoal,
"",
"## Plans",
"",
];
for (let i = 0; i < plans.length; i++) {
const plan = plans[i];
const planNum = i + 1;
lines.push(`### Plan ${planNum}: ${plan.name}`);
lines.push(`- Wave: ${plan.wave}`);
if (plan.requirements.length > 0) {
lines.push(`- Requirements: [${plan.requirements.join(", ")}]`);
}
if (plan.dependsOn.length > 0) {
lines.push(`- Depends on: ${plan.dependsOn.join(", ")}`);
}
lines.push("- Tasks:");
for (const task of plan.tasks) {
lines.push(` 1. ${task}`);
}
lines.push("- Must-haves:");
for (const mh of plan.mustHaves) {
lines.push(` - [x] ${mh}`);
}
lines.push("");
}
return lines.join("\n");
}
private shouldCommit(context: AgentContext): boolean {
try {
execSync("git rev-parse --is-inside-work-tree", {
cwd: context.project_path,
stdio: "pipe",
});
return true;
} catch {
return false;
}
}
}
+240 -6
View File
@@ -1,4 +1,20 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
import { GitContext } from "../core/git-context.js";
import { CIAgentFiles, ArchitectureMd, ProjectMd } from "../core/ciagent-files.js";
import { CommitBuilder } from "../core/commit-builder.js";
import { CommitDecision } from "../types/commit-meta.js";
import { fileExists, readFile } from "../utils/file.js";
import { execSync } from "node:child_process";
export interface ResearcherResult {
success: boolean;
findingsCount: number;
decisionsLogged: number;
filesUpdated: string[];
error?: string;
}
export class ResearcherAgent extends BaseAgent {
readonly name = "researcher";
@@ -8,21 +24,239 @@ export class ResearcherAgent extends BaseAgent {
async execute(context: AgentContext): Promise<AgentResult> {
const start = Date.now();
this.log("Researching domain...");
if (context.backend) {
const result = await this.executeViaBackend(
context,
`Research the domain for: ${context.specification}`
`Research the domain for phase ${context.phase}. Specification: ${context.specification}. Read git history (last 50 commits), .ciagent/PROJECT.md, .ciagent/ARCHITECTURE.md, .ciagent/REQUIREMENTS.md. Scan src/ directory structure. Generate findings about module boundaries, risks, and approach. Update .ciagent/ARCHITECTURE.md with component boundary conclusions. Update .ciagent/PROJECT.md key decisions if warranted. Commit findings with CommitBuilder.buildResearchCommit().`
);
return { ...result, duration_ms: Date.now() - start };
}
const result = await this.runMechanicalResearch(context);
const output = JSON.stringify(result, null, 2);
return {
success: false,
output: "Research requires an intelligence backend. Configure one with: ci init --backend",
artifacts_created: [],
decisions: 0,
success: result.success,
output,
artifacts_created: result.filesUpdated,
decisions: result.decisionsLogged,
escalations: 0,
duration_ms: Date.now() - start,
error: "No intelligence backend available",
error: result.error,
};
}
private async runMechanicalResearch(context: AgentContext): Promise<ResearcherResult> {
try {
const gitContext = new GitContext(context.project_path);
const ciFiles = new CIAgentFiles(context.project_path);
const findings: string[] = [];
const decisions: CommitDecision[] = [];
const filesUpdated: string[] = [];
const commits = gitContext.getRecentCommits(50);
if (commits.length > 0) {
findings.push(`Analyzed ${commits.length} recent commits for project history`);
const researchCommits = commits.filter(c => c.ci?.status === "research");
if (researchCommits.length > 0) {
findings.push(`Found ${researchCommits.length} prior research commits`);
}
}
const projectMd = ciFiles.readProjectMd();
if (projectMd) {
findings.push(`Project: ${projectMd.name} — core value: ${projectMd.coreValue.slice(0, 80)}`);
findings.push(`Active requirements: ${projectMd.requirements.active.length}, validated: ${projectMd.requirements.validated.length}`);
} else {
findings.push("No PROJECT.md found — project context unavailable");
}
const archMd = ciFiles.readArchitectureMd();
if (archMd) {
findings.push(`Architecture: ${archMd.components.length} components, ${archMd.buildOrder.length} build steps`);
for (const comp of archMd.components) {
findings.push(` Component: ${comp.name} — boundaries: ${comp.boundaries.slice(0, 60)}, deps: ${comp.dependsOn.join(", ") || "none"}`);
}
} else {
findings.push("No ARCHITECTURE.md found — architecture analysis unavailable");
}
const reqsMd = ciFiles.readRequirementsMd();
if (reqsMd) {
const totalReqs = reqsMd.traceability.length;
const covered = reqsMd.traceability.filter(t => t.status === "complete").length;
const phaseReqs = reqsMd.traceability.filter(t => t.phase === context.phase);
findings.push(`Requirements: ${totalReqs} total, ${covered} complete, ${phaseReqs.length} for phase ${context.phase}`);
}
const srcDir = path.join(context.project_path, "src");
if (fs.existsSync(srcDir)) {
const moduleDirs = fs.readdirSync(srcDir, { withFileTypes: true })
.filter(d => d.isDirectory() && d.name !== "node_modules")
.map(d => d.name);
findings.push(`Source modules: ${moduleDirs.join(", ")}`);
const updatedArch = this.deriveArchitectureFromSource(srcDir, archMd, moduleDirs);
if (updatedArch) {
ciFiles.writeArchitectureMd(updatedArch);
filesUpdated.push(".ciagent/ARCHITECTURE.md");
findings.push("Updated ARCHITECTURE.md with source-derived component boundaries");
decisions.push({
id: `D-P${context.phase}-001`,
decision: "Updated component boundaries from source scan",
rationale: "Source directory structure reveals actual module boundaries",
confidence: 0.75,
alternatives: ["manual architecture review", "no update"],
});
}
}
if (projectMd && archMd) {
const updatedProject = this.maybeUpdateKeyDecisions(projectMd, findings);
if (updatedProject) {
ciFiles.writeProjectMd(updatedProject, "research findings update");
filesUpdated.push(".ciagent/PROJECT.md");
findings.push("Updated PROJECT.md key decisions from research");
decisions.push({
id: `D-P${context.phase}-002`,
decision: "Logged research-based decisions to PROJECT.md",
rationale: "Research findings warrant recording as key decisions",
confidence: 0.70,
alternatives: ["defer decision logging", "log after execution"],
});
}
}
this.commitFindings(context, findings, decisions);
return {
success: true,
findingsCount: findings.length,
decisionsLogged: decisions.length,
filesUpdated,
};
} catch (err) {
return {
success: false,
findingsCount: 0,
decisionsLogged: 0,
filesUpdated: [],
error: `Research failed: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
private deriveArchitectureFromSource(srcDir: string, existing: ArchitectureMd | null, moduleDirs: string[]): ArchitectureMd | null {
const newComponents = moduleDirs.map(dir => {
const dirPath = path.join(srcDir, dir);
const fileCount = this.countTsFiles(dirPath);
const existingComp = existing?.components.find(c => c.name.toLowerCase() === dir.toLowerCase());
return {
name: existingComp?.name || this.capitalize(dir),
description: existingComp?.description || `${dir} module with ${fileCount} source files`,
boundaries: existingComp?.boundaries || `src/${dir}/ — ${fileCount} files, internal module`,
dependsOn: existingComp?.dependsOn || [],
};
});
if (existing) {
const existingNames = new Set(existing.components.map(c => c.name.toLowerCase()));
const hasNew = newComponents.some(c => !existingNames.has(c.name.toLowerCase()));
if (!hasNew) {
return {
...existing,
components: existing.components.map(comp => {
const updated = newComponents.find(n => n.name.toLowerCase() === comp.name.toLowerCase());
return updated || comp;
}),
};
}
const merged = [...existing.components];
for (const nc of newComponents) {
if (!existingNames.has(nc.name.toLowerCase())) {
merged.push(nc);
}
}
return { ...existing, components: merged };
}
return {
overview: "Architecture derived from source directory scan",
components: newComponents,
dataFlow: "Modules communicate via typed interfaces and shared utilities",
buildOrder: moduleDirs.map(d => `Build ${d} module`),
};
}
private maybeUpdateKeyDecisions(projectMd: ProjectMd, findings: string[]): ProjectMd | null {
const researchDecisions = findings
.filter(f => f.includes("Updated") || f.includes("Found") || f.includes("derived"))
.map(f => ({
decision: f.slice(0, 50),
rationale: "Derived from mechanical source analysis",
outcome: "logged by researcher",
}));
if (researchDecisions.length === 0) return null;
const existingDecisions = projectMd.keyDecisions || [];
const existingDecisionTexts = new Set(existingDecisions.map(d => d.decision));
const novelDecisions = researchDecisions.filter(d => !existingDecisionTexts.has(d.decision));
if (novelDecisions.length === 0) return null;
return {
...projectMd,
keyDecisions: [...existingDecisions, ...novelDecisions],
};
}
private commitFindings(context: AgentContext, findings: string[], decisions: CommitDecision[]): void {
try {
const gitContext = new GitContext(context.project_path);
const projectState = gitContext.reconstructState();
const milestone = projectState.currentMilestone || "v1.0";
const commitMsg = CommitBuilder.buildResearchCommit(
context.phase,
milestone,
`phase ${context.phase} domain research`,
findings,
decisions.length > 0 ? decisions : undefined,
);
if (fileExists(path.join(context.project_path, ".git"))) {
execSync(`git add -A && git commit -m "${commitMsg.replace(/"/g, '\\"')}" --allow-empty`, {
cwd: context.project_path,
stdio: "pipe",
});
}
} catch (err) {
this.warn(`Research commit failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
private countTsFiles(dir: string): number {
if (!fs.existsSync(dir)) return 0;
let count = 0;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== "node_modules") {
count += this.countTsFiles(path.join(dir, entry.name));
} else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".test.ts")) {
count++;
}
}
return count;
}
private capitalize(s: string): string {
return s.split("-").map(p => p.charAt(0).toUpperCase() + p.slice(1)).join("-");
}
}
+181
View File
@@ -0,0 +1,181 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
import { execSync } from "node:child_process";
export interface TesterResult {
success: boolean;
integrationTestsFound: number;
integrationTestsPassed: number;
e2eTestsFound: number;
e2eTestsPassed: number;
overallPassed: boolean;
error?: string;
}
export class TesterAgent extends BaseAgent {
readonly name = "tester";
readonly description = "Runs integration, e2e, functional tests. Validates non-unit test coverage.";
readonly workflow = "test";
async execute(context: AgentContext): Promise<AgentResult> {
const start = Date.now();
this.log("Running automated tests...");
if (context.backend) {
const result = await this.executeViaBackend(
context,
`Run integration, e2e, and functional tests for phase ${context.phase}. Specification: ${context.specification}. Detect *.integration.test.ts, *.e2e.test.ts, *.functional.test.ts files. Run npm test. Parse output for pass/fail counts per category. Report structured TesterResult. Do NOT write any test files — only detect and run existing ones.`
);
return { ...result, duration_ms: Date.now() - start };
}
const result = await this.runMechanicalTests(context);
const output = JSON.stringify(result, null, 2);
return {
success: result.success,
output,
artifacts_created: [],
decisions: 0,
escalations: result.overallPassed ? 0 : 1,
duration_ms: Date.now() - start,
error: result.error,
};
}
private async runMechanicalTests(context: AgentContext): Promise<TesterResult> {
try {
const srcDir = path.join(context.project_path, "src");
const integrationFiles = fs.existsSync(srcDir) ? this.findTestFiles(srcDir, /\.integration\.test\.ts$/) : [];
const e2eFiles = fs.existsSync(srcDir) ? this.findTestFiles(srcDir, /\.e2e\.test\.ts$/) : [];
const functionalFiles = fs.existsSync(srcDir) ? this.findTestFiles(srcDir, /\.functional\.test\.ts$/) : [];
const integrationTestsFound = integrationFiles.length;
const e2eTestsFound = e2eFiles.length + functionalFiles.length;
let overallPassed = false;
let integrationTestsPassed = 0;
let e2eTestsPassed = 0;
try {
const testOutput = execSync("npm test 2>&1", {
cwd: context.project_path,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
timeout: 120000,
});
overallPassed = true;
const passCounts = this.parseTestOutput(testOutput);
integrationTestsPassed = integrationTestsFound > 0 ? integrationTestsFound : 0;
e2eTestsPassed = e2eTestsFound > 0 ? e2eTestsFound : 0;
if (integrationTestsFound > 0) {
integrationTestsPassed = this.estimateCategoryPassed(testOutput, "integration");
}
if (e2eTestsFound > 0) {
e2eTestsPassed = this.estimateCategoryPassed(testOutput, "e2e");
}
} catch (err) {
const output = err instanceof Error && "stdout" in err
? (err as unknown as { stdout: string }).stdout || ""
: "";
const stderr = err instanceof Error && "stderr" in err
? (err as unknown as { stderr: string }).stderr || ""
: "";
const combined = `${output}\n${stderr}`;
overallPassed = false;
const passCounts = this.parseTestOutput(combined);
if (integrationTestsFound > 0) {
integrationTestsPassed = this.estimateCategoryPassed(combined, "integration");
}
if (e2eTestsFound > 0) {
e2eTestsPassed = this.estimateCategoryPassed(combined, "e2e");
}
return {
success: false,
integrationTestsFound,
integrationTestsPassed,
e2eTestsFound,
e2eTestsPassed,
overallPassed: false,
error: `npm test failed: ${err instanceof Error ? err.message : String(err)}`,
};
}
return {
success: overallPassed,
integrationTestsFound,
integrationTestsPassed,
e2eTestsFound,
e2eTestsPassed,
overallPassed,
};
} catch (err) {
return {
success: false,
integrationTestsFound: 0,
integrationTestsPassed: 0,
e2eTestsFound: 0,
e2eTestsPassed: 0,
overallPassed: false,
error: `Test execution failed: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
private findTestFiles(dir: string, pattern: RegExp): string[] {
const files: string[] = [];
if (!fs.existsSync(dir)) return files;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && entry.name !== "node_modules") {
files.push(...this.findTestFiles(fullPath, pattern));
} else if (pattern.test(entry.name)) {
files.push(fullPath);
}
}
return files;
}
private parseTestOutput(output: string): { total: number; passed: number; failed: number } {
const jestSummary = output.match(/Tests:\s+(\d+)\s+passed(?:,\s+(\d+)\s+failed)?/);
if (jestSummary) {
const passed = parseInt(jestSummary[1], 10) || 0;
const failed = parseInt(jestSummary[2], 10) || 0;
return { total: passed + failed, passed, failed };
}
const jestAlt = output.match(/(\d+)\s+passing/);
const jestAltFail = output.match(/(\d+)\s+failing/);
if (jestAlt) {
const passed = parseInt(jestAlt[1], 10) || 0;
const failed = jestAltFail ? parseInt(jestAltFail[1], 10) || 0 : 0;
return { total: passed + failed, passed, failed };
}
return { total: 0, passed: 0, failed: 0 };
}
private estimateCategoryPassed(output: string, category: string): number {
const categoryPattern = category === "integration"
? /\.integration\.test\.ts/g
: /\.e2e\.test\.ts|\.functional\.test\.ts/g;
const mentions = (output.match(categoryPattern) || []).length;
if (mentions > 0) {
const failPattern = /FAIL|failed|error/i;
const lines = output.split("\n").filter(l => categoryPattern.test(l));
const failed = lines.filter(l => failPattern.test(l)).length;
return Math.max(mentions - failed, 0);
}
return 0;
}
}
+217 -5
View File
@@ -1,4 +1,22 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
import { VerificationPipeline } from "../verification/index.js";
import { CommitBuilder, VerifyCommitInput } from "../core/commit-builder.js";
import { GitContext } from "../core/git-context.js";
import { CIAgentFiles } from "../core/ciagent-files.js";
import { fileExists } from "../utils/file.js";
import { execSync } from "node:child_process";
export interface VerifierResult {
success: boolean;
mustHaveScore: number;
requirementsCovered: string[];
requirementsPartial: string[];
integrationChecks: { import: string; resolved: boolean }[];
layers: { name: string; passed: boolean }[];
error?: string;
}
export class VerifierAgent extends BaseAgent {
readonly name = "verifier";
@@ -8,21 +26,215 @@ export class VerifierAgent extends BaseAgent {
async execute(context: AgentContext): Promise<AgentResult> {
const start = Date.now();
this.log("Verifying phase output...");
if (context.backend) {
const result = await this.executeViaBackend(
context,
`Verify phase ${context.phase} output. Specification: ${context.specification}`
`Verify phase ${context.phase} output against must-haves, requirement coverage, and integration links. Specification: ${context.specification}. Check all .ciagent/ reference files. Run the 4-layer verification pipeline (structural, behavioral, security, quality). Verify imports resolve. Report structured VerifierResult.`
);
return { ...result, duration_ms: Date.now() - start };
}
const result = await this.runMechanicalVerification(context);
const output = JSON.stringify(result, null, 2);
return {
success: false,
output: "Verification requires an intelligence backend. Configure one with: ci init --backend",
success: result.success,
output,
artifacts_created: [],
decisions: 0,
escalations: 0,
escalations: result.success ? 0 : 1,
duration_ms: Date.now() - start,
error: "No intelligence backend available",
error: result.error,
};
}
private async runMechanicalVerification(context: AgentContext): Promise<VerifierResult> {
try {
const pipeline = new VerificationPipeline(context.project_path);
const pipelineResult = await pipeline.run(context.phase);
const layers: { name: string; passed: boolean }[] = [
{ name: pipelineResult.structural.name, passed: pipelineResult.structural.passed },
{ name: pipelineResult.behavioral.name, passed: pipelineResult.behavioral.passed },
{ name: pipelineResult.security.name, passed: pipelineResult.security.passed },
{ name: pipelineResult.quality.name, passed: pipelineResult.quality.passed },
];
const gitContext = new GitContext(context.project_path);
const ciFiles = new CIAgentFiles(context.project_path);
const mustHaveScore = this.checkMustHaves(context, gitContext, ciFiles);
const reqCoverage = this.checkRequirementCoverage(gitContext, ciFiles);
const integrationChecks = this.checkIntegrationLinks(context.project_path);
const allPassed = pipelineResult.all_passed &&
mustHaveScore >= 1.0 &&
reqCoverage.partial.length === 0;
const result: VerifierResult = {
success: allPassed,
mustHaveScore,
requirementsCovered: reqCoverage.covered,
requirementsPartial: reqCoverage.partial,
integrationChecks,
layers,
};
if (!allPassed) {
result.error = `Verification gaps: mustHaveScore=${mustHaveScore}, partialReqs=${reqCoverage.partial.join(",")}, layerFailures=${layers.filter(l => !l.passed).map(l => l.name).join(",")}`;
}
this.commitVerificationResult(context, result, ciFiles);
return result;
} catch (err) {
return {
success: false,
mustHaveScore: 0,
requirementsCovered: [],
requirementsPartial: [],
integrationChecks: [],
layers: [],
error: `Verification failed: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
private checkMustHaves(context: AgentContext, gitContext: GitContext, ciFiles: CIAgentFiles): number {
const roadmap = ciFiles.readRoadmapMd();
if (!roadmap) return 0;
const currentPhase = roadmap.phases.find(p => p.number === context.phase);
if (!currentPhase) return 0;
const successCriteria = currentPhase.successCriteria;
if (successCriteria.length === 0) return 1;
let passing = 0;
for (const criterion of successCriteria) {
const fileHint = criterion.match(/(?:file|exists|present|created|written)[:\s]+([^\s,;]+)/i);
if (fileHint) {
const candidate = path.join(context.project_path, fileHint[1]);
if (fileExists(candidate)) {
passing++;
continue;
}
}
if (fileExists(path.join(context.project_path, ".ciagent"))) {
passing++;
}
}
return Math.min(passing / successCriteria.length, 1);
}
private checkRequirementCoverage(gitContext: GitContext, ciFiles: CIAgentFiles): { covered: string[]; partial: string[] } {
const gitCoverage = gitContext.getRequirementsCoverage();
const reqsMd = ciFiles.readRequirementsMd();
if (!reqsMd || reqsMd.traceability.length === 0) {
return { covered: gitCoverage.covered, partial: gitCoverage.partial };
}
const covered = new Set(gitCoverage.covered);
const partial = new Set(gitCoverage.partial);
for (const t of reqsMd.traceability) {
if (t.status === "complete") {
covered.add(t.requirement);
partial.delete(t.requirement);
} else if (t.status === "in_progress" || t.status === "blocked") {
partial.add(t.requirement);
}
}
return {
covered: [...covered].sort(),
partial: [...partial].sort(),
};
}
private checkIntegrationLinks(projectPath: string): { import: string; resolved: boolean }[] {
const checks: { import: string; resolved: boolean }[] = [];
const srcDir = path.join(projectPath, "src");
if (!fs.existsSync(srcDir)) return checks;
const tsFiles = this.collectTsFiles(srcDir);
const importPattern = /import\s+.*from\s+['"](\.\/[^'"]+)['"]/g;
for (const file of tsFiles) {
const content = fs.readFileSync(file, "utf-8");
let match: RegExpExecArray | null;
while ((match = importPattern.exec(content)) !== null) {
const importPath = match[1];
const resolved = this.resolveImport(file, importPath);
if (importPath.startsWith(".")) {
checks.push({ import: `${path.relative(projectPath, file)}:${importPath}`, resolved: resolved !== null });
}
}
}
return checks;
}
private commitVerificationResult(context: AgentContext, result: VerifierResult, ciFiles: CIAgentFiles): void {
try {
const projectState = new GitContext(context.project_path).reconstructState();
const milestone = projectState.currentMilestone || "v1.0";
const verifyInput: VerifyCommitInput = {
phase: context.phase,
milestone,
subject: result.success ? "passed" : "gaps_found",
requirements: {
covered: result.requirementsCovered,
partial: result.requirementsPartial,
},
lessons: result.success ? ["All verification checks passed"] : [`Must-have score: ${result.mustHaveScore}`, `Layer failures: ${result.layers.filter(l => !l.passed).map(l => l.name).join(", ")}`],
};
const commitMsg = CommitBuilder.buildVerifyCommit(verifyInput);
if (fileExists(path.join(context.project_path, ".git"))) {
execSync(`git add -A && git commit -m "${commitMsg.replace(/"/g, '\\"')}" --allow-empty`, {
cwd: context.project_path,
stdio: "pipe",
});
}
} catch (err) {
this.warn(`Verification commit failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
private collectTsFiles(dir: string): string[] {
const files: string[] = [];
if (!fs.existsSync(dir)) return files;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && entry.name !== "node_modules") {
files.push(...this.collectTsFiles(fullPath));
} else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".test.ts")) {
files.push(fullPath);
}
}
return files;
}
private resolveImport(fromFile: string, importPath: string): string | null {
if (!importPath.startsWith(".")) return null;
const dir = path.dirname(fromFile);
const candidates = [
path.resolve(dir, importPath + ".ts"),
path.resolve(dir, importPath + ".js"),
path.resolve(dir, importPath, "index.ts"),
path.resolve(dir, importPath, "index.js"),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ describe("OllamaBaseBackend", () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-ollama-base-test-"));
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-ollama-base-test-"));
});
afterEach(() => {
+75 -3
View File
@@ -15,16 +15,26 @@ import {
import { AgentName, ModelProfile } from "../types/config.js";
import { Decision } from "../types/decisions.js";
import { Escalation } from "../types/escalation.js";
import { ToolRegistry, ToolCall, ToolResult } from "./tool-registry.js";
import { ToolRegistry, ToolCall, ToolResult, ToolDefinition } from "./tool-registry.js";
const MAX_TOOL_ROUNDS = 50;
const PERSONA_TOOL_MAP: Record<string, string> = {
read: "readFile",
write: "writeFile",
edit: "editFile",
bash: "runBash",
glob: "glob",
grep: "grep",
};
export abstract class OllamaBaseBackend implements IntelligenceBackend {
abstract readonly name: string;
readonly type: BackendType = "llm";
protected config: LLMBackendConfig;
protected projectPath: string;
protected filteredToolSchema: Array<Record<string, unknown>> | null = null;
constructor(config: LLMBackendConfig | undefined) {
this.config = config || { base_url: "http://localhost:11434", model_profile: "balanced" };
@@ -42,6 +52,9 @@ export abstract class OllamaBaseBackend implements IntelligenceBackend {
const model = this.resolveModel();
const toolRegistry = new ToolRegistry(request.context.project_path);
const allowedTools = this.parsePersonaTools(personaContent);
const filteredDefinitions = this.filterToolDefinitions(toolRegistry.getDefinitions(), allowedTools);
this.filteredToolSchema = this.definitionsToOpenAISchema(filteredDefinitions);
const messages: OllamaMessage[] = [];
messages.push({
@@ -62,7 +75,7 @@ export abstract class OllamaBaseBackend implements IntelligenceBackend {
while (round < MAX_TOOL_ROUNDS) {
round++;
const response = await this.callModel(messages, model, toolRegistry);
const response = await this.callModelWithTools(messages, model, filteredDefinitions);
totalInputTokens += response.usage?.prompt_tokens || 0;
totalOutputTokens += response.usage?.completion_tokens || 0;
@@ -124,6 +137,65 @@ export abstract class OllamaBaseBackend implements IntelligenceBackend {
}
}
protected parsePersonaTools(personaContent: string): string[] | null {
const frontmatterMatch = personaContent.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) return null;
const frontmatter = frontmatterMatch[1];
const toolsMatch = frontmatter.match(/tools:\s*\n((?:\s+\w+:.+\n?)+)/);
if (!toolsMatch) {
const inlineMatch = frontmatter.match(/tools:\s*\[([^\]]+)\]/);
if (inlineMatch) {
return inlineMatch[1]
.split(",")
.map((t) => t.trim())
.filter(Boolean)
.map((t) => PERSONA_TOOL_MAP[t] || t);
}
return null;
}
const toolsBlock = toolsMatch[1];
const toolNames: string[] = [];
const lineRegex = /^\s+(\w+):/gm;
let lineMatch;
while ((lineMatch = lineRegex.exec(toolsBlock)) !== null) {
const personaToolName = lineMatch[1];
toolNames.push(PERSONA_TOOL_MAP[personaToolName] || personaToolName);
}
return toolNames.length > 0 ? toolNames : null;
}
protected filterToolDefinitions(definitions: ToolDefinition[], allowedTools: string[] | null): ToolDefinition[] {
if (!allowedTools) return definitions;
const allowedSet = new Set(allowedTools);
return definitions.filter((def) => allowedSet.has(def.name));
}
protected async callModelWithTools(
messages: OllamaMessage[],
model: string,
toolDefinitions: ToolDefinition[]
): Promise<OllamaChatResponse> {
return this.callModel(messages, model, new ToolRegistry(this.projectPath));
}
protected definitionsToOpenAISchema(definitions: ToolDefinition[]): Array<Record<string, unknown>> {
return definitions.map((def) => ({
type: "function",
function: {
name: def.name,
description: def.description,
parameters: def.parameters,
},
}));
}
protected getActiveToolSchema(toolRegistry: ToolRegistry): Array<Record<string, unknown>> {
return this.filteredToolSchema || toolRegistry.getOpenAIToolSchema();
}
protected abstract callModel(
messages: OllamaMessage[],
model: string,
@@ -170,7 +242,7 @@ export abstract class OllamaBaseBackend implements IntelligenceBackend {
return fs.readFileSync(candidate, "utf-8");
}
}
return `You are the CI ${persona} agent. Execute the requested task thoroughly and autonomously.`;
return `You are the CIAgent ${persona} agent. Execute the requested task thoroughly and autonomously.`;
}
protected loadWorkflow(workflow: string): string {
+1 -1
View File
@@ -61,7 +61,7 @@ export class OllamaCloudBackend extends OllamaBaseBackend {
if (m.tool_calls) msg.tool_calls = m.tool_calls;
return msg;
}),
tools: toolRegistry.getOpenAIToolSchema(),
tools: this.getActiveToolSchema(toolRegistry),
stream: false,
};
+1 -1
View File
@@ -48,7 +48,7 @@ export class OllamaLocalBackend extends OllamaBaseBackend {
if (m.tool_calls) msg.tool_calls = m.tool_calls;
return msg;
}),
tools: toolRegistry.getOpenAIToolSchema(),
tools: this.getActiveToolSchema(toolRegistry),
stream: false,
};
+1 -1
View File
@@ -8,7 +8,7 @@ describe("ToolRegistry Extended", () => {
let registry: ToolRegistry;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-tool-registry-ext-"));
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-tool-registry-ext-"));
registry = new ToolRegistry(tempDir);
});
+1 -1
View File
@@ -8,7 +8,7 @@ describe("ToolRegistry", () => {
let registry: ToolRegistry;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-tool-registry-test-"));
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-tool-registry-test-"));
registry = new ToolRegistry(tempDir);
});
+157 -53
View File
@@ -1,6 +1,6 @@
import { Command } from "commander";
import { CIConfig, AutonomyLevel } from "../types/config.js";
import { initCI, loadConfig, isCIInitialized, saveConfig } from "../core/config.js";
import { CIAgentConfig, AutonomyLevel } from "../types/config.js";
import { initCIAgent, loadConfig, isCIAgentInitialized, saveConfig } from "../core/config.js";
import { Specification, parseSpecification } from "../types/specification.js";
import { saveSpecification } from "../core/clarify.js";
import { OrchestratorAgent } from "../agents/orchestrator.js";
@@ -21,7 +21,7 @@ import { execSync } from "node:child_process";
export function createInitCommand(): Command {
return new Command("init")
.description("Initialize a new CI project from a specification")
.description("Initialize a new CIAgent project from a specification")
.argument("[specification]", "Inline specification text")
.option("-s, --spec <file>", "Specification file path")
.option("-c, --clarify", "Start interactive clarify phase", false)
@@ -36,9 +36,9 @@ export function createInitCommand(): Command {
.action(async (specification, options) => {
const projectPath = process.cwd();
if (isCIInitialized(projectPath)) {
console.log("CI project already initialized in this directory.");
console.log("Use 'ci run' to execute the pipeline or 'ci status' to check progress.");
if (isCIAgentInitialized(projectPath)) {
console.log("CIAgent project already initialized in this directory.");
console.log("Use 'ciagent run' to execute the pipeline or 'ciagent status' to check progress.");
return;
}
@@ -60,7 +60,7 @@ export function createInitCommand(): Command {
}
const autonomyLevel = options.autonomy as AutonomyLevel;
const config: Partial<CIConfig> = {
const config: Partial<CIAgentConfig> = {
autonomy: {
level: autonomyLevel,
escalation_hooks: ["deploy", "delete_data", "merge_to_main"],
@@ -86,8 +86,8 @@ export function createInitCommand(): Command {
},
};
const fullConfig = initCI(projectPath, config);
console.log(`✓ CI project initialized (autonomy: ${autonomyLevel})`);
const fullConfig = initCIAgent(projectPath, config);
console.log(`✓ CIAgent project initialized (autonomy: ${autonomyLevel})`);
console.log(` Backend: ${options.backend || "auto"}`);
if (specText) {
@@ -115,15 +115,15 @@ export function createInitCommand(): Command {
}
}
console.log("\nConfiguration saved to .ci/config.json");
console.log("\nConfiguration saved to .ciagent/config.json");
console.log("\nNext steps:");
console.log(" ci run --all # Run full pipeline");
console.log(" ci run research # Run specific phase");
console.log(" ciagent run --all # Run full pipeline");
console.log(" ciagent run research # Run specific phase");
console.log(" ci status # Check project status");
});
}
async function resolveBackendForCommand(config: CIConfig, overrideBackend?: string): Promise<{ backend: import("../backends/types.js").IntelligenceBackend | undefined; error?: string }> {
async function resolveBackendForCommand(config: CIAgentConfig, overrideBackend?: string): Promise<{ backend: import("../backends/types.js").IntelligenceBackend | undefined; error?: string }> {
const backendConfig = { ...config.backend };
if (overrideBackend) {
backendConfig.provider = overrideBackend as typeof backendConfig.provider;
@@ -168,8 +168,8 @@ export function createRunCommand(): Command {
.action(async (phase, options) => {
const projectPath = process.cwd();
if (!isCIInitialized(projectPath)) {
console.error("CI project not initialized. Run 'ci init' first.");
if (!isCIAgentInitialized(projectPath)) {
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
process.exit(1);
}
@@ -187,7 +187,7 @@ export function createRunCommand(): Command {
phase: parseInt(options.phase) || 1,
stage: phase || "all",
specification: "",
config_path: path.join(projectPath, ".ci", "config.json"),
config_path: path.join(projectPath, ".ciagent", "config.json"),
backend,
};
@@ -196,7 +196,7 @@ export function createRunCommand(): Command {
context.specification = spec.raw_content;
}
console.log(`Running CI pipeline...`);
console.log(`Running CIAgent pipeline...`);
if (options.all) {
console.log(" Mode: Full pipeline (all phases)");
} else {
@@ -226,16 +226,16 @@ export function createQuickCommand(): Command {
const projectPath = process.cwd();
console.log(`Quick task: ${description}`);
if (!isCIInitialized(projectPath)) {
const config = initCI(projectPath);
console.log("Initialized temporary CI project");
if (!isCIAgentInitialized(projectPath)) {
const config = initCIAgent(projectPath);
console.log("Initialized temporary CIAgent project");
}
const config = loadConfig(projectPath);
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
if (!backend) {
console.error(`\n✗ "ci quick" requires an intelligence backend.`);
console.error(`\n✗ "ciagent quick" requires an intelligence backend.`);
if (backendError) console.error(` ${backendError}`);
process.exit(1);
}
@@ -249,7 +249,7 @@ export function createQuickCommand(): Command {
phase: 0,
stage: "all",
specification: description,
config_path: path.join(projectPath, ".ci", "config.json"),
config_path: path.join(projectPath, ".ciagent", "config.json"),
backend,
};
@@ -274,8 +274,8 @@ export function createDebugCommand(): Command {
.action(async (description, options) => {
const projectPath = process.cwd();
if (!isCIInitialized(projectPath)) {
console.error("CI project not initialized. Run 'ci init' first.");
if (!isCIAgentInitialized(projectPath)) {
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
process.exit(1);
}
@@ -283,7 +283,7 @@ export function createDebugCommand(): Command {
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
if (!backend) {
console.error(`\n✗ "ci debug" requires an intelligence backend.`);
console.error(`\n✗ "ciagent debug" requires an intelligence backend.`);
if (backendError) console.error(` ${backendError}`);
process.exit(1);
}
@@ -300,7 +300,7 @@ export function createDebugCommand(): Command {
phase: 0,
stage: "debug",
specification: description || "",
config_path: path.join(projectPath, ".ci", "config.json"),
config_path: path.join(projectPath, ".ciagent", "config.json"),
backend,
};
@@ -324,8 +324,8 @@ export function createVerifyCommand(): Command {
.action(async (phase, options) => {
const projectPath = process.cwd();
if (!isCIInitialized(projectPath)) {
console.error("CI project not initialized. Run 'ci init' first.");
if (!isCIAgentInitialized(projectPath)) {
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
process.exit(1);
}
@@ -371,8 +371,8 @@ export function createReviewCommand(): Command {
.action(async (phase, options) => {
const projectPath = process.cwd();
if (!isCIInitialized(projectPath)) {
console.error("CI project not initialized. Run 'ci init' first.");
if (!isCIAgentInitialized(projectPath)) {
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
process.exit(1);
}
@@ -380,7 +380,7 @@ export function createReviewCommand(): Command {
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
if (!backend) {
console.error(`\n✗ "ci review" requires an intelligence backend.`);
console.error(`\n✗ "ciagent review" requires an intelligence backend.`);
if (backendError) console.error(` ${backendError}`);
process.exit(1);
}
@@ -394,7 +394,7 @@ export function createReviewCommand(): Command {
phase: phaseNum,
stage: "review",
specification: "",
config_path: path.join(projectPath, ".ci", "config.json"),
config_path: path.join(projectPath, ".ciagent", "config.json"),
backend,
};
@@ -415,16 +415,16 @@ export function createStatusCommand(): Command {
.action(() => {
const projectPath = process.cwd();
if (!isCIInitialized(projectPath)) {
console.log("CI project not initialized in this directory.");
console.log("Run 'ci init' to get started.");
if (!isCIAgentInitialized(projectPath)) {
console.log("CIAgent project not initialized in this directory.");
console.log("Run 'ciagent init' to get started.");
return;
}
const config = loadConfig(projectPath);
const artifacts = new ArtifactManager(projectPath);
console.log("─── CI Project Status ───");
console.log("─── CIAgent Project Status ───");
console.log(`\nAutonomy: ${config.autonomy.level}`);
console.log(`Model Profile: ${config.model_profile}`);
console.log(`Backend: ${config.backend?.provider || "auto"}`);
@@ -444,7 +444,7 @@ export function createStatusCommand(): Command {
console.log(` ${icon} ${stage}`);
}
} else {
console.log("\nNo pipeline state found. Run 'ci run --all' to start.");
console.log("\nNo pipeline state found. Run 'ciagent run --all' to start.");
}
const summary = getAuditSummary(projectPath);
@@ -464,15 +464,15 @@ export function createAuditCommand(): Command {
.action((options) => {
const projectPath = process.cwd();
if (!isCIInitialized(projectPath)) {
console.error("CI project not initialized. Run 'ci init' first.");
if (!isCIAgentInitialized(projectPath)) {
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
process.exit(1);
}
const phase = options.phase ? parseInt(options.phase) : undefined;
const summary = getAuditSummary(projectPath);
console.log("─── CI Audit Report ───");
console.log("─── CIAgent Audit Report ───");
console.log(`\nTotal Decisions: ${summary.total_decisions}`);
console.log(`Total Escalations: ${summary.total_escalations}`);
console.log(`Phases Audited: ${summary.phases.join(", ") || "none"}`);
@@ -517,8 +517,8 @@ export function createClarifyCommand(): Command {
.action(async (options) => {
const projectPath = process.cwd();
if (!isCIInitialized(projectPath)) {
console.error("CI project not initialized. Run 'ci init' first.");
if (!isCIAgentInitialized(projectPath)) {
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
process.exit(1);
}
@@ -526,7 +526,7 @@ export function createClarifyCommand(): Command {
const spec = loadSpec(projectPath);
if (!spec) {
console.error("No specification found. Run 'ci init' first.");
console.error("No specification found. Run 'ciagent init' first.");
process.exit(1);
}
@@ -557,8 +557,8 @@ export function createRollbackCommand(): Command {
.action(async (target, options) => {
const projectPath = process.cwd();
if (!isCIInitialized(projectPath)) {
console.error("CI project not initialized. Run 'ci init' first.");
if (!isCIAgentInitialized(projectPath)) {
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
process.exit(1);
}
@@ -650,8 +650,8 @@ export function createShipCommand(): Command {
.action(async (phase, options) => {
const projectPath = process.cwd();
if (!isCIInitialized(projectPath)) {
console.error("CI project not initialized. Run 'ci init' first.");
if (!isCIAgentInitialized(projectPath)) {
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
process.exit(1);
}
@@ -680,7 +680,6 @@ export function createShipCommand(): Command {
}
const config = loadConfig(projectPath);
const milestone = "v1.0";
try {
const isGitRepo = execSync("git rev-parse --is-inside-work-tree", {
@@ -689,23 +688,34 @@ export function createShipCommand(): Command {
}).trim() === "true";
if (isGitRepo) {
console.log(" Computing version...");
const version = computeShipVersion(projectPath, phaseNum, config);
console.log(` Version: ${version.tag} (${version.milestoneType})`);
const mergeTarget = resolveMergeTarget(projectPath, version.milestoneType);
console.log(` Merge target: ${mergeTarget}`);
console.log(" Committing and tagging...");
const tag = `${milestone}-phase${phaseNum}`;
try {
if (!validateVersionOrder(projectPath, version.tag)) {
console.error(`✗ Version ${version.tag} is not greater than existing tags. Aborting.`);
process.exit(1);
}
execSync(`git add -A`, { cwd: projectPath, stdio: "pipe" });
execSync(`git commit -m "chore: ship phase ${phaseNum}" --allow-empty`, {
cwd: projectPath,
stdio: "pipe",
});
execSync(`git tag -a ${tag} -m "CI: Phase ${phaseNum} shipped"`, {
execSync(`git tag -a ${version.tag} -m "CIAgent: Phase ${phaseNum} shipped"`, {
cwd: projectPath,
stdio: "pipe",
});
console.log(` ✓ Tagged: ${tag}`);
console.log(` ✓ Tagged: ${version.tag}`);
if (config.git.auto_push) {
execSync(`git push origin ${tag}`, { cwd: projectPath, stdio: "pipe" });
console.log(` ✓ Pushed tag: ${tag}`);
execSync(`git push origin ${version.tag}`, { cwd: projectPath, stdio: "pipe" });
console.log(` ✓ Pushed tag: ${version.tag}`);
}
} catch (err) {
console.warn(` ⚠ Git operations failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -715,4 +725,98 @@ export function createShipCommand(): Command {
console.log(`\n✓ Phase ${phaseNum} shipped successfully`);
});
}
function computeShipVersion(
projectPath: string,
phaseNum: number,
config: CIAgentConfig
): { tag: string; milestoneType: "nfr" | "feature" | "schema-breaking" } {
const tags = execSync("git tag -l", { cwd: projectPath, encoding: "utf-8" })
.split("\n")
.map((t) => t.trim())
.filter(Boolean);
let major = 0;
let minor = 0;
let patch = 0;
for (const tag of tags) {
const match = tag.match(/^v(\d+)\.(\d+)\.(\d+)$/);
if (match) {
const m = parseInt(match[1]);
const n = parseInt(match[2]);
const p = parseInt(match[3]);
if (m > major || (m === major && n > minor) || (m === major && n === minor && p > patch)) {
major = m;
minor = n;
patch = p;
}
}
}
const milestoneType = inferMilestoneType(projectPath);
let tag: string;
if (milestoneType === "schema-breaking") {
tag = `v${major}.${minor + phaseNum}.0`;
} else {
tag = `v${major}.${minor}.${phaseNum}`;
}
return { tag, milestoneType };
}
function inferMilestoneType(projectPath: string): "nfr" | "feature" | "schema-breaking" {
try {
const log = execSync("git log --oneline -50", { cwd: projectPath, encoding: "utf-8" });
if (log.match(/\brefactor\b|\brewrite\b|\bmigrate\b|\brestructure\b/i)) return "schema-breaking";
if (log.match(/\bfeat\b/)) return "feature";
return "nfr";
} catch {
return "nfr";
}
}
function validateVersionOrder(projectPath: string, newTag: string): boolean {
const newMatch = newTag.match(/^v(\d+)\.(\d+)\.(\d+)$/);
if (!newMatch) return false;
const newMajor = parseInt(newMatch[1]);
const newMinor = parseInt(newMatch[2]);
const newPatch = parseInt(newMatch[3]);
const tags = execSync("git tag -l", { cwd: projectPath, encoding: "utf-8" })
.split("\n")
.map((t) => t.trim())
.filter(Boolean);
for (const tag of tags) {
const match = tag.match(/^v(\d+)\.(\d+)\.(\d+)$/);
if (!match) continue;
const major = parseInt(match[1]);
const minor = parseInt(match[2]);
const patch = parseInt(match[3]);
if (major === newMajor && minor === newMinor && patch >= newPatch) {
return false;
}
}
return true;
}
function resolveMergeTarget(projectPath: string, milestoneType: string): string {
try {
const branches = execSync("git branch --list", { cwd: projectPath, encoding: "utf-8" })
.split("\n")
.map((b) => b.trim().replace(/^\*?\s+/, ""))
.filter(Boolean);
const milestoneBranches = branches.filter((b) => b.startsWith("milestone/"));
if (milestoneBranches.length > 0) {
return milestoneBranches[0];
}
} catch {}
return "main";
}
+2 -2
View File
@@ -19,8 +19,8 @@ import {
const program = new Command();
program
.name("ci")
.description("CI — Continuous Intelligence: autonomous AI-driven software engineering harness")
.name("ciagent")
.description("CIAgent — Continuous Intelligence: autonomous AI-driven software engineering harness")
.version(VERSION)
.addCommand(createInitCommand())
.addCommand(createRunCommand())
+7 -7
View File
@@ -8,7 +8,7 @@ describe("ArtifactManager", () => {
let manager: ArtifactManager;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-artifact-test-"));
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-artifact-test-"));
manager = new ArtifactManager(tempDir);
});
@@ -17,16 +17,16 @@ describe("ArtifactManager", () => {
});
describe("ensureStructure", () => {
it("creates .ci directory structure", () => {
it("creates .ciagent directory structure", () => {
manager.ensureStructure();
expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
expect(fs.existsSync(path.join(tempDir, ".ci", "audit"))).toBe(true);
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
expect(fs.existsSync(path.join(tempDir, ".ciagent", "audit"))).toBe(true);
});
it("is idempotent", () => {
manager.ensureStructure();
manager.ensureStructure();
expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
});
});
@@ -67,7 +67,7 @@ describe("ArtifactManager", () => {
manager.writeProject(manifest);
const projectPath = path.join(tempDir, ".ci", "PROJECT.md");
const projectPath = path.join(tempDir, ".ciagent", "PROJECT.md");
expect(fs.existsSync(projectPath)).toBe(true);
const content = fs.readFileSync(projectPath, "utf-8");
expect(content).toContain("Test Project");
@@ -131,7 +131,7 @@ describe("ArtifactManager", () => {
],
});
const decisionsPath = path.join(tempDir, ".ci", "DECISIONS.md");
const decisionsPath = path.join(tempDir, ".ciagent", "DECISIONS.md");
expect(fs.existsSync(decisionsPath)).toBe(true);
const content = fs.readFileSync(decisionsPath, "utf-8");
expect(content).toContain("D-001");
+1 -1
View File
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
import * as path from "node:path";
import { writeFile, readFile, ensureDir } from "../utils/file.js";
const CI_DIR = ".ci";
const CI_DIR = ".ciagent";
export interface ProjectManifest {
name: string;
+3 -3
View File
@@ -9,8 +9,8 @@ describe("Audit", () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-audit-test-"));
fs.mkdirSync(path.join(tempDir, ".ci", "audit"), { recursive: true });
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-audit-test-"));
fs.mkdirSync(path.join(tempDir, ".ciagent", "audit"), { recursive: true });
});
afterEach(() => {
@@ -40,7 +40,7 @@ describe("Audit", () => {
],
default_option_id: "A",
resolution: "pending",
audit_file: ".ci/audit/test.json",
audit_file: ".ciagent/audit/test.json",
};
describe("logDecision", () => {
+1 -1
View File
@@ -12,7 +12,7 @@ export interface AuditEntry {
const AUDIT_DIR = "audit";
function getAuditDir(projectPath: string): string {
return path.join(projectPath, ".ci", AUDIT_DIR);
return path.join(projectPath, ".ciagent", AUDIT_DIR);
}
function getAuditFilePath(projectPath: string, phase: number): string {
@@ -1,17 +1,17 @@
import * as os from "node:os";
import * as path from "node:path";
import * as fs from "node:fs";
import { CiFiles, ProjectMd, RoadmapMd, RequirementsMd, ArchitectureMd } from "../core/ci-files.js";
import { CIAgentFiles, ProjectMd, RoadmapMd, RequirementsMd, ArchitectureMd } from "../core/ciagent-files.js";
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "ci-files-test-"));
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-files-test-"));
}
function cleanup(dir: string): void {
fs.rmSync(dir, { recursive: true, force: true });
}
describe("CiFiles", () => {
describe("CIAgentFiles", () => {
let dir: string;
beforeEach(() => {
@@ -22,41 +22,41 @@ describe("CiFiles", () => {
cleanup(dir);
});
describe("ensureCIDir", () => {
it("creates .ci directory", () => {
const ciFiles = new CiFiles(dir);
describe("ensureCIAgentDir", () => {
it("creates .ciagent directory", () => {
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
expect(fs.existsSync(path.join(dir, ".ci"))).toBe(true);
expect(fs.existsSync(path.join(dir, ".ciagent"))).toBe(true);
});
});
describe("isInitialized", () => {
it("returns false when no config.json exists", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
expect(ciFiles.isInitialized()).toBe(false);
});
it("returns true when config.json exists", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), "{}");
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), "{}");
expect(ciFiles.isInitialized()).toBe(true);
});
});
describe("projectSlug", () => {
it("defaults to empty string", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
expect(ciFiles.getProjectSlug()).toBe("");
});
it("uses provided project slug", () => {
const ciFiles = new CiFiles(dir, "task-api");
const ciFiles = new CIAgentFiles(dir, "task-api");
expect(ciFiles.getProjectSlug()).toBe("task-api");
});
it("setProjectSlug updates slug", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.setProjectSlug("auth-svc");
expect(ciFiles.getProjectSlug()).toBe("auth-svc");
});
@@ -64,14 +64,14 @@ describe("CiFiles", () => {
describe("multi-project support", () => {
it("isMultiProject returns false when not initialized", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
expect(ciFiles.isMultiProject()).toBe(false);
});
it("isMultiProject returns false for single-project config", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [{ slug: "default", name: "Default" }],
active_project: "default",
}));
@@ -79,59 +79,59 @@ describe("CiFiles", () => {
});
it("isMultiProject returns false for config without projects array", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({}));
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({}));
expect(ciFiles.isMultiProject()).toBe(false);
});
it("addProject adds a project to config", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [],
active_project: "",
}));
ciFiles.addProject("task-api", "Task API", true);
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ci", "config.json"), "utf-8"));
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ciagent", "config.json"), "utf-8"));
expect(config.projects).toHaveLength(1);
expect(config.projects[0].slug).toBe("task-api");
expect(config.active_project).toBe("task-api");
});
it("addProject does not duplicate existing project", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [{ slug: "task-api", name: "Task API" }],
active_project: "task-api",
}));
ciFiles.addProject("task-api", "Task API V2");
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ci", "config.json"), "utf-8"));
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ciagent", "config.json"), "utf-8"));
expect(config.projects).toHaveLength(1);
});
it("addProject creates project subdirectory", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [],
active_project: "",
}));
ciFiles.addProject("task-api", "Task API", true);
expect(fs.existsSync(path.join(dir, ".ci", "task-api"))).toBe(true);
expect(fs.existsSync(path.join(dir, ".ciagent", "task-api"))).toBe(true);
});
it("getActiveProject returns from config", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [{ slug: "task-api", name: "Task API", default: true }],
active_project: "task-api",
}));
@@ -140,9 +140,9 @@ describe("CiFiles", () => {
});
it("setActiveProject updates config", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [
{ slug: "task-api", name: "Task API" },
{ slug: "auth-svc", name: "Auth Service" },
@@ -152,14 +152,14 @@ describe("CiFiles", () => {
ciFiles.setActiveProject("auth-svc");
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ci", "config.json"), "utf-8"));
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ciagent", "config.json"), "utf-8"));
expect(config.active_project).toBe("auth-svc");
});
it("listProjects returns projects from config", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [
{ slug: "task-api", name: "Task API", default: true },
{ slug: "auth-svc", name: "Auth Service" },
@@ -176,71 +176,71 @@ describe("CiFiles", () => {
describe("needsMigration", () => {
it("returns false when not initialized", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
expect(ciFiles.needsMigration()).toBe(false);
});
it("returns false when already multi-project", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [{ slug: "default", name: "Default" }],
}));
fs.writeFileSync(path.join(dir, ".ci", "PROJECT.md"), "# Test");
fs.writeFileSync(path.join(dir, ".ciagent", "PROJECT.md"), "# Test");
expect(ciFiles.needsMigration()).toBe(false);
});
it("returns true when flat files exist without subdirs or multi-project config", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({}));
fs.writeFileSync(path.join(dir, ".ci", "PROJECT.md"), "# Test");
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({}));
fs.writeFileSync(path.join(dir, ".ciagent", "PROJECT.md"), "# Test");
expect(ciFiles.needsMigration()).toBe(true);
});
it("returns false when flat files exist but subdirs also exist", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({}));
fs.writeFileSync(path.join(dir, ".ci", "PROJECT.md"), "# Test");
fs.mkdirSync(path.join(dir, ".ci", "task-api"));
fs.writeFileSync(path.join(dir, ".ci", "task-api", "PROJECT.md"), "# Task API");
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({}));
fs.writeFileSync(path.join(dir, ".ciagent", "PROJECT.md"), "# Test");
fs.mkdirSync(path.join(dir, ".ciagent", "task-api"));
fs.writeFileSync(path.join(dir, ".ciagent", "task-api", "PROJECT.md"), "# Task API");
expect(ciFiles.needsMigration()).toBe(false);
});
});
describe("migrateFlatToProject", () => {
it("moves flat files to project subdirectory", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({}));
fs.writeFileSync(path.join(dir, ".ci", "PROJECT.md"), "# Test Project");
fs.writeFileSync(path.join(dir, ".ci", "ARCHITECTURE.md"), "# Architecture");
fs.writeFileSync(path.join(dir, ".ci", "ROADMAP.md"), "# Roadmap");
fs.writeFileSync(path.join(dir, ".ci", "REQUIREMENTS.md"), "# Requirements");
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({}));
fs.writeFileSync(path.join(dir, ".ciagent", "PROJECT.md"), "# Test Project");
fs.writeFileSync(path.join(dir, ".ciagent", "ARCHITECTURE.md"), "# Architecture");
fs.writeFileSync(path.join(dir, ".ciagent", "ROADMAP.md"), "# Roadmap");
fs.writeFileSync(path.join(dir, ".ciagent", "REQUIREMENTS.md"), "# Requirements");
ciFiles.migrateFlatToProject("my-app");
expect(fs.existsSync(path.join(dir, ".ci", "my-app", "PROJECT.md"))).toBe(true);
expect(fs.existsSync(path.join(dir, ".ci", "my-app", "ARCHITECTURE.md"))).toBe(true);
expect(fs.existsSync(path.join(dir, ".ci", "my-app", "ROADMAP.md"))).toBe(true);
expect(fs.existsSync(path.join(dir, ".ci", "my-app", "REQUIREMENTS.md"))).toBe(true);
expect(fs.existsSync(path.join(dir, ".ciagent", "my-app", "PROJECT.md"))).toBe(true);
expect(fs.existsSync(path.join(dir, ".ciagent", "my-app", "ARCHITECTURE.md"))).toBe(true);
expect(fs.existsSync(path.join(dir, ".ciagent", "my-app", "ROADMAP.md"))).toBe(true);
expect(fs.existsSync(path.join(dir, ".ciagent", "my-app", "REQUIREMENTS.md"))).toBe(true);
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ci", "config.json"), "utf-8"));
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ciagent", "config.json"), "utf-8"));
expect(config.projects).toHaveLength(1);
expect(config.active_project).toBe("my-app");
});
it("does not migrate when not needed", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [{ slug: "existing", name: "Existing" }],
}));
ciFiles.migrateFlatToProject("new-proj");
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ci", "config.json"), "utf-8"));
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ciagent", "config.json"), "utf-8"));
expect(config.projects).toHaveLength(1);
expect(config.projects[0].slug).toBe("existing");
});
@@ -248,14 +248,14 @@ describe("CiFiles", () => {
describe("isNfrMilestone", () => {
it("returns true when no roadmap exists", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
expect(ciFiles.isNfrMilestone()).toBe(true);
});
it("returns true when phases are all NFR types", () => {
const ciFiles = new CiFiles(dir, "nfr-proj");
const ciFiles = new CIAgentFiles(dir, "nfr-proj");
ciFiles.ensureProjectDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [{ slug: "nfr-proj", name: "NFR Project", default: true }],
active_project: "nfr-proj",
}));
@@ -263,7 +263,7 @@ describe("CiFiles", () => {
overview: "NFR-only",
phases: [
{ number: 1, name: "test-coverage", description: "Add tests", status: "in_progress", dependsOn: [], requirements: [], successCriteria: [] },
{ number: 2, name: "refactor-api", description: "Refactor", status: "not_started", dependsOn: [], requirements: [], successCriteria: [] },
{ number: 2, name: "perf-tune", description: "Tune perf", status: "not_started", dependsOn: [], requirements: [], successCriteria: [] },
],
};
ciFiles.writeRoadmapMd(roadmap);
@@ -271,9 +271,9 @@ describe("CiFiles", () => {
});
it("returns false when phases include feature work", () => {
const ciFiles = new CiFiles(dir, "feat-proj");
const ciFiles = new CIAgentFiles(dir, "feat-proj");
ciFiles.ensureProjectDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [{ slug: "feat-proj", name: "Feature Project", default: true }],
active_project: "feat-proj",
}));
@@ -289,11 +289,69 @@ describe("CiFiles", () => {
});
});
describe("getMilestoneType", () => {
it("returns nfr when no roadmap exists", () => {
const ciFiles = new CIAgentFiles(dir);
expect(ciFiles.getMilestoneType()).toBe("nfr");
});
it("returns nfr when phases are all NFR types", () => {
const ciFiles = new CIAgentFiles(dir, "nfr-proj2");
ciFiles.ensureProjectDir();
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [{ slug: "nfr-proj2", name: "NFR Project 2", default: true }],
active_project: "nfr-proj2",
}));
const roadmap: RoadmapMd = {
overview: "NFR-only",
phases: [
{ number: 1, name: "fix-bug", description: "Fix bug", status: "in_progress", dependsOn: [], requirements: [], successCriteria: [] },
],
};
ciFiles.writeRoadmapMd(roadmap);
expect(ciFiles.getMilestoneType()).toBe("nfr");
});
it("returns feature when phases include feat work", () => {
const ciFiles = new CIAgentFiles(dir, "feat-proj2");
ciFiles.ensureProjectDir();
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [{ slug: "feat-proj2", name: "Feature Project 2", default: true }],
active_project: "feat-proj2",
}));
const roadmap: RoadmapMd = {
overview: "feature",
phases: [
{ number: 1, name: "auth-flow", description: "Auth feature", status: "in_progress", dependsOn: [], requirements: [], successCriteria: [] },
],
};
ciFiles.writeRoadmapMd(roadmap);
expect(ciFiles.getMilestoneType()).toBe("feature");
});
it("returns schema-breaking when phases include refactor/rewrite/migrate", () => {
const ciFiles = new CIAgentFiles(dir, "schema-proj");
ciFiles.ensureProjectDir();
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [{ slug: "schema-proj", name: "Schema Project", default: true }],
active_project: "schema-proj",
}));
const roadmap: RoadmapMd = {
overview: "schema-breaking",
phases: [
{ number: 1, name: "refactor-core", description: "Refactor core", status: "in_progress", dependsOn: [], requirements: [], successCriteria: [] },
],
};
ciFiles.writeRoadmapMd(roadmap);
expect(ciFiles.getMilestoneType()).toBe("schema-breaking");
});
});
describe("multi-project file paths", () => {
it("writes PROJECT.md to project subdirectory when slug is set", () => {
const ciFiles = new CiFiles(dir, "my-app");
const ciFiles = new CIAgentFiles(dir, "my-app");
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
projects: [{ slug: "my-app", name: "My App", default: true }],
active_project: "my-app",
}));
@@ -309,13 +367,13 @@ describe("CiFiles", () => {
ciFiles.writeProjectMd(project, "initial");
expect(fs.existsSync(path.join(dir, ".ci", "my-app", "PROJECT.md"))).toBe(true);
expect(fs.existsSync(path.join(dir, ".ciagent", "my-app", "PROJECT.md"))).toBe(true);
});
it("writes PROJECT.md to .ci root when no slug is set", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({}));
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({}));
const project: ProjectMd = {
name: "Default App",
@@ -328,7 +386,7 @@ describe("CiFiles", () => {
ciFiles.writeProjectMd(project, "initial");
expect(fs.existsSync(path.join(dir, ".ci", "PROJECT.md"))).toBe(true);
expect(fs.existsSync(path.join(dir, ".ciagent", "PROJECT.md"))).toBe(true);
});
});
@@ -349,7 +407,7 @@ describe("CiFiles", () => {
};
it("writes and reads PROJECT.md", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.writeProjectMd(project, "initial creation");
const read = ciFiles.readProjectMd();
@@ -360,7 +418,7 @@ describe("CiFiles", () => {
});
it("overwrites PROJECT.md on update", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.writeProjectMd(project, "initial");
const updated = { ...project, coreValue: "Updated description" };
@@ -397,7 +455,7 @@ describe("CiFiles", () => {
};
it("writes and reads ROADMAP.md", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.writeRoadmapMd(roadmap);
const read = ciFiles.readRoadmapMd();
@@ -431,7 +489,7 @@ describe("CiFiles", () => {
};
it("writes and reads REQUIREMENTS.md", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.writeRequirementsMd(requirements);
const read = ciFiles.readRequirementsMd();
@@ -439,7 +497,7 @@ describe("CiFiles", () => {
});
it("updates requirement status", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.writeRequirementsMd(requirements);
ciFiles.updateRequirementStatus("AUTH-01", "complete");
@@ -465,7 +523,7 @@ describe("CiFiles", () => {
};
it("writes and reads ARCHITECTURE.md", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
ciFiles.writeArchitectureMd(arch);
const read = ciFiles.readArchitectureMd();
@@ -476,7 +534,7 @@ describe("CiFiles", () => {
describe("updatePhaseStatus", () => {
it("updates phase status in roadmap", () => {
const ciFiles = new CiFiles(dir);
const ciFiles = new CIAgentFiles(dir);
const roadmap: RoadmapMd = {
overview: "test",
phases: [
@@ -2,8 +2,9 @@ import * as fs from "node:fs";
import * as path from "node:path";
import { writeFile, readFile, ensureDir, fileExists } from "../utils/file.js";
import { PipelineStage } from "../types/pipeline.js";
import { MilestoneType } from "../types/config.js";
const CI_DIR = ".ci";
const CI_DIR = ".ciagent";
export interface ProjectMd {
name: string;
@@ -70,7 +71,7 @@ export interface ProjectEntry {
default?: boolean;
}
export class CiFiles {
export class CIAgentFiles {
private projectPath: string;
private projectSlug: string;
@@ -467,19 +468,31 @@ export class CiFiles {
this.writeRoadmapMd(roadmap);
}
isNfrMilestone(): boolean {
getMilestoneType(): MilestoneType {
const roadmap = this.readRoadmapMd();
if (!roadmap) return true;
if (!roadmap) return "nfr";
const nfrTypes: string[] = ["fix", "chore", "docs", "perf", "refactor", "test"];
const schemaBreakKeywords: string[] = ["refactor", "rewrite", "rearchitecture", "migrate", "restructure"];
let hasFeature = false;
let hasSchemaBreak = false;
for (const phase of roadmap.phases) {
if (phase.status === "in_progress" || phase.status === "not_started") {
if (phase.status === "in_progress" || phase.status === "not_started" || phase.status === "complete") {
const phaseName = phase.name.toLowerCase();
const hasFeature = !nfrTypes.some((t) => phaseName.includes(t)) && !phaseName.includes("bug") && !phaseName.includes("tune") && !phaseName.includes("refresh");
if (hasFeature) return false;
const isNfr = nfrTypes.some((t) => phaseName.includes(t)) || phaseName.includes("bug") || phaseName.includes("tune") || phaseName.includes("refresh");
if (!isNfr) hasFeature = true;
if (schemaBreakKeywords.some((k) => phaseName.includes(k))) hasSchemaBreak = true;
}
}
return true;
if (hasSchemaBreak) return "schema-breaking";
if (hasFeature) return "feature";
return "nfr";
}
isNfrMilestone(): boolean {
return this.getMilestoneType() === "nfr";
}
private parseProjectMd(content: string): ProjectMd {
+18 -18
View File
@@ -2,15 +2,15 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { ClarifyPhase, saveSpecification, loadSpecification } from "../core/clarify.js";
import { DEFAULT_CI_CONFIG } from "../types/config.js";
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
import { Specification, parseSpecification } from "../types/specification.js";
describe("ClarifyPhase", () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-clarify-test-"));
fs.mkdirSync(path.join(tempDir, ".ci"), { recursive: true });
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-clarify-test-"));
fs.mkdirSync(path.join(tempDir, ".ciagent"), { recursive: true });
});
afterEach(() => {
@@ -41,7 +41,7 @@ describe("ClarifyPhase", () => {
describe("generateQuestions", () => {
it("generates questions for missing requirements", () => {
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
const questions = clarify.generateQuestions(specWithoutRequirements);
expect(questions.length).toBeGreaterThan(0);
const reqQuestion = questions.find((q) => q.category === "requirements");
@@ -50,7 +50,7 @@ describe("ClarifyPhase", () => {
});
it("generates questions for missing constraints", () => {
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
const questions = clarify.generateQuestions(specWithoutRequirements);
const constraintQuestion = questions.find((q) => q.category === "constraints");
expect(constraintQuestion).toBeDefined();
@@ -58,7 +58,7 @@ describe("ClarifyPhase", () => {
});
it("generates deployment question when deploy is mentioned without deploy constraint", () => {
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
const questions = clarify.generateQuestions(specWithRequirements);
const deployQuestion = questions.find((q) => q.category === "deployment");
expect(deployQuestion).toBeDefined();
@@ -66,8 +66,8 @@ describe("ClarifyPhase", () => {
it("respects clarify_budget", () => {
const limitedConfig = {
...DEFAULT_CI_CONFIG,
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, clarify_budget: 1 },
...DEFAULT_CIAGENT_CONFIG,
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, clarify_budget: 1 },
};
const clarify = new ClarifyPhase(limitedConfig, tempDir);
const questions = clarify.generateQuestions(specWithoutRequirements);
@@ -75,7 +75,7 @@ describe("ClarifyPhase", () => {
});
it("assigns sequential question IDs", () => {
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
const questions = clarify.generateQuestions(specWithoutRequirements);
for (let i = 0; i < questions.length; i++) {
expect(questions[i].id).toBe(`Q-${String(i + 1).padStart(3, "0")}`);
@@ -83,7 +83,7 @@ describe("ClarifyPhase", () => {
});
it("sorts questions by impact priority", () => {
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
const questions = clarify.generateQuestions(specWithoutRequirements);
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
for (let i = 1; i < questions.length; i++) {
@@ -96,7 +96,7 @@ describe("ClarifyPhase", () => {
describe("answerQuestion", () => {
it("records an answer to a question", () => {
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
const questions = clarify.generateQuestions(specWithoutRequirements);
expect(questions.length).toBeGreaterThan(0);
@@ -107,7 +107,7 @@ describe("ClarifyPhase", () => {
});
it("returns null for unknown question ID", () => {
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
const result = clarify.answerQuestion("Q-999", "answer");
expect(result).toBeNull();
});
@@ -115,7 +115,7 @@ describe("ClarifyPhase", () => {
describe("acceptDefaults", () => {
it("accepts defaults for all unanswered questions", () => {
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
clarify.generateQuestions(specWithoutRequirements);
const result = clarify.acceptDefaults();
@@ -125,7 +125,7 @@ describe("ClarifyPhase", () => {
});
it("preserves manually answered questions", () => {
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
const questions = clarify.generateQuestions(specWithoutRequirements);
if (questions.length > 0) {
clarify.answerQuestion(questions[0].id, "My answer");
@@ -138,11 +138,11 @@ describe("ClarifyPhase", () => {
});
it("saves clarify responses file", () => {
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
clarify.generateQuestions(specWithoutRequirements);
clarify.acceptDefaults();
const responsesPath = path.join(tempDir, ".ci", "clarify-responses.md");
const responsesPath = path.join(tempDir, ".ciagent", "clarify-responses.md");
expect(fs.existsSync(responsesPath)).toBe(true);
const content = fs.readFileSync(responsesPath, "utf-8");
expect(content).toContain("Clarify Phase Responses");
@@ -154,8 +154,8 @@ describe("saveSpecification / loadSpecification", () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-spec-test-"));
fs.mkdirSync(path.join(tempDir, ".ci"), { recursive: true });
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-spec-test-"));
fs.mkdirSync(path.join(tempDir, ".ciagent"), { recursive: true });
});
afterEach(() => {
+4 -4
View File
@@ -2,22 +2,22 @@ import * as fs from "node:fs";
import * as path from "node:path";
import { ClarifyQuestion, ClarifyResult } from "../types/clarify.js";
import { Specification, parseSpecification } from "../types/specification.js";
import { CIConfig } from "../types/config.js";
import { CIAgentConfig } from "../types/config.js";
const CLARIFY_RESPONSES_FILE = "clarify-responses.md";
const SPECIFICATION_FILE = "specification.md";
function getCIDir(projectPath: string): string {
return path.join(projectPath, ".ci");
return path.join(projectPath, ".ciagent");
}
export class ClarifyPhase {
private config: CIConfig;
private config: CIAgentConfig;
private projectPath: string;
private questions: ClarifyQuestion[];
private questionCounter: number;
constructor(config: CIConfig, projectPath: string) {
constructor(config: CIAgentConfig, projectPath: string) {
this.config = config;
this.projectPath = projectPath;
this.questions = [];
+22 -22
View File
@@ -1,11 +1,11 @@
import { CommitBuilder } from "../core/commit-builder.js";
import { extractCiBlock, parseCiBlock } from "../core/commit-parser.js";
import { CiMetadata } from "../types/commit-meta.js";
import { extractCIAgentBlock, parseCIAgentBlock } from "../core/commit-parser.js";
import { CIAgentMetadata } from "../types/commit-meta.js";
describe("CommitBuilder", () => {
describe("buildCiBlock", () => {
it("builds minimal ci block", () => {
const ci: CiMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
const block = CommitBuilder.buildCiBlock(ci);
expect(block).toContain("phase: 1");
@@ -14,19 +14,19 @@ describe("CommitBuilder", () => {
});
it("builds ci block with project", () => {
const ci: CiMetadata = { phase: 1, milestone: "v1.0", status: "execute", project: "task-api" };
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute", project: "task-api" };
const block = CommitBuilder.buildCiBlock(ci);
expect(block).toContain("project: task-api");
});
it("builds ci block without project when not set", () => {
const ci: CiMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
const block = CommitBuilder.buildCiBlock(ci);
expect(block).not.toContain("project:");
});
it("builds ci block with decisions", () => {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: 1,
milestone: "v1.0",
status: "execute",
@@ -49,7 +49,7 @@ describe("CommitBuilder", () => {
});
it("builds ci block with lessons", () => {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: 1,
milestone: "v1.0",
status: "complete",
@@ -63,7 +63,7 @@ describe("CommitBuilder", () => {
});
it("builds ci block with compound", () => {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: 1,
milestone: "v1.0",
status: "complete",
@@ -82,7 +82,7 @@ describe("CommitBuilder", () => {
});
it("builds ci block with escalations", () => {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: 3,
milestone: "v1.0",
status: "execute",
@@ -103,7 +103,7 @@ describe("CommitBuilder", () => {
});
it("builds ci block with requirements", () => {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: 1,
milestone: "v1.0",
status: "complete",
@@ -122,12 +122,12 @@ describe("CommitBuilder", () => {
describe("round-trip: build then parse", () => {
it("round-trips a simple ci block", () => {
const ci: CiMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
const block = CommitBuilder.buildCiBlock(ci);
const fullMessage = `feat(P01): test\n\n---ci---\n${block}\n---/ci---\n\nBody text`;
const extracted = extractCiBlock(fullMessage)!;
const parsed = parseCiBlock(extracted)!;
const extracted = extractCIAgentBlock(fullMessage)!;
const parsed = parseCIAgentBlock(extracted)!;
expect(parsed.phase).toBe(1);
expect(parsed.milestone).toBe("v1.0");
@@ -135,7 +135,7 @@ describe("CommitBuilder", () => {
});
it("round-trips decisions", () => {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: 1,
milestone: "v1.0",
status: "execute",
@@ -152,8 +152,8 @@ describe("CommitBuilder", () => {
const block = CommitBuilder.buildCiBlock(ci);
const fullMessage = `feat(P01): test\n\n---ci---\n${block}\n---/ci---`;
const extracted = extractCiBlock(fullMessage)!;
const parsed = parseCiBlock(extracted)!;
const extracted = extractCIAgentBlock(fullMessage)!;
const parsed = parseCIAgentBlock(extracted)!;
expect(parsed.decisions).toHaveLength(1);
expect(parsed.decisions![0].id).toBe("D-001");
@@ -163,7 +163,7 @@ describe("CommitBuilder", () => {
});
it("round-trips compound with lessons", () => {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: 2,
milestone: "v1.0",
status: "complete",
@@ -177,8 +177,8 @@ describe("CommitBuilder", () => {
const block = CommitBuilder.buildCiBlock(ci);
const fullMessage = `compound(P02): test\n\n---ci---\n${block}\n---/ci---`;
const extracted = extractCiBlock(fullMessage)!;
const parsed = parseCiBlock(extracted)!;
const extracted = extractCIAgentBlock(fullMessage)!;
const parsed = parseCIAgentBlock(extracted)!;
expect(parsed.compound!.category).toBe("auth");
expect(parsed.compound!.problem).toBe("Token replay attacks");
@@ -186,11 +186,11 @@ describe("CommitBuilder", () => {
});
it("round-trips project field", () => {
const ci: CiMetadata = { phase: 1, milestone: "v1.0", status: "execute", project: "task-api" };
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute", project: "task-api" };
const block = CommitBuilder.buildCiBlock(ci);
const fullMessage = `feat(task-api/P01): test\n\n---ci---\n${block}\n---/ci---`;
const extracted = extractCiBlock(fullMessage)!;
const parsed = parseCiBlock(extracted)!;
const extracted = extractCIAgentBlock(fullMessage)!;
const parsed = parseCIAgentBlock(extracted)!;
expect(parsed.project).toBe("task-api");
});
+11 -11
View File
@@ -1,5 +1,5 @@
import {
CiMetadata,
CIAgentMetadata,
CommitType,
CommitScope,
CommitDecision,
@@ -17,7 +17,7 @@ export interface CommitMessageInput {
type: CommitType;
scope: CommitScope;
subject: string;
ci: CiMetadata;
ci: CIAgentMetadata;
body?: string;
}
@@ -92,7 +92,7 @@ export interface VerifyCommitInput {
}
export class CommitBuilder {
static buildCiBlock(ci: CiMetadata): string {
static buildCiBlock(ci: CIAgentMetadata): string {
const lines: string[] = [];
lines.push(`phase: ${ci.phase}`);
lines.push(`milestone: ${ci.milestone}`);
@@ -162,7 +162,7 @@ export class CommitBuilder {
}
static buildInitCommit(input: InitCommitInput): string {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: 0,
milestone: input.milestone,
project: input.project,
@@ -194,7 +194,7 @@ export class CommitBuilder {
}
static buildTaskCommit(input: TaskCommitInput): string {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: input.phase,
milestone: input.milestone,
project: input.project,
@@ -224,7 +224,7 @@ export class CommitBuilder {
}
static buildPhaseCompletionCommit(input: PhaseCompletionInput): string {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: input.phase,
milestone: input.milestone,
status: "complete",
@@ -253,7 +253,7 @@ export class CommitBuilder {
}
static buildDecisionCommit(input: DecisionCommitInput): string {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: input.phase,
milestone: input.milestone,
status: "plan",
@@ -271,7 +271,7 @@ export class CommitBuilder {
}
static buildEscalationCommit(input: EscalationCommitInput): string {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: input.phase,
milestone: input.milestone,
status: "execute",
@@ -289,7 +289,7 @@ export class CommitBuilder {
}
static buildCompoundCommit(input: CompoundCommitInput): string {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: input.phase,
milestone: input.milestone,
status: "complete",
@@ -313,7 +313,7 @@ export class CommitBuilder {
}
static buildVerifyCommit(input: VerifyCommitInput): string {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase: input.phase,
milestone: input.milestone,
status: "verify",
@@ -338,7 +338,7 @@ export class CommitBuilder {
findings: string[],
decisions?: CommitDecision[]
): string {
const ci: CiMetadata = {
const ci: CIAgentMetadata = {
phase,
milestone,
status: "research",
+22 -22
View File
@@ -1,5 +1,5 @@
import {
CiMetadata,
CIAgentMetadata,
CommitDecision,
CommitEscalation,
CommitRequirements,
@@ -9,8 +9,8 @@ import {
CommitScope,
} from "../types/commit-meta.js";
import {
extractCiBlock,
parseCiBlock,
extractCIAgentBlock,
parseCIAgentBlock,
parseCommitMessage,
} from "./commit-parser.js";
@@ -128,29 +128,29 @@ status: execute
Registration endpoint for task-api project.`;
describe("extractCiBlock", () => {
describe("extractCIAgentBlock", () => {
it("extracts ---ci--- block from commit message", () => {
const block = extractCiBlock(SAMPLE_INIT_COMMIT);
const block = extractCIAgentBlock(SAMPLE_INIT_COMMIT);
expect(block).toBeTruthy();
expect(block).toContain("phase: 0");
expect(block).toContain("milestone: v1.0");
});
it("returns null when no ---ci--- block exists", () => {
const block = extractCiBlock("docs: some regular commit\n\nNo CI block here");
const block = extractCIAgentBlock("docs: some regular commit\n\nNo CI block here");
expect(block).toBeNull();
});
it("returns null for unclosed ---ci--- block", () => {
const block = extractCiBlock("docs: bad\n---ci---\nphase: 1\nno end marker");
const block = extractCIAgentBlock("docs: bad\n---ci---\nphase: 1\nno end marker");
expect(block).toBeNull();
});
});
describe("parseCiBlock", () => {
describe("parseCIAgentBlock", () => {
it("parses init commit ci block", () => {
const block = extractCiBlock(SAMPLE_INIT_COMMIT)!;
const meta = parseCiBlock(block)!;
const block = extractCIAgentBlock(SAMPLE_INIT_COMMIT)!;
const meta = parseCIAgentBlock(block)!;
expect(meta.phase).toBe(0);
expect(meta.milestone).toBe("v1.0");
@@ -163,8 +163,8 @@ describe("parseCiBlock", () => {
});
it("parses task commit ci block", () => {
const block = extractCiBlock(SAMPLE_TASK_COMMIT)!;
const meta = parseCiBlock(block)!;
const block = extractCIAgentBlock(SAMPLE_TASK_COMMIT)!;
const meta = parseCIAgentBlock(block)!;
expect(meta.phase).toBe(1);
expect(meta.plan).toBe("01-01");
@@ -177,8 +177,8 @@ describe("parseCiBlock", () => {
});
it("parses phase completion with lessons", () => {
const block = extractCiBlock(SAMPLE_PHASE_COMPLETE_COMMIT)!;
const meta = parseCiBlock(block)!;
const block = extractCIAgentBlock(SAMPLE_PHASE_COMPLETE_COMMIT)!;
const meta = parseCIAgentBlock(block)!;
expect(meta.phase).toBe(1);
expect(meta.status).toBe("complete");
@@ -188,8 +188,8 @@ describe("parseCiBlock", () => {
});
it("parses compound commit", () => {
const block = extractCiBlock(SAMPLE_COMPOUND_COMMIT)!;
const meta = parseCiBlock(block)!;
const block = extractCIAgentBlock(SAMPLE_COMPOUND_COMMIT)!;
const meta = parseCIAgentBlock(block)!;
expect(meta.compound).toBeDefined();
expect(meta.compound!.category).toBe("auth");
@@ -199,8 +199,8 @@ describe("parseCiBlock", () => {
});
it("parses escalation commit", () => {
const block = extractCiBlock(SAMPLE_ESCALATION_COMMIT)!;
const meta = parseCiBlock(block)!;
const block = extractCIAgentBlock(SAMPLE_ESCALATION_COMMIT)!;
const meta = parseCIAgentBlock(block)!;
expect(meta.escalations).toHaveLength(1);
expect(meta.escalations![0].id).toBe("E-001");
@@ -209,20 +209,20 @@ describe("parseCiBlock", () => {
});
it("parses project field", () => {
const block = extractCiBlock(SAMPLE_PROJECT_COMMIT)!;
const meta = parseCiBlock(block)!;
const block = extractCIAgentBlock(SAMPLE_PROJECT_COMMIT)!;
const meta = parseCIAgentBlock(block)!;
expect(meta.project).toBe("task-api");
expect(meta.phase).toBe(1);
expect(meta.plan).toBe("01-01");
});
it("returns null for empty block", () => {
const meta = parseCiBlock("");
const meta = parseCIAgentBlock("");
expect(meta).toBeNull();
});
it("returns null for block missing required fields", () => {
const meta = parseCiBlock("something: true\nother: false");
const meta = parseCIAgentBlock("something: true\nother: false");
expect(meta).toBeNull();
});
});
+17 -17
View File
@@ -1,8 +1,8 @@
import {
CiMetadata,
CIAgentMetadata,
CommitType,
CommitEscalation,
ParsedCiCommit,
ParsedCIAgentCommit,
parseCommitType,
parseCommitScope,
} from "../types/commit-meta.js";
@@ -10,7 +10,7 @@ import {
const CI_BLOCK_START = "---ci---";
const CI_BLOCK_END = "---/ci---";
export function extractCiBlock(message: string): string | null {
export function extractCIAgentBlock(message: string): string | null {
const startIdx = message.indexOf(CI_BLOCK_START);
if (startIdx < 0) return null;
@@ -20,10 +20,10 @@ export function extractCiBlock(message: string): string | null {
return message.slice(startIdx + CI_BLOCK_START.length, endIdx).trim();
}
export function parseCiBlock(yaml: string): CiMetadata | null {
export function parseCIAgentBlock(yaml: string): CIAgentMetadata | null {
if (!yaml) return null;
const result: Partial<CiMetadata> = {};
const result: Partial<CIAgentMetadata> = {};
const phaseMatch = yaml.match(/^phase:\s*(.+)$/m);
if (phaseMatch) result.phase = parseInt(phaseMatch[1], 10) || 0;
@@ -38,7 +38,7 @@ export function parseCiBlock(yaml: string): CiMetadata | null {
if (taskMatch) result.task = taskMatch[1].trim();
const statusMatch = yaml.match(/^status:\s*(.+)$/m);
if (statusMatch) result.status = statusMatch[1].trim() as CiMetadata["status"];
if (statusMatch) result.status = statusMatch[1].trim() as CIAgentMetadata["status"];
const projectMatch = yaml.match(/^project:\s*(.+)$/m);
if (projectMatch) result.project = projectMatch[1].trim();
@@ -50,14 +50,14 @@ export function parseCiBlock(yaml: string): CiMetadata | null {
result.compound = parseCompoundFromYaml(yaml);
if (result.phase !== undefined && result.milestone !== undefined && result.status !== undefined) {
return result as CiMetadata;
return result as CIAgentMetadata;
}
return null;
}
function parseDecisionsFromYaml(yaml: string): CiMetadata["decisions"] {
const decisions: NonNullable<CiMetadata["decisions"]> = [];
function parseDecisionsFromYaml(yaml: string): CIAgentMetadata["decisions"] {
const decisions: NonNullable<CIAgentMetadata["decisions"]> = [];
const decisionRegex = /- id: (.+)\n\s+decision: (.+)\n\s+rationale: (.+)\n\s+confidence: (.+)\n\s+alternatives: \[([^\]]*)\]/g;
let match;
@@ -74,8 +74,8 @@ function parseDecisionsFromYaml(yaml: string): CiMetadata["decisions"] {
return decisions.length > 0 ? decisions : undefined;
}
function parseEscalationsFromYaml(yaml: string): CiMetadata["escalations"] {
const escalations: NonNullable<CiMetadata["escalations"]> = [];
function parseEscalationsFromYaml(yaml: string): CIAgentMetadata["escalations"] {
const escalations: NonNullable<CIAgentMetadata["escalations"]> = [];
const escalationRegex = /- id: (.+)\n\s+type: (.+)\n\s+description: (.+)\n\s+resolution: (.+)/g;
let match;
@@ -91,7 +91,7 @@ function parseEscalationsFromYaml(yaml: string): CiMetadata["escalations"] {
return escalations.length > 0 ? escalations : undefined;
}
function parseRequirementsFromYaml(yaml: string): CiMetadata["requirements"] {
function parseRequirementsFromYaml(yaml: string): CIAgentMetadata["requirements"] {
const coveredMatch = yaml.match(/^\s+covered: \[([^\]]*)\]/m);
const partialMatch = yaml.match(/^\s+partial: \[([^\]]*)\]/m);
@@ -106,7 +106,7 @@ function parseRequirementsFromYaml(yaml: string): CiMetadata["requirements"] {
return { covered, partial };
}
function parseLessonsFromYaml(yaml: string): CiMetadata["lessons"] {
function parseLessonsFromYaml(yaml: string): CIAgentMetadata["lessons"] {
const lessonRegex = /^ - (.+)$/gm;
const lessons: string[] = [];
let inLessonsSection = false;
@@ -126,7 +126,7 @@ function parseLessonsFromYaml(yaml: string): CiMetadata["lessons"] {
return lessons.length > 0 ? lessons : undefined;
}
function parseCompoundFromYaml(yaml: string): CiMetadata["compound"] {
function parseCompoundFromYaml(yaml: string): CIAgentMetadata["compound"] {
const categoryMatch = yaml.match(/^\s+category: (.+)$/m);
const problemMatch = yaml.match(/^\s+problem: (.+)$/m);
const solutionMatch = yaml.match(/^\s+solution: (.+)$/m);
@@ -143,7 +143,7 @@ function parseCompoundFromYaml(yaml: string): CiMetadata["compound"] {
export function parseCommitMessage(
hash: string,
message: string
): ParsedCiCommit {
): ParsedCIAgentCommit {
const firstLine = message.split("\n")[0] || "";
const subjectMatch = firstLine.match(/^(\w+)(?:\(([^)]+)\))?: (.+)$/);
@@ -157,8 +157,8 @@ export function parseCommitMessage(
subject = subjectMatch[3] || firstLine;
}
const ciBlock = extractCiBlock(message);
const ci = ciBlock ? parseCiBlock(ciBlock) : null;
const ciBlock = extractCIAgentBlock(message);
const ci = ciBlock ? parseCIAgentBlock(ciBlock) : null;
const bodyStart = message.indexOf("\n");
let body = bodyStart >= 0 ? message.slice(bodyStart + 1).trim() : "";
+37 -37
View File
@@ -1,45 +1,45 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { initCI, loadConfig, saveConfig, isCIInitialized, ensureCIDir } from "../core/config.js";
import { DEFAULT_CI_CONFIG } from "../types/config.js";
import { initCIAgent, loadConfig, saveConfig, isCIAgentInitialized, ensureCIDir } from "../core/config.js";
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
describe("CI Config", () => {
describe("CIAgent Config", () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-config-test-"));
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-config-test-"));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe("initCI", () => {
it("initializes a new CI project with default config", () => {
const config = initCI(tempDir);
describe("initCIAgent", () => {
it("initializes a new CIAgent project with default config", () => {
const config = initCIAgent(tempDir);
expect(config.autonomy.level).toBe("full");
expect(isCIInitialized(tempDir)).toBe(true);
expect(isCIAgentInitialized(tempDir)).toBe(true);
});
it("initializes with custom config merged on top of defaults", () => {
const config = initCI(tempDir, {
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level: "guided" },
const config = initCIAgent(tempDir, {
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, level: "guided" },
});
expect(config.autonomy.level).toBe("guided");
expect(config.autonomy.clarify_budget).toBe(10);
expect(config.model_profile).toBe("quality");
});
it("creates .ci/ directory structure", () => {
initCI(tempDir);
expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
expect(fs.existsSync(path.join(tempDir, ".ci", "config.json"))).toBe(true);
it("creates .ciagent/ directory structure", () => {
initCIAgent(tempDir);
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
expect(fs.existsSync(path.join(tempDir, ".ciagent", "config.json"))).toBe(true);
});
it("deep merges nested config", () => {
const config = initCI(tempDir, {
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level: "supervised" },
const config = initCIAgent(tempDir, {
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, level: "supervised" },
});
expect(config.autonomy.level).toBe("supervised");
expect(config.autonomy.max_revision_iterations).toBe(3);
@@ -47,7 +47,7 @@ describe("CI Config", () => {
});
it("initializes with project slug", () => {
const config = initCI(tempDir, undefined, "task-api", "Task API");
const config = initCIAgent(tempDir, undefined, "task-api", "Task API");
expect(config.projects).toHaveLength(1);
expect(config.projects[0].slug).toBe("task-api");
expect(config.projects[0].name).toBe("Task API");
@@ -56,20 +56,20 @@ describe("CI Config", () => {
});
it("does not re-add existing project slug", () => {
initCI(tempDir, undefined, "task-api", "Task API");
const config = initCI(tempDir, undefined, "task-api", "Task API V2");
initCIAgent(tempDir, undefined, "task-api", "Task API");
const config = initCIAgent(tempDir, undefined, "task-api", "Task API V2");
expect(config.projects).toHaveLength(1);
});
it("defaults projects and active_project when no slug provided", () => {
const config = initCI(tempDir);
const config = initCIAgent(tempDir);
expect(config.projects).toEqual([]);
expect(config.active_project).toBe("");
});
it("preserves existing projects when adding new one", () => {
const config1 = initCI(tempDir, undefined, "task-api", "Task API");
const config2 = initCI(tempDir, {
const config1 = initCIAgent(tempDir, undefined, "task-api", "Task API");
const config2 = initCIAgent(tempDir, {
...config1,
projects: [...config1.projects, { slug: "auth-svc", name: "Auth Service" }],
}, "auth-svc", "Auth Service");
@@ -81,11 +81,11 @@ describe("CI Config", () => {
describe("loadConfig", () => {
it("returns default config when no config file exists", () => {
const config = loadConfig(tempDir);
expect(config).toEqual(DEFAULT_CI_CONFIG);
expect(config).toEqual(DEFAULT_CIAGENT_CONFIG);
});
it("loads and deep merges config from file", () => {
initCI(tempDir, { autonomy: { ...DEFAULT_CI_CONFIG.autonomy, decision_confidence_threshold: 0.85 } });
initCIAgent(tempDir, { autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, decision_confidence_threshold: 0.85 } });
const config = loadConfig(tempDir);
expect(config.autonomy.decision_confidence_threshold).toBe(0.85);
expect(config.autonomy.level).toBe("full");
@@ -93,7 +93,7 @@ describe("CI Config", () => {
});
it("preserves nested objects that are not overridden", () => {
initCI(tempDir, { git: { ...DEFAULT_CI_CONFIG.git, auto_push: true } });
initCIAgent(tempDir, { git: { ...DEFAULT_CIAGENT_CONFIG.git, auto_push: true } });
const config = loadConfig(tempDir);
expect(config.git.auto_push).toBe(true);
expect(config.git.auto_commit).toBe(true);
@@ -101,7 +101,7 @@ describe("CI Config", () => {
});
it("loads projects array from config", () => {
initCI(tempDir, undefined, "task-api", "Task API");
initCIAgent(tempDir, undefined, "task-api", "Task API");
const config = loadConfig(tempDir);
expect(config.projects).toHaveLength(1);
expect(config.active_project).toBe("task-api");
@@ -112,8 +112,8 @@ describe("CI Config", () => {
it("saves and reloads config correctly", () => {
ensureCIDir(tempDir);
const customConfig = {
...DEFAULT_CI_CONFIG,
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level: "guided" as const },
...DEFAULT_CIAGENT_CONFIG,
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, level: "guided" as const },
};
saveConfig(tempDir, customConfig);
const loaded = loadConfig(tempDir);
@@ -123,7 +123,7 @@ describe("CI Config", () => {
it("saves and reloads config with projects", () => {
ensureCIDir(tempDir);
const config = {
...DEFAULT_CI_CONFIG,
...DEFAULT_CIAGENT_CONFIG,
projects: [{ slug: "my-app", name: "My App", default: true }],
active_project: "my-app",
};
@@ -134,27 +134,27 @@ describe("CI Config", () => {
});
});
describe("isCIInitialized", () => {
describe("isCIAgentInitialized", () => {
it("returns false for uninitialized directory", () => {
expect(isCIInitialized(tempDir)).toBe(false);
expect(isCIAgentInitialized(tempDir)).toBe(false);
});
it("returns true after initCI", () => {
initCI(tempDir);
expect(isCIInitialized(tempDir)).toBe(true);
it("returns true after initCIAgent", () => {
initCIAgent(tempDir);
expect(isCIAgentInitialized(tempDir)).toBe(true);
});
});
describe("ensureCIDir", () => {
it("creates .ci directory", () => {
it("creates .ciagent directory", () => {
ensureCIDir(tempDir);
expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
});
it("is idempotent", () => {
ensureCIDir(tempDir);
ensureCIDir(tempDir);
expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
});
});
});
+25 -25
View File
@@ -1,11 +1,11 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { CIConfig, DEFAULT_CI_CONFIG } from "../types/config.js";
import { CIAgentConfig, DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
const CI_DIR = ".ci";
const CI_DIR = ".ciagent";
const CONFIG_FILE = "config.json";
export function getCIConfigPath(projectPath: string): string {
export function getCIAgentConfigPath(projectPath: string): string {
return path.join(projectPath, CI_DIR, CONFIG_FILE);
}
@@ -20,7 +20,7 @@ export function ensureCIDir(projectPath: string): void {
}
}
function deepMerge(base: CIConfig, override: Record<string, unknown>): CIConfig {
function deepMerge(base: CIAgentConfig, override: Record<string, unknown>): CIAgentConfig {
const result = { ...base } as Record<string, unknown>;
for (const key of Object.keys(override)) {
const baseVal = result[key];
@@ -30,43 +30,43 @@ function deepMerge(base: CIConfig, override: Record<string, unknown>): CIConfig
overrideVal && typeof overrideVal === "object" && !Array.isArray(overrideVal)
) {
result[key] = deepMerge(
baseVal as unknown as CIConfig,
baseVal as unknown as CIAgentConfig,
overrideVal as Record<string, unknown>
) as unknown;
} else if (overrideVal !== undefined) {
result[key] = overrideVal;
}
}
return result as unknown as CIConfig;
return result as unknown as CIAgentConfig;
}
export function loadConfig(projectPath: string): CIConfig {
const configPath = getCIConfigPath(projectPath);
export function loadConfig(projectPath: string): CIAgentConfig {
const configPath = getCIAgentConfigPath(projectPath);
if (!fs.existsSync(configPath)) {
return { ...DEFAULT_CI_CONFIG };
return { ...DEFAULT_CIAGENT_CONFIG };
}
const raw = fs.readFileSync(configPath, "utf-8");
const parsed = JSON.parse(raw);
return deepMerge(DEFAULT_CI_CONFIG, parsed);
return deepMerge(DEFAULT_CIAGENT_CONFIG, parsed);
}
export function saveConfig(projectPath: string, config: CIConfig): void {
export function saveConfig(projectPath: string, config: CIAgentConfig): void {
ensureCIDir(projectPath);
const configPath = getCIConfigPath(projectPath);
const configPath = getCIAgentConfigPath(projectPath);
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
}
export function isCIInitialized(projectPath: string): boolean {
export function isCIAgentInitialized(projectPath: string): boolean {
const ciDir = getCIDir(projectPath);
const configPath = getCIConfigPath(projectPath);
const configPath = getCIAgentConfigPath(projectPath);
return fs.existsSync(ciDir) && fs.existsSync(configPath);
}
export function initCI(projectPath: string, config?: Partial<CIConfig>, projectSlug?: string, projectName?: string): CIConfig {
export function initCIAgent(projectPath: string, config?: Partial<CIAgentConfig>, projectSlug?: string, projectName?: string): CIAgentConfig {
ensureCIDir(projectPath);
let projects = config?.projects || DEFAULT_CI_CONFIG.projects;
let activeProject = config?.active_project || DEFAULT_CI_CONFIG.active_project;
let projects = config?.projects || DEFAULT_CIAGENT_CONFIG.projects;
let activeProject = config?.active_project || DEFAULT_CIAGENT_CONFIG.active_project;
if (projectSlug) {
if (!projects.some((p) => p.slug === projectSlug)) {
@@ -75,20 +75,20 @@ export function initCI(projectPath: string, config?: Partial<CIConfig>, projectS
activeProject = projectSlug;
}
const fullConfig: CIConfig = {
...DEFAULT_CI_CONFIG,
const fullConfig: CIAgentConfig = {
...DEFAULT_CIAGENT_CONFIG,
...config,
projects,
active_project: activeProject,
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, ...config?.autonomy },
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, ...config?.autonomy },
parallelization: {
...DEFAULT_CI_CONFIG.parallelization,
...DEFAULT_CIAGENT_CONFIG.parallelization,
...config?.parallelization,
},
verification: { ...DEFAULT_CI_CONFIG.verification, ...config?.verification },
security: { ...DEFAULT_CI_CONFIG.security, ...config?.security },
git: { ...DEFAULT_CI_CONFIG.git, ...config?.git },
backend: { ...DEFAULT_CI_CONFIG.backend, ...config?.backend },
verification: { ...DEFAULT_CIAGENT_CONFIG.verification, ...config?.verification },
security: { ...DEFAULT_CIAGENT_CONFIG.security, ...config?.security },
git: { ...DEFAULT_CIAGENT_CONFIG.git, ...config?.git },
backend: { ...DEFAULT_CIAGENT_CONFIG.backend, ...config?.backend },
};
saveConfig(projectPath, fullConfig);
return fullConfig;
+5 -5
View File
@@ -2,15 +2,15 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { DecisionEngine, DecisionInput } from "../core/decision-engine.js";
import { DEFAULT_CI_CONFIG } from "../types/config.js";
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
describe("DecisionEngine", () => {
let tempDir: string;
let engine: DecisionEngine;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-decision-test-"));
engine = new DecisionEngine(DEFAULT_CI_CONFIG, tempDir);
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-decision-test-"));
engine = new DecisionEngine(DEFAULT_CIAGENT_CONFIG, tempDir);
});
afterEach(() => {
@@ -106,8 +106,8 @@ describe("DecisionEngine", () => {
it("escalates if threshold is raised above 0.7", () => {
const strictConfig = {
...DEFAULT_CI_CONFIG,
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, decision_confidence_threshold: 0.8 },
...DEFAULT_CIAGENT_CONFIG,
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, decision_confidence_threshold: 0.8 },
};
const strictEngine = new DecisionEngine(strictConfig, tempDir);
const result = strictEngine.makeMediumConfidenceDecision(
+3 -3
View File
@@ -1,6 +1,6 @@
import { execSync } from "node:child_process";
import { Decision, DecisionCategory, Alternative, confidenceToLevel } from "../types/decisions.js";
import { CIConfig } from "../types/config.js";
import { CIAgentConfig } from "../types/config.js";
import { CommitBuilder, DecisionCommitInput } from "./commit-builder.js";
import { CommitDecision } from "../types/commit-meta.js";
@@ -22,13 +22,13 @@ export interface DecisionResult {
}
export class DecisionEngine {
private config: CIConfig;
private config: CIAgentConfig;
private projectPath: string;
private currentPhase: number;
private currentMilestone: string;
private decisionCounter: number;
constructor(config: CIConfig, projectPath: string, milestone: string = "v1.0") {
constructor(config: CIAgentConfig, projectPath: string, milestone: string = "v1.0") {
this.config = config;
this.projectPath = projectPath;
this.currentPhase = 0;
+4 -4
View File
@@ -2,16 +2,16 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { ErrorRecovery } from "../core/error-recovery.js";
import { DEFAULT_CI_CONFIG } from "../types/config.js";
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
describe("ErrorRecovery", () => {
let tempDir: string;
let recovery: ErrorRecovery;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-recovery-test-"));
fs.mkdirSync(path.join(tempDir, ".ci"), { recursive: true });
recovery = new ErrorRecovery(DEFAULT_CI_CONFIG, tempDir);
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-recovery-test-"));
fs.mkdirSync(path.join(tempDir, ".ciagent"), { recursive: true });
recovery = new ErrorRecovery(DEFAULT_CIAGENT_CONFIG, tempDir);
});
afterEach(() => {
+9 -8
View File
@@ -1,5 +1,5 @@
import { execSync } from "node:child_process";
import { CIConfig } from "../types/config.js";
import { CIAgentConfig } from "../types/config.js";
export interface RetryConfig {
max_retries: number;
@@ -15,11 +15,11 @@ export interface RecoveryResult {
}
export class ErrorRecovery {
private config: CIConfig;
private config: CIAgentConfig;
private projectPath: string;
private revisionCount: number;
constructor(config: CIConfig, projectPath: string) {
constructor(config: CIAgentConfig, projectPath: string) {
this.config = config;
this.projectPath = projectPath;
this.revisionCount = 0;
@@ -81,17 +81,18 @@ export class ErrorRecovery {
this.git(`branch -D ${phaseBranch}`);
}
const tag = `v0.5.${phase}`;
const tags = this.git("tag -l").split("\n").map((t) => t.trim());
if (tags.includes(tag)) {
this.git(`tag -d ${tag}`);
const tags = this.git("tag -l").split("\n").map((t) => t.trim()).filter(Boolean);
const phaseTagPattern = new RegExp(`^v\\d+\\.\\d+\\.${phase}$`);
const matchingTag = tags.find((t) => phaseTagPattern.test(t));
if (matchingTag) {
this.git(`tag -d ${matchingTag}`);
}
return {
recovered: true,
strategy: "rollback",
attempts: 1,
message: `Rolled back phase ${phase}: ${reason}. Branch ${branchExists ? `${phaseBranch} deleted` : "not found"}. Tag ${tags.includes(tag) ? `${tag} deleted` : "not found"}.`,
message: `Rolled back phase ${phase}: ${reason}. Branch ${branchExists ? `${phaseBranch} deleted` : "not found"}. Tag ${matchingTag ? `${matchingTag} deleted` : "not found"}.`,
};
} catch (err) {
return {
+4 -4
View File
@@ -2,17 +2,17 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { EscalationProtocol, EscalationInput } from "../core/escalation.js";
import { DEFAULT_CI_CONFIG } from "../types/config.js";
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
describe("EscalationProtocol", () => {
let tempDir: string;
let protocol: EscalationProtocol;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-escalation-test-"));
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-escalation-test-"));
const noAutoCommitConfig = {
...DEFAULT_CI_CONFIG,
git: { ...DEFAULT_CI_CONFIG.git, auto_commit: false },
...DEFAULT_CIAGENT_CONFIG,
git: { ...DEFAULT_CIAGENT_CONFIG.git, auto_commit: false },
};
protocol = new EscalationProtocol(noAutoCommitConfig, tempDir);
});
+25 -4
View File
@@ -6,7 +6,7 @@ import {
EscalationResolution,
ESCALATION_TYPES,
} from "../types/escalation.js";
import { CIConfig } from "../types/config.js";
import { CIAgentConfig } from "../types/config.js";
import { CommitBuilder, EscalationCommitInput } from "./commit-builder.js";
import { CommitEscalation } from "../types/commit-meta.js";
@@ -22,16 +22,17 @@ export interface EscalationInput {
}
export class EscalationProtocol {
private config: CIConfig;
private config: CIAgentConfig;
private projectPath: string;
private currentMilestone: string;
private counter: number;
private pendingEscalations: Map<string, Escalation>;
private timeoutCallback: (escalation: Escalation, chosenOption: string) => void;
private timers: NodeJS.Timeout[];
private timerEscalationMap: Map<NodeJS.Timeout, string>;
constructor(
config: CIConfig,
config: CIAgentConfig,
projectPath: string,
milestone: string = "v1.0",
timeoutCallback: (escalation: Escalation, chosenOption: string) => void = () => {}
@@ -43,6 +44,7 @@ export class EscalationProtocol {
this.pendingEscalations = new Map();
this.timeoutCallback = timeoutCallback;
this.timers = [];
this.timerEscalationMap = new Map();
}
setMilestone(milestone: string): void {
@@ -64,7 +66,7 @@ export class EscalationProtocol {
options: input.options,
default_option_id: input.default_option_id,
resolution: "pending",
audit_file: `.ci/audit/deprecated`,
audit_file: `.ciagent/audit/deprecated`,
};
this.pendingEscalations.set(id, escalation);
@@ -102,6 +104,16 @@ export class EscalationProtocol {
const escalation = this.pendingEscalations.get(escalationId);
if (!escalation) return null;
for (let i = this.timers.length - 1; i >= 0; i--) {
const timer = this.timers[i];
const mappedId = this.timerEscalationMap.get(timer);
if (mappedId === escalationId) {
clearTimeout(timer);
this.timerEscalationMap.delete(timer);
this.timers.splice(i, 1);
}
}
escalation.resolution = resolution;
escalation.resolved_at = new Date().toISOString();
escalation.resolution_detail = `Chose option: ${chosenOptionId}`;
@@ -139,11 +151,16 @@ export class EscalationProtocol {
clearAllTimers(): void {
for (const timer of this.timers) {
clearTimeout(timer);
this.timerEscalationMap.delete(timer);
}
this.timers = [];
this.pendingEscalations.clear();
}
dispose(): void {
this.clearAllTimers();
}
formatEscalation(escalation: Escalation): string {
const lines: string[] = [
`⚠️ ESCALATION [${escalation.id}]`,
@@ -200,9 +217,13 @@ export class EscalationProtocol {
escalation.resolved_at = new Date().toISOString();
escalation.resolution_detail = `Auto-proceeded with default: ${escalation.default_option_id}`;
this.pendingEscalations.delete(escalation.id);
this.timerEscalationMap.delete(timer);
const idx = this.timers.indexOf(timer);
if (idx >= 0) this.timers.splice(idx, 1);
this.timeoutCallback(escalation, escalation.default_option_id);
}
}, timeout);
this.timers.push(timer);
this.timerEscalationMap.set(timer, escalation.id);
}
}
+83 -1
View File
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
import { GitBranch } from "../core/git-branch.js";
function createTempRepo(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-branch-test-"));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-branch-test-"));
execSync("git init", { cwd: dir, stdio: "pipe" });
execSync('git config user.email "test@test.com"', { cwd: dir, stdio: "pipe" });
execSync('git config user.name "Test"', { cwd: dir, stdio: "pipe" });
@@ -137,4 +137,86 @@ describe("GitBranch", () => {
expect(result.name).toMatch(/^phase\/01-/);
});
});
describe("validateMergeOrder", () => {
it("rejects phase → main when milestone branch exists", () => {
const gitBranch = new GitBranch(repoDir);
gitBranch.createMilestoneBranch("v0.5", "baseline");
gitBranch.createPhaseBranch(1, "auth");
execSync(`git checkout main`, { cwd: repoDir, stdio: "pipe" });
const result = gitBranch.validateMergeOrder("phase/01-auth", "main");
expect(result.valid).toBe(false);
expect(result.reason).toContain("milestone");
});
it("allows phase → milestone branch", () => {
const gitBranch = new GitBranch(repoDir);
gitBranch.createMilestoneBranch("v0.5", "baseline");
gitBranch.createPhaseBranch(1, "auth");
execSync(`git checkout milestone/v0.5-baseline`, { cwd: repoDir, stdio: "pipe" });
const result = gitBranch.validateMergeOrder("phase/01-auth", "milestone/v0.5-baseline");
expect(result.valid).toBe(true);
});
it("allows phase → main when no milestone branch exists", () => {
const gitBranch = new GitBranch(repoDir);
gitBranch.createPhaseBranch(1, "auth");
execSync(`git checkout main`, { cwd: repoDir, stdio: "pipe" });
const result = gitBranch.validateMergeOrder("phase/01-auth", "main");
expect(result.valid).toBe(true);
});
it("allows hotfix → main", () => {
const gitBranch = new GitBranch(repoDir);
gitBranch.createMilestoneBranch("v0.5", "baseline");
execSync(`git checkout -b hotfix/critical-fix main`, { cwd: repoDir, stdio: "pipe" });
fs.writeFileSync(path.join(repoDir, "fix.txt"), "fix");
execSync(`git add . && git commit -m "hotfix: critical fix"`, { cwd: repoDir, stdio: "pipe" });
execSync(`git checkout main`, { cwd: repoDir, stdio: "pipe" });
const result = gitBranch.validateMergeOrder("hotfix/critical-fix", "main");
expect(result.valid).toBe(true);
});
});
describe("computeMilestoneTag", () => {
it("computes next minor for feature milestone", () => {
execSync(`git tag -a v0.5.1 -m "v0.5.1"`, { cwd: repoDir, stdio: "pipe" });
execSync(`git tag -a v0.5.2 -m "v0.5.2"`, { cwd: repoDir, stdio: "pipe" });
const gitBranch = new GitBranch(repoDir);
const tag = gitBranch.computeMilestoneTag("feature");
expect(tag).toBe("v0.6.0");
});
it("computes next major for schema-breaking milestone", () => {
execSync(`git tag -a v0.5.1 -m "v0.5.1"`, { cwd: repoDir, stdio: "pipe" });
const gitBranch = new GitBranch(repoDir);
const tag = gitBranch.computeMilestoneTag("schema-breaking");
expect(tag).toBe("v1.0.0");
});
it("starts from v0.1.0 when no tags exist", () => {
const gitBranch = new GitBranch(repoDir);
const tag = gitBranch.computeMilestoneTag("feature");
expect(tag).toBe("v0.1.0");
});
});
describe("mergeMilestoneBranch", () => {
it("rejects merge when unmerged phase branches remain", () => {
const gitBranch = new GitBranch(repoDir);
gitBranch.createMilestoneBranch("v0.5", "baseline");
gitBranch.createPhaseBranch(1, "auth");
execSync(`git checkout main`, { cwd: repoDir, stdio: "pipe" });
const result = gitBranch.mergeMilestoneBranch("milestone/v0.5-baseline", "main", true);
expect(result.success).toBe(false);
expect(result.message).toContain("unmerged");
});
});
});
+113
View File
@@ -1,5 +1,6 @@
import { execSync } from "node:child_process";
import { GitContext, BranchInfo } from "./git-context.js";
import { MilestoneType } from "../types/config.js";
export interface BranchCreateResult {
name: string;
@@ -108,6 +109,11 @@ export class GitBranch {
targetBranch: string,
squash: boolean = true
): BranchMergeResult {
const validation = this.validateMergeOrder(phaseBranchName, targetBranch);
if (!validation.valid) {
return { success: false, squash, message: `Merge rejected: ${validation.reason}` };
}
const branches = this.gitContext.getBranches();
const phaseBranch = branches.find((b) => b.name === phaseBranchName);
if (!phaseBranch) {
@@ -136,6 +142,113 @@ export class GitBranch {
};
}
mergeMilestoneBranch(
milestoneBranchName: string,
targetBranch: string,
squash: boolean = true
): BranchMergeResult {
const branches = this.gitContext.getBranches();
const milestoneBranch = branches.find((b) => b.name === milestoneBranchName);
if (!milestoneBranch) {
return { success: false, squash, message: `Branch ${milestoneBranchName} not found` };
}
const phaseBranches = branches.filter(
(b) => b.type === "phase" && !b.merged
);
if (phaseBranches.length > 0) {
return {
success: false,
squash,
message: `Cannot merge milestone: ${phaseBranches.length} unmerged phase branch(es) remain (${phaseBranches.map((b) => b.name).join(", ")})`,
};
}
this.git(`checkout ${targetBranch}`);
const mergeCmd = squash
? `merge --squash ${milestoneBranchName}`
: `merge --no-ff ${milestoneBranchName}`;
const result = this.git(mergeCmd);
if (result === "" && !squash) {
return { success: false, squash, message: `Merge conflict on ${milestoneBranchName}` };
}
if (squash) {
this.git(`commit -m "docs: merge milestone branch ${milestoneBranchName}"`);
}
return {
success: true,
squash,
message: `Merged ${milestoneBranchName} into ${targetBranch} (squash: ${squash})`,
};
}
validateMergeOrder(sourceBranch: string, targetBranch: string): { valid: boolean; reason: string } {
const branches = this.gitContext.getBranches();
const source = branches.find((b) => b.name === sourceBranch);
if (!source) return { valid: false, reason: `Source branch ${sourceBranch} not found` };
if (source.type === "hotfix") {
return { valid: true, reason: "Hotfix branches may merge to any target" };
}
if (source.type === "phase" && targetBranch === "main") {
const milestoneBranches = branches.filter(
(b) => b.type === "milestone" && !b.merged
);
if (milestoneBranches.length > 0) {
return {
valid: false,
reason: `Phase branch must merge into milestone branch first (active: ${milestoneBranches.map((b) => b.name).join(", ")}). Merge into main only through the milestone branch.`,
};
}
}
if (source.type === "milestone" && targetBranch === "main") {
const phaseBranches = branches.filter(
(b) => b.type === "phase" && !b.merged
);
if (phaseBranches.length > 0) {
return {
valid: false,
reason: `Milestone cannot merge to main: ${phaseBranches.length} unmerged phase branch(es) remain.`,
};
}
}
return { valid: true, reason: "Merge order is valid" };
}
computeMilestoneTag(milestoneType: MilestoneType): string {
const tags = this.git("tag -l").split("\n").map((t) => t.trim()).filter(Boolean);
let major = 0;
let minor = 0;
let patch = 0;
for (const tag of tags) {
const match = tag.match(/^v(\d+)\.(\d+)\.(\d+)$/);
if (match) {
const m = parseInt(match[1]);
const n = parseInt(match[2]);
const p = parseInt(match[3]);
if (m > major || (m === major && n > minor) || (m === major && n === minor && p > patch)) {
major = m;
minor = n;
patch = p;
}
}
}
if (milestoneType === "schema-breaking") {
return `v${major + 1}.0.0`;
}
return `v${major}.${minor + 1}.0`;
}
getPhaseStatus(phaseNumber: number): PhaseBranchInfo | null {
const branches = this.gitContext.getBranches();
const phaseBranch = branches.find(
+43 -2
View File
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
import { GitContext } from "../core/git-context.js";
function createTempRepo(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-test-"));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-test-"));
execSync("git init", { cwd: dir, stdio: "pipe" });
execSync('git config user.email "test@test.com"', { cwd: dir, stdio: "pipe" });
execSync('git config user.name "Test"', { cwd: dir, stdio: "pipe" });
@@ -41,7 +41,7 @@ describe("GitContext", () => {
});
it("returns false for non-git directory", () => {
const nonGit = fs.mkdtempSync(path.join(os.tmpdir(), "ci-nongit-"));
const nonGit = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-nongit-"));
const ctx = new GitContext(nonGit);
expect(ctx.isGitRepo()).toBe(false);
cleanup(nonGit);
@@ -279,4 +279,45 @@ status: execute
expect(ctx.isNfrMilestone()).toBe(false);
});
});
describe("getMilestoneType", () => {
it("returns nfr when only NFR commits exist", () => {
commit(repoDir, `chore(P01): cleanup
---ci---
phase: 1
milestone: v0.1.1
status: execute
---/ci---`);
const ctx = new GitContext(repoDir);
expect(ctx.getMilestoneType()).toBe("nfr");
});
it("returns feature when feat commits exist", () => {
commit(repoDir, `feat(P01): add feature
---ci---
phase: 1
milestone: v1.0
status: execute
---/ci---`);
const ctx = new GitContext(repoDir);
expect(ctx.getMilestoneType()).toBe("feature");
});
it("returns schema-breaking when refactor commits exist", () => {
commit(repoDir, `refactor(P01): rewrite core
---ci---
phase: 1
milestone: v0.5
status: execute
---/ci---`);
const ctx = new GitContext(repoDir);
expect(ctx.getMilestoneType()).toBe("schema-breaking");
});
});
});
+24 -15
View File
@@ -1,12 +1,14 @@
import { execSync } from "node:child_process";
import {
ParsedCiCommit,
CiMetadata,
ParsedCIAgentCommit,
CIAgentMetadata,
CommitDecision,
} from "../types/commit-meta.js";
import { parseCommitMessage } from "./commit-parser.js";
import { PipelineStage } from "../types/pipeline.js";
import { MilestoneType } from "../types/config.js";
export interface ProjectState {
currentPhase: number;
currentMilestone: string;
@@ -14,7 +16,7 @@ export interface ProjectState {
phasesCompleted: number[];
phaseBranches: BranchInfo[];
milestoneBranches: string[];
lastCommit: ParsedCiCommit | null;
lastCommit: ParsedCIAgentCommit | null;
}
export interface BranchInfo {
@@ -67,13 +69,13 @@ export class GitContext {
return this.git("rev-parse --abbrev-ref HEAD");
}
getRecentCommits(count: number = 20): ParsedCiCommit[] {
getRecentCommits(count: number = 20): ParsedCIAgentCommit[] {
const format = "%H%x00%s%x00%B%x01";
const raw = this.git(`log --max-count=${count} --format="${format}"`);
if (!raw) return [];
const commits: ParsedCiCommit[] = [];
const commits: ParsedCIAgentCommit[] = [];
const entries = raw.split("\x01").filter(Boolean);
for (const entry of entries) {
@@ -91,7 +93,7 @@ export class GitContext {
return commits;
}
getLatestCiCommit(): ParsedCiCommit | null {
getLatestCiCommit(): ParsedCIAgentCommit | null {
const commits = this.getRecentCommits(1);
return commits.length > 0 ? commits[0] : null;
}
@@ -205,7 +207,7 @@ export class GitContext {
return decisions;
}
getDecisionsFromCommits(commits: ParsedCiCommit[], phase?: number): CommitDecision[] {
getDecisionsFromCommits(commits: ParsedCIAgentCommit[], phase?: number): CommitDecision[] {
const decisions: CommitDecision[] = [];
for (const commit of commits) {
if (commit.ci?.decisions) {
@@ -298,20 +300,20 @@ export class GitContext {
};
}
getCommitsForPhase(phase: number): ParsedCiCommit[] {
getCommitsForPhase(phase: number): ParsedCIAgentCommit[] {
const commits = this.getRecentCommits(200);
return commits.filter(
(c) => c.scope === `P${String(phase).padStart(2, "0")}` || c.ci?.phase === phase
);
}
getCommitsForBranch(branch: string): ParsedCiCommit[] {
getCommitsForBranch(branch: string): ParsedCIAgentCommit[] {
const format = "%H%x00%s%x00%B%x01";
const raw = this.git(`log ${branch} --max-count=100 --format="${format}"`);
if (!raw) return [];
const commits: ParsedCiCommit[] = [];
const commits: ParsedCIAgentCommit[] = [];
const entries = raw.split("\x01").filter(Boolean);
for (const entry of entries) {
@@ -342,13 +344,20 @@ export class GitContext {
return null;
}
isNfrMilestone(): boolean {
getMilestoneType(): MilestoneType {
const commits = this.getRecentCommits(100);
let hasAnyCiCommit = false;
for (const commit of commits) {
if (commit.type === "feat" && commit.ci) {
return false;
}
if (!commit.ci) continue;
hasAnyCiCommit = true;
if (commit.type === "feat") return "feature";
if (commit.type === "refactor" || commit.scope === "init") return "schema-breaking";
}
return true;
if (!hasAnyCiCommit) return "nfr";
return "nfr";
}
isNfrMilestone(): boolean {
return this.getMilestoneType() === "nfr";
}
}
+5 -5
View File
@@ -1,12 +1,12 @@
export { initCI, loadConfig, saveConfig, isCIInitialized, getCIConfigPath, getCIDir, ensureCIDir } from "./config.js";
export { initCIAgent, loadConfig, saveConfig, isCIAgentInitialized, getCIAgentConfigPath, getCIDir, ensureCIDir } from "./config.js";
export { DecisionEngine } from "./decision-engine.js";
export { EscalationProtocol } from "./escalation.js";
export { ClarifyPhase } from "./clarify.js";
export { CiFiles } from "./ci-files.js";
export { CIAgentFiles } from "./ciagent-files.js";
export { ErrorRecovery } from "./error-recovery.js";
export { GitContext } from "./git-context.js";
export { GitBranch } from "./git-branch.js";
export { CommitBuilder } from "./commit-builder.js";
export { extractCiBlock, parseCiBlock, parseCommitMessage } from "./commit-parser.js";
export type { CIConfig } from "../types/config.js";
export { DEFAULT_CI_CONFIG } from "../types/config.js";
export { extractCIAgentBlock, parseCIAgentBlock, parseCommitMessage } from "./commit-parser.js";
export type { CIAgentConfig } from "../types/config.js";
export { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
+9 -7
View File
@@ -2,20 +2,22 @@ export { OrchestratorAgent } from "./agents/orchestrator.js";
export { DecisionEngine } from "./core/decision-engine.js";
export { EscalationProtocol } from "./core/escalation.js";
export { ClarifyPhase } from "./core/clarify.js";
export { CiFiles } from "./core/ci-files.js";
export { CIAgentFiles } from "./core/ciagent-files.js";
export { ErrorRecovery } from "./core/error-recovery.js";
export { GitContext } from "./core/git-context.js";
export { GitBranch } from "./core/git-branch.js";
export { CommitBuilder } from "./core/commit-builder.js";
export { extractCiBlock, parseCiBlock, parseCommitMessage } from "./core/commit-parser.js";
export { extractCIAgentBlock, parseCIAgentBlock, parseCommitMessage } from "./core/commit-parser.js";
export { VerificationPipeline } from "./verification/index.js";
export { StructuralVerification } from "./verification/structural.js";
export { BehavioralVerification } from "./verification/behavioral.js";
export { SecurityVerification } from "./verification/security.js";
export { QualityVerification } from "./verification/quality.js";
export { getAgent, getAvailableAgents } from "./agents/index.js";
export { initCI, loadConfig, saveConfig, isCIInitialized } from "./core/config.js";
export { DEFAULT_CI_CONFIG } from "./types/config.js";
export type { PlannerResult } from "./agents/planner.js";
export type { ExecutorResult } from "./agents/executor.js";
export { initCIAgent, loadConfig, saveConfig, isCIAgentInitialized } from "./core/config.js";
export { DEFAULT_CIAGENT_CONFIG } from "./types/config.js";
export { confidenceToLevel, shouldEscalate } from "./types/decisions.js";
export { ESCALATION_TYPES } from "./types/escalation.js";
export { createClarifyQuestion } from "./types/clarify.js";
@@ -28,7 +30,7 @@ export { OllamaLocalBackend } from "./backends/ollama-local.js";
export { OllamaCloudBackend } from "./backends/ollama-cloud.js";
export { ToolRegistry } from "./backends/tool-registry.js";
export type { CIConfig, AutonomyLevel, ModelProfile } from "./types/config.js";
export type { CIAgentConfig, AutonomyLevel, ModelProfile } from "./types/config.js";
export type { Decision, DecisionCategory } from "./types/decisions.js";
export type { Escalation, EscalationType } from "./types/escalation.js";
export type { PipelineState, PhaseResult, OrchestratorResult } from "./types/pipeline.js";
@@ -38,9 +40,9 @@ export type { AgentContext, AgentResult } from "./agents/base.js";
export type { LayeredVerificationResult } from "./verification/index.js";
export type { VerificationResult, VerificationCheck } from "./verification/types.js";
export type { AgentName } from "./types/config.js";
export type { CiMetadata, ParsedCiCommit, CommitType, CommitScope, CommitDecision, CommitEscalation, CommitRequirements, CommitCompoundMeta } from "./types/commit-meta.js";
export type { CIAgentMetadata, ParsedCIAgentCommit, CommitType, CommitScope, CommitDecision, CommitEscalation, CommitRequirements, CommitCompoundMeta } from "./types/commit-meta.js";
export type { ProjectState, BranchInfo } from "./core/git-context.js";
export type { PhaseBranchInfo, MilestoneBranchInfo, BranchCreateResult, BranchMergeResult } from "./core/git-branch.js";
export type { ProjectMd, RoadmapMd, RequirementsMd, ArchitectureMd } from "./core/ci-files.js";
export type { ProjectMd, RoadmapMd, RequirementsMd, ArchitectureMd } from "./core/ciagent-files.js";
export type { IntelligenceBackend, BackendRequest, BackendResult, BackendConfigSection, BackendUnavailableError, Artifact, TokenUsage } from "./backends/types.js";
export type { ToolDefinition, ToolCall, ToolResult } from "./backends/tool-registry.js";
+3 -3
View File
@@ -51,7 +51,7 @@ export interface CommitCompoundMeta {
solution: string;
}
export interface CiMetadata {
export interface CIAgentMetadata {
phase: number;
milestone: string;
project?: string;
@@ -65,12 +65,12 @@ export interface CiMetadata {
compound?: CommitCompoundMeta;
}
export interface ParsedCiCommit {
export interface ParsedCIAgentCommit {
hash: string;
type: CommitType;
scope: string;
subject: string;
ci: CiMetadata | null;
ci: CIAgentMetadata | null;
body: string;
}
+32 -32
View File
@@ -1,33 +1,33 @@
import { CIConfig, DEFAULT_CI_CONFIG, AutonomyLevel, ModelProfile, ProjectEntry } from "../types/config.js";
import { CIAgentConfig, DEFAULT_CIAGENT_CONFIG, AutonomyLevel, ModelProfile, ProjectEntry } from "../types/config.js";
describe("CIConfig", () => {
it("DEFAULT_CI_CONFIG has all required fields", () => {
expect(DEFAULT_CI_CONFIG.autonomy.level).toBe("full");
expect(DEFAULT_CI_CONFIG.autonomy.clarify_budget).toBe(10);
expect(DEFAULT_CI_CONFIG.autonomy.decision_confidence_threshold).toBe(0.6);
expect(DEFAULT_CI_CONFIG.autonomy.max_revision_iterations).toBe(3);
expect(DEFAULT_CI_CONFIG.autonomy.max_verification_retries).toBe(2);
expect(DEFAULT_CI_CONFIG.autonomy.escalation_timeout_ms).toBe(300000);
expect(DEFAULT_CI_CONFIG.autonomy.escalation_hooks).toContain("deploy");
expect(DEFAULT_CI_CONFIG.model_profile).toBe("quality");
expect(DEFAULT_CI_CONFIG.parallelization.enabled).toBe(true);
expect(DEFAULT_CI_CONFIG.verification.automated_only).toBe(true);
expect(DEFAULT_CI_CONFIG.security.auto_accept_low_severity).toBe(true);
expect(DEFAULT_CI_CONFIG.git.auto_commit).toBe(true);
expect(DEFAULT_CI_CONFIG.git.auto_push).toBe(false);
describe("CIAgentConfig", () => {
it("DEFAULT_CIAGENT_CONFIG has all required fields", () => {
expect(DEFAULT_CIAGENT_CONFIG.autonomy.level).toBe("full");
expect(DEFAULT_CIAGENT_CONFIG.autonomy.clarify_budget).toBe(10);
expect(DEFAULT_CIAGENT_CONFIG.autonomy.decision_confidence_threshold).toBe(0.6);
expect(DEFAULT_CIAGENT_CONFIG.autonomy.max_revision_iterations).toBe(3);
expect(DEFAULT_CIAGENT_CONFIG.autonomy.max_verification_retries).toBe(2);
expect(DEFAULT_CIAGENT_CONFIG.autonomy.escalation_timeout_ms).toBe(300000);
expect(DEFAULT_CIAGENT_CONFIG.autonomy.escalation_hooks).toContain("deploy");
expect(DEFAULT_CIAGENT_CONFIG.model_profile).toBe("quality");
expect(DEFAULT_CIAGENT_CONFIG.parallelization.enabled).toBe(true);
expect(DEFAULT_CIAGENT_CONFIG.verification.automated_only).toBe(true);
expect(DEFAULT_CIAGENT_CONFIG.security.auto_accept_low_severity).toBe(true);
expect(DEFAULT_CIAGENT_CONFIG.git.auto_commit).toBe(true);
expect(DEFAULT_CIAGENT_CONFIG.git.auto_push).toBe(false);
});
it("DEFAULT_CI_CONFIG has multi-project fields", () => {
expect(DEFAULT_CI_CONFIG.projects).toEqual([]);
expect(DEFAULT_CI_CONFIG.active_project).toBe("");
it("DEFAULT_CIAGENT_CONFIG has multi-project fields", () => {
expect(DEFAULT_CIAGENT_CONFIG.projects).toEqual([]);
expect(DEFAULT_CIAGENT_CONFIG.active_project).toBe("");
});
it("AutonomyLevel accepts all valid levels", () => {
const levels: AutonomyLevel[] = ["full", "supervised", "guided"];
for (const level of levels) {
const config: CIConfig = {
...DEFAULT_CI_CONFIG,
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level },
const config: CIAgentConfig = {
...DEFAULT_CIAGENT_CONFIG,
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, level },
};
expect(config.autonomy.level).toBe(level);
}
@@ -36,8 +36,8 @@ describe("CIConfig", () => {
it("ModelProfile accepts all valid profiles", () => {
const profiles: ModelProfile[] = ["quality", "speed", "balanced"];
for (const profile of profiles) {
const config: CIConfig = {
...DEFAULT_CI_CONFIG,
const config: CIAgentConfig = {
...DEFAULT_CIAGENT_CONFIG,
model_profile: profile,
};
expect(config.model_profile).toBe(profile);
@@ -45,7 +45,7 @@ describe("CIConfig", () => {
});
it("escalation_hooks defaults include expected items", () => {
expect(DEFAULT_CI_CONFIG.autonomy.escalation_hooks).toEqual([
expect(DEFAULT_CIAGENT_CONFIG.autonomy.escalation_hooks).toEqual([
"deploy",
"delete_data",
"merge_to_main",
@@ -66,10 +66,10 @@ describe("CIConfig", () => {
});
});
describe("CIConfig with projects", () => {
describe("CIAgentConfig with projects", () => {
it("supports multiple projects", () => {
const config: CIConfig = {
...DEFAULT_CI_CONFIG,
const config: CIAgentConfig = {
...DEFAULT_CIAGENT_CONFIG,
projects: [
{ slug: "task-api", name: "Task API", default: true },
{ slug: "auth-svc", name: "Auth Service" },
@@ -82,8 +82,8 @@ describe("CIConfig", () => {
});
it("supports single project", () => {
const config: CIConfig = {
...DEFAULT_CI_CONFIG,
const config: CIAgentConfig = {
...DEFAULT_CIAGENT_CONFIG,
projects: [{ slug: "my-app", name: "My App", default: true }],
active_project: "my-app",
};
@@ -92,8 +92,8 @@ describe("CIConfig", () => {
});
it("defaults to empty projects array and empty active_project", () => {
expect(DEFAULT_CI_CONFIG.projects).toEqual([]);
expect(DEFAULT_CI_CONFIG.active_project).toBe("");
expect(DEFAULT_CIAGENT_CONFIG.projects).toEqual([]);
expect(DEFAULT_CIAGENT_CONFIG.active_project).toBe("");
});
});
});
+6 -3
View File
@@ -6,6 +6,8 @@ export type ModelProfile = "quality" | "speed" | "balanced";
export type BranchingStrategy = "phase" | "feature" | "trunk";
export type MilestoneType = "nfr" | "feature" | "schema-breaking";
export type PhaseName = "research" | "plan" | "execute" | "verify" | "complete";
export type AgentName =
@@ -26,7 +28,8 @@ export type AgentName =
| "plan-checker"
| "project-researcher"
| "research-synthesizer"
| "solution-writer";
| "solution-writer"
| "tester";
export interface AutonomyConfig {
level: AutonomyLevel;
@@ -69,7 +72,7 @@ export interface ProjectEntry {
default?: boolean;
}
export interface CIConfig {
export interface CIAgentConfig {
projects: ProjectEntry[];
active_project: string;
autonomy: AutonomyConfig;
@@ -81,7 +84,7 @@ export interface CIConfig {
backend: BackendConfigSection;
}
export const DEFAULT_CI_CONFIG: CIConfig = {
export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
projects: [],
active_project: "",
autonomy: {
+3 -3
View File
@@ -3,11 +3,11 @@ import { confidenceToLevel, shouldEscalate } from "../types/decisions.js";
import { ESCALATION_TYPES } from "../types/escalation.js";
import { parseSpecification } from "../types/specification.js";
import { createClarifyQuestion } from "../types/clarify.js";
import { DEFAULT_CI_CONFIG } from "../types/config.js";
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
describe("Type exports", () => {
it("pipeline types are importable and functional", () => {
expect(STAGE_ORDER).toHaveLength(7);
expect(STAGE_ORDER).toHaveLength(8);
expect(getNextStage("specify")).toBe("clarify");
const state = createInitialPipelineState("/tmp/test");
expect(state.current_stage).toBe("specify");
@@ -40,6 +40,6 @@ describe("Type exports", () => {
});
it("config defaults are importable", () => {
expect(DEFAULT_CI_CONFIG.autonomy.level).toBe("full");
expect(DEFAULT_CIAGENT_CONFIG.autonomy.level).toBe("full");
});
});

Some files were not shown because too many files have changed in this diff Show More