Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2b749d42e | |||
| c747d3e8be | |||
| d9927558d5 | |||
| 895d9f95a1 | |||
| 30352a3603 | |||
| d58fd0bdde |
@@ -18,6 +18,7 @@ export interface AgentContext {
|
||||
specification: string;
|
||||
config_path: string;
|
||||
backend?: IntelligenceBackend;
|
||||
project_slug?: string;
|
||||
}
|
||||
|
||||
export function backendResultToAgentResult(result: BackendResult): AgentResult {
|
||||
|
||||
+162
-1
@@ -47,6 +47,7 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
|
||||
private static readonly STAGE_AGENT_MAP: Partial<Record<PipelineStage, AgentName[]>> = {
|
||||
research: ["researcher"],
|
||||
ideate: ["ideation-agent"],
|
||||
plan: ["planner"],
|
||||
execute: ["executor", "code-reviewer", "security-auditor"],
|
||||
test: ["tester"],
|
||||
@@ -67,9 +68,10 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
try {
|
||||
this.config = loadConfig(context.project_path);
|
||||
|
||||
const projectSlug = context.project_slug || "";
|
||||
this.gitContext = new GitContext(context.project_path);
|
||||
this.gitBranch = new GitBranch(context.project_path);
|
||||
this.ciFiles = new CIAgentFiles(context.project_path);
|
||||
this.ciFiles = new CIAgentFiles(context.project_path, projectSlug || undefined);
|
||||
this.ciFiles.ensureCIDir();
|
||||
|
||||
const projectState = this.gitContext.reconstructState();
|
||||
@@ -459,6 +461,7 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
projectName: spec.objective.slice(0, 30),
|
||||
phaseCount: 0,
|
||||
milestone: this.currentMilestone,
|
||||
project: context.project_slug || undefined,
|
||||
specification: spec.raw_content,
|
||||
requirements: spec.requirements,
|
||||
constraints: spec.constraints,
|
||||
@@ -571,6 +574,69 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
break;
|
||||
}
|
||||
|
||||
case "ideate": {
|
||||
this.log("Running ideation stage...");
|
||||
const { IdeationEngine } = await import("../core/ideation.js");
|
||||
const ideationEngine = new IdeationEngine(context.project_path, context.project_slug || undefined);
|
||||
const ideas = ideationEngine.runMechanical();
|
||||
|
||||
const ideationConfig = this.config.ideation;
|
||||
if (ideationConfig?.categories && ideationConfig.categories.length > 0) {
|
||||
const categoryIdeas = ideationEngine.runMechanical(ideationConfig.categories);
|
||||
const seenTitles = new Set(ideas.map((i) => i.title));
|
||||
for (const idea of categoryIdeas) {
|
||||
if (!seenTitles.has(idea.title)) {
|
||||
ideas.push(idea);
|
||||
seenTitles.add(idea.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ideas.sort((a, b) => b.confidence - a.confidence);
|
||||
|
||||
const maxIdeas = ideationConfig?.max_ideas || 20;
|
||||
const trimmedIdeas = ideas.slice(0, maxIdeas);
|
||||
|
||||
if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) {
|
||||
const { accepted: savedIdeas, results } = ideationEngine.acceptIdeas(trimmedIdeas);
|
||||
const savedCount = results.filter((r) => r.addedToRequirements || r.addedToRoadmap).length;
|
||||
|
||||
const ideationCommit = CommitBuilder.buildTaskCommit({
|
||||
type: "decision",
|
||||
phase: this.pipelineState!.current_phase,
|
||||
milestone: this.currentMilestone,
|
||||
project: context.project_slug || undefined,
|
||||
plan: "ideation",
|
||||
task: "ideation-results",
|
||||
subject: `ideation results — ${trimmedIdeas.length} total, ${savedCount} accepted`,
|
||||
status: "ideate",
|
||||
decisions: savedIdeas.map((idea) => ({
|
||||
id: idea.id,
|
||||
decision: idea.title,
|
||||
rationale: idea.rationale,
|
||||
confidence: idea.confidence,
|
||||
alternatives: idea.actions,
|
||||
})),
|
||||
});
|
||||
|
||||
try {
|
||||
execSync(`git add -A && git commit -m "${ideationCommit.replace(/"/g, '\\"')}" --allow-empty`, {
|
||||
cwd: context.project_path,
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch (err) {
|
||||
this.warn(`Ideation commit failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
artifactsCreated.push(".ciagent/REQUIREMENTS.md", ".ciagent/ROADMAP.md");
|
||||
decisionsMade += savedCount;
|
||||
}
|
||||
|
||||
this.pipelineState!.ideate_completed = true;
|
||||
this.log(`Ideation stage complete: ${trimmedIdeas.length} ideas generated`);
|
||||
break;
|
||||
}
|
||||
|
||||
case "plan":
|
||||
this.log("Planning phase execution...");
|
||||
|
||||
@@ -790,4 +856,99 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async runForProject(projectSlug: string, context: AgentContext): Promise<AgentResult> {
|
||||
this.log(`Running pipeline for project: ${projectSlug}`);
|
||||
|
||||
this.ciFiles = new CIAgentFiles(context.project_path, projectSlug);
|
||||
this.ciFiles.ensureCIDir();
|
||||
this.ciFiles.setProjectSlug(projectSlug);
|
||||
|
||||
const projectContext: AgentContext = {
|
||||
...context,
|
||||
project_path: context.project_path,
|
||||
};
|
||||
|
||||
const result = await this.execute(projectContext);
|
||||
|
||||
return {
|
||||
...result,
|
||||
output: result.output ? `[${projectSlug}] ${result.output}` : result.output,
|
||||
};
|
||||
}
|
||||
|
||||
async runForAllProjects(context: AgentContext): Promise<Record<string, AgentResult>> {
|
||||
const config = loadConfig(context.project_path);
|
||||
const ciFiles = new CIAgentFiles(context.project_path);
|
||||
const projects = ciFiles.listProjects();
|
||||
|
||||
const activeProjects: string[] = config.active_projects?.length > 0
|
||||
? config.active_projects
|
||||
: projects.map((p) => p.slug);
|
||||
|
||||
if (activeProjects.length === 0) {
|
||||
this.log("No active projects found; running for default project");
|
||||
const result = await this.execute(context);
|
||||
return { default: result };
|
||||
}
|
||||
|
||||
this.log(`Running pipeline for ${activeProjects.length} project(s): ${activeProjects.join(", ")}`);
|
||||
|
||||
const results: Record<string, AgentResult> = {};
|
||||
const maxConcurrent = config.parallelization?.max_concurrent_projects ?? 3;
|
||||
const parallel = config.parallelization?.enabled && activeProjects.length > 1;
|
||||
|
||||
if (parallel) {
|
||||
const limitedConcurrency = Math.min(maxConcurrent, activeProjects.length);
|
||||
const batches: string[][] = [];
|
||||
for (let i = 0; i < activeProjects.length; i += limitedConcurrency) {
|
||||
batches.push(activeProjects.slice(i, i + limitedConcurrency));
|
||||
}
|
||||
|
||||
for (const batch of batches) {
|
||||
const batchResults = await Promise.allSettled(
|
||||
batch.map(async (slug): Promise<[string, AgentResult]> => {
|
||||
const orchestrator = new OrchestratorAgent(config);
|
||||
const result = await orchestrator.runForProject(slug, context);
|
||||
return [slug, result];
|
||||
})
|
||||
);
|
||||
|
||||
for (const settled of batchResults) {
|
||||
if (settled.status === "fulfilled") {
|
||||
const [slug, result] = settled.value;
|
||||
results[slug] = result;
|
||||
} else {
|
||||
this.warn(`Project pipeline failed: ${settled.reason instanceof Error ? settled.reason.message : String(settled.reason)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const slug of activeProjects) {
|
||||
this.log(`Processing project: ${slug}`);
|
||||
const orchestrator = new OrchestratorAgent(config);
|
||||
orchestrator.ciFiles = new CIAgentFiles(context.project_path, slug);
|
||||
orchestrator.ciFiles.ensureCIDir();
|
||||
orchestrator.ciFiles.setProjectSlug(slug);
|
||||
|
||||
try {
|
||||
const result = await orchestrator.runForProject(slug, context);
|
||||
results[slug] = result;
|
||||
} catch (err) {
|
||||
this.warn(`Failed for project ${slug}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
results[slug] = {
|
||||
success: false,
|
||||
output: `Pipeline failed for project ${slug}`,
|
||||
artifacts_created: 0,
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: 0,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
+250
-69
@@ -10,7 +10,7 @@ import { getAuditSummary, readAudit } from "../core/audit.js";
|
||||
import { VerificationPipeline } from "../verification/index.js";
|
||||
import { ClarifyPhase } from "../core/clarify.js";
|
||||
import { loadSpecification as loadSpec } from "../core/clarify.js";
|
||||
import { AgentContext } from "../agents/base.js";
|
||||
import { AgentContext, AgentResult } from "../agents/base.js";
|
||||
import { ErrorRecovery } from "../core/error-recovery.js";
|
||||
import { PipelineState, createInitialPipelineState } from "../types/pipeline.js";
|
||||
import { resolveBackend } from "../backends/index.js";
|
||||
@@ -79,6 +79,7 @@ export function createInitCommand(): Command {
|
||||
enabled: options.parallel !== false,
|
||||
max_concurrent_agents: 5,
|
||||
min_plans_for_parallel: 2,
|
||||
max_concurrent_projects: 3,
|
||||
},
|
||||
backend: {
|
||||
provider: options.backend || "auto",
|
||||
@@ -169,6 +170,8 @@ export function createRunCommand(): Command {
|
||||
.option("--all", "Execute all remaining phases sequentially")
|
||||
.option("--phase <number>", "Phase number", "1")
|
||||
.option("--backend <provider>", "Override intelligence backend for this run")
|
||||
.option("--ideate", "Insert ideation stage between research and plan")
|
||||
.option("--project <slug>", "Target project slug (comma-separated or 'all')")
|
||||
.action(async (phase, options) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
@@ -178,6 +181,141 @@ export function createRunCommand(): Command {
|
||||
}
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
const ciFiles = new CIAgentFiles(projectPath);
|
||||
const runForAllProjects = options.project === "all" || (Array.isArray(config.active_projects) && config.active_projects.length > 1 && !options.project);
|
||||
|
||||
if (runForAllProjects) {
|
||||
console.log("─── Running pipeline across all active projects ───\n");
|
||||
|
||||
const orchestrator = new OrchestratorAgent(config);
|
||||
const context: AgentContext = {
|
||||
project_path: projectPath,
|
||||
phase: parseInt(options.phase) || 1,
|
||||
stage: phase || "all",
|
||||
specification: "",
|
||||
config_path: path.join(projectPath, ".ciagent", "config.json"),
|
||||
backend: undefined,
|
||||
};
|
||||
|
||||
const spec = loadSpec(projectPath);
|
||||
if (spec) {
|
||||
context.specification = spec.raw_content;
|
||||
}
|
||||
|
||||
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
|
||||
if (backend) {
|
||||
context.backend = backend;
|
||||
} else if (backendError) {
|
||||
console.warn(` ⚠ No intelligence backend available: ${backendError}`);
|
||||
console.warn(" Continuing with mechanical-only execution (limited functionality).");
|
||||
}
|
||||
|
||||
const results = await orchestrator.runForAllProjects(context);
|
||||
|
||||
console.log("\n─── Multi-Project Pipeline Results ───\n");
|
||||
let allSuccess = true;
|
||||
for (const [slug, result] of Object.entries(results)) {
|
||||
const icon = result.success ? "✓" : "✗";
|
||||
console.log(` ${icon} ${slug}: ${result.success ? "success" : result.error || "failed"}`);
|
||||
if (!result.success) allSuccess = false;
|
||||
}
|
||||
|
||||
if (!allSuccess) {
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let projectSlug: string | undefined;
|
||||
if (options.project && options.project !== "all") {
|
||||
const slugs = options.project.split(",").map((s: string) => s.trim()).filter(Boolean);
|
||||
projectSlug = slugs[0];
|
||||
|
||||
if (slugs.length > 1) {
|
||||
console.log("─── Running pipeline across multiple projects ───\n");
|
||||
|
||||
const orchestrator = new OrchestratorAgent(config);
|
||||
const context: AgentContext = {
|
||||
project_path: projectPath,
|
||||
phase: parseInt(options.phase) || 1,
|
||||
stage: phase || "all",
|
||||
specification: "",
|
||||
config_path: path.join(projectPath, ".ciagent", "config.json"),
|
||||
backend: undefined,
|
||||
};
|
||||
|
||||
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
|
||||
if (backend) {
|
||||
context.backend = backend;
|
||||
} else if (backendError) {
|
||||
console.warn(` ⚠ No intelligence backend available: ${backendError}`);
|
||||
}
|
||||
|
||||
const allResults: Record<string, AgentResult> = {};
|
||||
for (const slug of slugs) {
|
||||
console.log(`\nProcessing project: ${slug}`);
|
||||
const projOrchestrator = new OrchestratorAgent(config);
|
||||
const result = await projOrchestrator.runForProject(slug, context);
|
||||
allResults[slug] = result;
|
||||
}
|
||||
|
||||
console.log("\n─── Multi-Project Pipeline Results ───\n");
|
||||
let allSuccess = true;
|
||||
for (const [slug, result] of Object.entries(allResults)) {
|
||||
const icon = result.success ? "✓" : "✗";
|
||||
console.log(` ${icon} ${slug}: ${result.success ? "success" : result.error || "failed"}`);
|
||||
if (!result.success) allSuccess = false;
|
||||
}
|
||||
|
||||
if (!allSuccess) {
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ideate) {
|
||||
console.log("─── CIAgent Ideate (pipeline mode) ───\n");
|
||||
|
||||
const currentSlug = projectSlug || ciFiles.getProjectSlug() || ciFiles.getActiveProject() || "default";
|
||||
const { IdeationEngine } = await import("../core/ideation.js");
|
||||
const engine = new IdeationEngine(projectPath, currentSlug);
|
||||
|
||||
const ideas = engine.runMechanical();
|
||||
|
||||
const ideaCategory: IdeationCategory[] = options.category
|
||||
? options.category.split(",").map((c: string) => c.trim() as IdeationCategory)
|
||||
: [];
|
||||
|
||||
if (ideaCategory.length > 0) {
|
||||
const filtered = engine.runMechanical(ideaCategory);
|
||||
ideas.push(...filtered);
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
const uniqueIdeas = ideas.filter((idea: Idea) => {
|
||||
if (seen.has(idea.title)) return false;
|
||||
seen.add(idea.title);
|
||||
return true;
|
||||
});
|
||||
|
||||
uniqueIdeas.sort((a: Idea, b: Idea) => b.confidence - a.confidence);
|
||||
|
||||
console.log(`Found ${uniqueIdeas.length} improvement ${uniqueIdeas.length === 1 ? "idea" : "ideas"} from ideation stage.\n`);
|
||||
|
||||
if (uniqueIdeas.length > 0) {
|
||||
const { accepted: savedIdeas, results } = engine.acceptIdeas(uniqueIdeas);
|
||||
const savedCount = results.filter((r: { addedToRequirements: boolean; addedToRoadmap: boolean }) => r.addedToRequirements || r.addedToRoadmap).length;
|
||||
|
||||
if (savedCount > 0) {
|
||||
console.log(`${savedCount} idea${savedCount === 1 ? "" : "s"} added to REQUIREMENTS.md and ROADMAP.md.`);
|
||||
}
|
||||
|
||||
const commitMsg = `decision(P${options.phase || 1}): ideation results — ${uniqueIdeas.length} total, ${savedCount} accepted`;
|
||||
console.log(`\nCommit suggestion: ${commitMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
|
||||
|
||||
if (!backend && backendError) {
|
||||
@@ -193,6 +331,7 @@ export function createRunCommand(): Command {
|
||||
specification: "",
|
||||
config_path: path.join(projectPath, ".ciagent", "config.json"),
|
||||
backend,
|
||||
project_slug: projectSlug || undefined,
|
||||
};
|
||||
|
||||
const spec = loadSpec(projectPath);
|
||||
@@ -200,7 +339,7 @@ export function createRunCommand(): Command {
|
||||
context.specification = spec.raw_content;
|
||||
}
|
||||
|
||||
console.log(`Running CIAgent pipeline...`);
|
||||
console.log(`Running CIAgent pipeline${projectSlug ? ` for project: ${projectSlug}` : ""}...`);
|
||||
if (options.all) {
|
||||
console.log(" Mode: Full pipeline (all phases)");
|
||||
} else {
|
||||
@@ -989,72 +1128,88 @@ export function createIdeateCommand(): Command {
|
||||
}
|
||||
|
||||
const ciFiles = new CIAgentFiles(projectPath);
|
||||
let slug = options.project || ciFiles.getActiveProject() || "default";
|
||||
const allProjects = slug === "all";
|
||||
|
||||
if (options.project) {
|
||||
ciFiles.setProjectSlug(options.project);
|
||||
}
|
||||
|
||||
const categories: IdeationCategory[] = options.category
|
||||
? options.category.split(",").map((c: string) => c.trim() as IdeationCategory)
|
||||
: [];
|
||||
|
||||
console.log("\n─── CIAgent Ideation ───");
|
||||
console.log(`Project: ${ciFiles.getProjectSlug() || "default"}`);
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
|
||||
console.log("\nMining git history for patterns...");
|
||||
const allProjects: string[] = options.project === "all"
|
||||
? ciFiles.listProjects().map((p) => p.slug)
|
||||
: options.project
|
||||
? options.project.split(",").map((s: string) => s.trim()).filter(Boolean)
|
||||
: [ciFiles.getProjectSlug() || ciFiles.getActiveProject() || "default"];
|
||||
|
||||
if (allProjects.length > 1) {
|
||||
console.log(`\n─── CIAgent Ideation (multi-project: ${allProjects.join(", ")}) ───\n`);
|
||||
} else {
|
||||
console.log("\n─── CIAgent Ideation ───");
|
||||
console.log(`Project: ${allProjects[0]}`);
|
||||
}
|
||||
|
||||
const { IdeationEngine } = await import("../core/ideation.js");
|
||||
const engine = new IdeationEngine(projectPath, ciFiles.getProjectSlug() || undefined);
|
||||
|
||||
let allIdeas: Idea[] = [];
|
||||
const allIdeasByProject: Record<string, Idea[]> = {};
|
||||
const allIdeas: Idea[] = [];
|
||||
const seenTitles = new Set<string>();
|
||||
|
||||
console.log("Running mechanical analysis (tier 1)...");
|
||||
allIdeas = engine.runMechanical(categories.length > 0 ? categories : undefined);
|
||||
for (const slug of allProjects) {
|
||||
const engine = new IdeationEngine(projectPath, slug);
|
||||
ciFiles.setProjectSlug(slug);
|
||||
|
||||
if (options.affected) {
|
||||
console.log("Running cascade impact analysis (--affected)...");
|
||||
const affectedIdeas = engine.runAffected();
|
||||
allIdeas = [...allIdeas, ...affectedIdeas];
|
||||
const categories: IdeationCategory[] = options.category
|
||||
? options.category.split(",").map((c: string) => c.trim() as IdeationCategory)
|
||||
: [];
|
||||
|
||||
console.log(`\nMining git history for patterns in project: ${slug}...`);
|
||||
|
||||
let projectIdeas: Idea[] = engine.runMechanical(categories.length > 0 ? categories : undefined);
|
||||
|
||||
if (options.affected) {
|
||||
console.log(`Running cascade impact analysis (--affected) for ${slug}...`);
|
||||
const affectedIdeas = engine.runAffected();
|
||||
projectIdeas = [...projectIdeas, ...affectedIdeas];
|
||||
}
|
||||
|
||||
if (options.spec) {
|
||||
console.log(`Running specification analysis (--spec) for ${slug}...`);
|
||||
const specIdeas = engine.runMechanical(["spec"]);
|
||||
const newSpecIdeas = specIdeas.filter(
|
||||
(idea: Idea) => !projectIdeas.some((existing: Idea) => existing.title === idea.title)
|
||||
);
|
||||
projectIdeas = [...projectIdeas, ...newSpecIdeas];
|
||||
}
|
||||
|
||||
if (options.external) {
|
||||
console.log(`Running external signal analysis (--external) for ${slug}...`);
|
||||
const externalIdeas = engine.runExternal();
|
||||
projectIdeas = [...projectIdeas, ...externalIdeas];
|
||||
}
|
||||
|
||||
if (options.crossProject && ciFiles.isMultiProject()) {
|
||||
console.log(`Running cross-project pattern mining (--cross-project) for ${slug}...`);
|
||||
const crossProjectIdeas = engine.runCrossProject();
|
||||
projectIdeas = [...projectIdeas, ...crossProjectIdeas];
|
||||
}
|
||||
|
||||
const uniqueProjectIdeas = projectIdeas.filter((idea: Idea) => {
|
||||
const dedupeKey = allProjects.length > 1 ? `${slug}:${idea.title}` : idea.title;
|
||||
if (seenTitles.has(dedupeKey)) return false;
|
||||
seenTitles.add(dedupeKey);
|
||||
return true;
|
||||
});
|
||||
|
||||
uniqueProjectIdeas.sort((a: Idea, b: Idea) => b.confidence - a.confidence);
|
||||
allIdeasByProject[slug] = uniqueProjectIdeas;
|
||||
allIdeas.push(...uniqueProjectIdeas);
|
||||
}
|
||||
|
||||
if (options.spec) {
|
||||
console.log("Running specification analysis (--spec)...");
|
||||
const specIdeas = engine.runMechanical(["spec"]);
|
||||
const newSpecIdeas = specIdeas.filter(
|
||||
(idea: Idea) => !allIdeas.some((existing: Idea) => existing.title === idea.title)
|
||||
);
|
||||
allIdeas = [...allIdeas, ...newSpecIdeas];
|
||||
}
|
||||
allIdeas.sort((a, b) => b.confidence - a.confidence);
|
||||
|
||||
if (options.external) {
|
||||
console.log("Running external signal analysis (--external)...");
|
||||
const externalIdeas = engine.runExternal();
|
||||
allIdeas = [...allIdeas, ...externalIdeas];
|
||||
}
|
||||
|
||||
if (options.crossProject && ciFiles.isMultiProject()) {
|
||||
console.log("Running cross-project pattern mining (--cross-project)...");
|
||||
const crossProjectIdeas = engine.runCrossProject();
|
||||
allIdeas = [...allIdeas, ...crossProjectIdeas];
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
allIdeas = allIdeas.filter((idea: Idea) => {
|
||||
if (seen.has(idea.title)) return false;
|
||||
seen.add(idea.title);
|
||||
return true;
|
||||
});
|
||||
|
||||
allIdeas.sort((a: Idea, b: Idea) => b.confidence - a.confidence);
|
||||
const currentSlug = allProjects.length === 1 ? allProjects[0] : "all";
|
||||
const engine = new IdeationEngine(projectPath, allProjects.length === 1 ? allProjects[0] : undefined);
|
||||
|
||||
if (options.output === "json") {
|
||||
const result = engine.formatIdeasJson(allIdeas);
|
||||
result.summary.accepted = 0;
|
||||
result.summary.skipped = allIdeas.length;
|
||||
result.project = currentSlug;
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
@@ -1065,21 +1220,33 @@ export function createIdeateCommand(): Command {
|
||||
console.log("No improvement ideas identified for this project.");
|
||||
return;
|
||||
}
|
||||
for (const idea of allIdeas) {
|
||||
console.log(`### ${idea.title}`);
|
||||
console.log(`- **Category**: ${idea.category}`);
|
||||
console.log(`- **Source**: ${idea.source}`);
|
||||
console.log(`- **Confidence**: ${idea.confidence.toFixed(2)}`);
|
||||
console.log(`- **Tier**: ${idea.tier}`);
|
||||
console.log(`- **Rationale**: ${idea.rationale}`);
|
||||
if (idea.relatedReq) console.log(`- **Related Req**: ${idea.relatedReq}`);
|
||||
console.log(`- **Actions**: ${idea.actions.join(", ")}`);
|
||||
console.log("");
|
||||
|
||||
if (allProjects.length > 1) {
|
||||
console.log("| Project | Idea | Category | Confidence | Tier |");
|
||||
console.log("|---------|-------|----------|------------|------|");
|
||||
for (const slug of allProjects) {
|
||||
const projectIdeas = allIdeasByProject[slug] || [];
|
||||
for (const idea of projectIdeas) {
|
||||
console.log(`| ${slug} | ${idea.title} | ${idea.category} | ${idea.confidence.toFixed(2)} | ${idea.tier} |`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const idea of allIdeas) {
|
||||
console.log(`### ${idea.title}`);
|
||||
console.log(`- **Category**: ${idea.category}`);
|
||||
console.log(`- **Source**: ${idea.source}`);
|
||||
console.log(`- **Confidence**: ${idea.confidence.toFixed(2)}`);
|
||||
console.log(`- **Tier**: ${idea.tier}`);
|
||||
console.log(`- **Rationale**: ${idea.rationale}`);
|
||||
if (idea.relatedReq) console.log(`- **Related Req**: ${idea.relatedReq}`);
|
||||
console.log(`- **Actions**: ${idea.actions.join(", ")}`);
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\nFound ${allIdeas.length} improvement ${allIdeas.length === 1 ? "idea" : "ideas"}\n`);
|
||||
console.log(`\nFound ${allIdeas.length} improvement ${allIdeas.length === 1 ? "idea" : "ideas"}${allProjects.length > 1 ? ` across ${allProjects.length} projects` : ""}\n`);
|
||||
|
||||
if (allIdeas.length === 0) {
|
||||
console.log("No improvement ideas identified for this project.");
|
||||
@@ -1110,8 +1277,9 @@ export function createIdeateCommand(): Command {
|
||||
|
||||
for (let i = 0; i < allIdeas.length; i++) {
|
||||
const idea = allIdeas[i];
|
||||
const projectLabel = allProjects.length > 1 ? ` [${idea.tier === "cross-project" ? "cross-project" : allProjects[0]}]` : "";
|
||||
console.log(`\n═══ Recommendation ${i + 1} of ${allIdeas.length} ═══\n`);
|
||||
console.log(` Category: ${idea.category.toUpperCase()} | Confidence: ${idea.confidence.toFixed(2)} | Tier: ${idea.tier}`);
|
||||
console.log(` Category: ${idea.category.toUpperCase()} | Confidence: ${idea.confidence.toFixed(2)} | Tier: ${idea.tier}${projectLabel}`);
|
||||
console.log(` Title: ${idea.title}`);
|
||||
console.log(` Rationale: ${idea.rationale}`);
|
||||
if (idea.relatedReq) console.log(` Related Req: ${idea.relatedReq}`);
|
||||
@@ -1160,17 +1328,30 @@ export function createIdeateCommand(): Command {
|
||||
console.log(`Accepted: ${accepted.length} recommendation${accepted.length === 1 ? "" : "s"}`);
|
||||
console.log(`Skipped: ${skipped.length} recommendation${skipped.length === 1 ? "" : "s"}`);
|
||||
|
||||
if (allProjects.length > 1) {
|
||||
console.log(`Projects: ${allProjects.join(", ")}`);
|
||||
}
|
||||
|
||||
if (accepted.length > 0) {
|
||||
console.log("\nAccepted ideas:");
|
||||
for (const idea of accepted) {
|
||||
console.log(` ${idea.id}: ${idea.title} (${idea.category.toUpperCase()})`);
|
||||
}
|
||||
|
||||
const { accepted: savedIdeas, results } = engine.acceptIdeas(accepted);
|
||||
const savedCount = results.filter((r) => r.addedToRequirements || r.addedToRoadmap).length;
|
||||
for (const slug of allProjects) {
|
||||
const projectAccepted = accepted.filter((idea) => {
|
||||
return allIdeasByProject[slug]?.some((pi) => pi.id === idea.id);
|
||||
});
|
||||
|
||||
if (savedCount > 0) {
|
||||
console.log(`\n${savedCount} idea${savedCount === 1 ? "" : "s"} added to REQUIREMENTS.md and ROADMAP.md.`);
|
||||
if (projectAccepted.length > 0) {
|
||||
const projEngine = new IdeationEngine(projectPath, slug);
|
||||
const { accepted: savedIdeas, results } = projEngine.acceptIdeas(projectAccepted);
|
||||
const savedCount = results.filter((r) => r.addedToRequirements || r.addedToRoadmap).length;
|
||||
|
||||
if (savedCount > 0) {
|
||||
console.log(`\n${savedCount} idea${savedCount === 1 ? "" : "s"} for project "${slug}" added to REQUIREMENTS.md and ROADMAP.md.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const kickoffAnswer = await askQuestion("\nWould you like to kick off the run workflow for these ideas? (y/n) > ");
|
||||
|
||||
@@ -327,4 +327,60 @@ describe("IdeationEngine", () => {
|
||||
expect(titles.some((t) => t.includes("coverage"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Phase 3: External signals and cascade impact", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
resetIdeaCounter();
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-p3-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("runAffected detects cascade from architecture.md", () => {
|
||||
const ciagentDir = path.join(tempDir, ".ciagent");
|
||||
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ciagentDir, "config.json"),
|
||||
JSON.stringify({ projects: [], active_project: "default" })
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(ciagentDir, "ARCHITECTURE.md"),
|
||||
"# Architecture\n\n## Overview\n\nTest.\n\n## Components\n\n### CLI\n\n- **Description**: Command line interface\n- **Boundaries**: User-facing only\n- **Depends on**: Core\n\n### Core\n\n- **Description**: Core engine\n- **Boundaries**: Internal only\n- **Depends on**: None\n\n## Data Flow\n\nSimple.\n\n## Build Order\n\n1. CLI\n2. Core\n"
|
||||
);
|
||||
|
||||
const engine = new IdeationEngine(tempDir);
|
||||
const ideas = engine.runAffected();
|
||||
expect(Array.isArray(ideas)).toBe(true);
|
||||
});
|
||||
|
||||
it("runExternal handles missing npm gracefully", () => {
|
||||
const ciagentDir = path.join(tempDir, ".ciagent");
|
||||
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ciagentDir, "config.json"),
|
||||
JSON.stringify({ projects: [], active_project: "default" })
|
||||
);
|
||||
|
||||
const engine = new IdeationEngine(tempDir);
|
||||
const ideas = engine.runExternal();
|
||||
expect(Array.isArray(ideas)).toBe(true);
|
||||
});
|
||||
|
||||
it("runCrossProject returns empty when only one project", () => {
|
||||
const ciagentDir = path.join(tempDir, ".ciagent");
|
||||
fs.mkdirSync(ciagentDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ciagentDir, "config.json"),
|
||||
JSON.stringify({ projects: [{ slug: "default", name: "Default Project", default: true }], active_project: "default" })
|
||||
);
|
||||
|
||||
const engine = new IdeationEngine(tempDir, "default");
|
||||
const ideas = engine.runCrossProject();
|
||||
expect(ideas).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,11 @@ import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { CIAgentFiles, ProjectEntry } from "../core/ciagent-files.js";
|
||||
import { initCIAgent, loadConfig, saveConfig } from "../core/config.js";
|
||||
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
||||
import { CommitBuilder } from "../core/commit-builder.js";
|
||||
import { IdeationEngine, resetIdeaCounter } from "../core/ideation.js";
|
||||
import { extractCIAgentBlock, parseCIAgentBlock } from "../core/commit-parser.js";
|
||||
import { DEFAULT_CIAGENT_CONFIG, ParallelizationConfig } from "../types/config.js";
|
||||
import { AgentContext } from "../agents/base.js";
|
||||
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-multiproject-test-"));
|
||||
@@ -13,6 +17,121 @@ function cleanup(dir: string): void {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function initMultiProjectWithFiles(dir: string, projectList: Array<{ slug: string; name: string }>): void {
|
||||
const ciDir = path.join(dir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
|
||||
const projects = projectList.map((p, i) => ({
|
||||
slug: p.slug,
|
||||
name: p.name,
|
||||
default: i === 0,
|
||||
}));
|
||||
|
||||
const config = {
|
||||
...DEFAULT_CIAGENT_CONFIG,
|
||||
projects,
|
||||
active_project: projectList[0].slug,
|
||||
active_projects: projectList.map((p) => p.slug),
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(ciDir, "config.json"), JSON.stringify(config, null, 2));
|
||||
|
||||
for (const project of projectList) {
|
||||
const projectDir = path.join(ciDir, project.slug);
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(projectDir, "PROJECT.md"), [
|
||||
`# ${project.name}`,
|
||||
"",
|
||||
"## What This Is",
|
||||
"",
|
||||
`A ${project.name} project for testing`,
|
||||
"",
|
||||
"## Requirements",
|
||||
"",
|
||||
"### Active",
|
||||
"",
|
||||
"- Build the project",
|
||||
"",
|
||||
"### Validated",
|
||||
"",
|
||||
"### Out of Scope",
|
||||
"",
|
||||
"## Context",
|
||||
"",
|
||||
"Testing",
|
||||
"",
|
||||
"## Constraints",
|
||||
"",
|
||||
"## Key Decisions",
|
||||
"",
|
||||
"| Decision | Rationale | Outcome |",
|
||||
"|----------|-----------|---------|",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(projectDir, "REQUIREMENTS.md"), [
|
||||
"# Requirements",
|
||||
"",
|
||||
`| REQ-ID | Requirement | Priority | Phase | Status |`,
|
||||
`|--------|-------------|----------|-------|--------|`,
|
||||
`| ${project.slug.toUpperCase()}-01 | Core feature | P0 | 1 | pending |`,
|
||||
"",
|
||||
"## Traceability",
|
||||
"",
|
||||
`| Requirement | Phase | Status |`,
|
||||
`|-------------|-------|--------|`,
|
||||
`| ${project.slug.toUpperCase()}-01 | 1 | pending |`,
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(projectDir, "ROADMAP.md"), [
|
||||
"# Roadmap",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
`${project.name} roadmap`,
|
||||
"",
|
||||
"## Phases",
|
||||
"",
|
||||
"- [ ] **Phase 1: Core** - Build features",
|
||||
"",
|
||||
"## Phase Details",
|
||||
"",
|
||||
"### Phase 1: Core",
|
||||
"**Goal.**: Build features",
|
||||
"**Depends on**: Nothing",
|
||||
"**Requirements**: CORE-01",
|
||||
"**Success Criteria**:",
|
||||
"1. Features work",
|
||||
"**Status**: not_started",
|
||||
"",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(projectDir, "ARCHITECTURE.md"), [
|
||||
"# Architecture",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
`${project.name} testing architecture`,
|
||||
"",
|
||||
"## Components",
|
||||
"",
|
||||
`### ${project.slug}-api`,
|
||||
"- **Description**: API",
|
||||
"- **Boundaries**: HTTP only",
|
||||
"- **Depends on**: None",
|
||||
"",
|
||||
"## Data Flow",
|
||||
"",
|
||||
"Client -> API",
|
||||
"",
|
||||
"## Build Order",
|
||||
"",
|
||||
"1. API",
|
||||
"",
|
||||
].join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
describe("Multi-project CIAgentFiles operations", () => {
|
||||
let dir: string;
|
||||
|
||||
@@ -168,4 +287,298 @@ describe("Multi-project CIAgentFiles operations", () => {
|
||||
expect(projectMd!.name).toBe("Task API");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AgentContext project_slug field", () => {
|
||||
it("accepts optional project_slug", () => {
|
||||
const context: AgentContext = {
|
||||
project_path: "/tmp/test",
|
||||
phase: 1,
|
||||
stage: "execute",
|
||||
specification: "test spec",
|
||||
config_path: "/tmp/test/.ciagent/config.json",
|
||||
project_slug: "my-project",
|
||||
};
|
||||
|
||||
expect(context.project_slug).toBe("my-project");
|
||||
});
|
||||
|
||||
it("project_slug is optional", () => {
|
||||
const context: AgentContext = {
|
||||
project_path: "/tmp/test",
|
||||
phase: 1,
|
||||
stage: "execute",
|
||||
specification: "test spec",
|
||||
config_path: "/tmp/test/.ciagent/config.json",
|
||||
};
|
||||
|
||||
expect(context.project_slug).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MULTI-03: Parallel project execution", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(dir);
|
||||
});
|
||||
|
||||
describe("OrchestratorAgent module has multi-project methods", () => {
|
||||
it("exports OrchestratorAgent class with runForProject and runForAllProjects", () => {
|
||||
expect(typeof DEFAULT_CIAGENT_CONFIG.parallelization.max_concurrent_projects).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
describe("active_projects config field", () => {
|
||||
it("stores active_projects array in config", () => {
|
||||
initMultiProjectWithFiles(dir, [
|
||||
{ slug: "task-api", name: "Task API" },
|
||||
{ slug: "auth-svc", name: "Auth Service" },
|
||||
]);
|
||||
|
||||
const config = loadConfig(dir);
|
||||
expect(config.active_projects).toEqual(["task-api", "auth-svc"]);
|
||||
});
|
||||
|
||||
it("defaults to empty array when not configured", () => {
|
||||
initCIAgent(dir);
|
||||
const config = loadConfig(dir);
|
||||
expect(config.active_projects).toEqual([]);
|
||||
});
|
||||
|
||||
it("max_concurrent_projects defaults to 3", () => {
|
||||
expect(DEFAULT_CIAGENT_CONFIG.parallelization.max_concurrent_projects).toBe(3);
|
||||
});
|
||||
|
||||
it("max_concurrent_projects can be configured", () => {
|
||||
initCIAgent(dir, {
|
||||
parallelization: {
|
||||
...DEFAULT_CIAGENT_CONFIG.parallelization,
|
||||
max_concurrent_projects: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const config = loadConfig(dir);
|
||||
expect(config.parallelization.max_concurrent_projects).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MULTI-05: ideate --project all", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = createTempDir();
|
||||
resetIdeaCounter();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(dir);
|
||||
});
|
||||
|
||||
describe("IdeationEngine with project slug for multi-project", () => {
|
||||
it("runs mechanical ideation for different project slugs", () => {
|
||||
initMultiProjectWithFiles(dir, [
|
||||
{ slug: "task-api", name: "Task API" },
|
||||
]);
|
||||
|
||||
resetIdeaCounter();
|
||||
const engine = new IdeationEngine(dir, "task-api");
|
||||
const ideas = engine.runMechanical();
|
||||
expect(Array.isArray(ideas)).toBe(true);
|
||||
});
|
||||
|
||||
it("runs ideation across multiple projects and collects results", () => {
|
||||
initMultiProjectWithFiles(dir, [
|
||||
{ slug: "task-api", name: "Task API" },
|
||||
{ slug: "auth-svc", name: "Auth Service" },
|
||||
]);
|
||||
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
const projects = ciFiles.listProjects();
|
||||
const allProjectIdeas: Record<string, number> = {};
|
||||
|
||||
for (const project of projects) {
|
||||
resetIdeaCounter();
|
||||
const engine = new IdeationEngine(dir, project.slug);
|
||||
const ideas = engine.runMechanical();
|
||||
allProjectIdeas[project.slug] = ideas.length;
|
||||
}
|
||||
|
||||
expect(Object.keys(allProjectIdeas)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("deduplicates ideas across projects with project-prefixed keys", () => {
|
||||
initMultiProjectWithFiles(dir, [
|
||||
{ slug: "task-api", name: "Task API" },
|
||||
{ slug: "auth-svc", name: "Auth Service" },
|
||||
]);
|
||||
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
const projects = ciFiles.listProjects();
|
||||
const allTitles: string[] = [];
|
||||
const seenKeys = new Set<string>();
|
||||
|
||||
for (const project of projects) {
|
||||
resetIdeaCounter();
|
||||
const engine = new IdeationEngine(dir, project.slug);
|
||||
const ideas = engine.runMechanical();
|
||||
|
||||
for (const idea of ideas) {
|
||||
const dedupeKey = `${project.slug}:${idea.title}`;
|
||||
if (!seenKeys.has(dedupeKey)) {
|
||||
seenKeys.add(dedupeKey);
|
||||
allTitles.push(idea.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(seenKeys.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("formats JSON output with project field for each project", () => {
|
||||
initMultiProjectWithFiles(dir, [
|
||||
{ slug: "task-api", name: "Task API" },
|
||||
]);
|
||||
|
||||
resetIdeaCounter();
|
||||
const engine = new IdeationEngine(dir, "task-api");
|
||||
const ideas = engine.runMechanical();
|
||||
const result = engine.formatIdeasJson(ideas);
|
||||
expect(result.project).toBe("task-api");
|
||||
});
|
||||
|
||||
it("runs cross-project analysis on multi-project setup", () => {
|
||||
initMultiProjectWithFiles(dir, [
|
||||
{ slug: "task-api", name: "Task API" },
|
||||
{ slug: "auth-svc", name: "Auth Service" },
|
||||
]);
|
||||
|
||||
resetIdeaCounter();
|
||||
const engine = new IdeationEngine(dir, "task-api");
|
||||
const crossIdeas = engine.runCrossProject();
|
||||
expect(Array.isArray(crossIdeas)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MULTI-07: ---ci--- project field in commits", () => {
|
||||
describe("CIAgentMetadata with project", () => {
|
||||
it("includes project field in ci block when set", () => {
|
||||
const ci = {
|
||||
phase: 5,
|
||||
milestone: "v0.10",
|
||||
project: "ci",
|
||||
status: "execute" as const,
|
||||
};
|
||||
|
||||
const block = CommitBuilder.buildCiBlock(ci);
|
||||
expect(block).toContain("project: ci");
|
||||
});
|
||||
|
||||
it("omits project field when not set", () => {
|
||||
const ci = {
|
||||
phase: 5,
|
||||
milestone: "v0.10",
|
||||
status: "execute" as const,
|
||||
};
|
||||
|
||||
const block = CommitBuilder.buildCiBlock(ci);
|
||||
expect(block).not.toContain("project:");
|
||||
});
|
||||
|
||||
it("commits with different project slugs include the correct project", () => {
|
||||
const projects = ["task-api", "auth-svc", "notification-svc"];
|
||||
for (const slug of projects) {
|
||||
const ci = {
|
||||
phase: 1,
|
||||
milestone: "v0.10",
|
||||
project: slug,
|
||||
status: "plan" as const,
|
||||
};
|
||||
const block = CommitBuilder.buildCiBlock(ci);
|
||||
expect(block).toContain(`project: ${slug}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildTaskCommit with project", () => {
|
||||
it("includes project prefix in scope and ci block", () => {
|
||||
const msg = CommitBuilder.buildTaskCommit({
|
||||
type: "feat",
|
||||
phase: 5,
|
||||
milestone: "v0.10",
|
||||
project: "ci",
|
||||
plan: "01-multi-project",
|
||||
task: "01-config-array",
|
||||
subject: "parallel project execution config",
|
||||
status: "execute",
|
||||
});
|
||||
|
||||
expect(msg).toContain("feat(ci/");
|
||||
expect(msg).toContain("project: ci");
|
||||
expect(msg).toContain("---ci---");
|
||||
});
|
||||
|
||||
it("builds commit without project when project is undefined", () => {
|
||||
const msg = CommitBuilder.buildTaskCommit({
|
||||
type: "feat",
|
||||
phase: 5,
|
||||
milestone: "v0.10",
|
||||
project: undefined,
|
||||
plan: "01-multi-project",
|
||||
task: "01-config-array",
|
||||
subject: "parallel project execution config",
|
||||
status: "execute",
|
||||
});
|
||||
|
||||
expect(msg).not.toContain("project:");
|
||||
expect(msg).toContain("feat(P05");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildInitCommit with project", () => {
|
||||
it("includes project in ci block", () => {
|
||||
const msg = CommitBuilder.buildInitCommit({
|
||||
projectName: "CIAgent",
|
||||
phaseCount: 6,
|
||||
milestone: "v0.10",
|
||||
project: "ci",
|
||||
specification: "Multi-project ideation support",
|
||||
requirements: ["MULTI-03", "MULTI-05", "MULTI-07"],
|
||||
});
|
||||
|
||||
expect(msg).toContain("project: ci");
|
||||
expect(msg).toContain("---ci---");
|
||||
expect(msg).toContain("phase: 0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Round-trip parsing with project field", () => {
|
||||
it("parses commit message with project scope and ci block", () => {
|
||||
const msg = CommitBuilder.buildTaskCommit({
|
||||
type: "feat",
|
||||
phase: 5,
|
||||
milestone: "v0.10",
|
||||
project: "ci",
|
||||
plan: "01-multi",
|
||||
task: "01-config",
|
||||
subject: "parallel project execution",
|
||||
status: "execute",
|
||||
});
|
||||
|
||||
const extracted = extractCIAgentBlock(msg);
|
||||
expect(extracted).not.toBeNull();
|
||||
|
||||
const parsed = parseCIAgentBlock(extracted!);
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.project).toBe("ci");
|
||||
expect(parsed!.phase).toBe(5);
|
||||
expect(parsed!.milestone).toBe("v0.10");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -46,6 +46,7 @@ export interface ParallelizationConfig {
|
||||
enabled: boolean;
|
||||
max_concurrent_agents: number;
|
||||
min_plans_for_parallel: number;
|
||||
max_concurrent_projects: number;
|
||||
}
|
||||
|
||||
export interface VerificationConfig {
|
||||
@@ -113,6 +114,7 @@ export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
|
||||
enabled: true,
|
||||
max_concurrent_agents: 5,
|
||||
min_plans_for_parallel: 2,
|
||||
max_concurrent_projects: 3,
|
||||
},
|
||||
verification: {
|
||||
automated_only: true,
|
||||
|
||||
@@ -63,3 +63,13 @@ describe("createInitialPipelineState", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user