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:
CI
2026-05-28 23:24:42 +00:00
commit 9cf5c000d9
57 changed files with 7336 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { writeFile, readFile, ensureDir } from "../utils/file.js";
const PLANNING_DIR = ".planning";
export interface ProjectManifest {
name: string;
objective: string;
created_at: string;
phases: PhaseInfo[];
current_phase: number;
status: "initializing" | "researching" | "planning" | "executing" | "verifying" | "complete" | "error";
}
export interface PhaseInfo {
id: number;
name: string;
status: "pending" | "active" | "complete" | "failed";
started_at?: string;
completed_at?: string;
}
export interface DecisionsManifest {
decisions: Array<{
id: string;
decision: string;
rationale: string;
confidence: number;
category: string;
timestamp: string;
}>;
}
export interface StateManifest {
current_phase: number;
current_stage: string;
last_agent: string;
last_action: string;
updated_at: string;
pipeline_progress: Record<string, boolean>;
}
export class ArtifactManager {
private projectPath: string;
constructor(projectPath: string) {
this.projectPath = projectPath;
}
private get planningDir(): string {
return path.join(this.projectPath, PLANNING_DIR);
}
ensureStructure(): void {
ensureDir(this.planningDir);
ensureDir(path.join(this.planningDir, "phases"));
ensureDir(path.join(this.projectPath, ".ci", "audit"));
}
isInitialized(): boolean {
return fs.existsSync(path.join(this.planningDir, "PROJECT.md"));
}
writeProject(manifest: ProjectManifest): void {
const lines = [
`# ${manifest.name}`,
"",
`**Objective**: ${manifest.objective}`,
`**Created**: ${manifest.created_at}`,
`**Status**: ${manifest.status}`,
`**Current Phase**: ${manifest.current_phase}`,
"",
"## Phases",
"",
];
for (const phase of manifest.phases) {
lines.push(
`- Phase ${phase.id}: ${phase.name} [${phase.status}]`
);
}
lines.push("");
writeFile(path.join(this.planningDir, "PROJECT.md"), lines.join("\n"));
}
writeDecisions(decisions: DecisionsManifest): void {
const lines = [
"# Decisions Log",
"",
`Total decisions: ${decisions.decisions.length}`,
"",
];
for (const d of decisions.decisions) {
lines.push(`## ${d.id}: ${d.decision}`);
lines.push(`- **Category**: ${d.category}`);
lines.push(`- **Confidence**: ${(d.confidence * 100).toFixed(0)}%`);
lines.push(`- **Rationale**: ${d.rationale}`);
lines.push(`- **Timestamp**: ${d.timestamp}`);
lines.push("");
}
writeFile(path.join(this.planningDir, "DECISIONS.md"), lines.join("\n"));
}
writeState(state: StateManifest): void {
writeJSON(path.join(this.planningDir, "STATE.md.json"), state);
const lines = [
"# Project State",
"",
`**Current Phase**: ${state.current_phase}`,
`**Current Stage**: ${state.current_stage}`,
`**Last Agent**: ${state.last_agent}`,
`**Last Action**: ${state.last_action}`,
`**Updated**: ${state.updated_at}`,
"",
"## Pipeline Progress",
"",
];
for (const [stage, complete] of Object.entries(
state.pipeline_progress
)) {
lines.push(`- ${stage}: ${complete ? "✓" : "○"}`);
}
lines.push("");
writeFile(path.join(this.planningDir, "STATE.md"), lines.join("\n"));
}
readState(): StateManifest | null {
const filePath = path.join(this.planningDir, "STATE.md.json");
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
}
writePhaseArtifact(
phase: number,
artifactName: string,
content: string
): void {
const phaseDir = path.join(this.planningDir, "phases", `phase-${phase}`);
ensureDir(phaseDir);
writeFile(path.join(phaseDir, artifactName), content);
}
readPhaseArtifact(
phase: number,
artifactName: string
): string | null {
const filePath = path.join(
this.planningDir,
"phases",
`phase-${phase}`,
artifactName
);
return readFile(filePath);
}
}
function writeJSON(filePath: string, data: unknown): void {
writeFile(filePath, JSON.stringify(data, null, 2));
}
+134
View File
@@ -0,0 +1,134 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { Decision } from "../types/decisions.js";
import { Escalation } from "../types/escalation.js";
export interface AuditEntry {
phase: number;
decisions: Decision[];
escalations: Escalation[];
}
const AUDIT_DIR = "audit";
function getAuditDir(projectPath: string): string {
return path.join(projectPath, ".ci", AUDIT_DIR);
}
function getAuditFilePath(projectPath: string, phase: number): string {
const date = new Date().toISOString().split("T")[0];
return path.join(getAuditDir(projectPath), `${date}-phase${phase}-decisions.json`);
}
function ensureAuditDir(projectPath: string): void {
const dir = getAuditDir(projectPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
export function logDecision(
projectPath: string,
phase: number,
decision: Decision
): void {
ensureAuditDir(projectPath);
const filePath = getAuditFilePath(projectPath, phase);
let entry: AuditEntry;
if (fs.existsSync(filePath)) {
entry = JSON.parse(fs.readFileSync(filePath, "utf-8"));
} else {
entry = { phase, decisions: [], escalations: [] };
}
entry.decisions.push(decision);
fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), "utf-8");
}
export function logEscalation(
projectPath: string,
phase: number,
escalation: Escalation
): void {
ensureAuditDir(projectPath);
const filePath = getAuditFilePath(projectPath, phase);
let entry: AuditEntry;
if (fs.existsSync(filePath)) {
entry = JSON.parse(fs.readFileSync(filePath, "utf-8"));
} else {
entry = { phase, decisions: [], escalations: [] };
}
entry.escalations.push(escalation);
fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), "utf-8");
}
export function readAudit(
projectPath: string,
phase?: number
): AuditEntry[] {
const auditDir = getAuditDir(projectPath);
if (!fs.existsSync(auditDir)) return [];
const files = fs
.readdirSync(auditDir)
.filter((f) => f.endsWith("-decisions.json"))
.sort();
const entries: AuditEntry[] = [];
for (const file of files) {
const content = fs.readFileSync(path.join(auditDir, file), "utf-8");
const entry: AuditEntry = JSON.parse(content);
if (phase === undefined || entry.phase === phase) {
entries.push(entry);
}
}
return entries;
}
export function getAuditSummary(projectPath: string): {
total_decisions: number;
total_escalations: number;
phases: number[];
decisions_by_confidence: Record<string, number>;
escalations_by_type: Record<string, number>;
} {
const entries = readAudit(projectPath);
let total_decisions = 0;
let total_escalations = 0;
const phases = new Set<number>();
const decisions_by_confidence: Record<string, number> = {
high: 0,
medium: 0,
low: 0,
};
const escalations_by_type: Record<string, number> = {};
for (const entry of entries) {
phases.add(entry.phase);
total_decisions += entry.decisions.length;
total_escalations += entry.escalations.length;
for (const d of entry.decisions) {
const level =
d.confidence > 0.85 ? "high" : d.confidence >= 0.6 ? "medium" : "low";
decisions_by_confidence[level]++;
}
for (const e of entry.escalations) {
escalations_by_type[e.type] =
(escalations_by_type[e.type] || 0) + 1;
}
}
return {
total_decisions,
total_escalations,
phases: [...phases],
decisions_by_confidence,
escalations_by_type,
};
}
+220
View File
@@ -0,0 +1,220 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { ClarifyQuestion, ClarifyResult } from "../types/clarify.js";
import { Specification, parseSpecification } from "../types/specification.js";
import { CIConfig } from "../types/config.js";
const CLARIFY_RESPONSES_FILE = "clarify-responses.md";
const SPECIFICATION_FILE = "specification.md";
function getCIDir(projectPath: string): string {
return path.join(projectPath, ".ci");
}
export class ClarifyPhase {
private config: CIConfig;
private projectPath: string;
private questions: ClarifyQuestion[];
private questionCounter: number;
constructor(config: CIConfig, projectPath: string) {
this.config = config;
this.projectPath = projectPath;
this.questions = [];
this.questionCounter = 0;
}
generateQuestions(spec: Specification): ClarifyQuestion[] {
this.questions = [];
const budget = this.config.autonomy.clarify_budget;
const ambiguities = this.identifyAmbiguities(spec);
const sorted = ambiguities.sort((a, b) => {
const priority = { critical: 0, high: 1, medium: 2, low: 3 } as const;
return priority[a.impact] - priority[b.impact];
});
for (const ambiguity of sorted.slice(0, budget)) {
this.questionCounter++;
const question: ClarifyQuestion = {
id: `Q-${String(this.questionCounter).padStart(3, "0")}`,
question: ambiguity.question,
context: ambiguity.context,
default_answer: ambiguity.default_answer,
rationale: ambiguity.rationale,
impact: ambiguity.impact,
category: ambiguity.category,
answered: false,
};
this.questions.push(question);
}
return this.questions;
}
answerQuestion(questionId: string, answer: string): ClarifyQuestion | null {
const question = this.questions.find((q) => q.id === questionId);
if (!question) return null;
question.answered = true;
question.answer = answer;
question.agent_interpretation = answer;
return question;
}
acceptDefaults(): ClarifyResult {
for (const q of this.questions) {
if (!q.answered) {
q.answered = true;
q.answer = `DEFAULT: ${q.default_answer}`;
q.agent_interpretation = q.default_answer;
}
}
return this.finalize();
}
finalize(): ClarifyResult {
const answered = this.questions.filter((q) => q.answered);
const unanswered_defaults = this.questions.filter(
(q) => q.answer && q.answer.startsWith("DEFAULT:")
);
const result: ClarifyResult = {
questions: [...this.questions],
total_questions: this.questions.length,
answered_questions: answered.length,
unanswered_defaults_accepted: unanswered_defaults.length,
completed_at: new Date().toISOString(),
};
this.saveResponses(result);
return result;
}
private saveResponses(result: ClarifyResult): void {
const ciDir = getCIDir(this.projectPath);
if (!fs.existsSync(ciDir)) {
fs.mkdirSync(ciDir, { recursive: true });
}
const lines: string[] = [
"# Clarify Phase Responses",
"",
`Completed: ${result.completed_at}`,
`Questions asked: ${result.total_questions}`,
`Questions answered: ${result.answered_questions}`,
`Defaults accepted: ${result.unanswered_defaults_accepted}`,
"",
];
for (const q of result.questions) {
lines.push(`## ${q.id}: ${q.question}`);
lines.push(`- **Context**: ${q.context}`);
lines.push(`- **Impact**: ${q.impact}`);
lines.push(`- **Default**: ${q.default_answer}`);
lines.push(`- **Rationale**: ${q.rationale}`);
lines.push(`- **Answer**: ${q.answer || "DEFAULT: " + q.default_answer}`);
if (q.agent_interpretation) {
lines.push(`- **Interpretation**: ${q.agent_interpretation}`);
}
lines.push("");
}
fs.writeFileSync(
path.join(ciDir, CLARIFY_RESPONSES_FILE),
lines.join("\n"),
"utf-8"
);
}
private identifyAmbiguities(
spec: Specification
): Array<{
question: string;
context: string;
default_answer: string;
rationale: string;
impact: "critical" | "high" | "medium" | "low";
category: string;
}> {
const ambiguities: Array<{
question: string;
context: string;
default_answer: string;
rationale: string;
impact: "critical" | "high" | "medium" | "low";
category: string;
}> = [];
if (spec.requirements.length === 0) {
ambiguities.push({
question: "What are the core functional requirements for this project?",
context: "No explicit requirements were provided in the specification.",
default_answer: "Infer requirements from objective and constraints",
rationale: "Without requirements, scope and deliverables are undefined",
impact: "critical",
category: "requirements",
});
}
if (spec.constraints.length === 0) {
ambiguities.push({
question: "Are there any technical or business constraints for this project?",
context: "No constraints were specified, which may lead to design choices that conflict with your needs.",
default_answer: "No specific constraints — agent will choose best practices",
rationale: "Constraints prevent inappropriate technology or architecture choices",
impact: "high",
category: "constraints",
});
}
const hasDeploy = spec.requirements.some(
(r) =>
r.toLowerCase().includes("deploy") ||
r.toLowerCase().includes("host") ||
r.toLowerCase().includes("server")
);
if (hasDeploy) {
const hasDeployConstraint = spec.constraints.some(
(c) =>
c.toLowerCase().includes("deploy") ||
c.toLowerCase().includes("aws") ||
c.toLowerCase().includes("gcp") ||
c.toLowerCase().includes("azure") ||
c.toLowerCase().includes("docker")
);
if (!hasDeployConstraint) {
ambiguities.push({
question: "What deployment target and strategy should be used?",
context: "Deployment is mentioned in requirements but no deployment constraints specified.",
default_answer: "Docker containers on standard cloud provider",
rationale: "Deployment target affects architecture decisions significantly",
impact: "high",
category: "deployment",
});
}
}
return ambiguities;
}
}
export function saveSpecification(projectPath: string, spec: Specification): void {
const ciDir = getCIDir(projectPath);
if (!fs.existsSync(ciDir)) {
fs.mkdirSync(ciDir, { recursive: true });
}
fs.writeFileSync(
path.join(ciDir, SPECIFICATION_FILE),
spec.raw_content,
"utf-8"
);
}
export function loadSpecification(projectPath: string): Specification | null {
const specPath = path.join(getCIDir(projectPath), SPECIFICATION_FILE);
if (!fs.existsSync(specPath)) return null;
const content = fs.readFileSync(specPath, "utf-8");
return parseSpecification(content, "file");
}
+65
View File
@@ -0,0 +1,65 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { CIConfig, DEFAULT_CI_CONFIG } from "../types/config.js";
const CI_DIR = ".ci";
const CONFIG_FILE = "config.json";
export function getCIConfigPath(projectPath: string): string {
return path.join(projectPath, CI_DIR, CONFIG_FILE);
}
export function getCIDir(projectPath: string): string {
return path.join(projectPath, CI_DIR);
}
export function ensureCIDir(projectPath: string): void {
const ciDir = getCIDir(projectPath);
if (!fs.existsSync(ciDir)) {
fs.mkdirSync(ciDir, { recursive: true });
}
const auditDir = path.join(ciDir, "audit");
if (!fs.existsSync(auditDir)) {
fs.mkdirSync(auditDir, { recursive: true });
}
}
export function loadConfig(projectPath: string): CIConfig {
const configPath = getCIConfigPath(projectPath);
if (!fs.existsSync(configPath)) {
return { ...DEFAULT_CI_CONFIG };
}
const raw = fs.readFileSync(configPath, "utf-8");
const parsed = JSON.parse(raw);
return { ...DEFAULT_CI_CONFIG, ...parsed } as CIConfig;
}
export function saveConfig(projectPath: string, config: CIConfig): void {
ensureCIDir(projectPath);
const configPath = getCIConfigPath(projectPath);
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
}
export function isCIInitialized(projectPath: string): boolean {
const ciDir = getCIDir(projectPath);
const configPath = getCIConfigPath(projectPath);
return fs.existsSync(ciDir) && fs.existsSync(configPath);
}
export function initCI(projectPath: string, config?: Partial<CIConfig>): CIConfig {
ensureCIDir(projectPath);
const fullConfig: CIConfig = {
...DEFAULT_CI_CONFIG,
...config,
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, ...config?.autonomy },
parallelization: {
...DEFAULT_CI_CONFIG.parallelization,
...config?.parallelization,
},
verification: { ...DEFAULT_CI_CONFIG.verification, ...config?.verification },
security: { ...DEFAULT_CI_CONFIG.security, ...config?.security },
git: { ...DEFAULT_CI_CONFIG.git, ...config?.git },
};
saveConfig(projectPath, fullConfig);
return fullConfig;
}
+116
View File
@@ -0,0 +1,116 @@
import * as crypto from "node:crypto";
import { Decision, DecisionCategory, Alternative, confidenceToLevel } from "../types/decisions.js";
import { CIConfig } from "../types/config.js";
import { logDecision } from "./audit.js";
export interface DecisionInput {
decision: string;
rationale: string;
confidence: number;
category: DecisionCategory;
alternatives_considered: Alternative[];
learnship_equivalent: string;
phase?: string;
task?: string;
}
export interface DecisionResult {
decision: Decision;
escalated: boolean;
reason?: string;
}
export class DecisionEngine {
private config: CIConfig;
private projectPath: string;
private currentPhase: number;
private decisionCounter: number;
constructor(config: CIConfig, projectPath: string) {
this.config = config;
this.projectPath = projectPath;
this.currentPhase = 0;
this.decisionCounter = 0;
}
setPhase(phase: number): void {
this.currentPhase = phase;
}
makeDecision(input: DecisionInput): DecisionResult {
const id = `D-${String(++this.decisionCounter).padStart(3, "0")}`;
const threshold = this.config.autonomy.decision_confidence_threshold;
const decision: Decision = {
id,
timestamp: new Date().toISOString(),
decision: input.decision,
rationale: input.rationale,
confidence: input.confidence,
category: input.category,
alternatives_considered: input.alternatives_considered,
learnship_equivalent: input.learnship_equivalent,
human_override: null,
phase: input.phase,
task: input.task,
};
logDecision(this.projectPath, this.currentPhase, decision);
const confidenceLevel = confidenceToLevel(input.confidence);
if (input.confidence < threshold) {
return {
decision,
escalated: true,
reason: `Confidence ${input.confidence.toFixed(2)} below threshold ${threshold} (${confidenceLevel})`,
};
}
return { decision, escalated: false };
}
makeHighConfidenceDecision(
decision: string,
rationale: string,
category: DecisionCategory,
alternatives: Alternative[] = [],
learnship_equivalent: string = ""
): DecisionResult {
return this.makeDecision({
decision,
rationale,
confidence: 0.95,
category,
alternatives_considered: alternatives,
learnship_equivalent,
});
}
makeMediumConfidenceDecision(
decision: string,
rationale: string,
category: DecisionCategory,
alternatives: Alternative[] = [],
learnship_equivalent: string = ""
): DecisionResult {
return this.makeDecision({
decision,
rationale,
confidence: 0.7,
category,
alternatives_considered: alternatives,
learnship_equivalent,
});
}
shouldAutoDecide(confidence: number): boolean {
return confidence >= this.config.autonomy.decision_confidence_threshold;
}
isIrreversibleAction(action: string): boolean {
return this.config.autonomy.escalation_hooks.some((hook) =>
action.toLowerCase().includes(hook.toLowerCase())
);
}
}
+93
View File
@@ -0,0 +1,93 @@
import { CIConfig } from "../types/config.js";
import { ArtifactManager } from "./artifacts.js";
import { DecisionEngine } from "./decision-engine.js";
export interface RetryConfig {
max_retries: number;
backoff_ms: number;
current_attempt: number;
}
export interface RecoveryResult {
recovered: boolean;
strategy: "retry" | "plan_revision" | "rollback" | "escalate";
attempts: number;
message: string;
}
export class ErrorRecovery {
private config: CIConfig;
private projectPath: string;
private revisionCount: number;
constructor(config: CIConfig, projectPath: string) {
this.config = config;
this.projectPath = projectPath;
this.revisionCount = 0;
}
async recoverFromFailure(
error: string,
phase: number,
stage: string,
attempt: number = 1
): Promise<RecoveryResult> {
if (attempt > this.config.autonomy.max_verification_retries + 1) {
return {
recovered: false,
strategy: "escalate",
attempts: attempt,
message: `Max retries (${this.config.autonomy.max_verification_retries}) exceeded for ${stage} in phase ${phase}: ${error}`,
};
}
if (stage === "verify" && attempt <= this.config.autonomy.max_verification_retries) {
return {
recovered: true,
strategy: "retry",
attempts: attempt,
message: `Retrying verification (attempt ${attempt}/${this.config.autonomy.max_verification_retries})`,
};
}
if (stage === "plan" && this.revisionCount < this.config.autonomy.max_revision_iterations) {
this.revisionCount++;
return {
recovered: true,
strategy: "plan_revision",
attempts: this.revisionCount,
message: `Revising plan (iteration ${this.revisionCount}/${this.config.autonomy.max_revision_iterations})`,
};
}
return {
recovered: false,
strategy: "escalate",
attempts: attempt,
message: `Cannot recover from failure in ${stage} for phase ${phase}: ${error}`,
};
}
async rollback(phase: number, reason: string): Promise<RecoveryResult> {
const artifactManager = new ArtifactManager(this.projectPath);
return {
recovered: true,
strategy: "rollback",
attempts: 1,
message: `Rolled back phase ${phase}: ${reason}`,
};
}
canAutoDebug(error: string, confidence: number): boolean {
return confidence >= this.config.autonomy.decision_confidence_threshold;
}
getMaxRetries(): number {
return this.config.autonomy.max_verification_retries;
}
getMaxRevisions(): number {
return this.config.autonomy.max_revision_iterations;
}
}
+148
View File
@@ -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);
}
}
+9
View File
@@ -0,0 +1,9 @@
export { initCI, loadConfig, saveConfig, isCIInitialized, getCIConfigPath, getCIDir, ensureCIDir } from "./config.js";
export { DecisionEngine } from "./decision-engine.js";
export { EscalationProtocol } from "./escalation.js";
export { ClarifyPhase, saveSpecification, loadSpecification } from "./clarify.js";
export { ArtifactManager } from "./artifacts.js";
export { ErrorRecovery } from "./error-recovery.js";
export { logDecision, logEscalation, readAudit, getAuditSummary } from "./audit.js";
export type { CIConfig } from "../types/config.js";
export { DEFAULT_CI_CONFIG } from "../types/config.js";