9cf5c000d9
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/)
148 lines
4.4 KiB
TypeScript
148 lines
4.4 KiB
TypeScript
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);
|
|
}
|
|
} |