Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a58aa1657 | |||
| e31afe3b59 | |||
| ab6af144b7 | |||
| 3d069319b5 | |||
| b33431c1a6 | |||
| 5753e2dc96 | |||
| 815c928a43 | |||
| a82926a22e | |||
| fb3f1df13e | |||
| 7a20784c87 | |||
| 940b85bfae | |||
| ddf04792c7 | |||
| e4bb3a9970 | |||
| 2f738c33b7 | |||
| eedcdd4282 |
Executable
+80
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
# AGENTS.md — CI Project Guidelines
|
||||
# AGENTS.md — CIAgent Project Guidelines
|
||||
|
||||
## Build & Run Commands
|
||||
|
||||
@@ -9,30 +9,38 @@
|
||||
|
||||
## 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 (all extend BaseAgent)
|
||||
agents/ # 19 agent implementations (persona loaders delegating to backends)
|
||||
backends/ # Intelligence backend layer
|
||||
types.ts # IntelligenceBackend, BackendRequest, BackendResult, BackendConfigSection
|
||||
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)
|
||||
opencode.ts # OpencodeBackend (shells out to opencode --non-interactive)
|
||||
index.ts # Backend registry + auto-detection
|
||||
cli/ # Commander.js CLI (commands.ts, index.ts)
|
||||
core/ # Core engine components
|
||||
artifacts.ts # Legacy .planning/ 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
|
||||
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
|
||||
@@ -41,11 +49,11 @@ src/
|
||||
utils/ # File utilities (readFile, writeFile, ensureDir, readJSON, writeJSON)
|
||||
verification/ # 4-layer verification pipeline
|
||||
structural.ts # Layer 1: file existence, imports wired, no stubs
|
||||
behavioral.ts # Layer 2: test generation and execution (stub)
|
||||
security.ts # Layer 3: STRIDE threat analysis (stub)
|
||||
quality.ts # Layer 4: multi-persona code review (stub)
|
||||
behavioral.ts # Layer 2: test infrastructure checks (static analysis, no test generation yet)
|
||||
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.2.0"
|
||||
version.ts # VERSION = "0.6.0"
|
||||
templates/ # Template files (config.json, DECISIONS.md, specification.md)
|
||||
```
|
||||
|
||||
@@ -54,18 +62,18 @@ 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.60–0.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** inherited from Learnship, all re-prompted 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
|
||||
|
||||
- **Language**: TypeScript with ES2022 target, Node16 modules
|
||||
- **Module resolution**: Node16 style with `.js` extensions in imports
|
||||
- **Agent pattern**: All agents extend `BaseAgent` with `name`, `description`, and `execute(context: AgentContext): Promise<AgentResult>`
|
||||
- **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
|
||||
@@ -77,7 +85,26 @@ templates/ # Template files (config.json, DECISIONS.md, specification.md
|
||||
SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → VERIFY → COMPLETE
|
||||
```
|
||||
|
||||
Each stage is executed by `OrchestratorAgent.executeStage()`. The orchestrator iterates through `STAGE_ORDER` and collects `PhaseResult` for each.
|
||||
Each stage is executed by `OrchestratorAgent.executeStage()`. The orchestrator delegates intelligent stages (research, plan, execute, verify) to specialized agents via `context.backend` when available, falling back to mechanical execution when no backend is configured. Mechanical stages (specify, clarify, complete) are always handled by the orchestrator directly.
|
||||
|
||||
## Intelligence Backend Architecture
|
||||
|
||||
```
|
||||
IntelligenceBackend (unified interface)
|
||||
├── 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, CIAgent sends request)
|
||||
├── OpencodeBackend (opencode --non-interactive)
|
||||
└── (future: Codex, Claude Code, Hermes, etc.)
|
||||
```
|
||||
|
||||
- **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**: `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)
|
||||
|
||||
@@ -95,26 +122,26 @@ Each stage is executed by `OrchestratorAgent.executeStage()`. The orchestrator i
|
||||
## Verification Layers
|
||||
|
||||
1. **Structural**: Files exist, imports wired, no stubs/TODOs
|
||||
2. **Behavioral**: Generate and run automated tests for must-haves (currently stub)
|
||||
3. **Security**: STRIDE analysis with auto-disposition (currently stub)
|
||||
4. **Code Quality**: Multi-persona review with P0 auto-fix (currently stub)
|
||||
2. **Behavioral**: Check test infrastructure and requirement traceability (static analysis — test generation not yet implemented)
|
||||
3. **Security**: Regex-based threat pattern scanning with auto-disposition (STRIDE analysis not yet implemented)
|
||||
4. **Code Quality**: Regex-based code quality checks (multi-persona review not yet implemented)
|
||||
|
||||
## Testing
|
||||
|
||||
- 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
|
||||
|
||||
@@ -164,15 +191,16 @@ Each stage is executed by `OrchestratorAgent.executeStage()`. The orchestrator i
|
||||
|
||||
## Current State
|
||||
|
||||
- **v0.2.0**: Git-native architecture — project memory lives in git log, not `.planning/` files
|
||||
- **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**: Stub agents return success immediately. Real LLM-based agent implementations are needed for research, planning, execution, verification, etc.
|
||||
- **Tests**: 25 test suites, 218 tests covering types, config, decision-engine, escalation, clarify, commit-parser, commit-builder, git-context, git-branch, ci-files, all 4 verification layers, file utils
|
||||
- **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**: 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
|
||||
@@ -1,22 +1,20 @@
|
||||
# CI — Continuous Intelligence
|
||||
# CIAgent — Continuous Intelligence
|
||||
|
||||
Fully autonomous AI-driven software engineering harness.
|
||||
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.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g @continuous-intelligence/ci
|
||||
```
|
||||
|
||||
Or from source:
|
||||
From source (package not yet published to npm):
|
||||
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
cd ci
|
||||
git clone https://git.cloudinit.dev/continuous-intelligence/ci.git
|
||||
cd ciagent
|
||||
npm install
|
||||
npm run build
|
||||
npm link
|
||||
@@ -26,57 +24,151 @@ 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
|
||||
|
||||
# Initialize with interactive clarify phase
|
||||
ci init --clarify "Build a REST API for task management"
|
||||
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"
|
||||
|
||||
# Verify a phase
|
||||
ci verify 1
|
||||
# Check project status (reads from git log + branches)
|
||||
ciagent status
|
||||
|
||||
# Check project status
|
||||
ci status
|
||||
|
||||
# Review autonomous decisions
|
||||
ci audit
|
||||
ci audit --verbose
|
||||
# Review autonomous decisions (extracted from git log ---ci--- blocks)
|
||||
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 CIAgent-generated commit contains a `---ci---` YAML block with structured metadata:
|
||||
|
||||
```
|
||||
feat(P01-01-02): create user registration endpoint
|
||||
|
||||
---ci---
|
||||
phase: 1
|
||||
milestone: v1.0
|
||||
plan: 01-01
|
||||
task: 01-01-02
|
||||
status: execute
|
||||
decisions:
|
||||
- id: D-003
|
||||
decision: Use bcrypt with 12 rounds for password hashing
|
||||
rationale: Industry standard; argon2 not available in target env
|
||||
confidence: 0.88
|
||||
alternatives: [argon2, scrypt]
|
||||
requirements:
|
||||
covered: [AUTH-01]
|
||||
---/ci---
|
||||
|
||||
- POST /auth/register validates email and password
|
||||
- Checks for duplicate users
|
||||
- Returns JWT token on success
|
||||
```
|
||||
|
||||
### What Lives Where
|
||||
|
||||
| Where | What | Why |
|
||||
|-------|------|-----|
|
||||
| `.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 |
|
||||
|
||||
### Branch Strategy
|
||||
|
||||
```
|
||||
main
|
||||
└── milestone/v1.0-mvp
|
||||
├── phase/01-authentication # in progress if not merged
|
||||
├── phase/02-task-management
|
||||
└── phase/03-realtime-notifications
|
||||
```
|
||||
|
||||
- Branch exists + not merged = phase **in progress**
|
||||
- Branch merged to milestone = phase **complete**
|
||||
- Milestone branch merged to main = milestone **complete**
|
||||
|
||||
### Context Reconstruction Protocol
|
||||
|
||||
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. `.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 `.ciagent/` files) can reconstruct:
|
||||
|
||||
| Reconstructable | How |
|
||||
|---------------|-----|
|
||||
| Project specification | Init commit body |
|
||||
| Current phase | `---ci---.phase` field + branch status |
|
||||
| Current milestone | Branch names + `---ci---.milestone` field |
|
||||
| All decisions with rationale | `git log --grep="decisions:" --format="%b"` |
|
||||
| Decision confidence | Each decision has `confidence: 0.XX` |
|
||||
| Alternatives considered | Each decision has `alternatives: [...]` |
|
||||
| Requirements coverage | `git log --grep="requirements:" --format="%b"` |
|
||||
| Lessons learned | `git log --grep="lessons:" --format="%b"` |
|
||||
| Compounded solutions | `git log --grep="compound:" --format="%b"` |
|
||||
| Escalation history | `git log --grep="escalation:" --format="%b"` |
|
||||
|
||||
### Commit Types
|
||||
|
||||
In addition to conventional commit types, CIAgent uses:
|
||||
|
||||
| Type | When Used |
|
||||
|------|-----------|
|
||||
| `decision` | Autonomous decision logged (no code change) |
|
||||
| `compound` | Compounded solution captured |
|
||||
| `escalation` | Escalation raised or resolved |
|
||||
| `verify` | Verification pass/fail |
|
||||
| `wip` | Work-in-progress checkpoint |
|
||||
|
||||
## Autonomy Levels
|
||||
|
||||
| Level | Behavior |
|
||||
|-------|----------|
|
||||
| `full` | No human interaction after Clarify. Escalate only irreversible decisions. |
|
||||
| `supervised` | Escalate on every Escalation Gate plus verification failures. |
|
||||
| `guided` | Escalate on every Decision Gate. Closest to Learnship behavior. |
|
||||
| `guided` | Escalate on every Decision Gate. |
|
||||
|
||||
## Configuration
|
||||
|
||||
CI uses `.ci/config.json` for project configuration:
|
||||
CIAgent uses `.ciagent/config.json` for project configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -120,24 +212,34 @@ CI uses `.ci/config.json` for project configuration:
|
||||
|
||||
```
|
||||
SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → VERIFY → COMPLETE
|
||||
↕ ↕ ↕ ↕
|
||||
(questions) (auto-decide) (auto-run) (auto-verify)
|
||||
↕ ↕ ↕ ↕
|
||||
(questions) (auto-decide) (auto-run) (auto-verify)
|
||||
```
|
||||
|
||||
### Git-Native Core Modules
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| `commit-parser` | `---ci---` YAML block extraction and parsing from commit messages |
|
||||
| `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 |
|
||||
| `ciagent-files` | `.ciagent/` long-lived reference file management with update discipline |
|
||||
|
||||
### Decision Engine
|
||||
|
||||
Every autonomous decision is classified by confidence:
|
||||
- **High (>0.85)**: Auto-decide, log to audit trail
|
||||
- **High (>0.85)**: Auto-decide, commit as `---ci---` block
|
||||
- **Medium (0.60-0.85)**: Auto-decide with assumption logging, flag for review
|
||||
- **Low (<0.60)**: Escalate to human
|
||||
|
||||
Decisions are committed to git as `decision` type commits. The audit trail is `git log --grep="decisions:"`.
|
||||
|
||||
### 18 Agents
|
||||
|
||||
All 17 Learnship agents retained, plus the CI Orchestrator:
|
||||
|
||||
| Agent | Role | Modification |
|
||||
|-------|------|-------------|
|
||||
| orchestrator | Pipeline controller | New — replaces interactive workflows |
|
||||
| Agent | Role | CIAgent Modification |
|
||||
|-------|------|----------------|
|
||||
| orchestrator | Pipeline controller | Git-first context loading, `---ci---` commit generation |
|
||||
| planner | Plan creation | Never sets `autonomous: false` |
|
||||
| executor | Task execution | Never pauses for checkpoints |
|
||||
| verifier | Output verification | Generates automated tests, not human UAT |
|
||||
@@ -145,7 +247,7 @@ All 17 Learnship agents retained, plus the CI Orchestrator:
|
||||
| challenger | Plan stress-testing | Binding verdicts, only escalates <0.60 |
|
||||
| security-auditor | Security audit | Auto-dispositions threats |
|
||||
| debugger | Bug fixing | Auto-fixes when confidence > threshold |
|
||||
| Others | Various | Unchanged from Learnship |
|
||||
| Others | Various | Retained from Learnship |
|
||||
|
||||
### Verification Layers
|
||||
|
||||
@@ -178,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
|
||||
@@ -186,17 +288,30 @@ When CI cannot proceed autonomously:
|
||||
4. **Security Escalation**: High-severity threat detected
|
||||
5. **Specification Ambiguity**: Multiple valid interpretations
|
||||
|
||||
Each escalation includes a recommended default with auto-proceed timeout.
|
||||
Each escalation is committed as an `escalation` type commit. Resolved escalations produce a follow-up commit with the resolution. The full escalation history is available via `git log --grep="escalation:"`.
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- **Agent implementations are stubs**: All 18 agents return success immediately. Real LLM-based agent implementations are needed for research, planning, execution, and verification.
|
||||
- **Package not published to npm**: Install from source only until a publishing pipeline is configured.
|
||||
- **Behavioral/Security/Quality verification layers**: Structural verification is fully implemented; behavioral, security, and quality layers are partially stubbed.
|
||||
|
||||
## Differences from Learnship
|
||||
|
||||
| Dimension | Learnship | CI |
|
||||
| Dimension | Learnship | CIAgent |
|
||||
|-----------|-----------|-----|
|
||||
| Project memory | `.planning/` directory files (legacy) | Git log + `---ci---` commit blocks |
|
||||
| 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 |
|
||||
| Decision Making | Human decides, agent implements | Agent decides, human reviews post-hoc |
|
||||
| Verification | Human UAT | Automated tests + escalation |
|
||||
| Specification | Multi-round conversation | Single spec file |
|
||||
| Learning Curve | Moderate | Low (5 core commands) |
|
||||
|
||||
## Repository
|
||||
|
||||
[git.cloudinit.dev/continuous-intelligence/ci](https://git.cloudinit.dev/continuous-intelligence/ci)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
grep: true
|
||||
glob: true
|
||||
---
|
||||
|
||||
<role>
|
||||
You are a CIAgent challenger. You stress-test proposals through product and engineering lenses using forcing questions that expose weak assumptions.
|
||||
|
||||
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 .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
|
||||
- .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 `.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>
|
||||
|
||||
<execution_flow>
|
||||
|
||||
## Step 1: Load Context
|
||||
|
||||
Read the proposal and all git context. Extract settled decisions that should not be re-litigated.
|
||||
|
||||
## Step 2: Challenge Through Lens
|
||||
|
||||
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 .ciagent/ files
|
||||
3. Note confidence level for each answer
|
||||
|
||||
### Product Lens Questions
|
||||
1. Who specifically wants this?
|
||||
2. What do they do today without it?
|
||||
3. How would you know it succeeded?
|
||||
4. What's the narrowest version that still delivers value?
|
||||
5. What are you saying NO to by building this?
|
||||
|
||||
### Engineering Lens Questions
|
||||
1. What's the complexity ceiling?
|
||||
2. What existing patterns does this break?
|
||||
3. What's the failure mode?
|
||||
4. What does this make harder later?
|
||||
5. Is there a simpler approach that delivers 80%?
|
||||
|
||||
## Step 3: Deliver Verdict
|
||||
|
||||
| Verdict | When | Confidence |
|
||||
|---------|------|-----------|
|
||||
| Proceed | Value and feasibility confirmed | >= 0.60 |
|
||||
| Reduce scope | Core value real but scope too broad | >= 0.60 |
|
||||
| Rethink | Fundamental concerns | >= 0.60 |
|
||||
| Escalate | Cannot determine with confidence | < 0.60 |
|
||||
|
||||
## Step 4: Return Result
|
||||
|
||||
Report forcing questions, answers, verdict, and confidence.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
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
|
||||
edit: true
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
You are a CIAgent code reviewer. You review code changes through a specific persona lens, finding issues by severity and confidence.
|
||||
|
||||
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 .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
|
||||
- .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:
|
||||
|
||||
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 `.ciagent/ARCHITECTURE.md` for component boundaries
|
||||
5. Read `./AGENTS.md` for project conventions and coding standards
|
||||
</project_context>
|
||||
|
||||
<execution_flow>
|
||||
|
||||
## Step 1: Load Changes
|
||||
|
||||
Read the diff or files to review. Load git context for relevant decisions.
|
||||
|
||||
## Step 2: Review Through Lens
|
||||
|
||||
For your assigned persona (correctness, testing, security, performance, maintainability, adversarial):
|
||||
|
||||
1. Check for issues specific to your persona
|
||||
2. Classify each issue by severity: P0 (blocking), P1 (important), P2 (nit)
|
||||
3. Note specific file:line for every finding
|
||||
4. State what is correct as well as what needs change
|
||||
|
||||
## Step 3: Auto-Apply P0 Fixes
|
||||
|
||||
For P0 issues (logic errors, security vulnerabilities, broken imports):
|
||||
- Fix immediately
|
||||
- Commit with `---ci---` block marking auto-applied fixes
|
||||
|
||||
For P1+: flag for post-hoc review — do not block execution.
|
||||
|
||||
## Step 4: Commit Review
|
||||
|
||||
```
|
||||
verify(P##): code review — [persona]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: verify
|
||||
lessons:
|
||||
- [P0 fix applied: description]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 5: Return Result
|
||||
|
||||
Report findings by severity, P0 fixes applied, P1+ flags for post-hoc review.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
description: Investigates bugs using systematic hypothesis testing — traces from symptoms to root cause. Auto-diagnoses and auto-fixes when confidence > 0.60.
|
||||
color: "#FFA500"
|
||||
tools:
|
||||
read: true
|
||||
write: true
|
||||
edit: true
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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.
|
||||
|
||||
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 .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
|
||||
- .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:
|
||||
|
||||
1. Run `git log --max-count=20` for recent changes that may have caused the bug
|
||||
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 `.ciagent/ARCHITECTURE.md` for component boundaries
|
||||
</project_context>
|
||||
|
||||
<execution_flow>
|
||||
|
||||
## Step 1: Load Context
|
||||
|
||||
Read the bug description. Load git history for recent changes. Read project conventions.
|
||||
|
||||
## Step 2: Investigate Hypotheses
|
||||
|
||||
For each hypothesis, starting with the most likely:
|
||||
|
||||
1. Plan the investigation — identify key files to check
|
||||
2. Trace the code path from symptom inward
|
||||
3. Read all files in the code path
|
||||
4. Confirm or deny: "If this were fixed, would the symptom go away?"
|
||||
|
||||
## Step 3: Auto-Fix or Escalate
|
||||
|
||||
| Confidence | Action |
|
||||
|-----------|--------|
|
||||
| High (> 0.85) | Auto-fix immediately, commit with `---ci---` block |
|
||||
| Medium (0.60–0.85) | Auto-fix with assumption logging, commit |
|
||||
| Low (< 0.60) | Escalate with proposed fix, wait for human |
|
||||
|
||||
## Step 4: Commit Fix
|
||||
|
||||
```
|
||||
fix(P##): [root cause description]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: execute
|
||||
decisions:
|
||||
- id: D-XXX
|
||||
decision: [fix approach]
|
||||
rationale: [evidence]
|
||||
confidence: 0.XX
|
||||
alternatives: []
|
||||
lessons:
|
||||
- [lesson learned from this bug]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 5: Return Result
|
||||
|
||||
Report root cause, location, confidence, and fix applied (or proposed).
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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.
|
||||
|
||||
**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 .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
|
||||
- .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 `.ciagent/PROJECT.md`, `.ciagent/ARCHITECTURE.md`, `.ciagent/REQUIREMENTS.md`, `.ciagent/ROADMAP.md`
|
||||
4. Read `./AGENTS.md` or `./CLAUDE.md` for project conventions
|
||||
</project_context>
|
||||
|
||||
<execution_flow>
|
||||
|
||||
## Step 1: Load Documentation
|
||||
|
||||
Read all .ciagent/ documentation files. Read the codebase for actual state.
|
||||
|
||||
## Step 2: Cross-Reference
|
||||
|
||||
For each documentation file:
|
||||
1. PROJECT.md: Do stated requirements match actual features?
|
||||
2. ARCHITECTURE.md: Do components, boundaries, and dependencies match code?
|
||||
3. REQUIREMENTS.md: Do requirement IDs match actual implementations?
|
||||
4. ROADMAP.md: Do phase statuses match git branch state?
|
||||
|
||||
## Step 3: Detect Drift
|
||||
|
||||
Compare recent code changes against documentation:
|
||||
- Files added/removed that docs don't reflect
|
||||
- API changes not documented
|
||||
- Architecture changes not reflected in ARCHITECTURE.md
|
||||
|
||||
## Step 4: Return Result
|
||||
|
||||
Report findings organized by file:
|
||||
- Stale sections with specific line references
|
||||
- Missing documentation for new code
|
||||
- Incorrect references (wrong paths, wrong names)
|
||||
- Severity: blocking (wrong API docs), important (missing sections), nit (minor drift)
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
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
|
||||
write: true
|
||||
edit: true
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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 `.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 .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
|
||||
- .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:
|
||||
|
||||
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 .ciagent/ file you're updating
|
||||
5. Read the relevant source code to verify claims
|
||||
</project_context>
|
||||
|
||||
<execution_flow>
|
||||
|
||||
## Step 1: Load Context
|
||||
|
||||
Understand what documentation needs updating. Read git history for recent changes.
|
||||
|
||||
## Step 2: Verify Claims
|
||||
|
||||
Before writing any factual claim:
|
||||
- Read the source code to confirm it's accurate
|
||||
- Check import paths and export names
|
||||
- Verify component boundaries against actual code
|
||||
|
||||
## Step 3: Write/Update Documentation
|
||||
|
||||
Use CiFiles methods to write .ciagent/ files:
|
||||
- writeProjectMd(project, reason)
|
||||
- writeArchitectureMd(architecture)
|
||||
- writeRoadmapMd(roadmap)
|
||||
- writeRequirementsMd(requirements)
|
||||
|
||||
## Step 4: Commit
|
||||
|
||||
```
|
||||
docs(P##): update [file] — [reason]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: plan
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 5: Return Result
|
||||
|
||||
Report what was updated, what was verified, and any claims that couldn't be confirmed.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
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
|
||||
write: true
|
||||
edit: true
|
||||
bash: true
|
||||
grep: true
|
||||
glob: true
|
||||
---
|
||||
|
||||
<role>
|
||||
You are a CIAgent executor. You execute plan tasks atomically — one task at a time, committing after each with `---ci---` blocks.
|
||||
|
||||
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 .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
|
||||
- .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:
|
||||
|
||||
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 `.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>
|
||||
|
||||
<execution_flow>
|
||||
|
||||
## Step 1: Load Context
|
||||
|
||||
Read the plan file. Extract wave, files_modified, autonomous (always true in CIAgent), must_haves.
|
||||
|
||||
Load git context for current state and decisions.
|
||||
|
||||
## Step 2: Pre-Flight Check
|
||||
|
||||
1. Verify all files to be modified exist (or are to be created)
|
||||
2. Check for conflicts with concurrent plans
|
||||
3. Confirm plan objective aligns with current phase
|
||||
|
||||
## Step 3: Execute Tasks
|
||||
|
||||
For each task in sequence:
|
||||
|
||||
1. Read task's files, action, verify, and done fields
|
||||
2. Implement exactly what the action describes
|
||||
3. Apply minimal upstream fix principle
|
||||
4. Verify using verify criteria
|
||||
5. Commit atomically with `---ci---` block:
|
||||
|
||||
```
|
||||
feat(P##-##-##): [task description]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
plan: ##-##
|
||||
task: ##-##-##
|
||||
status: execute
|
||||
---/ci---
|
||||
```
|
||||
|
||||
Deviation handling: implement the correct approach AND note the deviation. Never silently skip a task.
|
||||
|
||||
## Step 4: Verify Must-Haves
|
||||
|
||||
Check each item in the plan's must_haves section:
|
||||
- Does the file exist?
|
||||
- Does it have substance?
|
||||
- Do integration links work?
|
||||
|
||||
Self-check failed items: add to commit body for orchestrator detection.
|
||||
|
||||
## Step 5: Return Result
|
||||
|
||||
Report tasks executed, tasks committed, self-check status.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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.
|
||||
|
||||
**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 .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
|
||||
- .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:
|
||||
|
||||
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. Use GitContext.getLessons() for lessons that suggest improvements
|
||||
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 .ciagent/ files. Understand the codebase's current state and evolution.
|
||||
|
||||
## Step 2: Apply Thinking Frame
|
||||
|
||||
For your assigned frame (e.g., simplicity, resilience, developer-experience):
|
||||
|
||||
1. Scan the codebase through this lens
|
||||
2. Identify 3-5 specific improvement opportunities
|
||||
3. For each: describe the current state, proposed change, expected benefit, and risk
|
||||
4. Cross-reference with existing decisions to avoid re-litigating settled choices
|
||||
|
||||
## Step 3: Prioritize
|
||||
|
||||
Rank ideas by impact and feasibility. Tag each as:
|
||||
- Quick win (low effort, high impact)
|
||||
- Strategic (high effort, high impact)
|
||||
- Deferred (not now, but remember)
|
||||
|
||||
## Step 4: Return Result
|
||||
|
||||
Report ideas with rationale, priority, and confidence. Do not implement — only propose.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
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
|
||||
write: true
|
||||
edit: true
|
||||
bash: true
|
||||
grep: true
|
||||
glob: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
**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 .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
|
||||
- .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:
|
||||
|
||||
1. Run `git log --max-count=20` and `git branch -a` to discover project structure
|
||||
2. Use GitContext.reconstructState() to get current phase, milestone, stage
|
||||
3. Use GitContext.getDecisions() for all project decisions
|
||||
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 `.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>
|
||||
|
||||
## Stage Order
|
||||
|
||||
```
|
||||
SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → VERIFY → COMPLETE
|
||||
```
|
||||
|
||||
Each stage produces a PhaseResult. The pipeline stops on:
|
||||
- Escalation that requires human input
|
||||
- Abort gate triggered (context exhaustion, error loop)
|
||||
- Successful completion
|
||||
|
||||
## Stage Execution
|
||||
|
||||
For each stage:
|
||||
|
||||
1. Load git context (branches, recent commits, decisions)
|
||||
2. Determine current stage from latest commit's `---ci---` status field
|
||||
3. Execute the stage via its assigned agent
|
||||
4. Collect PhaseResult
|
||||
5. If success: commit with `---ci---` block, advance to next stage
|
||||
6. If failure: attempt ErrorRecovery, retry once, then escalate
|
||||
|
||||
## Autonomy Levels
|
||||
|
||||
| Level | Behavior |
|
||||
|-------|----------|
|
||||
| `full` | No HITL after clarify. Auto-decide everything above threshold. |
|
||||
| `supervised` | Escalate on gates + verification failures. |
|
||||
| `guided` | Escalate on every decision gate. |
|
||||
|
||||
## Decision Gates
|
||||
|
||||
The orchestrator uses DecisionEngine for every significant choice:
|
||||
- confidence >= 0.85: auto-decide, commit
|
||||
- confidence 0.60–0.85: auto-decide with assumption logging, commit
|
||||
- confidence < 0.60: escalate to human
|
||||
|
||||
## Error Recovery
|
||||
|
||||
On stage failure:
|
||||
1. Retry once with same parameters
|
||||
2. If second failure: attempt plan revision
|
||||
3. If third failure: escalate
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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 .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 .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
|
||||
- .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=50` for full project history
|
||||
2. Use GitContext.getDecisions() for existing decisions
|
||||
3. Use GitContext.getCompounds() for compound learnings
|
||||
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 .ciagent/ files. Understand the phase goal and requirements.
|
||||
|
||||
## Step 2: Research
|
||||
|
||||
1. Search git history for prior work on similar features
|
||||
2. Analyze the codebase for existing patterns to reuse
|
||||
3. Identify pitfalls and edge cases
|
||||
4. Recommend approaches with pros/cons
|
||||
5. Document assumptions with confidence scores
|
||||
|
||||
## Step 3: Commit Findings
|
||||
|
||||
```
|
||||
docs(P##): phase research — [topic]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: research
|
||||
decisions:
|
||||
- id: D-XXX
|
||||
decision: [recommended approach]
|
||||
rationale: [evidence]
|
||||
confidence: 0.XX
|
||||
alternatives: [alt1, alt2]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 4: Return Result
|
||||
|
||||
Report key findings, recommended approaches, and decisions.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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.
|
||||
|
||||
**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 .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
|
||||
- .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 `.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>
|
||||
|
||||
## Step 1: Load Plans
|
||||
|
||||
Read all PLAN.md files for the phase. Read git context for decisions.
|
||||
|
||||
## Step 2: Check Coverage
|
||||
|
||||
For each plan:
|
||||
- Does it cover at least one requirement ID?
|
||||
- Do all phase requirement IDs appear across all plans?
|
||||
- Does the plan deliver a demoable vertical slice?
|
||||
- Are must_haves observable and checkable?
|
||||
|
||||
## Step 3: Check Waves
|
||||
|
||||
- Wave 1 plans have no dependencies on other plans in this phase
|
||||
- Wave 2+ plans depend only on earlier waves
|
||||
- No shared file conflicts within the same wave
|
||||
|
||||
## Step 4: Check Goal Alignment
|
||||
|
||||
- Do all plans together achieve the phase goal from ROADMAP.md?
|
||||
- Do plans contradict any locked decisions from git history?
|
||||
|
||||
## Step 5: Return Result
|
||||
|
||||
Report pass/fail per check category. If issues found, provide specific feedback for the planner to address.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
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
|
||||
write: true
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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.
|
||||
|
||||
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 .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
|
||||
- .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 `.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
|
||||
</project_context>
|
||||
|
||||
<execution_flow>
|
||||
|
||||
## Step 1: Load Context
|
||||
|
||||
Read all context files and git history. Extract phase goal, requirements, and existing decisions.
|
||||
|
||||
## Step 2: Decompose Phase Goal
|
||||
|
||||
1. List all user-facing behaviors the phase must deliver
|
||||
2. Each behavior becomes one plan: schema + logic + API + UI + test
|
||||
3. Find dependencies between plans
|
||||
4. Group into 2-4 vertical slice plans, assign waves
|
||||
5. Every must-have must be observable — checkable by reading a file or running a command
|
||||
|
||||
Self-check: "Can someone demo this plan's deliverable after it completes, without completing other plans?" If no → restructure.
|
||||
|
||||
## Step 3: Write Plans
|
||||
|
||||
Write plan files and commit with `---ci---` block:
|
||||
|
||||
```
|
||||
docs(P##): create [N] phase plans
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: plan
|
||||
decisions:
|
||||
- id: D-XXX
|
||||
decision: [planning decision]
|
||||
rationale: [why]
|
||||
confidence: 0.XX
|
||||
alternatives: [alt1, alt2]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 4: Return Result
|
||||
|
||||
Report plan count, wave structure, and any decisions made to the orchestrator.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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.
|
||||
|
||||
**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 .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
|
||||
- .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 `.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>
|
||||
|
||||
<execution_flow>
|
||||
|
||||
## Step 1: Understand Domain
|
||||
|
||||
Read the project specification. Understand what the project needs to accomplish.
|
||||
|
||||
## Step 2: Research Ecosystem
|
||||
|
||||
1. Investigate the technology stack (languages, frameworks, tools)
|
||||
2. Identify key features the project must support
|
||||
3. Research architecture patterns used in similar systems
|
||||
4. Document common pitfalls and anti-patterns
|
||||
5. Evaluate alternative approaches with pros/cons
|
||||
|
||||
## Step 3: Produce Reference Files
|
||||
|
||||
Update `.ciagent/` static files with research conclusions:
|
||||
- PROJECT.md: project vision and requirements
|
||||
- ARCHITECTURE.md: recommended system architecture
|
||||
- REQUIREMENTS.md: formal requirements with IDs
|
||||
|
||||
## Step 4: Commit Research
|
||||
|
||||
```
|
||||
docs(init): project research — [project name]
|
||||
|
||||
---ci---
|
||||
phase: 0
|
||||
milestone: [vX.X]
|
||||
status: research
|
||||
decisions:
|
||||
- id: D-001
|
||||
decision: [key architectural decision]
|
||||
rationale: [evidence]
|
||||
confidence: 0.XX
|
||||
alternatives: [alt1, alt2]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 5: Return Result
|
||||
|
||||
Report research findings, recommended architecture, and key decisions.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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 .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 .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
|
||||
- .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 `.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>
|
||||
|
||||
<execution_flow>
|
||||
|
||||
## Step 1: Load All Research
|
||||
|
||||
Read all `.ciagent/` files and git history for research outputs. Identify the 4 research streams: stack, features, architecture, pitfalls.
|
||||
|
||||
## Step 2: Synthesize
|
||||
|
||||
Cross-reference the research streams:
|
||||
- Does the stack support the features?
|
||||
- Does the architecture address the pitfalls?
|
||||
- Are there contradictions between research streams?
|
||||
- What are the top 3-5 decisions that must be made?
|
||||
|
||||
## Step 3: Update .ci/ Files
|
||||
|
||||
Update `.ciagent/` static files with synthesized conclusions. Resolve contradictions by making decisions (logged with confidence).
|
||||
|
||||
## Step 4: Commit Synthesis
|
||||
|
||||
Commit updated .ciagent/ files with `---ci---` block capturing synthesis decisions.
|
||||
|
||||
## Step 5: Return Result
|
||||
|
||||
Report synthesized view, top decisions, and contradictions resolved.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
You are a CIAgent researcher. You investigate the domain for a phase using git history, web search, and codebase analysis.
|
||||
|
||||
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 .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
|
||||
- .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=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 `.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 .ciagent/ files. Extract phase requirements and existing decisions.
|
||||
|
||||
## Step 2: Research Domain
|
||||
|
||||
1. Search git history for prior research on similar topics
|
||||
2. Search the codebase for existing patterns and implementations
|
||||
3. Investigate ecosystem conventions and prior art
|
||||
4. Identify risks, edge cases, and failure modes
|
||||
5. Enumerate approaches with pros and cons
|
||||
|
||||
## Step 3: Commit Findings
|
||||
|
||||
Research conclusions update `.ciagent/` static files. Key findings go in the commit body. Decisions go in `---ci---` blocks:
|
||||
|
||||
```
|
||||
docs(P##): research [topic]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: research
|
||||
decisions:
|
||||
- id: D-XXX
|
||||
decision: [research-based decision]
|
||||
rationale: [evidence]
|
||||
confidence: 0.XX
|
||||
alternatives: [alt1, alt2]
|
||||
---/ci---
|
||||
|
||||
[Key findings documented here]
|
||||
```
|
||||
|
||||
## Step 4: Return Result
|
||||
|
||||
Report key findings, decisions made, and confidence levels.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
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
|
||||
write: true
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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.
|
||||
|
||||
**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 .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
|
||||
- .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 `.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 .ciagent/ files. Extract all requirements and architectural constraints.
|
||||
|
||||
## Step 2: Break Into Phases
|
||||
|
||||
1. Group requirements by dependency and cohesion
|
||||
2. Each phase is a demoable milestone with clear success criteria
|
||||
3. Map phases to milestone versions
|
||||
4. Ensure every requirement appears in at least one phase
|
||||
|
||||
## Step 3: Write ROADMAP.md
|
||||
|
||||
Write `.ciagent/ROADMAP.md` using CiFiles.writeRoadmapMd():
|
||||
- Overview
|
||||
- Phase list with status, dependencies, requirements, success criteria
|
||||
- Phase details section
|
||||
|
||||
## Step 4: Validate Coverage
|
||||
|
||||
Check: does every requirement ID appear in at least one phase? If not, add missing requirements to the most appropriate phase.
|
||||
|
||||
## Step 5: Commit Roadmap
|
||||
|
||||
```
|
||||
docs(init): create project roadmap ([N] phases)
|
||||
|
||||
---ci---
|
||||
phase: 0
|
||||
milestone: [vX.X]
|
||||
status: plan
|
||||
decisions:
|
||||
- id: D-XXX
|
||||
decision: [phase grouping decision]
|
||||
rationale: [why]
|
||||
confidence: 0.XX
|
||||
alternatives: []
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 6: Return Result
|
||||
|
||||
Report phase count, milestone mapping, and coverage validation results.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
You are a CIAgent security auditor. You verify that security threats identified during planning have been properly mitigated in the implementation.
|
||||
|
||||
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.
|
||||
|
||||
**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 .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
|
||||
- .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:
|
||||
|
||||
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 `.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 .ciagent/ files. Extract trust boundaries and prior threat classifications.
|
||||
|
||||
## Step 2: STRIDE Analysis
|
||||
|
||||
For each file modified in this phase, analyze:
|
||||
|
||||
| Category | Question |
|
||||
|----------|----------|
|
||||
| Spoofing | Can someone pretend to be someone else? |
|
||||
| Tampering | Can someone modify data they shouldn't? |
|
||||
| Repudiation | Can actions be denied after the fact? |
|
||||
| Info Disclosure | Can sensitive data leak? |
|
||||
| Denial of Service | Can the system be made unavailable? |
|
||||
| Elevation of Privilege | Can someone gain unauthorized access? |
|
||||
|
||||
## Step 3: Auto-Disposition
|
||||
|
||||
| Severity | Disposition | Action |
|
||||
|----------|-------------|--------|
|
||||
| Low | Accept | Document, no action needed |
|
||||
| Medium | Mitigate | Propose specific fix |
|
||||
| High | Escalate | Commit escalation, require human |
|
||||
|
||||
## Step 4: Commit Results
|
||||
|
||||
```
|
||||
escalation(P##): [high-severity threat description]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: execute
|
||||
escalations:
|
||||
- id: E-XXX
|
||||
type: security
|
||||
description: [threat]
|
||||
resolution: pending
|
||||
---/ci---
|
||||
```
|
||||
|
||||
For low/medium: document in commit body, no escalation needed.
|
||||
|
||||
## Step 5: Return Result
|
||||
|
||||
Report threat count by severity, dispositions, and any escalations.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
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
|
||||
write: true
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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.
|
||||
|
||||
**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 .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
|
||||
- .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:
|
||||
|
||||
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 `.ciagent/ARCHITECTURE.md` for component context
|
||||
</project_context>
|
||||
|
||||
<execution_flow>
|
||||
|
||||
## Step 1: Load Problem Context
|
||||
|
||||
Understand the problem that was solved, the approach taken, and the outcome.
|
||||
|
||||
## Step 2: Classify
|
||||
|
||||
Assign a category to the compound learning:
|
||||
- architecture, implementation, debugging, testing, security, performance, or domain-specific
|
||||
|
||||
## Step 3: Write Compound Learning
|
||||
|
||||
Capture the pattern:
|
||||
- Problem: what was the issue (generalized)
|
||||
- Solution: what approach worked (generalized)
|
||||
- Category: classification
|
||||
|
||||
## Step 4: Commit
|
||||
|
||||
```
|
||||
compound(P##): [category]: [problem summary]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: complete
|
||||
compound:
|
||||
category: [category]
|
||||
problem: [generalized problem]
|
||||
solution: [generalized solution]
|
||||
lessons:
|
||||
- [related lesson]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 5: Return Result
|
||||
|
||||
Report category, problem, solution, and phase.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<role>
|
||||
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.
|
||||
|
||||
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 .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
|
||||
- .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 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 `.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>
|
||||
|
||||
<execution_flow>
|
||||
|
||||
## Step 1: Load Phase Artifacts
|
||||
|
||||
Read all plans and summaries for the current phase. Read git history for the phase.
|
||||
|
||||
## Step 2: Check Must-Haves
|
||||
|
||||
For every plan, check every must_have:
|
||||
- File existence: `ls [file]`
|
||||
- Export existence: `grep "export.*[symbol]" [file]`
|
||||
- Test passage: `npm test 2>&1 | tail -5`
|
||||
- Build success: `npm run build 2>&1 | tail -5`
|
||||
|
||||
## Step 3: Check Requirement Coverage
|
||||
|
||||
For each requirement ID assigned to this phase:
|
||||
- Find which plan claims to address it
|
||||
- Verify the key deliverable exists
|
||||
- Record in `---ci---` requirements block
|
||||
|
||||
## Step 4: Check Integration Links
|
||||
|
||||
For files imported by other files:
|
||||
- Verify imports resolve
|
||||
- Verify exported symbols exist
|
||||
|
||||
## Step 5: Commit Verification
|
||||
|
||||
Commit verification result with `---ci---` block:
|
||||
|
||||
```
|
||||
verify(P##): [passed|gaps_found|human_needed]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: verify
|
||||
requirements:
|
||||
covered: [REQ-01, REQ-02]
|
||||
partial: [REQ-03]
|
||||
lessons:
|
||||
- [lesson learned]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 6: Return Result
|
||||
|
||||
Report status, must-have score, requirement coverage, integration checks.
|
||||
|
||||
</execution_flow>
|
||||
@@ -0,0 +1 @@
|
||||
0.5.0
|
||||
@@ -0,0 +1,37 @@
|
||||
<dev_context>
|
||||
|
||||
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 (`.ciagent/config.json` has `projects[]` with length > 0):
|
||||
- All commits include `project: <slug>` in `---ci---` block
|
||||
- Branch names are prefixed with `<slug>/`
|
||||
- `.ciagent/` files are in `.ciagent/<slug>/` subdirectories
|
||||
- Project scoping applies to all operations
|
||||
|
||||
NFR milestone versioning:
|
||||
- NFR milestones (all phases are fix/chore/docs/perf/refactor/test): progressive patch versions only, no minor tag
|
||||
- Feature milestones (any feat phase): progressive patch versions + minor milestone tag
|
||||
|
||||
## Output Style
|
||||
|
||||
- Concise, action-oriented responses
|
||||
- Lead with the commit or command, follow with brief rationale
|
||||
- Skip preamble — assume the developer has full context from the git log
|
||||
- Use `file:line` code references over prose descriptions
|
||||
|
||||
## Focus Areas
|
||||
|
||||
- Working code that compiles and passes tests
|
||||
- Minimal diff — change only what is necessary
|
||||
- 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
|
||||
|
||||
## Verbosity
|
||||
|
||||
Low. One-liner explanations unless the change is non-obvious. Omit background theory, alternative approaches, and caveats that do not affect the current task.
|
||||
</dev_context>
|
||||
@@ -0,0 +1,34 @@
|
||||
<research_context>
|
||||
|
||||
Agent output guidance for CIAgent research mode. Loaded when the orchestrator operates in research mode.
|
||||
|
||||
---
|
||||
|
||||
## Output Style
|
||||
|
||||
- Verbose, exploratory responses that surface trade-offs and alternatives
|
||||
- Present multiple approaches with pros and cons before recommending one
|
||||
- Include links, references, and citations where available
|
||||
- Use structured headings and bullet lists for scan-ability
|
||||
|
||||
## Focus Areas
|
||||
|
||||
- Breadth of options — enumerate before narrowing
|
||||
- Prior art and ecosystem conventions
|
||||
- Risks, edge cases, and failure modes
|
||||
- Dependencies and compatibility implications
|
||||
- Long-term maintainability of each approach
|
||||
|
||||
## Research Output
|
||||
|
||||
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 `.ciagent/<slug>/` subdirectories, not the root `.ciagent/` directory.
|
||||
|
||||
## Verbosity
|
||||
|
||||
High. Explain reasoning, show evidence, and document assumptions.
|
||||
</research_context>
|
||||
@@ -0,0 +1,38 @@
|
||||
<review_context>
|
||||
|
||||
Agent output guidance for CIAgent review mode. Loaded when the orchestrator operates in review mode.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Project Awareness
|
||||
|
||||
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>/`
|
||||
- Review findings reference project-scoped paths
|
||||
|
||||
## Output Style
|
||||
|
||||
- Critical, detail-focused responses that prioritize correctness
|
||||
- Organize findings by severity: blocking, important, nit
|
||||
- Reference specific lines and files for every finding
|
||||
- State what is correct as well as what needs change
|
||||
|
||||
## Focus Areas
|
||||
|
||||
- Correctness — logic errors, off-by-ones, missing edge cases
|
||||
- Security — input validation, injection vectors, secret exposure
|
||||
- Performance — unnecessary allocations, O(n^2) patterns, missing caching
|
||||
- Style and consistency — naming, formatting, import order
|
||||
- Test coverage — untested branches, missing assertions, flaky patterns
|
||||
|
||||
## Review Output
|
||||
|
||||
Review findings are committed as `---ci---` blocks with the review type.
|
||||
P0 findings are auto-applied. P1+ are flagged for post-hoc review via `git log --grep="review"`.
|
||||
|
||||
## Verbosity
|
||||
|
||||
Medium. Be thorough on findings but terse in explanation. Each issue: what is wrong, why it matters, how to fix it.
|
||||
</review_context>
|
||||
@@ -0,0 +1,248 @@
|
||||
<branch_strategy>
|
||||
|
||||
Canonical branch naming and lifecycle conventions for CIAgent. Branches encode project structure — merged branches indicate completed work, active branches indicate work in progress.
|
||||
|
||||
---
|
||||
|
||||
## Branch Types
|
||||
|
||||
### Phase Branches
|
||||
|
||||
**Format:** `phase/NN-slug`
|
||||
|
||||
| Part | Convention |
|
||||
|------|-----------|
|
||||
| `NN` | Zero-padded phase number (01, 02, ..., 12) |
|
||||
| `slug` | Lowercase, hyphenated phase name |
|
||||
|
||||
**Examples:**
|
||||
- `phase/01-git-native-architecture`
|
||||
- `phase/02-opencode-integration`
|
||||
- `phase/03-agent-implementations`
|
||||
|
||||
**Lifecycle:**
|
||||
1. Created at phase start by `GitBranch.createPhaseBranch()`
|
||||
2. All task commits for the phase land on this branch
|
||||
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
|
||||
|
||||
**Format:** `milestone/vX.X-slug`
|
||||
|
||||
| Part | Convention |
|
||||
|------|-----------|
|
||||
| `vX.X` | Semver milestone version |
|
||||
| `slug` | Lowercase, hyphenated milestone name |
|
||||
|
||||
**Examples:**
|
||||
- `milestone/v0.2-git-native`
|
||||
- `milestone/v1.0-mvp`
|
||||
|
||||
**Lifecycle:**
|
||||
1. Created at first phase of milestone by `GitBranch.createMilestoneBranch()`
|
||||
2. Spans multiple phases within the same milestone
|
||||
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 (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
|
||||
|
||||
The `GitBranch` class and `GitContext` class determine status from the branch list:
|
||||
|
||||
```typescript
|
||||
const branches = gitContext.getBranches();
|
||||
// Phase branches: type = "phase", phaseNumber, merged boolean
|
||||
// Milestone branches: type = "milestone", milestone string, merged boolean
|
||||
```
|
||||
|
||||
| Branch State | Meaning |
|
||||
|-------------|---------|
|
||||
| Branch exists, not merged | Phase/milestone is active (in progress) |
|
||||
| Branch exists, merged | Phase/milestone is complete |
|
||||
| Branch does not exist | Phase/milestone has not started |
|
||||
|
||||
## Merge Strategy
|
||||
|
||||
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
|
||||
// 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);
|
||||
```
|
||||
|
||||
Phase branches can be deleted after merge if desired.
|
||||
|
||||
## Versioning and Releases
|
||||
|
||||
**Every merge to main creates a release. No exceptions.** Versioning follows a 3-tier model based on milestone type:
|
||||
|
||||
### 3-Tier Versioning Model
|
||||
|
||||
| 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.1–v0.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 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.5.1
|
||||
```
|
||||
|
||||
Phase number within the milestone determines the patch version (1st phase = .1, 2nd phase = .2, etc.)
|
||||
|
||||
**Schema-breaking (minor release per phase):**
|
||||
```bash
|
||||
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.5.0
|
||||
```
|
||||
|
||||
Each schema-breaking phase bumps the minor. 1st phase = next available minor, 2nd = minor+1, etc.
|
||||
|
||||
### Milestone completion
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
**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 produce no milestone tag.** The last phase's patch version is the final release.
|
||||
|
||||
### Version Validation
|
||||
|
||||
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 (`.ciagent/config.json` has `projects[]` with length > 0):
|
||||
|
||||
| Branch Type | Format | Example |
|
||||
|-------------|--------|---------|
|
||||
| Phase | `<slug>/phase/NN-slug` | `auth/phase/01-jwt-setup` |
|
||||
| Milestone | `<slug>/milestone/vX.X-slug` | `auth/milestone/v0.2-login` |
|
||||
|
||||
Single-project mode keeps the existing `phase/NN-slug` and `milestone/vX.X-slug` conventions (no slug prefix).
|
||||
|
||||
## Phase Discovery
|
||||
|
||||
```typescript
|
||||
const gitBranch = new GitBranch(projectPath);
|
||||
const phases = gitBranch.listPhases();
|
||||
// Returns: PhaseBranchInfo[] with phaseNumber, slug, branchName, status
|
||||
|
||||
const milestones = gitBranch.listMilestones();
|
||||
// Returns: MilestoneBranchInfo[] with version, slug, branchName, status
|
||||
```
|
||||
|
||||
## Branch Creation Rules
|
||||
|
||||
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
|
||||
5. Use `GitBranch.createMilestoneBranch()` to ensure consistent naming
|
||||
|
||||
## Working with Phase Branches
|
||||
|
||||
```bash
|
||||
# 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.5
|
||||
plan: 01-01
|
||||
task: 01-01-01
|
||||
status: execute
|
||||
---/ci---"
|
||||
|
||||
# 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 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
|
||||
```
|
||||
|
||||
</branch_strategy>
|
||||
@@ -0,0 +1,177 @@
|
||||
<ci_files_discipline>
|
||||
|
||||
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, `.ciagent/` uses subdirectories per project:
|
||||
|
||||
```
|
||||
.ciagent/
|
||||
config.json # Registry with projects[] and active_project
|
||||
<project-slug>/
|
||||
PROJECT.md
|
||||
ARCHITECTURE.md
|
||||
ROADMAP.md
|
||||
REQUIREMENTS.md
|
||||
```
|
||||
|
||||
`.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 `.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 `.ciagent/`
|
||||
|
||||
| File | Purpose | Update Frequency |
|
||||
|------|---------|-------------------|
|
||||
| `config.json` | Project registry with `projects[]` and `active_project` | Rare (initialization, project changes) |
|
||||
| `<slug>/PROJECT.md` | Vision, core value, requirements, constraints, key decisions per project | Low (phase boundaries) |
|
||||
| `<slug>/ARCHITECTURE.md` | System architecture, component boundaries, data flow per project | Low (major refactors) |
|
||||
| `<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 `.ciagent/`
|
||||
|
||||
These were removed in v0.2.0 and now live in the git log:
|
||||
|
||||
| Previous Location | Now In | Access Method |
|
||||
|-------------------|--------|---------------|
|
||||
| `.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 `.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 |
|
||||
| `writeRoadmapMd(roadmap)` | void | Write roadmap |
|
||||
| `readRequirementsMd()` | RequirementsMd \| null | Read requirements |
|
||||
| `writeRequirementsMd(requirements)` | void | Write requirements |
|
||||
| `readArchitectureMd()` | ArchitectureMd \| null | Read architecture |
|
||||
| `writeArchitectureMd(architecture)` | void | Write architecture |
|
||||
| `updateRequirementStatus(reqId, status)` | void | Update a single requirement status |
|
||||
| `updatePhaseStatus(phaseNumber, status)` | void | Update a single phase status |
|
||||
|
||||
## Update Discipline
|
||||
|
||||
1. **Update with reason** — `writeProjectMd()` takes a `reason` parameter. Every write must justify why.
|
||||
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 `.ciagent/` file change should be committed immediately with a `---ci---` block.
|
||||
|
||||
## Update Triggers
|
||||
|
||||
| When | What to Update | Method |
|
||||
|------|---------------|--------|
|
||||
| Project initialization | All files from scratch | `write*` methods |
|
||||
| Phase transition | Phase status in ROADMAP.md | `updatePhaseStatus()` |
|
||||
| Requirement met | Requirement status in REQUIREMENTS.md | `updateRequirementStatus()` |
|
||||
| Architecture change | ARCHITECTURE.md | `writeArchitectureMd()` |
|
||||
| Scope change | PROJECT.md | `writeProjectMd()` |
|
||||
|
||||
## File Schemas
|
||||
|
||||
### PROJECT.md
|
||||
|
||||
```typescript
|
||||
interface ProjectMd {
|
||||
name: string;
|
||||
coreValue: string;
|
||||
requirements: {
|
||||
validated: string[];
|
||||
active: string[];
|
||||
outOfScope: string[];
|
||||
};
|
||||
constraints: string[];
|
||||
context: string;
|
||||
keyDecisions: Array<{
|
||||
decision: string;
|
||||
rationale: string;
|
||||
outcome: string;
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
### ROADMAP.md
|
||||
|
||||
```typescript
|
||||
interface RoadmapMd {
|
||||
overview: string;
|
||||
phases: Array<{
|
||||
number: number;
|
||||
name: string;
|
||||
description: string;
|
||||
status: "not_started" | "in_progress" | "complete" | "deferred";
|
||||
dependsOn: number[];
|
||||
requirements: string[];
|
||||
successCriteria: string[];
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
### REQUIREMENTS.md
|
||||
|
||||
```typescript
|
||||
interface RequirementsMd {
|
||||
v1: Array<{
|
||||
category: string;
|
||||
items: Array<{ id: string; description: string }>;
|
||||
}>;
|
||||
v2: Array<{
|
||||
category: string;
|
||||
items: Array<{ id: string; description: string }>;
|
||||
}>;
|
||||
outOfScope: Array<{ feature: string; reason: string }>;
|
||||
traceability: Array<{
|
||||
requirement: string;
|
||||
phase: number;
|
||||
status: "pending" | "in_progress" | "complete" | "blocked";
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
### ARCHITECTURE.md
|
||||
|
||||
```typescript
|
||||
interface ArchitectureMd {
|
||||
overview: string;
|
||||
components: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
boundaries: string;
|
||||
dependsOn: string[];
|
||||
}>;
|
||||
dataFlow: string;
|
||||
buildOrder: string[];
|
||||
}
|
||||
```
|
||||
|
||||
## Research and .ci/ File Updates
|
||||
|
||||
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 `.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 `.ciagent/` files
|
||||
- Never update `.ciagent/` files during task execution — update at phase boundaries
|
||||
- Never skip the `reason` parameter when writing PROJECT.md
|
||||
- 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>
|
||||
@@ -0,0 +1,126 @@
|
||||
<commit_schema>
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Block Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
---ci---
|
||||
project: <slug> # required in multi-project mode
|
||||
phase: <number>
|
||||
milestone: <string>
|
||||
plan: <string> # optional
|
||||
task: <string> # optional
|
||||
status: <pipeline_stage>
|
||||
decisions: # optional
|
||||
- id: D-001
|
||||
decision: <text>
|
||||
rationale: <text>
|
||||
confidence: <0.0-1.0>
|
||||
alternatives: [<alt1>, <alt2>]
|
||||
escalations: # optional
|
||||
- id: E-001
|
||||
type: <escalation_type>
|
||||
description: <text>
|
||||
resolution: pending|timeout|human|auto
|
||||
requirements: # optional
|
||||
covered: [REQ-01, REQ-02]
|
||||
partial: [REQ-03]
|
||||
lessons: # optional
|
||||
- <text>
|
||||
compound: # optional
|
||||
category: <string>
|
||||
problem: <text>
|
||||
solution: <text>
|
||||
---/ci---
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
feat(P01-01-01): implement JWT auth
|
||||
|
||||
---ci---
|
||||
project: auth-service
|
||||
phase: 1
|
||||
milestone: v0.2
|
||||
plan: 01-01
|
||||
task: 01-01-01
|
||||
status: execute
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Commit Types
|
||||
|
||||
| Type | Purpose | Scope |
|
||||
|------|---------|-------|
|
||||
| `feat` | New feature | `P##-##-##` |
|
||||
| `fix` | Bug fix | `P##-##-##` |
|
||||
| `test` | Test-only | `P##-##-##` |
|
||||
| `refactor` | Code cleanup | `P##-##-##` |
|
||||
| `docs` | Documentation | `P##`, `init`, `milestone` |
|
||||
| `chore` | Config, deps, tooling | `P##` |
|
||||
| `perf` | Performance | `P##-##-##` |
|
||||
| `wip` | Paused state | `P##` |
|
||||
| `decision` | Standalone decision record | `P##` |
|
||||
| `compound` | Compound learning | `P##` |
|
||||
| `escalation` | Escalation artifact | `P##` |
|
||||
| `verify` | Verification result | `P##` |
|
||||
| `note` | Contextual annotation | `P##` |
|
||||
| `todo` | Future intent | `P##` |
|
||||
|
||||
## Scope Format
|
||||
|
||||
| Context | Format | Example |
|
||||
|---------|--------|---------|
|
||||
| Initialization | `init` | `docs(init): initialize project (5 phases)` |
|
||||
| Milestone | `milestone` | `docs(milestone): complete v1.0-mvp` |
|
||||
| Phase-level | `P##` | `docs(P03): complete auth phase` |
|
||||
| Plan-level | `P##-##` | `feat(P03-01): implement JWT` |
|
||||
| Task-level | `P##-##-##` | `feat(P03-01-02): add refresh rotation` |
|
||||
|
||||
Phase numbers are zero-padded to 2 digits. Plan and task numbers are not zero-padded.
|
||||
|
||||
## Builder Methods
|
||||
|
||||
The `CommitBuilder` class provides typed constructors:
|
||||
|
||||
| Method | Input | Commit Type |
|
||||
|--------|-------|-------------|
|
||||
| `buildInitCommit` | InitCommitInput | `docs(init)` |
|
||||
| `buildTaskCommit` | TaskCommitInput | any task type |
|
||||
| `buildPhaseCompletionCommit` | PhaseCompletionInput | `docs(P##)` |
|
||||
| `buildDecisionCommit` | DecisionCommitInput | `decision(P##)` |
|
||||
| `buildEscalationCommit` | EscalationCommitInput | `escalation(P##)` |
|
||||
| `buildCompoundCommit` | CompoundCommitInput | `compound(P##)` |
|
||||
| `buildVerifyCommit` | VerifyCommitInput | `verify(P##)` |
|
||||
| `buildResearchCommit` | phase, milestone, subject, findings | `docs(P##)` |
|
||||
|
||||
## Reconstruction Guarantee
|
||||
|
||||
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
|
||||
3. **All decisions** — by collecting `decisions[]` from commits where `type: decision` or any commit with a `decisions` block
|
||||
4. **All escalations** — by collecting `escalations[]` from `type: escalation` commits
|
||||
5. **Requirements coverage** — by aggregating `requirements.covered` and `requirements.partial` across all commits
|
||||
6. **Lessons learned** — by collecting `lessons[]` across all commits
|
||||
7. **Compound learnings** — by collecting `compound` objects across all commits
|
||||
8. **Phase completion status** — from the branch state (merged = complete, active = in progress)
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- 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
|
||||
|
||||
</commit_schema>
|
||||
@@ -0,0 +1,108 @@
|
||||
<decision_engine>
|
||||
|
||||
How CIAgent makes decisions and commits them as git artifacts. The DecisionEngine uses bounded rationality with confidence thresholds to auto-decide or escalate.
|
||||
|
||||
---
|
||||
|
||||
## Confidence Thresholds
|
||||
|
||||
| Level | Range | Action |
|
||||
|-------|-------|--------|
|
||||
| High | > 0.85 | Auto-decide, commit with minimal logging |
|
||||
| Medium | 0.60–0.85 | Auto-decide, commit with assumption logging |
|
||||
| Low | < 0.60 | Escalate to human |
|
||||
|
||||
The threshold is configurable via `config.autonomy.decision_confidence_threshold` (default: 0.60).
|
||||
|
||||
## Decision Flow
|
||||
|
||||
```
|
||||
Input: decision + rationale + confidence + alternatives
|
||||
│
|
||||
├─ confidence >= threshold → Auto-decide
|
||||
│ ├─ High confidence: commit with type `decision`
|
||||
│ └─ Medium confidence: commit with type `decision`, log assumptions
|
||||
│
|
||||
└─ confidence < threshold → Escalate
|
||||
└─ Generate escalation commit, pause for human input
|
||||
```
|
||||
|
||||
## DecisionRecord in Commits
|
||||
|
||||
Every decision is recorded in a `---ci---` block:
|
||||
|
||||
```yaml
|
||||
decisions:
|
||||
- id: D-001
|
||||
decision: "Use YAML blocks in commit messages for project state"
|
||||
rationale: "Git log is queryable, diffable, and survives repo transfers"
|
||||
confidence: 0.92
|
||||
alternatives: [JSON sidecar files, .ci/audit/ directory, database]
|
||||
```
|
||||
|
||||
## DecisionEngine API
|
||||
|
||||
| Method | Returns | Purpose |
|
||||
|--------|---------|---------|
|
||||
| `makeDecision(input)` | DecisionResult | Full decision flow with confidence check |
|
||||
| `makeHighConfidenceDecision(...)` | DecisionResult | Shortcut for confidence = 0.95 |
|
||||
| `makeMediumConfidenceDecision(...)` | DecisionResult | Shortcut for confidence = 0.70 |
|
||||
| `shouldAutoDecide(confidence)` | boolean | Check threshold without making decision |
|
||||
| `isIrreversibleAction(action)` | boolean | Check against escalation hooks |
|
||||
| `commitDecision(commitMessage)` | boolean | Execute git commit |
|
||||
| `setPhase(phase)` | void | Update current phase |
|
||||
| `setMilestone(milestone)` | void | Update current milestone |
|
||||
|
||||
## DecisionResult
|
||||
|
||||
```typescript
|
||||
interface DecisionResult {
|
||||
decision: Decision;
|
||||
escalated: boolean;
|
||||
reason?: string; // set when escalated
|
||||
commitMessage?: string; // set when git.auto_commit is true
|
||||
}
|
||||
```
|
||||
|
||||
## Decision Categories
|
||||
|
||||
| Category | When Used |
|
||||
|----------|-----------|
|
||||
| `architecture` | System structure, component boundaries |
|
||||
| `technology` | Library, framework, tool choices |
|
||||
| `implementation` | Algorithm, data structure, approach |
|
||||
| `prioritization` | Feature ordering, scope decisions |
|
||||
| `security` | Threat disposition, auth approach |
|
||||
| `testing` | Test strategy, coverage targets |
|
||||
| `performance` | Optimization decisions, caching strategy |
|
||||
|
||||
## Irreversible Actions
|
||||
|
||||
The `isIrreversibleAction()` method checks against `config.autonomy.escalation_hooks`. Default hooks include patterns like `delete`, `drop`, `force`, `reset --hard`, and any custom patterns.
|
||||
|
||||
Even with high confidence, irreversible actions are flagged for additional scrutiny.
|
||||
|
||||
## Decision Retrieval
|
||||
|
||||
Decisions are retrieved from the git log, not from files:
|
||||
|
||||
```typescript
|
||||
const gitContext = new GitContext(projectPath);
|
||||
const allDecisions = gitContext.getDecisions(); // All phases
|
||||
const phaseDecisions = gitContext.getDecisions(3); // Phase 3 only
|
||||
const commitDecisions = gitContext.getDecisionsFromCommits(commits, 3);
|
||||
```
|
||||
|
||||
## Project-Scoped Decisions
|
||||
|
||||
Decisions can be project-scoped via the `project` field in `---ci---` blocks. When in multi-project mode, include the project slug so that `GitContext.getDecisions()` can filter decisions by project. Project-scoped decisions only apply to the specified project and do not affect other projects in the same repository.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- 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
|
||||
- Never store the decision counter in a file — it's ephemeral per session
|
||||
|
||||
</decision_engine>
|
||||
@@ -0,0 +1,125 @@
|
||||
<git_context_loading>
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Core Principle
|
||||
|
||||
**Read the log first, files second.**
|
||||
|
||||
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
|
||||
|
||||
1. **Branch scan** — `GitContext.getBranches()` to discover phase and milestone structure
|
||||
2. **State reconstruction** — `GitContext.reconstructState()` to get current phase, milestone, stage
|
||||
3. **Decision scan** — `GitContext.getDecisions()` for all project decisions
|
||||
4. **Escalation check** — `GitContext.getEscalations()` for any pending escalations
|
||||
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 `.ciagent/` files (PROJECT.md, ARCHITECTURE.md, etc.)
|
||||
|
||||
## GitContext API
|
||||
|
||||
| Method | Returns | Purpose |
|
||||
|--------|---------|---------|
|
||||
| `isGitRepo()` | boolean | Check if inside a git repo |
|
||||
| `getCurrentBranch()` | string | Current branch name |
|
||||
| `getRecentCommits(count)` | ParsedCiCommit[] | Recent commits with parsed `---ci---` blocks |
|
||||
| `getLatestCiCommit()` | ParsedCiCommit \| null | Most recent CI commit |
|
||||
| `getBranches()` | BranchInfo[] | All branches with type and merge status |
|
||||
| `getPhaseBranches()` | BranchInfo[] | Phase branches only |
|
||||
| `getMilestoneBranches()` | BranchInfo[] | Milestone branches only |
|
||||
| `reconstructState()` | ProjectState | Full project state from git log |
|
||||
| `getDecisions(phase?)` | CommitDecision[] | Decisions, optionally filtered by phase |
|
||||
| `getLessons(phase?)` | string[] | Learned lessons |
|
||||
| `getCompounds(category?)` | CompoundInfo[] | Compound learnings |
|
||||
| `getEscalations()` | EscalationInfo[] | All escalations |
|
||||
| `getRequirementsCoverage()` | { covered, partial } | Requirement traceability |
|
||||
| `getCommitsForPhase(phase)` | ParsedCiCommit[] | All commits for a phase |
|
||||
| `getCommitsForBranch(branch)` | ParsedCiCommit[] | All commits on a branch |
|
||||
|
||||
## ProjectState
|
||||
|
||||
The `reconstructState()` method returns:
|
||||
|
||||
```typescript
|
||||
interface ProjectState {
|
||||
currentPhase: number;
|
||||
currentMilestone: string;
|
||||
currentStage: PipelineStage;
|
||||
phasesCompleted: number[];
|
||||
phaseBranches: BranchInfo[];
|
||||
milestoneBranches: string[];
|
||||
lastCommit: ParsedCiCommit | null;
|
||||
}
|
||||
```
|
||||
|
||||
Derived entirely from git data — no file reads required.
|
||||
|
||||
## ParsedCiCommit
|
||||
|
||||
Every commit returned by `getRecentCommits()` is parsed into:
|
||||
|
||||
```typescript
|
||||
interface ParsedCiCommit {
|
||||
hash: string;
|
||||
type: CommitType;
|
||||
scope: string;
|
||||
subject: string;
|
||||
ci: CiMetadata | null; // null if no ---ci--- block
|
||||
body: string;
|
||||
}
|
||||
```
|
||||
|
||||
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 `.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 `.ciagent/` files (ROADMAP.md phase status, REQUIREMENTS.md requirement statuses)
|
||||
3. Verify `GitContext.reconstructState()` matches expected state
|
||||
4. Reset context — next phase begins fresh
|
||||
|
||||
The phase context reset ensures that each phase operates on verified git state, preventing context drift across long-running projects.
|
||||
|
||||
## Multi-Project Context
|
||||
|
||||
GitContext supports multi-project mode with optional project scoping:
|
||||
|
||||
| Method | Returns | Purpose |
|
||||
|--------|---------|---------|
|
||||
| `GitContext(projectPath, projectSlug?)` | GitContext | Optional project slug for scoping |
|
||||
| `detectProjectFromCommit()` | string \| null | Detect project from latest commit's `project` field |
|
||||
| `isNfrMilestone()` | boolean | Check if current milestone is NFR-only (no feat phases) |
|
||||
|
||||
In multi-project mode, `detectProjectFromCommit()` reads the `project` field from the latest `---ci---` block to determine which project context to load. `isNfrMilestone()` inspects phase commit types to determine versioning behavior.
|
||||
|
||||
## Context Budget Strategy
|
||||
|
||||
When context is limited:
|
||||
|
||||
1. `reconstructState()` — always (cheap, single call)
|
||||
2. `getDecisions(currentPhase)` — current phase decisions only
|
||||
3. `getRequirementsCoverage()` — aggregate view
|
||||
4. Skip lessons/compounds unless specifically needed
|
||||
5. Read `.ciagent/ROADMAP.md` instead of scanning all phase branches
|
||||
|
||||
## What NOT to Do
|
||||
|
||||
- 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
|
||||
- Never skip the branch scan — merged branches indicate completed phases
|
||||
|
||||
</git_context_loading>
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
description: Audit CIAgent project health — reconstruct state from git log, verify .ciagent/ files match codebase, check for stale references
|
||||
---
|
||||
|
||||
# CIAgent Audit
|
||||
|
||||
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:** `ciagent-audit`
|
||||
|
||||
## Step 0: Confirm Active Project
|
||||
|
||||
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
|
||||
|
||||
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
|
||||
- All commit messages must include `project: <slug>` in `---ci---` block
|
||||
|
||||
If single-project mode: proceed with existing conventions.
|
||||
|
||||
## Step 1: Reconstruction Test
|
||||
|
||||
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 `.ciagent/` file contents
|
||||
|
||||
## Step 2: Check .ci/ File Discipline
|
||||
|
||||
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
|
||||
|
||||
- Every `phase/NN-*` branch should be either merged or active
|
||||
- Active phase branches should have recent commits
|
||||
- No orphan branches (branches with no `---ci---` commits)
|
||||
|
||||
## 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 `.ciagent/` but not reflected in code)
|
||||
- No unresolved escalations older than the escalation timeout
|
||||
|
||||
## Step 5: Display Report
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
CIAgent ► AUDIT REPORT
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Reconstruction: [PASS/FAIL] — [details]
|
||||
.ciagent/ Files: [N] checked, [issues]
|
||||
Branches: [N] phase, [N] milestone, [issues]
|
||||
Commits: [N] CIAgent commits, [N] without ---ci--- blocks
|
||||
|
||||
[If issues found:]
|
||||
Issues:
|
||||
- [issue description]
|
||||
- [issue description]
|
||||
|
||||
[If clean:]
|
||||
All checks passed. Project state is fully reconstructable from git log.
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
description: Clarify CIAgent project ambiguities — generate questions, accept defaults at full autonomy, present at supervised/guided
|
||||
---
|
||||
|
||||
# CIAgent Clarify
|
||||
|
||||
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:** `ciagent-clarify [phase_number]`
|
||||
|
||||
## Step 0: Confirm Active Project
|
||||
|
||||
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
|
||||
|
||||
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
|
||||
|
||||
If single-project mode: proceed with existing conventions.
|
||||
|
||||
## Step 1: Load Git Context
|
||||
|
||||
```bash
|
||||
git log --max-count=20
|
||||
git branch -a
|
||||
```
|
||||
|
||||
Read `.ciagent/PROJECT.md` and `.ciagent/REQUIREMENTS.md` for the specification.
|
||||
|
||||
## Step 2: Identify Ambiguities
|
||||
|
||||
Analyze the specification and requirements for:
|
||||
|
||||
1. **Undefined terms** — words with multiple interpretations
|
||||
2. **Missing boundaries** — requirements without clear success criteria
|
||||
3. **Conflicting constraints** — requirements that contradict each other
|
||||
4. **Implicit assumptions** — things taken for granted but not stated
|
||||
5. **Scope ambiguity** — unclear what is in/out of scope
|
||||
|
||||
## Step 3: Generate Questions
|
||||
|
||||
For each ambiguity, generate a clarify question with:
|
||||
- The question text
|
||||
- A default answer (CI's best guess)
|
||||
- The reasoning for the default
|
||||
|
||||
## Step 4: Resolve Questions
|
||||
|
||||
Based on autonomy level:
|
||||
|
||||
| Level | Behavior |
|
||||
|-------|----------|
|
||||
| `full` | Accept all defaults automatically, log decisions |
|
||||
| `supervised` | Present questions, accept defaults after timeout |
|
||||
| `guided` | Present questions, wait for every answer |
|
||||
|
||||
## Step 5: Commit Clarifications
|
||||
|
||||
```
|
||||
decision(P##): clarification — [topic]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: clarify
|
||||
decisions:
|
||||
- id: D-XXX
|
||||
decision: [clarified choice]
|
||||
rationale: [why this answer]
|
||||
confidence: 0.XX
|
||||
alternatives: [alt1, alt2]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 6: Update .ci/ Files
|
||||
|
||||
Update `.ciagent/PROJECT.md` with clarified requirements.
|
||||
Update `.ciagent/REQUIREMENTS.md` with refined requirements.
|
||||
|
||||
## Step 7: Report
|
||||
|
||||
Report clarifications made, decisions logged, confidence levels.
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
description: Systematic CIAgent debugging with git context — triage, diagnose root cause, auto-fix or escalate
|
||||
---
|
||||
|
||||
# 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:** `ciagent-debug [description]`
|
||||
|
||||
## Step 0: Confirm Active Project
|
||||
|
||||
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
|
||||
|
||||
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
|
||||
- All commit messages must include `project: <slug>` in `---ci---` block
|
||||
|
||||
If single-project mode: proceed with existing conventions.
|
||||
|
||||
## Step 1: Load Git Context
|
||||
|
||||
```bash
|
||||
git log --max-count=20
|
||||
git diff HEAD~5
|
||||
git branch -a
|
||||
```
|
||||
|
||||
Load recent changes to identify potential causes.
|
||||
|
||||
## Step 2: Triage
|
||||
|
||||
If no description provided, ask: "What is the exact symptom?"
|
||||
|
||||
Gather:
|
||||
- Symptom description
|
||||
- Expected behavior
|
||||
- When it started (check git log for recent changes)
|
||||
- What has been tried
|
||||
|
||||
## Step 3: Hypothesis Testing
|
||||
|
||||
For each hypothesis (most likely first):
|
||||
|
||||
1. Identify key files to check
|
||||
2. Trace the code path from symptom to root
|
||||
3. Read all files in the path
|
||||
4. Confirm or deny: "If this were fixed, would the symptom go away?"
|
||||
|
||||
## Step 4: Auto-Fix or Escalate
|
||||
|
||||
Based on confidence:
|
||||
|
||||
| Confidence | Action |
|
||||
|-----------|--------|
|
||||
| High (> 0.85) | Auto-fix, commit with `---ci---` block |
|
||||
| Medium (0.60–0.85) | Auto-fix with assumption logging, commit |
|
||||
| Low (< 0.60) | Escalate with proposed fix |
|
||||
|
||||
## Step 5: Commit Fix
|
||||
|
||||
```
|
||||
fix(P##): [root cause description]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: execute
|
||||
decisions:
|
||||
- id: D-XXX
|
||||
decision: [fix approach]
|
||||
rationale: [evidence]
|
||||
confidence: 0.XX
|
||||
alternatives: []
|
||||
lessons:
|
||||
- [lesson learned]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 6: Verify Fix
|
||||
|
||||
Run relevant tests to confirm the fix works:
|
||||
```bash
|
||||
npm test
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
Report: root cause, location, confidence, fix applied.
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
description: Initialize a new CIAgent project — specification → clarify → create .ciagent/ reference files → initial commit
|
||||
---
|
||||
|
||||
# CIAgent Init
|
||||
|
||||
Initialize a new CIAgent project with specification parsing, clarification, and .ciagent/ reference file creation.
|
||||
|
||||
**Usage:** `ciagent-init [description]`
|
||||
|
||||
## Step 0: Confirm Active Project
|
||||
|
||||
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
|
||||
|
||||
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 `.ciagent/<slug>/` subdirectories
|
||||
- All commit messages must include `project: <slug>` in `---ci---` block
|
||||
|
||||
If single-project mode: proceed with existing conventions.
|
||||
|
||||
## Step 1: Check Prerequisites
|
||||
|
||||
Verify git is initialized:
|
||||
```bash
|
||||
[ -d .git ] && echo "GIT_EXISTS" || echo "NO_GIT"
|
||||
```
|
||||
|
||||
If NO_GIT: `git init`
|
||||
|
||||
Check if `.ciagent/config.json` already exists:
|
||||
```bash
|
||||
[ -f .ciagent/config.json ] && echo "ALREADY_INITIALIZED" || echo "NEW"
|
||||
```
|
||||
|
||||
If ALREADY_INITIALIZED: stop. Use `ciagent-status` to see project state.
|
||||
|
||||
## Step 2: Parse Specification
|
||||
|
||||
If a description was provided, use it as the project specification. Otherwise, ask:
|
||||
|
||||
"What is the project specification? Describe the objective, requirements, constraints, and out-of-scope items."
|
||||
|
||||
Extract from the specification:
|
||||
- Objective (what the project builds)
|
||||
- Requirements (what it must do)
|
||||
- Constraints (what it must not do or must use)
|
||||
- Out of scope (what is explicitly excluded)
|
||||
|
||||
## Step 3: Clarify
|
||||
|
||||
Analyze the specification for ambiguities. For each ambiguity:
|
||||
|
||||
1. Generate a clarify question with default answer
|
||||
2. If autonomy level is `full`: accept defaults automatically
|
||||
3. If autonomy level is `supervised` or `guided`: present question, wait for answer
|
||||
4. Log all clarification decisions
|
||||
|
||||
Record decisions in the `---ci---` block of the init commit.
|
||||
|
||||
## Step 4: Create .ciagent/ Files
|
||||
|
||||
Use CiFiles to create the project structure:
|
||||
|
||||
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.
|
||||
|
||||
## Step 5: Create Initial Branches
|
||||
|
||||
```bash
|
||||
git checkout -b milestone/v1.0-initial
|
||||
```
|
||||
|
||||
## Step 6: Initial Commit
|
||||
|
||||
```
|
||||
docs(init): initialize [project-name] ([N] phases)
|
||||
|
||||
---ci---
|
||||
project: <slug>
|
||||
phase: 0
|
||||
milestone: v1.0
|
||||
status: specify
|
||||
decisions:
|
||||
- id: D-001
|
||||
decision: [clarification decision]
|
||||
rationale: [why]
|
||||
confidence: 0.XX
|
||||
alternatives: []
|
||||
---/ci---
|
||||
|
||||
Specification: [objective]
|
||||
Requirements: [req1, req2, ...]
|
||||
Constraints: [constraint1, ...]
|
||||
Out of scope: [item1, ...]
|
||||
```
|
||||
|
||||
Include `project: <slug>` in the `---ci---` block when in multi-project mode.
|
||||
|
||||
## Step 7: Done
|
||||
|
||||
Report project initialized, .ciagent/ files created, initial branch created.
|
||||
|
||||
Next: `ciagent-run` to execute the pipeline, or `ciagent-quick` for ad-hoc tasks.
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
description: Execute an ad-hoc CIAgent task with full agentic guarantees — git context, ---ci--- commits, optional research and verification
|
||||
---
|
||||
|
||||
# CIAgent Quick
|
||||
|
||||
Execute small, ad-hoc tasks with CIAgent guarantees: git context loading, `---ci---` commit blocks, optional research and verification.
|
||||
|
||||
**Usage:** `ciagent-quick [description]`
|
||||
|
||||
**Flags:**
|
||||
- `--research` — spawn a focused research agent before execution
|
||||
- `--verify` — verify results after execution
|
||||
- `--full` — research + verify
|
||||
|
||||
## Step 0: Confirm Active Project
|
||||
|
||||
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
|
||||
|
||||
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
|
||||
|
||||
If single-project mode: proceed with existing conventions.
|
||||
|
||||
## Step 1: Get Task Description
|
||||
|
||||
If provided as argument, use it. Otherwise ask: "What do you want to do?"
|
||||
|
||||
## Step 2: Load Git Context
|
||||
|
||||
```bash
|
||||
git log --max-count=20
|
||||
git branch -a
|
||||
```
|
||||
|
||||
Use GitContext.reconstructState() to understand project state.
|
||||
|
||||
Check that `.ciagent/config.json` exists. If missing: stop, run `ciagent-init` first.
|
||||
|
||||
## Step 3: Research (only with `--research` or `--full`)
|
||||
|
||||
Delegate to ci-researcher for a focused research pass:
|
||||
- What libraries or approaches are relevant?
|
||||
- What pitfalls to avoid?
|
||||
- Existing patterns in the codebase?
|
||||
|
||||
## Step 4: Execute
|
||||
|
||||
Implement the task directly. Key principles:
|
||||
- Minimal diff — change only what is necessary
|
||||
- Commit with `---ci---` block:
|
||||
|
||||
```
|
||||
feat(P##): [task description]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: execute
|
||||
---/ci---
|
||||
```
|
||||
|
||||
Use the current phase and milestone from GitContext.reconstructState().
|
||||
|
||||
## Step 5: Verify (only with `--verify` or `--full`)
|
||||
|
||||
Delegate to ci-verifier:
|
||||
- Does the change work?
|
||||
- Does typecheck/lint pass?
|
||||
- Do existing tests still pass?
|
||||
|
||||
## Step 6: Commit Summary
|
||||
|
||||
Final commit if multiple changes were made:
|
||||
|
||||
```
|
||||
docs(P##): quick task — [description]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: execute
|
||||
lessons:
|
||||
- [lesson if any]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
Report completion with next suggested action.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
description: Review CIAgent code changes with multi-persona analysis — auto-apply P0 fixes, flag P1+ for post-hoc 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:** `ciagent-review [phase_number]`
|
||||
|
||||
## Step 0: Confirm Active Project
|
||||
|
||||
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
|
||||
|
||||
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
|
||||
|
||||
If single-project mode: proceed with existing conventions.
|
||||
|
||||
## Step 1: Load Changes
|
||||
|
||||
```bash
|
||||
git log --grep="P##" --max-count=30
|
||||
git diff phase/NN-slug...HEAD
|
||||
```
|
||||
|
||||
Load all changes for the current or specified phase.
|
||||
|
||||
## Step 2: Persona Reviews
|
||||
|
||||
For each persona (correctness, testing, security, performance, maintainability, adversarial):
|
||||
|
||||
### Correctness
|
||||
- Logic errors, off-by-ones, missing edge cases
|
||||
- Incorrect data transformations
|
||||
- Race conditions
|
||||
|
||||
### Testing
|
||||
- Missing test cases for new code
|
||||
- Flaky test patterns
|
||||
- Inadequate assertions
|
||||
|
||||
### Security
|
||||
- Input validation gaps
|
||||
- Injection vectors
|
||||
- Secret exposure
|
||||
- Missing auth checks
|
||||
|
||||
### Performance
|
||||
- Unnecessary allocations
|
||||
- O(n^2) patterns
|
||||
- Missing caching opportunities
|
||||
|
||||
### Maintainability
|
||||
- Naming inconsistencies
|
||||
- Coupling violations
|
||||
- Missing error handling
|
||||
|
||||
### Adversarial
|
||||
- Attack surface expansion
|
||||
- Abuse cases
|
||||
- Trust boundary violations
|
||||
|
||||
## Step 3: Classify and Fix
|
||||
|
||||
For each finding:
|
||||
- **P0** (blocking): Logic errors, security vulnerabilities, broken imports → auto-apply
|
||||
- **P1** (important): Coverage gaps, naming issues, missing edge cases → flag
|
||||
- **P2** (nit): Style, formatting, minor suggestions → flag
|
||||
|
||||
## Step 4: Commit
|
||||
|
||||
```
|
||||
verify(P##): code review — [N] P0 auto-fixed, [M] P1+ flagged
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: verify
|
||||
lessons:
|
||||
- [P0 fix: description]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 5: Return Result
|
||||
|
||||
Report findings by persona, P0 fixes applied, P1+ flags.
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
description: Rollback CIAgent phase — revert the last phase or specified phase by resetting to its pre-phase state
|
||||
---
|
||||
|
||||
# CIAgent Rollback
|
||||
|
||||
Rollback a CIAgent phase by reverting to the state before the phase started. Uses git to find the exact commit to reset to.
|
||||
|
||||
**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 `.ciagent/config.json` to determine if multi-project mode is active.
|
||||
|
||||
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>/`)
|
||||
- All commit messages must include `project: <slug>` in `---ci---` block
|
||||
|
||||
If single-project mode: proceed with existing conventions (branches without slug prefix).
|
||||
|
||||
## Step 1: Load Git Context
|
||||
|
||||
```bash
|
||||
git log --max-count=30
|
||||
git branch -a
|
||||
```
|
||||
|
||||
Find the phase branch and its merge commit.
|
||||
|
||||
## Step 2: Identify Rollback Point
|
||||
|
||||
For the specified phase:
|
||||
1. Find the merge commit that completed the phase
|
||||
2. Find the commit just before the phase branch was created
|
||||
3. This is the rollback point
|
||||
|
||||
```bash
|
||||
git log --grep="P##" --format="%H %s" | head -20
|
||||
```
|
||||
|
||||
## Step 3: Confirm (Safety Gate)
|
||||
|
||||
Even in full autonomy mode, destructive operations need confirmation:
|
||||
|
||||
```
|
||||
⚠ ROLLBACK: This will revert Phase [N] — [name]
|
||||
|
||||
Rollback point: [commit hash] [subject]
|
||||
Changes to be lost: [N] commits
|
||||
|
||||
Proceed? (y/n)
|
||||
```
|
||||
|
||||
Wait for confirmation. This is a safety gate — always confirm destructive operations.
|
||||
|
||||
## Step 4: Execute Rollback
|
||||
|
||||
```bash
|
||||
git revert [merge_commit_hash]
|
||||
```
|
||||
|
||||
Or for a hard rollback (not recommended, only if explicitly requested):
|
||||
```bash
|
||||
git reset --hard [rollback_point]
|
||||
```
|
||||
|
||||
## Step 5: Update State
|
||||
|
||||
- Delete the phase branch (if not already removed)
|
||||
- Update `.ciagent/REQUIREMENTS.md` — mark phase requirements as blocked
|
||||
- Update `.ciagent/ROADMAP.md` — mark phase as not_started
|
||||
|
||||
Commit the rollback:
|
||||
|
||||
```
|
||||
chore(P##): rollback [phase-name] — [reason]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: specify
|
||||
escalations:
|
||||
- id: E-XXX
|
||||
type: rollback
|
||||
description: Phase rolled back
|
||||
resolution: auto
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 6: Report
|
||||
|
||||
Report rollback complete, rollback point, and next steps.
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
description: Execute the full CIAgent pipeline — research → plan → execute → verify → complete for the current or specified phase
|
||||
---
|
||||
|
||||
# CIAgent Run
|
||||
|
||||
Execute the full CIAgent pipeline from the current stage to completion. The orchestrator iterates through stages and delegates to specialized agents.
|
||||
|
||||
**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 `.ciagent/config.json` to determine if multi-project mode is active.
|
||||
|
||||
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
|
||||
|
||||
If single-project mode: proceed with existing conventions.
|
||||
|
||||
## Step 1: Load Git Context
|
||||
|
||||
```bash
|
||||
git log --max-count=20
|
||||
git branch -a
|
||||
```
|
||||
|
||||
Determine current state:
|
||||
- Current phase from latest `---ci---` block
|
||||
- Current milestone from latest `---ci---` block or active milestone branch
|
||||
- Current pipeline stage from latest `---ci---` status field
|
||||
- Completed phases from merged `phase/NN-*` branches
|
||||
|
||||
## Step 2: Pre-Flight Check
|
||||
|
||||
Verify `.ciagent/config.json` exists. If missing: stop, run `ciagent-init` first.
|
||||
|
||||
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 `.ciagent/PROJECT.md`
|
||||
- Validate requirements exist in `.ciagent/REQUIREMENTS.md`
|
||||
- Commit: `docs(init): validate specification`
|
||||
|
||||
### CLARIFY
|
||||
- Generate clarify questions for ambiguities
|
||||
- Default-accept at `full` autonomy, present at `supervised`/`guided`
|
||||
- Commit: `decision(P##): clarification decisions`
|
||||
|
||||
### RESEARCH
|
||||
- Delegate to ci-researcher
|
||||
- Research domain, ecosystem, prior art
|
||||
- Update `.ciagent/` static files with conclusions
|
||||
- Commit: `docs(P##): research findings`
|
||||
|
||||
### PLAN
|
||||
- Delegate to ci-planner
|
||||
- Create vertical-slice plans with wave ordering
|
||||
- Commit: `docs(P##): create [N] phase plans`
|
||||
|
||||
### EXECUTE
|
||||
- Create phase branch: `phase/NN-slug`
|
||||
- Delegate to ci-executor per plan per wave
|
||||
- Commit each task with `---ci---` block
|
||||
- After all waves: commit phase completion
|
||||
|
||||
### VERIFY
|
||||
- Delegate to ci-verifier
|
||||
- Check must_haves, requirement coverage, integration links
|
||||
- Auto-generate tests for unverifiable items
|
||||
- Commit: `verify(P##): verification result`
|
||||
|
||||
### COMPLETE
|
||||
- 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 `.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.
|
||||
|
||||
## Phase Boundary Checkpoint
|
||||
|
||||
Between phases, perform a context reset:
|
||||
|
||||
1. Commit all work from the current phase
|
||||
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
|
||||
|
||||
## NFR Versioning Logic
|
||||
|
||||
Before tagging a phase completion, check `isNfrMilestone()`:
|
||||
|
||||
- **NFR milestone** (all phases are fix/chore/docs/perf/refactor/test): apply progressive patch versions (v0.1.1, v0.1.2, v0.1.3). No separate milestone tag.
|
||||
- **Feature milestone** (any feat phase): apply progressive patch versions per phase, then tag minor milestone version on completion (e.g., v0.2.0).
|
||||
|
||||
## Step 4: Error Recovery
|
||||
|
||||
On stage failure:
|
||||
1. Retry once
|
||||
2. Attempt plan revision
|
||||
3. Escalate to human
|
||||
|
||||
## Step 5: Advance or Complete
|
||||
|
||||
If more phases remain: advance to next phase, return to Step 3.
|
||||
If all phases complete:
|
||||
- Tag milestone with minor version (e.g., `v0.3.0`)
|
||||
- Create Gitea release for the milestone with full phase summary
|
||||
- Report project completion
|
||||
|
||||
Error handling: commit escalations as `---ci---` blocks with `escalation` type.
|
||||
@@ -0,0 +1,203 @@
|
||||
---
|
||||
description: Ship CIAgent phase or milestone — test, tag, release. Every phase and milestone gets a release. Full autopilot.
|
||||
---
|
||||
|
||||
# CIAgent Ship
|
||||
|
||||
Ship a CIAgent phase or milestone. Every ship creates a release — no exceptions.
|
||||
|
||||
**3-Tier Versioning Model:**
|
||||
|
||||
| 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` |
|
||||
|
||||
**CRITICAL:** Milestone tags are always the NEXT version, never the base:
|
||||
- Feature: patches v0.5.1–v0.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 `.ciagent/config.json` to determine if multi-project mode is active.
|
||||
|
||||
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
|
||||
- Branch names are prefixed with `<slug>/` in multi-project mode
|
||||
|
||||
If single-project mode: proceed with existing conventions.
|
||||
|
||||
## Step 1: Pre-Flight
|
||||
|
||||
```bash
|
||||
git log --max-count=10
|
||||
git branch -a
|
||||
```
|
||||
|
||||
Determine what is being shipped: a single phase or an entire milestone.
|
||||
|
||||
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 `.ciagent/config.json` for autonomy level.
|
||||
|
||||
## Step 2: Run Tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run typecheck
|
||||
npm run build
|
||||
```
|
||||
|
||||
If any fail: iterate autonomously until tests pass. Do NOT ask the user for guidance — debug and fix.
|
||||
|
||||
## Step 3: Compute Version
|
||||
|
||||
Determine milestone type by calling `getMilestoneType()` which returns `"nfr" | "feature" | "schema-breaking"`:
|
||||
|
||||
| What's shipping | Milestone Type | Phase release | Milestone release | Example |
|
||||
|----------------|---------------|-------------|------------|---------|
|
||||
| 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 |
|
||||
|
||||
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
|
||||
|
||||
### 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
|
||||
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---"
|
||||
```
|
||||
|
||||
### 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]
|
||||
|
||||
---ci---
|
||||
phase: 0
|
||||
milestone: [vX.Y]
|
||||
status: complete
|
||||
---/ci---"
|
||||
```
|
||||
|
||||
## Step 5: Tag and Push
|
||||
|
||||
```bash
|
||||
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.**
|
||||
|
||||
Generate release notes from git log:
|
||||
|
||||
```bash
|
||||
git log v[previous_tag]..vX.Y.Z --oneline
|
||||
```
|
||||
|
||||
Create the release via Gitea API:
|
||||
|
||||
```bash
|
||||
curl -X POST "https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/ci/releases" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tag_name":"vX.Y.Z","name":"vX.Y.Z","body":"[release notes from git log]"}'
|
||||
```
|
||||
|
||||
For milestone releases, include a summary of all phases completed and requirements covered.
|
||||
|
||||
## Step 7: Update .ci/ Files
|
||||
|
||||
- Update `.ciagent/REQUIREMENTS.md` — mark shipped requirements as complete
|
||||
- Update `.ciagent/ROADMAP.md` — mark shipped phase as complete
|
||||
|
||||
Commit the file updates.
|
||||
|
||||
## Step 8: Report
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
CIAgent ► SHIPPED
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Phase [N]: [name]
|
||||
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
|
||||
|
||||
Tests: PASS
|
||||
Typecheck: PASS
|
||||
Build: PASS
|
||||
|
||||
Requirements covered: [N]
|
||||
Commits: [N]
|
||||
|
||||
[If milestone complete:]
|
||||
All phases in milestone v0.2 complete. Milestone released as vX.Y.Z.
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
description: Show CIAgent project status — current phase, milestone, pipeline stage, decisions, escalations, and requirements coverage
|
||||
---
|
||||
|
||||
# CIAgent Status
|
||||
|
||||
Display the current CIAgent project status derived entirely from the git log and .ciagent/ files.
|
||||
|
||||
**Usage:** `ciagent-status`
|
||||
|
||||
## Step 0: Confirm Active Project
|
||||
|
||||
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
|
||||
|
||||
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>)`
|
||||
- All commit messages must include `project: <slug>` in `---ci---` block
|
||||
|
||||
If single-project mode: proceed with existing conventions.
|
||||
|
||||
## Step 1: Load Git Context
|
||||
|
||||
```bash
|
||||
git log --max-count=30
|
||||
git branch -a
|
||||
```
|
||||
|
||||
Use GitContext.reconstructState() to get:
|
||||
- Current phase
|
||||
- Current milestone
|
||||
- Current pipeline stage
|
||||
- Completed phases
|
||||
|
||||
## Step 2: Gather Details
|
||||
|
||||
Collect from git log:
|
||||
- GitContext.getDecisions() — all decisions
|
||||
- GitContext.getEscalations() — pending escalations
|
||||
- GitContext.getRequirementsCoverage() — covered/partial requirements
|
||||
- GitContext.getLessons() — learned lessons
|
||||
- GitContext.getCompounds() — compound learnings
|
||||
|
||||
## Step 3: Read .ci/ Files
|
||||
|
||||
Read:
|
||||
- `.ciagent/PROJECT.md` — project name and vision
|
||||
- `.ciagent/ROADMAP.md` — phase list with status
|
||||
- `.ciagent/config.json` — autonomy level
|
||||
|
||||
## Step 4: Display Status
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
CIAgent ► STATUS
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Project: [name] [If multi-project: (active)]
|
||||
[If multi-project: Other projects: [name1], [name2]]
|
||||
Milestone: [current] [NFR|Feature]
|
||||
Phase: [N] — [name]
|
||||
Stage: [current_stage]
|
||||
Autonomy: [level]
|
||||
|
||||
Phases:
|
||||
✓ [N] [name] (complete)
|
||||
→ [N] [name] (in progress)
|
||||
○ [N] [name] (not started)
|
||||
|
||||
Decisions: [N] total
|
||||
Escalations: [N] pending
|
||||
Requirements: [N] covered, [N] partial
|
||||
|
||||
Recent commits:
|
||||
[hash] [subject]
|
||||
[hash] [subject]
|
||||
[hash] [subject]
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
If no `.ciagent/` directory exists: report "Project not initialized. Run ciagent-init first."
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
description: Verify CIAgent project deliverables against requirements — structural, behavioral, security, and quality checks
|
||||
---
|
||||
|
||||
# CIAgent Verify
|
||||
|
||||
Run the CIAgent verification pipeline against the current or specified phase. Four layers: structural, behavioral, security, quality.
|
||||
|
||||
**Usage:** `ciagent-verify [phase_number]`
|
||||
|
||||
If no phase specified, verifies the current phase.
|
||||
|
||||
## Step 0: Confirm Active Project
|
||||
|
||||
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
|
||||
|
||||
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
|
||||
- All commit messages must include `project: <slug>` in `---ci---` block
|
||||
|
||||
If single-project mode: proceed with existing conventions.
|
||||
|
||||
**Phase Boundary Checkpoint:** Between phases, all state is committed to git, context is reset, and the next phase begins with fresh git log context. Verify that the current verification aligns with the reconstructed state.
|
||||
|
||||
## Step 1: Load Git Context
|
||||
|
||||
```bash
|
||||
git log --max-count=30
|
||||
git branch -a
|
||||
```
|
||||
|
||||
Determine the phase to verify from git context or argument.
|
||||
|
||||
## Step 2: Structural Verification (Layer 1)
|
||||
|
||||
Check:
|
||||
1. All files referenced in plans exist on disk
|
||||
2. All imports resolve (no dangling references)
|
||||
3. No stub implementations or TODO placeholders
|
||||
4. All declared exports actually exist
|
||||
|
||||
Run: `npm run typecheck` or equivalent
|
||||
Run: `npm run build` or equivalent
|
||||
|
||||
## Step 3: Behavioral Verification (Layer 2)
|
||||
|
||||
Check:
|
||||
1. All tests pass: `npm test`
|
||||
2. Must-have criteria from plan frontmatter are met
|
||||
3. Requirement coverage: each REQ-ID for this phase is covered
|
||||
|
||||
For unverifiable items: auto-generate test scripts.
|
||||
|
||||
## Step 4: Security Verification (Layer 3)
|
||||
|
||||
STRIDE analysis:
|
||||
- Spoofing, Tampering, Repudiation, Info Disclosure, Denial of Service, Elevation of Privilege
|
||||
|
||||
Auto-disposition: low=accept, medium=mitigate, high=escalate.
|
||||
|
||||
## Step 5: Quality Verification (Layer 4)
|
||||
|
||||
Multi-persona code review:
|
||||
- Correctness: logic errors, edge cases
|
||||
- Testing: coverage gaps, flaky tests
|
||||
- Security: input validation, injection vectors
|
||||
- Performance: unnecessary allocations, O(n^2)
|
||||
- Maintainability: naming, structure, coupling
|
||||
- Adversarial: attack surface, abuse cases
|
||||
|
||||
P0 fixes are auto-applied. P1+ are flagged for post-hoc review.
|
||||
|
||||
## Step 6: Commit Results
|
||||
|
||||
```
|
||||
verify(P##): [passed|gaps_found]
|
||||
|
||||
---ci---
|
||||
phase: [N]
|
||||
milestone: [vX.X]
|
||||
status: verify
|
||||
requirements:
|
||||
covered: [REQ-01, REQ-02]
|
||||
partial: [REQ-03]
|
||||
lessons:
|
||||
- [lesson from verification]
|
||||
---/ci---
|
||||
```
|
||||
|
||||
## Step 7: Return Result
|
||||
|
||||
Report verification score, any gaps found, and next steps.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
description: Audit CIAgent project health — reconstruct state from git log, verify .ciagent/ files match codebase, check for stale references
|
||||
tools:
|
||||
read: true
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<execution_context>
|
||||
@__OPENCODE_DIR__/ci/workflows/audit.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Arguments: $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the CIAgent audit workflow end-to-end.
|
||||
Preserve all workflow gates, validations, checkpoints, and routing.
|
||||
</process>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
description: Clarify CIAgent project ambiguities — generate questions, accept defaults at full autonomy, present at supervised/guided
|
||||
argument-hint: "[phase_number]"
|
||||
tools:
|
||||
read: true
|
||||
bash: true
|
||||
write: true
|
||||
edit: true
|
||||
glob: true
|
||||
grep: true
|
||||
task: true
|
||||
question: true
|
||||
---
|
||||
|
||||
<execution_context>
|
||||
@__OPENCODE_DIR__/ci/workflows/clarify.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Arguments: $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the CIAgent clarify workflow end-to-end.
|
||||
Preserve all workflow gates, validations, checkpoints, and routing.
|
||||
</process>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
description: Systematic CIAgent debugging with git context — triage, diagnose root cause, auto-fix or escalate
|
||||
argument-hint: "[description]"
|
||||
tools:
|
||||
read: true
|
||||
bash: true
|
||||
write: true
|
||||
edit: true
|
||||
glob: true
|
||||
grep: true
|
||||
task: true
|
||||
question: true
|
||||
---
|
||||
|
||||
<execution_context>
|
||||
@__OPENCODE_DIR__/ci/workflows/debug.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Arguments: $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the CIAgent debug workflow end-to-end.
|
||||
Preserve all workflow gates, validations, checkpoints, and routing.
|
||||
</process>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
description: Initialize a new CIAgent project — specification → clarify → create .ciagent/ reference files → initial commit
|
||||
argument-hint: "[description]"
|
||||
tools:
|
||||
read: true
|
||||
bash: true
|
||||
write: true
|
||||
edit: true
|
||||
glob: true
|
||||
grep: true
|
||||
task: true
|
||||
question: true
|
||||
---
|
||||
|
||||
<execution_context>
|
||||
@__OPENCODE_DIR__/ci/workflows/init.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Arguments: $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the CIAgent init workflow end-to-end.
|
||||
Preserve all workflow gates, validations, checkpoints, and routing.
|
||||
</process>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
write: true
|
||||
edit: true
|
||||
glob: true
|
||||
grep: true
|
||||
task: true
|
||||
question: true
|
||||
---
|
||||
|
||||
<execution_context>
|
||||
@__OPENCODE_DIR__/ci/workflows/quick.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Arguments: $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the CIAgent quick workflow end-to-end.
|
||||
Preserve all workflow gates, validations, checkpoints, and routing.
|
||||
</process>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
write: true
|
||||
edit: true
|
||||
glob: true
|
||||
grep: true
|
||||
task: true
|
||||
---
|
||||
|
||||
<execution_context>
|
||||
@__OPENCODE_DIR__/ci/workflows/review.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Arguments: $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the CIAgent review workflow end-to-end.
|
||||
Preserve all workflow gates, validations, checkpoints, and routing.
|
||||
</process>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
write: true
|
||||
edit: true
|
||||
glob: true
|
||||
grep: true
|
||||
task: true
|
||||
question: true
|
||||
---
|
||||
|
||||
<execution_context>
|
||||
@__OPENCODE_DIR__/ci/workflows/rollback.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Arguments: $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the CIAgent rollback workflow end-to-end.
|
||||
Preserve all workflow gates, validations, checkpoints, and routing.
|
||||
</process>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
description: Execute the full CIAgent pipeline — research → plan → execute → verify → complete
|
||||
argument-hint: "[phase_number]"
|
||||
tools:
|
||||
read: true
|
||||
bash: true
|
||||
write: true
|
||||
edit: true
|
||||
glob: true
|
||||
grep: true
|
||||
task: true
|
||||
question: true
|
||||
---
|
||||
|
||||
<execution_context>
|
||||
@__OPENCODE_DIR__/ci/workflows/run.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Arguments: $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the CIAgent run workflow end-to-end.
|
||||
Preserve all workflow gates, validations, checkpoints, and routing.
|
||||
</process>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
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
|
||||
bash: true
|
||||
write: true
|
||||
edit: true
|
||||
glob: true
|
||||
grep: true
|
||||
task: true
|
||||
---
|
||||
|
||||
<execution_context>
|
||||
@__OPENCODE_DIR__/ci/workflows/ship.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Arguments: $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the CIAgent ship workflow end-to-end.
|
||||
Preserve all workflow gates, validations, checkpoints, and routing.
|
||||
</process>
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
description: Show CIAgent project status — current phase, milestone, pipeline stage, decisions, escalations, and requirements coverage
|
||||
tools:
|
||||
read: true
|
||||
bash: true
|
||||
glob: true
|
||||
grep: true
|
||||
---
|
||||
|
||||
<execution_context>
|
||||
@__OPENCODE_DIR__/ci/workflows/status.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Arguments: $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the CIAgent status workflow end-to-end.
|
||||
Preserve all workflow gates, validations, checkpoints, and routing.
|
||||
</process>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
description: Verify CIAgent project deliverables against requirements — structural, behavioral, security, and quality checks
|
||||
argument-hint: "[phase_number]"
|
||||
tools:
|
||||
read: true
|
||||
bash: true
|
||||
write: true
|
||||
edit: true
|
||||
glob: true
|
||||
grep: true
|
||||
task: true
|
||||
---
|
||||
|
||||
<execution_context>
|
||||
@__OPENCODE_DIR__/ci/workflows/verify.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Arguments: $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the CIAgent verify workflow end-to-end.
|
||||
Preserve all workflow gates, validations, checkpoints, and routing.
|
||||
</process>
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"permission": {
|
||||
"read": {
|
||||
"__OPENCODE_DIR__/ci/*": "allow"
|
||||
},
|
||||
"external_directory": {
|
||||
"__OPENCODE_DIR__/ci/*": "allow"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+6
-5
@@ -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",
|
||||
|
||||
+13
-5
@@ -1,20 +1,28 @@
|
||||
{
|
||||
"name": "@continuous-intelligence/ci",
|
||||
"version": "0.2.0",
|
||||
"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/",
|
||||
"opencode/",
|
||||
"templates/",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "ts-node src/cli.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prepublishOnly": "npm run build",
|
||||
"install-opencode": "node scripts/postinstall.js"
|
||||
},
|
||||
"keywords": ["ci", "autonomous", "ai", "software-engineering", "agent"],
|
||||
"keywords": ["ciagent", "autonomous", "ai", "software-engineering", "agent", "multi-project"],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OPENCODE_DIR="${HOME}/.config/opencode"
|
||||
CI_DIR="$(cd "$(dirname "$0")/.." && pwd)/opencode"
|
||||
|
||||
UNINSTALL=false
|
||||
FORCE=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--uninstall) UNINSTALL=true ;;
|
||||
--force) FORCE=true ;;
|
||||
--help|-h)
|
||||
echo "Usage: $(basename "$0") [--uninstall] [--force]"
|
||||
echo ""
|
||||
echo "Install CIAgent opencode integration files to ~/.config/opencode/"
|
||||
echo ""
|
||||
echo " --uninstall Remove CIAgent integration files"
|
||||
echo " --force Overwrite existing files without prompting"
|
||||
echo " --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$UNINSTALL" = true ]; then
|
||||
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 CIAgent repository root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing CIAgent opencode integration..."
|
||||
echo " Source: ${CI_DIR}"
|
||||
echo " Target: ${OPENCODE_DIR}"
|
||||
echo ""
|
||||
|
||||
mkdir -p "${OPENCODE_DIR}/agents"
|
||||
mkdir -p "${OPENCODE_DIR}/command"
|
||||
mkdir -p "${OPENCODE_DIR}/ci/contexts"
|
||||
mkdir -p "${OPENCODE_DIR}/ci/references"
|
||||
mkdir -p "${OPENCODE_DIR}/ci/workflows"
|
||||
|
||||
COPIED=0
|
||||
SKIPPED=0
|
||||
|
||||
copy_file() {
|
||||
local src="$1"
|
||||
local dest="$2"
|
||||
|
||||
if [ -f "$dest" ] && [ "$FORCE" = false ]; then
|
||||
if cmp -s "$src" "$dest" 2>/dev/null; then
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
return
|
||||
fi
|
||||
echo " Conflict: $(basename "$src") already exists. Use --force to overwrite."
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
sed "s|__OPENCODE_DIR__|${OPENCODE_DIR}|g" "$src" > "$dest"
|
||||
COPIED=$((COPIED + 1))
|
||||
}
|
||||
|
||||
echo "Installing agents..."
|
||||
for f in "${CI_DIR}/agents/ci-"*.md; do
|
||||
[ -f "$f" ] && copy_file "$f" "${OPENCODE_DIR}/agents/$(basename "$f")"
|
||||
done
|
||||
|
||||
echo "Installing commands..."
|
||||
for f in "${CI_DIR}/command/ci-"*.md; do
|
||||
[ -f "$f" ] && copy_file "$f" "${OPENCODE_DIR}/command/$(basename "$f")"
|
||||
done
|
||||
|
||||
echo "Installing contexts..."
|
||||
for f in "${CI_DIR}/ci/contexts/"*.md; do
|
||||
[ -f "$f" ] && copy_file "$f" "${OPENCODE_DIR}/ci/contexts/$(basename "$f")"
|
||||
done
|
||||
|
||||
echo "Installing references..."
|
||||
for f in "${CI_DIR}/ci/references/"*.md; do
|
||||
[ -f "$f" ] && copy_file "$f" "${OPENCODE_DIR}/ci/references/$(basename "$f")"
|
||||
done
|
||||
|
||||
echo "Installing workflows..."
|
||||
for f in "${CI_DIR}/ci/workflows/"*.md; do
|
||||
[ -f "$f" ] && copy_file "$f" "${OPENCODE_DIR}/ci/workflows/$(basename "$f")"
|
||||
done
|
||||
|
||||
echo "Installing VERSION..."
|
||||
[ -f "${CI_DIR}/ci/VERSION" ] && copy_file "${CI_DIR}/ci/VERSION" "${OPENCODE_DIR}/ci/VERSION"
|
||||
|
||||
echo ""
|
||||
echo "Merging opencode.json permissions..."
|
||||
OPENCODE_JSON="${OPENCODE_DIR}/opencode.json"
|
||||
CI_JSON="${CI_DIR}/opencode.json"
|
||||
|
||||
if [ -f "$CI_JSON" ]; then
|
||||
if [ ! -f "$OPENCODE_JSON" ]; then
|
||||
sed "s|__OPENCODE_DIR__|${OPENCODE_DIR}|g" "$CI_JSON" > "$OPENCODE_JSON"
|
||||
echo " Created opencode.json"
|
||||
else
|
||||
if command -v node &>/dev/null; then
|
||||
local_ci_json="$(sed "s|__OPENCODE_DIR__|${OPENCODE_DIR}|g" "$CI_JSON")"
|
||||
echo "$local_ci_json" > /tmp/ci-json-merge.json
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const existing = JSON.parse(fs.readFileSync('${OPENCODE_JSON}', 'utf8'));
|
||||
const ci = JSON.parse(fs.readFileSync('/tmp/ci-json-merge.json', 'utf8'));
|
||||
const merged = { ...existing };
|
||||
merged.permission = merged.permission || {};
|
||||
merged.permission.read = merged.permission.read || {};
|
||||
merged.permission.external_directory = merged.permission.external_directory || {};
|
||||
for (const [k, v] of Object.entries(ci.permission?.read || {})) {
|
||||
if (!merged.permission.read[k]) merged.permission.read[k] = v;
|
||||
}
|
||||
for (const [k, v] of Object.entries(ci.permission?.external_directory || {})) {
|
||||
if (!merged.permission.external_directory[k]) merged.permission.external_directory[k] = v;
|
||||
}
|
||||
fs.writeFileSync('${OPENCODE_JSON}', JSON.stringify(merged, null, 2));
|
||||
console.log(' Merged permissions (preserved existing entries)');
|
||||
"
|
||||
rm -f /tmp/ci-json-merge.json
|
||||
else
|
||||
echo " Warning: node not found. Manually merge opencode.json permissions."
|
||||
echo " Add to opencode.json:"
|
||||
echo ' "~/.config/opencode/ci/*": "allow" (in permission.read and permission.external_directory)'
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " CIAgent ► INSTALL COMPLETE"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo " Copied: ${COPIED} files"
|
||||
echo " Skipped: ${SKIPPED} files"
|
||||
echo ""
|
||||
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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
const OPENCODE_DIR = path.join(process.env.HOME || "/root", ".config", "opencode");
|
||||
|
||||
function getPackageDir() {
|
||||
try {
|
||||
return path.resolve(__dirname, "..");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isGlobalInstall() {
|
||||
if (process.env.npm_config_global === "true") return true;
|
||||
if (process.env.npm_config_global === "1") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function copyFile(src, dest, force, templateVars) {
|
||||
if (!fs.existsSync(src)) return { copied: 0, skipped: 0 };
|
||||
|
||||
const dir = path.dirname(dest);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
if (fs.existsSync(dest) && !force) {
|
||||
try {
|
||||
const srcContent = applyTemplate(fs.readFileSync(src, "utf8"), templateVars);
|
||||
const destContent = fs.readFileSync(dest, "utf8");
|
||||
if (srcContent === destContent) return { copied: 0, skipped: 1 };
|
||||
} catch {}
|
||||
return { copied: 0, skipped: 1 };
|
||||
}
|
||||
|
||||
const content = applyTemplate(fs.readFileSync(src, "utf8"), templateVars);
|
||||
fs.writeFileSync(dest, content, "utf8");
|
||||
return { copied: 1, skipped: 0 };
|
||||
}
|
||||
|
||||
function applyTemplate(content, vars) {
|
||||
if (!vars) return content;
|
||||
let result = content;
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
result = result.replaceAll(key, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function install() {
|
||||
const pkgDir = getPackageDir();
|
||||
if (!pkgDir) {
|
||||
console.log("CIAgent postinstall: Could not determine package directory. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
const opencodeDir = path.join(pkgDir, "opencode");
|
||||
if (!fs.existsSync(opencodeDir)) {
|
||||
console.log("CIAgent postinstall: opencode/ directory not found. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isGlobalInstall()) {
|
||||
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;
|
||||
}
|
||||
|
||||
const templateVars = {
|
||||
__OPENCODE_DIR__: OPENCODE_DIR,
|
||||
};
|
||||
|
||||
let copied = 0;
|
||||
let skipped = 0;
|
||||
|
||||
function copyGlob(srcDir, destDir, pattern) {
|
||||
if (!fs.existsSync(srcDir)) return;
|
||||
const entries = fs.readdirSync(srcDir).filter((f) => {
|
||||
if (pattern instanceof RegExp) return pattern.test(f);
|
||||
return f.startsWith(pattern);
|
||||
});
|
||||
for (const entry of entries) {
|
||||
const result = copyFile(path.join(srcDir, entry), path.join(destDir, entry), false, templateVars);
|
||||
copied += result.copied;
|
||||
skipped += result.skipped;
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.join(OPENCODE_DIR, "agents"), { recursive: true });
|
||||
fs.mkdirSync(path.join(OPENCODE_DIR, "command"), { recursive: true });
|
||||
fs.mkdirSync(path.join(OPENCODE_DIR, "ci", "contexts"), { recursive: true });
|
||||
fs.mkdirSync(path.join(OPENCODE_DIR, "ci", "references"), { recursive: true });
|
||||
fs.mkdirSync(path.join(OPENCODE_DIR, "ci", "workflows"), { recursive: true });
|
||||
|
||||
copyGlob(path.join(opencodeDir, "agents"), path.join(OPENCODE_DIR, "agents"), /^ci-/);
|
||||
copyGlob(path.join(opencodeDir, "command"), path.join(OPENCODE_DIR, "command"), /^ci-/);
|
||||
copyGlob(path.join(opencodeDir, "ci", "contexts"), path.join(OPENCODE_DIR, "ci", "contexts"), /\.md$/);
|
||||
copyGlob(path.join(opencodeDir, "ci", "references"), path.join(OPENCODE_DIR, "ci", "references"), /\.md$/);
|
||||
copyGlob(path.join(opencodeDir, "ci", "workflows"), path.join(OPENCODE_DIR, "ci", "workflows"), /\.md$/);
|
||||
|
||||
const versionFile = path.join(opencodeDir, "ci", "VERSION");
|
||||
if (fs.existsSync(versionFile)) {
|
||||
const result = copyFile(versionFile, path.join(OPENCODE_DIR, "ci", "VERSION"), false, templateVars);
|
||||
copied += result.copied;
|
||||
skipped += result.skipped;
|
||||
}
|
||||
|
||||
const ciJsonPath = path.join(opencodeDir, "opencode.json");
|
||||
const targetJsonPath = path.join(OPENCODE_DIR, "opencode.json");
|
||||
|
||||
if (fs.existsSync(ciJsonPath)) {
|
||||
if (!fs.existsSync(targetJsonPath)) {
|
||||
const content = applyTemplate(fs.readFileSync(ciJsonPath, "utf8"), templateVars);
|
||||
fs.writeFileSync(targetJsonPath, content, "utf8");
|
||||
} else {
|
||||
try {
|
||||
const existing = JSON.parse(fs.readFileSync(targetJsonPath, "utf8"));
|
||||
const ciJson = JSON.parse(applyTemplate(fs.readFileSync(ciJsonPath, "utf8"), templateVars));
|
||||
existing.permission = existing.permission || {};
|
||||
existing.permission.read = existing.permission.read || {};
|
||||
existing.permission.external_directory = existing.permission.external_directory || {};
|
||||
for (const [k, v] of Object.entries(ciJson.permission?.read || {})) {
|
||||
if (!existing.permission.read[k]) existing.permission.read[k] = v;
|
||||
}
|
||||
for (const [k, v] of Object.entries(ciJson.permission?.external_directory || {})) {
|
||||
if (!existing.permission.external_directory[k]) existing.permission.external_directory[k] = v;
|
||||
}
|
||||
fs.writeFileSync(targetJsonPath, JSON.stringify(existing, null, 2));
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`CIAgent postinstall: ${copied} files installed, ${skipped} skipped.`);
|
||||
}
|
||||
|
||||
try {
|
||||
install();
|
||||
} catch (err) {
|
||||
console.log("CIAgent postinstall: Non-fatal error:", err.message);
|
||||
}
|
||||
+33
-1
@@ -1,3 +1,6 @@
|
||||
import { IntelligenceBackend, BackendRequest, BackendResult, BackendUnavailableError, emptyBackendResult } from "../backends/types.js";
|
||||
import { AgentName, AutonomyLevel } from "../types/config.js";
|
||||
|
||||
export interface AgentResult {
|
||||
success: boolean;
|
||||
output: string;
|
||||
@@ -14,14 +17,43 @@ export interface AgentContext {
|
||||
stage: string;
|
||||
specification: string;
|
||||
config_path: string;
|
||||
backend?: IntelligenceBackend;
|
||||
}
|
||||
|
||||
export function backendResultToAgentResult(result: BackendResult): AgentResult {
|
||||
return {
|
||||
success: result.success,
|
||||
output: result.output,
|
||||
artifacts_created: result.artifacts.map((a) => a.path),
|
||||
decisions: result.decisions.length,
|
||||
escalations: result.escalations.length,
|
||||
duration_ms: 0,
|
||||
error: result.error,
|
||||
};
|
||||
}
|
||||
|
||||
export abstract class BaseAgent {
|
||||
abstract readonly name: string;
|
||||
abstract readonly name: AgentName;
|
||||
abstract readonly description: string;
|
||||
abstract readonly workflow: string;
|
||||
|
||||
abstract execute(context: AgentContext): Promise<AgentResult>;
|
||||
|
||||
protected async executeViaBackend(context: AgentContext, task: string): Promise<AgentResult> {
|
||||
if (!context.backend) {
|
||||
throw new BackendUnavailableError("none", this.name);
|
||||
}
|
||||
const request: BackendRequest = {
|
||||
persona: this.name,
|
||||
workflow: this.workflow,
|
||||
task,
|
||||
context,
|
||||
autonomy: "full",
|
||||
};
|
||||
const result = await context.backend.execute(request);
|
||||
return backendResultToAgentResult(result);
|
||||
}
|
||||
|
||||
protected log(message: string): void {
|
||||
console.log(`[${this.name}] ${message}`);
|
||||
}
|
||||
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class ChallengerAgent extends BaseAgent {
|
||||
readonly name = "challenger";
|
||||
readonly description = "Stress-tests plans with binding verdicts. Only escalates when confidence < 0.60.";
|
||||
readonly workflow = "plan";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Challenging plan...");
|
||||
const start = Date.now();
|
||||
this.log("Challenging plan...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Stress-test the plan for phase ${context.phase}. Specification: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Plan challenge complete — verdict: proceed",
|
||||
success: false,
|
||||
output: "Plan challenge requires an intelligence backend. Configure one with: ci init --backend",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class CodeReviewerAgent extends BaseAgent {
|
||||
readonly name = "code-reviewer";
|
||||
readonly description = "Multi-persona code review. Auto-applies P0 fixes. Flags P1+ for post-hoc review.";
|
||||
readonly workflow = "review";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Running code review...");
|
||||
const start = Date.now();
|
||||
this.log("Running code review...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Perform multi-persona code review for phase ${context.phase}. Specification: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Code review complete — P0 fixes applied, P1+ flagged for review",
|
||||
artifacts_created: ["CODE-REVIEW.md"],
|
||||
success: false,
|
||||
output: "Code review requires an intelligence backend. Configure one with: ci init --backend",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
+13
-4
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class DebuggerAgent extends BaseAgent {
|
||||
readonly name = "debugger";
|
||||
readonly description = "Autonomous debugging. Auto-fixes when root cause confidence > 0.60, escalates otherwise.";
|
||||
readonly workflow = "debug";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Running autonomous debug...");
|
||||
const start = Date.now();
|
||||
this.log("Running autonomous debug...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Debug the following issue: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Debug complete — issue identified and resolved",
|
||||
artifacts_created: ["DEBUG.md"],
|
||||
success: false,
|
||||
output: "Debugging requires an intelligence backend. Configure one with: ci init --backend",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class DocVerifierAgent extends BaseAgent {
|
||||
readonly name = "doc-verifier";
|
||||
readonly description = "Verifies documentation matches live codebase.";
|
||||
readonly workflow = "verify";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Verifying documentation...");
|
||||
const start = Date.now();
|
||||
this.log("Verifying documentation...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Verify documentation matches codebase for phase ${context.phase}.`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Documentation verification complete",
|
||||
success: false,
|
||||
output: "Documentation verification requires an intelligence backend.",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class DocWriterAgent extends BaseAgent {
|
||||
readonly name = "doc-writer";
|
||||
readonly description = "Autonomous documentation writer. No behavioral changes from Learnship.";
|
||||
readonly workflow = "execute";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Writing documentation...");
|
||||
const start = Date.now();
|
||||
this.log("Writing documentation...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Write documentation for phase ${context.phase}. Specification: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Documentation written",
|
||||
artifacts_created: ["DOCS.md"],
|
||||
success: false,
|
||||
output: "Documentation writing requires an intelligence backend.",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class ExecutorAgent extends BaseAgent {
|
||||
readonly name = "executor";
|
||||
readonly description = "Executes plan tasks autonomously. Never pauses for checkpoints.";
|
||||
readonly workflow = "execute";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Executing tasks...");
|
||||
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 };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Tasks executed",
|
||||
success: false,
|
||||
output: "Execution requires an intelligence backend. Configure one with: ci init --backend",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class IdeationAgent extends BaseAgent {
|
||||
readonly name = "ideation-agent";
|
||||
readonly description = "Generates improvement ideas. Output feeds directly into planning pipeline.";
|
||||
readonly workflow = "research";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Generating improvement ideas...");
|
||||
const start = Date.now();
|
||||
this.log("Generating improvement ideas...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Generate improvement ideas for: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Ideation complete",
|
||||
artifacts_created: ["IDEAS.md"],
|
||||
success: false,
|
||||
output: "Ideation requires an intelligence backend.",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
export { BaseAgent } from "./base.js";
|
||||
export { BaseAgent, AgentContext, AgentResult, backendResultToAgentResult } from "./base.js";
|
||||
export { OrchestratorAgent } from "./orchestrator.js";
|
||||
export { PlannerAgent } from "./planner.js";
|
||||
export { ExecutorAgent } from "./executor.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 } 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 {
|
||||
|
||||
+106
-19
@@ -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 } from "../types/config.js";
|
||||
import { CIAgentConfig, AgentName } from "../types/config.js";
|
||||
import {
|
||||
PipelineState,
|
||||
PipelineStage,
|
||||
@@ -16,30 +16,41 @@ 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";
|
||||
|
||||
export interface GitAgentContext extends AgentContext {
|
||||
gitContext: GitContext;
|
||||
gitBranch: GitBranch;
|
||||
ciFiles: CiFiles;
|
||||
ciFiles: CIAgentFiles;
|
||||
milestone: string;
|
||||
}
|
||||
|
||||
export class OrchestratorAgent extends BaseAgent {
|
||||
readonly name = "orchestrator";
|
||||
readonly description = "Top-level autonomous controller that coordinates the full CI pipeline";
|
||||
readonly name: AgentName = "orchestrator";
|
||||
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[] = [];
|
||||
|
||||
constructor(config?: CIConfig) {
|
||||
private static readonly STAGE_AGENT_MAP: Partial<Record<PipelineStage, AgentName>> = {
|
||||
research: "researcher",
|
||||
plan: "planner",
|
||||
execute: "executor",
|
||||
test: "tester",
|
||||
verify: "verifier",
|
||||
};
|
||||
|
||||
constructor(config?: CIAgentConfig) {
|
||||
super();
|
||||
this.config = config || loadConfig(process.cwd());
|
||||
this.currentMilestone = "v1.0";
|
||||
@@ -47,14 +58,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();
|
||||
@@ -149,6 +160,32 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
context: AgentContext
|
||||
): Promise<PhaseResult> {
|
||||
const stageStart = Date.now();
|
||||
const agentName = OrchestratorAgent.STAGE_AGENT_MAP[stage];
|
||||
|
||||
if (agentName && context.backend) {
|
||||
this.log(`Delegating ${stage} to ${agentName} agent via backend...`);
|
||||
try {
|
||||
const agent = getAgent(agentName);
|
||||
const result = await agent.execute(context);
|
||||
return {
|
||||
phase: this.pipelineState!.current_phase,
|
||||
stage,
|
||||
success: result.success,
|
||||
artifacts_created: Array.isArray(result.artifacts_created) ? result.artifacts_created : [],
|
||||
decisions_made: result.decisions,
|
||||
escalations_raised: result.escalations,
|
||||
duration_ms: Date.now() - stageStart,
|
||||
error: result.error,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof BackendUnavailableError) {
|
||||
this.warn(`Backend unavailable for ${stage}, falling back to mechanical execution`);
|
||||
} else {
|
||||
this.warn(`Agent ${agentName} failed for ${stage}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let decisionsMade = 0;
|
||||
let escalationsRaised = 0;
|
||||
const artifactsCreated: string[] = [];
|
||||
@@ -171,7 +208,7 @@ 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 {
|
||||
@@ -188,7 +225,8 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
cwd: context.project_path,
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch {
|
||||
} catch (err) {
|
||||
this.warn(`Specify commit failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -239,12 +277,27 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
this.log("Researching project domain...");
|
||||
this.decisionEngine!.setPhase(1);
|
||||
|
||||
const archMd = this.ciFiles!.readArchitectureMd();
|
||||
if (!archMd) {
|
||||
this.log("No ARCHITECTURE.md found — mechanical research cannot proceed without backend");
|
||||
return {
|
||||
phase: this.pipelineState!.current_phase,
|
||||
stage: "research",
|
||||
success: false,
|
||||
artifacts_created: artifactsCreated,
|
||||
decisions_made: decisionsMade,
|
||||
escalations_raised: escalationsRaised,
|
||||
duration_ms: Date.now() - stageStart,
|
||||
error: "Research stage requires intelligence backend or existing ARCHITECTURE.md",
|
||||
};
|
||||
}
|
||||
|
||||
if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) {
|
||||
const researchCommit = CommitBuilder.buildResearchCommit(
|
||||
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");
|
||||
@@ -252,12 +305,13 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
cwd: context.project_path,
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch {
|
||||
} catch (err) {
|
||||
this.warn(`Research commit failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.pipelineState!.research_completed = true;
|
||||
artifactsCreated.push(".ci/ARCHITECTURE.md");
|
||||
artifactsCreated.push(".ciagent/ARCHITECTURE.md");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -273,11 +327,42 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
|
||||
case "execute":
|
||||
this.log("Executing implementation...");
|
||||
if (!context.backend) {
|
||||
this.log("No backend available — mechanical execution cannot implement code changes");
|
||||
return {
|
||||
phase: this.pipelineState!.current_phase,
|
||||
stage: "execute",
|
||||
success: false,
|
||||
artifacts_created: artifactsCreated,
|
||||
decisions_made: decisionsMade,
|
||||
escalations_raised: escalationsRaised,
|
||||
duration_ms: Date.now() - stageStart,
|
||||
error: "Execute stage requires intelligence backend for code implementation",
|
||||
};
|
||||
}
|
||||
this.pipelineState!.execute_completed = true;
|
||||
break;
|
||||
|
||||
case "verify": {
|
||||
this.log("Running verification...");
|
||||
|
||||
const { VerificationPipeline } = await import("../verification/index.js");
|
||||
const verification = new VerificationPipeline(context.project_path);
|
||||
const verifyResult = await verification.run(this.pipelineState!.current_phase || 1);
|
||||
|
||||
if (!verifyResult.all_passed) {
|
||||
return {
|
||||
phase: this.pipelineState!.current_phase,
|
||||
stage: "verify",
|
||||
success: false,
|
||||
artifacts_created: artifactsCreated,
|
||||
decisions_made: decisionsMade,
|
||||
escalations_raised: escalationsRaised,
|
||||
duration_ms: Date.now() - stageStart,
|
||||
error: `Verification failed: ${verifyResult.escalations_needed.join("; ")}`,
|
||||
};
|
||||
}
|
||||
|
||||
this.pipelineState!.verify_completed = true;
|
||||
|
||||
if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) {
|
||||
@@ -293,7 +378,8 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
cwd: context.project_path,
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch {
|
||||
} catch (err) {
|
||||
this.warn(`Verify commit failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +404,8 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
cwd: context.project_path,
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch {
|
||||
} catch (err) {
|
||||
this.warn(`Completion commit failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +426,7 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
|
||||
private generateCompletionReport(): string {
|
||||
const lines: string[] = [
|
||||
"# CI Completion Report",
|
||||
"# CIAgent Completion Report",
|
||||
"",
|
||||
`✓ Pipeline completed successfully (git-native)`,
|
||||
"",
|
||||
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class PhaseResearcherAgent extends BaseAgent {
|
||||
readonly name = "phase-researcher";
|
||||
readonly description = "Researches how to implement a specific phase well.";
|
||||
readonly workflow = "research";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Researching phase implementation...");
|
||||
const start = Date.now();
|
||||
this.log("Researching phase implementation...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Research how to implement phase ${context.phase} well. Specification: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Phase research complete",
|
||||
artifacts_created: ["RESEARCH.md"],
|
||||
success: false,
|
||||
output: "Phase research requires an intelligence backend.",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class PlanCheckerAgent extends BaseAgent {
|
||||
readonly name = "plan-checker";
|
||||
readonly description = "Verifies plan quality. On ISSUES FOUND, triggers automatic plan revision (up to 3 iterations).";
|
||||
readonly workflow = "plan";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Checking plan quality...");
|
||||
const start = Date.now();
|
||||
this.log("Checking plan quality...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Verify plan quality for phase ${context.phase}. Specification: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Plan check passed",
|
||||
success: false,
|
||||
output: "Plan checking requires an intelligence backend.",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class PlannerAgent extends BaseAgent {
|
||||
readonly name = "planner";
|
||||
readonly description = "Creates phase plans with tasks. Never sets autonomous:false — decomposes into verifiable subtasks.";
|
||||
readonly workflow = "plan";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Creating phase plan...");
|
||||
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}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Plan created with verifiable subtasks",
|
||||
artifacts_created: ["PLAN.md"],
|
||||
decisions: 1,
|
||||
success: false,
|
||||
output: "Planning requires an intelligence backend. Configure one with: ci init --backend",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class ProjectResearcherAgent extends BaseAgent {
|
||||
readonly name = "project-researcher";
|
||||
readonly description = "Researches the domain ecosystem for a new project.";
|
||||
readonly workflow = "research";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Researching project domain ecosystem...");
|
||||
const start = Date.now();
|
||||
this.log("Researching project domain ecosystem...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Research the domain ecosystem for: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Project research complete",
|
||||
artifacts_created: ["RESEARCH.md"],
|
||||
success: false,
|
||||
output: "Project research requires an intelligence backend.",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class ResearchSynthesizerAgent extends BaseAgent {
|
||||
readonly name = "research-synthesizer";
|
||||
readonly description = "Synthesizes research files into a cohesive summary for roadmap creation.";
|
||||
readonly workflow = "research";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Synthesizing research...");
|
||||
const start = Date.now();
|
||||
this.log("Synthesizing research...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Synthesize research findings into a summary for: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Research synthesis complete",
|
||||
artifacts_created: ["SUMMARY.md"],
|
||||
success: false,
|
||||
output: "Research synthesis requires an intelligence backend.",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class ResearcherAgent extends BaseAgent {
|
||||
readonly name = "researcher";
|
||||
readonly description = "Researches project domain. Logs assumptions instead of asking for validation.";
|
||||
readonly workflow = "research";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Researching domain...");
|
||||
const start = Date.now();
|
||||
this.log("Researching domain...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Research the domain for: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Research complete",
|
||||
artifacts_created: ["RESEARCH.md"],
|
||||
decisions: 1,
|
||||
success: false,
|
||||
output: "Research requires an intelligence backend. Configure one with: ci init --backend",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class RoadmapperAgent extends BaseAgent {
|
||||
readonly name = "roadmapper";
|
||||
readonly description = "Creates and maintains project roadmaps.";
|
||||
readonly workflow = "plan";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Creating roadmap...");
|
||||
const start = Date.now();
|
||||
this.log("Creating roadmap...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Create project roadmap for: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Roadmap created",
|
||||
artifacts_created: ["ROADMAP.md"],
|
||||
decisions: 1,
|
||||
success: false,
|
||||
output: "Roadmap creation requires an intelligence backend.",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class SecurityAuditorAgent extends BaseAgent {
|
||||
readonly name = "security-auditor";
|
||||
readonly description = "Auto-dispositions threats: low=accept, medium=mitigate, high=escalate.";
|
||||
readonly workflow = "verify";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Running security audit...");
|
||||
const start = Date.now();
|
||||
this.log("Running security audit...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Perform security audit for phase ${context.phase}. Specification: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Security audit complete",
|
||||
artifacts_created: ["SECURITY.md"],
|
||||
decisions: 1,
|
||||
success: false,
|
||||
output: "Security auditing requires an intelligence backend. Configure one with: ci init --backend",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,27 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
|
||||
export class SolutionWriterAgent extends BaseAgent {
|
||||
readonly name = "solution-writer";
|
||||
readonly description = "Produces structured solution documents for .planning/solutions/.";
|
||||
readonly description = "Produces structured solution documents.";
|
||||
readonly workflow = "execute";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Writing solution document...");
|
||||
const start = Date.now();
|
||||
this.log("Writing solution document...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Write a structured solution document for: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Solution document written",
|
||||
artifacts_created: ["SOLUTION.md"],
|
||||
success: false,
|
||||
output: "Solution writing requires an intelligence backend.",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
|
||||
export class TesterAgent extends BaseAgent {
|
||||
readonly name = "tester";
|
||||
readonly description = "Runs automated tests and validates 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 automated tests for: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: "Testing requires an intelligence backend.",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
+13
-4
@@ -3,17 +3,26 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
export class VerifierAgent extends BaseAgent {
|
||||
readonly name = "verifier";
|
||||
readonly description = "Verifies phase outputs. Generates automated tests instead of requesting human UAT.";
|
||||
readonly workflow = "verify";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
this.log("Verifying phase output...");
|
||||
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}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: "Verification complete — all checks passed",
|
||||
artifacts_created: ["VERIFICATION.md"],
|
||||
success: false,
|
||||
output: "Verification requires an intelligence backend. Configure one with: ci init --backend",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { OllamaLocalBackend } from "../backends/ollama-local.js";
|
||||
import { OllamaCloudBackend } from "../backends/ollama-cloud.js";
|
||||
import { OpencodeBackend } from "../backends/opencode.js";
|
||||
import { resolveBackend, createBackend } from "../backends/index.js";
|
||||
import { DEFAULT_BACKEND_CONFIG, BackendUnavailableError } from "../backends/types.js";
|
||||
|
||||
describe("Backend Availability Detection", () => {
|
||||
describe("OllamaLocalBackend.isAvailable", () => {
|
||||
it("returns false for unreachable host", async () => {
|
||||
const backend = new OllamaLocalBackend({
|
||||
base_url: "http://localhost:1",
|
||||
model_profile: "balanced",
|
||||
});
|
||||
expect(await backend.isAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for invalid URL", async () => {
|
||||
const backend = new OllamaLocalBackend({
|
||||
base_url: "not-a-url",
|
||||
model_profile: "balanced",
|
||||
});
|
||||
expect(await backend.isAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for timeout", async () => {
|
||||
const backend = new OllamaLocalBackend({
|
||||
base_url: "http://192.0.2.1",
|
||||
model_profile: "balanced",
|
||||
});
|
||||
expect(await backend.isAvailable()).toBe(false);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe("OllamaCloudBackend.isAvailable", () => {
|
||||
it("returns false when base_url is empty", async () => {
|
||||
const backend = new OllamaCloudBackend({
|
||||
base_url: "",
|
||||
api_key_env: "OLLAMA_CLOUD_API_KEY",
|
||||
model_profile: "quality",
|
||||
});
|
||||
expect(await backend.isAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no API key in env", async () => {
|
||||
const backend = new OllamaCloudBackend({
|
||||
base_url: "https://api.example.com",
|
||||
api_key_env: "NONEXISTENT_ENV_VAR_12345",
|
||||
model_profile: "quality",
|
||||
timeout_ms: 5000,
|
||||
});
|
||||
expect(await backend.isAvailable()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpencodeBackend.isAvailable", () => {
|
||||
it("returns false when executable not found", async () => {
|
||||
const backend = new OpencodeBackend({
|
||||
enabled: true,
|
||||
executable: "nonexistent-opencode-binary-xyz",
|
||||
});
|
||||
expect(await backend.isAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when disabled", async () => {
|
||||
const backend = new OpencodeBackend({ enabled: false });
|
||||
expect(await backend.isAvailable()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveBackend auto-detection", () => {
|
||||
it("throws BackendUnavailableError when no backends available", async () => {
|
||||
const config = {
|
||||
...DEFAULT_BACKEND_CONFIG,
|
||||
llm_backends: {
|
||||
"ollama-local": { base_url: "http://localhost:1", model_profile: "balanced" as const },
|
||||
"ollama-cloud": { base_url: "", api_key_env: "NONEXISTENT_12345", model_profile: "quality" as const },
|
||||
},
|
||||
agent_backends: {
|
||||
opencode: { enabled: true, executable: "nonexistent-opencode-binary-xyz" },
|
||||
},
|
||||
};
|
||||
|
||||
await expect(resolveBackend(config)).rejects.toThrow(BackendUnavailableError);
|
||||
});
|
||||
|
||||
it("tries opencode before ollama-local", async () => {
|
||||
expect(DEFAULT_BACKEND_CONFIG.provider).toBe("auto");
|
||||
});
|
||||
|
||||
it("createBackend throws for unknown provider", () => {
|
||||
expect(() => createBackend("unknown-provider" as "opencode", DEFAULT_BACKEND_CONFIG)).toThrow(BackendUnavailableError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("BackendUnavailableError", () => {
|
||||
it("contains installation hints", () => {
|
||||
const err = new BackendUnavailableError("auto");
|
||||
expect(err.message).toContain("opencode");
|
||||
expect(err.message).toContain("Ollama");
|
||||
expect(err.message).toContain("OLLAMA_CLOUD_API_KEY");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import { BackendUnavailableError, emptyTokenUsage, emptyBackendResult, DEFAULT_BACKEND_CONFIG } from "../backends/types.js";
|
||||
import { OllamaLocalBackend } from "../backends/ollama-local.js";
|
||||
import { OllamaCloudBackend } from "../backends/ollama-cloud.js";
|
||||
import { OpencodeBackend } from "../backends/opencode.js";
|
||||
|
||||
describe("BackendUnavailableError", () => {
|
||||
it("includes backend name in message", () => {
|
||||
const err = new BackendUnavailableError("ollama-local");
|
||||
expect(err.message).toContain("ollama-local");
|
||||
expect(err.backendName).toBe("ollama-local");
|
||||
});
|
||||
|
||||
it("includes agent name when provided", () => {
|
||||
const err = new BackendUnavailableError("opencode", "executor");
|
||||
expect(err.agentName).toBe("executor");
|
||||
expect(err.message).toContain("executor");
|
||||
});
|
||||
});
|
||||
|
||||
describe("emptyTokenUsage", () => {
|
||||
it("returns zeroed usage", () => {
|
||||
const usage = emptyTokenUsage();
|
||||
expect(usage.input_tokens).toBe(0);
|
||||
expect(usage.output_tokens).toBe(0);
|
||||
expect(usage.total_tokens).toBe(0);
|
||||
expect(usage.estimated_cost_usd).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("emptyBackendResult", () => {
|
||||
it("returns failed result with no artifacts", () => {
|
||||
const result = emptyBackendResult("something failed");
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe("something failed");
|
||||
expect(result.artifacts).toEqual([]);
|
||||
expect(result.decisions).toEqual([]);
|
||||
expect(result.escalations).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns result without error when no message provided", () => {
|
||||
const result = emptyBackendResult();
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DEFAULT_BACKEND_CONFIG", () => {
|
||||
it("has auto provider by default", () => {
|
||||
expect(DEFAULT_BACKEND_CONFIG.provider).toBe("auto");
|
||||
});
|
||||
|
||||
it("has opencode agent backend enabled", () => {
|
||||
expect(DEFAULT_BACKEND_CONFIG.agent_backends.opencode?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("has ollama-local and ollama-cloud llm backends", () => {
|
||||
expect(DEFAULT_BACKEND_CONFIG.llm_backends["ollama-local"]).toBeDefined();
|
||||
expect(DEFAULT_BACKEND_CONFIG.llm_backends["ollama-cloud"]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("OllamaLocalBackend", () => {
|
||||
it("has correct name and type", () => {
|
||||
const backend = new OllamaLocalBackend();
|
||||
expect(backend.name).toBe("ollama-local");
|
||||
expect(backend.type).toBe("llm");
|
||||
});
|
||||
|
||||
it("returns false when local Ollama is not available", async () => {
|
||||
const backend = new OllamaLocalBackend({
|
||||
base_url: "http://localhost:1",
|
||||
model_profile: "balanced",
|
||||
});
|
||||
const available = await backend.isAvailable();
|
||||
expect(available).toBe(false);
|
||||
});
|
||||
|
||||
it("uses default config when none provided", () => {
|
||||
const backend = new OllamaLocalBackend();
|
||||
expect(backend).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("OllamaCloudBackend", () => {
|
||||
it("has correct name and type", () => {
|
||||
const backend = new OllamaCloudBackend();
|
||||
expect(backend.name).toBe("ollama-cloud");
|
||||
expect(backend.type).toBe("llm");
|
||||
});
|
||||
|
||||
it("returns false when no base_url configured", async () => {
|
||||
const backend = new OllamaCloudBackend({
|
||||
base_url: "",
|
||||
api_key_env: "NONEXISTENT_KEY",
|
||||
model_profile: "quality",
|
||||
timeout_ms: 5000,
|
||||
});
|
||||
const available = await backend.isAvailable();
|
||||
expect(available).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no API key available", async () => {
|
||||
const backend = new OllamaCloudBackend({
|
||||
base_url: "https://example.com",
|
||||
api_key_env: "NONEXISTENT_CI_KEY_12345",
|
||||
model_profile: "quality",
|
||||
timeout_ms: 5000,
|
||||
});
|
||||
const available = await backend.isAvailable();
|
||||
expect(available).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpencodeBackend", () => {
|
||||
it("has correct name and type", () => {
|
||||
const backend = new OpencodeBackend();
|
||||
expect(backend.name).toBe("opencode");
|
||||
expect(backend.type).toBe("agent");
|
||||
});
|
||||
|
||||
it("returns false when opencode is not installed", async () => {
|
||||
const backend = new OpencodeBackend({
|
||||
enabled: true,
|
||||
executable: "nonexistent-opencode-binary-xyz",
|
||||
});
|
||||
const available = await backend.isAvailable();
|
||||
expect(available).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { IntelligenceBackend, BackendConfigSection, BackendUnavailableError } from "./types.js";
|
||||
import { OpencodeBackend } from "./opencode.js";
|
||||
import { OllamaLocalBackend } from "./ollama-local.js";
|
||||
import { OllamaCloudBackend } from "./ollama-cloud.js";
|
||||
|
||||
const AUTO_DETECT_ORDER: Array<"opencode" | "ollama-local" | "ollama-cloud"> = [
|
||||
"opencode",
|
||||
"ollama-local",
|
||||
"ollama-cloud",
|
||||
];
|
||||
|
||||
export function createBackend(
|
||||
name: string,
|
||||
config: BackendConfigSection
|
||||
): IntelligenceBackend {
|
||||
switch (name) {
|
||||
case "opencode":
|
||||
return new OpencodeBackend(config.agent_backends.opencode);
|
||||
case "ollama-local":
|
||||
return new OllamaLocalBackend(config.llm_backends["ollama-local"]);
|
||||
case "ollama-cloud":
|
||||
return new OllamaCloudBackend(config.llm_backends["ollama-cloud"]);
|
||||
default:
|
||||
throw new BackendUnavailableError(name);
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveBackend(
|
||||
config: BackendConfigSection
|
||||
): Promise<IntelligenceBackend> {
|
||||
if (config.provider !== "auto") {
|
||||
const backend = createBackend(config.provider, config);
|
||||
if (!(await backend.isAvailable())) {
|
||||
throw new BackendUnavailableError(config.provider);
|
||||
}
|
||||
return backend;
|
||||
}
|
||||
|
||||
for (const name of AUTO_DETECT_ORDER) {
|
||||
try {
|
||||
const backend = createBackend(name, config);
|
||||
if (await backend.isAvailable()) {
|
||||
return backend;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
throw new BackendUnavailableError("auto");
|
||||
}
|
||||
|
||||
export { IntelligenceBackend, BackendConfigSection, BackendUnavailableError } from "./types.js";
|
||||
export { ToolRegistry, ToolDefinition, ToolCall, ToolResult } from "./tool-registry.js";
|
||||
export { OpencodeBackend } from "./opencode.js";
|
||||
export { OllamaLocalBackend } from "./ollama-local.js";
|
||||
export { OllamaCloudBackend } from "./ollama-cloud.js";
|
||||
@@ -0,0 +1,229 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { OllamaBaseBackend, OllamaMessage, OllamaChatResponse } from "../backends/ollama-base.js";
|
||||
import { ToolRegistry } from "../backends/tool-registry.js";
|
||||
import { BackendRequest } from "../backends/types.js";
|
||||
|
||||
class TestableOllamaBaseBackend extends OllamaBaseBackend {
|
||||
readonly name = "test-base";
|
||||
private mockResponse: OllamaChatResponse;
|
||||
private callCount: number;
|
||||
|
||||
constructor(mockResponse: OllamaChatResponse) {
|
||||
super(undefined);
|
||||
this.mockResponse = mockResponse;
|
||||
this.callCount = 0;
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
getCallCount(): number {
|
||||
return this.callCount;
|
||||
}
|
||||
|
||||
protected async callModel(
|
||||
messages: OllamaMessage[],
|
||||
model: string,
|
||||
toolRegistry: ToolRegistry
|
||||
): Promise<OllamaChatResponse> {
|
||||
this.callCount++;
|
||||
return this.mockResponse;
|
||||
}
|
||||
|
||||
protected resolveModel(): string {
|
||||
return "test-model";
|
||||
}
|
||||
}
|
||||
|
||||
describe("OllamaBaseBackend", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-ollama-base-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("returns success when model responds without tool calls", async () => {
|
||||
const mockResponse: OllamaChatResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
content: '{"success": true, "output": "task completed"}',
|
||||
},
|
||||
}],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
|
||||
};
|
||||
|
||||
const backend = new TestableOllamaBaseBackend(mockResponse);
|
||||
const request: BackendRequest = {
|
||||
persona: "executor",
|
||||
workflow: "execute",
|
||||
task: "Do something",
|
||||
context: {
|
||||
project_path: tempDir,
|
||||
phase: 1,
|
||||
stage: "execute",
|
||||
specification: "",
|
||||
config_path: "",
|
||||
},
|
||||
autonomy: "full",
|
||||
};
|
||||
|
||||
const result = await backend.execute(request);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain("task completed");
|
||||
});
|
||||
|
||||
it("handles tool calls in response", async () => {
|
||||
const writePath = path.join(tempDir, "output.txt");
|
||||
const responses: OllamaChatResponse[] = [
|
||||
{
|
||||
choices: [{
|
||||
message: {
|
||||
content: "",
|
||||
tool_calls: [{
|
||||
function: { name: "writeFile", arguments: JSON.stringify({ path: writePath, content: "hello" }) },
|
||||
}],
|
||||
},
|
||||
}],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||
},
|
||||
{
|
||||
choices: [{
|
||||
message: {
|
||||
content: '{"success": true, "output": "file written"}',
|
||||
},
|
||||
}],
|
||||
usage: { prompt_tokens: 5, completion_tokens: 10, total_tokens: 15 },
|
||||
},
|
||||
];
|
||||
|
||||
let callIndex = 0;
|
||||
class ToolCallBackend extends OllamaBaseBackend {
|
||||
readonly name = "tool-call-test";
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
async isAvailable(): Promise<boolean> { return true; }
|
||||
protected async callModel(): Promise<OllamaChatResponse> {
|
||||
return responses[callIndex++];
|
||||
}
|
||||
protected resolveModel(): string { return "test-model"; }
|
||||
}
|
||||
|
||||
const backend = new ToolCallBackend();
|
||||
const request: BackendRequest = {
|
||||
persona: "executor",
|
||||
workflow: "execute",
|
||||
task: "Write a file",
|
||||
context: {
|
||||
project_path: tempDir,
|
||||
phase: 1,
|
||||
stage: "execute",
|
||||
specification: "",
|
||||
config_path: "",
|
||||
},
|
||||
autonomy: "full",
|
||||
};
|
||||
|
||||
const result = await backend.execute(request);
|
||||
expect(result.success).toBe(true);
|
||||
expect(fs.existsSync(writePath)).toBe(true);
|
||||
expect(fs.readFileSync(writePath, "utf-8")).toBe("hello");
|
||||
expect(result.artifacts.length).toBe(1);
|
||||
expect(result.artifacts[0].path).toBe(writePath);
|
||||
});
|
||||
|
||||
it("stops after max tool rounds", async () => {
|
||||
const alwaysToolCall: OllamaChatResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
content: "",
|
||||
tool_calls: [{
|
||||
function: { name: "readFile", arguments: JSON.stringify({ path: "/etc/hostname" }) },
|
||||
}],
|
||||
},
|
||||
}],
|
||||
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
||||
};
|
||||
|
||||
class InfiniteLoopBackend extends OllamaBaseBackend {
|
||||
readonly name = "infinite-loop";
|
||||
private callCount = 0;
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
async isAvailable(): Promise<boolean> { return true; }
|
||||
protected async callModel(): Promise<OllamaChatResponse> {
|
||||
this.callCount++;
|
||||
return alwaysToolCall;
|
||||
}
|
||||
protected resolveModel(): string { return "test-model"; }
|
||||
getCallCount() { return this.callCount; }
|
||||
}
|
||||
|
||||
const backend = new InfiniteLoopBackend();
|
||||
const request: BackendRequest = {
|
||||
persona: "executor",
|
||||
workflow: "execute",
|
||||
task: "Infinite loop test",
|
||||
context: {
|
||||
project_path: tempDir,
|
||||
phase: 1,
|
||||
stage: "execute",
|
||||
specification: "",
|
||||
config_path: "",
|
||||
},
|
||||
autonomy: "full",
|
||||
};
|
||||
|
||||
const result = await backend.execute(request);
|
||||
expect(result.output).toContain("maximum rounds");
|
||||
expect(backend.getCallCount()).toBe(50);
|
||||
});
|
||||
|
||||
it("handles error from callModel gracefully", async () => {
|
||||
class ErrorBackend extends OllamaBaseBackend {
|
||||
readonly name = "error-backend";
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
async isAvailable(): Promise<boolean> { return true; }
|
||||
protected async callModel(): Promise<OllamaChatResponse> {
|
||||
throw new Error("Model connection failed");
|
||||
}
|
||||
protected resolveModel(): string { return "test-model"; }
|
||||
}
|
||||
|
||||
const backend = new ErrorBackend();
|
||||
const request: BackendRequest = {
|
||||
persona: "executor",
|
||||
workflow: "execute",
|
||||
task: "Fail test",
|
||||
context: {
|
||||
project_path: tempDir,
|
||||
phase: 1,
|
||||
stage: "execute",
|
||||
specification: "",
|
||||
config_path: "",
|
||||
},
|
||||
autonomy: "full",
|
||||
};
|
||||
|
||||
const result = await backend.execute(request);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("Backend execution failed");
|
||||
});
|
||||
|
||||
it("modelProfileToModel selects smallest for speed", () => {
|
||||
const backend = new TestableOllamaBaseBackend({} as OllamaChatResponse);
|
||||
const models = ["llama3.1:70b", "llama3.1:8b", "llama3.1"];
|
||||
const selected = (backend as unknown as { modelProfileToModel: (p: string, m: string[]) => string }).modelProfileToModel("speed", models);
|
||||
expect(selected).toBe("llama3.1");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,315 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import {
|
||||
IntelligenceBackend,
|
||||
BackendRequest,
|
||||
BackendResult,
|
||||
BackendType,
|
||||
LLMBackendConfig,
|
||||
TokenUsage,
|
||||
Artifact,
|
||||
emptyTokenUsage,
|
||||
emptyBackendResult,
|
||||
} from "./types.js";
|
||||
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";
|
||||
|
||||
const MAX_TOOL_ROUNDS = 50;
|
||||
|
||||
export abstract class OllamaBaseBackend implements IntelligenceBackend {
|
||||
abstract readonly name: string;
|
||||
readonly type: BackendType = "llm";
|
||||
|
||||
protected config: LLMBackendConfig;
|
||||
protected projectPath: string;
|
||||
|
||||
constructor(config: LLMBackendConfig | undefined) {
|
||||
this.config = config || { base_url: "http://localhost:11434", model_profile: "balanced" };
|
||||
this.projectPath = process.cwd();
|
||||
}
|
||||
|
||||
abstract isAvailable(): Promise<boolean>;
|
||||
|
||||
async execute(request: BackendRequest): Promise<BackendResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const personaContent = this.loadPersona(request.persona);
|
||||
const workflowContent = this.loadWorkflow(request.workflow);
|
||||
const model = this.resolveModel();
|
||||
|
||||
const toolRegistry = new ToolRegistry(request.context.project_path);
|
||||
|
||||
const messages: OllamaMessage[] = [];
|
||||
messages.push({
|
||||
role: "system",
|
||||
content: this.buildSystemPrompt(personaContent, workflowContent, request),
|
||||
});
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: request.task,
|
||||
});
|
||||
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
let round = 0;
|
||||
const allArtifacts: Artifact[] = [];
|
||||
const allDecisions: Decision[] = [];
|
||||
const allEscalations: Escalation[] = [];
|
||||
|
||||
while (round < MAX_TOOL_ROUNDS) {
|
||||
round++;
|
||||
const response = await this.callModel(messages, model, toolRegistry);
|
||||
|
||||
totalInputTokens += response.usage?.prompt_tokens || 0;
|
||||
totalOutputTokens += response.usage?.completion_tokens || 0;
|
||||
|
||||
const assistantContent = response.choices?.[0]?.message?.content || "";
|
||||
const toolCalls = response.choices?.[0]?.message?.tool_calls;
|
||||
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: assistantContent,
|
||||
tool_calls: toolCalls,
|
||||
});
|
||||
|
||||
if (!toolCalls || toolCalls.length === 0) {
|
||||
return this.parseFinalResponse(assistantContent, allArtifacts, allDecisions, allEscalations, {
|
||||
input_tokens: totalInputTokens,
|
||||
output_tokens: totalOutputTokens,
|
||||
total_tokens: totalInputTokens + totalOutputTokens,
|
||||
estimated_cost_usd: 0,
|
||||
});
|
||||
}
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
const call: ToolCall = {
|
||||
name: toolCall.function.name,
|
||||
arguments: JSON.parse(toolCall.function.arguments),
|
||||
};
|
||||
const result = toolRegistry.execute(call);
|
||||
messages.push({
|
||||
role: "tool",
|
||||
name: call.name,
|
||||
content: result.content,
|
||||
});
|
||||
|
||||
if (call.name === "writeFile" && !result.isError) {
|
||||
allArtifacts.push({
|
||||
path: String(call.arguments.path),
|
||||
content: String(call.arguments.content),
|
||||
operation: "create",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalContent = messages
|
||||
.filter((m) => m.role === "assistant" && m.content)
|
||||
.map((m) => m.content)
|
||||
.join("\n");
|
||||
|
||||
return this.parseFinalResponse(
|
||||
`Tool loop reached maximum rounds (${MAX_TOOL_ROUNDS}). Partial progress:\n${finalContent}`,
|
||||
allArtifacts,
|
||||
allDecisions,
|
||||
allEscalations,
|
||||
{ input_tokens: totalInputTokens, output_tokens: totalOutputTokens, total_tokens: totalInputTokens + totalOutputTokens, estimated_cost_usd: 0 }
|
||||
);
|
||||
} catch (err) {
|
||||
return emptyBackendResult(`Backend execution failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract callModel(
|
||||
messages: OllamaMessage[],
|
||||
model: string,
|
||||
toolRegistry: ToolRegistry
|
||||
): Promise<OllamaChatResponse>;
|
||||
|
||||
protected abstract resolveModel(): string;
|
||||
|
||||
protected buildSystemPrompt(persona: string, workflow: string, request: BackendRequest): string {
|
||||
const parts = [persona];
|
||||
if (workflow) {
|
||||
parts.push("", "## Workflow Instructions", workflow);
|
||||
}
|
||||
parts.push(
|
||||
"",
|
||||
"## Execution Context",
|
||||
`Autonomy level: ${request.autonomy}`,
|
||||
`Project path: ${request.context.project_path}`,
|
||||
`Phase: ${request.context.phase}`,
|
||||
`Stage: ${request.context.stage}`,
|
||||
"",
|
||||
"## Output Format",
|
||||
"When you have completed your task, output a JSON object with this structure:",
|
||||
"```json",
|
||||
'{',
|
||||
' "success": true,',
|
||||
' "output": "Summary of what was accomplished",',
|
||||
' "artifacts": [{"path": "file/path", "content": "...", "operation": "create"}],',
|
||||
' "decisions": [{"id": "D-NNN", "decision": "what", "rationale": "why", "confidence": 0.85, "category": "general", "alternatives_considered": [], "human_override": null, "timestamp": ""}],',
|
||||
' "escalations": []',
|
||||
'}',
|
||||
"```"
|
||||
);
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
protected loadPersona(persona: AgentName): string {
|
||||
const candidates = [
|
||||
path.join(os.homedir(), ".config", "opencode", "agents", `ci-${persona}.md`),
|
||||
path.join(process.cwd(), "opencode", "agents", `ci-${persona}.md`),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return fs.readFileSync(candidate, "utf-8");
|
||||
}
|
||||
}
|
||||
return `You are the CIAgent ${persona} agent. Execute the requested task thoroughly and autonomously.`;
|
||||
}
|
||||
|
||||
protected loadWorkflow(workflow: string): string {
|
||||
const candidates = [
|
||||
path.join(os.homedir(), ".config", "opencode", "ci", "workflows", `${workflow}.md`),
|
||||
path.join(process.cwd(), "opencode", "workflows", `${workflow}.md`),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return fs.readFileSync(candidate, "utf-8");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
protected parseFinalResponse(
|
||||
content: string,
|
||||
artifacts: Artifact[],
|
||||
decisions: Decision[],
|
||||
escalations: Escalation[],
|
||||
usage: TokenUsage
|
||||
): BackendResult {
|
||||
const jsonMatch = content.match(/\{[\s\S]*"success"[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
return {
|
||||
success: parsed.success ?? true,
|
||||
output: parsed.output || content,
|
||||
artifacts: parsed.artifacts?.length ? this.parseArtifacts(parsed.artifacts) : artifacts,
|
||||
decisions: parsed.decisions?.length ? this.parseDecisions(parsed.decisions) : decisions,
|
||||
escalations: parsed.escalations?.length ? this.parseEscalations(parsed.escalations) : escalations,
|
||||
usage,
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: content,
|
||||
artifacts,
|
||||
decisions,
|
||||
escalations,
|
||||
usage,
|
||||
};
|
||||
}
|
||||
|
||||
private parseArtifacts(raw: unknown[]): Artifact[] {
|
||||
return raw.filter((a): a is Record<string, unknown> => !!a).map((a) => ({
|
||||
path: String(a.path || ""),
|
||||
content: String(a.content || ""),
|
||||
operation: (a.operation as Artifact["operation"]) || "create",
|
||||
}));
|
||||
}
|
||||
|
||||
private parseDecisions(raw: unknown[]): Decision[] {
|
||||
return raw.filter((d): d is Record<string, unknown> => !!d).map((d) => ({
|
||||
id: String(d.id || "D-000"),
|
||||
decision: String(d.decision || ""),
|
||||
rationale: String(d.rationale || ""),
|
||||
confidence: Number(d.confidence || 0.5),
|
||||
category: (d.category as Decision["category"]) || "general",
|
||||
alternatives_considered: Array.isArray(d.alternatives_considered)
|
||||
? d.alternatives_considered.map((a: unknown) =>
|
||||
typeof a === "string"
|
||||
? { option: a, rejected_reason: "" }
|
||||
: (a as { option: string; rejected_reason: string })
|
||||
)
|
||||
: [],
|
||||
human_override: d.human_override ? String(d.human_override) : null,
|
||||
timestamp: String(d.timestamp || new Date().toISOString()),
|
||||
}));
|
||||
}
|
||||
|
||||
private parseEscalations(raw: unknown[]): Escalation[] {
|
||||
return raw.filter((e): e is Record<string, unknown> => !!e).map((e) => ({
|
||||
id: String(e.id || "E-000"),
|
||||
timestamp: String(e.timestamp || new Date().toISOString()),
|
||||
type: (e.type as Escalation["type"]) || "specification_ambiguity",
|
||||
phase: String(e.phase || ""),
|
||||
description: String(e.description || ""),
|
||||
context: String(e.context || ""),
|
||||
options: Array.isArray(e.options) ? e.options : [],
|
||||
default_option_id: String(e.default_option_id || ""),
|
||||
resolution: (e.resolution as Escalation["resolution"]) || "pending",
|
||||
audit_file: String(e.audit_file || ""),
|
||||
}));
|
||||
}
|
||||
|
||||
protected modelProfileToModel(profile: ModelProfile, availableModels: string[]): string {
|
||||
if (availableModels.length === 0) return "llama3.1";
|
||||
|
||||
const sorted = [...availableModels].sort((a, b) => a.length - b.length);
|
||||
switch (profile) {
|
||||
case "speed":
|
||||
return sorted[0];
|
||||
case "quality":
|
||||
return sorted[sorted.length - 1];
|
||||
case "balanced":
|
||||
default:
|
||||
return sorted[Math.floor(sorted.length / 2)] || sorted[0];
|
||||
}
|
||||
}
|
||||
|
||||
protected async fetchAvailableModels(): Promise<string[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.config.base_url}/api/tags`);
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json() as { models?: Array<{ name: string }> };
|
||||
return (data.models || []).map((m) => m.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OllamaMessage {
|
||||
role: "system" | "user" | "assistant" | "tool";
|
||||
content: string;
|
||||
name?: string;
|
||||
tool_calls?: Array<{
|
||||
function: { name: string; arguments: string };
|
||||
}>;
|
||||
}
|
||||
|
||||
interface OllamaChatResponse {
|
||||
choices?: Array<{
|
||||
message: {
|
||||
content: string;
|
||||
tool_calls?: Array<{
|
||||
function: { name: string; arguments: string };
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
export { OllamaMessage, OllamaChatResponse };
|
||||
@@ -0,0 +1,90 @@
|
||||
import * as os from "node:os";
|
||||
import { OllamaCloudBackend } from "../backends/ollama-cloud.js";
|
||||
|
||||
describe("OllamaCloudBackend Retry/Rate-Limit", () => {
|
||||
describe("configuration", () => {
|
||||
it("uses default config when none provided", () => {
|
||||
const backend = new OllamaCloudBackend();
|
||||
expect(backend.name).toBe("ollama-cloud");
|
||||
expect(backend.type).toBe("llm");
|
||||
});
|
||||
|
||||
it("accepts custom config", () => {
|
||||
const backend = new OllamaCloudBackend({
|
||||
base_url: "https://custom.api.com",
|
||||
api_key_env: "MY_API_KEY",
|
||||
model_profile: "quality",
|
||||
timeout_ms: 30000,
|
||||
});
|
||||
expect(backend).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isAvailable", () => {
|
||||
it("returns false when base_url is empty", async () => {
|
||||
const backend = new OllamaCloudBackend({
|
||||
base_url: "",
|
||||
api_key_env: "KEY",
|
||||
model_profile: "quality",
|
||||
});
|
||||
expect(await backend.isAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no API key in environment", async () => {
|
||||
const backend = new OllamaCloudBackend({
|
||||
base_url: "https://api.example.com",
|
||||
api_key_env: "NONEXISTENT_API_KEY_VAR_98765",
|
||||
model_profile: "quality",
|
||||
timeout_ms: 5000,
|
||||
});
|
||||
expect(await backend.isAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for unreachable endpoint", async () => {
|
||||
process.env.TEST_OLLAMA_CLOUD_KEY = "test-key";
|
||||
const backend = new OllamaCloudBackend({
|
||||
base_url: "http://localhost:1",
|
||||
api_key_env: "TEST_OLLAMA_CLOUD_KEY",
|
||||
model_profile: "quality",
|
||||
timeout_ms: 5000,
|
||||
});
|
||||
expect(await backend.isAvailable()).toBe(false);
|
||||
delete process.env.TEST_OLLAMA_CLOUD_KEY;
|
||||
});
|
||||
});
|
||||
|
||||
describe("retry behavior", () => {
|
||||
it("MAX_RETRIES is 3", () => {
|
||||
const source = OllamaCloudBackend.toString();
|
||||
expect(source).toBeDefined();
|
||||
});
|
||||
|
||||
it("BASE_BACKOFF_MS is 1000", () => {
|
||||
const source = OllamaCloudBackend.toString();
|
||||
expect(source).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("authentication", () => {
|
||||
it("uses API key from environment variable", () => {
|
||||
process.env.TEST_CI_CLOUD_KEY = "sk-test-key-123";
|
||||
const backend = new OllamaCloudBackend({
|
||||
base_url: "https://api.example.com",
|
||||
api_key_env: "TEST_CI_CLOUD_KEY",
|
||||
model_profile: "quality",
|
||||
});
|
||||
expect(backend).toBeDefined();
|
||||
delete process.env.TEST_CI_CLOUD_KEY;
|
||||
});
|
||||
|
||||
it("returns false when API key env var is not set", async () => {
|
||||
const backend = new OllamaCloudBackend({
|
||||
base_url: "https://api.example.com",
|
||||
api_key_env: "DEFINITELY_NOT_SET_99999",
|
||||
model_profile: "quality",
|
||||
timeout_ms: 5000,
|
||||
});
|
||||
expect(await backend.isAvailable()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { OllamaBaseBackend, OllamaMessage, OllamaChatResponse } from "./ollama-base.js";
|
||||
import { OllamaCloudConfig, emptyBackendResult } from "./types.js";
|
||||
import { ToolRegistry } from "./tool-registry.js";
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
const BASE_BACKOFF_MS = 1000;
|
||||
|
||||
export class OllamaCloudBackend extends OllamaBaseBackend {
|
||||
readonly name = "ollama-cloud";
|
||||
|
||||
private cloudConfig: OllamaCloudConfig;
|
||||
private apiKey: string | null;
|
||||
|
||||
constructor(config?: OllamaCloudConfig) {
|
||||
super(config);
|
||||
this.cloudConfig = config || {
|
||||
base_url: "",
|
||||
api_key_env: "OLLAMA_CLOUD_API_KEY",
|
||||
model_profile: "quality",
|
||||
timeout_ms: 60000,
|
||||
};
|
||||
this.apiKey = this.resolveApiKey();
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
if (!this.cloudConfig.base_url) return false;
|
||||
if (!this.apiKey) return false;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.cloudConfig.base_url}/v1/models`, {
|
||||
headers: this.getAuthHeaders(),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected resolveModel(): string {
|
||||
if (this.cloudConfig.model) return this.cloudConfig.model;
|
||||
return "llama3.1:70b";
|
||||
}
|
||||
|
||||
protected async callModel(
|
||||
messages: OllamaMessage[],
|
||||
model: string,
|
||||
toolRegistry: ToolRegistry
|
||||
): Promise<OllamaChatResponse> {
|
||||
if (!this.apiKey) {
|
||||
throw new Error(`API key not found. Set ${this.cloudConfig.api_key_env} environment variable.`);
|
||||
}
|
||||
|
||||
const url = `${this.cloudConfig.base_url}/v1/chat/completions`;
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
model,
|
||||
messages: messages.map((m) => {
|
||||
const msg: Record<string, unknown> = { role: m.role, content: m.content };
|
||||
if (m.name) msg.name = m.name;
|
||||
if (m.tool_calls) msg.tool_calls = m.tool_calls;
|
||||
return msg;
|
||||
}),
|
||||
tools: toolRegistry.getOpenAIToolSchema(),
|
||||
stream: false,
|
||||
};
|
||||
|
||||
return this.callWithRetry(url, body);
|
||||
}
|
||||
|
||||
private async callWithRetry(
|
||||
url: string,
|
||||
body: Record<string, unknown>,
|
||||
attempt: number = 0
|
||||
): Promise<OllamaChatResponse> {
|
||||
const timeout = this.cloudConfig.timeout_ms || 60000;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...this.getAuthHeaders(),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(timeout),
|
||||
});
|
||||
|
||||
if (response.status === 429 && attempt < MAX_RETRIES) {
|
||||
const retryAfter = response.headers.get("Retry-After");
|
||||
const delay = retryAfter
|
||||
? parseInt(retryAfter) * 1000
|
||||
: BASE_BACKOFF_MS * Math.pow(2, attempt);
|
||||
|
||||
await this.sleep(delay);
|
||||
return this.callWithRetry(url, body, attempt + 1);
|
||||
}
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new Error(`Authentication failed. Check ${this.cloudConfig.api_key_env} environment variable.`);
|
||||
}
|
||||
|
||||
if (response.status === 402) {
|
||||
throw new Error("Quota exceeded. Check your Ollama Cloud billing status.");
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "unknown error");
|
||||
throw new Error(`Ollama Cloud API error (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as OllamaChatResponse;
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError && err.message.includes("fetch")) {
|
||||
if (attempt < MAX_RETRIES) {
|
||||
await this.sleep(BASE_BACKOFF_MS * Math.pow(2, attempt));
|
||||
return this.callWithRetry(url, body, attempt + 1);
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (this.apiKey) {
|
||||
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
private resolveApiKey(): string | null {
|
||||
return process.env[this.cloudConfig.api_key_env] || null;
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { OllamaBaseBackend, OllamaMessage, OllamaChatResponse } from "./ollama-base.js";
|
||||
import { OllamaLocalConfig } from "./types.js";
|
||||
import { ToolRegistry } from "./tool-registry.js";
|
||||
|
||||
export class OllamaLocalBackend extends OllamaBaseBackend {
|
||||
readonly name = "ollama-local";
|
||||
|
||||
private localConfig: OllamaLocalConfig;
|
||||
|
||||
constructor(config?: OllamaLocalConfig) {
|
||||
super(config);
|
||||
this.localConfig = config || { base_url: "http://localhost:11434", model_profile: "balanced" };
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.localConfig.base_url}/api/tags`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected resolveModel(): string {
|
||||
if (this.localConfig.model) return this.localConfig.model;
|
||||
return this.modelProfileToModel(this.localConfig.model_profile, []);
|
||||
}
|
||||
|
||||
protected async callModel(
|
||||
messages: OllamaMessage[],
|
||||
model: string,
|
||||
toolRegistry: ToolRegistry
|
||||
): Promise<OllamaChatResponse> {
|
||||
let resolvedModel = model;
|
||||
if (!this.localConfig.model) {
|
||||
const models = await this.fetchAvailableModels();
|
||||
resolvedModel = this.modelProfileToModel(this.localConfig.model_profile, models);
|
||||
}
|
||||
const url = `${this.localConfig.base_url}/v1/chat/completions`;
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
model: resolvedModel,
|
||||
messages: messages.map((m) => {
|
||||
const msg: Record<string, unknown> = { role: m.role, content: m.content };
|
||||
if (m.name) msg.name = m.name;
|
||||
if (m.tool_calls) msg.tool_calls = m.tool_calls;
|
||||
return msg;
|
||||
}),
|
||||
tools: toolRegistry.getOpenAIToolSchema(),
|
||||
stream: false,
|
||||
};
|
||||
|
||||
const timeout = this.localConfig.timeout_ms || 10000;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(timeout),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "unknown error");
|
||||
throw new Error(`Ollama local API error (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as OllamaChatResponse;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { execSync, spawn } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import {
|
||||
IntelligenceBackend,
|
||||
BackendRequest,
|
||||
BackendResult,
|
||||
BackendType,
|
||||
OpencodeBackendConfig,
|
||||
emptyTokenUsage,
|
||||
emptyBackendResult,
|
||||
} from "./types.js";
|
||||
|
||||
export class OpencodeBackend implements IntelligenceBackend {
|
||||
readonly name = "opencode";
|
||||
readonly type: BackendType = "agent";
|
||||
|
||||
private config: OpencodeBackendConfig;
|
||||
|
||||
constructor(config?: OpencodeBackendConfig) {
|
||||
this.config = config || { enabled: true };
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
const executable = this.config.executable || "opencode";
|
||||
try {
|
||||
const result = execSync(`${executable} --version`, {
|
||||
encoding: "utf-8",
|
||||
timeout: 5000,
|
||||
stdio: "pipe",
|
||||
});
|
||||
return !!result;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async execute(request: BackendRequest): Promise<BackendResult> {
|
||||
const executable = this.config.executable || "opencode";
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const serializedRequest = this.serializeRequest(request);
|
||||
const tempFile = path.join(
|
||||
os.tmpdir(),
|
||||
`ci-request-${request.persona}-${Date.now()}.json`
|
||||
);
|
||||
|
||||
fs.writeFileSync(tempFile, serializedRequest, "utf-8");
|
||||
|
||||
const command = `${executable} --non-interactive "/ci-${request.workflow} ${request.task}"`;
|
||||
const contextEnv = {
|
||||
...process.env,
|
||||
CI_BACKEND_REQUEST: tempFile,
|
||||
CI_PROJECT_PATH: request.context.project_path,
|
||||
CI_PHASE: String(request.context.phase),
|
||||
CI_STAGE: request.context.stage,
|
||||
CI_AUTONOMY: request.autonomy,
|
||||
};
|
||||
|
||||
const result = execSync(command, {
|
||||
cwd: request.context.project_path,
|
||||
encoding: "utf-8",
|
||||
timeout: 600000,
|
||||
env: contextEnv,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
try {
|
||||
fs.unlinkSync(tempFile);
|
||||
} catch {}
|
||||
|
||||
return this.parseResult(result, Date.now() - startTime);
|
||||
} catch (err) {
|
||||
const execErr = err as { stderr?: string; status?: number };
|
||||
|
||||
try {
|
||||
const tempFile = path.join(
|
||||
os.tmpdir(),
|
||||
`ci-request-${request.persona}-${startTime}.json`
|
||||
);
|
||||
if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile);
|
||||
} catch {}
|
||||
|
||||
if (execErr.stderr) {
|
||||
return emptyBackendResult(
|
||||
`opencode execution failed (exit ${execErr.status || "unknown"}): ${execErr.stderr}`
|
||||
);
|
||||
}
|
||||
|
||||
return emptyBackendResult(
|
||||
`opencode backend error: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private serializeRequest(request: BackendRequest): string {
|
||||
return JSON.stringify({
|
||||
persona: request.persona,
|
||||
workflow: request.workflow,
|
||||
task: request.task,
|
||||
context: {
|
||||
project_path: request.context.project_path,
|
||||
phase: request.context.phase,
|
||||
stage: request.context.stage,
|
||||
specification: request.context.specification,
|
||||
config_path: request.context.config_path,
|
||||
},
|
||||
autonomy: request.autonomy,
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
private parseResult(output: string, durationMs: number): BackendResult {
|
||||
const jsonMatch = output.match(/\{[\s\S]*"success"[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
return {
|
||||
success: parsed.success ?? true,
|
||||
output: parsed.output || output,
|
||||
artifacts: Array.isArray(parsed.artifacts)
|
||||
? parsed.artifacts.filter((a: unknown) => !!a).map((a: Record<string, unknown>) => ({
|
||||
path: String(a.path || ""),
|
||||
content: String(a.content || ""),
|
||||
operation: (a.operation as "create" | "update" | "delete") || "create",
|
||||
}))
|
||||
: [],
|
||||
decisions: Array.isArray(parsed.decisions)
|
||||
? parsed.decisions.filter((d: unknown) => !!d).map((d: Record<string, unknown>) => ({
|
||||
id: String(d.id || "D-000"),
|
||||
decision: String(d.decision || ""),
|
||||
rationale: String(d.rationale || ""),
|
||||
confidence: Number(d.confidence || 0.5),
|
||||
category: (d.category as "implementation_approach" | "technology_choice" | "architecture" | "scope" | "verification" | "security" | "deployment" | "general") || "general",
|
||||
alternatives_considered: Array.isArray(d.alternatives_considered)
|
||||
? d.alternatives_considered.map((a: unknown) =>
|
||||
typeof a === "string"
|
||||
? { option: a, rejected_reason: "" }
|
||||
: (a as { option: string; rejected_reason: string })
|
||||
)
|
||||
: [],
|
||||
human_override: d.human_override ? String(d.human_override) : null,
|
||||
timestamp: String(d.timestamp || new Date().toISOString()),
|
||||
}))
|
||||
: [],
|
||||
escalations: Array.isArray(parsed.escalations)
|
||||
? parsed.escalations.filter((e: unknown) => !!e).map((e: Record<string, unknown>) => ({
|
||||
id: String(e.id || "E-000"),
|
||||
timestamp: String(e.timestamp || new Date().toISOString()),
|
||||
type: (e.type as "irreversible_action" | "verification_failure" | "low_confidence_decision" | "security_escalation" | "specification_ambiguity") || "specification_ambiguity",
|
||||
phase: String(e.phase || ""),
|
||||
description: String(e.description || ""),
|
||||
context: String(e.context || ""),
|
||||
options: Array.isArray(e.options) ? e.options : [],
|
||||
default_option_id: String(e.default_option_id || ""),
|
||||
resolution: (e.resolution as "approved" | "rejected" | "modified" | "pending" | "timeout_auto_proceed") || "pending",
|
||||
audit_file: String(e.audit_file || ""),
|
||||
}))
|
||||
: [],
|
||||
usage: parsed.usage || {
|
||||
...emptyTokenUsage(),
|
||||
total_tokens: Math.ceil(output.length / 4),
|
||||
},
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
artifacts: [],
|
||||
decisions: [],
|
||||
escalations: [],
|
||||
usage: {
|
||||
...emptyTokenUsage(),
|
||||
total_tokens: Math.ceil(output.length / 4),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { ToolRegistry, TOOL_DEFINITIONS } from "../backends/tool-registry.js";
|
||||
|
||||
describe("ToolRegistry Extended", () => {
|
||||
let tempDir: string;
|
||||
let registry: ToolRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-tool-registry-ext-"));
|
||||
registry = new ToolRegistry(tempDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("readFile edge cases", () => {
|
||||
it("reads empty file", () => {
|
||||
const filePath = path.join(tempDir, "empty.txt");
|
||||
fs.writeFileSync(filePath, "");
|
||||
const result = registry.execute({ name: "readFile", arguments: { path: filePath } });
|
||||
expect(result.content).toBe("");
|
||||
expect(result.isError).toBeFalsy();
|
||||
});
|
||||
|
||||
it("reads file with unicode content", () => {
|
||||
const filePath = path.join(tempDir, "unicode.txt");
|
||||
fs.writeFileSync(filePath, "héllo wörld 🌍");
|
||||
const result = registry.execute({ name: "readFile", arguments: { path: filePath } });
|
||||
expect(result.content).toBe("héllo wörld 🌍");
|
||||
});
|
||||
|
||||
it("handles unreadable file gracefully", () => {
|
||||
if (process.getuid?.() === 0) return;
|
||||
const filePath = path.join(tempDir, "unreadable.txt");
|
||||
fs.writeFileSync(filePath, "data");
|
||||
fs.chmodSync(filePath, 0o000);
|
||||
const result = registry.execute({ name: "readFile", arguments: { path: filePath } });
|
||||
expect(result.isError).toBe(true);
|
||||
fs.chmodSync(filePath, 0o644);
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeFile edge cases", () => {
|
||||
it("overwrites existing file", () => {
|
||||
const filePath = path.join(tempDir, "overwrite.txt");
|
||||
fs.writeFileSync(filePath, "old");
|
||||
const result = registry.execute({ name: "writeFile", arguments: { path: filePath, content: "new" } });
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(fs.readFileSync(filePath, "utf-8")).toBe("new");
|
||||
});
|
||||
|
||||
it("creates nested directories", () => {
|
||||
const filePath = path.join(tempDir, "a", "b", "c", "deep.txt");
|
||||
const result = registry.execute({ name: "writeFile", arguments: { path: filePath, content: "deep" } });
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(fs.readFileSync(filePath, "utf-8")).toBe("deep");
|
||||
});
|
||||
});
|
||||
|
||||
describe("editFile edge cases", () => {
|
||||
it("replaces only first occurrence", () => {
|
||||
const filePath = path.join(tempDir, "multi.txt");
|
||||
fs.writeFileSync(filePath, "aaa bbb aaa");
|
||||
const result = registry.execute({ name: "editFile", arguments: { path: filePath, old: "aaa", new: "zzz" } });
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(fs.readFileSync(filePath, "utf-8")).toBe("zzz bbb aaa");
|
||||
});
|
||||
|
||||
it("handles empty old string", () => {
|
||||
const filePath = path.join(tempDir, "empty-old.txt");
|
||||
fs.writeFileSync(filePath, "hello");
|
||||
const result = registry.execute({ name: "editFile", arguments: { path: filePath, old: "", new: "X" } });
|
||||
expect(fs.readFileSync(filePath, "utf-8")).toContain("X");
|
||||
});
|
||||
});
|
||||
|
||||
describe("runBash edge cases", () => {
|
||||
it("respects cwd argument", () => {
|
||||
const subDir = path.join(tempDir, "subdir");
|
||||
fs.mkdirSync(subDir);
|
||||
const result = registry.execute({ name: "runBash", arguments: { command: "pwd", cwd: subDir } });
|
||||
expect(result.content).toContain("subdir");
|
||||
expect(result.isError).toBeFalsy();
|
||||
});
|
||||
|
||||
it("respects timeout argument", () => {
|
||||
const result = registry.execute({ name: "runBash", arguments: { command: "sleep 100", timeout: 500 } });
|
||||
expect(result.isError).toBe(true);
|
||||
});
|
||||
|
||||
it("captures stderr in error output", () => {
|
||||
const result = registry.execute({ name: "runBash", arguments: { command: "echo error >&2 && exit 1" } });
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content).toContain("error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("glob edge cases", () => {
|
||||
it("finds files in subdirectories", () => {
|
||||
const subDir = path.join(tempDir, "src");
|
||||
fs.mkdirSync(subDir);
|
||||
fs.writeFileSync(path.join(subDir, "app.ts"), "");
|
||||
fs.writeFileSync(path.join(subDir, "util.ts"), "");
|
||||
const result = registry.execute({ name: "glob", arguments: { pattern: "**/*.ts" } });
|
||||
const matches = JSON.parse(result.content);
|
||||
expect(matches.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("returns empty array for no matches", () => {
|
||||
const result = registry.execute({ name: "glob", arguments: { pattern: "*.xyz" } });
|
||||
const matches = JSON.parse(result.content);
|
||||
expect(matches).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("grep edge cases", () => {
|
||||
it("supports include pattern filter", () => {
|
||||
fs.writeFileSync(path.join(tempDir, "app.ts"), "const x = 1;\n");
|
||||
fs.writeFileSync(path.join(tempDir, "app.js"), "const x = 1;\n");
|
||||
const result = registry.execute({ name: "grep", arguments: { pattern: "const", include: "*.ts" } });
|
||||
const matches = JSON.parse(result.content);
|
||||
expect(matches.every((m: { file: string }) => m.file.endsWith(".ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns empty for no matches", () => {
|
||||
fs.writeFileSync(path.join(tempDir, "app.ts"), "nothing interesting\n");
|
||||
const result = registry.execute({ name: "grep", arguments: { pattern: "NONEXISTENT_PATTERN_XYZ", include: "*.ts" } });
|
||||
const matches = JSON.parse(result.content);
|
||||
expect(matches).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { ToolRegistry, TOOL_DEFINITIONS } from "../backends/tool-registry.js";
|
||||
|
||||
describe("ToolRegistry", () => {
|
||||
let tempDir: string;
|
||||
let registry: ToolRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-tool-registry-test-"));
|
||||
registry = new ToolRegistry(tempDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("definitions", () => {
|
||||
it("provides 6 tool definitions", () => {
|
||||
expect(TOOL_DEFINITIONS).toHaveLength(6);
|
||||
const names = TOOL_DEFINITIONS.map((d) => d.name);
|
||||
expect(names).toContain("readFile");
|
||||
expect(names).toContain("writeFile");
|
||||
expect(names).toContain("editFile");
|
||||
expect(names).toContain("runBash");
|
||||
expect(names).toContain("glob");
|
||||
expect(names).toContain("grep");
|
||||
});
|
||||
|
||||
it("getOpenAIToolSchema returns function-type schema", () => {
|
||||
const schema = registry.getOpenAIToolSchema();
|
||||
expect(schema.length).toBe(6);
|
||||
expect(schema[0].type).toBe("function");
|
||||
expect((schema[0].function as Record<string, unknown>).name).toBeDefined();
|
||||
expect((schema[0].function as Record<string, unknown>).parameters).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("readFile", () => {
|
||||
it("reads an existing file", () => {
|
||||
const filePath = path.join(tempDir, "test.txt");
|
||||
fs.writeFileSync(filePath, "hello world");
|
||||
const result = registry.execute({ name: "readFile", arguments: { path: filePath } });
|
||||
expect(result.name).toBe("readFile");
|
||||
expect(result.content).toBe("hello world");
|
||||
expect(result.isError).toBeFalsy();
|
||||
});
|
||||
|
||||
it("returns error for missing file", () => {
|
||||
const result = registry.execute({ name: "readFile", arguments: { path: "/nonexistent/file.txt" } });
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content).toContain("not found");
|
||||
});
|
||||
|
||||
it("returns error for files exceeding max size", () => {
|
||||
const bigRegistry = new ToolRegistry(tempDir, 10);
|
||||
const filePath = path.join(tempDir, "big.txt");
|
||||
fs.writeFileSync(filePath, "x".repeat(100));
|
||||
const result = bigRegistry.execute({ name: "readFile", arguments: { path: filePath } });
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content).toContain("too large");
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeFile", () => {
|
||||
it("writes a file creating parent directories", () => {
|
||||
const filePath = path.join(tempDir, "sub", "dir", "test.txt");
|
||||
const result = registry.execute({ name: "writeFile", arguments: { path: filePath, content: "written" } });
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(fs.readFileSync(filePath, "utf-8")).toBe("written");
|
||||
});
|
||||
});
|
||||
|
||||
describe("editFile", () => {
|
||||
it("replaces an exact string in a file", () => {
|
||||
const filePath = path.join(tempDir, "edit.txt");
|
||||
fs.writeFileSync(filePath, "hello world");
|
||||
const result = registry.execute({ name: "editFile", arguments: { path: filePath, old: "hello", new: "goodbye" } });
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(fs.readFileSync(filePath, "utf-8")).toBe("goodbye world");
|
||||
});
|
||||
|
||||
it("returns error when old string not found", () => {
|
||||
const filePath = path.join(tempDir, "edit.txt");
|
||||
fs.writeFileSync(filePath, "hello world");
|
||||
const result = registry.execute({ name: "editFile", arguments: { path: filePath, old: "missing", new: "replacement" } });
|
||||
expect(result.isError).toBe(true);
|
||||
});
|
||||
|
||||
it("returns error for missing file", () => {
|
||||
const result = registry.execute({ name: "editFile", arguments: { path: "/nonexistent", old: "a", new: "b" } });
|
||||
expect(result.isError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runBash", () => {
|
||||
it("executes a command and returns stdout", () => {
|
||||
const result = registry.execute({ name: "runBash", arguments: { command: "echo hello" } });
|
||||
expect(result.content).toContain("hello");
|
||||
expect(result.isError).toBeFalsy();
|
||||
});
|
||||
|
||||
it("returns error with stderr for failing commands", () => {
|
||||
const result = registry.execute({ name: "runBash", arguments: { command: "false" } });
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content).toContain("Exit code");
|
||||
});
|
||||
});
|
||||
|
||||
describe("glob", () => {
|
||||
it("finds files matching a pattern", () => {
|
||||
fs.writeFileSync(path.join(tempDir, "app.ts"), "");
|
||||
fs.writeFileSync(path.join(tempDir, "app.test.ts"), "");
|
||||
fs.writeFileSync(path.join(tempDir, "README.md"), "");
|
||||
const result = registry.execute({ name: "glob", arguments: { pattern: "*.ts" } });
|
||||
const matches = JSON.parse(result.content);
|
||||
expect(matches.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("grep", () => {
|
||||
it("finds matching lines", () => {
|
||||
fs.writeFileSync(path.join(tempDir, "app.ts"), "export function main() {}\nconst x = 1;\n");
|
||||
const result = registry.execute({ name: "grep", arguments: { pattern: "export", include: "*.ts" } });
|
||||
const matches = JSON.parse(result.content);
|
||||
expect(matches.length).toBe(1);
|
||||
expect(matches[0].content).toContain("export");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unknown tool", () => {
|
||||
it("returns error for unknown tool name", () => {
|
||||
const result = registry.execute({ name: "unknownTool", arguments: {} });
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content).toContain("Unknown tool");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: {
|
||||
type: "object";
|
||||
properties: Record<string, { type: string; description: string }>;
|
||||
required: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ToolResult {
|
||||
name: string;
|
||||
content: string;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
{
|
||||
name: "readFile",
|
||||
description: "Read the contents of a file at the given path",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Absolute file path to read" },
|
||||
},
|
||||
required: ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writeFile",
|
||||
description: "Write content to a file, creating it if it doesn't exist",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Absolute file path to write" },
|
||||
content: { type: "string", description: "Content to write to the file" },
|
||||
},
|
||||
required: ["path", "content"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "editFile",
|
||||
description: "Replace an exact string in a file with a new string",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Absolute file path to edit" },
|
||||
old: { type: "string", description: "Exact string to find in the file" },
|
||||
new: { type: "string", description: "String to replace it with" },
|
||||
},
|
||||
required: ["path", "old", "new"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "runBash",
|
||||
description: "Execute a bash command and return stdout/stderr",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
command: { type: "string", description: "Bash command to execute" },
|
||||
cwd: { type: "string", description: "Working directory for the command" },
|
||||
timeout: { type: "number", description: "Timeout in milliseconds (default 30000)" },
|
||||
},
|
||||
required: ["command"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "glob",
|
||||
description: "Find files matching a glob pattern recursively",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
pattern: { type: "string", description: "Glob pattern (e.g. **/*.ts)" },
|
||||
cwd: { type: "string", description: "Directory to search in" },
|
||||
},
|
||||
required: ["pattern"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "grep",
|
||||
description: "Search file contents using a regular expression",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
pattern: { type: "string", description: "Regex pattern to search for" },
|
||||
include: { type: "string", description: "File pattern to include (e.g. *.ts)" },
|
||||
cwd: { type: "string", description: "Directory to search in" },
|
||||
},
|
||||
required: ["pattern"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export class ToolRegistry {
|
||||
private projectPath: string;
|
||||
private maxFileSize: number;
|
||||
|
||||
constructor(projectPath: string, maxFileSize: number = 1024 * 1024) {
|
||||
this.projectPath = projectPath;
|
||||
this.maxFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
execute(call: ToolCall): ToolResult {
|
||||
try {
|
||||
switch (call.name) {
|
||||
case "readFile":
|
||||
return this.readFile(call.arguments);
|
||||
case "writeFile":
|
||||
return this.writeFile(call.arguments);
|
||||
case "editFile":
|
||||
return this.editFile(call.arguments);
|
||||
case "runBash":
|
||||
return this.runBash(call.arguments);
|
||||
case "glob":
|
||||
return this.glob(call.arguments);
|
||||
case "grep":
|
||||
return this.grep(call.arguments);
|
||||
default:
|
||||
return { name: call.name, content: `Unknown tool: ${call.name}`, isError: true };
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
name: call.name,
|
||||
content: `Tool error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getDefinitions(): ToolDefinition[] {
|
||||
return TOOL_DEFINITIONS;
|
||||
}
|
||||
|
||||
getOpenAIToolSchema(): Array<Record<string, unknown>> {
|
||||
return TOOL_DEFINITIONS.map((def) => ({
|
||||
type: "function",
|
||||
function: {
|
||||
name: def.name,
|
||||
description: def.description,
|
||||
parameters: def.parameters,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private readFile(args: Record<string, unknown>): ToolResult {
|
||||
const filePath = String(args.path);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { name: "readFile", content: `File not found: ${filePath}`, isError: true };
|
||||
}
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.size > this.maxFileSize) {
|
||||
return { name: "readFile", content: `File too large: ${filePath} (${stat.size} bytes)`, isError: true };
|
||||
}
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
return { name: "readFile", content };
|
||||
} catch (err) {
|
||||
return { name: "readFile", content: `Read error: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
||||
}
|
||||
}
|
||||
|
||||
private writeFile(args: Record<string, unknown>): ToolResult {
|
||||
const filePath = String(args.path);
|
||||
const content = String(args.content);
|
||||
try {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(filePath, content, "utf-8");
|
||||
return { name: "writeFile", content: `Written: ${filePath}` };
|
||||
} catch (err) {
|
||||
return { name: "writeFile", content: `Write error: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
||||
}
|
||||
}
|
||||
|
||||
private editFile(args: Record<string, unknown>): ToolResult {
|
||||
const filePath = String(args.path);
|
||||
const oldStr = String(args.old);
|
||||
const newStr = String(args.new);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { name: "editFile", content: `File not found: ${filePath}`, isError: true };
|
||||
}
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
if (!content.includes(oldStr)) {
|
||||
return { name: "editFile", content: `String not found in ${filePath}`, isError: true };
|
||||
}
|
||||
const updated = content.replace(oldStr, newStr);
|
||||
fs.writeFileSync(filePath, updated, "utf-8");
|
||||
return { name: "editFile", content: `Edited: ${filePath}` };
|
||||
} catch (err) {
|
||||
return { name: "editFile", content: `Edit error: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
||||
}
|
||||
}
|
||||
|
||||
private runBash(args: Record<string, unknown>): ToolResult {
|
||||
const command = String(args.command);
|
||||
const cwd = args.cwd ? String(args.cwd) : this.projectPath;
|
||||
const timeout = args.timeout ? Number(args.timeout) : 30000;
|
||||
try {
|
||||
const stdout = execSync(command, {
|
||||
cwd,
|
||||
timeout,
|
||||
encoding: "utf-8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
return { name: "runBash", content: stdout || "(no output)" };
|
||||
} catch (err: unknown) {
|
||||
const execErr = err as { stderr?: string; stdout?: string; status?: number };
|
||||
const output = [`Exit code: ${execErr.status || 1}`, `stdout: ${execErr.stdout || ""}`, `stderr: ${execErr.stderr || ""}`].join("\n");
|
||||
return { name: "runBash", content: output, isError: true };
|
||||
}
|
||||
}
|
||||
|
||||
private glob(args: Record<string, unknown>): ToolResult {
|
||||
const pattern = String(args.pattern);
|
||||
const cwd = args.cwd ? String(args.cwd) : this.projectPath;
|
||||
const matches = this.globRecursive(cwd, pattern);
|
||||
return { name: "glob", content: JSON.stringify(matches.slice(0, 200)) };
|
||||
}
|
||||
|
||||
private grep(args: Record<string, unknown>): ToolResult {
|
||||
const pattern = String(args.pattern);
|
||||
const cwd = args.cwd ? String(args.cwd) : this.projectPath;
|
||||
const include = args.include ? String(args.include) : undefined;
|
||||
const matches = this.grepRecursive(cwd, pattern, include);
|
||||
return { name: "grep", content: JSON.stringify(matches.slice(0, 100)) };
|
||||
}
|
||||
|
||||
private globRecursive(dir: string, pattern: string): string[] {
|
||||
const results: string[] = [];
|
||||
const regex = this.globToRegex(pattern);
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".git") continue;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...this.globRecursive(fullPath, pattern));
|
||||
} else if (regex.test(entry.name) || regex.test(path.relative(this.projectPath, fullPath))) {
|
||||
results.push(path.relative(this.projectPath, fullPath));
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return results.sort();
|
||||
}
|
||||
|
||||
private globToRegex(pattern: string): RegExp {
|
||||
const escaped = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
||||
.replace(/\*\*/g, "{{GLOBSTAR}}")
|
||||
.replace(/\*/g, "[^/]*")
|
||||
.replace(/{{GLOBSTAR}}/g, ".*")
|
||||
.replace(/\?/g, "[^/]");
|
||||
return new RegExp(`^${escaped}$`);
|
||||
}
|
||||
|
||||
private grepRecursive(dir: string, patternStr: string, include?: string): Array<{ file: string; line: number; content: string }> {
|
||||
const results: Array<{ file: string; line: number; content: string }> = [];
|
||||
const regex = new RegExp(patternStr);
|
||||
const includeRegex = include ? this.globToRegex(include) : null;
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".git") continue;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...this.grepRecursive(fullPath, patternStr, include));
|
||||
} else if (includeRegex ? includeRegex.test(entry.name) : true) {
|
||||
try {
|
||||
const content = fs.readFileSync(fullPath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (regex.test(lines[i])) {
|
||||
results.push({
|
||||
file: path.relative(this.projectPath, fullPath),
|
||||
line: i + 1,
|
||||
content: lines[i].trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { AgentName, AutonomyLevel, ModelProfile } from "../types/config.js";
|
||||
import { AgentContext } from "../agents/base.js";
|
||||
import { Decision } from "../types/decisions.js";
|
||||
import { Escalation } from "../types/escalation.js";
|
||||
|
||||
export type BackendType = "llm" | "agent";
|
||||
|
||||
export interface BackendRequest {
|
||||
persona: AgentName;
|
||||
workflow: string;
|
||||
task: string;
|
||||
context: AgentContext;
|
||||
autonomy: AutonomyLevel;
|
||||
}
|
||||
|
||||
export interface Artifact {
|
||||
path: string;
|
||||
content: string;
|
||||
operation: "create" | "update" | "delete";
|
||||
}
|
||||
|
||||
export interface TokenUsage {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
total_tokens: number;
|
||||
estimated_cost_usd: number;
|
||||
}
|
||||
|
||||
export interface BackendResult {
|
||||
success: boolean;
|
||||
output: string;
|
||||
artifacts: Artifact[];
|
||||
decisions: Decision[];
|
||||
escalations: Escalation[];
|
||||
usage: TokenUsage;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IntelligenceBackend {
|
||||
readonly name: string;
|
||||
readonly type: BackendType;
|
||||
isAvailable(): Promise<boolean>;
|
||||
execute(request: BackendRequest): Promise<BackendResult>;
|
||||
}
|
||||
|
||||
export interface LLMBackendConfig {
|
||||
base_url: string;
|
||||
model_profile: ModelProfile;
|
||||
model?: string;
|
||||
timeout_ms?: number;
|
||||
}
|
||||
|
||||
export interface OllamaLocalConfig extends LLMBackendConfig {
|
||||
base_url: string;
|
||||
model_profile: ModelProfile;
|
||||
model?: string;
|
||||
timeout_ms?: number;
|
||||
}
|
||||
|
||||
export interface OllamaCloudConfig extends LLMBackendConfig {
|
||||
base_url: string;
|
||||
api_key_env: string;
|
||||
model_profile: ModelProfile;
|
||||
model?: string;
|
||||
timeout_ms?: number;
|
||||
}
|
||||
|
||||
export interface OpencodeBackendConfig {
|
||||
enabled: boolean;
|
||||
executable?: string;
|
||||
}
|
||||
|
||||
export interface BackendConfigSection {
|
||||
provider: "auto" | "opencode" | "ollama-local" | "ollama-cloud";
|
||||
fallback?: "opencode" | "ollama-local" | "ollama-cloud";
|
||||
agent_backends: {
|
||||
opencode?: OpencodeBackendConfig;
|
||||
};
|
||||
llm_backends: {
|
||||
"ollama-local"?: OllamaLocalConfig;
|
||||
"ollama-cloud"?: OllamaCloudConfig;
|
||||
};
|
||||
}
|
||||
|
||||
export const DEFAULT_BACKEND_CONFIG: BackendConfigSection = {
|
||||
provider: "auto",
|
||||
agent_backends: {
|
||||
opencode: { enabled: true },
|
||||
},
|
||||
llm_backends: {
|
||||
"ollama-local": {
|
||||
base_url: "http://localhost:11434",
|
||||
model_profile: "balanced",
|
||||
},
|
||||
"ollama-cloud": {
|
||||
base_url: "",
|
||||
api_key_env: "OLLAMA_CLOUD_API_KEY",
|
||||
model_profile: "quality",
|
||||
timeout_ms: 60000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export class BackendUnavailableError extends Error {
|
||||
readonly backendName: string;
|
||||
readonly agentName?: string;
|
||||
|
||||
constructor(backendName: string, agentName?: string) {
|
||||
const agentMsg = agentName ? ` (agent: ${agentName})` : "";
|
||||
super(
|
||||
`Intelligence backend "${backendName}" is not available${agentMsg}. ` +
|
||||
`Configure one of:\n` +
|
||||
` 1. Install opencode: npm i -g opencode\n` +
|
||||
` 2. Run Ollama locally: ollama serve\n` +
|
||||
` 3. Set OLLAMA_CLOUD_API_KEY for remote inference`
|
||||
);
|
||||
this.name = "BackendUnavailableError";
|
||||
this.backendName = backendName;
|
||||
this.agentName = agentName;
|
||||
}
|
||||
}
|
||||
|
||||
export function emptyTokenUsage(): TokenUsage {
|
||||
return { input_tokens: 0, output_tokens: 0, total_tokens: 0, estimated_cost_usd: 0 };
|
||||
}
|
||||
|
||||
export function emptyBackendResult(error?: string): BackendResult {
|
||||
return {
|
||||
success: false,
|
||||
output: "",
|
||||
artifacts: [],
|
||||
decisions: [],
|
||||
escalations: [],
|
||||
usage: emptyTokenUsage(),
|
||||
error,
|
||||
};
|
||||
}
|
||||
+387
-62
@@ -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";
|
||||
@@ -12,12 +12,16 @@ import { loadSpecification as loadSpec } from "../core/clarify.js";
|
||||
import { AgentContext } from "../agents/base.js";
|
||||
import { ErrorRecovery } from "../core/error-recovery.js";
|
||||
import { PipelineState, createInitialPipelineState } from "../types/pipeline.js";
|
||||
import { resolveBackend } from "../backends/index.js";
|
||||
import { BackendUnavailableError } from "../backends/types.js";
|
||||
import { getAgent } from "../agents/index.js";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
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)
|
||||
@@ -28,12 +32,13 @@ export function createInitCommand(): Command {
|
||||
)
|
||||
.option("--model-profile <profile>", "Model profile: quality, speed, balanced", "quality")
|
||||
.option("--no-parallel", "Disable parallel agent execution")
|
||||
.option("--backend <provider>", "Intelligence backend: auto, opencode, ollama-local, ollama-cloud", "auto")
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -55,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"],
|
||||
@@ -71,10 +76,19 @@ export function createInitCommand(): Command {
|
||||
max_concurrent_agents: 5,
|
||||
min_plans_for_parallel: 2,
|
||||
},
|
||||
backend: {
|
||||
provider: options.backend || "auto",
|
||||
agent_backends: { opencode: { enabled: true } },
|
||||
llm_backends: {
|
||||
"ollama-local": { base_url: "http://localhost:11434", model_profile: "balanced" },
|
||||
"ollama-cloud": { base_url: "", api_key_env: "OLLAMA_CLOUD_API_KEY", model_profile: "quality", timeout_ms: 60000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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) {
|
||||
const spec: Specification = parseSpecification(specText, options.spec ? "file" : "inline");
|
||||
@@ -101,36 +115,80 @@ 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: 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;
|
||||
}
|
||||
|
||||
if (backendConfig.provider === "auto") {
|
||||
try {
|
||||
const backend = await resolveBackend(backendConfig);
|
||||
console.log(` Backend: ${backend.name} (${backend.type})`);
|
||||
return { backend };
|
||||
} catch (err) {
|
||||
if (err instanceof BackendUnavailableError) {
|
||||
return { backend: undefined, error: err.message };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { createBackend } = await import("../backends/index.js");
|
||||
const backend = createBackend(backendConfig.provider, backendConfig);
|
||||
if (await backend.isAvailable()) {
|
||||
console.log(` Backend: ${backend.name} (${backend.type})`);
|
||||
return { backend };
|
||||
}
|
||||
return { backend: undefined, error: `Configured backend "${backendConfig.provider}" is not available.` };
|
||||
} catch (err) {
|
||||
if (err instanceof BackendUnavailableError) {
|
||||
return { backend: undefined, error: err.message };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function createRunCommand(): Command {
|
||||
return new Command("run")
|
||||
.description("Execute a specific phase autonomously")
|
||||
.argument("[phase]", "Phase to run: research, plan, execute, verify, or --all")
|
||||
.option("--all", "Execute all remaining phases sequentially")
|
||||
.option("--phase <number>", "Phase number", "1")
|
||||
.option("--backend <provider>", "Override intelligence backend for this run")
|
||||
.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);
|
||||
}
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
|
||||
|
||||
if (!backend && backendError) {
|
||||
console.warn(` ⚠ No intelligence backend available: ${backendError}`);
|
||||
console.warn(" Continuing with mechanical-only execution (limited functionality).");
|
||||
}
|
||||
|
||||
const orchestrator = new OrchestratorAgent(config);
|
||||
const context: AgentContext = {
|
||||
project_path: projectPath,
|
||||
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,
|
||||
};
|
||||
|
||||
const spec = loadSpec(projectPath);
|
||||
@@ -138,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 {
|
||||
@@ -163,16 +221,25 @@ export function createQuickCommand(): Command {
|
||||
return new Command("quick")
|
||||
.description("Execute an ad-hoc task with full agentic guarantees")
|
||||
.argument("<description>", "Task description")
|
||||
.action(async (description) => {
|
||||
.option("--backend <provider>", "Override intelligence backend")
|
||||
.action(async (description, options) => {
|
||||
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✗ "ciagent quick" requires an intelligence backend.`);
|
||||
if (backendError) console.error(` ${backendError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const spec = parseSpecification(description, "inline");
|
||||
saveSpecification(projectPath, spec);
|
||||
|
||||
@@ -182,7 +249,8 @@ 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,
|
||||
};
|
||||
|
||||
const result = await orchestrator.execute(context);
|
||||
@@ -202,11 +270,21 @@ export function createDebugCommand(): Command {
|
||||
.description("Autonomous debugging: diagnose root cause, propose fix")
|
||||
.argument("[description]", "Description of the issue to debug")
|
||||
.option("--confidence <threshold>", "Minimum confidence to auto-fix", "0.6")
|
||||
.option("--backend <provider>", "Override intelligence backend")
|
||||
.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);
|
||||
}
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
|
||||
|
||||
if (!backend) {
|
||||
console.error(`\n✗ "ciagent debug" requires an intelligence backend.`);
|
||||
if (backendError) console.error(` ${backendError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -214,14 +292,26 @@ export function createDebugCommand(): Command {
|
||||
if (description) {
|
||||
console.log(` Issue: ${description}`);
|
||||
}
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
const recovery = new ErrorRecovery(config, projectPath);
|
||||
|
||||
console.log(` Confidence threshold: ${options.confidence}`);
|
||||
console.log(" Diagnosing root cause...");
|
||||
|
||||
console.log("\n✓ Debug complete — autonomous diagnosis finished");
|
||||
const debuggerAgent = getAgent("debugger");
|
||||
const context: AgentContext = {
|
||||
project_path: projectPath,
|
||||
phase: 0,
|
||||
stage: "debug",
|
||||
specification: description || "",
|
||||
config_path: path.join(projectPath, ".ciagent", "config.json"),
|
||||
backend,
|
||||
};
|
||||
|
||||
const result = await debuggerAgent.execute(context);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`\n✓ ${result.output}`);
|
||||
} else {
|
||||
console.error(`\n✗ Debug failed: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -230,11 +320,12 @@ export function createVerifyCommand(): Command {
|
||||
.description("Automated verification of a phase")
|
||||
.argument("[phase]", "Phase number to verify", "1")
|
||||
.option("--layer <layer>", "Run specific layer: structural, behavioral, security, quality", "all")
|
||||
.option("--backend <provider>", "Override intelligence backend for behavioral verification")
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -276,17 +367,45 @@ export function createReviewCommand(): Command {
|
||||
return new Command("review")
|
||||
.description("Multi-persona autonomous code review")
|
||||
.argument("[phase]", "Phase number to review", "1")
|
||||
.action(async (phase) => {
|
||||
.option("--backend <provider>", "Override intelligence backend")
|
||||
.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);
|
||||
}
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
|
||||
|
||||
if (!backend) {
|
||||
console.error(`\n✗ "ciagent review" requires an intelligence backend.`);
|
||||
if (backendError) console.error(` ${backendError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const phaseNum = parseInt(phase) || 1;
|
||||
console.log(`Running code review for phase ${phaseNum}...`);
|
||||
console.log("Review complete — findings logged to audit trail");
|
||||
|
||||
const reviewer = getAgent("code-reviewer");
|
||||
const context: AgentContext = {
|
||||
project_path: projectPath,
|
||||
phase: phaseNum,
|
||||
stage: "review",
|
||||
specification: "",
|
||||
config_path: path.join(projectPath, ".ciagent", "config.json"),
|
||||
backend,
|
||||
};
|
||||
|
||||
const result = await reviewer.execute(context);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`\n✓ ${result.output}`);
|
||||
} else {
|
||||
console.error(`\n✗ Review failed: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -296,18 +415,19 @@ 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"}`);
|
||||
console.log(`Parallelization: ${config.parallelization.enabled ? "enabled" : "disabled"}`);
|
||||
|
||||
const state = artifacts.readState();
|
||||
@@ -324,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);
|
||||
@@ -344,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"}`);
|
||||
@@ -393,11 +513,12 @@ export function createAuditCommand(): Command {
|
||||
export function createClarifyCommand(): Command {
|
||||
return new Command("clarify")
|
||||
.description("Re-run the Clarify phase if new ambiguities have emerged")
|
||||
.action(() => {
|
||||
.option("--backend <provider>", "Use intelligence backend for question generation")
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -405,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);
|
||||
}
|
||||
|
||||
@@ -432,25 +553,91 @@ export function createRollbackCommand(): Command {
|
||||
.description("Autonomous undo with automatic dependency resolution")
|
||||
.argument("<target>", "Phase number or plan ID to rollback to")
|
||||
.option("--force", "Force rollback even with downstream dependencies")
|
||||
.option("--backend <provider>", "Use intelligence backend for dependency resolution")
|
||||
.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);
|
||||
}
|
||||
|
||||
console.log(`Rolling back to: ${target}`);
|
||||
const phaseNum = parseInt(target) || 0;
|
||||
console.log(`Rolling back to phase ${phaseNum}...`);
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
const recovery = new ErrorRecovery(config, projectPath);
|
||||
const result = await recovery.rollback(parseInt(target) || 0, "User-requested rollback");
|
||||
try {
|
||||
const branchName = `phase/${String(phaseNum).padStart(2, "0")}-*`;
|
||||
const branches = execSync("git branch --list", {
|
||||
cwd: projectPath,
|
||||
encoding: "utf-8",
|
||||
}).split("\n").map((b) => b.trim()).filter(Boolean);
|
||||
|
||||
if (result.recovered) {
|
||||
console.log(`✓ Rollback complete: ${result.message}`);
|
||||
} else {
|
||||
console.error(`✗ Rollback failed: ${result.message}`);
|
||||
process.exit(1);
|
||||
const phaseBranches = branches.filter((b) =>
|
||||
b.includes(`phase/${String(phaseNum).padStart(2, "0")}`)
|
||||
);
|
||||
|
||||
if (phaseBranches.length > 0 && !options.force) {
|
||||
console.log(`Found phase ${phaseNum} branches:`);
|
||||
for (const b of phaseBranches) {
|
||||
console.log(` ${b}`);
|
||||
}
|
||||
console.log("\nChecking for downstream dependencies...");
|
||||
|
||||
const downstreamPhases = branches.filter((b) => {
|
||||
const match = b.match(/phase\/(\d+)/);
|
||||
if (!match) return false;
|
||||
return parseInt(match[1]) > phaseNum;
|
||||
});
|
||||
|
||||
if (downstreamPhases.length > 0) {
|
||||
console.warn(`⚠ Downstream phases found:`);
|
||||
for (const b of downstreamPhases) {
|
||||
console.warn(` ${b}`);
|
||||
}
|
||||
console.warn("Use --force to rollback anyway.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const targetCommit = execSync(
|
||||
`git log --all --grep="phase: ${phaseNum}" --format="%H" -1`,
|
||||
{ cwd: projectPath, encoding: "utf-8" }
|
||||
).trim();
|
||||
|
||||
if (targetCommit) {
|
||||
console.log(` Resetting to commit: ${targetCommit.slice(0, 8)}`);
|
||||
execSync(`git reset --hard ${targetCommit}`, {
|
||||
cwd: projectPath,
|
||||
stdio: "pipe",
|
||||
});
|
||||
console.log(`✓ Rollback complete: reset to phase ${phaseNum}`);
|
||||
} else {
|
||||
console.warn(` Could not find phase ${phaseNum} commit. Performing branch cleanup only.`);
|
||||
|
||||
for (const b of phaseBranches) {
|
||||
const cleanName = b.replace(/^\*?\s*/, "");
|
||||
if (cleanName) {
|
||||
try {
|
||||
execSync(`git branch -D ${cleanName}`, {
|
||||
cwd: projectPath,
|
||||
stdio: "pipe",
|
||||
});
|
||||
console.log(` Deleted branch: ${cleanName}`);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
console.log(`✓ Rollback complete: cleaned up phase ${phaseNum} branches`);
|
||||
}
|
||||
} catch (err) {
|
||||
const recovery = new ErrorRecovery(loadConfig(projectPath), projectPath);
|
||||
const result = await recovery.rollback(phaseNum, "User-requested rollback");
|
||||
|
||||
if (result.recovered) {
|
||||
console.log(`✓ Rollback complete: ${result.message}`);
|
||||
} else {
|
||||
console.error(`✗ Rollback failed: ${result.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -459,11 +646,12 @@ export function createShipCommand(): Command {
|
||||
return new Command("ship")
|
||||
.description("Auto-complete phase: verify, security, commit, tag")
|
||||
.argument("[phase]", "Phase number to ship", "1")
|
||||
.action(async (phase) => {
|
||||
.option("--backend <provider>", "Override intelligence backend")
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -491,7 +679,144 @@ export function createShipCommand(): Command {
|
||||
console.log("\n Resolve escalations before deploying.");
|
||||
}
|
||||
|
||||
console.log(" Committing and tagging...");
|
||||
const config = loadConfig(projectPath);
|
||||
|
||||
try {
|
||||
const isGitRepo = execSync("git rev-parse --is-inside-work-tree", {
|
||||
cwd: projectPath,
|
||||
encoding: "utf-8",
|
||||
}).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...");
|
||||
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 ${version.tag} -m "CIAgent: Phase ${phaseNum} shipped"`, {
|
||||
cwd: projectPath,
|
||||
stdio: "pipe",
|
||||
});
|
||||
console.log(` ✓ Tagged: ${version.tag}`);
|
||||
|
||||
if (config.git.auto_push) {
|
||||
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)}`);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
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
@@ -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())
|
||||
|
||||
@@ -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,17 +17,16 @@ describe("ArtifactManager", () => {
|
||||
});
|
||||
|
||||
describe("ensureStructure", () => {
|
||||
it("creates .planning directory structure", () => {
|
||||
it("creates .ciagent directory structure", () => {
|
||||
manager.ensureStructure();
|
||||
expect(fs.existsSync(path.join(tempDir, ".planning"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(tempDir, ".planning", "phases"))).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, ".planning"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,7 +67,7 @@ describe("ArtifactManager", () => {
|
||||
|
||||
manager.writeProject(manifest);
|
||||
|
||||
const projectPath = path.join(tempDir, ".planning", "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");
|
||||
@@ -132,7 +131,7 @@ describe("ArtifactManager", () => {
|
||||
],
|
||||
});
|
||||
|
||||
const decisionsPath = path.join(tempDir, ".planning", "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");
|
||||
|
||||
+14
-14
@@ -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 PLANNING_DIR = ".planning";
|
||||
const CI_DIR = ".ciagent";
|
||||
|
||||
export interface ProjectManifest {
|
||||
name: string;
|
||||
@@ -48,18 +48,18 @@ export class ArtifactManager {
|
||||
this.projectPath = projectPath;
|
||||
}
|
||||
|
||||
private get planningDir(): string {
|
||||
return path.join(this.projectPath, PLANNING_DIR);
|
||||
private get ciDir(): string {
|
||||
return path.join(this.projectPath, CI_DIR);
|
||||
}
|
||||
|
||||
ensureStructure(): void {
|
||||
ensureDir(this.planningDir);
|
||||
ensureDir(path.join(this.planningDir, "phases"));
|
||||
ensureDir(path.join(this.projectPath, ".ci", "audit"));
|
||||
ensureDir(this.ciDir);
|
||||
ensureDir(path.join(this.ciDir, "phases"));
|
||||
ensureDir(path.join(this.ciDir, "audit"));
|
||||
}
|
||||
|
||||
isInitialized(): boolean {
|
||||
return fs.existsSync(path.join(this.planningDir, "PROJECT.md"));
|
||||
return fs.existsSync(path.join(this.ciDir, "PROJECT.md"));
|
||||
}
|
||||
|
||||
writeProject(manifest: ProjectManifest): void {
|
||||
@@ -81,7 +81,7 @@ export class ArtifactManager {
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
writeFile(path.join(this.planningDir, "PROJECT.md"), lines.join("\n"));
|
||||
writeFile(path.join(this.ciDir, "PROJECT.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
writeDecisions(decisions: DecisionsManifest): void {
|
||||
@@ -99,11 +99,11 @@ export class ArtifactManager {
|
||||
lines.push(`- **Timestamp**: ${d.timestamp}`);
|
||||
lines.push("");
|
||||
}
|
||||
writeFile(path.join(this.planningDir, "DECISIONS.md"), lines.join("\n"));
|
||||
writeFile(path.join(this.ciDir, "DECISIONS.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
writeState(state: StateManifest): void {
|
||||
writeJSON(path.join(this.planningDir, "STATE.md.json"), state);
|
||||
writeJSON(path.join(this.ciDir, "STATE.md.json"), state);
|
||||
|
||||
const lines = [
|
||||
"# Project State",
|
||||
@@ -124,11 +124,11 @@ export class ArtifactManager {
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
writeFile(path.join(this.planningDir, "STATE.md"), lines.join("\n"));
|
||||
writeFile(path.join(this.ciDir, "STATE.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
readState(): StateManifest | null {
|
||||
const filePath = path.join(this.planningDir, "STATE.md.json");
|
||||
const filePath = path.join(this.ciDir, "STATE.md.json");
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
}
|
||||
@@ -150,7 +150,7 @@ export class ArtifactManager {
|
||||
artifactName: string,
|
||||
content: string
|
||||
): void {
|
||||
const phaseDir = path.join(this.planningDir, "phases", `phase-${phase}`);
|
||||
const phaseDir = path.join(this.ciDir, "phases", `phase-${phase}`);
|
||||
ensureDir(phaseDir);
|
||||
writeFile(path.join(phaseDir, artifactName), content);
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export class ArtifactManager {
|
||||
artifactName: string
|
||||
): string | null {
|
||||
const filePath = path.join(
|
||||
this.planningDir,
|
||||
this.ciDir,
|
||||
"phases",
|
||||
`phase-${phase}`,
|
||||
artifactName
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -25,7 +25,6 @@ describe("Audit", () => {
|
||||
confidence: 0.92,
|
||||
category: "technology_choice",
|
||||
alternatives_considered: [{ option: "MongoDB", rejected_reason: "No ACID" }],
|
||||
learnship_equivalent: "discuss-phase would ask: What database?",
|
||||
human_override: null,
|
||||
};
|
||||
|
||||
@@ -41,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
@@ -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 {
|
||||
|
||||
+363
-17
@@ -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,28 +22,374 @@ 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 CIAgentFiles(dir);
|
||||
expect(ciFiles.getProjectSlug()).toBe("");
|
||||
});
|
||||
|
||||
it("uses provided project slug", () => {
|
||||
const ciFiles = new CIAgentFiles(dir, "task-api");
|
||||
expect(ciFiles.getProjectSlug()).toBe("task-api");
|
||||
});
|
||||
|
||||
it("setProjectSlug updates slug", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.setProjectSlug("auth-svc");
|
||||
expect(ciFiles.getProjectSlug()).toBe("auth-svc");
|
||||
});
|
||||
});
|
||||
|
||||
describe("multi-project support", () => {
|
||||
it("isMultiProject returns false when not initialized", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
expect(ciFiles.isMultiProject()).toBe(false);
|
||||
});
|
||||
|
||||
it("isMultiProject returns false for single-project config", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "default", name: "Default" }],
|
||||
active_project: "default",
|
||||
}));
|
||||
expect(ciFiles.isMultiProject()).toBe(true);
|
||||
});
|
||||
|
||||
it("isMultiProject returns false for config without projects array", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
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 CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
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, ".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 CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
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, ".ciagent", "config.json"), "utf-8"));
|
||||
expect(config.projects).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("addProject creates project subdirectory", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
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, ".ciagent", "task-api"))).toBe(true);
|
||||
});
|
||||
|
||||
it("getActiveProject returns from config", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "task-api", name: "Task API", default: true }],
|
||||
active_project: "task-api",
|
||||
}));
|
||||
|
||||
expect(ciFiles.getActiveProject()).toBe("task-api");
|
||||
});
|
||||
|
||||
it("setActiveProject updates config", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [
|
||||
{ slug: "task-api", name: "Task API" },
|
||||
{ slug: "auth-svc", name: "Auth Service" },
|
||||
],
|
||||
active_project: "task-api",
|
||||
}));
|
||||
|
||||
ciFiles.setActiveProject("auth-svc");
|
||||
|
||||
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 CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
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" },
|
||||
],
|
||||
active_project: "task-api",
|
||||
}));
|
||||
|
||||
const projects = ciFiles.listProjects();
|
||||
expect(projects).toHaveLength(2);
|
||||
expect(projects[0].slug).toBe("task-api");
|
||||
expect(projects[1].slug).toBe("auth-svc");
|
||||
});
|
||||
});
|
||||
|
||||
describe("needsMigration", () => {
|
||||
it("returns false when not initialized", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
expect(ciFiles.needsMigration()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when already multi-project", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "default", name: "Default" }],
|
||||
}));
|
||||
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 CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
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 CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
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 CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
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, ".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, ".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 CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
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, ".ciagent", "config.json"), "utf-8"));
|
||||
expect(config.projects).toHaveLength(1);
|
||||
expect(config.projects[0].slug).toBe("existing");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isNfrMilestone", () => {
|
||||
it("returns true when no roadmap exists", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
expect(ciFiles.isNfrMilestone()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when phases are all NFR types", () => {
|
||||
const ciFiles = new CIAgentFiles(dir, "nfr-proj");
|
||||
ciFiles.ensureProjectDir();
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "nfr-proj", name: "NFR Project", default: true }],
|
||||
active_project: "nfr-proj",
|
||||
}));
|
||||
const roadmap: RoadmapMd = {
|
||||
overview: "NFR-only",
|
||||
phases: [
|
||||
{ number: 1, name: "test-coverage", description: "Add tests", status: "in_progress", dependsOn: [], requirements: [], successCriteria: [] },
|
||||
{ number: 2, name: "perf-tune", description: "Tune perf", status: "not_started", dependsOn: [], requirements: [], successCriteria: [] },
|
||||
],
|
||||
};
|
||||
ciFiles.writeRoadmapMd(roadmap);
|
||||
expect(ciFiles.isNfrMilestone()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when phases include feature work", () => {
|
||||
const ciFiles = new CIAgentFiles(dir, "feat-proj");
|
||||
ciFiles.ensureProjectDir();
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "feat-proj", name: "Feature Project", default: true }],
|
||||
active_project: "feat-proj",
|
||||
}));
|
||||
const roadmap: RoadmapMd = {
|
||||
overview: "mixed",
|
||||
phases: [
|
||||
{ number: 1, name: "authentication", description: "Auth feature", status: "in_progress", dependsOn: [], requirements: [], successCriteria: [] },
|
||||
{ number: 2, name: "test-coverage", description: "Add tests", status: "not_started", dependsOn: [], requirements: [], successCriteria: [] },
|
||||
],
|
||||
};
|
||||
ciFiles.writeRoadmapMd(roadmap);
|
||||
expect(ciFiles.isNfrMilestone()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
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 CIAgentFiles(dir, "my-app");
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "my-app", name: "My App", default: true }],
|
||||
active_project: "my-app",
|
||||
}));
|
||||
|
||||
const project: ProjectMd = {
|
||||
name: "My App",
|
||||
coreValue: "Build something cool",
|
||||
requirements: { validated: [], active: [], outOfScope: [] },
|
||||
constraints: [],
|
||||
context: "Test context",
|
||||
keyDecisions: [],
|
||||
};
|
||||
|
||||
ciFiles.writeProjectMd(project, "initial");
|
||||
|
||||
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 CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({}));
|
||||
|
||||
const project: ProjectMd = {
|
||||
name: "Default App",
|
||||
coreValue: "Build something",
|
||||
requirements: { validated: [], active: [], outOfScope: [] },
|
||||
constraints: [],
|
||||
context: "Test context",
|
||||
keyDecisions: [],
|
||||
};
|
||||
|
||||
ciFiles.writeProjectMd(project, "initial");
|
||||
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent", "PROJECT.md"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PROJECT.md", () => {
|
||||
const project: ProjectMd = {
|
||||
name: "Task API",
|
||||
@@ -61,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();
|
||||
@@ -72,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" };
|
||||
@@ -109,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();
|
||||
@@ -143,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();
|
||||
@@ -151,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");
|
||||
@@ -177,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();
|
||||
@@ -188,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: [
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
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";
|
||||
|
||||
const CI_DIR = ".ci";
|
||||
|
||||
export interface ProjectMd {
|
||||
name: string;
|
||||
coreValue: string;
|
||||
requirements: {
|
||||
validated: string[];
|
||||
active: string[];
|
||||
outOfScope: string[];
|
||||
};
|
||||
constraints: string[];
|
||||
context: string;
|
||||
keyDecisions: Array<{
|
||||
decision: string;
|
||||
rationale: string;
|
||||
outcome: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface RoadmapMd {
|
||||
overview: string;
|
||||
phases: Array<{
|
||||
number: number;
|
||||
name: string;
|
||||
description: string;
|
||||
status: "not_started" | "in_progress" | "complete" | "deferred";
|
||||
dependsOn: number[];
|
||||
requirements: string[];
|
||||
successCriteria: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface RequirementsMd {
|
||||
v1: Array<{
|
||||
category: string;
|
||||
items: Array<{ id: string; description: string }>;
|
||||
}>;
|
||||
v2: Array<{
|
||||
category: string;
|
||||
items: Array<{ id: string; description: string }>;
|
||||
}>;
|
||||
outOfScope: Array<{ feature: string; reason: string }>;
|
||||
traceability: Array<{
|
||||
requirement: string;
|
||||
phase: number;
|
||||
status: "pending" | "in_progress" | "complete" | "blocked";
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ArchitectureMd {
|
||||
overview: string;
|
||||
components: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
boundaries: string;
|
||||
dependsOn: string[];
|
||||
}>;
|
||||
dataFlow: string;
|
||||
buildOrder: string[];
|
||||
}
|
||||
|
||||
export class CiFiles {
|
||||
private projectPath: string;
|
||||
|
||||
constructor(projectPath: string) {
|
||||
this.projectPath = projectPath;
|
||||
}
|
||||
|
||||
private get ciDir(): string {
|
||||
return path.join(this.projectPath, CI_DIR);
|
||||
}
|
||||
|
||||
ensureCIDir(): void {
|
||||
ensureDir(this.ciDir);
|
||||
}
|
||||
|
||||
isInitialized(): boolean {
|
||||
return fileExists(path.join(this.ciDir, "config.json"));
|
||||
}
|
||||
|
||||
readProjectMd(): ProjectMd | null {
|
||||
const content = readFile(path.join(this.ciDir, "PROJECT.md"));
|
||||
if (!content) return null;
|
||||
return this.parseProjectMd(content);
|
||||
}
|
||||
|
||||
writeProjectMd(project: ProjectMd, reason: string): void {
|
||||
this.ensureCIDir();
|
||||
const lines: string[] = [
|
||||
`# ${project.name}`,
|
||||
"",
|
||||
"## What This Is",
|
||||
"",
|
||||
project.coreValue,
|
||||
"",
|
||||
"## Requirements",
|
||||
"",
|
||||
"### Validated",
|
||||
"",
|
||||
...project.requirements.validated.map((r) => `- ✓ ${r}`),
|
||||
"",
|
||||
"### Active",
|
||||
"",
|
||||
...project.requirements.active.map((r) => `- [ ] ${r}`),
|
||||
"",
|
||||
"### Out of Scope",
|
||||
"",
|
||||
...project.requirements.outOfScope.map((r) => `- ${r}`),
|
||||
"",
|
||||
"## Context",
|
||||
"",
|
||||
project.context,
|
||||
"",
|
||||
"## Constraints",
|
||||
"",
|
||||
...project.constraints.map((c) => `- ${c}`),
|
||||
"",
|
||||
"## Key Decisions",
|
||||
"",
|
||||
"| Decision | Rationale | Outcome |",
|
||||
"|----------|-----------|---------|",
|
||||
...project.keyDecisions.map(
|
||||
(d) => `| ${d.decision} | ${d.rationale} | ${d.outcome} |`
|
||||
),
|
||||
"",
|
||||
];
|
||||
|
||||
writeFile(path.join(this.ciDir, "PROJECT.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
readRoadmapMd(): RoadmapMd | null {
|
||||
const content = readFile(path.join(this.ciDir, "ROADMAP.md"));
|
||||
if (!content) return null;
|
||||
return this.parseRoadmapMd(content);
|
||||
}
|
||||
|
||||
writeRoadmapMd(roadmap: RoadmapMd): void {
|
||||
this.ensureCIDir();
|
||||
const lines: string[] = [
|
||||
"# Roadmap",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
roadmap.overview,
|
||||
"",
|
||||
"## Phases",
|
||||
"",
|
||||
...roadmap.phases.map(
|
||||
(p) => `- [${p.status === "complete" ? "x" : " "}] **Phase ${p.number}: ${p.name}** - ${p.description}`
|
||||
),
|
||||
"",
|
||||
"## Phase Details",
|
||||
"",
|
||||
];
|
||||
|
||||
for (const phase of roadmap.phases) {
|
||||
lines.push(`### Phase ${phase.number}: ${phase.name}`);
|
||||
lines.push(`**Goal**: ${phase.description}`);
|
||||
lines.push(`**Depends on**: ${phase.dependsOn.length > 0 ? phase.dependsOn.map((d) => `Phase ${d}`).join(", ") : "Nothing"}`);
|
||||
lines.push(`**Requirements**: ${phase.requirements.length > 0 ? phase.requirements.join(", ") : "None"}`);
|
||||
lines.push("**Success Criteria**:");
|
||||
for (const sc of phase.successCriteria) {
|
||||
lines.push(`1. ${sc}`);
|
||||
}
|
||||
lines.push(`**Status**: ${phase.status}`);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
writeFile(path.join(this.ciDir, "ROADMAP.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
readRequirementsMd(): RequirementsMd | null {
|
||||
const content = readFile(path.join(this.ciDir, "REQUIREMENTS.md"));
|
||||
if (!content) return null;
|
||||
return this.parseRequirementsMd(content);
|
||||
}
|
||||
|
||||
writeRequirementsMd(requirements: RequirementsMd): void {
|
||||
this.ensureCIDir();
|
||||
const lines: string[] = [
|
||||
"# Requirements",
|
||||
"",
|
||||
"## v1 Requirements",
|
||||
"",
|
||||
];
|
||||
|
||||
for (const cat of requirements.v1) {
|
||||
lines.push(`### ${cat.category}`);
|
||||
lines.push("");
|
||||
for (const item of cat.items) {
|
||||
lines.push(`- [ ] **${item.id}**: ${item.description}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("## v2 Requirements");
|
||||
lines.push("");
|
||||
for (const cat of requirements.v2) {
|
||||
lines.push(`### ${cat.category}`);
|
||||
lines.push("");
|
||||
for (const item of cat.items) {
|
||||
lines.push(`- **${item.id}**: ${item.description}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("## Out of Scope");
|
||||
lines.push("");
|
||||
lines.push("| Feature | Reason |");
|
||||
lines.push("|---------|--------|");
|
||||
for (const item of requirements.outOfScope) {
|
||||
lines.push(`| ${item.feature} | ${item.reason} |`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Traceability");
|
||||
lines.push("");
|
||||
lines.push("| Requirement | Phase | Status |");
|
||||
lines.push("|-------------|-------|--------|");
|
||||
for (const t of requirements.traceability) {
|
||||
lines.push(`| ${t.requirement} | Phase ${t.phase} | ${t.status} |`);
|
||||
}
|
||||
|
||||
writeFile(path.join(this.ciDir, "REQUIREMENTS.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
readArchitectureMd(): ArchitectureMd | null {
|
||||
const content = readFile(path.join(this.ciDir, "ARCHITECTURE.md"));
|
||||
if (!content) return null;
|
||||
return this.parseArchitectureMd(content);
|
||||
}
|
||||
|
||||
writeArchitectureMd(architecture: ArchitectureMd): void {
|
||||
this.ensureCIDir();
|
||||
const lines: string[] = [
|
||||
"# Architecture",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
architecture.overview,
|
||||
"",
|
||||
"## Components",
|
||||
"",
|
||||
];
|
||||
|
||||
for (const comp of architecture.components) {
|
||||
lines.push(`### ${comp.name}`);
|
||||
lines.push(`- **Description**: ${comp.description}`);
|
||||
lines.push(`- **Boundaries**: ${comp.boundaries}`);
|
||||
lines.push(`- **Depends on**: ${comp.dependsOn.length > 0 ? comp.dependsOn.join(", ") : "None"}`);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("## Data Flow");
|
||||
lines.push("");
|
||||
lines.push(architecture.dataFlow);
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Build Order");
|
||||
lines.push("");
|
||||
for (const step of architecture.buildOrder) {
|
||||
lines.push(`1. ${step}`);
|
||||
}
|
||||
|
||||
writeFile(path.join(this.ciDir, "ARCHITECTURE.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
updateRequirementStatus(reqId: string, status: "pending" | "in_progress" | "complete" | "blocked"): void {
|
||||
const reqs = this.readRequirementsMd();
|
||||
if (!reqs) return;
|
||||
|
||||
for (const t of reqs.traceability) {
|
||||
if (t.requirement === reqId) {
|
||||
t.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
this.writeRequirementsMd(reqs);
|
||||
}
|
||||
|
||||
updatePhaseStatus(phaseNumber: number, status: "not_started" | "in_progress" | "complete" | "deferred"): void {
|
||||
const roadmap = this.readRoadmapMd();
|
||||
if (!roadmap) return;
|
||||
|
||||
for (const phase of roadmap.phases) {
|
||||
if (phase.number === phaseNumber) {
|
||||
phase.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
this.writeRoadmapMd(roadmap);
|
||||
}
|
||||
|
||||
private parseProjectMd(content: string): ProjectMd {
|
||||
return {
|
||||
name: this.extractSection(content, "# ") || "Unknown",
|
||||
coreValue: this.extractSection(content, "## What This Is") || "",
|
||||
requirements: {
|
||||
validated: this.extractListItems(content, "### Validated"),
|
||||
active: this.extractListItems(content, "### Active"),
|
||||
outOfScope: this.extractListItems(content, "### Out of Scope"),
|
||||
},
|
||||
constraints: this.extractListItems(content, "## Constraints"),
|
||||
context: this.extractSection(content, "## Context") || "",
|
||||
keyDecisions: [],
|
||||
};
|
||||
}
|
||||
|
||||
private parseRoadmapMd(content: string): RoadmapMd {
|
||||
return {
|
||||
overview: this.extractSection(content, "## Overview") || "",
|
||||
phases: [],
|
||||
};
|
||||
}
|
||||
|
||||
private parseRequirementsMd(content: string): RequirementsMd {
|
||||
return {
|
||||
v1: [],
|
||||
v2: [],
|
||||
outOfScope: [],
|
||||
traceability: [],
|
||||
};
|
||||
}
|
||||
|
||||
private parseArchitectureMd(content: string): ArchitectureMd {
|
||||
return {
|
||||
overview: this.extractSection(content, "## Overview") || "",
|
||||
components: [],
|
||||
dataFlow: this.extractSection(content, "## Data Flow") || "",
|
||||
buildOrder: [],
|
||||
};
|
||||
}
|
||||
|
||||
private extractSection(content: string, header: string): string | null {
|
||||
const headerIdx = content.indexOf(header);
|
||||
if (headerIdx < 0) return null;
|
||||
|
||||
const startIdx = headerIdx + header.length;
|
||||
const nextHeaderIdx = content.indexOf("\n## ", startIdx);
|
||||
const endIdx = nextHeaderIdx >= 0 ? nextHeaderIdx : content.length;
|
||||
|
||||
return content.slice(startIdx, endIdx).trim();
|
||||
}
|
||||
|
||||
private extractListItems(content: string, header: string): string[] {
|
||||
const section = this.extractSection(content, header);
|
||||
if (!section) return [];
|
||||
|
||||
return section
|
||||
.split("\n")
|
||||
.filter((line) => line.trim().startsWith("-"))
|
||||
.map((line) => line.replace(/^-\s*(?:\[[ x]\]\s*)?(?:✓\s*)?/, "").trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,750 @@
|
||||
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 = ".ciagent";
|
||||
|
||||
export interface ProjectMd {
|
||||
name: string;
|
||||
coreValue: string;
|
||||
requirements: {
|
||||
validated: string[];
|
||||
active: string[];
|
||||
outOfScope: string[];
|
||||
};
|
||||
constraints: string[];
|
||||
context: string;
|
||||
keyDecisions: Array<{
|
||||
decision: string;
|
||||
rationale: string;
|
||||
outcome: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface RoadmapMd {
|
||||
overview: string;
|
||||
phases: Array<{
|
||||
number: number;
|
||||
name: string;
|
||||
description: string;
|
||||
status: "not_started" | "in_progress" | "complete" | "deferred";
|
||||
dependsOn: number[];
|
||||
requirements: string[];
|
||||
successCriteria: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface RequirementsMd {
|
||||
v1: Array<{
|
||||
category: string;
|
||||
items: Array<{ id: string; description: string }>;
|
||||
}>;
|
||||
v2: Array<{
|
||||
category: string;
|
||||
items: Array<{ id: string; description: string }>;
|
||||
}>;
|
||||
outOfScope: Array<{ feature: string; reason: string }>;
|
||||
traceability: Array<{
|
||||
requirement: string;
|
||||
phase: number;
|
||||
status: "pending" | "in_progress" | "complete" | "blocked";
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ArchitectureMd {
|
||||
overview: string;
|
||||
components: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
boundaries: string;
|
||||
dependsOn: string[];
|
||||
}>;
|
||||
dataFlow: string;
|
||||
buildOrder: string[];
|
||||
}
|
||||
|
||||
export interface ProjectEntry {
|
||||
slug: string;
|
||||
name: string;
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
export class CIAgentFiles {
|
||||
private projectPath: string;
|
||||
private projectSlug: string;
|
||||
|
||||
constructor(projectPath: string, projectSlug?: string) {
|
||||
this.projectPath = projectPath;
|
||||
this.projectSlug = projectSlug || "";
|
||||
}
|
||||
|
||||
private get ciDir(): string {
|
||||
return path.join(this.projectPath, CI_DIR);
|
||||
}
|
||||
|
||||
private get projectDir(): string {
|
||||
if (this.projectSlug) {
|
||||
return path.join(this.ciDir, this.projectSlug);
|
||||
}
|
||||
return this.ciDir;
|
||||
}
|
||||
|
||||
setProjectSlug(slug: string): void {
|
||||
this.projectSlug = slug;
|
||||
}
|
||||
|
||||
getProjectSlug(): string {
|
||||
return this.projectSlug;
|
||||
}
|
||||
|
||||
ensureCIDir(): void {
|
||||
ensureDir(this.ciDir);
|
||||
}
|
||||
|
||||
ensureProjectDir(): void {
|
||||
this.ensureCIDir();
|
||||
if (this.projectSlug) {
|
||||
ensureDir(this.projectDir);
|
||||
}
|
||||
}
|
||||
|
||||
isInitialized(): boolean {
|
||||
return fileExists(path.join(this.ciDir, "config.json"));
|
||||
}
|
||||
|
||||
isMultiProject(): boolean {
|
||||
if (!this.isInitialized()) return false;
|
||||
const config = this.readConfigJson();
|
||||
const projects = config?.projects;
|
||||
return Array.isArray(projects) && (projects as unknown[]).length > 0;
|
||||
}
|
||||
|
||||
listProjects(): ProjectEntry[] {
|
||||
if (!this.isInitialized()) return [];
|
||||
|
||||
const config = this.readConfigJson();
|
||||
if (Array.isArray(config?.projects) && config.projects.length > 0) {
|
||||
return config.projects;
|
||||
}
|
||||
|
||||
const subdirs = this.getProjectSubdirectories();
|
||||
if (subdirs.length > 0) {
|
||||
return subdirs.map((slug) => {
|
||||
const projMd = this.readProjectMdForSlug(slug);
|
||||
return {
|
||||
slug,
|
||||
name: projMd?.name || slug,
|
||||
default: subdirs.length === 1,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return [{ slug: "default", name: "Default Project", default: true }];
|
||||
}
|
||||
|
||||
getActiveProject(): string {
|
||||
if (!this.isInitialized()) return "";
|
||||
|
||||
const config = this.readConfigJson();
|
||||
if (config && typeof config.active_project === "string") return config.active_project;
|
||||
|
||||
const projects = this.listProjects();
|
||||
const defaultProject = projects.find((p) => p.default);
|
||||
if (defaultProject) return defaultProject.slug;
|
||||
|
||||
return projects.length > 0 ? projects[0].slug : "";
|
||||
}
|
||||
|
||||
setActiveProject(slug: string): void {
|
||||
this.ensureCIDir();
|
||||
const config = this.readConfigJson() || {};
|
||||
config.active_project = slug;
|
||||
this.writeConfigJson(config);
|
||||
}
|
||||
|
||||
addProject(slug: string, name: string, isDefault: boolean = false): void {
|
||||
this.ensureCIDir();
|
||||
const config = this.readConfigJson() || {};
|
||||
|
||||
if (!Array.isArray(config.projects)) {
|
||||
config.projects = [];
|
||||
}
|
||||
|
||||
if ((config.projects as unknown[]).some((p: unknown) => (p as ProjectEntry).slug === slug)) return;
|
||||
|
||||
(config.projects as ProjectEntry[]).push({ slug, name, default: isDefault });
|
||||
|
||||
if (isDefault || (config.projects as unknown[]).length === 1) {
|
||||
config.active_project = slug;
|
||||
}
|
||||
|
||||
this.writeConfigJson(config);
|
||||
ensureDir(path.join(this.ciDir, slug));
|
||||
}
|
||||
|
||||
needsMigration(): boolean {
|
||||
if (!this.isInitialized()) return false;
|
||||
if (this.isMultiProject()) return false;
|
||||
|
||||
const hasFlatFiles = fileExists(path.join(this.ciDir, "PROJECT.md"));
|
||||
const hasSubdirs = this.getProjectSubdirectories().length > 0;
|
||||
|
||||
return hasFlatFiles && !hasSubdirs;
|
||||
}
|
||||
|
||||
migrateFlatToProject(slug: string): void {
|
||||
if (!this.needsMigration()) return;
|
||||
|
||||
this.ensureCIDir();
|
||||
const projectDir = path.join(this.ciDir, slug);
|
||||
ensureDir(projectDir);
|
||||
|
||||
const filesToMove = ["PROJECT.md", "ARCHITECTURE.md", "ROADMAP.md", "REQUIREMENTS.md"];
|
||||
for (const file of filesToMove) {
|
||||
const src = path.join(this.ciDir, file);
|
||||
const dest = path.join(projectDir, file);
|
||||
if (fileExists(src) && !fileExists(dest)) {
|
||||
const content = readFile(src);
|
||||
if (content) {
|
||||
writeFile(dest, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = this.readConfigJson() || {};
|
||||
config.projects = [{ slug, name: slug, default: true }];
|
||||
config.active_project = slug;
|
||||
this.writeConfigJson(config);
|
||||
}
|
||||
|
||||
private getProjectSubdirectories(): string[] {
|
||||
if (!fs.existsSync(this.ciDir)) return [];
|
||||
|
||||
try {
|
||||
return fs.readdirSync(this.ciDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.filter((d) => {
|
||||
const projectFile = path.join(this.ciDir, d.name, "PROJECT.md");
|
||||
return fileExists(projectFile);
|
||||
})
|
||||
.map((d) => d.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private readConfigJson(): Record<string, unknown> | null {
|
||||
const content = readFile(path.join(this.ciDir, "config.json"));
|
||||
if (!content) return null;
|
||||
try {
|
||||
return JSON.parse(content) as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private writeConfigJson(config: Record<string, unknown>): void {
|
||||
writeFile(path.join(this.ciDir, "config.json"), JSON.stringify(config, null, 2));
|
||||
}
|
||||
|
||||
private readProjectMdForSlug(slug: string): ProjectMd | null {
|
||||
const content = readFile(path.join(this.ciDir, slug, "PROJECT.md"));
|
||||
if (!content) return null;
|
||||
return this.parseProjectMd(content);
|
||||
}
|
||||
|
||||
readProjectMd(): ProjectMd | null {
|
||||
const content = readFile(path.join(this.projectDir, "PROJECT.md"));
|
||||
if (!content) return null;
|
||||
return this.parseProjectMd(content);
|
||||
}
|
||||
|
||||
writeProjectMd(project: ProjectMd, reason: string): void {
|
||||
this.ensureProjectDir();
|
||||
const lines: string[] = [
|
||||
`# ${project.name}`,
|
||||
"",
|
||||
"## What This Is",
|
||||
"",
|
||||
project.coreValue,
|
||||
"",
|
||||
"## Requirements",
|
||||
"",
|
||||
"### Validated",
|
||||
"",
|
||||
...project.requirements.validated.map((r) => `- ✓ ${r}`),
|
||||
"",
|
||||
"### Active",
|
||||
"",
|
||||
...project.requirements.active.map((r) => `- [ ] ${r}`),
|
||||
"",
|
||||
"### Out of Scope",
|
||||
"",
|
||||
...project.requirements.outOfScope.map((r) => `- ${r}`),
|
||||
"",
|
||||
"## Context",
|
||||
"",
|
||||
project.context,
|
||||
"",
|
||||
"## Constraints",
|
||||
"",
|
||||
...project.constraints.map((c) => `- ${c}`),
|
||||
"",
|
||||
"## Key Decisions",
|
||||
"",
|
||||
"| Decision | Rationale | Outcome |",
|
||||
"|----------|-----------|---------|",
|
||||
...project.keyDecisions.map(
|
||||
(d) => `| ${d.decision} | ${d.rationale} | ${d.outcome} |`
|
||||
),
|
||||
"",
|
||||
];
|
||||
|
||||
writeFile(path.join(this.projectDir, "PROJECT.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
readRoadmapMd(): RoadmapMd | null {
|
||||
const content = readFile(path.join(this.projectDir, "ROADMAP.md"));
|
||||
if (!content) return null;
|
||||
return this.parseRoadmapMd(content);
|
||||
}
|
||||
|
||||
writeRoadmapMd(roadmap: RoadmapMd): void {
|
||||
this.ensureProjectDir();
|
||||
const lines: string[] = [
|
||||
"# Roadmap",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
roadmap.overview,
|
||||
"",
|
||||
"## Phases",
|
||||
"",
|
||||
...roadmap.phases.map(
|
||||
(p) => `- [${p.status === "complete" ? "x" : " "}] **Phase ${p.number}: ${p.name}** - ${p.description}`
|
||||
),
|
||||
"",
|
||||
"## Phase Details",
|
||||
"",
|
||||
];
|
||||
|
||||
for (const phase of roadmap.phases) {
|
||||
lines.push(`### Phase ${phase.number}: ${phase.name}`);
|
||||
lines.push(`**Goal**.: ${phase.description}`);
|
||||
lines.push(`**Depends on**: ${phase.dependsOn.length > 0 ? phase.dependsOn.map((d) => `Phase ${d}`).join(", ") : "Nothing"}`);
|
||||
lines.push(`**Requirements**: ${phase.requirements.length > 0 ? phase.requirements.join(", ") : "None"}`);
|
||||
lines.push("**Success Criteria**:");
|
||||
for (const sc of phase.successCriteria) {
|
||||
lines.push(`1. ${sc}`);
|
||||
}
|
||||
lines.push(`**Status**: ${phase.status}`);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
writeFile(path.join(this.projectDir, "ROADMAP.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
readRequirementsMd(): RequirementsMd | null {
|
||||
const content = readFile(path.join(this.projectDir, "REQUIREMENTS.md"));
|
||||
if (!content) return null;
|
||||
return this.parseRequirementsMd(content);
|
||||
}
|
||||
|
||||
writeRequirementsMd(requirements: RequirementsMd): void {
|
||||
this.ensureProjectDir();
|
||||
const lines: string[] = [
|
||||
"# Requirements",
|
||||
"",
|
||||
"## v1 Requirements",
|
||||
"",
|
||||
];
|
||||
|
||||
for (const cat of requirements.v1) {
|
||||
lines.push(`### ${cat.category}`);
|
||||
lines.push("");
|
||||
for (const item of cat.items) {
|
||||
lines.push(`- [ ] **${item.id}**: ${item.description}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("## v2 Requirements");
|
||||
lines.push("");
|
||||
for (const cat of requirements.v2) {
|
||||
lines.push(`### ${cat.category}`);
|
||||
lines.push("");
|
||||
for (const item of cat.items) {
|
||||
lines.push(`- **${item.id}**: ${item.description}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("## Out of Scope");
|
||||
lines.push("");
|
||||
lines.push("| Feature | Reason |");
|
||||
lines.push("|---------|--------|");
|
||||
for (const item of requirements.outOfScope) {
|
||||
lines.push(`| ${item.feature} | ${item.reason} |`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Traceability");
|
||||
lines.push("");
|
||||
lines.push("| Requirement | Phase | Status |");
|
||||
lines.push("|-------------|-------|--------|");
|
||||
for (const t of requirements.traceability) {
|
||||
lines.push(`| ${t.requirement} | Phase ${t.phase} | ${t.status} |`);
|
||||
}
|
||||
|
||||
writeFile(path.join(this.projectDir, "REQUIREMENTS.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
readArchitectureMd(): ArchitectureMd | null {
|
||||
const content = readFile(path.join(this.projectDir, "ARCHITECTURE.md"));
|
||||
if (!content) return null;
|
||||
return this.parseArchitectureMd(content);
|
||||
}
|
||||
|
||||
writeArchitectureMd(architecture: ArchitectureMd): void {
|
||||
this.ensureProjectDir();
|
||||
const lines: string[] = [
|
||||
"# Architecture",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
architecture.overview,
|
||||
"",
|
||||
"## Components",
|
||||
"",
|
||||
];
|
||||
|
||||
for (const comp of architecture.components) {
|
||||
lines.push(`### ${comp.name}`);
|
||||
lines.push(`- **Description**: ${comp.description}`);
|
||||
lines.push(`- **Boundaries**: ${comp.boundaries}`);
|
||||
lines.push(`- **Depends on**: ${comp.dependsOn.length > 0 ? comp.dependsOn.join(", ") : "None"}`);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("## Data Flow");
|
||||
lines.push("");
|
||||
lines.push(architecture.dataFlow);
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Build Order");
|
||||
lines.push("");
|
||||
for (const step of architecture.buildOrder) {
|
||||
lines.push(`1. ${step}`);
|
||||
}
|
||||
|
||||
writeFile(path.join(this.projectDir, "ARCHITECTURE.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
updateRequirementStatus(reqId: string, status: "pending" | "in_progress" | "complete" | "blocked"): void {
|
||||
const reqs = this.readRequirementsMd();
|
||||
if (!reqs) return;
|
||||
|
||||
for (const t of reqs.traceability) {
|
||||
if (t.requirement === reqId) {
|
||||
t.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
this.writeRequirementsMd(reqs);
|
||||
}
|
||||
|
||||
updatePhaseStatus(phaseNumber: number, status: "not_started" | "in_progress" | "complete" | "deferred"): void {
|
||||
const roadmap = this.readRoadmapMd();
|
||||
if (!roadmap) return;
|
||||
|
||||
for (const phase of roadmap.phases) {
|
||||
if (phase.number === phaseNumber) {
|
||||
phase.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
this.writeRoadmapMd(roadmap);
|
||||
}
|
||||
|
||||
getMilestoneType(): MilestoneType {
|
||||
const roadmap = this.readRoadmapMd();
|
||||
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" || phase.status === "complete") {
|
||||
const phaseName = phase.name.toLowerCase();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSchemaBreak) return "schema-breaking";
|
||||
if (hasFeature) return "feature";
|
||||
return "nfr";
|
||||
}
|
||||
|
||||
isNfrMilestone(): boolean {
|
||||
return this.getMilestoneType() === "nfr";
|
||||
}
|
||||
|
||||
private parseProjectMd(content: string): ProjectMd {
|
||||
return {
|
||||
name: this.extractSection(content, "# ") || "Unknown",
|
||||
coreValue: this.extractSection(content, "## What This Is") || "",
|
||||
requirements: {
|
||||
validated: this.extractListItems(content, "### Validated"),
|
||||
active: this.extractListItems(content, "### Active"),
|
||||
outOfScope: this.extractListItems(content, "### Out of Scope"),
|
||||
},
|
||||
constraints: this.extractListItems(content, "## Constraints"),
|
||||
context: this.extractSection(content, "## Context") || "",
|
||||
keyDecisions: [],
|
||||
};
|
||||
}
|
||||
|
||||
private parseRoadmapMd(content: string): RoadmapMd {
|
||||
const overview = this.extractSection(content, "## Overview") || "";
|
||||
|
||||
const phases: RoadmapMd["phases"] = [];
|
||||
const phaseRegex = /### Phase (\d+): (.+)/g;
|
||||
let match;
|
||||
|
||||
while ((match = phaseRegex.exec(content)) !== null) {
|
||||
const number = parseInt(match[1], 10);
|
||||
const name = match[2].trim();
|
||||
const sectionStart = match.index + match[0].length;
|
||||
const nextPhase = content.indexOf("\n### Phase ", sectionStart);
|
||||
const nextH2 = content.indexOf("\n## ", sectionStart);
|
||||
const sectionEnd = Math.min(
|
||||
nextPhase >= 0 ? nextPhase : content.length,
|
||||
nextH2 >= 0 ? nextH2 : content.length
|
||||
);
|
||||
const section = content.slice(sectionStart, sectionEnd);
|
||||
|
||||
const goalMatch = section.match(/\*\*Goal\.?\*\*:\s*(.+)/);
|
||||
const statusMatch = section.match(/\*\*Status\*\*:\s*(.+)/);
|
||||
const reqMatch = section.match(/\*\*Requirements\*\*:\s*(.+)/);
|
||||
const depsMatch = section.match(/\*\*Depends on\*\*:\s*(.+)/);
|
||||
|
||||
const statusVal = statusMatch ? statusMatch[1].trim() : "not_started";
|
||||
const validStatuses = ["not_started", "in_progress", "complete", "deferred"] as const;
|
||||
|
||||
phases.push({
|
||||
number,
|
||||
name,
|
||||
description: goalMatch ? goalMatch[1].trim() : "",
|
||||
status: validStatuses.includes(statusVal as typeof validStatuses[number])
|
||||
? (statusVal as RoadmapMd["phases"][number]["status"])
|
||||
: "not_started",
|
||||
dependsOn: depsMatch && depsMatch[1].trim() !== "Nothing"
|
||||
? depsMatch[1].split(",").map((d: string) => parseInt(d.trim().replace(/Phase /g, ""), 10)).filter((n: number) => !isNaN(n))
|
||||
: [],
|
||||
requirements: reqMatch && reqMatch[1].trim() !== "None"
|
||||
? reqMatch[1].split(",").map((r: string) => r.trim()).filter(Boolean)
|
||||
: [],
|
||||
successCriteria: [],
|
||||
});
|
||||
}
|
||||
|
||||
return { overview, phases };
|
||||
}
|
||||
|
||||
private parseRequirementsMd(content: string): RequirementsMd {
|
||||
const v1: RequirementsMd["v1"] = [];
|
||||
const v2: RequirementsMd["v2"] = [];
|
||||
|
||||
const v1Section = this.extractSection(content, "## v1 Requirements");
|
||||
if (v1Section) {
|
||||
const categoryBlocks = v1Section.split(/\n### /).filter(Boolean);
|
||||
for (const block of categoryBlocks) {
|
||||
const lines = block.split("\n");
|
||||
const category = lines[0].trim();
|
||||
const items: Array<{ id: string; description: string }> = [];
|
||||
|
||||
for (const line of lines.slice(1)) {
|
||||
const tableMatch = line.match(/^\|\s*([A-Z]+-\d+)\s*\|\s*(.+?)\s*\|/);
|
||||
if (tableMatch) {
|
||||
items.push({ id: tableMatch[1], description: tableMatch[2] });
|
||||
continue;
|
||||
}
|
||||
const listMatch = line.match(/^\s*-?\s*\*?\s*\[?\s*\*?\s*([A-Z]+-\d+)[\]:\s*]*(.+)/);
|
||||
if (listMatch) {
|
||||
items.push({ id: listMatch[1], description: listMatch[2].trim() });
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
v1.push({ category, items });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const v2Section = this.extractSection(content, "## v2 Requirements");
|
||||
if (v2Section) {
|
||||
const categoryBlocks = v2Section.split(/\n### /).filter(Boolean);
|
||||
for (const block of categoryBlocks) {
|
||||
const lines = block.split("\n");
|
||||
const category = lines[0].trim();
|
||||
const items: Array<{ id: string; description: string }> = [];
|
||||
|
||||
for (const line of lines.slice(1)) {
|
||||
const tableMatch = line.match(/^\|\s*([A-Z]+-\d+)\s*\|\s*(.+?)\s*\|/);
|
||||
if (tableMatch) {
|
||||
items.push({ id: tableMatch[1], description: tableMatch[2] });
|
||||
continue;
|
||||
}
|
||||
const listMatch = line.match(/^\s*-?\s*\*?\s*\[?\s*\*?\s*([A-Z]+-\d+)[\]:\s*]*(.+)/);
|
||||
if (listMatch) {
|
||||
items.push({ id: listMatch[1], description: listMatch[2].trim() });
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
v2.push({ category, items });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const outOfScope: RequirementsMd["outOfScope"] = [];
|
||||
const outSection = this.extractSection(content, "## Out of Scope");
|
||||
if (outSection) {
|
||||
const tableRows = outSection.split("\n").filter((line) => /^\|/.test(line) && !line.includes("---") && !line.includes("Feature"));
|
||||
for (const row of tableRows) {
|
||||
const cols = row.split("|").map((c) => c.trim()).filter(Boolean);
|
||||
if (cols.length >= 2) {
|
||||
outOfScope.push({ feature: cols[0], reason: cols[1] });
|
||||
}
|
||||
}
|
||||
if (outOfScope.length === 0) {
|
||||
const listItems = this.extractListItems(content, "## Out of Scope");
|
||||
for (const item of listItems) {
|
||||
outOfScope.push({ feature: item, reason: "" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const traceability: RequirementsMd["traceability"] = [];
|
||||
const traceSection = this.extractSection(content, "## Traceability");
|
||||
if (traceSection) {
|
||||
const activeHeader = traceSection.includes("Active Milestone")
|
||||
? "## v0.5 Requirements (Active Milestone)"
|
||||
: content.includes("## v1 Requirements")
|
||||
? "## v1 Requirements"
|
||||
: undefined;
|
||||
|
||||
const tableRows = traceSection.split("\n").filter((line) => /^\|/.test(line) && !line.includes("---") && !line.includes("Requirement") && !line.includes("REQ-ID"));
|
||||
for (const row of tableRows) {
|
||||
const cols = row.split("|").map((c) => c.trim()).filter(Boolean);
|
||||
if (cols.length >= 3) {
|
||||
const req = cols[0];
|
||||
const phaseStr = cols[1];
|
||||
const phaseMatch = phaseStr.match(/(\d+)/);
|
||||
const phase = phaseMatch ? parseInt(phaseMatch[1], 10) : 0;
|
||||
const statusStr = cols[2].toLowerCase();
|
||||
const status = ["pending", "in_progress", "complete", "blocked", "covered"].includes(statusStr)
|
||||
? (statusStr === "covered" ? "complete" : statusStr as "pending" | "in_progress" | "complete" | "blocked")
|
||||
: "pending";
|
||||
traceability.push({ requirement: req, phase, status });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allReqIds = new Set<string>();
|
||||
for (const cat of [...v1, ...v2]) {
|
||||
for (const item of cat.items) {
|
||||
allReqIds.add(item.id);
|
||||
}
|
||||
}
|
||||
for (const t of traceability) {
|
||||
allReqIds.add(t.requirement);
|
||||
}
|
||||
const coveredInTrace = new Set(traceability.filter((t) => t.status === "complete").map((t) => t.requirement));
|
||||
for (const reqId of allReqIds) {
|
||||
if (!coveredInTrace.has(reqId)) {
|
||||
traceability.push({ requirement: reqId, phase: 0, status: "pending" });
|
||||
}
|
||||
}
|
||||
|
||||
return { v1, v2, outOfScope, traceability };
|
||||
}
|
||||
|
||||
private parseArchitectureMd(content: string): ArchitectureMd {
|
||||
const overview = this.extractSection(content, "## Overview") || "";
|
||||
|
||||
const components: ArchitectureMd["components"] = [];
|
||||
const section = content;
|
||||
const componentRegex = /###\s+(.+)/g;
|
||||
let compMatch;
|
||||
|
||||
const h3Positions: Array<{ name: string; start: number }> = [];
|
||||
while ((compMatch = componentRegex.exec(section)) !== null) {
|
||||
h3Positions.push({ name: compMatch[1].trim(), start: compMatch.index + compMatch[0].length });
|
||||
}
|
||||
|
||||
for (let i = 0; i < h3Positions.length; i++) {
|
||||
const name = h3Positions[i].name;
|
||||
const start = h3Positions[i].start;
|
||||
const end = i + 1 < h3Positions.length ? h3Positions[i + 1].start - (content.substring(h3Positions[i + 1].start - 4, h3Positions[i + 1].start) === "### " ? 4 : 0) : content.length;
|
||||
const block = content.slice(start, end);
|
||||
|
||||
const descMatch = block.match(/[-*]\s*\*?\*?(?:Description|description)\*?\*?\s*[::]\s*(.+)/);
|
||||
const boundaryMatch = block.match(/[-*]\s*\*?\*?(?:Boundaries|boundaries)\*?\*?\s*[::]\s*(.+)/);
|
||||
const depsMatch = block.match(/[-*]\s*\*?\*?(?:Depends on|depends on|Dependencies)\*?\*?\s*[::]\s*(.+)/);
|
||||
|
||||
components.push({
|
||||
name,
|
||||
description: descMatch ? descMatch[1].trim() : "",
|
||||
boundaries: boundaryMatch ? boundaryMatch[1].trim() : "",
|
||||
dependsOn: depsMatch
|
||||
? depsMatch[1].split(",").map((d: string) => d.trim().replace(/\*\*/g, "")).filter(Boolean)
|
||||
: [],
|
||||
});
|
||||
}
|
||||
|
||||
const dataFlow = this.extractSection(content, "## Data Flow")
|
||||
|| this.extractSection(content, "## Data flow")
|
||||
|| "";
|
||||
|
||||
const buildOrder: string[] = [];
|
||||
const buildSection = this.extractSection(content, "## Build Order");
|
||||
if (buildSection) {
|
||||
const listItems = buildSection
|
||||
.split("\n")
|
||||
.filter((line) => /^\d+\./.test(line.trim()))
|
||||
.map((line) => line.trim().replace(/^\d+\.\s*/, ""));
|
||||
buildOrder.push(...listItems);
|
||||
}
|
||||
|
||||
return { overview, components, dataFlow, buildOrder };
|
||||
}
|
||||
|
||||
private extractSection(content: string, header: string): string | null {
|
||||
const headerIdx = content.indexOf(header);
|
||||
if (headerIdx < 0) return null;
|
||||
|
||||
const startIdx = headerIdx + header.length;
|
||||
const nextHeaderIdx = content.indexOf("\n## ", startIdx);
|
||||
const endIdx = nextHeaderIdx >= 0 ? nextHeaderIdx : content.length;
|
||||
|
||||
return content.slice(startIdx, endIdx).trim();
|
||||
}
|
||||
|
||||
private extractListItems(content: string, header: string): string[] {
|
||||
const section = this.extractSection(content, header);
|
||||
if (!section) return [];
|
||||
|
||||
return section
|
||||
.split("\n")
|
||||
.filter((line) => line.trim().startsWith("-"))
|
||||
.map((line) => line.replace(/^-\s*(?:\[[ x]\]\s*)?(?:✓\s*)?/, "").trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user