v0.2.0: Git-native architecture (#1)

This commit was merged in pull request #1.
This commit is contained in:
2026-05-29 12:59:45 +00:00
parent 9cf5c000d9
commit 6e637e4af0
50 changed files with 5852 additions and 135 deletions
+69 -9
View File
@@ -1,5 +1,4 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { execSync } from "node:child_process";
import {
Escalation,
EscalationType,
@@ -8,7 +7,8 @@ import {
ESCALATION_TYPES,
} from "../types/escalation.js";
import { CIConfig } from "../types/config.js";
import { logEscalation } from "./audit.js";
import { CommitBuilder, EscalationCommitInput } from "./commit-builder.js";
import { CommitEscalation } from "../types/commit-meta.js";
export interface EscalationInput {
type: EscalationType;
@@ -24,25 +24,33 @@ export interface EscalationInput {
export class EscalationProtocol {
private config: CIConfig;
private projectPath: string;
private currentMilestone: string;
private counter: number;
private pendingEscalations: Map<string, Escalation>;
private timeoutCallback: (escalation: Escalation, chosenOption: string) => void;
private timers: NodeJS.Timeout[];
constructor(
config: CIConfig,
projectPath: string,
milestone: string = "v1.0",
timeoutCallback: (escalation: Escalation, chosenOption: string) => void = () => {}
) {
this.config = config;
this.projectPath = projectPath;
this.currentMilestone = milestone;
this.counter = 0;
this.pendingEscalations = new Map();
this.timeoutCallback = timeoutCallback;
this.timers = [];
}
setMilestone(milestone: string): void {
this.currentMilestone = milestone;
}
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,
@@ -56,11 +64,28 @@ export class EscalationProtocol {
options: input.options,
default_option_id: input.default_option_id,
resolution: "pending",
audit_file: `.ci/audit/${date}-phase${input.phase}-decisions.json`,
audit_file: `.ci/audit/deprecated`,
};
this.pendingEscalations.set(id, escalation);
logEscalation(this.projectPath, parseInt(input.phase) || 0, escalation);
if (this.config.git.auto_commit) {
const commitEscalation: CommitEscalation = {
id,
type: input.type,
description: input.description,
resolution: "pending",
};
const commitMessage = CommitBuilder.buildEscalationCommit({
phase: parseInt(input.phase) || 0,
milestone: this.currentMilestone,
subject: input.description,
escalations: [commitEscalation],
});
this.commitEscalation(commitMessage);
}
if (this.config.autonomy.escalation_timeout_ms > 0) {
this.scheduleTimeout(escalation);
@@ -81,6 +106,24 @@ export class EscalationProtocol {
escalation.resolved_at = new Date().toISOString();
escalation.resolution_detail = `Chose option: ${chosenOptionId}`;
if (this.config.git.auto_commit) {
const commitEscalation: CommitEscalation = {
id: escalation.id,
type: escalation.type,
description: escalation.description,
resolution: resolution === "timeout_auto_proceed" ? "timeout" : resolution === "approved" ? "auto" : resolution === "rejected" ? "human" : resolution === "modified" ? "human" : resolution,
};
const commitMessage = CommitBuilder.buildEscalationCommit({
phase: parseInt(escalation.phase) || 0,
milestone: this.currentMilestone,
subject: `resolved: ${escalation.description}`,
escalations: [commitEscalation],
});
this.commitEscalation(commitMessage);
}
this.pendingEscalations.delete(escalationId);
return escalation;
}
@@ -93,6 +136,14 @@ export class EscalationProtocol {
return this.pendingEscalations.size > 0;
}
clearAllTimers(): void {
for (const timer of this.timers) {
clearTimeout(timer);
}
this.timers = [];
this.pendingEscalations.clear();
}
formatEscalation(escalation: Escalation): string {
const lines: string[] = [
`⚠️ ESCALATION [${escalation.id}]`,
@@ -126,16 +177,24 @@ export class EscalationProtocol {
);
}
lines.push(`\nAudit: ${escalation.audit_file}`);
return lines.join("\n");
}
private commitEscalation(commitMessage: string): void {
try {
execSync(`git add -A && git commit -m "${commitMessage.replace(/"/g, '\\"')}" --allow-empty`, {
cwd: this.projectPath,
stdio: "pipe",
});
} catch {
}
}
private scheduleTimeout(escalation: Escalation): void {
const timeout = this.config.autonomy.escalation_timeout_ms;
if (timeout <= 0) return;
setTimeout(() => {
const timer = setTimeout(() => {
if (this.pendingEscalations.has(escalation.id)) {
escalation.resolution = "timeout_auto_proceed";
escalation.resolved_at = new Date().toISOString();
@@ -144,5 +203,6 @@ export class EscalationProtocol {
this.timeoutCallback(escalation, escalation.default_option_id);
}
}, timeout);
this.timers.push(timer);
}
}