Compare commits
11 Commits
v0.9.0
...
v0.10.0-phase5
| Author | SHA1 | Date | |
|---|---|---|---|
| e2b749d42e | |||
| c747d3e8be | |||
| d9927558d5 | |||
| 895d9f95a1 | |||
| 30352a3603 | |||
| d58fd0bdde | |||
| 0799cfc644 | |||
| 70ee21856d | |||
| b7d02ee4a4 | |||
| 8e50049ba5 | |||
| da528cc493 |
@@ -0,0 +1,288 @@
|
|||||||
|
---
|
||||||
|
description: Run the CIAgent ideation pipeline — analyze project for improvement opportunities, validate recommendations with user, update long-term documents
|
||||||
|
---
|
||||||
|
|
||||||
|
# CIAgent Ideate
|
||||||
|
|
||||||
|
Run the CIAgent ideation engine to discover improvement opportunities based on git-native signals, codebase analysis, and cross-project patterns.
|
||||||
|
|
||||||
|
**Usage:** `ciagent ideate [options]`
|
||||||
|
|
||||||
|
## Step 0: Confirm Active Project
|
||||||
|
|
||||||
|
Check `ci listProjects()` or read `.ciagent/config.json` to determine project context.
|
||||||
|
|
||||||
|
If `.ciagent/config.json` has `active_projects` array with length > 0:
|
||||||
|
- Use `--project <slug>` to target a specific project
|
||||||
|
- Use `--project all` to run ideation across all active projects (deduplicate findings)
|
||||||
|
- If no `--project` flag, use first project in `active_projects`
|
||||||
|
|
||||||
|
If `.ciagent/config.json` has `active_project` string (legacy):
|
||||||
|
- Use that project as the target
|
||||||
|
- Backwards-compatible: if both `active_project` and `active_projects` exist, `active_projects` takes precedence
|
||||||
|
|
||||||
|
## Step 1: Load Project Context
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --max-count=50
|
||||||
|
git branch -a
|
||||||
|
```
|
||||||
|
|
||||||
|
Read project reference files:
|
||||||
|
- `.ciagent/PROJECT.md` — Vision, requirements, constraints, key decisions
|
||||||
|
- `.ciagent/ROADMAP.md` — Phases, milestones, success criteria
|
||||||
|
- `.ciagent/REQUIREMENTS.md` — REQ-IDs, status, traceability
|
||||||
|
- `.ciagent/ARCHITECTURE.md` — Component boundaries, data flow
|
||||||
|
- `.ciagent/config.json` — Ideation configuration, autonomy level
|
||||||
|
|
||||||
|
## Step 2: Run Ideation Tiers
|
||||||
|
|
||||||
|
Execute tiers in order. Each tier produces `Idea[]` objects. Ideas from all tiers are merged and deduplicated before presentation.
|
||||||
|
|
||||||
|
### Tier 1: Mechanical Analysis (Always Available)
|
||||||
|
|
||||||
|
No backend required. All signals come from git history, `.ciagent/` files, and filesystem.
|
||||||
|
|
||||||
|
#### 2.1 Git-Native Pattern Mining
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --all --grep="lessons:" --format="%B" -50
|
||||||
|
git log --all --grep="decisions:" --format="%B" -50 -- "***confidence***0.*"
|
||||||
|
git log --all --grep="escalation:" --format="%B" -50
|
||||||
|
git log --all --grep="compound:" --format="%B" -50
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract:
|
||||||
|
- **Repeated lessons** — topics appearing > 1 time → systemic issue
|
||||||
|
- **Low-confidence decisions** — confidence < 0.7 in `---ci---` blocks → improvement targets
|
||||||
|
- **Escalation types** — each type identifies a process gap
|
||||||
|
- **Compound solutions** — suggest generalizing patterns that were solved multiple times
|
||||||
|
- **Partial requirements** — `requirements: partial: [REQ-XX]` in `---ci---` blocks
|
||||||
|
|
||||||
|
#### 2.2 Coverage Gap Analysis
|
||||||
|
|
||||||
|
- Parse REQUIREMENTS.md for `pending` and `in_progress` status requirements
|
||||||
|
- Cross-reference with PLAN.md task completion
|
||||||
|
- Identify requirements with no corresponding implementation tasks
|
||||||
|
|
||||||
|
#### 2.3 Verification Layer Inversion
|
||||||
|
|
||||||
|
For each verification layer, identify what's MISSING:
|
||||||
|
|
||||||
|
- **Structural**: Files referenced but not created, stubs, TODOs, placeholder implementations
|
||||||
|
- **Behavioral**: Test suites with < 80% coverage, missing test files for covered requirements
|
||||||
|
- **Security**: No STRIDE analysis for modified components, missing input validation patterns
|
||||||
|
- **Quality**: P1/P2 review findings unresolved, consistent style violations
|
||||||
|
|
||||||
|
#### 2.4 Architectural Drift Detection
|
||||||
|
|
||||||
|
- Parse ARCHITECTURE.md component tree
|
||||||
|
- Compare against actual `src/` directory structure
|
||||||
|
- Flag components documented but not implemented
|
||||||
|
- Flag components implemented but not documented
|
||||||
|
- Check import graph for unauthorized dependencies between components
|
||||||
|
|
||||||
|
#### 2.5 Spec-Driven Improvement
|
||||||
|
|
||||||
|
- Analyze REQUIREMENTS.md for ambiguous language ("should" vs "must", undefined terms)
|
||||||
|
- Check for contradictions between requirements
|
||||||
|
- Compare against common patterns for the project type (identified from package.json keywords)
|
||||||
|
- Flag requirements with no verification criteria
|
||||||
|
|
||||||
|
### Tier 2: Backend-Enriched Analysis (When LLM Available)
|
||||||
|
|
||||||
|
Requires an intelligence backend (opencode, openai, anthropic, or ollama).
|
||||||
|
|
||||||
|
#### 2.6 Prioritization and Ranking
|
||||||
|
|
||||||
|
- Evaluate all mechanical findings for impact and feasibility
|
||||||
|
- Rank ideas by: (1) number of signals corroborating, (2) severity of the gap, (3) ease of addressing
|
||||||
|
|
||||||
|
#### 2.7 Novel Improvement Suggestions
|
||||||
|
|
||||||
|
- Suggest improvements beyond pattern matching (e.g., "consider rate limiting" based on industry best practices, not just a repeated lesson)
|
||||||
|
- Generate concrete action plans for each accepted idea
|
||||||
|
- Identify bleeding-edge approaches relevant to the project's tech stack
|
||||||
|
|
||||||
|
#### 2.8 Chaos Engineering Ideation
|
||||||
|
|
||||||
|
- Generate failure scenarios: "What if the backend is unavailable?", "What if a requirement changes mid-implementation?", "What if test coverage drops below threshold?"
|
||||||
|
- Map failure scenarios to code that would break
|
||||||
|
- Suggest resilience improvements for each scenario
|
||||||
|
|
||||||
|
### Tier 3: Cross-Project Pattern Transfer (When Multi-Project Registry Exists)
|
||||||
|
|
||||||
|
#### 2.9 Cross-Project Mining
|
||||||
|
|
||||||
|
For each project in `.ciagent/config.json` projects array:
|
||||||
|
- Read that project's `---ci---` blocks for lessons, decisions, compound solutions
|
||||||
|
- Find patterns relevant to the current project (same requirement area, same tech stack from package.json)
|
||||||
|
- Suggest adaptations of lessons learned elsewhere
|
||||||
|
- Calculate relevance score based on tech stack similarity
|
||||||
|
|
||||||
|
## Step 3: Merge and DeduplicateIdeas
|
||||||
|
|
||||||
|
Combine ideas from all tiers. Deduplicate by:
|
||||||
|
- Same `title` strings → keep highest confidence version
|
||||||
|
- Same `relatedReq` → merge into single idea with combined sources
|
||||||
|
- Same `category` + overlapping domains → keep most specific
|
||||||
|
|
||||||
|
Sort by confidence (descending), then by number of corroborating signals.
|
||||||
|
|
||||||
|
## Step 4: Interactive Validation
|
||||||
|
|
||||||
|
Present ideas one-at-a-time to the user:
|
||||||
|
|
||||||
|
```
|
||||||
|
═══ Recommendation N of M ═══
|
||||||
|
|
||||||
|
Category: [CATEGORY] | Confidence: [0.XX] | Tier: [mechanical/backend-enriched/cross-project]
|
||||||
|
Title: [idea title]
|
||||||
|
Rationale: [idea rationale]
|
||||||
|
Related Req: [REQ-ID or "new requirement"]
|
||||||
|
Source: [source signal type]
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
1. Accept (add to next milestone as new requirement)
|
||||||
|
2. Skip
|
||||||
|
3. Modify (edit title/rationale before accepting)
|
||||||
|
4. Details (show full analysis including signal sources)
|
||||||
|
```
|
||||||
|
|
||||||
|
For each accepted idea:
|
||||||
|
1. Generate `IDEATE-NN` requirement ID
|
||||||
|
2. Prompt for milestone placement (append to existing or create new)
|
||||||
|
3. Add to REQUIREMENTS.md with status `pending`
|
||||||
|
4. Add to ROADMAP.md next milestone
|
||||||
|
|
||||||
|
## Step 5: Update Long-Term Documents
|
||||||
|
|
||||||
|
For each accepted idea:
|
||||||
|
|
||||||
|
### REQUIREMENTS.md
|
||||||
|
|
||||||
|
Add a new row in the appropriate milestone section:
|
||||||
|
```
|
||||||
|
| IDEATE-NN | [idea title] | [priority] | [phase] | pending |
|
||||||
|
```
|
||||||
|
|
||||||
|
### ROADMAP.md
|
||||||
|
|
||||||
|
Add the idea to the next milestone's phase structure:
|
||||||
|
- If next milestone has a matching phase category, append to that phase
|
||||||
|
- If no matching phase, suggest a new phase
|
||||||
|
|
||||||
|
### ARCHITECTURE.md
|
||||||
|
|
||||||
|
If the idea involves architectural changes, note the component change needed.
|
||||||
|
|
||||||
|
### PROJECT.md
|
||||||
|
|
||||||
|
If the idea adds new requirements or key decisions, update accordingly.
|
||||||
|
|
||||||
|
Commit all document updates:
|
||||||
|
```
|
||||||
|
decision(P##): ideation results — [N] accepted, [M] skipped
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Ask-After-Validation Kickoff
|
||||||
|
|
||||||
|
After all ideas have been validated:
|
||||||
|
|
||||||
|
```
|
||||||
|
Accepted: [N] recommendations
|
||||||
|
Skipped: [M] recommendations
|
||||||
|
|
||||||
|
Would you like to kick off the run workflow for these ideas? (y/n)
|
||||||
|
```
|
||||||
|
|
||||||
|
If yes: Start `ciagent run` with the updated project context. The `--ideate` flag is NOT needed because the ideas are already in ROADMAP.md and REQUIREMENTS.md — the standard pipeline will pick them up.
|
||||||
|
|
||||||
|
If no: Output summary and exit.
|
||||||
|
|
||||||
|
## Command Flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--category <cats>` | Focus on specific categories: security,quality,architecture,coverage,improvement,spec,chaos (comma-separated) |
|
||||||
|
| `--affected` | Cascade impact analysis: given current changes, what else needs updating |
|
||||||
|
| `--spec` | Analyze specification completeness and ambiguity |
|
||||||
|
| `--external` | Include external signals: npm audit, OSV advisories, dependency staleness |
|
||||||
|
| `--cross-project` | Mine patterns from all projects in multi-project registry |
|
||||||
|
| `--output <format>` | Output format: interactive (default), json, markdown |
|
||||||
|
| `--project <slugs>` | Target project(s): slug, comma-separated, or `all` |
|
||||||
|
| `--backend <provider>` | Override intelligence backend for enrichment tier |
|
||||||
|
|
||||||
|
## Pipeline Integration
|
||||||
|
|
||||||
|
When `ciagent run --ideate` is used, the IDEATE stage is inserted between RESEARCH and PLAN:
|
||||||
|
|
||||||
|
```
|
||||||
|
SPECIFY → CLARIFY → RESEARCH → IDEATE → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
|
||||||
|
```
|
||||||
|
|
||||||
|
IDEATE stage commit:
|
||||||
|
```
|
||||||
|
---ci---
|
||||||
|
phase: [phase-number]
|
||||||
|
milestone: [milestone-version]
|
||||||
|
status: ideate
|
||||||
|
decisions:
|
||||||
|
- id: D-XXX
|
||||||
|
decision: "Accepted [N] ideation recommendations"
|
||||||
|
rationale: "[summary of accepted ideas]"
|
||||||
|
confidence: [avg confidence]
|
||||||
|
requirements:
|
||||||
|
covered: [IDEATE-NN, ...]
|
||||||
|
---/ci---
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Modes
|
||||||
|
|
||||||
|
### Interactive (default)
|
||||||
|
|
||||||
|
Presented one-at-a-time with accept/skip/modify actions.
|
||||||
|
|
||||||
|
### JSON
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"project": "[slug]",
|
||||||
|
"milestone": "[version]",
|
||||||
|
"ideas": [
|
||||||
|
{
|
||||||
|
"id": "IDEATE-NN",
|
||||||
|
"source": "[source type]",
|
||||||
|
"category": "[category]",
|
||||||
|
"title": "[title]",
|
||||||
|
"rationale": "[rationale]",
|
||||||
|
"confidence": 0.XX,
|
||||||
|
"relatedReq": "[REQ-ID or null]",
|
||||||
|
"actions": ["[action types]"],
|
||||||
|
"tier": "[mechanical/backend-enriched/cross-project]",
|
||||||
|
"accepted": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"total": 8,
|
||||||
|
"accepted": 6,
|
||||||
|
"skipped": 2,
|
||||||
|
"by_category": { "coverage": 2, "architecture": 1, "security": 1, "quality": 1, "improvement": 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Markdown
|
||||||
|
|
||||||
|
Formatted report suitable for PR descriptions or documentation.
|
||||||
|
|
||||||
|
## Error Recovery
|
||||||
|
|
||||||
|
On tier failure:
|
||||||
|
1. Mechanical tier always succeeds (git + filesystem only)
|
||||||
|
2. Backend-enriched tier: if backend unavailable, fall back to mechanical-only output
|
||||||
|
3. Cross-project tier: if no other projects in registry, skip silently
|
||||||
|
|
||||||
|
On validation failure (no ideas generated):
|
||||||
|
- Output "No improvement ideas identified for this project."
|
||||||
|
- Suggest `ciagent ideate --spec` for specification analysis or `--external` for external signals
|
||||||
@@ -14,11 +14,18 @@ If no phase number specified, continues from the current phase (detected from gi
|
|||||||
|
|
||||||
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
|
Check `ci listProjects()` or read `.ciagent/config.json` to determine if multi-project mode is active.
|
||||||
|
|
||||||
If `.ciagent/config.json` has `projects[]` with length > 0:
|
If `.ciagent/config.json` has `projects[]` with length > 0, or `active_projects` array exists:
|
||||||
- Confirm `active_project` is correct for this run
|
- Confirm `active_projects` is correct for this run
|
||||||
- If not, set it with `ci setActiveProject(<slug>)`
|
- If `--project all` is specified: iterate over all projects in `active_projects`
|
||||||
|
- If `--project <slug>` is specified: run for that project only
|
||||||
|
- If no `--project` flag: use first project in `active_projects`
|
||||||
- All commit messages must include `project: <slug>` in `---ci---` block
|
- All commit messages must include `project: <slug>` in `---ci---` block
|
||||||
|
|
||||||
|
For multi-project execution (`--project all`):
|
||||||
|
- Execute pipeline for each project sequentially by default
|
||||||
|
- When `parallelization.enabled=true`: execute projects concurrently up to `max_concurrent_agents`
|
||||||
|
- Each project has independent phase branches and milestone tracking
|
||||||
|
|
||||||
If single-project mode: proceed with existing conventions.
|
If single-project mode: proceed with existing conventions.
|
||||||
|
|
||||||
## Step 1: Load Git Context
|
## Step 1: Load Git Context
|
||||||
@@ -60,6 +67,15 @@ For each stage in order (starting from current or from `specify`):
|
|||||||
- Update `.ciagent/` static files with conclusions
|
- Update `.ciagent/` static files with conclusions
|
||||||
- Commit: `docs(P##): research findings`
|
- Commit: `docs(P##): research findings`
|
||||||
|
|
||||||
|
### IDEATE (when --ideate flag is passed)
|
||||||
|
- Delegate to ci-ideation-agent
|
||||||
|
- Mine git history for patterns, analyze coverage gaps, detect drift
|
||||||
|
- If backend available: enrich with LLM suggestions
|
||||||
|
- If --cross-project: mine patterns from other projects
|
||||||
|
- Present recommendations interactively (accept/skip/modify)
|
||||||
|
- Accepted ideas update ROADMAP.md and REQUIREMENTS.md
|
||||||
|
- Commit: `decision(P##): ideation results — [N] accepted, [M] skipped`
|
||||||
|
|
||||||
### PLAN
|
### PLAN
|
||||||
- Delegate to ci-planner
|
- Delegate to ci-planner
|
||||||
- Create vertical-slice plans with wave ordering
|
- Create vertical-slice plans with wave ordering
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@continuous-intelligence/ciagent",
|
"name": "@continuous-intelligence/ciagent",
|
||||||
"version": "0.7.0",
|
"version": "0.9.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@continuous-intelligence/ciagent",
|
"name": "@continuous-intelligence/ciagent",
|
||||||
"version": "0.7.0",
|
"version": "0.9.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface AgentContext {
|
|||||||
specification: string;
|
specification: string;
|
||||||
config_path: string;
|
config_path: string;
|
||||||
backend?: IntelligenceBackend;
|
backend?: IntelligenceBackend;
|
||||||
|
project_slug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function backendResultToAgentResult(result: BackendResult): AgentResult {
|
export function backendResultToAgentResult(result: BackendResult): AgentResult {
|
||||||
|
|||||||
@@ -4,74 +4,24 @@ import * as os from "node:os";
|
|||||||
import { IdeationAgent } from "../agents/ideation-agent.js";
|
import { IdeationAgent } from "../agents/ideation-agent.js";
|
||||||
|
|
||||||
describe("IdeationAgent", () => {
|
describe("IdeationAgent", () => {
|
||||||
let tempDir: string;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-ideation-test-"));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("generates ideas from uncovered requirements", () => {
|
|
||||||
const ciagentDir = path.join(tempDir, ".ciagent");
|
|
||||||
fs.mkdirSync(ciagentDir, { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(ciagentDir, "REQUIREMENTS.md"),
|
|
||||||
"REQ-1: First requirement\nREQ-2: Second requirement"
|
|
||||||
);
|
|
||||||
|
|
||||||
const agent = new IdeationAgent();
|
|
||||||
const ideas = agent.mechanicalIdeate(tempDir);
|
|
||||||
|
|
||||||
const reqIdeas = ideas.filter((i) => i.source === "uncovered_requirement");
|
|
||||||
expect(reqIdeas.length).toBeGreaterThanOrEqual(2);
|
|
||||||
expect(reqIdeas.some((i) => i.relatedReq === "REQ-1")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("identifies coverage gaps from PROJECT.md", () => {
|
|
||||||
const ciagentDir = path.join(tempDir, ".ciagent");
|
|
||||||
fs.mkdirSync(ciagentDir, { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(ciagentDir, "PROJECT.md"),
|
|
||||||
"We use agent: magic-agent and agent: super-agent for tasks."
|
|
||||||
);
|
|
||||||
|
|
||||||
const srcDir = path.join(tempDir, "src", "agents");
|
|
||||||
fs.mkdirSync(srcDir, { recursive: true });
|
|
||||||
fs.writeFileSync(path.join(srcDir, "base.ts"), "");
|
|
||||||
fs.writeFileSync(path.join(srcDir, "index.ts"), "");
|
|
||||||
|
|
||||||
const agent = new IdeationAgent();
|
|
||||||
const gaps = agent.identifyCoverageGaps(tempDir);
|
|
||||||
|
|
||||||
expect(gaps).toContain("magic-agent");
|
|
||||||
expect(gaps).toContain("super-agent");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds repeated patterns from lessons list", () => {
|
|
||||||
const agent = new IdeationAgent();
|
|
||||||
const lessons = [
|
|
||||||
{ topic: "testing", detail: "testing: tests are flaky" },
|
|
||||||
{ topic: "testing", detail: "testing: more test failures" },
|
|
||||||
{ topic: "build", detail: "build: CI broken" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const repeated = agent.findRepeatedPatterns(lessons);
|
|
||||||
expect(repeated).toContain("testing");
|
|
||||||
expect(repeated).not.toContain("build");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns empty ideas when no project files exist", () => {
|
|
||||||
const agent = new IdeationAgent();
|
|
||||||
const ideas = agent.mechanicalIdeate(tempDir);
|
|
||||||
|
|
||||||
expect(ideas).toEqual(expect.arrayContaining([]));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("agent name is ideation-agent", () => {
|
it("agent name is ideation-agent", () => {
|
||||||
const agent = new IdeationAgent();
|
const agent = new IdeationAgent();
|
||||||
expect(agent.name).toBe("ideation-agent");
|
expect(agent.name).toBe("ideation-agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("workflow is research", () => {
|
||||||
|
const agent = new IdeationAgent();
|
||||||
|
expect(agent.workflow).toBe("research");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates mechanicalIdeate to IdeationEngine", () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-agent-test-"));
|
||||||
|
try {
|
||||||
|
const agent = new IdeationAgent();
|
||||||
|
const ideas = agent.mechanicalIdeate(tempDir);
|
||||||
|
expect(Array.isArray(ideas)).toBe(true);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,18 +1,9 @@
|
|||||||
import * as fs from "node:fs";
|
|
||||||
import * as path from "node:path";
|
|
||||||
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||||
|
import { IdeationEngine } from "../core/ideation.js";
|
||||||
interface Idea {
|
|
||||||
source: "uncovered_requirement" | "repeated_lesson" | "gap_in_coverage" | "improvement_pattern";
|
|
||||||
title: string;
|
|
||||||
rationale: string;
|
|
||||||
confidence: number;
|
|
||||||
relatedReq?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class IdeationAgent extends BaseAgent {
|
export class IdeationAgent extends BaseAgent {
|
||||||
readonly name = "ideation-agent";
|
readonly name = "ideation-agent";
|
||||||
readonly description = "Generates improvement ideas. Output feeds directly into planning pipeline.";
|
readonly description = "Generates improvement ideas using git-native pattern mining, coverage gap analysis, and architectural drift detection. Output feeds directly into planning pipeline.";
|
||||||
readonly workflow = "research";
|
readonly workflow = "research";
|
||||||
|
|
||||||
async execute(context: AgentContext): Promise<AgentResult> {
|
async execute(context: AgentContext): Promise<AgentResult> {
|
||||||
@@ -27,8 +18,9 @@ export class IdeationAgent extends BaseAgent {
|
|||||||
return { ...result, duration_ms: Date.now() - start };
|
return { ...result, duration_ms: Date.now() - start };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ideas = this.mechanicalIdeate(context.project_path);
|
const engine = new IdeationEngine(context.project_path);
|
||||||
const output = this.formatIdeas(ideas);
|
const ideas = engine.runMechanical();
|
||||||
|
const output = engine.formatIdeas(ideas);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -40,153 +32,8 @@ export class IdeationAgent extends BaseAgent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mechanicalIdeate(projectPath: string): Idea[] {
|
mechanicalIdeate(projectPath: string) {
|
||||||
const ideas: Idea[] = [];
|
const engine = new IdeationEngine(projectPath);
|
||||||
const uncoveredReqs = this.readUncoveredRequirements(projectPath);
|
return engine.runMechanical();
|
||||||
const lessons = this.readRecentLessons(projectPath);
|
|
||||||
const repeated = this.findRepeatedPatterns(lessons);
|
|
||||||
const coverageGaps = this.identifyCoverageGaps(projectPath);
|
|
||||||
|
|
||||||
for (const req of uncoveredReqs) {
|
|
||||||
ideas.push({
|
|
||||||
source: "uncovered_requirement",
|
|
||||||
title: `Address uncovered requirement: ${req}`,
|
|
||||||
rationale: `Requirement ${req} has no corresponding implementation task.`,
|
|
||||||
confidence: 0.8,
|
|
||||||
relatedReq: req,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const topic of repeated) {
|
|
||||||
ideas.push({
|
|
||||||
source: "repeated_lesson",
|
|
||||||
title: `Investigate repeated lesson: ${topic}`,
|
|
||||||
rationale: `Topic "${topic}" appears in multiple commit lessons, indicating a systemic issue.`,
|
|
||||||
confidence: 0.7,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const gap of coverageGaps) {
|
|
||||||
ideas.push({
|
|
||||||
source: "gap_in_coverage",
|
|
||||||
title: `Fill coverage gap: ${gap}`,
|
|
||||||
rationale: `Agent "${gap}" is claimed in PROJECT.md but not found in the agent registry.`,
|
|
||||||
confidence: 0.75,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.generateIdeas(uncoveredReqs, repeated, ideas);
|
|
||||||
|
|
||||||
return ideas;
|
|
||||||
}
|
|
||||||
|
|
||||||
readUncoveredRequirements(projectPath: string): string[] {
|
|
||||||
const reqPath = path.join(projectPath, ".ciagent", "REQUIREMENTS.md");
|
|
||||||
if (!fs.existsSync(reqPath)) return [];
|
|
||||||
|
|
||||||
const content = fs.readFileSync(reqPath, "utf-8");
|
|
||||||
const reqIds: string[] = [];
|
|
||||||
const reqIdRegex = /REQ-(\d+)/g;
|
|
||||||
let match;
|
|
||||||
while ((match = reqIdRegex.exec(content)) !== null) {
|
|
||||||
reqIds.push(`REQ-${match[1]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const planPath = path.join(projectPath, ".ciagent", "PLAN.md");
|
|
||||||
if (!fs.existsSync(planPath)) return reqIds;
|
|
||||||
|
|
||||||
const planContent = fs.readFileSync(planPath, "utf-8");
|
|
||||||
const coveredReqIds = new Set<string>();
|
|
||||||
const planRegex = /REQ-(\d+)/g;
|
|
||||||
let planMatch;
|
|
||||||
while ((planMatch = planRegex.exec(planContent)) !== null) {
|
|
||||||
coveredReqIds.add(`REQ-${planMatch[1]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return reqIds.filter((id) => !coveredReqIds.has(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
readRecentLessons(projectPath: string): Array<{ topic: string; detail: string }> {
|
|
||||||
const lessons: Array<{ topic: string; detail: string }> = [];
|
|
||||||
try {
|
|
||||||
const { execSync } = require("node:child_process");
|
|
||||||
const log = execSync('git log --grep="lessons:" --format="%B" -50', {
|
|
||||||
cwd: projectPath,
|
|
||||||
encoding: "utf-8",
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
const lessonsRegex = /lessons:\s*\n((?:\s+-\s+.+\n?)+)/g;
|
|
||||||
let match;
|
|
||||||
while ((match = lessonsRegex.exec(log)) !== null) {
|
|
||||||
const items = match[1].split("\n").filter((l: string) => l.trim().startsWith("-"));
|
|
||||||
for (const item of items) {
|
|
||||||
const detail = item.replace(/^\s*-\s*/, "").trim();
|
|
||||||
const topic = detail.split(":")[0].trim().toLowerCase();
|
|
||||||
lessons.push({ topic, detail });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
return lessons;
|
|
||||||
}
|
|
||||||
|
|
||||||
findRepeatedPatterns(lessons: Array<{ topic: string; detail: string }>): string[] {
|
|
||||||
const counts: Record<string, number> = {};
|
|
||||||
for (const lesson of lessons) {
|
|
||||||
counts[lesson.topic] = (counts[lesson.topic] || 0) + 1;
|
|
||||||
}
|
|
||||||
return Object.entries(counts)
|
|
||||||
.filter(([, count]) => count > 1)
|
|
||||||
.map(([topic]) => topic);
|
|
||||||
}
|
|
||||||
|
|
||||||
generateIdeas(uncoveredReqs: string[], repeated: string[], ideas: Idea[]): void {
|
|
||||||
const repeatedSet = new Set(repeated.map((r) => r.toLowerCase()));
|
|
||||||
for (const req of uncoveredReqs) {
|
|
||||||
for (const topic of repeated) {
|
|
||||||
if (req.toLowerCase().includes(topic) || topic.includes(req.toLowerCase())) {
|
|
||||||
ideas.push({
|
|
||||||
source: "improvement_pattern",
|
|
||||||
title: `Cross-reference: ${req} ↔ ${topic}`,
|
|
||||||
rationale: `Repeated lesson "${topic}" directly relates to uncovered requirement ${req}.`,
|
|
||||||
confidence: 0.85,
|
|
||||||
relatedReq: req,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
identifyCoverageGaps(projectPath: string): string[] {
|
|
||||||
const projectMdPath = path.join(projectPath, ".ciagent", "PROJECT.md");
|
|
||||||
if (!fs.existsSync(projectMdPath)) return [];
|
|
||||||
|
|
||||||
const content = fs.readFileSync(projectMdPath, "utf-8");
|
|
||||||
const agentMentionRegex = /(?:agent|Agent):\s*(\S+)/g;
|
|
||||||
const mentionedAgents: string[] = [];
|
|
||||||
let match;
|
|
||||||
while ((match = agentMentionRegex.exec(content)) !== null) {
|
|
||||||
mentionedAgents.push(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentsDir = path.join(projectPath, "src", "agents");
|
|
||||||
if (!fs.existsSync(agentsDir)) return mentionedAgents;
|
|
||||||
|
|
||||||
const existingAgents = new Set(
|
|
||||||
fs.readdirSync(agentsDir)
|
|
||||||
.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts") && f !== "index.ts" && f !== "base.ts")
|
|
||||||
.map((f) => f.replace(".ts", ""))
|
|
||||||
);
|
|
||||||
|
|
||||||
return mentionedAgents.filter((a) => !existingAgents.has(a) && !existingAgents.has(a.replace(/-agent$/, "")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatIdeas(ideas: Idea[]): string {
|
|
||||||
if (ideas.length === 0) return "No improvement ideas generated.";
|
|
||||||
const lines: string[] = ["Improvement Ideas:", ""];
|
|
||||||
for (const idea of ideas) {
|
|
||||||
lines.push(`[${idea.source}|${idea.confidence.toFixed(2)}] ${idea.title} — ${idea.rationale}${idea.relatedReq ? ` (req: ${idea.relatedReq})` : ""}`);
|
|
||||||
}
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+162
-1
@@ -47,6 +47,7 @@ export class OrchestratorAgent extends BaseAgent {
|
|||||||
|
|
||||||
private static readonly STAGE_AGENT_MAP: Partial<Record<PipelineStage, AgentName[]>> = {
|
private static readonly STAGE_AGENT_MAP: Partial<Record<PipelineStage, AgentName[]>> = {
|
||||||
research: ["researcher"],
|
research: ["researcher"],
|
||||||
|
ideate: ["ideation-agent"],
|
||||||
plan: ["planner"],
|
plan: ["planner"],
|
||||||
execute: ["executor", "code-reviewer", "security-auditor"],
|
execute: ["executor", "code-reviewer", "security-auditor"],
|
||||||
test: ["tester"],
|
test: ["tester"],
|
||||||
@@ -67,9 +68,10 @@ export class OrchestratorAgent extends BaseAgent {
|
|||||||
try {
|
try {
|
||||||
this.config = loadConfig(context.project_path);
|
this.config = loadConfig(context.project_path);
|
||||||
|
|
||||||
|
const projectSlug = context.project_slug || "";
|
||||||
this.gitContext = new GitContext(context.project_path);
|
this.gitContext = new GitContext(context.project_path);
|
||||||
this.gitBranch = new GitBranch(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();
|
this.ciFiles.ensureCIDir();
|
||||||
|
|
||||||
const projectState = this.gitContext.reconstructState();
|
const projectState = this.gitContext.reconstructState();
|
||||||
@@ -459,6 +461,7 @@ export class OrchestratorAgent extends BaseAgent {
|
|||||||
projectName: spec.objective.slice(0, 30),
|
projectName: spec.objective.slice(0, 30),
|
||||||
phaseCount: 0,
|
phaseCount: 0,
|
||||||
milestone: this.currentMilestone,
|
milestone: this.currentMilestone,
|
||||||
|
project: context.project_slug || undefined,
|
||||||
specification: spec.raw_content,
|
specification: spec.raw_content,
|
||||||
requirements: spec.requirements,
|
requirements: spec.requirements,
|
||||||
constraints: spec.constraints,
|
constraints: spec.constraints,
|
||||||
@@ -571,6 +574,69 @@ export class OrchestratorAgent extends BaseAgent {
|
|||||||
break;
|
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":
|
case "plan":
|
||||||
this.log("Planning phase execution...");
|
this.log("Planning phase execution...");
|
||||||
|
|
||||||
@@ -790,4 +856,99 @@ export class OrchestratorAgent extends BaseAgent {
|
|||||||
|
|
||||||
return lines.join("\n");
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+438
-7
@@ -1,5 +1,6 @@
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import { CIAgentConfig, AutonomyLevel } from "../types/config.js";
|
import { CIAgentConfig, AutonomyLevel } from "../types/config.js";
|
||||||
|
import { IdeationCategory, Idea } from "../types/ideation.js";
|
||||||
import { initCIAgent, loadConfig, isCIAgentInitialized, saveConfig } from "../core/config.js";
|
import { initCIAgent, loadConfig, isCIAgentInitialized, saveConfig } from "../core/config.js";
|
||||||
import { Specification, parseSpecification } from "../types/specification.js";
|
import { Specification, parseSpecification } from "../types/specification.js";
|
||||||
import { saveSpecification } from "../core/clarify.js";
|
import { saveSpecification } from "../core/clarify.js";
|
||||||
@@ -9,7 +10,7 @@ import { getAuditSummary, readAudit } from "../core/audit.js";
|
|||||||
import { VerificationPipeline } from "../verification/index.js";
|
import { VerificationPipeline } from "../verification/index.js";
|
||||||
import { ClarifyPhase } from "../core/clarify.js";
|
import { ClarifyPhase } from "../core/clarify.js";
|
||||||
import { loadSpecification as loadSpec } 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 { ErrorRecovery } from "../core/error-recovery.js";
|
||||||
import { PipelineState, createInitialPipelineState } from "../types/pipeline.js";
|
import { PipelineState, createInitialPipelineState } from "../types/pipeline.js";
|
||||||
import { resolveBackend } from "../backends/index.js";
|
import { resolveBackend } from "../backends/index.js";
|
||||||
@@ -19,6 +20,7 @@ import { CIAgentFiles } from "../core/ciagent-files.js";
|
|||||||
import { GiteaClient, generateReleaseNotes } from "../core/gitea.js";
|
import { GiteaClient, generateReleaseNotes } from "../core/gitea.js";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
|
import * as readline from "node:readline";
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
export function createInitCommand(): Command {
|
export function createInitCommand(): Command {
|
||||||
@@ -77,6 +79,7 @@ export function createInitCommand(): Command {
|
|||||||
enabled: options.parallel !== false,
|
enabled: options.parallel !== false,
|
||||||
max_concurrent_agents: 5,
|
max_concurrent_agents: 5,
|
||||||
min_plans_for_parallel: 2,
|
min_plans_for_parallel: 2,
|
||||||
|
max_concurrent_projects: 3,
|
||||||
},
|
},
|
||||||
backend: {
|
backend: {
|
||||||
provider: options.backend || "auto",
|
provider: options.backend || "auto",
|
||||||
@@ -167,6 +170,8 @@ export function createRunCommand(): Command {
|
|||||||
.option("--all", "Execute all remaining phases sequentially")
|
.option("--all", "Execute all remaining phases sequentially")
|
||||||
.option("--phase <number>", "Phase number", "1")
|
.option("--phase <number>", "Phase number", "1")
|
||||||
.option("--backend <provider>", "Override intelligence backend for this run")
|
.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) => {
|
.action(async (phase, options) => {
|
||||||
const projectPath = process.cwd();
|
const projectPath = process.cwd();
|
||||||
|
|
||||||
@@ -176,6 +181,141 @@ export function createRunCommand(): Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = loadConfig(projectPath);
|
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);
|
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
|
||||||
|
|
||||||
if (!backend && backendError) {
|
if (!backend && backendError) {
|
||||||
@@ -191,6 +331,7 @@ export function createRunCommand(): Command {
|
|||||||
specification: "",
|
specification: "",
|
||||||
config_path: path.join(projectPath, ".ciagent", "config.json"),
|
config_path: path.join(projectPath, ".ciagent", "config.json"),
|
||||||
backend,
|
backend,
|
||||||
|
project_slug: projectSlug || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const spec = loadSpec(projectPath);
|
const spec = loadSpec(projectPath);
|
||||||
@@ -198,7 +339,7 @@ export function createRunCommand(): Command {
|
|||||||
context.specification = spec.raw_content;
|
context.specification = spec.raw_content;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Running CIAgent pipeline...`);
|
console.log(`Running CIAgent pipeline${projectSlug ? ` for project: ${projectSlug}` : ""}...`);
|
||||||
if (options.all) {
|
if (options.all) {
|
||||||
console.log(" Mode: Full pipeline (all phases)");
|
console.log(" Mode: Full pipeline (all phases)");
|
||||||
} else {
|
} else {
|
||||||
@@ -412,7 +553,8 @@ export function createReviewCommand(): Command {
|
|||||||
export function createStatusCommand(): Command {
|
export function createStatusCommand(): Command {
|
||||||
return new Command("status")
|
return new Command("status")
|
||||||
.description("Non-interactive project status")
|
.description("Non-interactive project status")
|
||||||
.action(() => {
|
.option("--project <slug>", "Show status for specific project (comma-separated or 'all')")
|
||||||
|
.action((options) => {
|
||||||
const projectPath = process.cwd();
|
const projectPath = process.cwd();
|
||||||
|
|
||||||
if (!isCIAgentInitialized(projectPath)) {
|
if (!isCIAgentInitialized(projectPath)) {
|
||||||
@@ -422,14 +564,31 @@ export function createStatusCommand(): Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = loadConfig(projectPath);
|
const config = loadConfig(projectPath);
|
||||||
|
const ciFiles = new CIAgentFiles(projectPath);
|
||||||
const artifacts = new ArtifactManager(projectPath);
|
const artifacts = new ArtifactManager(projectPath);
|
||||||
|
|
||||||
console.log("─── CIAgent Project Status ───");
|
const activeProjects: string[] = (config as any).active_projects?.length > 0
|
||||||
console.log(`\nAutonomy: ${config.autonomy.level}`);
|
? (config as any).active_projects
|
||||||
|
: config.active_project ? [config.active_project] : [];
|
||||||
|
|
||||||
|
console.log("─── CIAgent Project Status ───\n");
|
||||||
|
|
||||||
|
if (activeProjects.length > 1 || (options.project && options.project === "all")) {
|
||||||
|
console.log(`Active Projects: ${activeProjects.join(", ")}`);
|
||||||
|
console.log(`Total: ${activeProjects.length} projects`);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Autonomy: ${config.autonomy.level}`);
|
||||||
console.log(`Model Profile: ${config.model_profile}`);
|
console.log(`Model Profile: ${config.model_profile}`);
|
||||||
console.log(`Backend: ${config.backend?.provider || "auto"}`);
|
console.log(`Backend: ${config.backend?.provider || "auto"}`);
|
||||||
console.log(`Parallelization: ${config.parallelization.enabled ? "enabled" : "disabled"}`);
|
console.log(`Parallelization: ${config.parallelization.enabled ? "enabled" : "disabled"}`);
|
||||||
|
|
||||||
|
const ideationConfig = (config as any).ideation;
|
||||||
|
if (ideationConfig) {
|
||||||
|
console.log(`Ideation: ${ideationConfig.enabled ? "enabled" : "disabled"} (categories: ${ideationConfig.categories?.join(", ") || "default"})`);
|
||||||
|
}
|
||||||
|
|
||||||
const state = artifacts.readState();
|
const state = artifacts.readState();
|
||||||
if (state) {
|
if (state) {
|
||||||
console.log(`\nCurrent Phase: ${state.current_phase}`);
|
console.log(`\nCurrent Phase: ${state.current_phase}`);
|
||||||
@@ -660,6 +819,9 @@ export function createProjectsCommand(): Command {
|
|||||||
const ciFiles = new CIAgentFiles(projectPath);
|
const ciFiles = new CIAgentFiles(projectPath);
|
||||||
const projects = ciFiles.listProjects();
|
const projects = ciFiles.listProjects();
|
||||||
const activeProject = config.active_project || ciFiles.getActiveProject();
|
const activeProject = config.active_project || ciFiles.getActiveProject();
|
||||||
|
const activeProjects: string[] = (config as any).active_projects?.length > 0
|
||||||
|
? (config as any).active_projects
|
||||||
|
: activeProject ? [activeProject] : [];
|
||||||
|
|
||||||
if (projects.length === 0) {
|
if (projects.length === 0) {
|
||||||
console.log("No projects registered.");
|
console.log("No projects registered.");
|
||||||
@@ -669,11 +831,13 @@ export function createProjectsCommand(): Command {
|
|||||||
|
|
||||||
console.log("─── CIAgent Projects ───\n");
|
console.log("─── CIAgent Projects ───\n");
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
const isActive = project.slug === activeProject;
|
const isActive = activeProjects.includes(project.slug);
|
||||||
const marker = isActive ? " *" : "";
|
const marker = isActive ? " *" : "";
|
||||||
console.log(` ${project.slug} — ${project.name}${marker}`);
|
console.log(` ${project.slug} — ${project.name}${marker}`);
|
||||||
}
|
}
|
||||||
console.log("\n * = active project");
|
if (activeProjects.length > 0) {
|
||||||
|
console.log(`\n Active: ${activeProjects.join(", ")}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cmd.command("add <slug> <name>")
|
cmd.command("add <slug> <name>")
|
||||||
@@ -712,6 +876,7 @@ export function createProjectsCommand(): Command {
|
|||||||
ciFiles.setActiveProject(slug);
|
ciFiles.setActiveProject(slug);
|
||||||
const config = loadConfig(projectPath);
|
const config = loadConfig(projectPath);
|
||||||
config.active_project = slug;
|
config.active_project = slug;
|
||||||
|
(config as any).active_projects = [slug];
|
||||||
saveConfig(projectPath, config);
|
saveConfig(projectPath, config);
|
||||||
console.log(`✓ Active project set to: ${slug}`);
|
console.log(`✓ Active project set to: ${slug}`);
|
||||||
});
|
});
|
||||||
@@ -941,4 +1106,270 @@ function getPreviousTag(projectPath: string, currentTag: string): string | null
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createIdeateCommand(): Command {
|
||||||
|
return new Command("ideate")
|
||||||
|
.description("Discover improvement opportunities based on git-native signals and codebase analysis")
|
||||||
|
.option("-c, --category <categories>", "Focus on specific categories: security,quality,architecture,coverage,improvement,spec,chaos (comma-separated)")
|
||||||
|
.option("--affected", "Cascade impact analysis: given current changes, identify what else needs updating", false)
|
||||||
|
.option("--spec", "Analyze specification completeness and ambiguity", false)
|
||||||
|
.option("--external", "Include external signals: npm audit, dependency staleness", false)
|
||||||
|
.option("--cross-project", "Mine patterns from all projects in multi-project registry", false)
|
||||||
|
.option("--output <format>", "Output format: interactive, json, markdown", "interactive")
|
||||||
|
.option("--project <slug>", "Target project slug (comma-separated or 'all')")
|
||||||
|
.action(async (options) => {
|
||||||
|
const projectPath = process.cwd();
|
||||||
|
|
||||||
|
if (!isCIAgentInitialized(projectPath)) {
|
||||||
|
console.error("CIAgent project not initialized in this directory.");
|
||||||
|
console.error("Run 'ciagent init' to get started.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ciFiles = new CIAgentFiles(projectPath);
|
||||||
|
const config = loadConfig(projectPath);
|
||||||
|
|
||||||
|
const allProjects: string[] = options.project === "all"
|
||||||
|
? ciFiles.listProjects().map((p) => p.slug)
|
||||||
|
: options.project
|
||||||
|
? options.project.split(",").map((s: string) => s.trim()).filter(Boolean)
|
||||||
|
: [ciFiles.getProjectSlug() || ciFiles.getActiveProject() || "default"];
|
||||||
|
|
||||||
|
if (allProjects.length > 1) {
|
||||||
|
console.log(`\n─── CIAgent Ideation (multi-project: ${allProjects.join(", ")}) ───\n`);
|
||||||
|
} else {
|
||||||
|
console.log("\n─── CIAgent Ideation ───");
|
||||||
|
console.log(`Project: ${allProjects[0]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { IdeationEngine } = await import("../core/ideation.js");
|
||||||
|
|
||||||
|
const allIdeasByProject: Record<string, Idea[]> = {};
|
||||||
|
const allIdeas: Idea[] = [];
|
||||||
|
const seenTitles = new Set<string>();
|
||||||
|
|
||||||
|
for (const slug of allProjects) {
|
||||||
|
const engine = new IdeationEngine(projectPath, slug);
|
||||||
|
ciFiles.setProjectSlug(slug);
|
||||||
|
|
||||||
|
const categories: IdeationCategory[] = options.category
|
||||||
|
? options.category.split(",").map((c: string) => c.trim() as IdeationCategory)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
console.log(`\nMining git history for patterns in project: ${slug}...`);
|
||||||
|
|
||||||
|
let projectIdeas: Idea[] = engine.runMechanical(categories.length > 0 ? categories : undefined);
|
||||||
|
|
||||||
|
if (options.affected) {
|
||||||
|
console.log(`Running cascade impact analysis (--affected) for ${slug}...`);
|
||||||
|
const affectedIdeas = engine.runAffected();
|
||||||
|
projectIdeas = [...projectIdeas, ...affectedIdeas];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.spec) {
|
||||||
|
console.log(`Running specification analysis (--spec) for ${slug}...`);
|
||||||
|
const specIdeas = engine.runMechanical(["spec"]);
|
||||||
|
const newSpecIdeas = specIdeas.filter(
|
||||||
|
(idea: Idea) => !projectIdeas.some((existing: Idea) => existing.title === idea.title)
|
||||||
|
);
|
||||||
|
projectIdeas = [...projectIdeas, ...newSpecIdeas];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.external) {
|
||||||
|
console.log(`Running external signal analysis (--external) for ${slug}...`);
|
||||||
|
const externalIdeas = engine.runExternal();
|
||||||
|
projectIdeas = [...projectIdeas, ...externalIdeas];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.crossProject && ciFiles.isMultiProject()) {
|
||||||
|
console.log(`Running cross-project pattern mining (--cross-project) for ${slug}...`);
|
||||||
|
const crossProjectIdeas = engine.runCrossProject();
|
||||||
|
projectIdeas = [...projectIdeas, ...crossProjectIdeas];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueProjectIdeas = projectIdeas.filter((idea: Idea) => {
|
||||||
|
const dedupeKey = allProjects.length > 1 ? `${slug}:${idea.title}` : idea.title;
|
||||||
|
if (seenTitles.has(dedupeKey)) return false;
|
||||||
|
seenTitles.add(dedupeKey);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueProjectIdeas.sort((a: Idea, b: Idea) => b.confidence - a.confidence);
|
||||||
|
allIdeasByProject[slug] = uniqueProjectIdeas;
|
||||||
|
allIdeas.push(...uniqueProjectIdeas);
|
||||||
|
}
|
||||||
|
|
||||||
|
allIdeas.sort((a, b) => b.confidence - a.confidence);
|
||||||
|
|
||||||
|
const currentSlug = allProjects.length === 1 ? allProjects[0] : "all";
|
||||||
|
const engine = new IdeationEngine(projectPath, allProjects.length === 1 ? allProjects[0] : undefined);
|
||||||
|
|
||||||
|
if (options.output === "json") {
|
||||||
|
const result = engine.formatIdeasJson(allIdeas);
|
||||||
|
result.summary.accepted = 0;
|
||||||
|
result.summary.skipped = allIdeas.length;
|
||||||
|
result.project = currentSlug;
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.output === "markdown") {
|
||||||
|
console.log("\n## Ideation Results\n");
|
||||||
|
if (allIdeas.length === 0) {
|
||||||
|
console.log("No improvement ideas identified for this project.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allProjects.length > 1) {
|
||||||
|
console.log("| Project | Idea | Category | Confidence | Tier |");
|
||||||
|
console.log("|---------|-------|----------|------------|------|");
|
||||||
|
for (const slug of allProjects) {
|
||||||
|
const projectIdeas = allIdeasByProject[slug] || [];
|
||||||
|
for (const idea of projectIdeas) {
|
||||||
|
console.log(`| ${slug} | ${idea.title} | ${idea.category} | ${idea.confidence.toFixed(2)} | ${idea.tier} |`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const idea of allIdeas) {
|
||||||
|
console.log(`### ${idea.title}`);
|
||||||
|
console.log(`- **Category**: ${idea.category}`);
|
||||||
|
console.log(`- **Source**: ${idea.source}`);
|
||||||
|
console.log(`- **Confidence**: ${idea.confidence.toFixed(2)}`);
|
||||||
|
console.log(`- **Tier**: ${idea.tier}`);
|
||||||
|
console.log(`- **Rationale**: ${idea.rationale}`);
|
||||||
|
if (idea.relatedReq) console.log(`- **Related Req**: ${idea.relatedReq}`);
|
||||||
|
console.log(`- **Actions**: ${idea.actions.join(", ")}`);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nFound ${allIdeas.length} improvement ${allIdeas.length === 1 ? "idea" : "ideas"}${allProjects.length > 1 ? ` across ${allProjects.length} projects` : ""}\n`);
|
||||||
|
|
||||||
|
if (allIdeas.length === 0) {
|
||||||
|
console.log("No improvement ideas identified for this project.");
|
||||||
|
console.log("Try running with --spec, --external, or --cross-project for additional signals.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.output !== "interactive") {
|
||||||
|
console.log("Use --output interactive for accept/skip/modify validation.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accepted: Idea[] = [];
|
||||||
|
const skipped: Idea[] = [];
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
const askQuestion = (question: string): Promise<string> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer: string) => {
|
||||||
|
resolve(answer.trim().toLowerCase());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < allIdeas.length; i++) {
|
||||||
|
const idea = allIdeas[i];
|
||||||
|
const projectLabel = allProjects.length > 1 ? ` [${idea.tier === "cross-project" ? "cross-project" : allProjects[0]}]` : "";
|
||||||
|
console.log(`\n═══ Recommendation ${i + 1} of ${allIdeas.length} ═══\n`);
|
||||||
|
console.log(` Category: ${idea.category.toUpperCase()} | Confidence: ${idea.confidence.toFixed(2)} | Tier: ${idea.tier}${projectLabel}`);
|
||||||
|
console.log(` Title: ${idea.title}`);
|
||||||
|
console.log(` Rationale: ${idea.rationale}`);
|
||||||
|
if (idea.relatedReq) console.log(` Related Req: ${idea.relatedReq}`);
|
||||||
|
console.log(` Source: ${idea.source}`);
|
||||||
|
console.log(` Actions: ${idea.actions.join(", ")}`);
|
||||||
|
console.log("");
|
||||||
|
console.log(" 1) Accept (add to next milestone)");
|
||||||
|
console.log(" 2) Skip");
|
||||||
|
console.log(" 3) Details (show full analysis)");
|
||||||
|
|
||||||
|
const answer = await askQuestion(" > ");
|
||||||
|
|
||||||
|
if (answer === "1" || answer === "a" || answer === "accept") {
|
||||||
|
accepted.push(idea);
|
||||||
|
console.log(` ✓ Accepted: ${idea.id} — ${idea.title}`);
|
||||||
|
} else if (answer === "3" || answer === "d" || answer === "details") {
|
||||||
|
console.log(`\n ─── Details for ${idea.id} ───`);
|
||||||
|
console.log(` ID: ${idea.id}`);
|
||||||
|
console.log(` Source: ${idea.source}`);
|
||||||
|
console.log(` Category: ${idea.category}`);
|
||||||
|
console.log(` Confidence: ${idea.confidence.toFixed(2)}`);
|
||||||
|
console.log(` Tier: ${idea.tier}`);
|
||||||
|
console.log(` Title: ${idea.title}`);
|
||||||
|
console.log(` Rationale: ${idea.rationale}`);
|
||||||
|
if (idea.relatedReq) console.log(` Related Req: ${idea.relatedReq}`);
|
||||||
|
console.log(` Actions: ${idea.actions.join(", ")}`);
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
const retryAnswer = await askQuestion(" Accept this idea? (y/n) > ");
|
||||||
|
if (retryAnswer === "y" || retryAnswer === "yes") {
|
||||||
|
accepted.push(idea);
|
||||||
|
console.log(` ✓ Accepted: ${idea.id} — ${idea.title}`);
|
||||||
|
} else {
|
||||||
|
skipped.push(idea);
|
||||||
|
console.log(` ✗ Skipped: ${idea.id}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
skipped.push(idea);
|
||||||
|
console.log(` ✗ Skipped: ${idea.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
console.log("\n─── Summary ───\n");
|
||||||
|
console.log(`Accepted: ${accepted.length} recommendation${accepted.length === 1 ? "" : "s"}`);
|
||||||
|
console.log(`Skipped: ${skipped.length} recommendation${skipped.length === 1 ? "" : "s"}`);
|
||||||
|
|
||||||
|
if (allProjects.length > 1) {
|
||||||
|
console.log(`Projects: ${allProjects.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accepted.length > 0) {
|
||||||
|
console.log("\nAccepted ideas:");
|
||||||
|
for (const idea of accepted) {
|
||||||
|
console.log(` ${idea.id}: ${idea.title} (${idea.category.toUpperCase()})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const slug of allProjects) {
|
||||||
|
const projectAccepted = accepted.filter((idea) => {
|
||||||
|
return allIdeasByProject[slug]?.some((pi) => pi.id === idea.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (projectAccepted.length > 0) {
|
||||||
|
const projEngine = new IdeationEngine(projectPath, slug);
|
||||||
|
const { accepted: savedIdeas, results } = projEngine.acceptIdeas(projectAccepted);
|
||||||
|
const savedCount = results.filter((r) => r.addedToRequirements || r.addedToRoadmap).length;
|
||||||
|
|
||||||
|
if (savedCount > 0) {
|
||||||
|
console.log(`\n${savedCount} idea${savedCount === 1 ? "" : "s"} for project "${slug}" added to REQUIREMENTS.md and ROADMAP.md.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const kickoffAnswer = await askQuestion("\nWould you like to kick off the run workflow for these ideas? (y/n) > ");
|
||||||
|
if (kickoffAnswer === "y" || kickoffAnswer === "yes") {
|
||||||
|
console.log("\nStarting CIAgent pipeline...");
|
||||||
|
console.log("Run: ciagent run --ideate\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
const byCategory: Record<string, number> = {};
|
||||||
|
for (const idea of allIdeas) {
|
||||||
|
byCategory[idea.category] = (byCategory[idea.category] || 0) + 1;
|
||||||
|
}
|
||||||
|
console.log("\n─── Category Breakdown ───\n");
|
||||||
|
for (const [cat, count] of Object.entries(byCategory)) {
|
||||||
|
console.log(` ${cat}: ${count}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
+8
-3
@@ -17,6 +17,7 @@ import {
|
|||||||
createRollbackCommand,
|
createRollbackCommand,
|
||||||
createShipCommand,
|
createShipCommand,
|
||||||
createProjectsCommand,
|
createProjectsCommand,
|
||||||
|
createIdeateCommand,
|
||||||
} from "./commands.js";
|
} from "./commands.js";
|
||||||
|
|
||||||
let activeEscalationProtocol: { dispose(): void } | null = null;
|
let activeEscalationProtocol: { dispose(): void } | null = null;
|
||||||
@@ -44,12 +45,15 @@ program
|
|||||||
.name("ciagent")
|
.name("ciagent")
|
||||||
.description("CIAgent — Continuous Intelligence: autonomous AI-driven software engineering harness")
|
.description("CIAgent — Continuous Intelligence: autonomous AI-driven software engineering harness")
|
||||||
.version(VERSION)
|
.version(VERSION)
|
||||||
.option("--project <slug>", "Specify which project to operate on")
|
.option("--project <slug>", "Specify which project to operate on (comma-separated or 'all')")
|
||||||
.hook("preAction", () => {
|
.hook("preAction", () => {
|
||||||
const opts = program.opts();
|
const opts = program.opts();
|
||||||
if (opts.project && isCIAgentInitialized(process.cwd())) {
|
if (opts.project && isCIAgentInitialized(process.cwd())) {
|
||||||
const ciFiles = new CIAgentFiles(process.cwd());
|
const ciFiles = new CIAgentFiles(process.cwd());
|
||||||
ciFiles.setProjectSlug(opts.project);
|
const projectSlug = opts.project;
|
||||||
|
if (projectSlug !== "all" && !projectSlug.includes(",")) {
|
||||||
|
ciFiles.setProjectSlug(projectSlug);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addCommand(createInitCommand())
|
.addCommand(createInitCommand())
|
||||||
@@ -63,6 +67,7 @@ program
|
|||||||
.addCommand(createClarifyCommand())
|
.addCommand(createClarifyCommand())
|
||||||
.addCommand(createRollbackCommand())
|
.addCommand(createRollbackCommand())
|
||||||
.addCommand(createShipCommand())
|
.addCommand(createShipCommand())
|
||||||
.addCommand(createProjectsCommand());
|
.addCommand(createProjectsCommand())
|
||||||
|
.addCommand(createIdeateCommand());
|
||||||
|
|
||||||
program.parse();
|
program.parse();
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import { IdeationAgent } from "../agents/ideation-agent.js";
|
||||||
|
import { IdeationEngine, resetIdeaCounter } from "../core/ideation.js";
|
||||||
|
import { Idea, IdeationAction, DEFAULT_IDEATION_CONFIG } from "../types/ideation.js";
|
||||||
|
|
||||||
|
describe("IdeationAgent", () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-ideation-test-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("agent name is ideation-agent", () => {
|
||||||
|
const agent = new IdeationAgent();
|
||||||
|
expect(agent.name).toBe("ideation-agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates to IdeationEngine for mechanical ideation", () => {
|
||||||
|
const agent = new IdeationAgent();
|
||||||
|
const ideas = agent.mechanicalIdeate(tempDir);
|
||||||
|
expect(Array.isArray(ideas)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("IdeationEngine", () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetIdeaCounter();
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-engine-test-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates ideas from uncovered requirements", () => {
|
||||||
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
||||||
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "config.json"),
|
||||||
|
JSON.stringify({ projects: [], active_project: "default" })
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "REQUIREMENTS.md"),
|
||||||
|
"# Requirements\n\n## v1 Requirements\n\n### Core\n\n- **REQ-01**: First requirement\n- **REQ-02**: Second requirement\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n| REQ-02 | Phase 1 | pending |\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "PROJECT.md"),
|
||||||
|
"# Test Project\n\n## What This Is\n\nA test project.\n\n## Requirements\n\n### Validated\n\n- REQ-01: First\n\n### Active\n\n- [ ] REQ-02: Second\n\n## Constraints\n\n- Must work\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ROADMAP.md"),
|
||||||
|
"# Roadmap\n\n## Overview\n\nTest roadmap.\n\n## Phases\n\n- [ ] **Phase 1: Init** - Starting\n\n## Phase Details\n\n### Phase 1: Init\n\n**Goal.**: Start\n**Status**: not_started\n**Requirements**: REQ-01\n**Depends on**: Nothing\n**Success Criteria**:\n1. Project initialized\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ARCHITECTURE.md"),
|
||||||
|
"# Architecture\n\n## Overview\n\nTest architecture.\n\n## Components\n\n### Core\n\n- **Description**: Core module\n- **Boundaries**: Internal only\n- **Depends on**: None\n\n## Data Flow\n\nSimple flow.\n\n## Build Order\n\n1. Core\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const ideas = engine.runMechanical(["coverage"]);
|
||||||
|
const reqIdeas = ideas.filter((i) => i.source === "uncovered_requirement");
|
||||||
|
expect(reqIdeas.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects architecture drift when documented components are missing", () => {
|
||||||
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
||||||
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "config.json"),
|
||||||
|
JSON.stringify({ projects: [], active_project: "default" })
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ARCHITECTURE.md"),
|
||||||
|
"# Architecture\n\n## Overview\n\nTest.\n\n## Components\n\n### NonExistentModule\n\n- **Description**: A module that does not exist\n- **Boundaries**: None\n- **Depends on**: None\n\n## Data Flow\n\nFlow.\n\n## Build Order\n\n1. Core\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const ideas = engine.runMechanical(["architecture"]);
|
||||||
|
const driftIdeas = ideas.filter((i) => i.source === "architecture_drift");
|
||||||
|
expect(driftIdeas.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects spec ambiguity or spec missing", () => {
|
||||||
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
||||||
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "config.json"),
|
||||||
|
JSON.stringify({ projects: [], active_project: "default" })
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "PROJECT.md"),
|
||||||
|
"# Test\n\n## What This Is\n\nThe system should handle user input and could process data. It might also log events.\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n- [ ] The system should handle errors\n- [ ] Users could configure settings\n- [ ] It might send notifications\n\n## Constraints\n\n- Must work\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "REQUIREMENTS.md"),
|
||||||
|
"# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ROADMAP.md"),
|
||||||
|
"# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ARCHITECTURE.md"),
|
||||||
|
"# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const ideas = engine.runMechanical(["spec"]);
|
||||||
|
const specIdeas = ideas.filter((i) => i.source === "spec_ambiguity" || i.source === "spec_missing" || i.source === "spec_contradiction");
|
||||||
|
expect(specIdeas.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty ideas when no project files exist", () => {
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const ideas = engine.runMechanical();
|
||||||
|
expect(Array.isArray(ideas)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats ideas as readable text", () => {
|
||||||
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
||||||
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "config.json"),
|
||||||
|
JSON.stringify({ projects: [], active_project: "default" })
|
||||||
|
);
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "PROJECT.md"), "# Test\n\n## What This Is\n\nTest\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n\n## Constraints\n\n- None\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "REQUIREMENTS.md"), "# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "ROADMAP.md"), "# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "ARCHITECTURE.md"), "# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n");
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const formatted = engine.formatIdeas(engine.runMechanical());
|
||||||
|
expect(typeof formatted).toBe("string");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats ideas as JSON", () => {
|
||||||
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
||||||
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "config.json"),
|
||||||
|
JSON.stringify({ projects: [], active_project: "default" })
|
||||||
|
);
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "PROJECT.md"), "# Test\n\n## What This Is\n\nTest\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n\n## Constraints\n\n- None\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "REQUIREMENTS.md"), "# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "ROADMAP.md"), "# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "ARCHITECTURE.md"), "# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n");
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const result = engine.formatIdeasJson(engine.runMechanical());
|
||||||
|
expect(result).toHaveProperty("ideas");
|
||||||
|
expect(result).toHaveProperty("summary");
|
||||||
|
expect(result).toHaveProperty("project");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("acceptIdea", () => {
|
||||||
|
let acceptDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetIdeaCounter();
|
||||||
|
acceptDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-accept-test-"));
|
||||||
|
const ciagentDir = path.join(acceptDir, ".ciagent");
|
||||||
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "config.json"),
|
||||||
|
JSON.stringify({ projects: [], active_project: "default" })
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "REQUIREMENTS.md"),
|
||||||
|
"# Requirements\n\n## v1 Requirements\n\n### Core\n\n- **CORE-01**: Test core requirement\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| CORE-01 | Phase 1 | pending |\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ROADMAP.md"),
|
||||||
|
"# Roadmap\n\n## Overview\n\nTest roadmap.\n\n## Phases\n\n- [x] **Phase 1: Init** - Starting\n\n## Phase Details\n\n### Phase 1: Init\n\n**Goal.**: Start\n**Status**: complete\n**Requirements**: CORE-01\n**Depends on**: Nothing\n**Success Criteria**:\n1. Project initialized\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "PROJECT.md"),
|
||||||
|
"# Test\n\n## What This Is\n\nTest\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n\n## Constraints\n\n- None\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ARCHITECTURE.md"),
|
||||||
|
"# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(acceptDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts an idea and updates REQUIREMENTS.md and ROADMAP.md", () => {
|
||||||
|
const engine = new IdeationEngine(acceptDir);
|
||||||
|
const idea: Idea = {
|
||||||
|
id: "IDEATE-01",
|
||||||
|
source: "uncovered_requirement",
|
||||||
|
category: "coverage",
|
||||||
|
title: "Add rate limiting to cloud backends",
|
||||||
|
rationale: "No rate limiting REQ exists for cloud backends.",
|
||||||
|
confidence: 0.92,
|
||||||
|
actions: ["add_requirement", "update_roadmap"],
|
||||||
|
tier: "mechanical",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = engine.acceptIdea(idea);
|
||||||
|
|
||||||
|
expect(result.addedToRequirements).toBe(true);
|
||||||
|
expect(result.addedToRoadmap).toBe(true);
|
||||||
|
expect(result.reqId).toBe("IDEATE-01");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("acceptIdeas accepts multiple ideas", () => {
|
||||||
|
const engine = new IdeationEngine(acceptDir);
|
||||||
|
const ideas: Idea[] = [
|
||||||
|
{
|
||||||
|
id: "IDEATE-01",
|
||||||
|
source: "uncovered_requirement",
|
||||||
|
category: "coverage",
|
||||||
|
title: "Add rate limiting",
|
||||||
|
rationale: "No rate limiting.",
|
||||||
|
confidence: 0.9,
|
||||||
|
actions: ["add_requirement"],
|
||||||
|
tier: "mechanical",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "IDEATE-02",
|
||||||
|
source: "architecture_drift",
|
||||||
|
category: "architecture",
|
||||||
|
title: "Fix architecture drift",
|
||||||
|
rationale: "Component documented but missing.",
|
||||||
|
confidence: 0.8,
|
||||||
|
actions: ["update_architecture"],
|
||||||
|
tier: "mechanical",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { accepted, results } = engine.acceptIdeas(ideas);
|
||||||
|
expect(accepted.length).toBe(2);
|
||||||
|
expect(results.length).toBe(2);
|
||||||
|
expect(results.every((r) => r.addedToRequirements || r.addedToRoadmap)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Phase 2: Backend-enriched and chaos", () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetIdeaCounter();
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-p2-test-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runBackendEnriched prioritizes mechanical findings", () => {
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const mechanicalIdeas: Idea[] = [
|
||||||
|
{
|
||||||
|
id: "IDEATE-01",
|
||||||
|
source: "uncovered_requirement",
|
||||||
|
category: "coverage",
|
||||||
|
title: "Missing test",
|
||||||
|
rationale: "No test file",
|
||||||
|
confidence: 0.7,
|
||||||
|
actions: ["add_test"],
|
||||||
|
tier: "mechanical",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "IDEATE-02",
|
||||||
|
source: "escalation_pattern",
|
||||||
|
category: "security",
|
||||||
|
title: "Security issue",
|
||||||
|
rationale: "Repeated escalation",
|
||||||
|
confidence: 0.8,
|
||||||
|
actions: ["add_security_pattern"],
|
||||||
|
tier: "mechanical",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const enriched = engine.runBackendEnriched(mechanicalIdeas);
|
||||||
|
expect(enriched.length).toBeGreaterThanOrEqual(2);
|
||||||
|
const prioritizedIdeas = enriched.filter((i) => i.source === "uncovered_requirement" || i.source === "escalation_pattern");
|
||||||
|
expect(prioritizedIdeas.length).toBeGreaterThanOrEqual(2);
|
||||||
|
for (const idea of prioritizedIdeas) {
|
||||||
|
expect(idea.tier).toBe("backend-enriched");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runBackendEnriched adds novel suggestions for missing categories", () => {
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const mechanicalIdeas: Idea[] = [
|
||||||
|
{
|
||||||
|
id: "IDEATE-01",
|
||||||
|
source: "uncovered_requirement",
|
||||||
|
category: "coverage",
|
||||||
|
title: "Cover this",
|
||||||
|
rationale: "Missing",
|
||||||
|
confidence: 0.7,
|
||||||
|
actions: ["add_test"],
|
||||||
|
tier: "mechanical",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const enriched = engine.runBackendEnriched(mechanicalIdeas);
|
||||||
|
const novelIdeas = enriched.filter((i) => i.source === "improvement_pattern");
|
||||||
|
expect(novelIdeas.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateChaosScenarios uses default scenarios when enabled", () => {
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const chaosIdeas = engine.generateChaosScenarios();
|
||||||
|
|
||||||
|
expect(chaosIdeas.length).toBe(3);
|
||||||
|
expect(chaosIdeas.every((i) => i.source === "chaos_scenario")).toBe(true);
|
||||||
|
expect(chaosIdeas.every((i) => i.category === "chaos")).toBe(true);
|
||||||
|
expect(chaosIdeas.every((i) => i.tier === "backend-enriched")).toBe(true);
|
||||||
|
expect(chaosIdeas.every((i) => i.confidence >= 0.5)).toBe(true);
|
||||||
|
const titles = chaosIdeas.map((i) => i.title);
|
||||||
|
expect(titles.some((t) => t.includes("backend"))).toBe(true);
|
||||||
|
expect(titles.some((t) => t.includes("requirement"))).toBe(true);
|
||||||
|
expect(titles.some((t) => t.includes("coverage"))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Phase 3: External signals and cascade impact", () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetIdeaCounter();
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-p3-test-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runAffected detects cascade from architecture.md", () => {
|
||||||
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
||||||
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "config.json"),
|
||||||
|
JSON.stringify({ projects: [], active_project: "default" })
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ARCHITECTURE.md"),
|
||||||
|
"# Architecture\n\n## Overview\n\nTest.\n\n## Components\n\n### CLI\n\n- **Description**: Command line interface\n- **Boundaries**: User-facing only\n- **Depends on**: Core\n\n### Core\n\n- **Description**: Core engine\n- **Boundaries**: Internal only\n- **Depends on**: None\n\n## Data Flow\n\nSimple.\n\n## Build Order\n\n1. CLI\n2. Core\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const ideas = engine.runAffected();
|
||||||
|
expect(Array.isArray(ideas)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runExternal handles missing npm gracefully", () => {
|
||||||
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
||||||
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "config.json"),
|
||||||
|
JSON.stringify({ projects: [], active_project: "default" })
|
||||||
|
);
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const ideas = engine.runExternal();
|
||||||
|
expect(Array.isArray(ideas)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runCrossProject returns empty when only one project", () => {
|
||||||
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
||||||
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "config.json"),
|
||||||
|
JSON.stringify({ projects: [{ slug: "default", name: "Default Project", default: true }], active_project: "default" })
|
||||||
|
);
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir, "default");
|
||||||
|
const ideas = engine.runCrossProject();
|
||||||
|
expect(ideas).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,11 @@ import * as path from "node:path";
|
|||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import { CIAgentFiles, ProjectEntry } from "../core/ciagent-files.js";
|
import { CIAgentFiles, ProjectEntry } from "../core/ciagent-files.js";
|
||||||
import { initCIAgent, loadConfig, saveConfig } from "../core/config.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 {
|
function createTempDir(): string {
|
||||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-multiproject-test-"));
|
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 });
|
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", () => {
|
describe("Multi-project CIAgentFiles operations", () => {
|
||||||
let dir: string;
|
let dir: string;
|
||||||
|
|
||||||
@@ -168,4 +287,298 @@ describe("Multi-project CIAgentFiles operations", () => {
|
|||||||
expect(projectMd!.name).toBe("Task API");
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BackendConfigSection } from "../backends/types.js";
|
import { BackendConfigSection } from "../backends/types.js";
|
||||||
|
import { IdeationConfig, IdeationCategory } from "./ideation.js";
|
||||||
|
|
||||||
export type AutonomyLevel = "full" | "supervised" | "guided";
|
export type AutonomyLevel = "full" | "supervised" | "guided";
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ export interface ParallelizationConfig {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
max_concurrent_agents: number;
|
max_concurrent_agents: number;
|
||||||
min_plans_for_parallel: number;
|
min_plans_for_parallel: number;
|
||||||
|
max_concurrent_projects: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VerificationConfig {
|
export interface VerificationConfig {
|
||||||
@@ -82,6 +84,7 @@ export interface ProjectEntry {
|
|||||||
export interface CIAgentConfig {
|
export interface CIAgentConfig {
|
||||||
projects: ProjectEntry[];
|
projects: ProjectEntry[];
|
||||||
active_project: string;
|
active_project: string;
|
||||||
|
active_projects: string[];
|
||||||
autonomy: AutonomyConfig;
|
autonomy: AutonomyConfig;
|
||||||
model_profile: ModelProfile;
|
model_profile: ModelProfile;
|
||||||
parallelization: ParallelizationConfig;
|
parallelization: ParallelizationConfig;
|
||||||
@@ -90,11 +93,13 @@ export interface CIAgentConfig {
|
|||||||
git: GitConfig;
|
git: GitConfig;
|
||||||
backend: BackendConfigSection;
|
backend: BackendConfigSection;
|
||||||
gitea?: GiteaConfig;
|
gitea?: GiteaConfig;
|
||||||
|
ideation?: IdeationConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
|
export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
|
||||||
projects: [],
|
projects: [],
|
||||||
active_project: "",
|
active_project: "",
|
||||||
|
active_projects: [],
|
||||||
autonomy: {
|
autonomy: {
|
||||||
level: "full",
|
level: "full",
|
||||||
escalation_hooks: ["deploy", "delete_data", "merge_to_main"],
|
escalation_hooks: ["deploy", "delete_data", "merge_to_main"],
|
||||||
@@ -109,6 +114,7 @@ export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
max_concurrent_agents: 5,
|
max_concurrent_agents: 5,
|
||||||
min_plans_for_parallel: 2,
|
min_plans_for_parallel: 2,
|
||||||
|
max_concurrent_projects: 3,
|
||||||
},
|
},
|
||||||
verification: {
|
verification: {
|
||||||
automated_only: true,
|
automated_only: true,
|
||||||
@@ -165,4 +171,23 @@ export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
|
|||||||
owner: "",
|
owner: "",
|
||||||
repo: "",
|
repo: "",
|
||||||
},
|
},
|
||||||
|
ideation: {
|
||||||
|
enabled: true,
|
||||||
|
categories: ["security", "quality", "architecture", "coverage", "improvement"] as IdeationCategory[],
|
||||||
|
confidence_threshold: 0.6,
|
||||||
|
max_ideas: 20,
|
||||||
|
external_signals: {
|
||||||
|
npm_audit: true,
|
||||||
|
osv_advisories: true,
|
||||||
|
dependency_staleness: true,
|
||||||
|
},
|
||||||
|
cross_project: {
|
||||||
|
enabled: false,
|
||||||
|
similarity_weight: 0.5,
|
||||||
|
},
|
||||||
|
chaos: {
|
||||||
|
enabled: true,
|
||||||
|
scenarios: ["backend_unavailable", "requirement_change", "test_coverage_drop"],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
export type IdeationSource =
|
||||||
|
| "uncovered_requirement"
|
||||||
|
| "repeated_lesson"
|
||||||
|
| "low_confidence_decision"
|
||||||
|
| "escalation_pattern"
|
||||||
|
| "compound_pattern"
|
||||||
|
| "partial_requirement"
|
||||||
|
| "gap_in_coverage"
|
||||||
|
| "improvement_pattern"
|
||||||
|
| "architecture_drift"
|
||||||
|
| "verification_inversion"
|
||||||
|
| "spec_ambiguity"
|
||||||
|
| "spec_contradiction"
|
||||||
|
| "spec_missing"
|
||||||
|
| "external_signal"
|
||||||
|
| "cross_project_lesson"
|
||||||
|
| "chaos_scenario";
|
||||||
|
|
||||||
|
export type IdeationCategory =
|
||||||
|
| "security"
|
||||||
|
| "quality"
|
||||||
|
| "architecture"
|
||||||
|
| "coverage"
|
||||||
|
| "improvement"
|
||||||
|
| "spec"
|
||||||
|
| "chaos";
|
||||||
|
|
||||||
|
export type IdeationAction =
|
||||||
|
| "add_requirement"
|
||||||
|
| "update_architecture"
|
||||||
|
| "update_roadmap"
|
||||||
|
| "fix_documentation"
|
||||||
|
| "add_test"
|
||||||
|
| "add_security_pattern"
|
||||||
|
| "refactor"
|
||||||
|
| "new_milestone_phase";
|
||||||
|
|
||||||
|
export type IdeationTier = "mechanical" | "backend-enriched" | "cross-project";
|
||||||
|
|
||||||
|
export interface Idea {
|
||||||
|
id: string;
|
||||||
|
source: IdeationSource;
|
||||||
|
category: IdeationCategory;
|
||||||
|
title: string;
|
||||||
|
rationale: string;
|
||||||
|
confidence: number;
|
||||||
|
relatedReq?: string;
|
||||||
|
actions: IdeationAction[];
|
||||||
|
tier: IdeationTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IdeationResult {
|
||||||
|
project: string;
|
||||||
|
milestone: string;
|
||||||
|
ideas: Idea[];
|
||||||
|
summary: IdeationSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IdeationSummary {
|
||||||
|
total: number;
|
||||||
|
accepted: number;
|
||||||
|
skipped: number;
|
||||||
|
by_category: Record<string, number>;
|
||||||
|
by_tier: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IdeationConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
categories: IdeationCategory[];
|
||||||
|
confidence_threshold: number;
|
||||||
|
max_ideas: number;
|
||||||
|
external_signals: {
|
||||||
|
npm_audit: boolean;
|
||||||
|
osv_advisories: boolean;
|
||||||
|
dependency_staleness: boolean;
|
||||||
|
};
|
||||||
|
cross_project: {
|
||||||
|
enabled: boolean;
|
||||||
|
similarity_weight: number;
|
||||||
|
};
|
||||||
|
chaos: {
|
||||||
|
enabled: boolean;
|
||||||
|
scenarios: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_IDEATION_CONFIG: IdeationConfig = {
|
||||||
|
enabled: true,
|
||||||
|
categories: ["security", "quality", "architecture", "coverage", "improvement"],
|
||||||
|
confidence_threshold: 0.6,
|
||||||
|
max_ideas: 20,
|
||||||
|
external_signals: {
|
||||||
|
npm_audit: true,
|
||||||
|
osv_advisories: true,
|
||||||
|
dependency_staleness: true,
|
||||||
|
},
|
||||||
|
cross_project: {
|
||||||
|
enabled: false,
|
||||||
|
similarity_weight: 0.5,
|
||||||
|
},
|
||||||
|
chaos: {
|
||||||
|
enabled: true,
|
||||||
|
scenarios: ["backend_unavailable", "requirement_change", "test_coverage_drop"],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@ import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
|||||||
|
|
||||||
describe("Type exports", () => {
|
describe("Type exports", () => {
|
||||||
it("pipeline types are importable and functional", () => {
|
it("pipeline types are importable and functional", () => {
|
||||||
expect(STAGE_ORDER).toHaveLength(8);
|
expect(STAGE_ORDER).toHaveLength(9);
|
||||||
expect(getNextStage("specify")).toBe("clarify");
|
expect(getNextStage("specify")).toBe("clarify");
|
||||||
const state = createInitialPipelineState("/tmp/test");
|
const state = createInitialPipelineState("/tmp/test");
|
||||||
expect(state.current_stage).toBe("specify");
|
expect(state.current_stage).toBe("specify");
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import {
|
|||||||
} from "../types/pipeline.js";
|
} from "../types/pipeline.js";
|
||||||
|
|
||||||
describe("STAGE_ORDER", () => {
|
describe("STAGE_ORDER", () => {
|
||||||
it("has 8 stages in correct order", () => {
|
it("has 9 stages in correct order", () => {
|
||||||
expect(STAGE_ORDER).toEqual([
|
expect(STAGE_ORDER).toEqual([
|
||||||
"specify",
|
"specify",
|
||||||
"clarify",
|
"clarify",
|
||||||
"research",
|
"research",
|
||||||
|
"ideate",
|
||||||
"plan",
|
"plan",
|
||||||
"execute",
|
"execute",
|
||||||
"test",
|
"test",
|
||||||
@@ -26,7 +27,8 @@ describe("getNextStage", () => {
|
|||||||
it("returns the next stage in sequence", () => {
|
it("returns the next stage in sequence", () => {
|
||||||
expect(getNextStage("specify")).toBe("clarify");
|
expect(getNextStage("specify")).toBe("clarify");
|
||||||
expect(getNextStage("clarify")).toBe("research");
|
expect(getNextStage("clarify")).toBe("research");
|
||||||
expect(getNextStage("research")).toBe("plan");
|
expect(getNextStage("research")).toBe("ideate");
|
||||||
|
expect(getNextStage("ideate")).toBe("plan");
|
||||||
expect(getNextStage("plan")).toBe("execute");
|
expect(getNextStage("plan")).toBe("execute");
|
||||||
expect(getNextStage("execute")).toBe("test");
|
expect(getNextStage("execute")).toBe("test");
|
||||||
expect(getNextStage("test")).toBe("verify");
|
expect(getNextStage("test")).toBe("verify");
|
||||||
@@ -51,6 +53,7 @@ describe("createInitialPipelineState", () => {
|
|||||||
expect(state.specification_loaded).toBe(false);
|
expect(state.specification_loaded).toBe(false);
|
||||||
expect(state.clarify_completed).toBe(false);
|
expect(state.clarify_completed).toBe(false);
|
||||||
expect(state.research_completed).toBe(false);
|
expect(state.research_completed).toBe(false);
|
||||||
|
expect(state.ideate_completed).toBe(false);
|
||||||
expect(state.plan_completed).toBe(false);
|
expect(state.plan_completed).toBe(false);
|
||||||
expect(state.execute_completed).toBe(false);
|
expect(state.execute_completed).toBe(false);
|
||||||
expect(state.test_completed).toBe(false);
|
expect(state.test_completed).toBe(false);
|
||||||
@@ -59,4 +62,14 @@ describe("createInitialPipelineState", () => {
|
|||||||
expect(state.started_at).toBeTruthy();
|
expect(state.started_at).toBeTruthy();
|
||||||
expect(state.last_updated).toBeTruthy();
|
expect(state.last_updated).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("STAGE_ORDER ideate position", () => {
|
||||||
|
it("places ideate between research and plan", () => {
|
||||||
|
const ideateIdx = STAGE_ORDER.indexOf("ideate");
|
||||||
|
const researchIdx = STAGE_ORDER.indexOf("research");
|
||||||
|
const planIdx = STAGE_ORDER.indexOf("plan");
|
||||||
|
expect(ideateIdx).toBeGreaterThan(researchIdx);
|
||||||
|
expect(ideateIdx).toBeLessThan(planIdx);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -4,6 +4,7 @@ export type PipelineStage =
|
|||||||
| "specify"
|
| "specify"
|
||||||
| "clarify"
|
| "clarify"
|
||||||
| "research"
|
| "research"
|
||||||
|
| "ideate"
|
||||||
| "plan"
|
| "plan"
|
||||||
| "execute"
|
| "execute"
|
||||||
| "test"
|
| "test"
|
||||||
@@ -18,6 +19,7 @@ export interface PipelineState {
|
|||||||
specification_loaded: boolean;
|
specification_loaded: boolean;
|
||||||
clarify_completed: boolean;
|
clarify_completed: boolean;
|
||||||
research_completed: boolean;
|
research_completed: boolean;
|
||||||
|
ideate_completed: boolean;
|
||||||
plan_completed: boolean;
|
plan_completed: boolean;
|
||||||
execute_completed: boolean;
|
execute_completed: boolean;
|
||||||
test_completed: boolean;
|
test_completed: boolean;
|
||||||
@@ -61,6 +63,7 @@ export const STAGE_ORDER: PipelineStage[] = [
|
|||||||
"specify",
|
"specify",
|
||||||
"clarify",
|
"clarify",
|
||||||
"research",
|
"research",
|
||||||
|
"ideate",
|
||||||
"plan",
|
"plan",
|
||||||
"execute",
|
"execute",
|
||||||
"test",
|
"test",
|
||||||
@@ -85,6 +88,7 @@ export function createInitialPipelineState(
|
|||||||
specification_loaded: false,
|
specification_loaded: false,
|
||||||
clarify_completed: false,
|
clarify_completed: false,
|
||||||
research_completed: false,
|
research_completed: false,
|
||||||
|
ideate_completed: false,
|
||||||
plan_completed: false,
|
plan_completed: false,
|
||||||
execute_completed: false,
|
execute_completed: false,
|
||||||
test_completed: false,
|
test_completed: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user