ab6af144b7
---ci---
phase: 6
milestone: v0.5
status: complete
decisions:
- id: D-006
decision: Research as intermediate work product
rationale: Conclusions update .ci/ files; full research doc intentionally not preserved
confidence: 0.90
- id: D-007
decision: Branch hierarchy enforcement: main > milestone > phase
rationale: Prevents out-of-order merges and semantically wrong tags
confidence: 0.92
- id: D-008
decision: 3-tier versioning: NFR/feature/schema-breaking
rationale: Patch per phase (NFR/feature) or minor per phase (schema-breaking); milestone gets minor (feature) or major (schema-breaking)
confidence: 0.95
requirements:
covered: [VER-06, BRANCH-01, BRANCH-02, ARCH-01]
---/ci---
- Synthesize ARCHITECTURE-PLAN.md into .ci/ci/ARCHITECTURE.md (expanded 51→230 lines)
- Add D-006/D-007/D-008 to .ci/ci/PROJECT.md key decisions table
- Delete ARCHITECTURE-PLAN.md after synthesis
- Rewrite ship.md with 3-tier versioning model + branch hierarchy merge flows
- Rewrite branch-strategy.md with 3-tier versioning + branch hierarchy + version validation
- Add MilestoneType to config types
- Replace isNfrMilestone() with getMilestoneType() returning nfr|feature|schema-breaking
- Add validateMergeOrder(), mergeMilestoneBranch(), computeMilestoneTag() to GitBranch
- Add computeShipVersion(), validateVersionOrder(), resolveMergeTarget() to ship command
- Remove hardcoded v0.5. from error-recovery rollback
- Create .githooks/pre-push for semver ordering + branch hierarchy validation
- Add 15 new tests (370 total, all passing)
130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
import { execSync } from "node:child_process";
|
|
import { CIConfig } from "../types/config.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> {
|
|
try {
|
|
const phaseBranch = `phase/${String(phase).padStart(2, "0")}`;
|
|
const branches = this.git("branch --list");
|
|
const branchExists = branches.split("\n").some((b) => b.trim().replace(/^\*?\s+/, "") === phaseBranch);
|
|
|
|
if (branchExists) {
|
|
const currentBranch = this.git("rev-parse --abbrev-ref HEAD");
|
|
if (currentBranch === phaseBranch) {
|
|
this.git("checkout main");
|
|
}
|
|
this.git(`branch -D ${phaseBranch}`);
|
|
}
|
|
|
|
const tags = this.git("tag -l").split("\n").map((t) => t.trim()).filter(Boolean);
|
|
const phaseTagPattern = new RegExp(`^v\\d+\\.\\d+\\.${phase}$`);
|
|
const matchingTag = tags.find((t) => phaseTagPattern.test(t));
|
|
if (matchingTag) {
|
|
this.git(`tag -d ${matchingTag}`);
|
|
}
|
|
|
|
return {
|
|
recovered: true,
|
|
strategy: "rollback",
|
|
attempts: 1,
|
|
message: `Rolled back phase ${phase}: ${reason}. Branch ${branchExists ? `${phaseBranch} deleted` : "not found"}. Tag ${matchingTag ? `${matchingTag} deleted` : "not found"}.`,
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
recovered: false,
|
|
strategy: "rollback",
|
|
attempts: 1,
|
|
message: `Rollback failed for phase ${phase}: ${err instanceof Error ? err.message : String(err)}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private git(args: string): string {
|
|
try {
|
|
return execSync(`git ${args}`, {
|
|
cwd: this.projectPath,
|
|
encoding: "utf-8",
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
}).trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
} |