@tea-agent/loop-agent 0.42.0-next.2 → 0.42.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +242 -1
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/task-lifecycle/advance.js +24 -5
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +3784 -231
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +942 -187
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/infrastructure/console/operation-store.js +226 -6
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +7 -0
- package/dist/task/config-types.js +34 -5
- package/dist/task/contract/adopt.js +4 -0
- package/dist/task/contract/import-revision.js +4 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +203 -20
- package/dist/task/runtime.js +5 -2
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +15 -9
- package/dist/task/source-prepare/prepare.js +89 -1
- package/dist/task/source-prepare/semantic-intake.js +6 -2
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/chat-ui-policy.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +173 -7
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +30 -19
- package/dist/worker/console/chat/session-store.js +117 -70
- package/dist/worker/console/chat/shortcuts.js +1 -1
- package/dist/worker/console/chat/turn-process.js +111 -56
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operator-actions.js +169 -7
- package/dist/worker/console/server.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CeznJ4Iw.js} +1 -1
- package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-B4u9u7SX.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-oghpT81p.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-BPJ5Kvcj.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-CoIu69pd.js} +1 -1
- package/dist/worker/console/static/assets/channel-DmIXqJ6B.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-DtgiAsSc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-YSu_EVSD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-DaRO-NB6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-Dh0ZKiGA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DOrvbR-H.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-C3b22LN2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-CXlqO8l6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-BblTBrDu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-DswqPALe.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-CfTwVOlc.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-MFV5Qxb5.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-MFV5Qxb5.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-bLD8jVFo.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-BvoG1fh5.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-DEPAdJu3.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-DmrNvBLy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-CdwzIwvs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-CJ3hj-md.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-C6YJT2a4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-C_LPfjhX.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-xvsSb5Ie.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-Rxp2tKcD.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-DRyLYmZh.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-TLSU_D2B.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DGqeTvHC.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-BZxyH2wh.js} +1 -1
- package/dist/worker/console/static/assets/index-C8V8K9b4.js +451 -0
- package/dist/worker/console/static/assets/index-Cnf_KxTQ.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-BsYtzx01.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-Qr-ezGv-.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-DYbB3ibo.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-BHJHewZO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-CmPIPmCG.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-F--C1uV0.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-YTXfL1st.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-DdDMYXP9.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-C3nH4KVd.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-kXyxjG7Y.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-KNzbl-7w.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-CSwI6dG0.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-Bl1AyuXy.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-2y4EmXQa.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-E8v8YrMg.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-eGGlR856.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8e4J3at.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-B7g7xc07.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Do88I1Nb.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-DR3VCHBG.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-LUvapZIy.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-C7GDcaXq.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CbNAqoGK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/workspace-context.js +111 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/static/dag-node-purpose.js +5 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +907 -162
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +1 -432
- package/dist/workflows/dag/frontend-review-context.js +261 -15
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +419 -0
- package/dist/workflows/dag/frontend-shape.js +435 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +249 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +285 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +677 -492
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +732 -4
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +256 -1
- package/dist/workflows/dag/rerun-plan.js +17 -6
- package/dist/workflows/dag/rerun-run.js +23 -0
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +214 -104
- package/dist/workflows/dag/runner.js +433 -126
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/types.js +239 -12
- package/dist/workflows/dag/validate.js +28 -12
- package/docs/examples/README.md +5 -0
- package/docs/init-surface.manifest.json +30 -12
- package/docs/skills/vetted-skill-registry.md +4 -2
- package/docs/templates/README.md +2 -0
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +4 -1
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +15 -14
- package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
- package/skills/frontend-contract/SKILL.md +23 -0
- package/skills/frontend-contract/references/contract-protocol.md +34 -0
- package/skills/frontend-design-review/SKILL.md +22 -41
- package/skills/frontend-plan/SKILL.md +26 -0
- package/skills/frontend-plan/references/decision-contract.md +37 -0
- package/skills/frontend-plan/references/design-decisions.md +17 -0
- package/skills/frontend-review/SKILL.md +20 -15
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/frontend-scout/SKILL.md +25 -0
- package/skills/frontend-scout/references/design-evidence.md +16 -0
- package/skills/frontend-scout/references/scout-evidence.md +23 -0
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
- package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
- package/skills/frontend-implementation/SKILL.md +0 -52
- package/skills/frontend-implementation/references/code-standards.md +0 -33
- package/skills/frontend-implementation/references/design-spec.md +0 -56
- package/skills/frontend-implementation/references/node-contracts.md +0 -31
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
3
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
4
4
|
import { hostname } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -7,7 +7,7 @@ import { isHardBudgetBreached, resolveEffectiveMaxConcurrent, } from "../../appl
|
|
|
7
7
|
import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
|
|
8
8
|
import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
|
|
9
9
|
import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
|
|
10
|
-
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun,
|
|
10
|
+
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
|
|
11
11
|
import { markInterruptNeedsReconcile, mergeAbortSignals, readInterruptRequest, runnerIdentityFromState, settleDagInterrupt, startInterruptWatcher, } from "./interrupt-request.js";
|
|
12
12
|
import { moveToCompletedRunDir, moveToPausedRunDir, prepareRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
|
|
13
13
|
import { evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
@@ -26,7 +26,12 @@ import { runConvergencePassController, } from "./convergence/controller.js";
|
|
|
26
26
|
import { executeDagRanksOnce, FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isConditionSkippedReason, readFrontendPrewriteResult, } from "./scheduler.js";
|
|
27
27
|
import { topoSortToRanks } from "./topo.js";
|
|
28
28
|
import { isFrontendWriterTransientPartialWrite } from "./frontend-writer-recovery.js";
|
|
29
|
+
import { classifyReviewIssueCategoryFailureOwner, readCommittedDesignRequestFact, readCommittedReviewRequestFact, writeFrontendReviewFindingsPrd, } from "./frontend-review-findings.js";
|
|
30
|
+
import { bindFrontendShapeStateToRunTopology, commitFrontendShapeTransitionCas, commitRuntimeFrontendShapeFact, isCanonicalFrontendShapeDag, } from "./frontend-shape-facts.js";
|
|
31
|
+
import { assertFrontendTopologyBinding, buildFrontendTopologyBinding, computeFrontendShapeSourceDigest, } from "./frontend-shape.js";
|
|
32
|
+
import { ensureFrontendShapeCapsuleRecord } from "./frontend-shape-capsule-store.js";
|
|
29
33
|
import { parseDagSpec, resolveModelForTask, } from "./types.js";
|
|
34
|
+
import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
|
|
30
35
|
import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
|
|
31
36
|
import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
|
|
32
37
|
import { executeDynamicMapExpansion } from "./dynamic-runtime/map.js";
|
|
@@ -46,6 +51,51 @@ export function toKebabSlug(text, maxLen = 40) {
|
|
|
46
51
|
.replace(/^-|-$/g, "")
|
|
47
52
|
.slice(0, maxLen) || "dag");
|
|
48
53
|
}
|
|
54
|
+
export async function loadCanonicalFrontendTaskSourceBinding(cwd, spec) {
|
|
55
|
+
const binding = spec.sourceBinding;
|
|
56
|
+
if (!binding?.taskId)
|
|
57
|
+
throw new Error("canonical frontend DAG requires sourceBinding.taskId");
|
|
58
|
+
const requirement = binding.sources.find((source) => source.kind === "requirement");
|
|
59
|
+
if (!requirement)
|
|
60
|
+
throw new Error("canonical frontend DAG requires a bound requirement source");
|
|
61
|
+
const constraint = binding.sources.find((source) => source.kind === "constraint");
|
|
62
|
+
const readFreshSource = async (source) => {
|
|
63
|
+
let bytes;
|
|
64
|
+
try {
|
|
65
|
+
bytes = await readFile(path.resolve(cwd, source.path));
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`canonical frontend ${source.kind} source freshness read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
69
|
+
}
|
|
70
|
+
const actualSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
71
|
+
if (actualSha256 !== source.sha256) {
|
|
72
|
+
throw new Error(`canonical frontend ${source.kind} source sha256 drifted: expected ${source.sha256}, received ${actualSha256}`);
|
|
73
|
+
}
|
|
74
|
+
return bytes.toString("utf8");
|
|
75
|
+
};
|
|
76
|
+
const requirementMarkdown = await readFreshSource(requirement);
|
|
77
|
+
const constraintMarkdown = constraint ? await readFreshSource(constraint) : "";
|
|
78
|
+
return {
|
|
79
|
+
taskId: binding.taskId,
|
|
80
|
+
requirementMarkdown,
|
|
81
|
+
constraintMarkdown,
|
|
82
|
+
sourceDigest: computeFrontendShapeSourceDigest({ requirementMarkdown, constraintMarkdown }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function ensureFreshFrontendShapeCapsuleRecord(input) {
|
|
86
|
+
const freshSource = await loadCanonicalFrontendTaskSourceBinding(input.cwd, input.spec);
|
|
87
|
+
if (freshSource.taskId !== input.canonicalSource.taskId ||
|
|
88
|
+
freshSource.sourceDigest !== input.canonicalSource.sourceDigest) {
|
|
89
|
+
throw new Error("canonical frontend task source freshness binding drifted before transition commit");
|
|
90
|
+
}
|
|
91
|
+
await (input.ensureRecord ?? ensureFrontendShapeCapsuleRecord)({
|
|
92
|
+
repoRoot: input.cwd,
|
|
93
|
+
taskId: input.canonicalSource.taskId,
|
|
94
|
+
parentRunId: input.parentRunId,
|
|
95
|
+
terminalOutcome: input.terminalOutcome,
|
|
96
|
+
capsule: input.capsule,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
49
99
|
export function extractTaskIdFromDagSpec(spec) {
|
|
50
100
|
if (spec.objective) {
|
|
51
101
|
const match = spec.objective.match(TASK_ID_IN_TEXT_PATTERN);
|
|
@@ -180,6 +230,15 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
180
230
|
...(opts.workerAssociation
|
|
181
231
|
? { workerAssociation: structuredClone(opts.workerAssociation) }
|
|
182
232
|
: {}),
|
|
233
|
+
...(isCanonicalFrontendShapeDag(new Map(spec.tasks.map((task) => [task.id, task])))
|
|
234
|
+
? {
|
|
235
|
+
frontendShapeState: {
|
|
236
|
+
frozenTopology: buildFrontendTopologyBinding(spec.tasks.map((task) => task.id)),
|
|
237
|
+
revision: 0,
|
|
238
|
+
committedFacts: {},
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
: {}),
|
|
183
242
|
};
|
|
184
243
|
initRunBudgetLedger(state, spec.budget);
|
|
185
244
|
return state;
|
|
@@ -301,6 +360,7 @@ export async function runDag(spec, opts) {
|
|
|
301
360
|
activeRunDir,
|
|
302
361
|
completedRunDir,
|
|
303
362
|
pausedRunDir,
|
|
363
|
+
recovery: opts.recovery,
|
|
304
364
|
});
|
|
305
365
|
}
|
|
306
366
|
finally {
|
|
@@ -322,6 +382,16 @@ export async function runDagContinuation(opts) {
|
|
|
322
382
|
await assertTaskContractBindingConsistent({ repoRoot: opts.cwd, spec: opts.spec });
|
|
323
383
|
const { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), opts.spec.budget);
|
|
324
384
|
const state = opts.state;
|
|
385
|
+
const canonicalFrontend = isCanonicalFrontendShapeDag(new Map(opts.spec.tasks.map((task) => [task.id, task])));
|
|
386
|
+
if (canonicalFrontend) {
|
|
387
|
+
assertFrontendTopologyBinding(state.frontendShapeState?.frozenTopology, opts.spec.tasks.map((task) => task.id));
|
|
388
|
+
}
|
|
389
|
+
else if (state.frontendShapeState) {
|
|
390
|
+
throw new Error("non-frontend DAG unexpectedly contains frontend topology state");
|
|
391
|
+
}
|
|
392
|
+
if (state.frontendShapeState?.terminalOutcome) {
|
|
393
|
+
throw new Error(`dag run ${state.runId} is terminal due to frontend shape ${state.frontendShapeState.terminalOutcome}`);
|
|
394
|
+
}
|
|
325
395
|
state.status = "running";
|
|
326
396
|
const resumedAt = new Date().toISOString();
|
|
327
397
|
state.runner = {
|
|
@@ -347,6 +417,7 @@ export async function runDagContinuation(opts) {
|
|
|
347
417
|
activeRunDir,
|
|
348
418
|
completedRunDir,
|
|
349
419
|
pausedRunDir,
|
|
420
|
+
recovery: opts.recovery,
|
|
350
421
|
});
|
|
351
422
|
}
|
|
352
423
|
export async function resumeDagRun(opts) {
|
|
@@ -449,9 +520,13 @@ export async function resumeDagRun(opts) {
|
|
|
449
520
|
}
|
|
450
521
|
async function executeDagCheckpoint(input) {
|
|
451
522
|
const { spec, state, ranks, runDir: initialRunDir, cwd, maxConcurrent, completedRunDir, pausedRunDir, } = input;
|
|
523
|
+
bindFrontendShapeStateToRunTopology(state);
|
|
452
524
|
let runDir = initialRunDir;
|
|
453
525
|
let stateWriteQueue = Promise.resolve();
|
|
526
|
+
let shapeTransitionBarrier;
|
|
454
527
|
const persistState = async (options) => {
|
|
528
|
+
if (shapeTransitionBarrier)
|
|
529
|
+
await shapeTransitionBarrier;
|
|
455
530
|
stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, state, options));
|
|
456
531
|
await stateWriteQueue;
|
|
457
532
|
};
|
|
@@ -576,12 +651,50 @@ async function executeDagCheckpoint(input) {
|
|
|
576
651
|
.map((rank) => rank.filter((nodeId) => nodeId === "closeout-pi"))
|
|
577
652
|
.filter((rank) => rank.length > 0)
|
|
578
653
|
: [];
|
|
654
|
+
const canonicalFrontendSource = isCanonicalFrontendShapeDag(tasksById)
|
|
655
|
+
? await loadCanonicalFrontendTaskSourceBinding(cwd, spec)
|
|
656
|
+
: undefined;
|
|
579
657
|
const executeRanks = (selectedRanks) => executeDagRanksOnce({
|
|
580
658
|
state,
|
|
581
659
|
ranks: selectedRanks,
|
|
582
660
|
tasksById,
|
|
583
661
|
maxConcurrent,
|
|
584
662
|
persistState,
|
|
663
|
+
commitFrontendShapeTransition: async (request) => {
|
|
664
|
+
let releaseBarrier;
|
|
665
|
+
shapeTransitionBarrier = new Promise((resolve) => {
|
|
666
|
+
releaseBarrier = resolve;
|
|
667
|
+
});
|
|
668
|
+
try {
|
|
669
|
+
if (!canonicalFrontendSource) {
|
|
670
|
+
throw new Error("frontend shape transition requires canonical task source binding");
|
|
671
|
+
}
|
|
672
|
+
return await commitFrontendShapeTransitionCas({
|
|
673
|
+
state,
|
|
674
|
+
tasksById,
|
|
675
|
+
request,
|
|
676
|
+
taskSourceDigest: canonicalFrontendSource.sourceDigest,
|
|
677
|
+
ensureRecord: async (capsule, terminalOutcome) => {
|
|
678
|
+
await ensureFreshFrontendShapeCapsuleRecord({
|
|
679
|
+
cwd,
|
|
680
|
+
spec,
|
|
681
|
+
canonicalSource: canonicalFrontendSource,
|
|
682
|
+
parentRunId: state.runId,
|
|
683
|
+
terminalOutcome,
|
|
684
|
+
capsule,
|
|
685
|
+
});
|
|
686
|
+
},
|
|
687
|
+
persist: async (nextState) => {
|
|
688
|
+
stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, nextState));
|
|
689
|
+
await stateWriteQueue;
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
finally {
|
|
694
|
+
releaseBarrier();
|
|
695
|
+
shapeTransitionBarrier = undefined;
|
|
696
|
+
}
|
|
697
|
+
},
|
|
585
698
|
runDir,
|
|
586
699
|
abortSignal: input.abortSignal,
|
|
587
700
|
createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
|
|
@@ -617,6 +730,18 @@ async function executeDagCheckpoint(input) {
|
|
|
617
730
|
});
|
|
618
731
|
const node = state.nodes[nodeId];
|
|
619
732
|
if (node && (node.status === "FINISHED" || node.status === "ERROR")) {
|
|
733
|
+
if (node.status === "FINISHED") {
|
|
734
|
+
if (canonicalFrontendSource) {
|
|
735
|
+
await commitRuntimeFrontendShapeFact({
|
|
736
|
+
state,
|
|
737
|
+
tasksById,
|
|
738
|
+
sourceNodeId: nodeId,
|
|
739
|
+
taskId: canonicalFrontendSource.taskId,
|
|
740
|
+
requirementMarkdown: canonicalFrontendSource.requirementMarkdown,
|
|
741
|
+
constraintMarkdown: canonicalFrontendSource.constraintMarkdown,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
620
745
|
recordFinishedNodeBudget(state, node);
|
|
621
746
|
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
622
747
|
await persistState();
|
|
@@ -626,6 +751,9 @@ async function executeDagCheckpoint(input) {
|
|
|
626
751
|
let pausedByNodeId;
|
|
627
752
|
while (true) {
|
|
628
753
|
pausedByNodeId = await executeRanks(convergenceRanks);
|
|
754
|
+
if (state.frontendShapeState?.terminalOutcome) {
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
629
757
|
if (pausedByNodeId) {
|
|
630
758
|
break;
|
|
631
759
|
}
|
|
@@ -681,7 +809,7 @@ async function executeDagCheckpoint(input) {
|
|
|
681
809
|
runDir = await moveToPausedRunDir(runDir, pausedRunDir);
|
|
682
810
|
}
|
|
683
811
|
else {
|
|
684
|
-
const { recoveryPending } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
|
|
812
|
+
const { recoveryPending, recovery: recoveryTrigger } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd, spec.frontendRecovery?.maxContinuations ?? 1);
|
|
685
813
|
await finalizeRunOwnedInterrupt({
|
|
686
814
|
runDir,
|
|
687
815
|
state,
|
|
@@ -689,56 +817,69 @@ async function executeDagCheckpoint(input) {
|
|
|
689
817
|
interruptAborted: interruptController.signal.aborted,
|
|
690
818
|
});
|
|
691
819
|
await persistState();
|
|
692
|
-
if (recoveryPending) {
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
820
|
+
if (recoveryPending && input.recovery && recoveryTrigger) {
|
|
821
|
+
// M6 recovery operation controller path: construct/reuse one recovery
|
|
822
|
+
// operation keyed by a deterministic clientRequestId and supervise it
|
|
823
|
+
// to terminal, instead of staging an in-process child run.
|
|
824
|
+
const created = await createRecoveryOperation({
|
|
825
|
+
cwd,
|
|
826
|
+
runDir,
|
|
827
|
+
spec,
|
|
828
|
+
state,
|
|
829
|
+
trigger: recoveryTrigger,
|
|
830
|
+
deps: input.recovery,
|
|
831
|
+
});
|
|
832
|
+
if (created.kind === "conflict") {
|
|
833
|
+
// Same clientRequestId with a different payload → fail closed; never
|
|
834
|
+
// create a second recovery operation (AC-004).
|
|
835
|
+
state.status = "failed";
|
|
836
|
+
state.terminalReason =
|
|
837
|
+
"frontend recovery request-id reuse conflict";
|
|
838
|
+
state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, "auto-recovery-blocked");
|
|
839
|
+
}
|
|
840
|
+
else if (created.kind === "lease-held") {
|
|
841
|
+
// A different recovery entrance has already durably claimed this
|
|
842
|
+
// parent. Do not "help" by running its operation: that would reopen
|
|
843
|
+
// the exact two-child-DAG race the lease prevents.
|
|
844
|
+
state.status = "failed";
|
|
845
|
+
state.terminalReason =
|
|
846
|
+
"frontend recovery parent lease already held";
|
|
847
|
+
state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, "auto-recovery-blocked");
|
|
848
|
+
}
|
|
849
|
+
else {
|
|
850
|
+
const { runRecoveryOperationController } = await import("./frontend-recovery-controller.js");
|
|
851
|
+
const result = await runRecoveryOperationController({
|
|
852
|
+
operationId: created.operation.operationId,
|
|
705
853
|
parentRunId: state.runId,
|
|
854
|
+
deps: {
|
|
855
|
+
...input.recovery,
|
|
856
|
+
failureOwner: input.recovery.failureOwner ??
|
|
857
|
+
recoveryTrigger.failureOwner,
|
|
858
|
+
protocolFailureReason: input.recovery.protocolFailureReason ??
|
|
859
|
+
recoveryTrigger.protocolFailureReason,
|
|
860
|
+
recoveryMode: input.recovery.recoveryMode ?? recoveryTrigger.recoveryMode,
|
|
861
|
+
},
|
|
706
862
|
});
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
// back to stale child-staging.
|
|
711
|
-
const parentOnDisk = await readDagRunState(runDir);
|
|
712
|
-
state.frontendRecoveryState = parentOnDisk.frontendRecoveryState;
|
|
713
|
-
const childState = await readDagRunState(staged.childRunDir);
|
|
714
|
-
const childSpec = await readDagRunSpec(staged.childRunDir);
|
|
715
|
-
await runDagContinuation({
|
|
716
|
-
cwd,
|
|
717
|
-
spec: childSpec,
|
|
718
|
-
state: childState,
|
|
719
|
-
runDir: staged.childRunDir,
|
|
720
|
-
maxConcurrent,
|
|
721
|
-
executeNode: input.executeNode,
|
|
722
|
-
observer: input.observer,
|
|
723
|
-
abortSignal,
|
|
724
|
-
});
|
|
863
|
+
state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, result.terminalReason === "succeeded"
|
|
864
|
+
? "recovered"
|
|
865
|
+
: "auto-recovery-blocked");
|
|
725
866
|
}
|
|
726
867
|
}
|
|
727
868
|
await notifyRunObserver(input.observer, "onRunFinish", state);
|
|
728
|
-
if (
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
catch (error) {
|
|
738
|
-
console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
|
|
739
|
-
}
|
|
740
|
-
runDir = await moveToCompletedRunDir(runDir, completedRunDir);
|
|
869
|
+
if (input.beforeTerminalArchive) {
|
|
870
|
+
await input.beforeTerminalArchive({ runDir, state });
|
|
871
|
+
await persistState();
|
|
872
|
+
}
|
|
873
|
+
// Parent is always archived to completed/ — no active/ retention for an
|
|
874
|
+
// in-process child CAS anymore (recovery runs as a separate operation).
|
|
875
|
+
try {
|
|
876
|
+
const terminalCheckpoint = await captureWorkspaceCheckpoint(cwd);
|
|
877
|
+
await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL, terminalCheckpoint);
|
|
741
878
|
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
|
|
881
|
+
}
|
|
882
|
+
runDir = await moveToCompletedRunDir(runDir, completedRunDir);
|
|
742
883
|
}
|
|
743
884
|
await relocateRunArtifactPaths({
|
|
744
885
|
runDir,
|
|
@@ -871,39 +1012,21 @@ function isSuccessfulConvergenceTerminal(state) {
|
|
|
871
1012
|
return true;
|
|
872
1013
|
return SUCCESSFUL_CONVERGENCE_TERMINAL_REASONS.has(reason);
|
|
873
1014
|
}
|
|
874
|
-
function buildFrontendRecoveryIntent(state, failureSource) {
|
|
875
|
-
const existing = state.frontendRecoveryState;
|
|
876
|
-
const requestId = existing?.requestId ?? randomUUID();
|
|
877
|
-
return {
|
|
878
|
-
schemaVersion: 1,
|
|
879
|
-
phase: "child-staging",
|
|
880
|
-
requestId,
|
|
881
|
-
recoveryRootRunId: existing?.recoveryRootRunId ?? state.runId,
|
|
882
|
-
parentRunId: state.runId,
|
|
883
|
-
...(existing?.childRunId ? { childRunId: existing.childRunId } : {}),
|
|
884
|
-
attemptId: existing?.attemptId ?? `recovery-${requestId}`,
|
|
885
|
-
attemptIndex: existing?.attemptIndex ?? 0,
|
|
886
|
-
continuationCount: existing?.continuationCount ?? 0,
|
|
887
|
-
...(failureSource ? { failureSource } : {}),
|
|
888
|
-
revision: (existing?.revision ?? 0) + 1,
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
1015
|
function buildCandidateContractInvalidResult(state, result, artifactHash) {
|
|
892
|
-
const recovery = state.frontendRecoveryState;
|
|
893
1016
|
return {
|
|
894
1017
|
schemaVersion: 1,
|
|
895
1018
|
outcome: "candidate-contract-invalid",
|
|
896
1019
|
origin: {
|
|
897
1020
|
kind: "frontend-prewrite-gate",
|
|
898
|
-
parentRunId:
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
failedNodeId: result.selectedPlanNodeId,
|
|
1021
|
+
parentRunId: state.runId,
|
|
1022
|
+
requestId: state.runId,
|
|
1023
|
+
failedNodeId: result.failureSource ?? "frontend-plan-pi",
|
|
902
1024
|
},
|
|
903
1025
|
failureClass: {
|
|
904
1026
|
code: "candidate-contract-invalid",
|
|
905
1027
|
classification: result.classification,
|
|
906
|
-
reason: result.
|
|
1028
|
+
reason: result.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ") ??
|
|
1029
|
+
"candidate contract invalid",
|
|
907
1030
|
},
|
|
908
1031
|
evidenceRefs: [
|
|
909
1032
|
{
|
|
@@ -915,15 +1038,13 @@ function buildCandidateContractInvalidResult(state, result, artifactHash) {
|
|
|
915
1038
|
};
|
|
916
1039
|
}
|
|
917
1040
|
function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
|
|
918
|
-
const recovery = state.frontendRecoveryState;
|
|
919
1041
|
return {
|
|
920
1042
|
schemaVersion: 1,
|
|
921
1043
|
outcome: "auto-recovery-blocked",
|
|
922
1044
|
origin: {
|
|
923
1045
|
kind: "frontend-writer",
|
|
924
|
-
parentRunId:
|
|
925
|
-
|
|
926
|
-
requestId: recovery?.requestId ?? "",
|
|
1046
|
+
parentRunId: state.runId,
|
|
1047
|
+
requestId: state.runId,
|
|
927
1048
|
failedNodeId,
|
|
928
1049
|
},
|
|
929
1050
|
failureClass: {
|
|
@@ -934,64 +1055,116 @@ function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
|
|
|
934
1055
|
evidenceRefs: [],
|
|
935
1056
|
};
|
|
936
1057
|
}
|
|
937
|
-
function
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
failedNodeId: "",
|
|
952
|
-
},
|
|
953
|
-
evidenceRefs: [],
|
|
954
|
-
};
|
|
955
|
-
}
|
|
1058
|
+
function buildRecoveryResultForTrigger(state, trigger, outcome) {
|
|
1059
|
+
return {
|
|
1060
|
+
schemaVersion: 1,
|
|
1061
|
+
outcome,
|
|
1062
|
+
origin: {
|
|
1063
|
+
kind: trigger.failureSource === "frontend-implement-pi"
|
|
1064
|
+
? "frontend-writer"
|
|
1065
|
+
: "frontend-prewrite-gate",
|
|
1066
|
+
parentRunId: state.runId,
|
|
1067
|
+
requestId: trigger.requestId,
|
|
1068
|
+
failedNodeId: trigger.failureSource,
|
|
1069
|
+
},
|
|
1070
|
+
evidenceRefs: [],
|
|
1071
|
+
};
|
|
956
1072
|
}
|
|
957
|
-
export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
1073
|
+
export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, frontendRecoveryQuota = 1) {
|
|
958
1074
|
const repoRoot = cwd ?? path.resolve(runDir, "..", "..", "..", "..");
|
|
1075
|
+
if (state.frontendShapeState?.terminalOutcome &&
|
|
1076
|
+
state.frontendShapeState.transitionCapsule) {
|
|
1077
|
+
state.status = "failed";
|
|
1078
|
+
return { recoveryPending: false };
|
|
1079
|
+
}
|
|
959
1080
|
// Terminal override: a denied frontend writer must never surface as a
|
|
960
1081
|
// mechanical partial_failed just because prewrite FINISHED while the writer
|
|
961
|
-
// was SKIPPED. Fail closed on
|
|
962
|
-
let
|
|
1082
|
+
// was SKIPPED. Fail closed on blocked before aggregation.
|
|
1083
|
+
let recoveryTrigger;
|
|
963
1084
|
const skipFrontendRecovery = state.failureCategory === "controller-interrupted" ||
|
|
964
1085
|
state.continuation?.writerTransaction !== undefined;
|
|
965
1086
|
const hasFrontendWriter = Object.keys(state.nodes).some((id) => FRONTEND_WRITER_NODE_IDS.includes(id));
|
|
966
1087
|
if (hasFrontendWriter && !skipFrontendRecovery) {
|
|
967
1088
|
const admission = await readFrontendPrewriteResult(runDir);
|
|
968
1089
|
if (admission.ok) {
|
|
969
|
-
|
|
1090
|
+
// M8: admission has only admitted|blocked. A blocked admission whose
|
|
1091
|
+
// failureSource is frontend-plan-pi is the contract-invalid analog of
|
|
1092
|
+
// the old retryable-invalid path (candidate continuation may re-plan);
|
|
1093
|
+
// every other blocked admission is terminal.
|
|
1094
|
+
const planContractInvalid = admission.result.classification === "blocked" &&
|
|
1095
|
+
admission.result.failureSource === "frontend-plan-pi";
|
|
1096
|
+
// A blocked admission caused by the design review committing
|
|
1097
|
+
// `request_design_changes` is a definite finding (not a protocol or
|
|
1098
|
+
// environment error): the design was reviewed and rejected. It is the
|
|
1099
|
+
// design-review analog of review-request-changes — not terminal, but
|
|
1100
|
+
// re-enters the next plan round with the committed design findings.
|
|
1101
|
+
// The failureSource may be frontend-writer-admission-shell or
|
|
1102
|
+
// frontend-design-policy-shell; the committed design fact is the
|
|
1103
|
+
// authoritative signal.
|
|
1104
|
+
const designRequestFact = await readCommittedDesignRequestFact({
|
|
1105
|
+
runDir,
|
|
1106
|
+
});
|
|
1107
|
+
const designRejected = admission.result.classification === "blocked" &&
|
|
1108
|
+
designRequestFact !== undefined;
|
|
1109
|
+
const terminalAdmissionDenial = (admission.result.classification === "blocked" ||
|
|
1110
|
+
admission.result.classification === "stale") &&
|
|
1111
|
+
!planContractInvalid &&
|
|
1112
|
+
!designRejected;
|
|
1113
|
+
if (terminalAdmissionDenial) {
|
|
970
1114
|
state.status = "failed";
|
|
971
1115
|
return { recoveryPending: false };
|
|
972
1116
|
}
|
|
973
|
-
if (
|
|
1117
|
+
if (designRejected && designRequestFact) {
|
|
1118
|
+
state.terminalReason = "design-request-changes";
|
|
1119
|
+
try {
|
|
1120
|
+
await writeFrontendReviewFindingsPrd({
|
|
1121
|
+
runDir,
|
|
1122
|
+
parentRunId: state.runId,
|
|
1123
|
+
fact: designRequestFact,
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
catch (error) {
|
|
1127
|
+
console.warn(`[run-dag] warning: failed to write frontend design findings PRD: ${error instanceof Error ? error.message : String(error)}`);
|
|
1128
|
+
}
|
|
1129
|
+
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1130
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
1131
|
+
recoveryTrigger = {
|
|
1132
|
+
requestId: state.runId,
|
|
1133
|
+
failureSource: "frontend-design-review-pi",
|
|
1134
|
+
failureOwner: classifyReviewIssueCategoryFailureOwner(designRequestFact.issueCategory),
|
|
1135
|
+
protocolFailureReason: `frontend design review request_design_changes (issueCategory=${designRequestFact.issueCategory})`,
|
|
1136
|
+
recoveryMode: "new-dag",
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (planContractInvalid) {
|
|
974
1141
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
975
|
-
if (continuationCount >=
|
|
976
|
-
// Continuation quota exhausted: a second
|
|
1142
|
+
if (continuationCount >= frontendRecoveryQuota) {
|
|
1143
|
+
// Continuation quota exhausted: a second contract-invalid candidate
|
|
977
1144
|
// is authoritative candidate-contract-invalid → forced failed.
|
|
978
1145
|
state.frontendRecoveryResult = buildCandidateContractInvalidResult(state, admission.result, admission.artifactHash);
|
|
979
1146
|
state.status = "failed";
|
|
980
1147
|
return { recoveryPending: false };
|
|
981
1148
|
}
|
|
982
|
-
// Root continuation still available:
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
|
|
986
|
-
|
|
1149
|
+
// Root continuation still available: emit a recovery trigger for the
|
|
1150
|
+
// M6 operation controller. The parent no longer writes
|
|
1151
|
+
// frontendRecoveryState or stays in active/ for a child CAS.
|
|
1152
|
+
recoveryTrigger = {
|
|
1153
|
+
requestId: state.runId,
|
|
1154
|
+
failureSource: admission.result.failureSource ?? "frontend-plan-pi",
|
|
1155
|
+
failureOwner: "plan",
|
|
1156
|
+
protocolFailureReason: "frontend candidate contract invalid",
|
|
1157
|
+
recoveryMode: "new-dag",
|
|
1158
|
+
};
|
|
987
1159
|
}
|
|
988
1160
|
}
|
|
989
1161
|
}
|
|
990
|
-
// Phase 5: writer transient partial write → rollback + recovery
|
|
991
|
-
// for frontend-implementation writers with a remaining root
|
|
992
|
-
// only when the rollback journal (captured before the
|
|
993
|
-
// cleanly; otherwise the run stays failed with
|
|
994
|
-
|
|
1162
|
+
// Phase 5: writer transient partial write → rollback + recovery trigger.
|
|
1163
|
+
// Only for frontend-implementation writers with a remaining root
|
|
1164
|
+
// continuation, and only when the rollback journal (captured before the
|
|
1165
|
+
// provider call) restores cleanly; otherwise the run stays failed with
|
|
1166
|
+
// auto-recovery-blocked.
|
|
1167
|
+
if (hasFrontendWriter && !recoveryTrigger && !skipFrontendRecovery) {
|
|
995
1168
|
const writerNodeId = FRONTEND_WRITER_NODE_IDS.find((id) => {
|
|
996
1169
|
const node = state.nodes[id];
|
|
997
1170
|
return (node &&
|
|
@@ -1002,15 +1175,20 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1002
1175
|
});
|
|
1003
1176
|
if (writerNodeId) {
|
|
1004
1177
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1005
|
-
if (continuationCount <
|
|
1178
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
1006
1179
|
const { rollbackFrontendWriter } = await import("./frontend-writer-recovery.js");
|
|
1007
1180
|
const rollback = await rollbackFrontendWriter({
|
|
1008
1181
|
cwd: repoRoot,
|
|
1009
1182
|
parentRunId: state.runId,
|
|
1010
1183
|
});
|
|
1011
1184
|
if (rollback.ok) {
|
|
1012
|
-
|
|
1013
|
-
|
|
1185
|
+
recoveryTrigger = {
|
|
1186
|
+
requestId: state.runId,
|
|
1187
|
+
failureSource: "frontend-implement-pi",
|
|
1188
|
+
failureOwner: "implementation",
|
|
1189
|
+
protocolFailureReason: "frontend writer transient partial write",
|
|
1190
|
+
recoveryMode: "continuation",
|
|
1191
|
+
};
|
|
1014
1192
|
}
|
|
1015
1193
|
else {
|
|
1016
1194
|
state.frontendRecoveryResult = buildWriterPartialWriteBlockedResult(state, writerNodeId, rollback.reason);
|
|
@@ -1020,6 +1198,64 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1020
1198
|
}
|
|
1021
1199
|
}
|
|
1022
1200
|
}
|
|
1201
|
+
// Phase 6: verification failure → automatic recovery (re-implement +
|
|
1202
|
+
// re-verify). The verify shell is terminal by design so the failed run's
|
|
1203
|
+
// evidence stays clean; the M6 recovery operation re-runs implement and
|
|
1204
|
+
// verify in a fresh continuation, guarded by the same continuation quota,
|
|
1205
|
+
// budget fold and failure fingerprint as the other recovery paths.
|
|
1206
|
+
if (hasFrontendWriter && !recoveryTrigger && !skipFrontendRecovery) {
|
|
1207
|
+
const verifyFailed = state.nodes["frontend-verify-shell"]?.status === "ERROR";
|
|
1208
|
+
if (verifyFailed) {
|
|
1209
|
+
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1210
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
1211
|
+
recoveryTrigger = {
|
|
1212
|
+
requestId: state.runId,
|
|
1213
|
+
failureSource: "frontend-verify-shell",
|
|
1214
|
+
failureOwner: "implementation",
|
|
1215
|
+
protocolFailureReason: "frontend verification failure",
|
|
1216
|
+
recoveryMode: "continuation",
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
// Phase 7: frontend review "request changes" → not passed. When the review
|
|
1222
|
+
// node FINISHED but committed `request_review_changes` (typed terminal
|
|
1223
|
+
// verdict), the implementation was reviewed and rejected. The run must not
|
|
1224
|
+
// surface as finished: terminal aggregation below stays partial_failed, a
|
|
1225
|
+
// terminal reason is recorded, the committed findings are rendered into a
|
|
1226
|
+
// downstream-task PRD draft (run-owned), and a recovery trigger carries the
|
|
1227
|
+
// issueCategory-derived owner so a later DAG round consumes the findings
|
|
1228
|
+
// instead of repeating the previous flow.
|
|
1229
|
+
const reviewRequestFact = await readCommittedReviewRequestFact({
|
|
1230
|
+
runDir,
|
|
1231
|
+
});
|
|
1232
|
+
if (reviewRequestFact) {
|
|
1233
|
+
state.terminalReason = "review-request-changes";
|
|
1234
|
+
try {
|
|
1235
|
+
await writeFrontendReviewFindingsPrd({
|
|
1236
|
+
runDir,
|
|
1237
|
+
parentRunId: state.runId,
|
|
1238
|
+
fact: reviewRequestFact,
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
catch (error) {
|
|
1242
|
+
// The PRD draft is additive; a write failure must not mask the
|
|
1243
|
+
// not-passed verdict. The terminal aggregation below still applies.
|
|
1244
|
+
console.warn(`[run-dag] warning: failed to write frontend review findings PRD: ${error instanceof Error ? error.message : String(error)}`);
|
|
1245
|
+
}
|
|
1246
|
+
if (!recoveryTrigger && !skipFrontendRecovery) {
|
|
1247
|
+
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1248
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
1249
|
+
recoveryTrigger = {
|
|
1250
|
+
requestId: state.runId,
|
|
1251
|
+
failureSource: "frontend-review-pi",
|
|
1252
|
+
failureOwner: classifyReviewIssueCategoryFailureOwner(reviewRequestFact.issueCategory),
|
|
1253
|
+
protocolFailureReason: `frontend review request_review_changes (issueCategory=${reviewRequestFact.issueCategory})`,
|
|
1254
|
+
recoveryMode: "new-dag",
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1023
1259
|
const finishedCount = Object.values(state.nodes).filter((n) => n.status === "FINISHED").length;
|
|
1024
1260
|
const { supersededIds, supersededFailures } = collectSupersededIntermediateFailures(state);
|
|
1025
1261
|
if (state.convergence && supersededFailures.length > 0) {
|
|
@@ -1035,7 +1271,13 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1035
1271
|
if (effectiveErrorCount === 0 &&
|
|
1036
1272
|
successTerminal &&
|
|
1037
1273
|
finishedCount + conditionSkippedCount + supersededErrorCount === taskCount) {
|
|
1038
|
-
|
|
1274
|
+
// Phase 7 guard: a committed `request_review_changes` means the review
|
|
1275
|
+
// rejected the implementation; the run must never surface as finished
|
|
1276
|
+
// even though every node technically FINISHED.
|
|
1277
|
+
state.status =
|
|
1278
|
+
state.terminalReason === "review-request-changes"
|
|
1279
|
+
? "partial_failed"
|
|
1280
|
+
: "finished";
|
|
1039
1281
|
}
|
|
1040
1282
|
else if (finishedCount > 0) {
|
|
1041
1283
|
state.status = "partial_failed";
|
|
@@ -1043,19 +1285,84 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1043
1285
|
else {
|
|
1044
1286
|
state.status = "failed";
|
|
1045
1287
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1288
|
+
return recoveryTrigger
|
|
1289
|
+
? { recoveryPending: true, recovery: recoveryTrigger }
|
|
1290
|
+
: { recoveryPending: false };
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Construct (or reuse) the recovery operation for a failed frontend run.
|
|
1294
|
+
*
|
|
1295
|
+
* A parent-run atomic lease is acquired before `OperationStore.create`, then
|
|
1296
|
+
* its operation id is persisted into that lease before the controller may run.
|
|
1297
|
+
* `clientRequestId` remains a useful store-level replay key, but is not a
|
|
1298
|
+
* cross-process recovery admission boundary. The continuation DagSpec/reset
|
|
1299
|
+
* partition is frozen via `computeFrontendRecoveryPlanForSource` and
|
|
1300
|
+
* serialized under the run dir's bounded `.runtime/` namespace. `planHash` is
|
|
1301
|
+
* the second idempotency dimension of `actionParams`.
|
|
1302
|
+
*/
|
|
1303
|
+
async function createRecoveryOperation(input) {
|
|
1304
|
+
const { cwd, runDir, spec, state, trigger } = input;
|
|
1305
|
+
const recoveryLease = await acquireDagRecoveryLease({
|
|
1306
|
+
cwd,
|
|
1307
|
+
parentRunId: state.runId,
|
|
1308
|
+
requestId: trigger.requestId,
|
|
1309
|
+
kind: "auto-frontend-recovery",
|
|
1310
|
+
});
|
|
1311
|
+
if (recoveryLease.kind === "held") {
|
|
1312
|
+
return { kind: "lease-held", lease: recoveryLease.lease };
|
|
1313
|
+
}
|
|
1314
|
+
const { computeFrontendRecoveryPlanForSource } = await import("./frontend-recovery-plan.js");
|
|
1315
|
+
const plan = computeFrontendRecoveryPlanForSource({
|
|
1316
|
+
spec,
|
|
1317
|
+
parentRunId: state.runId,
|
|
1318
|
+
requestId: trigger.requestId,
|
|
1319
|
+
failureSource: trigger.failureSource,
|
|
1320
|
+
});
|
|
1321
|
+
const planHash = createHash("sha256")
|
|
1322
|
+
.update(JSON.stringify(plan))
|
|
1323
|
+
.digest("hex");
|
|
1324
|
+
const continuationPath = path.join(runDir, ".runtime", "frontend-recovery", `${trigger.requestId}.json`);
|
|
1325
|
+
await writeJsonAtomic(continuationPath, {
|
|
1326
|
+
schemaVersion: 1,
|
|
1327
|
+
parentRunId: state.runId,
|
|
1328
|
+
requestId: trigger.requestId,
|
|
1329
|
+
failureSource: trigger.failureSource,
|
|
1330
|
+
recoveryMode: trigger.recoveryMode,
|
|
1331
|
+
plan,
|
|
1332
|
+
});
|
|
1333
|
+
const created = await input.deps.store.create({
|
|
1334
|
+
clientRequestId: `dag-recovery-${state.runId}`,
|
|
1335
|
+
action: "dagRunTask",
|
|
1336
|
+
actionParams: {
|
|
1337
|
+
parentRunId: state.runId,
|
|
1338
|
+
requestId: trigger.requestId,
|
|
1339
|
+
failureSource: trigger.failureSource,
|
|
1340
|
+
failureOwner: trigger.failureOwner,
|
|
1341
|
+
protocolFailureReason: trigger.protocolFailureReason,
|
|
1342
|
+
recoveryMode: trigger.recoveryMode,
|
|
1343
|
+
planHash,
|
|
1344
|
+
},
|
|
1345
|
+
cliArgs: [
|
|
1346
|
+
"dag",
|
|
1347
|
+
"execute",
|
|
1348
|
+
"--dag",
|
|
1349
|
+
continuationPath,
|
|
1350
|
+
"--cwd",
|
|
1351
|
+
cwd,
|
|
1352
|
+
"--json",
|
|
1353
|
+
],
|
|
1354
|
+
});
|
|
1355
|
+
if (created.kind !== "conflict") {
|
|
1356
|
+
// The durable lease must name the operation before controller execution.
|
|
1357
|
+
// A crash in this narrow window leaves a safely blocked reservation rather
|
|
1358
|
+
// than a second process that can launch another recovery DAG.
|
|
1359
|
+
await bindDagRecoveryLeaseExecution({
|
|
1360
|
+
leasePath: recoveryLease.leasePath,
|
|
1361
|
+
lease: recoveryLease.lease,
|
|
1362
|
+
executionRef: created.operation.operationId,
|
|
1363
|
+
});
|
|
1057
1364
|
}
|
|
1058
|
-
return
|
|
1365
|
+
return created;
|
|
1059
1366
|
}
|
|
1060
1367
|
function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
|
|
1061
1368
|
if (rankWriterNodeIds.length <= 1)
|