Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d0034dc88 | |||
| a153291643 | |||
| a0619f9740 | |||
| f478088797 | |||
| e2b749d42e | |||
| c747d3e8be | |||
| d9927558d5 | |||
| 895d9f95a1 | |||
| 30352a3603 | |||
| d58fd0bdde | |||
| 0799cfc644 |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.1–v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
|
||||
- Schema-breaking: minors v0.3.0, v0.4.0, v0.5.0 → milestone tag is v1.0.0
|
||||
- NFR: no milestone tag — the milestone is implicit from the patch sequence
|
||||
|
||||
Determine milestone type via `getMilestoneType()` which returns `"nfr" | "feature" | "schema-breaking"`.
|
||||
**Tag rules (CRITICAL):**
|
||||
- Milestone tags are always the NEXT version, never the base:
|
||||
- Feature: patches v0.5.1–v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
|
||||
- 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):
|
||||
|
||||
@@ -101,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
|
||||
|
||||
@@ -113,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
@@ -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.1–v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
|
||||
- Schema-breaking: minors v0.3.0, v0.4.0, v0.5.0 → milestone tag is v1.0.0
|
||||
- NFR: no milestone tag — the milestone is implicit from the patch sequence
|
||||
**Tag rules (CRITICAL):**
|
||||
|
||||
**Usage:** `ciagent-ship [phase_number|milestone]`
|
||||
- Milestone tags are always the NEXT version, never the base:
|
||||
- Feature: patches v0.5.1–v0.5.5 → milestone tag is v0.6.0 (NOT v0.5.0)
|
||||
- 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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@continuous-intelligence/ciagent",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@continuous-intelligence/ciagent",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^12.1.0",
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface AgentContext {
|
||||
specification: string;
|
||||
config_path: string;
|
||||
backend?: IntelligenceBackend;
|
||||
project_slug?: string;
|
||||
}
|
||||
|
||||
export function backendResultToAgentResult(result: BackendResult): AgentResult {
|
||||
|
||||
+162
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
+254
-73
@@ -10,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";
|
||||
@@ -79,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",
|
||||
@@ -169,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();
|
||||
|
||||
@@ -178,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) {
|
||||
@@ -193,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);
|
||||
@@ -200,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 {
|
||||
@@ -863,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())
|
||||
@@ -890,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}`;
|
||||
@@ -899,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 {
|
||||
@@ -989,72 +1128,88 @@ export function createIdeateCommand(): Command {
|
||||
}
|
||||
|
||||
const ciFiles = new CIAgentFiles(projectPath);
|
||||
let slug = options.project || ciFiles.getActiveProject() || "default";
|
||||
const allProjects = slug === "all";
|
||||
|
||||
if (options.project) {
|
||||
ciFiles.setProjectSlug(options.project);
|
||||
}
|
||||
|
||||
const categories: IdeationCategory[] = options.category
|
||||
? options.category.split(",").map((c: string) => c.trim() as IdeationCategory)
|
||||
: [];
|
||||
|
||||
console.log("\n─── CIAgent Ideation ───");
|
||||
console.log(`Project: ${ciFiles.getProjectSlug() || "default"}`);
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
|
||||
console.log("\nMining git history for patterns...");
|
||||
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 engine = new IdeationEngine(projectPath, ciFiles.getProjectSlug() || undefined);
|
||||
|
||||
let allIdeas: Idea[] = [];
|
||||
const allIdeasByProject: Record<string, Idea[]> = {};
|
||||
const allIdeas: Idea[] = [];
|
||||
const seenTitles = new Set<string>();
|
||||
|
||||
console.log("Running mechanical analysis (tier 1)...");
|
||||
allIdeas = engine.runMechanical(categories.length > 0 ? categories : undefined);
|
||||
for (const slug of allProjects) {
|
||||
const engine = new IdeationEngine(projectPath, slug);
|
||||
ciFiles.setProjectSlug(slug);
|
||||
|
||||
if (options.affected) {
|
||||
console.log("Running cascade impact analysis (--affected)...");
|
||||
const affectedIdeas = engine.runAffected();
|
||||
allIdeas = [...allIdeas, ...affectedIdeas];
|
||||
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);
|
||||
}
|
||||
|
||||
if (options.spec) {
|
||||
console.log("Running specification analysis (--spec)...");
|
||||
const specIdeas = engine.runMechanical(["spec"]);
|
||||
const newSpecIdeas = specIdeas.filter(
|
||||
(idea: Idea) => !allIdeas.some((existing: Idea) => existing.title === idea.title)
|
||||
);
|
||||
allIdeas = [...allIdeas, ...newSpecIdeas];
|
||||
}
|
||||
allIdeas.sort((a, b) => b.confidence - a.confidence);
|
||||
|
||||
if (options.external) {
|
||||
console.log("Running external signal analysis (--external)...");
|
||||
const externalIdeas = engine.runExternal();
|
||||
allIdeas = [...allIdeas, ...externalIdeas];
|
||||
}
|
||||
|
||||
if (options.crossProject && ciFiles.isMultiProject()) {
|
||||
console.log("Running cross-project pattern mining (--cross-project)...");
|
||||
const crossProjectIdeas = engine.runCrossProject();
|
||||
allIdeas = [...allIdeas, ...crossProjectIdeas];
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
allIdeas = allIdeas.filter((idea: Idea) => {
|
||||
if (seen.has(idea.title)) return false;
|
||||
seen.add(idea.title);
|
||||
return true;
|
||||
});
|
||||
|
||||
allIdeas.sort((a: Idea, b: Idea) => 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;
|
||||
}
|
||||
@@ -1065,21 +1220,33 @@ export function createIdeateCommand(): Command {
|
||||
console.log("No improvement ideas identified for this project.");
|
||||
return;
|
||||
}
|
||||
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("");
|
||||
|
||||
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"}\n`);
|
||||
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.");
|
||||
@@ -1110,8 +1277,9 @@ export function createIdeateCommand(): Command {
|
||||
|
||||
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}`);
|
||||
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}`);
|
||||
@@ -1160,17 +1328,30 @@ export function createIdeateCommand(): Command {
|
||||
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()})`);
|
||||
}
|
||||
|
||||
const { accepted: savedIdeas, results } = engine.acceptIdeas(accepted);
|
||||
const savedCount = results.filter((r) => r.addedToRequirements || r.addedToRoadmap).length;
|
||||
for (const slug of allProjects) {
|
||||
const projectAccepted = accepted.filter((idea) => {
|
||||
return allIdeasByProject[slug]?.some((pi) => pi.id === idea.id);
|
||||
});
|
||||
|
||||
if (savedCount > 0) {
|
||||
console.log(`\n${savedCount} idea${savedCount === 1 ? "" : "s"} added to REQUIREMENTS.md and ROADMAP.md.`);
|
||||
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) > ");
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -486,7 +486,7 @@ export class CIAgentFiles {
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSchemaBreak) return "schema-breaking";
|
||||
if (hasSchemaBreak) return "major";
|
||||
if (hasFeature) return "feature";
|
||||
return "nfr";
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ export class GitBranch {
|
||||
}
|
||||
}
|
||||
|
||||
if (milestoneType === "schema-breaking") {
|
||||
if (milestoneType === "major") {
|
||||
return `v${major + 1}.0.0`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
@@ -327,4 +327,60 @@ describe("IdeationEngine", () => {
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
+3
-1
@@ -7,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";
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface ParallelizationConfig {
|
||||
enabled: boolean;
|
||||
max_concurrent_agents: number;
|
||||
min_plans_for_parallel: number;
|
||||
max_concurrent_projects: number;
|
||||
}
|
||||
|
||||
export interface VerificationConfig {
|
||||
@@ -113,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,
|
||||
|
||||
@@ -62,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);
|
||||
});
|
||||
});
|
||||
@@ -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)$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -1 +1 @@
|
||||
export const VERSION = "0.9.0";
|
||||
export const VERSION = "0.10.0";
|
||||
Reference in New Issue
Block a user