Compare commits

..

15 Commits

Author SHA1 Message Date
Jon Chery 6d0034dc88 docs(P07): release flow hardening — consistent milestone type taxonomy
CI / build-and-test (push) Has been cancelled
Publish to npm / publish (push) Has been cancelled
---ci---
phase: 7
milestone: v0.10
status: complete
decisions:
  - id: D-092
    decision: Rename schema-breaking → major across all framework files
    rationale: Major aligns with semver terminology and is more descriptive of the version bump impact
    confidence: 0.95
  - id: D-093
    decision: Add Major milestone type to dev context and branch-strategy merge validation gates
    rationale: Release flow was documented but not enforced. Zero-HITL, PR+QA, and branch hierarchy are now hard gates
    confidence: 0.92
---/ci---
2026-06-01 16:14:54 +00:00
grimacing a153291643 Merge pull request 'feat(P06): Integration & hardening — INTEG-01..05, MULTI-04, v0.10.0' (#9) from phase/06-integration-hardening into main
CI / build-and-test (push) Has been cancelled
Publish to npm / publish (push) Has been cancelled
2026-06-01 15:41:20 +00:00
Jon Chery a0619f9740 feat(P06): Integration & hardening — INTEG-01..05, MULTI-04
CI / build-and-test (push) Has been cancelled
CI / build-and-test (pull_request) Has been cancelled
- INTEG-01: E2E ideation test (19 tests with proper structure)
- INTEG-02: E2E multi-project test (14 tests)
- INTEG-03: Version bump 0.9.0 → 0.10.0
- INTEG-04: AGENTS.md and README updates
- INTEG-05: All 594 tests passing
- MULTI-04: max_concurrent_projects config in ParallelizationConfig
- Fixed e2e-ideation test nesting and assertion issues

---ci---
phase: 6
milestone: v0.10
status: execute
decisions:
  - id: INTEG-01
    decision: E2E ideation test covers mechanical, acceptance, cascade, external, cross-project, chaos, spec
    rationale: 19 tests covering all ideation engine methods
    confidence: 0.95
  - id: INTEG-03
    decision: Version bumped to 0.10.0
    rationale: Minor update per semver for new ideation and multi-project features
    confidence: 0.99
  - id: MULTI-04
    decision: max_concurrent_projects added to ParallelizationConfig
    rationale: Controls parallel execution limit for multi-project pipelines
    confidence: 0.90
requirements:
  covered: [INTEG-01, INTEG-02, INTEG-03, INTEG-04, INTEG-05, MULTI-04]
---/ci---
2026-06-01 15:39:47 +00:00
Jon Chery f478088797 refactor(P06): rename milestone type schema-breaking → major, reinforce release flow
---ci---
phase: 6
milestone: v0.10
status: execute
decisions:
  - id: D-001
    decision: Rename MilestoneType schema-breaking to major for clarity
    rationale: Major better describes the semver impact (major version bump) and aligns with standard semver terminology
    confidence: 0.95
    alternatives: [schema-breaking, breaking, major-change]
  - id: D-002
    decision: Add autopilot rules, PR+QA gates, and merge validation to ship workflow
    rationale: Release flow was documented but not enforced in the workflow. Zero-HITL rules, branch hierarchy validation, and coreci packaging steps ensure consistent releases
    confidence: 0.90
    alternatives: [keep-as-documentation-only, add-to-AGENTS.md-only]
---/ci---
2026-06-01 15:29:43 +00:00
grimacing e2b749d42e Merge pull request 'feat(P05): Multi-project ideation support — MULTI-03, MULTI-05, MULTI-07' (#8) from phase/05-multi-project-ideation into main
CI / build-and-test (push) Has been cancelled
Publish to npm / publish (push) Has been cancelled
2026-06-01 13:58:10 +00:00
Jon Chery c747d3e8be feat(P05): Multi-project ideation support — MULTI-03, MULTI-05, MULTI-07
CI / build-and-test (push) Has been cancelled
CI / build-and-test (pull_request) Has been cancelled
---ci---
phase: 5
milestone: v0.10
status: execute
decisions:
  - id: MULTI-03
    decision: Parallel project execution via OrchestratorAgent.runForAllProjects
    rationale: Sequential by default, parallel when parallelization.enabled with max_concurrent_projects limit
    confidence: 0.85
    alternatives: [single-project-only, manual-iteration]
  - id: MULTI-05
    decision: ideate --project all iterates all active_projects with deduplication
    rationale: Each project gets its own IdeationEngine; ideas deduplicated by project:title key
    confidence: 0.90
    alternatives: [single-project-only, merge-all-ideas]
  - id: MULTI-07
    decision: project field in ---ci--- commit blocks and CommitScope for multi-project tracking
    rationale: CIAgentMetadata.project and CommitScope.project fields propagated through all commit builders
    confidence: 0.92
    alternatives: [separate-repos-only, branch-prefix-only]
requirements:
  covered: [MULTI-03, MULTI-05, MULTI-07]
  partial: []
---/ci---

- Add max_concurrent_projects to ParallelizationConfig (default: 3)
- Add AgentContext.project_slug optional field for multi-project pipeline tracking
- Implement OrchestratorAgent.runForProject() for single-project execution
- Implement OrchestratorAgent.runForAllProjects() for multi-project iteration
  - Sequential execution by default
  - Parallel when parallelization.enabled with limitConcurrency batching
- Add --project flag to createRunCommand for targeted project execution
  - --project all triggers multi-project pipeline
  - --project slug1,slug2 for comma-separated projects
- Enhance createIdeateCommand --project all support
  - Iterates all active projects from config
  - Deduplicates findings by project:title key
  - Per-project idea acceptance via separate IdeationEngine instances
  - Markdown table output for multi-project results
- Propagate project slug through orchestrator pipeline commits
  - Specify stage: project field in CIAgentMetadata init commit
  - Ideate stage: project field in task commit via buildTaskCommit
  - Orchestrator sets ciFiles with project slug for per-project .ciagent dirs
- 19 new tests covering MULTI-03, MULTI-05, MULTI-07 functionality
- All 561 tests pass, typecheck clean
2026-06-01 13:56:43 +00:00
grimacing d9927558d5 Merge pull request 'feat(P04): Cross-project pipeline integration — IDEATE-16, IDEATE-11, IDEATE-18' (#7) from phase/04-cross-project-pipeline into main
CI / build-and-test (push) Has been cancelled
Publish to npm / publish (push) Has been cancelled
2026-05-30 21:20:34 +00:00
Jon Chery 895d9f95a1 feat(P04): Add IDEATE stage to orchestrator pipeline — IDEATE-16
CI / build-and-test (push) Has been cancelled
CI / build-and-test (pull_request) Has been cancelled
- Add ideation-agent to STAGE_AGENT_MAP for ideate stage
- Implement ideate case in executeStage() with mechanical ideation,
  config-aware category filtering, idea deduplication, auto-accept,
  and ---ci--- commit with decision block
- Add test verifying ideate position between research and plan in
  STAGE_ORDER
- 542 tests passing
2026-05-30 21:17:21 +00:00
Jon Chery 30352a3603 feat(P03): External/cascade tests + --ideate flag on run (#6)
CI / build-and-test (push) Has been cancelled
Publish to npm / publish (push) Has been cancelled
2026-05-30 20:59:40 +00:00
Jon Chery d58fd0bdde feat(P03): external/cascade tests + --ideate flag on run — IDEATE-07,08,15
CI / build-and-test (push) Has been cancelled
CI / build-and-test (pull_request) Has been cancelled
---ci---
phase: 3
milestone: v0.10
status: execute
decisions:
  - id: D-084
    decision: Dual integration: standalone ciagent ideate + --ideate flag on run
    confidence: 0.90
requirements:
  covered:
    - IDEATE-07
    - IDEATE-08
    - IDEATE-15
---/ci---

- IDEATE-07: External signal collection (npm audit, dependency staleness) tested
- IDEATE-08: Cascade impact analysis (--affected) tested
- IDEATE-15: --ideate flag on ciagent run inserts IDEATE stage between RESEARCH and PLAN
- Tests for runAffected, runExternal, runCrossProject
- 541 tests passing
2026-05-30 20:58:30 +00:00
Jon Chery 0799cfc644 feat(P02): Backend-enriched tier, chaos engineering, prioritization — IDEATE-04,05,06,09,10 (#5)
CI / build-and-test (push) Has been cancelled
Publish to npm / publish (push) Has been cancelled
2026-05-30 20:51:36 +00:00
Jon Chery 70ee21856d feat(P02): backend-enriched tier, chaos engineering, prioritization — IDEATE-04,05,06,09,10
CI / build-and-test (push) Has been cancelled
CI / build-and-test (pull_request) Has been cancelled
---ci---
phase: 2
milestone: v0.10
status: execute
decisions:
  - id: D-087
    decision: All 6 innovative features in v1 (pattern mining, drift detection, layer inversion, cross-project, chaos, spec)
    rationale: User wants bleeding-edge; all uniquely differentiated
    confidence: 0.82
requirements:
  covered:
    - IDEATE-04
    - IDEATE-05
    - IDEATE-06
    - IDEATE-09
    - IDEATE-10
---/ci---

- IDEATE-04: Verification layer inversion (structural, behavioral, security, quality missing detection)
- IDEATE-05: Architectural drift detection (documented vs actual component comparison)
- IDEATE-06: Spec-driven improvement (ambiguity detection, missing category detection)
- IDEATE-09: Backend-enriched analysis (prioritization, novel suggestions, action plans)
- IDEATE-10: Chaos engineering ideation (backend unavailable, requirement change, coverage drop)
- Deduplicated type exports: IdeationSource/Idea/etc now in types/ideation.ts
- 538 tests passing
2026-05-30 20:50:29 +00:00
Jon Chery b7d02ee4a4 feat(P01): interactive validation + doc updates + multi-project CLI — IDEATE-12,13,14 + MULTI-02,06
CI / build-and-test (pull_request) Has been cancelled
Publish to npm / publish (push) Has been cancelled
CI / build-and-test (push) Has been cancelled
---ci---
phase: 1
milestone: v0.10
status: execute
decisions:
  - id: D-083
    decision: Interactive one-at-a-time validation with accept/skip/modify
    rationale: Gives user full control over ideation results
    confidence: 0.87
  - id: D-085
    decision: Ask-after-validation kickoff of run workflow
    rationale: Balances automation with user control
    confidence: 0.85
  - id: D-091
    decision: Full multi-project support with active_projects array + parallel execution
    rationale: User wants complete multi-project capability
    confidence: 0.85
requirements:
  covered:
    - IDEATE-12
    - IDEATE-13
    - IDEATE-14
    - MULTI-02
    - MULTI-06
---/ci---

- IDEATE-12: Interactive accept/skip/modify validation with readline
- IDEATE-13: acceptIdea/acceptIdeas methods update REQUIREMENTS.md and ROADMAP.md
- IDEATE-14: Ask-after-validation kickoff prompt for
- MULTI-02: --project flag accepts comma-separated or 'all' in pre-action hook
- MULTI-06: ciagent status shows active_projects and ideation config
- projects list shows all active projects with multi-marker
- projects set updates both active_project and active_projects
2026-05-30 20:26:36 +00:00
Jon Chery 8e50049ba5 feat(P01): add ideation engine + ciagent ideate command — IDEATE-01,02,03,17 + MULTI-01
---ci---
phase: 1
milestone: v0.10
status: execute
decisions:
  - id: D-080
    decision: Three-tier ideation (mechanical, backend-enriched, cross-project)
    rationale: Mechanical tier always produces output without backend
    confidence: 0.92
  - id: D-089
    decision: No separate codebase map command
    rationale: Git-native + .ciagent/ covers mapping; avoids tree-sitter dep
    confidence: 0.88
requirements:
  covered:
    - IDEATE-01
    - IDEATE-02
    - IDEATE-03
    - IDEATE-17
    - MULTI-01
---/ci---

Add IdeationEngine core module with 15 signal collectors:
- Uncovered/partial requirements from REQUIREMENTS.md
- Coverage gaps (documented but unimplemented agents)
- Repeated lessons from git history
- Low-confidence decisions from ---ci--- blocks
- Escalation patterns from git history
- Compound solution patterns
- Architecture drift (ARCHITECTURE.md vs src/)
- Verification inversion (missing test files)
- Improvement patterns (cross-referencing lessons + requirements)
- Spec ambiguity (should/could/might patterns)
- Spec missing (common requirement categories)
- Cascade impact (--affected from git diff)
- External signals (npm audit, dependency staleness)
- Cross-project lesson mining

Add ciagent ideate CLI command with flags:
--category, --affected, --spec, --external, --cross-project, --output

Add active_projects to CIAgentConfig (backwards compatible with active_project).
Add IDEATE pipeline stage between RESEARCH and PLAN.
Update IdeationAgent to delegate to IdeationEngine.

533 tests passing.
2026-05-30 20:13:43 +00:00
Jon Chery da528cc493 docs: add ideate workflow + update run workflow with IDEATE stage and multi-project
---ci---
phase: 0
milestone: v0.10
status: specify
decisions:
  - id: D-089
    decision: No separate codebase map command — subsumed by ideation
    rationale: Git-native + .ciagent/ covers all mapping needs; avoids tree-sitter dep
    confidence: 0.88
  - id: D-090
    decision: Milestone v0.10 for ideate + multi-project
    rationale: Significant features but not schema-breaking
    confidence: 0.95
---/ci---

- Add opencode/ci/workflows/ideate.md: full ideation pipeline specification
- Update opencode/ci/workflows/run.md: add IDEATE stage, update multi-project Step 0
2026-05-30 19:45:30 +00:00
33 changed files with 4039 additions and 340 deletions
+11 -12
View File
@@ -84,7 +84,7 @@ templates/ # Template files (config.json, DECISIONS.md, specification.md
## Pipeline Flow
```
SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
SPECIFY → CLARIFY → RESEARCH → IDEATE → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
```
Each stage is executed by `OrchestratorAgent.executeStage()`. The orchestrator delegates intelligent stages (research, plan, execute, test, 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.
@@ -134,7 +134,7 @@ IntelligenceBackend (unified interface)
- Test framework: Jest with ts-jest
- Test file pattern: `**/*.test.ts` in `src/`
- Run: `npm run test`
- 57 test suites, 527 tests covering types, core, git-native, verification, agent, backends, and utility modules
- 58 test suites, 561 tests covering types, core, git-native, verification, agent, backends, ideation, multi-project, 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
@@ -194,19 +194,18 @@ IntelligenceBackend (unified interface)
## Current State
- **v0.10.0**: Ideate & Multi-Project — 3-tier ideation engine, `ciagent ideate` command, multi-project execution, `---ci--- project:` blocks, E2E tests
- **v0.9.0**: Integration & hardening — OpenAI and Anthropic backends, all 19 agents with intrinsic mechanical logic, E2E v0.9 integration tests, parallel agent execution
- **v0.8.0**: 11 newly-fleshed agents with mechanical methods, OpenAI/Anthropic config types, Gitea CI workflows
- **New in v0.10**: IdeationEngine with mechanical/backend-enriched/cross-project tiers, `ciagent ideate` command with --category/--affected/--spec/--external/--cross-project/--project/--output flags, `IDEATE` pipeline stage between RESEARCH and PLAN, multi-project support with `active_projects` config and `--project all` flag, `---ci--- project: <slug>` commit blocks, `max_concurrent_projects` parallelization config
- **New backends (v0.9)**: OpenAIBackend (gpt-4o, API key auth, OpenAI-Organization header), AnthropicBackend (Claude, API key auth, anthropic-version header, tool use translation)
- **Config expansion**: BackendConfigSection now includes `openai` and `anthropic` in `llm_backends` with dedicated `OpenAIConfig` and `AnthropicConfig` types
- **Auto-detection order (v0.9)**: opencode → openai → ollama-local → ollama-cloud → anthropic
- **Config expansion (v0.10)**: `ideation` section in config with categories, thresholds, external signals, cross-project, chaos; `active_projects` array; `max_concurrent_projects` in parallelization
- **Auto-detection order**: opencode → openai → ollama-local → ollama-cloud → anthropic
- **All agents mechanical**: Every non-orchestrator agent (18/19) produces meaningful output without a backend — no "requires intelligence backend" stub errors
- **Integration tests**: E2E v0.9 test with mock backend verifies multi-agent pipeline (researcher → planner → security-auditor → code-reviewer → verifier); all-agents-mechanical test iterates 18 agents
- **Parallel execution**: OrchestratorAgent supports concurrent review agents with `limitConcurrency()`, controlled by `parallelization.max_concurrent_agents`
- **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
- **Integration tests**: E2E v0.10 tests verify ideation CLI (mechanical tier), multi-project execution, all-agents-mechanical, parallel execution
- **Pipeline stages**: SPECIFY → CLARIFY → RESEARCH → **IDEATE** → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
- **Commit schema**: Every CIAgent-generated commit contains a `---ci---` YAML block with phase, milestone, status, decisions, escalations, requirements, lessons, compound, and **project** 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
- **Verification layers**: All 4 layers implemented — structural, behavioral (test execution), security (STRIDE + CWE), quality (3-persona review)
- **CLI**: All 11 commands wired up (`init`, `run`, `quick`, `debug`, `verify`, `review`, `status`, `audit`, `clarify`, `rollback`, `ship`)
- **CLI commands**: `init`, `run`, `quick`, `debug`, `verify`, `review`, `status`, `audit`, `clarify`, `rollback`, `ship`, `ideate`, `projects`
- **Intelligence backends**: 5 options — OpenAI (LLM), Anthropic (LLM), OllamaLocal (LLM, localhost), OllamaCloud (LLM, remote), Opencode (Agent, --non-interactive). Auto-detection: opencode → openai → ollama-local → ollama-cloud → anthropic.
- **Tests**: 57 test suites, 527 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 (ollama, openai, anthropic, opencode, tool-registry), agents (all 18 non-orchestrator), zod validation, e2e, parallel execution
- **Tests**: 58 test suites, 561 tests covering types, config, decision-engine, escalation, clarify, commit-parser, commit-builder, git-context, git-branch, ciagent-files, ideation, multi-project, all 4 verification layers, file utils, backends (ollama, openai, anthropic, opencode, tool-registry), agents (all 18 non-orchestrator), zod validation, E2E, parallel execution
+97 -6
View File
@@ -63,6 +63,28 @@ ciagent quick "Add authentication middleware"
# Check project status (reads from git log + branches)
ciagent status
# Discover improvement opportunities
ciagent ideate # Mechanical tier (always available)
ciagent ideate --category security # Focus on specific categories
ciagent ideate --affected # Cascade impact analysis
ciagent ideate --spec # Specification completeness analysis
ciagent ideate --external # npm audit + dependency staleness
ciagent ideate --cross-project # Cross-project pattern mining
ciagent ideate --project all # Run across all active projects
ciagent ideate --output json # JSON output mode
ciagent ideate --output markdown # Markdown output mode
# Manage multiple projects
ciagent projects list # List all registered projects
ciagent projects add <slug> <name> # Add a new project
ciagent projects set <slug> # Set the active project
# Run with ideation stage
ciagent run --ideate # Insert IDEATE stage between RESEARCH and PLAN
# Run across all active projects
ciagent run --project all # Execute pipeline for each project
# Review autonomous decisions (extracted from git log ---ci--- blocks)
ciagent audit
ciagent audit --verbose
@@ -77,7 +99,7 @@ ciagent rollback 1
ciagent ship 1
```
## Git-Native Architecture (v0.9.0)
## Git-Native Architecture (v0.10.0)
### The Commit Schema
@@ -111,7 +133,7 @@ requirements:
| Where | What | Why |
|-------|------|-----|
| `.ciagent/config.json` | Autonomy, thresholds, git strategy | Controls system behavior before any commits exist |
| `.ciagent/config.json` | Autonomy, thresholds, git strategy, ideation, multi-project | 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 |
@@ -204,7 +226,8 @@ CIAgent uses `.ciagent/config.json` for project configuration:
"parallelization": {
"enabled": true,
"max_concurrent_agents": 5,
"min_plans_for_parallel": 2
"min_plans_for_parallel": 2,
"max_concurrent_projects": 3
},
"verification": {
"automated_only": true,
@@ -221,6 +244,25 @@ CIAgent uses `.ciagent/config.json` for project configuration:
"branching_strategy": "phase",
"auto_commit": true,
"auto_push": false
},
"ideation": {
"enabled": true,
"categories": ["security", "quality", "architecture", "coverage", "improvement"],
"confidence_threshold": 0.6,
"max_ideas": 20,
"external_signals": {
"npm_audit": true,
"osv_advisories": true,
"dependency_staleness": true
},
"cross_project": {
"enabled": false,
"similarity_weight": 0.5
},
"chaos": {
"enabled": true,
"scenarios": ["backend_unavailable", "requirement_change", "test_coverage_drop"]
}
}
}
```
@@ -230,9 +272,9 @@ CIAgent uses `.ciagent/config.json` for project configuration:
### Pipeline
```
SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
↕ ↕ ↕ ↕ ↕
(questions) (auto-decide) (auto-run) (auto-test) (auto-verify)
SPECIFY → CLARIFY → RESEARCH → IDEATE → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
↕ ↕ ↕ ↕ ↕
(questions) (auto-decide) (ideas) (auto-run) (auto-test) (auto-verify)
```
### Git-Native Core Modules
@@ -278,6 +320,55 @@ Decisions are committed to git as `decision` type commits. The audit trail is `g
| solution-writer | Solution docs | Produces structured solution documents from plan + requirements |
| phase-researcher | Phase research | Extracts decisions, lessons, risks from git log for a specific phase |
### Ideation
CIAgent includes a built-in ideation engine that discovers improvement opportunities from git-native signals:
1. **Tier 1 — Mechanical**: Mines git history for uncovered requirements, repeated lessons, low-confidence decisions, escalation patterns, coverage gaps, architecture drift, and verification inversions
2. **Tier 2 — Backend-enriched**: When a backend is available, prioritizes mechanical findings and suggests novel improvements
3. **Tier 3 — Cross-project**: Mines patterns from other projects in the multi-project registry
```
ciagent ideate # All mechanical tiers
ciagent ideate --category security # Security-focused ideas
ciagent ideate --affected # Cascade impact from current changes
ciagent ideate --spec # Specification completeness analysis
ciagent ideate --external # npm audit + OSV advisories
ciagent ideate --cross-project # Cross-project pattern mining
ciagent ideate --project all # Across all active projects
ciagent ideate --output json # Machine-readable output
```
### Multi-Project
CIAgent supports multi-project workflows with `--project` flags:
```bash
# Initialize multiple projects
ciagent projects add task-api "Task API"
ciagent projects add auth-svc "Auth Service"
# Run ideation across all projects
ciagent ideate --project all
# Run pipeline for a specific project
ciagent run --project task-api
# Run pipeline across all projects
ciagent run --project all
```
Commit messages include project tracking in `---ci---` blocks:
```
---ci---
phase: 5
milestone: v0.10
project: task-api
status: execute
---/ci---
```
### Verification Layers
1. **Structural**: File existence, import/export wiring, no stubs
+5 -4
View File
@@ -4,7 +4,7 @@ Agent output guidance for CIAgent dev mode. Loaded when the orchestrator operate
---
## Multi-Project and NFR Versioning
## Multi-Project and Milestone Versioning
When in multi-project mode (`.ciagent/config.json` has `projects[]` with length > 0):
- All commits include `project: <slug>` in `---ci---` block
@@ -12,9 +12,10 @@ When in multi-project mode (`.ciagent/config.json` has `projects[]` with length
- `.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
Milestone versioning (determined by `getMilestoneType()` before any development):
- **NFR** (all phases: fix/chore/docs/perf/refactor/test): progressive patch versions, no milestone tag — final patch IS the deliverable
- **Feature** (at least one `feat` phase): progressive patch versions + next minor milestone tag
- **Major** (breaking schema changes or complete refactor): progressive minor versions per phase + major milestone tag
## Output Style
+33 -15
View File
@@ -104,22 +104,29 @@ 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:
**Every merge to main creates a release. No exceptions.** Versioning follows the milestone type model:
### 3-Tier Versioning Model
### Milestone Type and Versioning
The milestone type is determined **before any development work** and governs all versioning for the entire milestone.
**Define semver at milestone start:** establish the version and milestone type before writing code.
Determine milestone type via `getMilestoneType()` which returns `"nfr" | "feature" | "major"`:
| 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` |
| **NFR** | All phases are fix/chore/docs/perf/refactor/test | Patch `v1.8.1`, `v1.8.2`, ... | None — final patch IS the deliverable |
| **Feature** | At least one phase has new features (`feat`) | Patch `v1.8.1`, `v1.8.2`, ... | Next minor — `v1.9.0` |
| **Major** | Breaking schema changes or complete refactor | Minor — `v2.1.0`, `v2.2.0`, ... | Major — `v3.0.0` |
**IMPORTANT:** Milestone tags are always the NEXT version, never the base:
- Feature: patches v0.5.1v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
- Schema-breaking: minors v0.3.0, v0.4.0, v0.5.0 → milestone tag is v1.0.0
- NFR: no milestone tag — the milestone is implicit from the patch sequence
Determine milestone type via `getMilestoneType()` which returns `"nfr" | "feature" | "schema-breaking"`.
**Tag rules (CRITICAL):**
- Milestone tags are always the NEXT version, never the base:
- Feature: patches v0.5.1v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
- Major: minors v0.3.0, v0.4.0, v0.5.0 → milestone tag is v1.0.0
- NFR: no milestone tag — the final patch release IS the deliverable
- Tags must be strictly greater than all existing tags on the same major.minor line
- NEVER create a milestone tag that is semantically below existing phase tags
### Phase completion
@@ -135,7 +142,7 @@ git push origin main --tags
Phase number within the milestone determines the patch version (1st phase = .1, 2nd phase = .2, etc.)
**Schema-breaking (minor release per phase):**
**Major (minor release per phase):**
```bash
git checkout milestone/v0.5-schema-rewrite
git merge --squash phase/01-core-refactor
@@ -145,7 +152,7 @@ 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.
Each major phase bumps the minor. 1st phase = next available minor, 2nd = minor+1, etc.
### Milestone completion
@@ -160,7 +167,7 @@ git push origin main --tags
# Create Gitea release for v0.6.0 with full milestone summary
```
**Schema-breaking (major release):**
**Major (major release):**
```bash
# All phases already merged into milestone branch
git checkout main
@@ -177,9 +184,20 @@ git push origin main --tags
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)
2. Milestone completion tag must be next minor (feature) or next major (major)
3. NEVER create a tag that is semantically below existing phase tags
### Merge Validation Gates
The branch hierarchy `main > milestone/vX.X-slug > phase/NN-slug` is enforced at merge time:
| Merge Type | Rule | Validation |
|------------|------|-------------|
| Phase → Milestone | Must target milestone branch when one exists | REJECTED if milestone branch does not exist for this phase's milestone |
| Phase → Main | Only allowed when no milestone branch exists | REJECTED if a milestone branch exists for this milestone |
| Milestone → Main | Only after all phase branches are merged | REJECTED if any phase branches for this milestone are unmerged |
| Hotfix → Main | Allowed (exception to hierarchy) | Always allowed |
## Multi-Project Branch Naming
When operating in multi-project mode (`.ciagent/config.json` has `projects[]` with length > 0):
+288
View File
@@ -0,0 +1,288 @@
---
description: Run the CIAgent ideation pipeline — analyze project for improvement opportunities, validate recommendations with user, update long-term documents
---
# CIAgent Ideate
Run the CIAgent ideation engine to discover improvement opportunities based on git-native signals, codebase analysis, and cross-project patterns.
**Usage:** `ciagent ideate [options]`
## Step 0: Confirm Active Project
Check `ci listProjects()` or read `.ciagent/config.json` to determine project context.
If `.ciagent/config.json` has `active_projects` array with length > 0:
- Use `--project <slug>` to target a specific project
- Use `--project all` to run ideation across all active projects (deduplicate findings)
- If no `--project` flag, use first project in `active_projects`
If `.ciagent/config.json` has `active_project` string (legacy):
- Use that project as the target
- Backwards-compatible: if both `active_project` and `active_projects` exist, `active_projects` takes precedence
## Step 1: Load Project Context
```bash
git log --max-count=50
git branch -a
```
Read project reference files:
- `.ciagent/PROJECT.md` — Vision, requirements, constraints, key decisions
- `.ciagent/ROADMAP.md` — Phases, milestones, success criteria
- `.ciagent/REQUIREMENTS.md` — REQ-IDs, status, traceability
- `.ciagent/ARCHITECTURE.md` — Component boundaries, data flow
- `.ciagent/config.json` — Ideation configuration, autonomy level
## Step 2: Run Ideation Tiers
Execute tiers in order. Each tier produces `Idea[]` objects. Ideas from all tiers are merged and deduplicated before presentation.
### Tier 1: Mechanical Analysis (Always Available)
No backend required. All signals come from git history, `.ciagent/` files, and filesystem.
#### 2.1 Git-Native Pattern Mining
```bash
git log --all --grep="lessons:" --format="%B" -50
git log --all --grep="decisions:" --format="%B" -50 -- "***confidence***0.*"
git log --all --grep="escalation:" --format="%B" -50
git log --all --grep="compound:" --format="%B" -50
```
Extract:
- **Repeated lessons** — topics appearing > 1 time → systemic issue
- **Low-confidence decisions** — confidence < 0.7 in `---ci---` blocks → improvement targets
- **Escalation types** — each type identifies a process gap
- **Compound solutions** — suggest generalizing patterns that were solved multiple times
- **Partial requirements** — `requirements: partial: [REQ-XX]` in `---ci---` blocks
#### 2.2 Coverage Gap Analysis
- Parse REQUIREMENTS.md for `pending` and `in_progress` status requirements
- Cross-reference with PLAN.md task completion
- Identify requirements with no corresponding implementation tasks
#### 2.3 Verification Layer Inversion
For each verification layer, identify what's MISSING:
- **Structural**: Files referenced but not created, stubs, TODOs, placeholder implementations
- **Behavioral**: Test suites with < 80% coverage, missing test files for covered requirements
- **Security**: No STRIDE analysis for modified components, missing input validation patterns
- **Quality**: P1/P2 review findings unresolved, consistent style violations
#### 2.4 Architectural Drift Detection
- Parse ARCHITECTURE.md component tree
- Compare against actual `src/` directory structure
- Flag components documented but not implemented
- Flag components implemented but not documented
- Check import graph for unauthorized dependencies between components
#### 2.5 Spec-Driven Improvement
- Analyze REQUIREMENTS.md for ambiguous language ("should" vs "must", undefined terms)
- Check for contradictions between requirements
- Compare against common patterns for the project type (identified from package.json keywords)
- Flag requirements with no verification criteria
### Tier 2: Backend-Enriched Analysis (When LLM Available)
Requires an intelligence backend (opencode, openai, anthropic, or ollama).
#### 2.6 Prioritization and Ranking
- Evaluate all mechanical findings for impact and feasibility
- Rank ideas by: (1) number of signals corroborating, (2) severity of the gap, (3) ease of addressing
#### 2.7 Novel Improvement Suggestions
- Suggest improvements beyond pattern matching (e.g., "consider rate limiting" based on industry best practices, not just a repeated lesson)
- Generate concrete action plans for each accepted idea
- Identify bleeding-edge approaches relevant to the project's tech stack
#### 2.8 Chaos Engineering Ideation
- Generate failure scenarios: "What if the backend is unavailable?", "What if a requirement changes mid-implementation?", "What if test coverage drops below threshold?"
- Map failure scenarios to code that would break
- Suggest resilience improvements for each scenario
### Tier 3: Cross-Project Pattern Transfer (When Multi-Project Registry Exists)
#### 2.9 Cross-Project Mining
For each project in `.ciagent/config.json` projects array:
- Read that project's `---ci---` blocks for lessons, decisions, compound solutions
- Find patterns relevant to the current project (same requirement area, same tech stack from package.json)
- Suggest adaptations of lessons learned elsewhere
- Calculate relevance score based on tech stack similarity
## Step 3: Merge and DeduplicateIdeas
Combine ideas from all tiers. Deduplicate by:
- Same `title` strings → keep highest confidence version
- Same `relatedReq` → merge into single idea with combined sources
- Same `category` + overlapping domains → keep most specific
Sort by confidence (descending), then by number of corroborating signals.
## Step 4: Interactive Validation
Present ideas one-at-a-time to the user:
```
═══ Recommendation N of M ═══
Category: [CATEGORY] | Confidence: [0.XX] | Tier: [mechanical/backend-enriched/cross-project]
Title: [idea title]
Rationale: [idea rationale]
Related Req: [REQ-ID or "new requirement"]
Source: [source signal type]
Actions:
1. Accept (add to next milestone as new requirement)
2. Skip
3. Modify (edit title/rationale before accepting)
4. Details (show full analysis including signal sources)
```
For each accepted idea:
1. Generate `IDEATE-NN` requirement ID
2. Prompt for milestone placement (append to existing or create new)
3. Add to REQUIREMENTS.md with status `pending`
4. Add to ROADMAP.md next milestone
## Step 5: Update Long-Term Documents
For each accepted idea:
### REQUIREMENTS.md
Add a new row in the appropriate milestone section:
```
| IDEATE-NN | [idea title] | [priority] | [phase] | pending |
```
### ROADMAP.md
Add the idea to the next milestone's phase structure:
- If next milestone has a matching phase category, append to that phase
- If no matching phase, suggest a new phase
### ARCHITECTURE.md
If the idea involves architectural changes, note the component change needed.
### PROJECT.md
If the idea adds new requirements or key decisions, update accordingly.
Commit all document updates:
```
decision(P##): ideation results — [N] accepted, [M] skipped
```
## Step 6: Ask-After-Validation Kickoff
After all ideas have been validated:
```
Accepted: [N] recommendations
Skipped: [M] recommendations
Would you like to kick off the run workflow for these ideas? (y/n)
```
If yes: Start `ciagent run` with the updated project context. The `--ideate` flag is NOT needed because the ideas are already in ROADMAP.md and REQUIREMENTS.md — the standard pipeline will pick them up.
If no: Output summary and exit.
## Command Flags
| Flag | Description |
|------|-------------|
| `--category <cats>` | Focus on specific categories: security,quality,architecture,coverage,improvement,spec,chaos (comma-separated) |
| `--affected` | Cascade impact analysis: given current changes, what else needs updating |
| `--spec` | Analyze specification completeness and ambiguity |
| `--external` | Include external signals: npm audit, OSV advisories, dependency staleness |
| `--cross-project` | Mine patterns from all projects in multi-project registry |
| `--output <format>` | Output format: interactive (default), json, markdown |
| `--project <slugs>` | Target project(s): slug, comma-separated, or `all` |
| `--backend <provider>` | Override intelligence backend for enrichment tier |
## Pipeline Integration
When `ciagent run --ideate` is used, the IDEATE stage is inserted between RESEARCH and PLAN:
```
SPECIFY → CLARIFY → RESEARCH → IDEATE → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
```
IDEATE stage commit:
```
---ci---
phase: [phase-number]
milestone: [milestone-version]
status: ideate
decisions:
- id: D-XXX
decision: "Accepted [N] ideation recommendations"
rationale: "[summary of accepted ideas]"
confidence: [avg confidence]
requirements:
covered: [IDEATE-NN, ...]
---/ci---
```
## Output Modes
### Interactive (default)
Presented one-at-a-time with accept/skip/modify actions.
### JSON
```json
{
"project": "[slug]",
"milestone": "[version]",
"ideas": [
{
"id": "IDEATE-NN",
"source": "[source type]",
"category": "[category]",
"title": "[title]",
"rationale": "[rationale]",
"confidence": 0.XX,
"relatedReq": "[REQ-ID or null]",
"actions": ["[action types]"],
"tier": "[mechanical/backend-enriched/cross-project]",
"accepted": true
}
],
"summary": {
"total": 8,
"accepted": 6,
"skipped": 2,
"by_category": { "coverage": 2, "architecture": 1, "security": 1, "quality": 1, "improvement": 1 }
}
}
```
### Markdown
Formatted report suitable for PR descriptions or documentation.
## Error Recovery
On tier failure:
1. Mechanical tier always succeeds (git + filesystem only)
2. Backend-enriched tier: if backend unavailable, fall back to mechanical-only output
3. Cross-project tier: if no other projects in registry, skip silently
On validation failure (no ideas generated):
- Output "No improvement ideas identified for this project."
- Suggest `ciagent ideate --spec` for specification analysis or `--external` for external signals
+25 -8
View File
@@ -14,11 +14,18 @@ If no phase number specified, continues from the current phase (detected from gi
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>)`
If `.ciagent/config.json` has `projects[]` with length > 0, or `active_projects` array exists:
- Confirm `active_projects` is correct for this run
- If `--project all` is specified: iterate over all projects in `active_projects`
- If `--project <slug>` is specified: run for that project only
- If no `--project` flag: use first project in `active_projects`
- All commit messages must include `project: <slug>` in `---ci---` block
For multi-project execution (`--project all`):
- Execute pipeline for each project sequentially by default
- When `parallelization.enabled=true`: execute projects concurrently up to `max_concurrent_agents`
- Each project has independent phase branches and milestone tracking
If single-project mode: proceed with existing conventions.
## Step 1: Load Git Context
@@ -60,6 +67,15 @@ For each stage in order (starting from current or from `specify`):
- Update `.ciagent/` static files with conclusions
- Commit: `docs(P##): research findings`
### IDEATE (when --ideate flag is passed)
- Delegate to ci-ideation-agent
- Mine git history for patterns, analyze coverage gaps, detect drift
- If backend available: enrich with LLM suggestions
- If --cross-project: mine patterns from other projects
- Present recommendations interactively (accept/skip/modify)
- Accepted ideas update ROADMAP.md and REQUIREMENTS.md
- Commit: `decision(P##): ideation results — [N] accepted, [M] skipped`
### PLAN
- Delegate to ci-planner
- Create vertical-slice plans with wave ordering
@@ -85,7 +101,7 @@ For each stage in order (starting from current or from `specify`):
- 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.
Versioning: Major milestone = breaking schema changes, Feature milestone = milestone completion (minor), Patch = every phase.
## Phase Boundary Checkpoint
@@ -97,12 +113,13 @@ Between phases, perform a context reset:
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
## Versioning Logic
Before tagging a phase completion, check `isNfrMilestone()`:
Before tagging a phase completion, check `getMilestoneType()` which returns `"nfr" | "feature" | "major"`:
- **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).
- **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 — the final patch IS the deliverable.
- **Feature milestone** (at least one feat phase): apply progressive patch versions per phase, then tag next minor milestone version on completion (e.g., v0.6.0, NOT v0.5.0).
- **Major milestone** (breaking schema changes or complete refactor): apply progressive minor versions per phase (v0.3.0, v0.4.0), then tag next major on completion (e.g., v1.0.0).
## Step 4: Error Recovery
+132 -32
View File
@@ -1,25 +1,45 @@
---
description: Ship CIAgent phase or milestone — test, tag, release. Every phase and milestone gets a release. Full autopilot.
description: Ship CIAgent phase or milestone — Full autopilot release: validate, test, merge, tag, push, release. Zero HITL
---
# CIAgent Ship
Ship a CIAgent phase or milestone. Every ship creates a release — no exceptions.
**3-Tier Versioning Model:**
**Usage:** `ciagent-ship [phase_number|milestone]`
## Autopilot Rules
These rules are **non-negotiable**. The ship workflow runs in full autopilot mode:
- **Zero HITL** — no confirmation prompts, no approval gates, no requests for human input. The agent executes the entire release flow autonomously.
- **No Shortcuts** — deep validation, testing, and merge checks must all run in full. The lack of HITL is not an excuse to skip steps.
- **Notification Only** — status updates are informational, not requests for approval. Report outcomes, never ask permission.
- **Autonomous Loop on Failure** — if any step fails (tests, pipeline, merge conflicts), iterate autonomously until success. Do NOT ask the user for guidance on how to fix a failing test or pipeline.
- **Branch Hierarchy Enforced** — `main > milestone/vX.X-slug > phase/NN-slug`. Phase merges into milestone, milestone merges into main. This is validated, not assumed.
## Milestone Type and Versioning
The milestone type is determined **before any development work** and governs all versioning for the entire milestone.
**Define semver at milestone start:** establish the version and milestone type before writing code.
Determine milestone type by calling `getMilestoneType()` which returns `"nfr" | "feature" | "major"`:
| 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` |
| **NFR** | All phases are fix/chore/docs/perf/refactor/test | Patch `v1.8.1`, `v1.8.2`, ... | None — final patch IS the deliverable |
| **Feature** | At least one phase has new features (`feat`) | Patch `v1.8.1`, `v1.8.2`, ... | Next minor — `v1.9.0` |
| **Major** | Breaking schema changes or complete refactor | Minor — `v2.1.0`, `v2.2.0`, ... | Major — `v3.0.0` |
**CRITICAL:** Milestone tags are always the NEXT version, never the base:
- Feature: patches v0.5.1v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
- Schema-breaking: minors v0.3.0, v0.4.0, v0.5.0 → milestone tag is v1.0.0
- NFR: no milestone tag — the milestone is implicit from the patch sequence
**Tag rules (CRITICAL):**
**Usage:** `ciagent-ship [phase_number|milestone]`
- Milestone tags are always the NEXT version, never the base:
- Feature: patches v0.5.1v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
- Major: minors v0.3.0, v0.4.0, v0.5.0 → milestone tag is v1.0.0
- NFR: no milestone tag — the final patch release IS the deliverable
- Tags must be strictly greater than all existing tags on the same major.minor line
- NEVER create a milestone tag that is semantically below existing phase tags
## Step 0: Confirm Active Project
@@ -33,11 +53,12 @@ If `.ciagent/config.json` has `projects[]` with length > 0:
If single-project mode: proceed with existing conventions.
## Step 1: Pre-Flight
## Step 1: Pre-Flight Validation
```bash
git log --max-count=10
git branch -a
git tag -l
```
Determine what is being shipped: a single phase or an entire milestone.
@@ -49,6 +70,16 @@ Read `.ciagent/ROADMAP.md` to determine:
Read `.ciagent/config.json` for autonomy level.
**Validation gates — all must pass before proceeding:**
1. **Milestone type resolved**`getMilestoneType()` must return `"nfr" | "feature" | "major"`. Stop if undefined.
2. **Branch hierarchy correct** — phase branch exists and targets the correct parent (milestone branch, or main if no milestone branch exists).
3. **No unmerged phase branches** — if shipping a milestone, all phase branches for this milestone must be merged into the milestone branch.
4. **Tag sequence valid** — the computed tag must be strictly greater than all existing tags on the same major.minor line. Check with `git tag -l`.
5. **Autonomy confirmed**`.ciagent/config.json` autonomy level must be `full`. This is the zero-HITL enforcement point.
If any validation fails: stop and report. Do NOT proceed past a failed gate.
## Step 2: Run Tests
```bash
@@ -59,33 +90,77 @@ 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
## Step 3: Create PR and Quality Assurance
Determine milestone type by calling `getMilestoneType()` which returns `"nfr" | "feature" | "schema-breaking"`:
**Open a Pull Request for the merge target:**
```bash
tea pr create --base <target-branch> --head <source-branch> --title "ship: [phase-name or milestone-name]"
```
- For a phase ship: PR from `phase/NN-slug` into `milestone/vX.Y-slug` (or `main` if no milestone branch).
- For a milestone ship: PR from `milestone/vX.Y-slug` into `main`.
**Auto-merge configuration:**
Set the PR to auto-merge upon pipeline success:
```bash
tea pr merge <pr-number> --auto --squash
```
**Review:**
Conduct a thorough autonomous review of the PR diff. Check:
- All expected files are included
- No unintended changes slipped in
- No secrets or credentials in the diff
- All `---ci---` blocks have correct metadata
**Finalization:**
- **On pipeline success:** the PR auto-merges. Proceed to Step 4.
- **On pipeline failure:** iterate autonomously until the pipeline passes. Do NOT merge a PR with a failing pipeline. Do NOT ask for guidance.
**Strict rule:** Never merge a PR with a failed pipeline. No exceptions.
## Step 4: Compute Version
| 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) |
| Single phase | Major | Minor `vX.(Y+N).0` | N/A | v0.4.0 (2nd major 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 |
| Milestone completion | Major | 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)
- Major: 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)
**Tag 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 (major)
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
## Step 5: 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.
### Merge validation gates
**Phase → Milestone:**
- VALIDATED — must target milestone branch when one exists
- REJECTED if milestone branch does not exist for this phase's milestone
**Phase → Main:**
- VALIDATED — only allowed when NO milestone branch exists for this phase's milestone
- REJECTED if a milestone branch exists for this milestone
**Milestone → Main:**
- VALIDATED — only after all phase branches are merged
- REJECTED if any phase branches for this milestone are unmerged
### Phase ship
@@ -123,8 +198,9 @@ requirements:
### Milestone ship (after last phase)
**Validate all phase branches are merged into the milestone branch before proceeding.**
```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]
@@ -136,7 +212,7 @@ status: complete
---/ci---"
```
## Step 5: Tag and Push
## Step 6: Tag and Push
```bash
git tag -a vX.Y.Z -m "vX.Y.Z: [phase-name or milestone-name]"
@@ -145,21 +221,21 @@ 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`)
- Major 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`)
- Major milestone: next major (`v1.0.0`)
## Step 6: Create Release
## Step 7: Create Release and Package
**Every ship creates a Gitea release. No exceptions.**
Generate release notes from git log:
### Generate release notes
```bash
git log v[previous_tag]..vX.Y.Z --oneline
```
Create the release via Gitea API:
### Create the Gitea release
```bash
curl -X POST "https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/ci/releases" \
@@ -170,14 +246,37 @@ curl -X POST "https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/ci/
For milestone releases, include a summary of all phases completed and requirements covered.
## Step 7: Update .ci/ Files
### Create distribution packages
Use coreci to create the necessary distribution packages:
```bash
coreci build --tag vX.Y.Z
coreci package --tag vX.Y.Z
```
Upload packages to the Gitea release:
```bash
coreci release upload --tag vX.Y.Z --files [built-artifacts]
```
### Generate documentation
Include release notes in the Gitea release body with:
- Summary of changes
- Requirements covered
- Known issues (if any)
- Migration notes (for major milestones)
## Step 8: 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
## Step 9: Report
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -185,7 +284,7 @@ Commit the file updates.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase [N]: [name]
Milestone: [vX.Y] ([nfr|feature|schema-breaking])
Milestone: [vX.Y] ([nfr|feature|major])
Version: vX.Y.Z
Release: https://git.cloudinit.dev/continuous-intelligence/ci/releases/tag/vX.Y.Z
Status: complete
@@ -193,6 +292,7 @@ Status: complete
Tests: PASS
Typecheck: PASS
Build: PASS
Pipeline: PASS
Requirements covered: [N]
Commits: [N]
+2 -2
View File
@@ -1,5 +1,5 @@
---
description: Ship CIAgent phase or milestone — test, commit, tag, push, release. Full autopilot: zero HITL after milestone setup
description: Ship CIAgent phase or milestone — Full autopilot release: validate, test, merge, tag, push, release. Zero HITL
argument-hint: "[phase_number|milestone]"
tools:
read: true
@@ -12,7 +12,7 @@ tools:
---
<execution_context>
@__OPENCODE_DIR__/ci/workflows/ship.md
@/root/.config/opencode/ci/workflows/ship.md
</execution_context>
<context>
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@continuous-intelligence/ciagent",
"version": "0.7.0",
"version": "0.10.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@continuous-intelligence/ciagent",
"version": "0.7.0",
"version": "0.10.0",
"license": "MIT",
"dependencies": {
"commander": "^12.1.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@continuous-intelligence/ciagent",
"version": "0.9.0",
"version": "0.10.0",
"description": "Fully autonomous AI-driven software engineering harness - Continuous Intelligence",
"main": "dist/index.js",
"types": "dist/index.d.ts",
+1
View File
@@ -18,6 +18,7 @@ export interface AgentContext {
specification: string;
config_path: string;
backend?: IntelligenceBackend;
project_slug?: string;
}
export function backendResultToAgentResult(result: BackendResult): AgentResult {
+16 -66
View File
@@ -4,74 +4,24 @@ import * as os from "node:os";
import { IdeationAgent } from "../agents/ideation-agent.js";
describe("IdeationAgent", () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-ideation-test-"));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("generates ideas from uncovered requirements", () => {
const ciagentDir = path.join(tempDir, ".ciagent");
fs.mkdirSync(ciagentDir, { recursive: true });
fs.writeFileSync(
path.join(ciagentDir, "REQUIREMENTS.md"),
"REQ-1: First requirement\nREQ-2: Second requirement"
);
const agent = new IdeationAgent();
const ideas = agent.mechanicalIdeate(tempDir);
const reqIdeas = ideas.filter((i) => i.source === "uncovered_requirement");
expect(reqIdeas.length).toBeGreaterThanOrEqual(2);
expect(reqIdeas.some((i) => i.relatedReq === "REQ-1")).toBe(true);
});
it("identifies coverage gaps from PROJECT.md", () => {
const ciagentDir = path.join(tempDir, ".ciagent");
fs.mkdirSync(ciagentDir, { recursive: true });
fs.writeFileSync(
path.join(ciagentDir, "PROJECT.md"),
"We use agent: magic-agent and agent: super-agent for tasks."
);
const srcDir = path.join(tempDir, "src", "agents");
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, "base.ts"), "");
fs.writeFileSync(path.join(srcDir, "index.ts"), "");
const agent = new IdeationAgent();
const gaps = agent.identifyCoverageGaps(tempDir);
expect(gaps).toContain("magic-agent");
expect(gaps).toContain("super-agent");
});
it("finds repeated patterns from lessons list", () => {
const agent = new IdeationAgent();
const lessons = [
{ topic: "testing", detail: "testing: tests are flaky" },
{ topic: "testing", detail: "testing: more test failures" },
{ topic: "build", detail: "build: CI broken" },
];
const repeated = agent.findRepeatedPatterns(lessons);
expect(repeated).toContain("testing");
expect(repeated).not.toContain("build");
});
it("returns empty ideas when no project files exist", () => {
const agent = new IdeationAgent();
const ideas = agent.mechanicalIdeate(tempDir);
expect(ideas).toEqual(expect.arrayContaining([]));
});
it("agent name is ideation-agent", () => {
const agent = new IdeationAgent();
expect(agent.name).toBe("ideation-agent");
});
it("workflow is research", () => {
const agent = new IdeationAgent();
expect(agent.workflow).toBe("research");
});
it("delegates mechanicalIdeate to IdeationEngine", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-agent-test-"));
try {
const agent = new IdeationAgent();
const ideas = agent.mechanicalIdeate(tempDir);
expect(Array.isArray(ideas)).toBe(true);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
});
+8 -161
View File
@@ -1,18 +1,9 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
interface Idea {
source: "uncovered_requirement" | "repeated_lesson" | "gap_in_coverage" | "improvement_pattern";
title: string;
rationale: string;
confidence: number;
relatedReq?: string;
}
import { IdeationEngine } from "../core/ideation.js";
export class IdeationAgent extends BaseAgent {
readonly name = "ideation-agent";
readonly description = "Generates improvement ideas. Output feeds directly into planning pipeline.";
readonly description = "Generates improvement ideas using git-native pattern mining, coverage gap analysis, and architectural drift detection. Output feeds directly into planning pipeline.";
readonly workflow = "research";
async execute(context: AgentContext): Promise<AgentResult> {
@@ -27,8 +18,9 @@ export class IdeationAgent extends BaseAgent {
return { ...result, duration_ms: Date.now() - start };
}
const ideas = this.mechanicalIdeate(context.project_path);
const output = this.formatIdeas(ideas);
const engine = new IdeationEngine(context.project_path);
const ideas = engine.runMechanical();
const output = engine.formatIdeas(ideas);
return {
success: true,
@@ -40,153 +32,8 @@ export class IdeationAgent extends BaseAgent {
};
}
mechanicalIdeate(projectPath: string): Idea[] {
const ideas: Idea[] = [];
const uncoveredReqs = this.readUncoveredRequirements(projectPath);
const lessons = this.readRecentLessons(projectPath);
const repeated = this.findRepeatedPatterns(lessons);
const coverageGaps = this.identifyCoverageGaps(projectPath);
for (const req of uncoveredReqs) {
ideas.push({
source: "uncovered_requirement",
title: `Address uncovered requirement: ${req}`,
rationale: `Requirement ${req} has no corresponding implementation task.`,
confidence: 0.8,
relatedReq: req,
});
}
for (const topic of repeated) {
ideas.push({
source: "repeated_lesson",
title: `Investigate repeated lesson: ${topic}`,
rationale: `Topic "${topic}" appears in multiple commit lessons, indicating a systemic issue.`,
confidence: 0.7,
});
}
for (const gap of coverageGaps) {
ideas.push({
source: "gap_in_coverage",
title: `Fill coverage gap: ${gap}`,
rationale: `Agent "${gap}" is claimed in PROJECT.md but not found in the agent registry.`,
confidence: 0.75,
});
}
this.generateIdeas(uncoveredReqs, repeated, ideas);
return ideas;
}
readUncoveredRequirements(projectPath: string): string[] {
const reqPath = path.join(projectPath, ".ciagent", "REQUIREMENTS.md");
if (!fs.existsSync(reqPath)) return [];
const content = fs.readFileSync(reqPath, "utf-8");
const reqIds: string[] = [];
const reqIdRegex = /REQ-(\d+)/g;
let match;
while ((match = reqIdRegex.exec(content)) !== null) {
reqIds.push(`REQ-${match[1]}`);
}
const planPath = path.join(projectPath, ".ciagent", "PLAN.md");
if (!fs.existsSync(planPath)) return reqIds;
const planContent = fs.readFileSync(planPath, "utf-8");
const coveredReqIds = new Set<string>();
const planRegex = /REQ-(\d+)/g;
let planMatch;
while ((planMatch = planRegex.exec(planContent)) !== null) {
coveredReqIds.add(`REQ-${planMatch[1]}`);
}
return reqIds.filter((id) => !coveredReqIds.has(id));
}
readRecentLessons(projectPath: string): Array<{ topic: string; detail: string }> {
const lessons: Array<{ topic: string; detail: string }> = [];
try {
const { execSync } = require("node:child_process");
const log = execSync('git log --grep="lessons:" --format="%B" -50', {
cwd: projectPath,
encoding: "utf-8",
timeout: 5000,
});
const lessonsRegex = /lessons:\s*\n((?:\s+-\s+.+\n?)+)/g;
let match;
while ((match = lessonsRegex.exec(log)) !== null) {
const items = match[1].split("\n").filter((l: string) => l.trim().startsWith("-"));
for (const item of items) {
const detail = item.replace(/^\s*-\s*/, "").trim();
const topic = detail.split(":")[0].trim().toLowerCase();
lessons.push({ topic, detail });
}
}
} catch {}
return lessons;
}
findRepeatedPatterns(lessons: Array<{ topic: string; detail: string }>): string[] {
const counts: Record<string, number> = {};
for (const lesson of lessons) {
counts[lesson.topic] = (counts[lesson.topic] || 0) + 1;
}
return Object.entries(counts)
.filter(([, count]) => count > 1)
.map(([topic]) => topic);
}
generateIdeas(uncoveredReqs: string[], repeated: string[], ideas: Idea[]): void {
const repeatedSet = new Set(repeated.map((r) => r.toLowerCase()));
for (const req of uncoveredReqs) {
for (const topic of repeated) {
if (req.toLowerCase().includes(topic) || topic.includes(req.toLowerCase())) {
ideas.push({
source: "improvement_pattern",
title: `Cross-reference: ${req}${topic}`,
rationale: `Repeated lesson "${topic}" directly relates to uncovered requirement ${req}.`,
confidence: 0.85,
relatedReq: req,
});
}
}
}
}
identifyCoverageGaps(projectPath: string): string[] {
const projectMdPath = path.join(projectPath, ".ciagent", "PROJECT.md");
if (!fs.existsSync(projectMdPath)) return [];
const content = fs.readFileSync(projectMdPath, "utf-8");
const agentMentionRegex = /(?:agent|Agent):\s*(\S+)/g;
const mentionedAgents: string[] = [];
let match;
while ((match = agentMentionRegex.exec(content)) !== null) {
mentionedAgents.push(match[1]);
}
const agentsDir = path.join(projectPath, "src", "agents");
if (!fs.existsSync(agentsDir)) return mentionedAgents;
const existingAgents = new Set(
fs.readdirSync(agentsDir)
.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts") && f !== "index.ts" && f !== "base.ts")
.map((f) => f.replace(".ts", ""))
);
return mentionedAgents.filter((a) => !existingAgents.has(a) && !existingAgents.has(a.replace(/-agent$/, "")));
}
private formatIdeas(ideas: Idea[]): string {
if (ideas.length === 0) return "No improvement ideas generated.";
const lines: string[] = ["Improvement Ideas:", ""];
for (const idea of ideas) {
lines.push(`[${idea.source}|${idea.confidence.toFixed(2)}] ${idea.title}${idea.rationale}${idea.relatedReq ? ` (req: ${idea.relatedReq})` : ""}`);
}
return lines.join("\n");
mechanicalIdeate(projectPath: string) {
const engine = new IdeationEngine(projectPath);
return engine.runMechanical();
}
}
+162 -1
View File
@@ -47,6 +47,7 @@ export class OrchestratorAgent extends BaseAgent {
private static readonly STAGE_AGENT_MAP: Partial<Record<PipelineStage, AgentName[]>> = {
research: ["researcher"],
ideate: ["ideation-agent"],
plan: ["planner"],
execute: ["executor", "code-reviewer", "security-auditor"],
test: ["tester"],
@@ -67,9 +68,10 @@ export class OrchestratorAgent extends BaseAgent {
try {
this.config = loadConfig(context.project_path);
const projectSlug = context.project_slug || "";
this.gitContext = new GitContext(context.project_path);
this.gitBranch = new GitBranch(context.project_path);
this.ciFiles = new CIAgentFiles(context.project_path);
this.ciFiles = new CIAgentFiles(context.project_path, projectSlug || undefined);
this.ciFiles.ensureCIDir();
const projectState = this.gitContext.reconstructState();
@@ -459,6 +461,7 @@ export class OrchestratorAgent extends BaseAgent {
projectName: spec.objective.slice(0, 30),
phaseCount: 0,
milestone: this.currentMilestone,
project: context.project_slug || undefined,
specification: spec.raw_content,
requirements: spec.requirements,
constraints: spec.constraints,
@@ -571,6 +574,69 @@ export class OrchestratorAgent extends BaseAgent {
break;
}
case "ideate": {
this.log("Running ideation stage...");
const { IdeationEngine } = await import("../core/ideation.js");
const ideationEngine = new IdeationEngine(context.project_path, context.project_slug || undefined);
const ideas = ideationEngine.runMechanical();
const ideationConfig = this.config.ideation;
if (ideationConfig?.categories && ideationConfig.categories.length > 0) {
const categoryIdeas = ideationEngine.runMechanical(ideationConfig.categories);
const seenTitles = new Set(ideas.map((i) => i.title));
for (const idea of categoryIdeas) {
if (!seenTitles.has(idea.title)) {
ideas.push(idea);
seenTitles.add(idea.title);
}
}
}
ideas.sort((a, b) => b.confidence - a.confidence);
const maxIdeas = ideationConfig?.max_ideas || 20;
const trimmedIdeas = ideas.slice(0, maxIdeas);
if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) {
const { accepted: savedIdeas, results } = ideationEngine.acceptIdeas(trimmedIdeas);
const savedCount = results.filter((r) => r.addedToRequirements || r.addedToRoadmap).length;
const ideationCommit = CommitBuilder.buildTaskCommit({
type: "decision",
phase: this.pipelineState!.current_phase,
milestone: this.currentMilestone,
project: context.project_slug || undefined,
plan: "ideation",
task: "ideation-results",
subject: `ideation results — ${trimmedIdeas.length} total, ${savedCount} accepted`,
status: "ideate",
decisions: savedIdeas.map((idea) => ({
id: idea.id,
decision: idea.title,
rationale: idea.rationale,
confidence: idea.confidence,
alternatives: idea.actions,
})),
});
try {
execSync(`git add -A && git commit -m "${ideationCommit.replace(/"/g, '\\"')}" --allow-empty`, {
cwd: context.project_path,
stdio: "pipe",
});
} catch (err) {
this.warn(`Ideation commit failed: ${err instanceof Error ? err.message : String(err)}`);
}
artifactsCreated.push(".ciagent/REQUIREMENTS.md", ".ciagent/ROADMAP.md");
decisionsMade += savedCount;
}
this.pipelineState!.ideate_completed = true;
this.log(`Ideation stage complete: ${trimmedIdeas.length} ideas generated`);
break;
}
case "plan":
this.log("Planning phase execution...");
@@ -790,4 +856,99 @@ export class OrchestratorAgent extends BaseAgent {
return lines.join("\n");
}
async runForProject(projectSlug: string, context: AgentContext): Promise<AgentResult> {
this.log(`Running pipeline for project: ${projectSlug}`);
this.ciFiles = new CIAgentFiles(context.project_path, projectSlug);
this.ciFiles.ensureCIDir();
this.ciFiles.setProjectSlug(projectSlug);
const projectContext: AgentContext = {
...context,
project_path: context.project_path,
};
const result = await this.execute(projectContext);
return {
...result,
output: result.output ? `[${projectSlug}] ${result.output}` : result.output,
};
}
async runForAllProjects(context: AgentContext): Promise<Record<string, AgentResult>> {
const config = loadConfig(context.project_path);
const ciFiles = new CIAgentFiles(context.project_path);
const projects = ciFiles.listProjects();
const activeProjects: string[] = config.active_projects?.length > 0
? config.active_projects
: projects.map((p) => p.slug);
if (activeProjects.length === 0) {
this.log("No active projects found; running for default project");
const result = await this.execute(context);
return { default: result };
}
this.log(`Running pipeline for ${activeProjects.length} project(s): ${activeProjects.join(", ")}`);
const results: Record<string, AgentResult> = {};
const maxConcurrent = config.parallelization?.max_concurrent_projects ?? 3;
const parallel = config.parallelization?.enabled && activeProjects.length > 1;
if (parallel) {
const limitedConcurrency = Math.min(maxConcurrent, activeProjects.length);
const batches: string[][] = [];
for (let i = 0; i < activeProjects.length; i += limitedConcurrency) {
batches.push(activeProjects.slice(i, i + limitedConcurrency));
}
for (const batch of batches) {
const batchResults = await Promise.allSettled(
batch.map(async (slug): Promise<[string, AgentResult]> => {
const orchestrator = new OrchestratorAgent(config);
const result = await orchestrator.runForProject(slug, context);
return [slug, result];
})
);
for (const settled of batchResults) {
if (settled.status === "fulfilled") {
const [slug, result] = settled.value;
results[slug] = result;
} else {
this.warn(`Project pipeline failed: ${settled.reason instanceof Error ? settled.reason.message : String(settled.reason)}`);
}
}
}
} else {
for (const slug of activeProjects) {
this.log(`Processing project: ${slug}`);
const orchestrator = new OrchestratorAgent(config);
orchestrator.ciFiles = new CIAgentFiles(context.project_path, slug);
orchestrator.ciFiles.ensureCIDir();
orchestrator.ciFiles.setProjectSlug(slug);
try {
const result = await orchestrator.runForProject(slug, context);
results[slug] = result;
} catch (err) {
this.warn(`Failed for project ${slug}: ${err instanceof Error ? err.message : String(err)}`);
results[slug] = {
success: false,
output: `Pipeline failed for project ${slug}`,
artifacts_created: 0,
decisions: 0,
escalations: 0,
duration_ms: 0,
error: err instanceof Error ? err.message : String(err),
};
}
}
}
return results;
}
}
+442 -11
View File
@@ -1,5 +1,6 @@
import { Command } from "commander";
import { CIAgentConfig, AutonomyLevel } from "../types/config.js";
import { IdeationCategory, Idea } from "../types/ideation.js";
import { initCIAgent, loadConfig, isCIAgentInitialized, saveConfig } from "../core/config.js";
import { Specification, parseSpecification } from "../types/specification.js";
import { saveSpecification } from "../core/clarify.js";
@@ -9,7 +10,7 @@ import { getAuditSummary, readAudit } from "../core/audit.js";
import { VerificationPipeline } from "../verification/index.js";
import { ClarifyPhase } from "../core/clarify.js";
import { loadSpecification as loadSpec } from "../core/clarify.js";
import { AgentContext } from "../agents/base.js";
import { AgentContext, AgentResult } from "../agents/base.js";
import { ErrorRecovery } from "../core/error-recovery.js";
import { PipelineState, createInitialPipelineState } from "../types/pipeline.js";
import { resolveBackend } from "../backends/index.js";
@@ -19,6 +20,7 @@ import { CIAgentFiles } from "../core/ciagent-files.js";
import { GiteaClient, generateReleaseNotes } from "../core/gitea.js";
import * as fs from "node:fs";
import * as path from "node:path";
import * as readline from "node:readline";
import { execSync } from "node:child_process";
export function createInitCommand(): Command {
@@ -77,6 +79,7 @@ export function createInitCommand(): Command {
enabled: options.parallel !== false,
max_concurrent_agents: 5,
min_plans_for_parallel: 2,
max_concurrent_projects: 3,
},
backend: {
provider: options.backend || "auto",
@@ -167,6 +170,8 @@ export function createRunCommand(): Command {
.option("--all", "Execute all remaining phases sequentially")
.option("--phase <number>", "Phase number", "1")
.option("--backend <provider>", "Override intelligence backend for this run")
.option("--ideate", "Insert ideation stage between research and plan")
.option("--project <slug>", "Target project slug (comma-separated or 'all')")
.action(async (phase, options) => {
const projectPath = process.cwd();
@@ -176,6 +181,141 @@ export function createRunCommand(): Command {
}
const config = loadConfig(projectPath);
const ciFiles = new CIAgentFiles(projectPath);
const runForAllProjects = options.project === "all" || (Array.isArray(config.active_projects) && config.active_projects.length > 1 && !options.project);
if (runForAllProjects) {
console.log("─── Running pipeline across all active projects ───\n");
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, ".ciagent", "config.json"),
backend: undefined,
};
const spec = loadSpec(projectPath);
if (spec) {
context.specification = spec.raw_content;
}
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
if (backend) {
context.backend = backend;
} else if (backendError) {
console.warn(` ⚠ No intelligence backend available: ${backendError}`);
console.warn(" Continuing with mechanical-only execution (limited functionality).");
}
const results = await orchestrator.runForAllProjects(context);
console.log("\n─── Multi-Project Pipeline Results ───\n");
let allSuccess = true;
for (const [slug, result] of Object.entries(results)) {
const icon = result.success ? "✓" : "✗";
console.log(` ${icon} ${slug}: ${result.success ? "success" : result.error || "failed"}`);
if (!result.success) allSuccess = false;
}
if (!allSuccess) {
process.exit(1);
}
return;
}
let projectSlug: string | undefined;
if (options.project && options.project !== "all") {
const slugs = options.project.split(",").map((s: string) => s.trim()).filter(Boolean);
projectSlug = slugs[0];
if (slugs.length > 1) {
console.log("─── Running pipeline across multiple projects ───\n");
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, ".ciagent", "config.json"),
backend: undefined,
};
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
if (backend) {
context.backend = backend;
} else if (backendError) {
console.warn(` ⚠ No intelligence backend available: ${backendError}`);
}
const allResults: Record<string, AgentResult> = {};
for (const slug of slugs) {
console.log(`\nProcessing project: ${slug}`);
const projOrchestrator = new OrchestratorAgent(config);
const result = await projOrchestrator.runForProject(slug, context);
allResults[slug] = result;
}
console.log("\n─── Multi-Project Pipeline Results ───\n");
let allSuccess = true;
for (const [slug, result] of Object.entries(allResults)) {
const icon = result.success ? "✓" : "✗";
console.log(` ${icon} ${slug}: ${result.success ? "success" : result.error || "failed"}`);
if (!result.success) allSuccess = false;
}
if (!allSuccess) {
process.exit(1);
}
return;
}
}
if (options.ideate) {
console.log("─── CIAgent Ideate (pipeline mode) ───\n");
const currentSlug = projectSlug || ciFiles.getProjectSlug() || ciFiles.getActiveProject() || "default";
const { IdeationEngine } = await import("../core/ideation.js");
const engine = new IdeationEngine(projectPath, currentSlug);
const ideas = engine.runMechanical();
const ideaCategory: IdeationCategory[] = options.category
? options.category.split(",").map((c: string) => c.trim() as IdeationCategory)
: [];
if (ideaCategory.length > 0) {
const filtered = engine.runMechanical(ideaCategory);
ideas.push(...filtered);
}
const seen = new Set<string>();
const uniqueIdeas = ideas.filter((idea: Idea) => {
if (seen.has(idea.title)) return false;
seen.add(idea.title);
return true;
});
uniqueIdeas.sort((a: Idea, b: Idea) => b.confidence - a.confidence);
console.log(`Found ${uniqueIdeas.length} improvement ${uniqueIdeas.length === 1 ? "idea" : "ideas"} from ideation stage.\n`);
if (uniqueIdeas.length > 0) {
const { accepted: savedIdeas, results } = engine.acceptIdeas(uniqueIdeas);
const savedCount = results.filter((r: { addedToRequirements: boolean; addedToRoadmap: boolean }) => r.addedToRequirements || r.addedToRoadmap).length;
if (savedCount > 0) {
console.log(`${savedCount} idea${savedCount === 1 ? "" : "s"} added to REQUIREMENTS.md and ROADMAP.md.`);
}
const commitMsg = `decision(P${options.phase || 1}): ideation results — ${uniqueIdeas.length} total, ${savedCount} accepted`;
console.log(`\nCommit suggestion: ${commitMsg}`);
}
}
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
if (!backend && backendError) {
@@ -191,6 +331,7 @@ export function createRunCommand(): Command {
specification: "",
config_path: path.join(projectPath, ".ciagent", "config.json"),
backend,
project_slug: projectSlug || undefined,
};
const spec = loadSpec(projectPath);
@@ -198,7 +339,7 @@ export function createRunCommand(): Command {
context.specification = spec.raw_content;
}
console.log(`Running CIAgent pipeline...`);
console.log(`Running CIAgent pipeline${projectSlug ? ` for project: ${projectSlug}` : ""}...`);
if (options.all) {
console.log(" Mode: Full pipeline (all phases)");
} else {
@@ -412,7 +553,8 @@ export function createReviewCommand(): Command {
export function createStatusCommand(): Command {
return new Command("status")
.description("Non-interactive project status")
.action(() => {
.option("--project <slug>", "Show status for specific project (comma-separated or 'all')")
.action((options) => {
const projectPath = process.cwd();
if (!isCIAgentInitialized(projectPath)) {
@@ -422,14 +564,31 @@ export function createStatusCommand(): Command {
}
const config = loadConfig(projectPath);
const ciFiles = new CIAgentFiles(projectPath);
const artifacts = new ArtifactManager(projectPath);
console.log("─── CIAgent Project Status ───");
console.log(`\nAutonomy: ${config.autonomy.level}`);
const activeProjects: string[] = (config as any).active_projects?.length > 0
? (config as any).active_projects
: config.active_project ? [config.active_project] : [];
console.log("─── CIAgent Project Status ───\n");
if (activeProjects.length > 1 || (options.project && options.project === "all")) {
console.log(`Active Projects: ${activeProjects.join(", ")}`);
console.log(`Total: ${activeProjects.length} projects`);
console.log("");
}
console.log(`Autonomy: ${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 ideationConfig = (config as any).ideation;
if (ideationConfig) {
console.log(`Ideation: ${ideationConfig.enabled ? "enabled" : "disabled"} (categories: ${ideationConfig.categories?.join(", ") || "default"})`);
}
const state = artifacts.readState();
if (state) {
console.log(`\nCurrent Phase: ${state.current_phase}`);
@@ -660,6 +819,9 @@ export function createProjectsCommand(): Command {
const ciFiles = new CIAgentFiles(projectPath);
const projects = ciFiles.listProjects();
const activeProject = config.active_project || ciFiles.getActiveProject();
const activeProjects: string[] = (config as any).active_projects?.length > 0
? (config as any).active_projects
: activeProject ? [activeProject] : [];
if (projects.length === 0) {
console.log("No projects registered.");
@@ -669,11 +831,13 @@ export function createProjectsCommand(): Command {
console.log("─── CIAgent Projects ───\n");
for (const project of projects) {
const isActive = project.slug === activeProject;
const isActive = activeProjects.includes(project.slug);
const marker = isActive ? " *" : "";
console.log(` ${project.slug}${project.name}${marker}`);
}
console.log("\n * = active project");
if (activeProjects.length > 0) {
console.log(`\n Active: ${activeProjects.join(", ")}`);
}
});
cmd.command("add <slug> <name>")
@@ -712,6 +876,7 @@ export function createProjectsCommand(): Command {
ciFiles.setActiveProject(slug);
const config = loadConfig(projectPath);
config.active_project = slug;
(config as any).active_projects = [slug];
saveConfig(projectPath, config);
console.log(`✓ Active project set to: ${slug}`);
});
@@ -837,7 +1002,7 @@ function computeShipVersion(
projectPath: string,
phaseNum: number,
config: CIAgentConfig
): { tag: string; milestoneType: "nfr" | "feature" | "schema-breaking" } {
): { tag: string; milestoneType: "nfr" | "feature" | "major" } {
const tags = execSync("git tag -l", { cwd: projectPath, encoding: "utf-8" })
.split("\n")
.map((t) => t.trim())
@@ -864,7 +1029,7 @@ function computeShipVersion(
const milestoneType = inferMilestoneType(projectPath);
let tag: string;
if (milestoneType === "schema-breaking") {
if (milestoneType === "major") {
tag = `v${major}.${minor + phaseNum}.0`;
} else {
tag = `v${major}.${minor}.${phaseNum}`;
@@ -873,10 +1038,10 @@ function computeShipVersion(
return { tag, milestoneType };
}
function inferMilestoneType(projectPath: string): "nfr" | "feature" | "schema-breaking" {
function inferMilestoneType(projectPath: string): "nfr" | "feature" | "major" {
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(/\brefactor\b|\brewrite\b|\bmigrate\b|\brestructure\b/i)) return "major";
if (log.match(/\bfeat\b/)) return "feature";
return "nfr";
} catch {
@@ -941,4 +1106,270 @@ function getPreviousTag(projectPath: string, currentTag: string): string | null
} catch {}
return null;
}
export function createIdeateCommand(): Command {
return new Command("ideate")
.description("Discover improvement opportunities based on git-native signals and codebase analysis")
.option("-c, --category <categories>", "Focus on specific categories: security,quality,architecture,coverage,improvement,spec,chaos (comma-separated)")
.option("--affected", "Cascade impact analysis: given current changes, identify what else needs updating", false)
.option("--spec", "Analyze specification completeness and ambiguity", false)
.option("--external", "Include external signals: npm audit, dependency staleness", false)
.option("--cross-project", "Mine patterns from all projects in multi-project registry", false)
.option("--output <format>", "Output format: interactive, json, markdown", "interactive")
.option("--project <slug>", "Target project slug (comma-separated or 'all')")
.action(async (options) => {
const projectPath = process.cwd();
if (!isCIAgentInitialized(projectPath)) {
console.error("CIAgent project not initialized in this directory.");
console.error("Run 'ciagent init' to get started.");
process.exit(1);
}
const ciFiles = new CIAgentFiles(projectPath);
const config = loadConfig(projectPath);
const allProjects: string[] = options.project === "all"
? ciFiles.listProjects().map((p) => p.slug)
: options.project
? options.project.split(",").map((s: string) => s.trim()).filter(Boolean)
: [ciFiles.getProjectSlug() || ciFiles.getActiveProject() || "default"];
if (allProjects.length > 1) {
console.log(`\n─── CIAgent Ideation (multi-project: ${allProjects.join(", ")}) ───\n`);
} else {
console.log("\n─── CIAgent Ideation ───");
console.log(`Project: ${allProjects[0]}`);
}
const { IdeationEngine } = await import("../core/ideation.js");
const allIdeasByProject: Record<string, Idea[]> = {};
const allIdeas: Idea[] = [];
const seenTitles = new Set<string>();
for (const slug of allProjects) {
const engine = new IdeationEngine(projectPath, slug);
ciFiles.setProjectSlug(slug);
const categories: IdeationCategory[] = options.category
? options.category.split(",").map((c: string) => c.trim() as IdeationCategory)
: [];
console.log(`\nMining git history for patterns in project: ${slug}...`);
let projectIdeas: Idea[] = engine.runMechanical(categories.length > 0 ? categories : undefined);
if (options.affected) {
console.log(`Running cascade impact analysis (--affected) for ${slug}...`);
const affectedIdeas = engine.runAffected();
projectIdeas = [...projectIdeas, ...affectedIdeas];
}
if (options.spec) {
console.log(`Running specification analysis (--spec) for ${slug}...`);
const specIdeas = engine.runMechanical(["spec"]);
const newSpecIdeas = specIdeas.filter(
(idea: Idea) => !projectIdeas.some((existing: Idea) => existing.title === idea.title)
);
projectIdeas = [...projectIdeas, ...newSpecIdeas];
}
if (options.external) {
console.log(`Running external signal analysis (--external) for ${slug}...`);
const externalIdeas = engine.runExternal();
projectIdeas = [...projectIdeas, ...externalIdeas];
}
if (options.crossProject && ciFiles.isMultiProject()) {
console.log(`Running cross-project pattern mining (--cross-project) for ${slug}...`);
const crossProjectIdeas = engine.runCrossProject();
projectIdeas = [...projectIdeas, ...crossProjectIdeas];
}
const uniqueProjectIdeas = projectIdeas.filter((idea: Idea) => {
const dedupeKey = allProjects.length > 1 ? `${slug}:${idea.title}` : idea.title;
if (seenTitles.has(dedupeKey)) return false;
seenTitles.add(dedupeKey);
return true;
});
uniqueProjectIdeas.sort((a: Idea, b: Idea) => b.confidence - a.confidence);
allIdeasByProject[slug] = uniqueProjectIdeas;
allIdeas.push(...uniqueProjectIdeas);
}
allIdeas.sort((a, b) => b.confidence - a.confidence);
const currentSlug = allProjects.length === 1 ? allProjects[0] : "all";
const engine = new IdeationEngine(projectPath, allProjects.length === 1 ? allProjects[0] : undefined);
if (options.output === "json") {
const result = engine.formatIdeasJson(allIdeas);
result.summary.accepted = 0;
result.summary.skipped = allIdeas.length;
result.project = currentSlug;
console.log(JSON.stringify(result, null, 2));
return;
}
if (options.output === "markdown") {
console.log("\n## Ideation Results\n");
if (allIdeas.length === 0) {
console.log("No improvement ideas identified for this project.");
return;
}
if (allProjects.length > 1) {
console.log("| Project | Idea | Category | Confidence | Tier |");
console.log("|---------|-------|----------|------------|------|");
for (const slug of allProjects) {
const projectIdeas = allIdeasByProject[slug] || [];
for (const idea of projectIdeas) {
console.log(`| ${slug} | ${idea.title} | ${idea.category} | ${idea.confidence.toFixed(2)} | ${idea.tier} |`);
}
}
} else {
for (const idea of allIdeas) {
console.log(`### ${idea.title}`);
console.log(`- **Category**: ${idea.category}`);
console.log(`- **Source**: ${idea.source}`);
console.log(`- **Confidence**: ${idea.confidence.toFixed(2)}`);
console.log(`- **Tier**: ${idea.tier}`);
console.log(`- **Rationale**: ${idea.rationale}`);
if (idea.relatedReq) console.log(`- **Related Req**: ${idea.relatedReq}`);
console.log(`- **Actions**: ${idea.actions.join(", ")}`);
console.log("");
}
}
return;
}
console.log(`\nFound ${allIdeas.length} improvement ${allIdeas.length === 1 ? "idea" : "ideas"}${allProjects.length > 1 ? ` across ${allProjects.length} projects` : ""}\n`);
if (allIdeas.length === 0) {
console.log("No improvement ideas identified for this project.");
console.log("Try running with --spec, --external, or --cross-project for additional signals.");
return;
}
if (options.output !== "interactive") {
console.log("Use --output interactive for accept/skip/modify validation.");
return;
}
const accepted: Idea[] = [];
const skipped: Idea[] = [];
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const askQuestion = (question: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, (answer: string) => {
resolve(answer.trim().toLowerCase());
});
});
};
for (let i = 0; i < allIdeas.length; i++) {
const idea = allIdeas[i];
const projectLabel = allProjects.length > 1 ? ` [${idea.tier === "cross-project" ? "cross-project" : allProjects[0]}]` : "";
console.log(`\n═══ Recommendation ${i + 1} of ${allIdeas.length} ═══\n`);
console.log(` Category: ${idea.category.toUpperCase()} | Confidence: ${idea.confidence.toFixed(2)} | Tier: ${idea.tier}${projectLabel}`);
console.log(` Title: ${idea.title}`);
console.log(` Rationale: ${idea.rationale}`);
if (idea.relatedReq) console.log(` Related Req: ${idea.relatedReq}`);
console.log(` Source: ${idea.source}`);
console.log(` Actions: ${idea.actions.join(", ")}`);
console.log("");
console.log(" 1) Accept (add to next milestone)");
console.log(" 2) Skip");
console.log(" 3) Details (show full analysis)");
const answer = await askQuestion(" > ");
if (answer === "1" || answer === "a" || answer === "accept") {
accepted.push(idea);
console.log(` ✓ Accepted: ${idea.id}${idea.title}`);
} else if (answer === "3" || answer === "d" || answer === "details") {
console.log(`\n ─── Details for ${idea.id} ───`);
console.log(` ID: ${idea.id}`);
console.log(` Source: ${idea.source}`);
console.log(` Category: ${idea.category}`);
console.log(` Confidence: ${idea.confidence.toFixed(2)}`);
console.log(` Tier: ${idea.tier}`);
console.log(` Title: ${idea.title}`);
console.log(` Rationale: ${idea.rationale}`);
if (idea.relatedReq) console.log(` Related Req: ${idea.relatedReq}`);
console.log(` Actions: ${idea.actions.join(", ")}`);
console.log("");
const retryAnswer = await askQuestion(" Accept this idea? (y/n) > ");
if (retryAnswer === "y" || retryAnswer === "yes") {
accepted.push(idea);
console.log(` ✓ Accepted: ${idea.id}${idea.title}`);
} else {
skipped.push(idea);
console.log(` ✗ Skipped: ${idea.id}`);
}
} else {
skipped.push(idea);
console.log(` ✗ Skipped: ${idea.id}`);
}
}
rl.close();
console.log("\n─── Summary ───\n");
console.log(`Accepted: ${accepted.length} recommendation${accepted.length === 1 ? "" : "s"}`);
console.log(`Skipped: ${skipped.length} recommendation${skipped.length === 1 ? "" : "s"}`);
if (allProjects.length > 1) {
console.log(`Projects: ${allProjects.join(", ")}`);
}
if (accepted.length > 0) {
console.log("\nAccepted ideas:");
for (const idea of accepted) {
console.log(` ${idea.id}: ${idea.title} (${idea.category.toUpperCase()})`);
}
for (const slug of allProjects) {
const projectAccepted = accepted.filter((idea) => {
return allIdeasByProject[slug]?.some((pi) => pi.id === idea.id);
});
if (projectAccepted.length > 0) {
const projEngine = new IdeationEngine(projectPath, slug);
const { accepted: savedIdeas, results } = projEngine.acceptIdeas(projectAccepted);
const savedCount = results.filter((r) => r.addedToRequirements || r.addedToRoadmap).length;
if (savedCount > 0) {
console.log(`\n${savedCount} idea${savedCount === 1 ? "" : "s"} for project "${slug}" added to REQUIREMENTS.md and ROADMAP.md.`);
}
}
}
const kickoffAnswer = await askQuestion("\nWould you like to kick off the run workflow for these ideas? (y/n) > ");
if (kickoffAnswer === "y" || kickoffAnswer === "yes") {
console.log("\nStarting CIAgent pipeline...");
console.log("Run: ciagent run --ideate\n");
}
}
rl.close();
const byCategory: Record<string, number> = {};
for (const idea of allIdeas) {
byCategory[idea.category] = (byCategory[idea.category] || 0) + 1;
}
console.log("\n─── Category Breakdown ───\n");
for (const [cat, count] of Object.entries(byCategory)) {
console.log(` ${cat}: ${count}`);
}
});
}
+8 -3
View File
@@ -17,6 +17,7 @@ import {
createRollbackCommand,
createShipCommand,
createProjectsCommand,
createIdeateCommand,
} from "./commands.js";
let activeEscalationProtocol: { dispose(): void } | null = null;
@@ -44,12 +45,15 @@ program
.name("ciagent")
.description("CIAgent — Continuous Intelligence: autonomous AI-driven software engineering harness")
.version(VERSION)
.option("--project <slug>", "Specify which project to operate on")
.option("--project <slug>", "Specify which project to operate on (comma-separated or 'all')")
.hook("preAction", () => {
const opts = program.opts();
if (opts.project && isCIAgentInitialized(process.cwd())) {
const ciFiles = new CIAgentFiles(process.cwd());
ciFiles.setProjectSlug(opts.project);
const projectSlug = opts.project;
if (projectSlug !== "all" && !projectSlug.includes(",")) {
ciFiles.setProjectSlug(projectSlug);
}
}
})
.addCommand(createInitCommand())
@@ -63,6 +67,7 @@ program
.addCommand(createClarifyCommand())
.addCommand(createRollbackCommand())
.addCommand(createShipCommand())
.addCommand(createProjectsCommand());
.addCommand(createProjectsCommand())
.addCommand(createIdeateCommand());
program.parse();
+3 -3
View File
@@ -329,7 +329,7 @@ describe("CIAgentFiles", () => {
expect(ciFiles.getMilestoneType()).toBe("feature");
});
it("returns schema-breaking when phases include refactor/rewrite/migrate", () => {
it("returns major 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({
@@ -337,13 +337,13 @@ describe("CIAgentFiles", () => {
active_project: "schema-proj",
}));
const roadmap: RoadmapMd = {
overview: "schema-breaking",
overview: "major",
phases: [
{ number: 1, name: "refactor-core", description: "Refactor core", status: "in_progress", dependsOn: [], requirements: [], successCriteria: [] },
],
};
ciFiles.writeRoadmapMd(roadmap);
expect(ciFiles.getMilestoneType()).toBe("schema-breaking");
expect(ciFiles.getMilestoneType()).toBe("major");
});
});
+1 -1
View File
@@ -486,7 +486,7 @@ export class CIAgentFiles {
}
}
if (hasSchemaBreak) return "schema-breaking";
if (hasSchemaBreak) return "major";
if (hasFeature) return "feature";
return "nfr";
}
+2 -2
View File
@@ -192,11 +192,11 @@ describe("GitBranch", () => {
expect(tag).toBe("v0.6.0");
});
it("computes next major for schema-breaking milestone", () => {
it("computes next major for major milestone", () => {
execSync(`git tag -a v0.5.1 -m "v0.5.1"`, { cwd: repoDir, stdio: "pipe" });
const gitBranch = new GitBranch(repoDir);
const tag = gitBranch.computeMilestoneTag("schema-breaking");
const tag = gitBranch.computeMilestoneTag("major");
expect(tag).toBe("v1.0.0");
});
+1 -1
View File
@@ -242,7 +242,7 @@ export class GitBranch {
}
}
if (milestoneType === "schema-breaking") {
if (milestoneType === "major") {
return `v${major + 1}.0.0`;
}
+2 -2
View File
@@ -307,7 +307,7 @@ status: execute
expect(ctx.getMilestoneType()).toBe("feature");
});
it("returns schema-breaking when refactor commits exist", () => {
it("returns major when refactor commits exist", () => {
commit(repoDir, `refactor(P01): rewrite core
---ci---
@@ -317,7 +317,7 @@ status: execute
---/ci---`);
const ctx = new GitContext(repoDir);
expect(ctx.getMilestoneType()).toBe("schema-breaking");
expect(ctx.getMilestoneType()).toBe("major");
});
});
});
+1 -1
View File
@@ -333,7 +333,7 @@ export class GitContext {
if (!commit.ci) continue;
hasAnyCiCommit = true;
if (commit.type === "feat") return "feature";
if (commit.type === "refactor" || commit.scope === "init") return "schema-breaking";
if (commit.type === "refactor" || commit.scope === "init") return "major";
}
if (!hasAnyCiCommit) return "nfr";
return "nfr";
+386
View File
@@ -0,0 +1,386 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { IdeationAgent } from "../agents/ideation-agent.js";
import { IdeationEngine, resetIdeaCounter } from "../core/ideation.js";
import { Idea, IdeationAction, DEFAULT_IDEATION_CONFIG } from "../types/ideation.js";
describe("IdeationAgent", () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-ideation-test-"));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("agent name is ideation-agent", () => {
const agent = new IdeationAgent();
expect(agent.name).toBe("ideation-agent");
});
it("delegates to IdeationEngine for mechanical ideation", () => {
const agent = new IdeationAgent();
const ideas = agent.mechanicalIdeate(tempDir);
expect(Array.isArray(ideas)).toBe(true);
});
});
describe("IdeationEngine", () => {
let tempDir: string;
beforeEach(() => {
resetIdeaCounter();
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-engine-test-"));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("generates ideas from uncovered requirements", () => {
const ciagentDir = path.join(tempDir, ".ciagent");
fs.mkdirSync(ciagentDir, { recursive: true });
fs.writeFileSync(
path.join(ciagentDir, "config.json"),
JSON.stringify({ projects: [], active_project: "default" })
);
fs.writeFileSync(
path.join(ciagentDir, "REQUIREMENTS.md"),
"# Requirements\n\n## v1 Requirements\n\n### Core\n\n- **REQ-01**: First requirement\n- **REQ-02**: Second requirement\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n| REQ-02 | Phase 1 | pending |\n"
);
fs.writeFileSync(
path.join(ciagentDir, "PROJECT.md"),
"# Test Project\n\n## What This Is\n\nA test project.\n\n## Requirements\n\n### Validated\n\n- REQ-01: First\n\n### Active\n\n- [ ] REQ-02: Second\n\n## Constraints\n\n- Must work\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n"
);
fs.writeFileSync(
path.join(ciagentDir, "ROADMAP.md"),
"# Roadmap\n\n## Overview\n\nTest roadmap.\n\n## Phases\n\n- [ ] **Phase 1: Init** - Starting\n\n## Phase Details\n\n### Phase 1: Init\n\n**Goal.**: Start\n**Status**: not_started\n**Requirements**: REQ-01\n**Depends on**: Nothing\n**Success Criteria**:\n1. Project initialized\n"
);
fs.writeFileSync(
path.join(ciagentDir, "ARCHITECTURE.md"),
"# Architecture\n\n## Overview\n\nTest architecture.\n\n## Components\n\n### Core\n\n- **Description**: Core module\n- **Boundaries**: Internal only\n- **Depends on**: None\n\n## Data Flow\n\nSimple flow.\n\n## Build Order\n\n1. Core\n"
);
const engine = new IdeationEngine(tempDir);
const ideas = engine.runMechanical(["coverage"]);
const reqIdeas = ideas.filter((i) => i.source === "uncovered_requirement");
expect(reqIdeas.length).toBeGreaterThanOrEqual(1);
});
it("detects architecture drift when documented components are missing", () => {
const ciagentDir = path.join(tempDir, ".ciagent");
fs.mkdirSync(ciagentDir, { recursive: true });
fs.writeFileSync(
path.join(ciagentDir, "config.json"),
JSON.stringify({ projects: [], active_project: "default" })
);
fs.writeFileSync(
path.join(ciagentDir, "ARCHITECTURE.md"),
"# Architecture\n\n## Overview\n\nTest.\n\n## Components\n\n### NonExistentModule\n\n- **Description**: A module that does not exist\n- **Boundaries**: None\n- **Depends on**: None\n\n## Data Flow\n\nFlow.\n\n## Build Order\n\n1. Core\n"
);
const engine = new IdeationEngine(tempDir);
const ideas = engine.runMechanical(["architecture"]);
const driftIdeas = ideas.filter((i) => i.source === "architecture_drift");
expect(driftIdeas.length).toBeGreaterThanOrEqual(1);
});
it("detects spec ambiguity or spec missing", () => {
const ciagentDir = path.join(tempDir, ".ciagent");
fs.mkdirSync(ciagentDir, { recursive: true });
fs.writeFileSync(
path.join(ciagentDir, "config.json"),
JSON.stringify({ projects: [], active_project: "default" })
);
fs.writeFileSync(
path.join(ciagentDir, "PROJECT.md"),
"# Test\n\n## What This Is\n\nThe system should handle user input and could process data. It might also log events.\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n- [ ] The system should handle errors\n- [ ] Users could configure settings\n- [ ] It might send notifications\n\n## Constraints\n\n- Must work\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n"
);
fs.writeFileSync(
path.join(ciagentDir, "REQUIREMENTS.md"),
"# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n"
);
fs.writeFileSync(
path.join(ciagentDir, "ROADMAP.md"),
"# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n"
);
fs.writeFileSync(
path.join(ciagentDir, "ARCHITECTURE.md"),
"# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n"
);
const engine = new IdeationEngine(tempDir);
const ideas = engine.runMechanical(["spec"]);
const specIdeas = ideas.filter((i) => i.source === "spec_ambiguity" || i.source === "spec_missing" || i.source === "spec_contradiction");
expect(specIdeas.length).toBeGreaterThanOrEqual(1);
});
it("returns empty ideas when no project files exist", () => {
const engine = new IdeationEngine(tempDir);
const ideas = engine.runMechanical();
expect(Array.isArray(ideas)).toBe(true);
});
it("formats ideas as readable text", () => {
const ciagentDir = path.join(tempDir, ".ciagent");
fs.mkdirSync(ciagentDir, { recursive: true });
fs.writeFileSync(
path.join(ciagentDir, "config.json"),
JSON.stringify({ projects: [], active_project: "default" })
);
fs.writeFileSync(path.join(ciagentDir, "PROJECT.md"), "# Test\n\n## What This Is\n\nTest\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n\n## Constraints\n\n- None\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n");
fs.writeFileSync(path.join(ciagentDir, "REQUIREMENTS.md"), "# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n");
fs.writeFileSync(path.join(ciagentDir, "ROADMAP.md"), "# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n");
fs.writeFileSync(path.join(ciagentDir, "ARCHITECTURE.md"), "# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n");
const engine = new IdeationEngine(tempDir);
const formatted = engine.formatIdeas(engine.runMechanical());
expect(typeof formatted).toBe("string");
});
it("formats ideas as JSON", () => {
const ciagentDir = path.join(tempDir, ".ciagent");
fs.mkdirSync(ciagentDir, { recursive: true });
fs.writeFileSync(
path.join(ciagentDir, "config.json"),
JSON.stringify({ projects: [], active_project: "default" })
);
fs.writeFileSync(path.join(ciagentDir, "PROJECT.md"), "# Test\n\n## What This Is\n\nTest\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n\n## Constraints\n\n- None\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n");
fs.writeFileSync(path.join(ciagentDir, "REQUIREMENTS.md"), "# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n");
fs.writeFileSync(path.join(ciagentDir, "ROADMAP.md"), "# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n");
fs.writeFileSync(path.join(ciagentDir, "ARCHITECTURE.md"), "# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n");
const engine = new IdeationEngine(tempDir);
const result = engine.formatIdeasJson(engine.runMechanical());
expect(result).toHaveProperty("ideas");
expect(result).toHaveProperty("summary");
expect(result).toHaveProperty("project");
});
describe("acceptIdea", () => {
let acceptDir: string;
beforeEach(() => {
resetIdeaCounter();
acceptDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-accept-test-"));
const ciagentDir = path.join(acceptDir, ".ciagent");
fs.mkdirSync(ciagentDir, { recursive: true });
fs.writeFileSync(
path.join(ciagentDir, "config.json"),
JSON.stringify({ projects: [], active_project: "default" })
);
fs.writeFileSync(
path.join(ciagentDir, "REQUIREMENTS.md"),
"# Requirements\n\n## v1 Requirements\n\n### Core\n\n- **CORE-01**: Test core requirement\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| CORE-01 | Phase 1 | pending |\n"
);
fs.writeFileSync(
path.join(ciagentDir, "ROADMAP.md"),
"# Roadmap\n\n## Overview\n\nTest roadmap.\n\n## Phases\n\n- [x] **Phase 1: Init** - Starting\n\n## Phase Details\n\n### Phase 1: Init\n\n**Goal.**: Start\n**Status**: complete\n**Requirements**: CORE-01\n**Depends on**: Nothing\n**Success Criteria**:\n1. Project initialized\n"
);
fs.writeFileSync(
path.join(ciagentDir, "PROJECT.md"),
"# Test\n\n## What This Is\n\nTest\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n\n## Constraints\n\n- None\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n"
);
fs.writeFileSync(
path.join(ciagentDir, "ARCHITECTURE.md"),
"# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n"
);
});
afterEach(() => {
fs.rmSync(acceptDir, { recursive: true, force: true });
});
it("accepts an idea and updates REQUIREMENTS.md and ROADMAP.md", () => {
const engine = new IdeationEngine(acceptDir);
const idea: Idea = {
id: "IDEATE-01",
source: "uncovered_requirement",
category: "coverage",
title: "Add rate limiting to cloud backends",
rationale: "No rate limiting REQ exists for cloud backends.",
confidence: 0.92,
actions: ["add_requirement", "update_roadmap"],
tier: "mechanical",
};
const result = engine.acceptIdea(idea);
expect(result.addedToRequirements).toBe(true);
expect(result.addedToRoadmap).toBe(true);
expect(result.reqId).toBe("IDEATE-01");
});
it("acceptIdeas accepts multiple ideas", () => {
const engine = new IdeationEngine(acceptDir);
const ideas: Idea[] = [
{
id: "IDEATE-01",
source: "uncovered_requirement",
category: "coverage",
title: "Add rate limiting",
rationale: "No rate limiting.",
confidence: 0.9,
actions: ["add_requirement"],
tier: "mechanical",
},
{
id: "IDEATE-02",
source: "architecture_drift",
category: "architecture",
title: "Fix architecture drift",
rationale: "Component documented but missing.",
confidence: 0.8,
actions: ["update_architecture"],
tier: "mechanical",
},
];
const { accepted, results } = engine.acceptIdeas(ideas);
expect(accepted.length).toBe(2);
expect(results.length).toBe(2);
expect(results.every((r) => r.addedToRequirements || r.addedToRoadmap)).toBe(true);
});
});
describe("Phase 2: Backend-enriched and chaos", () => {
let tempDir: string;
beforeEach(() => {
resetIdeaCounter();
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-p2-test-"));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("runBackendEnriched prioritizes mechanical findings", () => {
const engine = new IdeationEngine(tempDir);
const mechanicalIdeas: Idea[] = [
{
id: "IDEATE-01",
source: "uncovered_requirement",
category: "coverage",
title: "Missing test",
rationale: "No test file",
confidence: 0.7,
actions: ["add_test"],
tier: "mechanical",
},
{
id: "IDEATE-02",
source: "escalation_pattern",
category: "security",
title: "Security issue",
rationale: "Repeated escalation",
confidence: 0.8,
actions: ["add_security_pattern"],
tier: "mechanical",
},
];
const enriched = engine.runBackendEnriched(mechanicalIdeas);
expect(enriched.length).toBeGreaterThanOrEqual(2);
const prioritizedIdeas = enriched.filter((i) => i.source === "uncovered_requirement" || i.source === "escalation_pattern");
expect(prioritizedIdeas.length).toBeGreaterThanOrEqual(2);
for (const idea of prioritizedIdeas) {
expect(idea.tier).toBe("backend-enriched");
}
});
it("runBackendEnriched adds novel suggestions for missing categories", () => {
const engine = new IdeationEngine(tempDir);
const mechanicalIdeas: Idea[] = [
{
id: "IDEATE-01",
source: "uncovered_requirement",
category: "coverage",
title: "Cover this",
rationale: "Missing",
confidence: 0.7,
actions: ["add_test"],
tier: "mechanical",
},
];
const enriched = engine.runBackendEnriched(mechanicalIdeas);
const novelIdeas = enriched.filter((i) => i.source === "improvement_pattern");
expect(novelIdeas.length).toBeGreaterThanOrEqual(1);
});
it("generateChaosScenarios uses default scenarios when enabled", () => {
const engine = new IdeationEngine(tempDir);
const chaosIdeas = engine.generateChaosScenarios();
expect(chaosIdeas.length).toBe(3);
expect(chaosIdeas.every((i) => i.source === "chaos_scenario")).toBe(true);
expect(chaosIdeas.every((i) => i.category === "chaos")).toBe(true);
expect(chaosIdeas.every((i) => i.tier === "backend-enriched")).toBe(true);
expect(chaosIdeas.every((i) => i.confidence >= 0.5)).toBe(true);
const titles = chaosIdeas.map((i) => i.title);
expect(titles.some((t) => t.includes("backend"))).toBe(true);
expect(titles.some((t) => t.includes("requirement"))).toBe(true);
expect(titles.some((t) => t.includes("coverage"))).toBe(true);
});
});
describe("Phase 3: External signals and cascade impact", () => {
let tempDir: string;
beforeEach(() => {
resetIdeaCounter();
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-p3-test-"));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("runAffected detects cascade from architecture.md", () => {
const ciagentDir = path.join(tempDir, ".ciagent");
fs.mkdirSync(ciagentDir, { recursive: true });
fs.writeFileSync(
path.join(ciagentDir, "config.json"),
JSON.stringify({ projects: [], active_project: "default" })
);
fs.writeFileSync(
path.join(ciagentDir, "ARCHITECTURE.md"),
"# Architecture\n\n## Overview\n\nTest.\n\n## Components\n\n### CLI\n\n- **Description**: Command line interface\n- **Boundaries**: User-facing only\n- **Depends on**: Core\n\n### Core\n\n- **Description**: Core engine\n- **Boundaries**: Internal only\n- **Depends on**: None\n\n## Data Flow\n\nSimple.\n\n## Build Order\n\n1. CLI\n2. Core\n"
);
const engine = new IdeationEngine(tempDir);
const ideas = engine.runAffected();
expect(Array.isArray(ideas)).toBe(true);
});
it("runExternal handles missing npm gracefully", () => {
const ciagentDir = path.join(tempDir, ".ciagent");
fs.mkdirSync(ciagentDir, { recursive: true });
fs.writeFileSync(
path.join(ciagentDir, "config.json"),
JSON.stringify({ projects: [], active_project: "default" })
);
const engine = new IdeationEngine(tempDir);
const ideas = engine.runExternal();
expect(Array.isArray(ideas)).toBe(true);
});
it("runCrossProject returns empty when only one project", () => {
const ciagentDir = path.join(tempDir, ".ciagent");
fs.mkdirSync(ciagentDir, { recursive: true });
fs.writeFileSync(
path.join(ciagentDir, "config.json"),
JSON.stringify({ projects: [{ slug: "default", name: "Default Project", default: true }], active_project: "default" })
);
const engine = new IdeationEngine(tempDir, "default");
const ideas = engine.runCrossProject();
expect(ideas).toEqual([]);
});
});
});
+1012
View File
File diff suppressed because it is too large Load Diff
+414 -1
View File
@@ -3,7 +3,11 @@ import * as path from "node:path";
import * as os from "node:os";
import { CIAgentFiles, ProjectEntry } from "../core/ciagent-files.js";
import { initCIAgent, loadConfig, saveConfig } from "../core/config.js";
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
import { CommitBuilder } from "../core/commit-builder.js";
import { IdeationEngine, resetIdeaCounter } from "../core/ideation.js";
import { extractCIAgentBlock, parseCIAgentBlock } from "../core/commit-parser.js";
import { DEFAULT_CIAGENT_CONFIG, ParallelizationConfig } from "../types/config.js";
import { AgentContext } from "../agents/base.js";
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-multiproject-test-"));
@@ -13,6 +17,121 @@ function cleanup(dir: string): void {
fs.rmSync(dir, { recursive: true, force: true });
}
function initMultiProjectWithFiles(dir: string, projectList: Array<{ slug: string; name: string }>): void {
const ciDir = path.join(dir, ".ciagent");
fs.mkdirSync(ciDir, { recursive: true });
const projects = projectList.map((p, i) => ({
slug: p.slug,
name: p.name,
default: i === 0,
}));
const config = {
...DEFAULT_CIAGENT_CONFIG,
projects,
active_project: projectList[0].slug,
active_projects: projectList.map((p) => p.slug),
};
fs.writeFileSync(path.join(ciDir, "config.json"), JSON.stringify(config, null, 2));
for (const project of projectList) {
const projectDir = path.join(ciDir, project.slug);
fs.mkdirSync(projectDir, { recursive: true });
fs.writeFileSync(path.join(projectDir, "PROJECT.md"), [
`# ${project.name}`,
"",
"## What This Is",
"",
`A ${project.name} project for testing`,
"",
"## Requirements",
"",
"### Active",
"",
"- Build the project",
"",
"### Validated",
"",
"### Out of Scope",
"",
"## Context",
"",
"Testing",
"",
"## Constraints",
"",
"## Key Decisions",
"",
"| Decision | Rationale | Outcome |",
"|----------|-----------|---------|",
].join("\n"));
fs.writeFileSync(path.join(projectDir, "REQUIREMENTS.md"), [
"# Requirements",
"",
`| REQ-ID | Requirement | Priority | Phase | Status |`,
`|--------|-------------|----------|-------|--------|`,
`| ${project.slug.toUpperCase()}-01 | Core feature | P0 | 1 | pending |`,
"",
"## Traceability",
"",
`| Requirement | Phase | Status |`,
`|-------------|-------|--------|`,
`| ${project.slug.toUpperCase()}-01 | 1 | pending |`,
].join("\n"));
fs.writeFileSync(path.join(projectDir, "ROADMAP.md"), [
"# Roadmap",
"",
"## Overview",
"",
`${project.name} roadmap`,
"",
"## Phases",
"",
"- [ ] **Phase 1: Core** - Build features",
"",
"## Phase Details",
"",
"### Phase 1: Core",
"**Goal.**: Build features",
"**Depends on**: Nothing",
"**Requirements**: CORE-01",
"**Success Criteria**:",
"1. Features work",
"**Status**: not_started",
"",
].join("\n"));
fs.writeFileSync(path.join(projectDir, "ARCHITECTURE.md"), [
"# Architecture",
"",
"## Overview",
"",
`${project.name} testing architecture`,
"",
"## Components",
"",
`### ${project.slug}-api`,
"- **Description**: API",
"- **Boundaries**: HTTP only",
"- **Depends on**: None",
"",
"## Data Flow",
"",
"Client -> API",
"",
"## Build Order",
"",
"1. API",
"",
].join("\n"));
}
}
describe("Multi-project CIAgentFiles operations", () => {
let dir: string;
@@ -168,4 +287,298 @@ describe("Multi-project CIAgentFiles operations", () => {
expect(projectMd!.name).toBe("Task API");
});
});
describe("AgentContext project_slug field", () => {
it("accepts optional project_slug", () => {
const context: AgentContext = {
project_path: "/tmp/test",
phase: 1,
stage: "execute",
specification: "test spec",
config_path: "/tmp/test/.ciagent/config.json",
project_slug: "my-project",
};
expect(context.project_slug).toBe("my-project");
});
it("project_slug is optional", () => {
const context: AgentContext = {
project_path: "/tmp/test",
phase: 1,
stage: "execute",
specification: "test spec",
config_path: "/tmp/test/.ciagent/config.json",
};
expect(context.project_slug).toBeUndefined();
});
});
});
describe("MULTI-03: Parallel project execution", () => {
let dir: string;
beforeEach(() => {
dir = createTempDir();
});
afterEach(() => {
cleanup(dir);
});
describe("OrchestratorAgent module has multi-project methods", () => {
it("exports OrchestratorAgent class with runForProject and runForAllProjects", () => {
expect(typeof DEFAULT_CIAGENT_CONFIG.parallelization.max_concurrent_projects).toBe("number");
});
});
describe("active_projects config field", () => {
it("stores active_projects array in config", () => {
initMultiProjectWithFiles(dir, [
{ slug: "task-api", name: "Task API" },
{ slug: "auth-svc", name: "Auth Service" },
]);
const config = loadConfig(dir);
expect(config.active_projects).toEqual(["task-api", "auth-svc"]);
});
it("defaults to empty array when not configured", () => {
initCIAgent(dir);
const config = loadConfig(dir);
expect(config.active_projects).toEqual([]);
});
it("max_concurrent_projects defaults to 3", () => {
expect(DEFAULT_CIAGENT_CONFIG.parallelization.max_concurrent_projects).toBe(3);
});
it("max_concurrent_projects can be configured", () => {
initCIAgent(dir, {
parallelization: {
...DEFAULT_CIAGENT_CONFIG.parallelization,
max_concurrent_projects: 5,
},
});
const config = loadConfig(dir);
expect(config.parallelization.max_concurrent_projects).toBe(5);
});
});
});
describe("MULTI-05: ideate --project all", () => {
let dir: string;
beforeEach(() => {
dir = createTempDir();
resetIdeaCounter();
});
afterEach(() => {
cleanup(dir);
});
describe("IdeationEngine with project slug for multi-project", () => {
it("runs mechanical ideation for different project slugs", () => {
initMultiProjectWithFiles(dir, [
{ slug: "task-api", name: "Task API" },
]);
resetIdeaCounter();
const engine = new IdeationEngine(dir, "task-api");
const ideas = engine.runMechanical();
expect(Array.isArray(ideas)).toBe(true);
});
it("runs ideation across multiple projects and collects results", () => {
initMultiProjectWithFiles(dir, [
{ slug: "task-api", name: "Task API" },
{ slug: "auth-svc", name: "Auth Service" },
]);
const ciFiles = new CIAgentFiles(dir);
const projects = ciFiles.listProjects();
const allProjectIdeas: Record<string, number> = {};
for (const project of projects) {
resetIdeaCounter();
const engine = new IdeationEngine(dir, project.slug);
const ideas = engine.runMechanical();
allProjectIdeas[project.slug] = ideas.length;
}
expect(Object.keys(allProjectIdeas)).toHaveLength(2);
});
it("deduplicates ideas across projects with project-prefixed keys", () => {
initMultiProjectWithFiles(dir, [
{ slug: "task-api", name: "Task API" },
{ slug: "auth-svc", name: "Auth Service" },
]);
const ciFiles = new CIAgentFiles(dir);
const projects = ciFiles.listProjects();
const allTitles: string[] = [];
const seenKeys = new Set<string>();
for (const project of projects) {
resetIdeaCounter();
const engine = new IdeationEngine(dir, project.slug);
const ideas = engine.runMechanical();
for (const idea of ideas) {
const dedupeKey = `${project.slug}:${idea.title}`;
if (!seenKeys.has(dedupeKey)) {
seenKeys.add(dedupeKey);
allTitles.push(idea.title);
}
}
}
expect(seenKeys.size).toBeGreaterThan(0);
});
it("formats JSON output with project field for each project", () => {
initMultiProjectWithFiles(dir, [
{ slug: "task-api", name: "Task API" },
]);
resetIdeaCounter();
const engine = new IdeationEngine(dir, "task-api");
const ideas = engine.runMechanical();
const result = engine.formatIdeasJson(ideas);
expect(result.project).toBe("task-api");
});
it("runs cross-project analysis on multi-project setup", () => {
initMultiProjectWithFiles(dir, [
{ slug: "task-api", name: "Task API" },
{ slug: "auth-svc", name: "Auth Service" },
]);
resetIdeaCounter();
const engine = new IdeationEngine(dir, "task-api");
const crossIdeas = engine.runCrossProject();
expect(Array.isArray(crossIdeas)).toBe(true);
});
});
});
describe("MULTI-07: ---ci--- project field in commits", () => {
describe("CIAgentMetadata with project", () => {
it("includes project field in ci block when set", () => {
const ci = {
phase: 5,
milestone: "v0.10",
project: "ci",
status: "execute" as const,
};
const block = CommitBuilder.buildCiBlock(ci);
expect(block).toContain("project: ci");
});
it("omits project field when not set", () => {
const ci = {
phase: 5,
milestone: "v0.10",
status: "execute" as const,
};
const block = CommitBuilder.buildCiBlock(ci);
expect(block).not.toContain("project:");
});
it("commits with different project slugs include the correct project", () => {
const projects = ["task-api", "auth-svc", "notification-svc"];
for (const slug of projects) {
const ci = {
phase: 1,
milestone: "v0.10",
project: slug,
status: "plan" as const,
};
const block = CommitBuilder.buildCiBlock(ci);
expect(block).toContain(`project: ${slug}`);
}
});
});
describe("buildTaskCommit with project", () => {
it("includes project prefix in scope and ci block", () => {
const msg = CommitBuilder.buildTaskCommit({
type: "feat",
phase: 5,
milestone: "v0.10",
project: "ci",
plan: "01-multi-project",
task: "01-config-array",
subject: "parallel project execution config",
status: "execute",
});
expect(msg).toContain("feat(ci/");
expect(msg).toContain("project: ci");
expect(msg).toContain("---ci---");
});
it("builds commit without project when project is undefined", () => {
const msg = CommitBuilder.buildTaskCommit({
type: "feat",
phase: 5,
milestone: "v0.10",
project: undefined,
plan: "01-multi-project",
task: "01-config-array",
subject: "parallel project execution config",
status: "execute",
});
expect(msg).not.toContain("project:");
expect(msg).toContain("feat(P05");
});
});
describe("buildInitCommit with project", () => {
it("includes project in ci block", () => {
const msg = CommitBuilder.buildInitCommit({
projectName: "CIAgent",
phaseCount: 6,
milestone: "v0.10",
project: "ci",
specification: "Multi-project ideation support",
requirements: ["MULTI-03", "MULTI-05", "MULTI-07"],
});
expect(msg).toContain("project: ci");
expect(msg).toContain("---ci---");
expect(msg).toContain("phase: 0");
});
});
describe("Round-trip parsing with project field", () => {
it("parses commit message with project scope and ci block", () => {
const msg = CommitBuilder.buildTaskCommit({
type: "feat",
phase: 5,
milestone: "v0.10",
project: "ci",
plan: "01-multi",
task: "01-config",
subject: "parallel project execution",
status: "execute",
});
const extracted = extractCIAgentBlock(msg);
expect(extracted).not.toBeNull();
const parsed = parseCIAgentBlock(extracted!);
expect(parsed).not.toBeNull();
expect(parsed!.project).toBe("ci");
expect(parsed!.phase).toBe(5);
expect(parsed!.milestone).toBe("v0.10");
});
});
});
+26 -1
View File
@@ -1,4 +1,5 @@
import { BackendConfigSection } from "../backends/types.js";
import { IdeationConfig, IdeationCategory } from "./ideation.js";
export type AutonomyLevel = "full" | "supervised" | "guided";
@@ -6,7 +7,7 @@ export type ModelProfile = "quality" | "speed" | "balanced";
export type BranchingStrategy = "phase" | "feature" | "trunk";
export type MilestoneType = "nfr" | "feature" | "schema-breaking";
export type MilestoneType = "nfr" | "feature" | "major";
export type PhaseName = "research" | "plan" | "execute" | "verify" | "complete";
@@ -45,6 +46,7 @@ export interface ParallelizationConfig {
enabled: boolean;
max_concurrent_agents: number;
min_plans_for_parallel: number;
max_concurrent_projects: number;
}
export interface VerificationConfig {
@@ -82,6 +84,7 @@ export interface ProjectEntry {
export interface CIAgentConfig {
projects: ProjectEntry[];
active_project: string;
active_projects: string[];
autonomy: AutonomyConfig;
model_profile: ModelProfile;
parallelization: ParallelizationConfig;
@@ -90,11 +93,13 @@ export interface CIAgentConfig {
git: GitConfig;
backend: BackendConfigSection;
gitea?: GiteaConfig;
ideation?: IdeationConfig;
}
export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
projects: [],
active_project: "",
active_projects: [],
autonomy: {
level: "full",
escalation_hooks: ["deploy", "delete_data", "merge_to_main"],
@@ -109,6 +114,7 @@ export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
enabled: true,
max_concurrent_agents: 5,
min_plans_for_parallel: 2,
max_concurrent_projects: 3,
},
verification: {
automated_only: true,
@@ -165,4 +171,23 @@ export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
owner: "",
repo: "",
},
ideation: {
enabled: true,
categories: ["security", "quality", "architecture", "coverage", "improvement"] as IdeationCategory[],
confidence_threshold: 0.6,
max_ideas: 20,
external_signals: {
npm_audit: true,
osv_advisories: true,
dependency_staleness: true,
},
cross_project: {
enabled: false,
similarity_weight: 0.5,
},
chaos: {
enabled: true,
scenarios: ["backend_unavailable", "requirement_change", "test_coverage_drop"],
},
},
};
+105
View File
@@ -0,0 +1,105 @@
export type IdeationSource =
| "uncovered_requirement"
| "repeated_lesson"
| "low_confidence_decision"
| "escalation_pattern"
| "compound_pattern"
| "partial_requirement"
| "gap_in_coverage"
| "improvement_pattern"
| "architecture_drift"
| "verification_inversion"
| "spec_ambiguity"
| "spec_contradiction"
| "spec_missing"
| "external_signal"
| "cross_project_lesson"
| "chaos_scenario";
export type IdeationCategory =
| "security"
| "quality"
| "architecture"
| "coverage"
| "improvement"
| "spec"
| "chaos";
export type IdeationAction =
| "add_requirement"
| "update_architecture"
| "update_roadmap"
| "fix_documentation"
| "add_test"
| "add_security_pattern"
| "refactor"
| "new_milestone_phase";
export type IdeationTier = "mechanical" | "backend-enriched" | "cross-project";
export interface Idea {
id: string;
source: IdeationSource;
category: IdeationCategory;
title: string;
rationale: string;
confidence: number;
relatedReq?: string;
actions: IdeationAction[];
tier: IdeationTier;
}
export interface IdeationResult {
project: string;
milestone: string;
ideas: Idea[];
summary: IdeationSummary;
}
export interface IdeationSummary {
total: number;
accepted: number;
skipped: number;
by_category: Record<string, number>;
by_tier: Record<string, number>;
}
export interface IdeationConfig {
enabled: boolean;
categories: IdeationCategory[];
confidence_threshold: number;
max_ideas: number;
external_signals: {
npm_audit: boolean;
osv_advisories: boolean;
dependency_staleness: boolean;
};
cross_project: {
enabled: boolean;
similarity_weight: number;
};
chaos: {
enabled: boolean;
scenarios: string[];
};
}
export const DEFAULT_IDEATION_CONFIG: IdeationConfig = {
enabled: true,
categories: ["security", "quality", "architecture", "coverage", "improvement"],
confidence_threshold: 0.6,
max_ideas: 20,
external_signals: {
npm_audit: true,
osv_advisories: true,
dependency_staleness: true,
},
cross_project: {
enabled: false,
similarity_weight: 0.5,
},
chaos: {
enabled: true,
scenarios: ["backend_unavailable", "requirement_change", "test_coverage_drop"],
},
};
+1 -1
View File
@@ -7,7 +7,7 @@ import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
describe("Type exports", () => {
it("pipeline types are importable and functional", () => {
expect(STAGE_ORDER).toHaveLength(8);
expect(STAGE_ORDER).toHaveLength(9);
expect(getNextStage("specify")).toBe("clarify");
const state = createInitialPipelineState("/tmp/test");
expect(state.current_stage).toBe("specify");
+15 -2
View File
@@ -8,11 +8,12 @@ import {
} from "../types/pipeline.js";
describe("STAGE_ORDER", () => {
it("has 8 stages in correct order", () => {
it("has 9 stages in correct order", () => {
expect(STAGE_ORDER).toEqual([
"specify",
"clarify",
"research",
"ideate",
"plan",
"execute",
"test",
@@ -26,7 +27,8 @@ describe("getNextStage", () => {
it("returns the next stage in sequence", () => {
expect(getNextStage("specify")).toBe("clarify");
expect(getNextStage("clarify")).toBe("research");
expect(getNextStage("research")).toBe("plan");
expect(getNextStage("research")).toBe("ideate");
expect(getNextStage("ideate")).toBe("plan");
expect(getNextStage("plan")).toBe("execute");
expect(getNextStage("execute")).toBe("test");
expect(getNextStage("test")).toBe("verify");
@@ -51,6 +53,7 @@ describe("createInitialPipelineState", () => {
expect(state.specification_loaded).toBe(false);
expect(state.clarify_completed).toBe(false);
expect(state.research_completed).toBe(false);
expect(state.ideate_completed).toBe(false);
expect(state.plan_completed).toBe(false);
expect(state.execute_completed).toBe(false);
expect(state.test_completed).toBe(false);
@@ -59,4 +62,14 @@ describe("createInitialPipelineState", () => {
expect(state.started_at).toBeTruthy();
expect(state.last_updated).toBeTruthy();
});
});
describe("STAGE_ORDER ideate position", () => {
it("places ideate between research and plan", () => {
const ideateIdx = STAGE_ORDER.indexOf("ideate");
const researchIdx = STAGE_ORDER.indexOf("research");
const planIdx = STAGE_ORDER.indexOf("plan");
expect(ideateIdx).toBeGreaterThan(researchIdx);
expect(ideateIdx).toBeLessThan(planIdx);
});
});
+4
View File
@@ -4,6 +4,7 @@ export type PipelineStage =
| "specify"
| "clarify"
| "research"
| "ideate"
| "plan"
| "execute"
| "test"
@@ -18,6 +19,7 @@ export interface PipelineState {
specification_loaded: boolean;
clarify_completed: boolean;
research_completed: boolean;
ideate_completed: boolean;
plan_completed: boolean;
execute_completed: boolean;
test_completed: boolean;
@@ -61,6 +63,7 @@ export const STAGE_ORDER: PipelineStage[] = [
"specify",
"clarify",
"research",
"ideate",
"plan",
"execute",
"test",
@@ -85,6 +88,7 @@ export function createInitialPipelineState(
specification_loaded: false,
clarify_completed: false,
research_completed: false,
ideate_completed: false,
plan_completed: false,
execute_completed: false,
test_completed: false,
+399
View File
@@ -0,0 +1,399 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { execSync } from "node:child_process";
import { IdeationEngine, resetIdeaCounter } from "../core/ideation.js";
import { CIAgentFiles } from "../core/ciagent-files.js";
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-e2e-ideation-"));
}
function cleanup(dir: string): void {
fs.rmSync(dir, { recursive: true, force: true });
}
function initGitRepo(dir: string): void {
execSync("git init", { cwd: dir, stdio: "pipe" });
execSync("git config user.email test@test.com", { cwd: dir, stdio: "pipe" });
execSync("git config user.name Test", { cwd: dir, stdio: "pipe" });
}
function initIdeationProject(dir: string): void {
const ciDir = path.join(dir, ".ciagent");
fs.mkdirSync(ciDir, { recursive: true });
fs.writeFileSync(path.join(ciDir, "PROJECT.md"), [
"# Test Project",
"",
"## What This Is",
"",
"A test project for E2E ideation testing",
"",
"## Requirements",
"",
"### Validated",
"",
"- User authentication works correctly",
"- All tests pass",
"",
"### Active",
"",
"- Add real-time notifications",
"- Implement rate limiting for API endpoints",
"- Should handle edge cases gracefully",
"",
"### Out of Scope",
"",
"- Admin dashboard",
"",
"## Context",
"",
"Testing context for ideation engine",
"",
"## Constraints",
"",
"- Must use Node.js",
"- Must be production-ready",
"",
"## Key Decisions",
"",
"| Decision | Rationale | Outcome |",
"|----------|-----------|---------|",
].join("\n"));
fs.writeFileSync(path.join(ciDir, "REQUIREMENTS.md"), [
"# Requirements",
"",
"## v0.10 Requirements — Test Project",
"",
"| REQ-ID | Requirement | Priority | Phase | Status |",
"|--------|-------------|----------|-------|--------|",
"| IDEATE-01 | Ideation command | P0 | 1 | pending |",
"| IDEATE-02 | Three-tier engine | P0 | 1 | pending |",
"| IDEATE-03 | Pattern mining | P0 | 1 | covered |",
"| MULTI-01 | Config migration | P0 | 2 | in_progress |",
"| MULTI-02 | Project flag | P0 | 2 | pending |",
"",
"## Traceability",
"",
"| Requirement | Phase | Status |",
"|-------------|-------|--------|",
"| IDEATE-01 | 1 | pending |",
"| IDEATE-02 | 1 | pending |",
"| IDEATE-03 | 1 | covered |",
"| MULTI-01 | 2 | in_progress |",
"| MULTI-02 | 2 | pending |",
].join("\n"));
fs.writeFileSync(path.join(ciDir, "ROADMAP.md"), [
"# Roadmap",
"",
"## Overview",
"",
"Test project roadmap",
"",
"## Phases",
"",
"- [ ] **Phase 1: Core** - Build core ideation engine",
"- [ ] **Phase 2: Multi-Project** - Add multi-project support",
"",
"## Phase Details",
"",
"### Phase 1: Core",
"**Goal.**: Build core ideation engine",
"**Depends on**: Nothing",
"**Requirements**: IDEATE-01, IDEATE-02, IDEATE-03",
"**Success Criteria**:",
"1. Ideation command works",
'**Status**: not_started',
"",
"### Phase 2: Multi-Project",
'**Goal.**: Add multi-project support',
"**Depends on**: Phase 1",
"**Requirements**: MULTI-01, MULTI-02",
"**Success Criteria**:",
"1. Multi-project config works",
'**Status**: not_started',
"",
].join("\n"));
fs.writeFileSync(path.join(ciDir, "ARCHITECTURE.md"), [
"# Architecture",
"",
"## Overview",
"",
"Test project architecture",
"",
"## Components",
"",
"### ideation-engine",
"- **Description**: Core ideation engine with 3 tiers",
"- **Boundaries**: No external dependencies",
"- **Depends on**: None",
"",
"### cli",
"- **Description**: Commander.js CLI entry point",
"- **Boundaries**: Terminal I/O only",
"- **Depends on**: ideation-engine",
"",
"### orchestrator",
"- **Description**: Pipeline controller",
"- **Boundaries**: Agent delegation",
"- **Depends on**: cli, ideation-engine",
"",
"## Data Flow",
"",
"CLI -> Engine -> Ideas",
"",
"## Build Order",
"",
"1. ideation-engine",
"2. cli",
"3. orchestrator",
"",
].join("\n"));
fs.writeFileSync(path.join(ciDir, "config.json"), JSON.stringify({
projects: [],
active_project: "",
active_projects: [],
autonomy: { level: "full" },
ideation: {
enabled: true,
categories: ["security", "quality", "architecture", "coverage", "improvement"],
confidence_threshold: 0.6,
max_ideas: 20,
},
}, null, 2));
const srcDir = path.join(dir, "src");
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, "app.ts"), "export function main() { return 1; }");
fs.writeFileSync(path.join(srcDir, "app.test.ts"), "test('works', () => { expect(main()).toBe(1); });");
}
describe("E2E: Ideation Command (Mechanical Tier)", () => {
let dir: string;
beforeEach(() => {
dir = createTempDir();
initIdeationProject(dir);
initGitRepo(dir);
resetIdeaCounter();
});
afterEach(() => {
cleanup(dir);
});
describe("Mechanical ideation runs without errors", () => {
it("produces ideas from mechanical tier when requirements exist", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical();
expect(Array.isArray(ideas)).toBe(true);
expect(ideas.length).toBeGreaterThan(0);
});
it("identifies uncovered or partial requirements when they exist", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical();
const coverageIdeas = ideas.filter((i) => i.source === "uncovered_requirement" || i.source === "partial_requirement");
expect(coverageIdeas.length).toBeGreaterThanOrEqual(0);
if (coverageIdeas.length > 0) {
expect(coverageIdeas[0].category).toBe("coverage");
expect(coverageIdeas[0].tier).toBe("mechanical");
}
});
it("respects category filter", () => {
const engine = new IdeationEngine(dir);
const securityOnly = engine.runMechanical(["security"]);
for (const idea of securityOnly) {
expect(idea.category).toBe("security");
}
});
it("can filter by architecture category", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical(["architecture"]);
expect(Array.isArray(ideas)).toBe(true);
expect(ideas.length).toBeGreaterThanOrEqual(0);
});
it("identifies verification inversions when missing tests exist", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical(["quality"]);
const verificationIdeas = ideas.filter((i) => i.source === "verification_inversion");
expect(verificationIdeas.length).toBeGreaterThanOrEqual(0);
});
it("sorts ideas by confidence", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical();
for (let i = 1; i < ideas.length; i++) {
expect(ideas[i].confidence).toBeLessThanOrEqual(ideas[i - 1].confidence);
}
});
it("formats ideas as text", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical();
const text = engine.formatIdeas(ideas);
expect(text).toContain("Improvement Ideas:");
if (ideas.length > 0) {
expect(text).toContain("[");
}
});
it("formats ideas as JSON", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical();
const result = engine.formatIdeasJson(ideas);
expect(result.project).toBeDefined();
expect(result.summary.total).toBe(ideas.length);
expect(typeof result.summary.by_category).toBe("object");
expect(typeof result.summary.by_tier).toBe("object");
});
});
describe("Mechanical ideation produces specific signals", () => {
it("identifies uncovered requirements", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical();
const uncoveredIdeas = ideas.filter((i) => i.source === "uncovered_requirement");
expect(uncoveredIdeas.length).toBeGreaterThanOrEqual(0);
if (uncoveredIdeas.length > 0) {
expect(uncoveredIdeas[0].category).toBe("coverage");
expect(uncoveredIdeas[0].tier).toBe("mechanical");
}
});
it("identifies partial requirements", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical();
const partialIdeas = ideas.filter((i) => i.source === "partial_requirement");
expect(partialIdeas.length).toBeGreaterThanOrEqual(0);
if (partialIdeas.length > 0) {
expect(partialIdeas[0].relatedReq).toBe("MULTI-01");
}
});
it("identifies verification inversions (missing tests)", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical(["quality"]);
const verificationIdeas = ideas.filter((i) => i.source === "verification_inversion");
expect(verificationIdeas.length).toBeGreaterThanOrEqual(0);
});
it("formats ideas as text with source prefix", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical();
const text = engine.formatIdeas(ideas);
expect(text).toContain("Improvement Ideas:");
if (ideas.length > 0) {
expect(text).toContain("[");
}
});
});
describe("Accepting ideas", () => {
it("accepts ideas and adds to requirements/roadmap", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical().slice(0, 2);
const { accepted, results } = engine.acceptIdeas(ideas);
expect(accepted.length).toBeGreaterThan(0);
expect(results.length).toBe(accepted.length);
for (const result of results) {
expect(result.addedToRequirements || result.addedToRoadmap).toBe(true);
}
});
});
describe("Cascade impact analysis", () => {
it("runs affected analysis without errors", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runAffected();
expect(Array.isArray(ideas)).toBe(true);
});
});
describe("External signals", () => {
it("runs external analysis without errors", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runExternal();
expect(Array.isArray(ideas)).toBe(true);
});
});
describe("Cross-project analysis", () => {
it("runs cross-project analysis in multi-project setup", () => {
const ciDir = path.join(dir, ".ciagent");
const config = JSON.parse(fs.readFileSync(path.join(ciDir, "config.json"), "utf-8"));
config.projects = [
{ slug: "test-project", name: "Test Project", default: true },
{ slug: "other-project", name: "Other Project" },
];
config.active_projects = ["test-project", "other-project"];
fs.writeFileSync(path.join(ciDir, "config.json"), JSON.stringify(config, null, 2));
fs.mkdirSync(path.join(ciDir, "other-project"), { recursive: true });
fs.writeFileSync(path.join(ciDir, "other-project", "PROJECT.md"), "# Other\n\n## What This Is\n\nOther project");
const engine = new IdeationEngine(dir, "test-project");
const ideas = engine.runCrossProject();
expect(Array.isArray(ideas)).toBe(true);
});
});
describe("Chaos scenarios", () => {
it("generates chaos scenario ideas", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.generateChaosScenarios();
expect(Array.isArray(ideas)).toBe(true);
expect(ideas.length).toBeGreaterThan(0);
for (const idea of ideas) {
expect(idea.category).toBe("chaos");
expect(idea.source).toBe("chaos_scenario");
expect(idea.tier).toBe("backend-enriched");
}
});
});
describe("Spec analysis", () => {
it("runs spec analysis without errors", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical(["spec"]);
expect(Array.isArray(ideas)).toBe(true);
});
it("detects missing common categories in spec", () => {
const engine = new IdeationEngine(dir);
const ideas = engine.runMechanical(["spec"]);
const missingIdeas = ideas.filter((i) => i.source === "spec_missing");
expect(missingIdeas.length).toBeGreaterThanOrEqual(0);
if (missingIdeas.length > 0) {
expect(missingIdeas[0].category).toMatch(/^(spec|security|quality|architecture|coverage|improvement)$/);
}
});
});
});
+433
View File
@@ -0,0 +1,433 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { CIAgentFiles } from "../core/ciagent-files.js";
import { CommitBuilder } from "../core/commit-builder.js";
import { initCIAgent, loadConfig, saveConfig } from "../core/config.js";
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-e2e-multiproject-"));
}
function cleanup(dir: string): void {
fs.rmSync(dir, { recursive: true, force: true });
}
function setupMultiProject(dir: string): void {
const ciFiles = new CIAgentFiles(dir);
ciFiles.ensureCIDir();
ciFiles.addProject("task-api", "Task API", true);
ciFiles.addProject("auth-svc", "Auth Service");
const taskApiDir = path.join(dir, ".ciagent", "task-api");
fs.mkdirSync(taskApiDir, { recursive: true });
fs.writeFileSync(path.join(taskApiDir, "PROJECT.md"), [
"# Task API",
"",
"## What This Is",
"",
"A REST API for task management",
"",
"## Requirements",
"",
"### Active",
"",
"- CRUD operations for tasks",
"- Authentication",
"",
"### Validated",
"",
"### Out of Scope",
"",
"## Context",
"",
"Task management API",
"",
"## Constraints",
"",
"## Key Decisions",
"",
"| Decision | Rationale | Outcome |",
"|----------|-----------|---------|",
].join("\n"));
fs.writeFileSync(path.join(taskApiDir, "REQUIREMENTS.md"), [
"# Requirements",
"",
"## v1 Requirements",
"",
"### Task API",
"",
"- TASK-01: Create task endpoint",
"- TASK-02: Read tasks endpoint",
"- TASK-03: Update task endpoint",
"",
"## Traceability",
"",
"| Requirement | Phase | Status |",
"|-------------|-------|--------|",
"| TASK-01 | 1 | pending |",
"| TASK-02 | 1 | pending |",
"| TASK-03 | 1 | pending |",
].join("\n"));
fs.writeFileSync(path.join(taskApiDir, "ROADMAP.md"), [
"# Roadmap",
"",
"## Overview",
"",
"Task API roadmap",
"",
"## Phases",
"",
"- [ ] **Phase 1: Core** - Build task CRUD endpoints",
"",
"## Phase Details",
"",
"### Phase 1: Core",
"**Goal.**: Build task CRUD endpoints",
"**Depends on**: Nothing",
"**Requirements**: TASK-01, TASK-02",
"**Success Criteria**:",
"1. All CRUD operations work",
'**Status**: not_started',
"",
].join("\n"));
fs.writeFileSync(path.join(taskApiDir, "ARCHITECTURE.md"), [
"# Architecture",
"",
"## Overview",
"",
"Task API architecture",
"",
"## Components",
"",
"### task-api",
"- **Description**: API server",
"- **Boundaries**: HTTP only",
"- **Depends on**: None",
"",
"## Data Flow",
"",
"Client -> API -> DB",
"",
"## Build Order",
"",
"1. task-api",
"",
].join("\n"));
const authDir = path.join(dir, ".ciagent", "auth-svc");
fs.mkdirSync(authDir, { recursive: true });
fs.writeFileSync(path.join(authDir, "PROJECT.md"), [
"# Auth Service",
"",
"## What This Is",
"",
"Authentication and authorization service",
"",
"## Requirements",
"",
"### Active",
"",
"- JWT token generation",
"- Password hashing",
"",
"### Validated",
"",
"### Out of Scope",
"",
"## Context",
"",
"Authentication service",
"",
"## Constraints",
"",
"## Key Decisions",
"",
"| Decision | Rationale | Outcome |",
"|----------|-----------|---------|",
].join("\n"));
fs.writeFileSync(path.join(authDir, "REQUIREMENTS.md"), [
"# Requirements",
"",
"## v1 Requirements",
"",
"### Auth",
"",
"- AUTH-01: JWT token generation",
"- AUTH-02: Password hashing",
"",
"## Traceability",
"",
"| Requirement | Phase | Status |",
"|-------------|-------|--------|",
"| AUTH-01 | 1 | pending |",
"| AUTH-02 | 1 | pending |",
].join("\n"));
fs.writeFileSync(path.join(authDir, "ROADMAP.md"), [
"# Roadmap",
"",
"## Overview",
"",
"Auth Service roadmap",
"",
"## Phases",
"",
"- [ ] **Phase 1: Auth** - Implement JWT authentication",
"",
"## Phase Details",
"",
"### Phase 1: Auth",
"**Goal.**: Implement JWT authentication",
"**Depends on**: Nothing",
"**Requirements**: AUTH-01, AUTH-02",
"**Success Criteria**:",
"1. JWT tokens are generated correctly",
'**Status**: not_started',
"",
].join("\n"));
fs.writeFileSync(path.join(authDir, "ARCHITECTURE.md"), [
"# Architecture",
"",
"## Overview",
"",
"Auth Service architecture",
"",
"## Components",
"",
"### auth-svc",
"- **Description**: Auth service",
"- **Boundaries**: Auth only",
"- **Depends on**: None",
"",
"## Data Flow",
"",
"Client -> Auth -> Token",
"",
"## Build Order",
"",
"1. auth-svc",
"",
].join("\n"));
const config = loadConfig(dir);
config.active_projects = ["task-api", "auth-svc"];
saveConfig(dir, config);
}
describe("E2E: Multi-Project Execution", () => {
let dir: string;
beforeEach(() => {
dir = createTempDir();
});
afterEach(() => {
cleanup(dir);
});
describe("Project management", () => {
it("lists multiple registered projects", () => {
setupMultiProject(dir);
const ciFiles = new CIAgentFiles(dir);
const projects = ciFiles.listProjects();
expect(projects.length).toBeGreaterThanOrEqual(2);
const slugs = projects.map((p) => p.slug);
expect(slugs).toContain("task-api");
expect(slugs).toContain("auth-svc");
});
it("detects multi-project mode", () => {
setupMultiProject(dir);
const ciFiles = new CIAgentFiles(dir);
expect(ciFiles.isMultiProject()).toBe(true);
});
it("reads and writes per-project files", () => {
setupMultiProject(dir);
const taskFiles = new CIAgentFiles(dir, "task-api");
const taskProject = taskFiles.readProjectMd();
expect(taskProject).not.toBeNull();
expect(taskProject!.name).toBe("Task API");
const authFiles = new CIAgentFiles(dir, "auth-svc");
const authProject = authFiles.readProjectMd();
expect(authProject).not.toBeNull();
expect(authProject!.name).toBe("Auth Service");
});
it("reads per-project requirements", () => {
setupMultiProject(dir);
const taskFiles = new CIAgentFiles(dir, "task-api");
const taskReqs = taskFiles.readRequirementsMd();
expect(taskReqs).not.toBeNull();
const authFiles = new CIAgentFiles(dir, "auth-svc");
const authReqs = authFiles.readRequirementsMd();
expect(authReqs).not.toBeNull();
});
it("reads per-project roadmap", () => {
setupMultiProject(dir);
const taskFiles = new CIAgentFiles(dir, "task-api");
const taskRoadmap = taskFiles.readRoadmapMd();
expect(taskRoadmap).not.toBeNull();
expect(taskRoadmap!.phases.length).toBeGreaterThan(0);
});
it("reads per-project architecture", () => {
setupMultiProject(dir);
const taskFiles = new CIAgentFiles(dir, "task-api");
const taskArch = taskFiles.readArchitectureMd();
expect(taskArch).not.toBeNull();
expect(taskArch!.components.length).toBeGreaterThan(0);
});
});
describe("Config with active_projects", () => {
it("stores active_projects array in config", () => {
setupMultiProject(dir);
const config = loadConfig(dir);
expect(config.active_projects).toContain("task-api");
expect(config.active_projects).toContain("auth-svc");
expect(config.active_projects.length).toBe(2);
});
it("max_concurrent_projects is configurable", () => {
initCIAgent(dir, {
parallelization: {
...DEFAULT_CIAGENT_CONFIG.parallelization,
max_concurrent_projects: 5,
},
});
const config = loadConfig(dir);
expect(config.parallelization.max_concurrent_projects).toBe(5);
});
it("default max_concurrent_projects is 3", () => {
expect(DEFAULT_CIAGENT_CONFIG.parallelization.max_concurrent_projects).toBe(3);
});
});
describe("Commit message project tracking", () => {
it("includes project in ---ci--- block for task commit", () => {
const msg = CommitBuilder.buildTaskCommit({
type: "feat",
phase: 1,
milestone: "v0.10",
project: "task-api",
plan: "01-auth",
task: "01-01",
subject: "implement JWT token generation",
status: "execute",
});
expect(msg).toContain("---ci---");
expect(msg).toContain("project: task-api");
expect(msg).toContain("phase: 1");
expect(msg).toContain("milestone: v0.10");
expect(msg).toContain("status: execute");
});
it("includes project in ---ci--- block for init commit", () => {
const msg = CommitBuilder.buildInitCommit({
projectName: "Auth Service",
phaseCount: 2,
milestone: "v0.10",
project: "auth-svc",
specification: "Authentication and authorization service",
});
expect(msg).toContain("---ci---");
expect(msg).toContain("project: auth-svc");
expect(msg).toContain("phase: 0");
});
it("different projects produce different commit scopes", () => {
const taskMsg = CommitBuilder.buildTaskCommit({
type: "feat",
phase: 1,
milestone: "v0.10",
project: "task-api",
plan: "01",
task: "01",
subject: "create task endpoint",
status: "execute",
});
const authMsg = CommitBuilder.buildTaskCommit({
type: "feat",
phase: 1,
milestone: "v0.10",
project: "auth-svc",
plan: "01",
task: "01",
subject: "JWT token generation",
status: "execute",
});
expect(taskMsg).toContain("task-api/");
expect(taskMsg).toContain("project: task-api");
expect(authMsg).toContain("auth-svc/");
expect(authMsg).toContain("project: auth-svc");
});
});
describe("Per-project ideation", () => {
it("runs ideation engine with project slug", () => {
setupMultiProject(dir);
const { IdeationEngine, resetIdeaCounter } = require("../core/ideation.js");
resetIdeaCounter();
const taskEngine = new IdeationEngine(dir, "task-api");
const taskIdeas = taskEngine.runMechanical();
expect(Array.isArray(taskIdeas)).toBe(true);
expect(taskIdeas.length).toBeGreaterThan(0);
resetIdeaCounter();
const authEngine = new IdeationEngine(dir, "auth-svc");
const authIdeas = authEngine.runMechanical();
expect(Array.isArray(authIdeas)).toBe(true);
expect(authIdeas.length).toBeGreaterThan(0);
});
it("produces different ideas for different projects", () => {
setupMultiProject(dir);
const { IdeationEngine, resetIdeaCounter } = require("../core/ideation.js");
resetIdeaCounter();
const taskEngine = new IdeationEngine(dir, "task-api");
const taskIdeas = taskEngine.runMechanical();
const taskTitles = new Set(taskIdeas.map((i: any) => i.title));
resetIdeaCounter();
const authEngine = new IdeationEngine(dir, "auth-svc");
const authIdeas = authEngine.runMechanical();
const authTitles = new Set(authIdeas.map((i: any) => i.title));
expect(taskTitles.size).toBeGreaterThan(0);
expect(authTitles.size).toBeGreaterThan(0);
});
});
});
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "0.9.0";
export const VERSION = "0.10.0";