feat(P01): interactive validation + doc updates + multi-project CLI — IDEATE-12,13,14 + MULTI-02,06
---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:
+123
-15
@@ -20,6 +20,7 @@ import { CIAgentFiles } from "../core/ciagent-files.js";
|
||||
import { GiteaClient, generateReleaseNotes } from "../core/gitea.js";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as readline from "node:readline";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
export function createInitCommand(): Command {
|
||||
@@ -413,7 +414,8 @@ export function createReviewCommand(): Command {
|
||||
export function createStatusCommand(): Command {
|
||||
return new Command("status")
|
||||
.description("Non-interactive project status")
|
||||
.action(() => {
|
||||
.option("--project <slug>", "Show status for specific project (comma-separated or 'all')")
|
||||
.action((options) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
@@ -423,14 +425,31 @@ export function createStatusCommand(): Command {
|
||||
}
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
const ciFiles = new CIAgentFiles(projectPath);
|
||||
const artifacts = new ArtifactManager(projectPath);
|
||||
|
||||
console.log("─── CIAgent Project Status ───");
|
||||
console.log(`\nAutonomy: ${config.autonomy.level}`);
|
||||
const activeProjects: string[] = (config as any).active_projects?.length > 0
|
||||
? (config as any).active_projects
|
||||
: config.active_project ? [config.active_project] : [];
|
||||
|
||||
console.log("─── CIAgent Project Status ───\n");
|
||||
|
||||
if (activeProjects.length > 1 || (options.project && options.project === "all")) {
|
||||
console.log(`Active Projects: ${activeProjects.join(", ")}`);
|
||||
console.log(`Total: ${activeProjects.length} projects`);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
console.log(`Autonomy: ${config.autonomy.level}`);
|
||||
console.log(`Model Profile: ${config.model_profile}`);
|
||||
console.log(`Backend: ${config.backend?.provider || "auto"}`);
|
||||
console.log(`Parallelization: ${config.parallelization.enabled ? "enabled" : "disabled"}`);
|
||||
|
||||
const ideationConfig = (config as any).ideation;
|
||||
if (ideationConfig) {
|
||||
console.log(`Ideation: ${ideationConfig.enabled ? "enabled" : "disabled"} (categories: ${ideationConfig.categories?.join(", ") || "default"})`);
|
||||
}
|
||||
|
||||
const state = artifacts.readState();
|
||||
if (state) {
|
||||
console.log(`\nCurrent Phase: ${state.current_phase}`);
|
||||
@@ -661,6 +680,9 @@ export function createProjectsCommand(): Command {
|
||||
const ciFiles = new CIAgentFiles(projectPath);
|
||||
const projects = ciFiles.listProjects();
|
||||
const activeProject = config.active_project || ciFiles.getActiveProject();
|
||||
const activeProjects: string[] = (config as any).active_projects?.length > 0
|
||||
? (config as any).active_projects
|
||||
: activeProject ? [activeProject] : [];
|
||||
|
||||
if (projects.length === 0) {
|
||||
console.log("No projects registered.");
|
||||
@@ -670,11 +692,13 @@ export function createProjectsCommand(): Command {
|
||||
|
||||
console.log("─── CIAgent Projects ───\n");
|
||||
for (const project of projects) {
|
||||
const isActive = project.slug === activeProject;
|
||||
const isActive = activeProjects.includes(project.slug);
|
||||
const marker = isActive ? " *" : "";
|
||||
console.log(` ${project.slug} — ${project.name}${marker}`);
|
||||
}
|
||||
console.log("\n * = active project");
|
||||
if (activeProjects.length > 0) {
|
||||
console.log(`\n Active: ${activeProjects.join(", ")}`);
|
||||
}
|
||||
});
|
||||
|
||||
cmd.command("add <slug> <name>")
|
||||
@@ -713,6 +737,7 @@ export function createProjectsCommand(): Command {
|
||||
ciFiles.setActiveProject(slug);
|
||||
const config = loadConfig(projectPath);
|
||||
config.active_project = slug;
|
||||
(config as any).active_projects = [slug];
|
||||
saveConfig(projectPath, config);
|
||||
console.log(`✓ Active project set to: ${slug}`);
|
||||
});
|
||||
@@ -1028,6 +1053,8 @@ export function createIdeateCommand(): Command {
|
||||
|
||||
if (options.output === "json") {
|
||||
const result = engine.formatIdeasJson(allIdeas);
|
||||
result.summary.accepted = 0;
|
||||
result.summary.skipped = allIdeas.length;
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
@@ -1060,25 +1087,106 @@ export function createIdeateCommand(): Command {
|
||||
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];
|
||||
console.log(`═══ Recommendation ${i + 1} of ${allIdeas.length} ═══\n`);
|
||||
console.log(`Category: ${idea.category.toUpperCase()} | Confidence: ${idea.confidence.toFixed(2)} | Tier: ${idea.tier}`);
|
||||
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(`\n═══ Recommendation ${i + 1} of ${allIdeas.length} ═══\n`);
|
||||
console.log(` Category: ${idea.category.toUpperCase()} | Confidence: ${idea.confidence.toFixed(2)} | Tier: ${idea.tier}`);
|
||||
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 (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> = {};
|
||||
for (const idea of allIdeas) {
|
||||
byCategory[idea.category] = (byCategory[idea.category] || 0) + 1;
|
||||
}
|
||||
|
||||
console.log("─── Summary ───\n");
|
||||
console.log(`Total ideas: ${allIdeas.length}`);
|
||||
console.log("\n─── Category Breakdown ───\n");
|
||||
for (const [cat, count] of Object.entries(byCategory)) {
|
||||
console.log(` ${cat}: ${count}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user