feat(P01): interactive validation + doc updates + multi-project CLI — IDEATE-12,13,14 + MULTI-02,06
CI / build-and-test (pull_request) Has been cancelled
Publish to npm / publish (push) Has been cancelled
CI / build-and-test (push) Has been cancelled

---ci---
phase: 1
milestone: v0.10
status: execute
decisions:
  - id: D-083
    decision: Interactive one-at-a-time validation with accept/skip/modify
    rationale: Gives user full control over ideation results
    confidence: 0.87
  - id: D-085
    decision: Ask-after-validation kickoff of run workflow
    rationale: Balances automation with user control
    confidence: 0.85
  - id: D-091
    decision: Full multi-project support with active_projects array + parallel execution
    rationale: User wants complete multi-project capability
    confidence: 0.85
requirements:
  covered:
    - IDEATE-12
    - IDEATE-13
    - IDEATE-14
    - MULTI-02
    - MULTI-06
---/ci---

- IDEATE-12: Interactive accept/skip/modify validation with readline
- IDEATE-13: acceptIdea/acceptIdeas methods update REQUIREMENTS.md and ROADMAP.md
- IDEATE-14: Ask-after-validation kickoff prompt for
- MULTI-02: --project flag accepts comma-separated or 'all' in pre-action hook
- MULTI-06: ciagent status shows active_projects and ideation config
- projects list shows all active projects with multi-marker
- projects set updates both active_project and active_projects
This commit is contained in:
Jon Chery
2026-05-30 20:26:36 +00:00
parent 8e50049ba5
commit b7d02ee4a4
5 changed files with 308 additions and 20 deletions
+2 -2
View File
@@ -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",
+123 -15
View File
@@ -20,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 {
@@ -413,7 +414,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)) {
@@ -423,14 +425,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}`);
@@ -661,6 +680,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.");
@@ -670,11 +692,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>")
@@ -713,6 +737,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}`);
}); });
@@ -1028,6 +1053,8 @@ export function createIdeateCommand(): Command {
if (options.output === "json") { if (options.output === "json") {
const result = engine.formatIdeasJson(allIdeas); const result = engine.formatIdeasJson(allIdeas);
result.summary.accepted = 0;
result.summary.skipped = allIdeas.length;
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
return; return;
} }
@@ -1060,25 +1087,106 @@ export function createIdeateCommand(): Command {
return; 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++) { for (let i = 0; i < allIdeas.length; i++) {
const idea = allIdeas[i]; const idea = allIdeas[i];
console.log(`═══ Recommendation ${i + 1} of ${allIdeas.length} ═══\n`); console.log(`\n═══ Recommendation ${i + 1} of ${allIdeas.length} ═══\n`);
console.log(`Category: ${idea.category.toUpperCase()} | Confidence: ${idea.confidence.toFixed(2)} | Tier: ${idea.tier}`); console.log(` Category: ${idea.category.toUpperCase()} | Confidence: ${idea.confidence.toFixed(2)} | Tier: ${idea.tier}`);
console.log(`Title: ${idea.title}`); console.log(` Title: ${idea.title}`);
console.log(`Rationale: ${idea.rationale}`); console.log(` Rationale: ${idea.rationale}`);
if (idea.relatedReq) console.log(`Related Req: ${idea.relatedReq}`); if (idea.relatedReq) console.log(` Related Req: ${idea.relatedReq}`);
console.log(`Source: ${idea.source}`); console.log(` Source: ${idea.source}`);
console.log(`Actions: ${idea.actions.join(", ")}`); console.log(` Actions: ${idea.actions.join(", ")}`);
console.log(""); 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 (accepted.length > 0) {
console.log("\nAccepted ideas:");
for (const idea of accepted) {
console.log(` ${idea.id}: ${idea.title} (${idea.category.toUpperCase()})`);
}
const { accepted: savedIdeas, results } = engine.acceptIdeas(accepted);
const savedCount = results.filter((r) => r.addedToRequirements || r.addedToRoadmap).length;
if (savedCount > 0) {
console.log(`\n${savedCount} idea${savedCount === 1 ? "" : "s"} 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> = {}; const byCategory: Record<string, number> = {};
for (const idea of allIdeas) { for (const idea of allIdeas) {
byCategory[idea.category] = (byCategory[idea.category] || 0) + 1; byCategory[idea.category] = (byCategory[idea.category] || 0) + 1;
} }
console.log("\n─── Category Breakdown ───\n");
console.log("─── Summary ───\n");
console.log(`Total ideas: ${allIdeas.length}`);
for (const [cat, count] of Object.entries(byCategory)) { for (const [cat, count] of Object.entries(byCategory)) {
console.log(` ${cat}: ${count}`); console.log(` ${cat}: ${count}`);
} }
+5 -2
View File
@@ -45,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())
+87 -1
View File
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import * as os from "node:os"; import * as os from "node:os";
import { IdeationAgent } from "../agents/ideation-agent.js"; import { IdeationAgent } from "../agents/ideation-agent.js";
import { IdeationEngine, resetIdeaCounter } from "../core/ideation.js"; import { IdeationEngine, resetIdeaCounter, Idea } from "../core/ideation.js";
describe("IdeationAgent", () => { describe("IdeationAgent", () => {
let tempDir: string; let tempDir: string;
@@ -158,4 +158,90 @@ describe("IdeationEngine", () => {
expect(result).toHaveProperty("summary"); expect(result).toHaveProperty("summary");
expect(result).toHaveProperty("project"); 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);
});
});
}); });
+91
View File
@@ -846,4 +846,95 @@ export class IdeationEngine {
}, },
}; };
} }
acceptIdea(idea: Idea): { reqId: string; addedToRequirements: boolean; addedToRoadmap: boolean } {
const reqId = idea.id;
const reqs = this.ciFiles.readRequirementsMd();
const roadmap = this.ciFiles.readRoadmapMd();
let addedToRequirements = false;
let addedToRoadmap = false;
if (reqs) {
const categoryMap: Record<string, string> = {
security: "Security",
quality: "Quality",
architecture: "Architecture",
coverage: "Coverage",
improvement: "Improvement",
spec: "Specification",
chaos: "Resilience",
};
const categoryName = categoryMap[idea.category] || "Improvement";
let foundCategory = false;
for (const cat of reqs.v1) {
if (cat.category.toLowerCase() === categoryName.toLowerCase()) {
cat.items.push({ id: reqId, description: idea.title });
foundCategory = true;
break;
}
}
if (!foundCategory) {
reqs.v1.push({
category: categoryName,
items: [{ id: reqId, description: idea.title }],
});
}
reqs.traceability.push({
requirement: reqId,
phase: 0,
status: "pending",
});
this.ciFiles.writeRequirementsMd(reqs);
addedToRequirements = true;
}
if (roadmap) {
const lastPhase = roadmap.phases.length > 0
? roadmap.phases[roadmap.phases.length - 1]
: null;
const nextPhaseNumber = lastPhase ? lastPhase.number + 1 : 1;
const phaseName = idea.category === "security" ? "security-hardening"
: idea.category === "architecture" ? "architecture-fix"
: idea.category === "coverage" ? "coverage-expansion"
: idea.category === "quality" ? "quality-improvement"
: idea.category === "spec" ? "spec-refinement"
: idea.category === "chaos" ? "resilience-hardening"
: "improvement";
roadmap.phases.push({
number: nextPhaseNumber,
name: phaseName,
description: idea.title,
status: "not_started",
dependsOn: lastPhase ? [lastPhase.number] : [],
requirements: [reqId],
successCriteria: [idea.rationale],
});
this.ciFiles.writeRoadmapMd(roadmap);
addedToRoadmap = true;
}
return { reqId, addedToRequirements, addedToRoadmap };
}
acceptIdeas(ideas: Idea[]): { accepted: Idea[]; results: Array<{ reqId: string; addedToRequirements: boolean; addedToRoadmap: boolean }> } {
const accepted: Idea[] = [];
const results: Array<{ reqId: string; addedToRequirements: boolean; addedToRoadmap: boolean }> = [];
for (const idea of ideas) {
const result = this.acceptIdea(idea);
if (result.addedToRequirements || result.addedToRoadmap) {
accepted.push(idea);
results.push(result);
}
}
return { accepted, results };
}
} }