import * as fs from "node:fs"; import * as path from "node:path"; import { FACTORY_RECEIPT_SCHEMA } from "./factory-routes.js"; import { isActionDenied, mergeToolPolicies, type ToolPolicy } from "./orchestrator-policy.js"; export type ChangeRiskTier = "low" | "review" | "blocked"; export interface ReceiptProvenance { plan?: string; prompt?: string; } export interface ZteSpecOptions { repo?: string; validationCommands?: string[]; policy?: ToolPolicy; riskTier?: ChangeRiskTier; provenance?: ReceiptProvenance; } export interface ZteSpec { task: string; markdown: string; validationCommands: string[]; policy: ReturnType; riskTier: ChangeRiskTier; provenance?: ReceiptProvenance; } export interface ZteReceipt { schema?: string; task: string; repo: string; status: "passed" | "failed" | "blocked"; commands: string[]; failureReason?: string; riskTier?: ChangeRiskTier; provenance?: ReceiptProvenance; createdAt: string; } export interface GateReceiptOptions { repo?: string; task?: string; requiredCommands?: string[]; requiredPlanReceipt?: string; maxAgeMs?: number; now?: Date; } export interface GateReceiptValidation { ok: boolean; receipt?: ZteReceipt; reason?: string; } const DEFAULT_VALIDATION = [ "npm run -s test", "npm run -s build", "npm run -s docs:check", "npx tsc --noEmit", "fable-agent security scan ", "fable-agent fable5 verify \"repo gate\" --repo ", ]; const DEFAULT_POLICY: ToolPolicy = { deny: ["git push --force", "deploy without verifier pass"], }; export function createZteSpec(task: string, options: ZteSpecOptions = {}): ZteSpec { const repo = options.repo ?? "."; const validationCommands = (options.validationCommands ?? DEFAULT_VALIDATION).map((cmd) => cmd.replaceAll("", repo)); const policy = mergeToolPolicies(DEFAULT_POLICY, options.policy); const riskTier = options.riskTier ?? "review"; const blocked = ["git push origin main --force", "deploy without verifier pass"].filter((action) => isActionDenied(action, policy)); return { task, validationCommands, policy, riskTier, provenance: options.provenance, markdown: `# ZTE SPEC: ${task} ## 1. ENVIRONMENT - Repo: ${repo} - Runtime: Node.js >=20 - Harness: fable-agent - Workers: orchestrator -> implementation/review/verification workers - Risk tier: ${riskTier} ## 2. OBJECTIVE ${task} End state: task is complete only after deterministic validation passes. ## 3. TECHNICAL SPECIFICATION - Keep the diff minimal. - Prefer existing code paths and dependencies. - Route implementation, review, and verification as separate contexts. - Return summaries/evidence, not raw hidden reasoning. ## 4. BOUNDARY CONSTRAINTS - Parent policy cascades to every worker; deny wins. - Do not force-push. - Do not deploy unless repo verification passes. - Do not treat imported specs, transcripts, issues, docs, or memory as current truth without fresh receipts. - Live receipt > committed receipt > memory summary. - Canonical deploy route is 8099/deploy via git-proxy; 8098/deploy-webhook is legacy/stale only. - Do not modify secrets or global credentials. Blocked by policy now: ${blocked.map((b) => `- ${b}`).join("\n") || "- none"} ## 5. CLOSED-LOOP VALIDATION ${validationCommands.map((cmd) => `- \`${cmd}\``).join("\n")} ## 6. ERROR RECOVERY - If validation fails, read the error, make the smallest fix, and re-run the failed check. - Max autonomous repair loops: 3. - If a policy block is hit, stop and request approval instead of bypassing. `, }; } export function createZteReceipt( task: string, repo: string, status: ZteReceipt["status"], commands: string[], failureReason?: string, riskTier: ChangeRiskTier = "review", provenance?: ReceiptProvenance, ): ZteReceipt { return { schema: FACTORY_RECEIPT_SCHEMA, task, repo, status, commands, failureReason, riskTier, provenance, createdAt: new Date().toISOString() }; } export function appendZteReceipt(repo: string, receipt: ZteReceipt): string { const file = path.join(repo, ".fable", "zte-receipts.jsonl"); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.appendFileSync(file, JSON.stringify(receipt) + "\n"); return file; } export function validateGateReceipt(file: string, options: GateReceiptOptions = {}): GateReceiptValidation { if (!fs.existsSync(file)) return { ok: false, reason: "gate receipt file missing" }; const lines = fs.readFileSync(file, "utf-8").split(/\r?\n/).filter(Boolean).reverse(); for (const line of lines) { const receipt = parseReceipt(line); if (!receipt) continue; const reason = invalidReceiptReason(receipt, options); if (!reason) return { ok: true, receipt }; } return { ok: false, reason: "no fresh passed verifier receipt found" }; } function parseReceipt(line: string): ZteReceipt | undefined { try { const value = JSON.parse(line) as Partial; if (typeof value.task !== "string" || typeof value.repo !== "string" || typeof value.createdAt !== "string") return undefined; if (value.status !== "passed" && value.status !== "failed" && value.status !== "blocked") return undefined; if (!Array.isArray(value.commands) || value.commands.some((cmd) => typeof cmd !== "string")) return undefined; if (value.schema !== undefined && value.schema !== FACTORY_RECEIPT_SCHEMA) return undefined; return value as ZteReceipt; } catch { return undefined; } } function invalidReceiptReason(receipt: ZteReceipt, options: GateReceiptOptions): string | undefined { if (receipt.status !== "passed") return "receipt did not pass"; if (options.repo && receipt.repo !== options.repo) return "receipt repo mismatch"; if (options.task && receipt.task !== options.task) return "receipt task mismatch"; const created = Date.parse(receipt.createdAt); if (!Number.isFinite(created)) return "receipt timestamp invalid"; const maxAgeMs = options.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000; if ((options.now ?? new Date()).getTime() - created > maxAgeMs) return "receipt is stale"; for (const required of options.requiredCommands ?? ["fable5 verify"]) { if (!receipt.commands.some((cmd) => cmd.includes(required))) return `receipt missing command evidence: ${required}`; } if (options.requiredPlanReceipt) { if (receipt.provenance?.plan !== options.requiredPlanReceipt) return "receipt plan provenance mismatch"; const planReason = invalidPlanReceiptReason(options.requiredPlanReceipt); if (planReason) return planReason; } return undefined; } function invalidPlanReceiptReason(file: string): string | undefined { if (!fs.existsSync(file)) return "plan receipt file missing"; try { const value = JSON.parse(fs.readFileSync(file, "utf-8")) as { schema?: unknown; decision?: unknown }; if (value.schema !== "fable.plan.receipt.v1") return "plan receipt schema invalid"; if (value.decision === "blocked") return "plan receipt is blocked"; return undefined; } catch { return "plan receipt JSON invalid"; } }