feat: implement CI (Continuous Intelligence) autonomous engineering harness
Implements the full PRD for CI - a fully autonomous AI-driven software engineering harness derived from Learnship's architecture. Core components: - CI Orchestrator agent with autonomous pipeline (SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → VERIFY → COMPLETE) - Decision Engine with confidence thresholds (high/medium/low) - Clarify Phase with question budget and default acceptance - Escalation Protocol with timeout auto-proceed - Audit Trail system (.ci/audit/) for post-hoc review - Error Recovery with retry, plan revision, and rollback 18 agents (all Learnship agents + Orchestrator): - Autonomous behavioral modifications per PRD §7.1 - Agent registry with factory pattern 11 CLI commands: - ci init, ci run, ci quick, ci debug, ci verify - ci review, ci status, ci audit, ci clarify - ci rollback, ci ship 4-layer verification system: - Structural, Behavioral, Security, Code Quality 3 autonomy levels: full, supervised, guided Compatible with Learnship artifact schemas (.planning/)
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
Escalation,
|
||||
EscalationType,
|
||||
EscalationOption,
|
||||
EscalationResolution,
|
||||
ESCALATION_TYPES,
|
||||
} from "../types/escalation.js";
|
||||
import { CIConfig } from "../types/config.js";
|
||||
import { logEscalation } from "./audit.js";
|
||||
|
||||
export interface EscalationInput {
|
||||
type: EscalationType;
|
||||
phase: string;
|
||||
description: string;
|
||||
context: string;
|
||||
options: EscalationOption[];
|
||||
default_option_id: string;
|
||||
plan?: string;
|
||||
task?: string;
|
||||
}
|
||||
|
||||
export class EscalationProtocol {
|
||||
private config: CIConfig;
|
||||
private projectPath: string;
|
||||
private counter: number;
|
||||
private pendingEscalations: Map<string, Escalation>;
|
||||
private timeoutCallback: (escalation: Escalation, chosenOption: string) => void;
|
||||
|
||||
constructor(
|
||||
config: CIConfig,
|
||||
projectPath: string,
|
||||
timeoutCallback: (escalation: Escalation, chosenOption: string) => void = () => {}
|
||||
) {
|
||||
this.config = config;
|
||||
this.projectPath = projectPath;
|
||||
this.counter = 0;
|
||||
this.pendingEscalations = new Map();
|
||||
this.timeoutCallback = timeoutCallback;
|
||||
}
|
||||
|
||||
escalate(input: EscalationInput): Escalation {
|
||||
const id = `E-${String(++this.counter).padStart(3, "0")}`;
|
||||
const date = new Date().toISOString().split("T")[0];
|
||||
|
||||
const escalation: Escalation = {
|
||||
id,
|
||||
timestamp: new Date().toISOString(),
|
||||
type: input.type,
|
||||
phase: input.phase,
|
||||
plan: input.plan,
|
||||
task: input.task,
|
||||
description: input.description,
|
||||
context: input.context,
|
||||
options: input.options,
|
||||
default_option_id: input.default_option_id,
|
||||
resolution: "pending",
|
||||
audit_file: `.ci/audit/${date}-phase${input.phase}-decisions.json`,
|
||||
};
|
||||
|
||||
this.pendingEscalations.set(id, escalation);
|
||||
logEscalation(this.projectPath, parseInt(input.phase) || 0, escalation);
|
||||
|
||||
if (this.config.autonomy.escalation_timeout_ms > 0) {
|
||||
this.scheduleTimeout(escalation);
|
||||
}
|
||||
|
||||
return escalation;
|
||||
}
|
||||
|
||||
resolveEscalation(
|
||||
escalationId: string,
|
||||
chosenOptionId: string,
|
||||
resolution: EscalationResolution = "approved"
|
||||
): Escalation | null {
|
||||
const escalation = this.pendingEscalations.get(escalationId);
|
||||
if (!escalation) return null;
|
||||
|
||||
escalation.resolution = resolution;
|
||||
escalation.resolved_at = new Date().toISOString();
|
||||
escalation.resolution_detail = `Chose option: ${chosenOptionId}`;
|
||||
|
||||
this.pendingEscalations.delete(escalationId);
|
||||
return escalation;
|
||||
}
|
||||
|
||||
getPendingEscalations(): Escalation[] {
|
||||
return [...this.pendingEscalations.values()];
|
||||
}
|
||||
|
||||
hasPending(): boolean {
|
||||
return this.pendingEscalations.size > 0;
|
||||
}
|
||||
|
||||
formatEscalation(escalation: Escalation): string {
|
||||
const lines: string[] = [
|
||||
`⚠️ ESCALATION [${escalation.id}]`,
|
||||
"",
|
||||
`Type: ${ESCALATION_TYPES[escalation.type]}`,
|
||||
`Phase: ${escalation.phase}${escalation.plan ? `, Plan: ${escalation.plan}` : ""}${escalation.task ? `, Task: ${escalation.task}` : ""}`,
|
||||
`Decision Required: ${escalation.description}`,
|
||||
"",
|
||||
`Context: ${escalation.context}`,
|
||||
"",
|
||||
"Options:",
|
||||
];
|
||||
|
||||
for (const opt of escalation.options) {
|
||||
const marker = opt.recommended ? " (recommended)" : "";
|
||||
lines.push(` ${opt.id}) ${opt.label}${marker} - ${opt.description}`);
|
||||
}
|
||||
|
||||
const defaultOpt = escalation.options.find(
|
||||
(o) => o.id === escalation.default_option_id
|
||||
);
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`Default: ${defaultOpt?.label || escalation.default_option_id}`
|
||||
);
|
||||
|
||||
if (this.config.autonomy.escalation_timeout_ms > 0) {
|
||||
const seconds = Math.floor(this.config.autonomy.escalation_timeout_ms / 1000);
|
||||
lines.push(
|
||||
`(auto-proceed in ${seconds}s if no response)`
|
||||
);
|
||||
}
|
||||
|
||||
lines.push(`\nAudit: ${escalation.audit_file}`);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
private scheduleTimeout(escalation: Escalation): void {
|
||||
const timeout = this.config.autonomy.escalation_timeout_ms;
|
||||
if (timeout <= 0) return;
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.pendingEscalations.has(escalation.id)) {
|
||||
escalation.resolution = "timeout_auto_proceed";
|
||||
escalation.resolved_at = new Date().toISOString();
|
||||
escalation.resolution_detail = `Auto-proceeded with default: ${escalation.default_option_id}`;
|
||||
this.pendingEscalations.delete(escalation.id);
|
||||
this.timeoutCallback(escalation, escalation.default_option_id);
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user