b84230e389
---ci---
phase: 1
milestone: v0.2.0
status: execute
decisions:
- id: D-001
decision: Git log as primary project memory, .ci/ for long-lived references only
rationale: Eliminates state drift, enables reconstruction from commit messages alone
confidence: 0.95
alternatives: [hybrid file+git, pure git with no .ci/]
- id: D-002
decision: ---ci--- YAML blocks in commit bodies for machine-parseable metadata
rationale: Structured and human-readable; grep-friendly; round-trips through parser
confidence: 0.92
alternatives: [JSON payload, conventional-commit-only]
- id: D-003
decision: Phase+milestone branch naming (phase/NN-slug, milestone/vX.X-slug)
rationale: Branch list immediately shows project state; merged equals complete
confidence: 0.88
alternatives: [trunk+tags, milestone-only branches]
requirements:
covered: [ARCH-01, ARCH-02, ARCH-03, ARCH-04, ARCH-05, ARCH-06]
lessons:
- Commit body YAML must round-trip through parser — tested before shipping
- .ci/audit/ removal required updating 4 test suites that depended on audit files
---/ci---
New modules: commit-parser, commit-builder, git-context, git-branch, ci-files
Core rewrites: DecisionEngine, EscalationProtocol, OrchestratorAgent
Removed: .ci/audit/, .planning/ directory support
Tests: 25 suites, 218 passing (up from 20/158)
364 lines
12 KiB
TypeScript
364 lines
12 KiB
TypeScript
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
|
import { DecisionEngine } from "../core/decision-engine.js";
|
|
import { ClarifyPhase } from "../core/clarify.js";
|
|
import { EscalationProtocol, EscalationInput } from "../core/escalation.js";
|
|
import { GitContext, ProjectState } from "../core/git-context.js";
|
|
import { GitBranch } from "../core/git-branch.js";
|
|
import { CiFiles } from "../core/ci-files.js";
|
|
import { CommitBuilder } from "../core/commit-builder.js";
|
|
import { CIConfig } from "../types/config.js";
|
|
import {
|
|
PipelineState,
|
|
PipelineStage,
|
|
PhaseResult,
|
|
OrchestratorResult,
|
|
createInitialPipelineState,
|
|
STAGE_ORDER,
|
|
} from "../types/pipeline.js";
|
|
import { Specification, parseSpecification } from "../types/specification.js";
|
|
import { loadConfig, saveConfig, isCIInitialized, initCI } from "../core/config.js";
|
|
|
|
export interface GitAgentContext extends AgentContext {
|
|
gitContext: GitContext;
|
|
gitBranch: GitBranch;
|
|
ciFiles: CiFiles;
|
|
milestone: string;
|
|
}
|
|
|
|
export class OrchestratorAgent extends BaseAgent {
|
|
readonly name = "orchestrator";
|
|
readonly description = "Top-level autonomous controller that coordinates the full CI pipeline";
|
|
|
|
private config: CIConfig;
|
|
private pipelineState: PipelineState | null = null;
|
|
private decisionEngine: DecisionEngine | null = null;
|
|
private escalationProtocol: EscalationProtocol | null = null;
|
|
private gitContext: GitContext | null = null;
|
|
private gitBranch: GitBranch | null = null;
|
|
private ciFiles: CiFiles | null = null;
|
|
private currentMilestone: string;
|
|
private phaseResults: PhaseResult[] = [];
|
|
|
|
constructor(config?: CIConfig) {
|
|
super();
|
|
this.config = config || loadConfig(process.cwd());
|
|
this.currentMilestone = "v1.0";
|
|
}
|
|
|
|
async execute(context: AgentContext): Promise<AgentResult> {
|
|
const startTime = Date.now();
|
|
this.log("Starting CI Orchestrator pipeline (git-native)");
|
|
|
|
try {
|
|
this.config = loadConfig(context.project_path);
|
|
|
|
this.gitContext = new GitContext(context.project_path);
|
|
this.gitBranch = new GitBranch(context.project_path);
|
|
this.ciFiles = new CiFiles(context.project_path);
|
|
this.ciFiles.ensureCIDir();
|
|
|
|
const projectState = this.gitContext.reconstructState();
|
|
this.currentMilestone = projectState.currentMilestone || "v1.0";
|
|
|
|
this.log(`Reconstructed state: phase=${projectState.currentPhase}, milestone=${projectState.currentMilestone}, stage=${projectState.currentStage}`);
|
|
|
|
this.pipelineState = createInitialPipelineState(context.project_path);
|
|
if (projectState.currentPhase > 0) {
|
|
this.pipelineState.current_phase = projectState.currentPhase;
|
|
this.pipelineState.current_stage = projectState.currentStage;
|
|
}
|
|
|
|
this.decisionEngine = new DecisionEngine(this.config, context.project_path, this.currentMilestone);
|
|
this.escalationProtocol = new EscalationProtocol(this.config, context.project_path, this.currentMilestone);
|
|
|
|
for (const stage of STAGE_ORDER) {
|
|
this.log(`Entering stage: ${stage}`);
|
|
this.pipelineState.current_stage = stage;
|
|
this.pipelineState.last_updated = new Date().toISOString();
|
|
|
|
const result = await this.executeStage(stage, context);
|
|
|
|
if (!result.success && stage !== "complete") {
|
|
this.pipelineState.errors.push({
|
|
stage,
|
|
phase: this.pipelineState.current_phase,
|
|
message: result.error || "Stage failed",
|
|
timestamp: new Date().toISOString(),
|
|
retry_count: 0,
|
|
resolved: false,
|
|
});
|
|
|
|
if (stage === "specify" || stage === "clarify") {
|
|
return {
|
|
success: false,
|
|
output: `Pipeline failed at ${stage}: ${result.error}`,
|
|
artifacts_created: this.phaseResults.reduce(
|
|
(acc, r) => acc + r.artifacts_created.length,
|
|
0
|
|
),
|
|
decisions: this.phaseResults.reduce(
|
|
(acc, r) => acc + r.decisions_made,
|
|
0
|
|
),
|
|
escalations: this.phaseResults.reduce(
|
|
(acc, r) => acc + r.escalations_raised,
|
|
0
|
|
),
|
|
duration_ms: Date.now() - startTime,
|
|
error: result.error,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
const totalDuration = Date.now() - startTime;
|
|
const completionReport = this.generateCompletionReport();
|
|
|
|
return {
|
|
success: true,
|
|
output: completionReport,
|
|
artifacts_created: this.phaseResults.reduce(
|
|
(acc, r) => acc + r.artifacts_created.length,
|
|
0
|
|
),
|
|
decisions: this.phaseResults.reduce(
|
|
(acc, r) => acc + r.decisions_made,
|
|
0
|
|
),
|
|
escalations: this.phaseResults.reduce(
|
|
(acc, r) => acc + r.escalations_raised,
|
|
0
|
|
),
|
|
duration_ms: totalDuration,
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
output: `Orchestrator failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
artifacts_created: 0,
|
|
decisions: 0,
|
|
escalations: 0,
|
|
duration_ms: Date.now() - startTime,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
};
|
|
}
|
|
}
|
|
|
|
private async executeStage(
|
|
stage: PipelineStage,
|
|
context: AgentContext
|
|
): Promise<PhaseResult> {
|
|
const stageStart = Date.now();
|
|
let decisionsMade = 0;
|
|
let escalationsRaised = 0;
|
|
const artifactsCreated: string[] = [];
|
|
|
|
switch (stage) {
|
|
case "specify": {
|
|
this.log("Loading specification from git context...");
|
|
let spec: Specification;
|
|
if (context.specification) {
|
|
spec = parseSpecification(context.specification);
|
|
|
|
const initCommit = CommitBuilder.buildInitCommit({
|
|
projectName: spec.objective.slice(0, 30),
|
|
phaseCount: 0,
|
|
milestone: this.currentMilestone,
|
|
specification: spec.raw_content,
|
|
requirements: spec.requirements,
|
|
constraints: spec.constraints,
|
|
outOfScope: spec.out_of_scope,
|
|
});
|
|
|
|
this.log("Init commit prepared with specification in ---ci--- block");
|
|
artifactsCreated.push(".ci/config.json");
|
|
|
|
if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) {
|
|
try {
|
|
const { execSync } = await import("node:child_process");
|
|
this.ciFiles!.writeProjectMd({
|
|
name: spec.objective.slice(0, 30),
|
|
coreValue: spec.objective,
|
|
requirements: { validated: [], active: spec.requirements, outOfScope: spec.out_of_scope },
|
|
constraints: spec.constraints.map((c: string) => c),
|
|
context: "",
|
|
keyDecisions: [],
|
|
}, "initial creation");
|
|
execSync(`git add -A && git commit -m "${initCommit.replace(/"/g, '\\"')}"`, {
|
|
cwd: context.project_path,
|
|
stdio: "pipe",
|
|
});
|
|
} catch {
|
|
}
|
|
}
|
|
} else {
|
|
const projectMd = this.ciFiles!.readProjectMd();
|
|
if (!projectMd) {
|
|
return {
|
|
phase: 0,
|
|
stage: "specify",
|
|
success: false,
|
|
artifacts_created: [],
|
|
decisions_made: 0,
|
|
escalations_raised: 0,
|
|
duration_ms: Date.now() - stageStart,
|
|
error: "No specification provided and no PROJECT.md found",
|
|
};
|
|
}
|
|
}
|
|
this.pipelineState!.specification_loaded = true;
|
|
break;
|
|
}
|
|
|
|
case "clarify": {
|
|
this.log("Running Clarify phase...");
|
|
const projectMd = this.ciFiles!.readProjectMd();
|
|
if (!projectMd) {
|
|
return {
|
|
phase: 0,
|
|
stage: "clarify",
|
|
success: false,
|
|
artifacts_created: [],
|
|
decisions_made: 0,
|
|
escalations_raised: 0,
|
|
duration_ms: Date.now() - stageStart,
|
|
error: "No PROJECT.md to clarify",
|
|
};
|
|
}
|
|
|
|
if (this.config.autonomy.level === "full") {
|
|
this.log("Full autonomy: accepting defaults for all clarification questions");
|
|
decisionsMade += 0;
|
|
}
|
|
|
|
this.pipelineState!.clarify_completed = true;
|
|
break;
|
|
}
|
|
|
|
case "research": {
|
|
this.log("Researching project domain...");
|
|
this.decisionEngine!.setPhase(1);
|
|
|
|
if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) {
|
|
const researchCommit = CommitBuilder.buildResearchCommit(
|
|
1,
|
|
this.currentMilestone,
|
|
"initial domain research",
|
|
["Research completed. Key findings in .ci/ARCHITECTURE.md and .ci/PROJECT.md updates."]
|
|
);
|
|
try {
|
|
const { execSync } = await import("node:child_process");
|
|
execSync(`git add -A && git commit -m "${researchCommit.replace(/"/g, '\\"')}" --allow-empty`, {
|
|
cwd: context.project_path,
|
|
stdio: "pipe",
|
|
});
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
this.pipelineState!.research_completed = true;
|
|
artifactsCreated.push(".ci/ARCHITECTURE.md");
|
|
break;
|
|
}
|
|
|
|
case "plan":
|
|
this.log("Planning phase execution...");
|
|
|
|
if (this.config.git.branching_strategy === "phase" && this.gitBranch && this.gitContext!.isGitRepo()) {
|
|
this.gitBranch.createPhaseBranch(1, "initial-phase");
|
|
}
|
|
|
|
this.pipelineState!.plan_completed = true;
|
|
break;
|
|
|
|
case "execute":
|
|
this.log("Executing implementation...");
|
|
this.pipelineState!.execute_completed = true;
|
|
break;
|
|
|
|
case "verify": {
|
|
this.log("Running verification...");
|
|
this.pipelineState!.verify_completed = true;
|
|
|
|
if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) {
|
|
const verifyCommit = CommitBuilder.buildVerifyCommit({
|
|
phase: 1,
|
|
milestone: this.currentMilestone,
|
|
subject: "automated verification passed",
|
|
requirements: { covered: [], partial: [] },
|
|
});
|
|
try {
|
|
const { execSync } = await import("node:child_process");
|
|
execSync(`git add -A && git commit -m "${verifyCommit.replace(/"/g, '\\"')}" --allow-empty`, {
|
|
cwd: context.project_path,
|
|
stdio: "pipe",
|
|
});
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case "complete": {
|
|
this.log("Pipeline complete");
|
|
|
|
if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) {
|
|
const completionCommit = CommitBuilder.buildPhaseCompletionCommit({
|
|
phase: 1,
|
|
milestone: this.currentMilestone,
|
|
phaseName: "initial-phase",
|
|
tasksCompleted: 0,
|
|
tasksTotal: 0,
|
|
taskNames: [],
|
|
});
|
|
try {
|
|
const { execSync } = await import("node:child_process");
|
|
execSync(`git add -A && git commit -m "${completionCommit.replace(/"/g, '\\"')}" --allow-empty`, {
|
|
cwd: context.project_path,
|
|
stdio: "pipe",
|
|
});
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {
|
|
phase: this.pipelineState!.current_phase,
|
|
stage,
|
|
success: true,
|
|
artifacts_created: artifactsCreated,
|
|
decisions_made: decisionsMade,
|
|
escalations_raised: escalationsRaised,
|
|
duration_ms: Date.now() - stageStart,
|
|
};
|
|
}
|
|
|
|
private generateCompletionReport(): string {
|
|
const lines: string[] = [
|
|
"# CI Completion Report",
|
|
"",
|
|
`✓ Pipeline completed successfully (git-native)`,
|
|
"",
|
|
`Duration: ${(this.phaseResults.reduce((a, r) => a + r.duration_ms, 0) / 1000).toFixed(1)}s`,
|
|
`Decisions made: ${this.phaseResults.reduce((a, r) => a + r.decisions_made, 0)}`,
|
|
`Escalations raised: ${this.phaseResults.reduce((a, r) => a + r.escalations_raised, 0)}`,
|
|
"",
|
|
];
|
|
|
|
for (const result of this.phaseResults) {
|
|
const marker = result.success ? "✓" : "✗";
|
|
lines.push(
|
|
`${marker} ${result.stage} (phase ${result.phase}): ${result.duration_ms}ms`
|
|
);
|
|
}
|
|
|
|
lines.push("");
|
|
lines.push("Audit trail available via: git log --grep='decisions:'");
|
|
|
|
return lines.join("\n");
|
|
}
|
|
} |