@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.4
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/AGENTS.md +4 -1
- package/CHANGELOG.md +187 -92
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +429 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +1691 -29
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1152 -171
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +19 -5
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +60 -17
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +36 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- 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/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +263 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +151 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.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/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +366 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
- package/dist/workflows/dag/frontend-prewrite-gate.js +343 -391
- 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 +201 -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 +7 -424
- package/dist/workflows/dag/frontend-review-context.js +195 -8
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +425 -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-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +439 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
- package/dist/workflows/dag/frontend-writer-admission.js +260 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +572 -500
- package/dist/workflows/dag/node-execution.js +307 -70
- package/dist/workflows/dag/prompt.js +10 -18
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +228 -4
- package/dist/workflows/dag/runner.js +314 -119
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +235 -23
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +60 -10
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -3
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/SKILL.md +4 -4
- package/skills/frontend-implementation/references/design-spec.md +11 -7
- package/skills/frontend-implementation/references/node-contracts.md +20 -11
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -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,6 +26,9 @@ 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 { bindFrontendShapeStateToRunTopology, commitFrontendShapeTransitionCas, commitRuntimeFrontendShapeFact, isCanonicalFrontendShapeDag, } from "./frontend-shape-facts.js";
|
|
30
|
+
import { assertFrontendTopologyBinding, buildFrontendTopologyBinding, computeFrontendShapeSourceDigest, } from "./frontend-shape.js";
|
|
31
|
+
import { ensureFrontendShapeCapsuleRecord } from "./frontend-shape-capsule-store.js";
|
|
29
32
|
import { parseDagSpec, resolveModelForTask, } from "./types.js";
|
|
30
33
|
import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
|
|
31
34
|
import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
|
|
@@ -46,6 +49,51 @@ export function toKebabSlug(text, maxLen = 40) {
|
|
|
46
49
|
.replace(/^-|-$/g, "")
|
|
47
50
|
.slice(0, maxLen) || "dag");
|
|
48
51
|
}
|
|
52
|
+
export async function loadCanonicalFrontendTaskSourceBinding(cwd, spec) {
|
|
53
|
+
const binding = spec.sourceBinding;
|
|
54
|
+
if (!binding?.taskId)
|
|
55
|
+
throw new Error("canonical frontend DAG requires sourceBinding.taskId");
|
|
56
|
+
const requirement = binding.sources.find((source) => source.kind === "requirement");
|
|
57
|
+
if (!requirement)
|
|
58
|
+
throw new Error("canonical frontend DAG requires a bound requirement source");
|
|
59
|
+
const constraint = binding.sources.find((source) => source.kind === "constraint");
|
|
60
|
+
const readFreshSource = async (source) => {
|
|
61
|
+
let bytes;
|
|
62
|
+
try {
|
|
63
|
+
bytes = await readFile(path.resolve(cwd, source.path));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new Error(`canonical frontend ${source.kind} source freshness read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
|
+
}
|
|
68
|
+
const actualSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
69
|
+
if (actualSha256 !== source.sha256) {
|
|
70
|
+
throw new Error(`canonical frontend ${source.kind} source sha256 drifted: expected ${source.sha256}, received ${actualSha256}`);
|
|
71
|
+
}
|
|
72
|
+
return bytes.toString("utf8");
|
|
73
|
+
};
|
|
74
|
+
const requirementMarkdown = await readFreshSource(requirement);
|
|
75
|
+
const constraintMarkdown = constraint ? await readFreshSource(constraint) : "";
|
|
76
|
+
return {
|
|
77
|
+
taskId: binding.taskId,
|
|
78
|
+
requirementMarkdown,
|
|
79
|
+
constraintMarkdown,
|
|
80
|
+
sourceDigest: computeFrontendShapeSourceDigest({ requirementMarkdown, constraintMarkdown }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export async function ensureFreshFrontendShapeCapsuleRecord(input) {
|
|
84
|
+
const freshSource = await loadCanonicalFrontendTaskSourceBinding(input.cwd, input.spec);
|
|
85
|
+
if (freshSource.taskId !== input.canonicalSource.taskId ||
|
|
86
|
+
freshSource.sourceDigest !== input.canonicalSource.sourceDigest) {
|
|
87
|
+
throw new Error("canonical frontend task source freshness binding drifted before transition commit");
|
|
88
|
+
}
|
|
89
|
+
await (input.ensureRecord ?? ensureFrontendShapeCapsuleRecord)({
|
|
90
|
+
repoRoot: input.cwd,
|
|
91
|
+
taskId: input.canonicalSource.taskId,
|
|
92
|
+
parentRunId: input.parentRunId,
|
|
93
|
+
terminalOutcome: input.terminalOutcome,
|
|
94
|
+
capsule: input.capsule,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
49
97
|
export function extractTaskIdFromDagSpec(spec) {
|
|
50
98
|
if (spec.objective) {
|
|
51
99
|
const match = spec.objective.match(TASK_ID_IN_TEXT_PATTERN);
|
|
@@ -180,6 +228,15 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
180
228
|
...(opts.workerAssociation
|
|
181
229
|
? { workerAssociation: structuredClone(opts.workerAssociation) }
|
|
182
230
|
: {}),
|
|
231
|
+
...(isCanonicalFrontendShapeDag(new Map(spec.tasks.map((task) => [task.id, task])))
|
|
232
|
+
? {
|
|
233
|
+
frontendShapeState: {
|
|
234
|
+
frozenTopology: buildFrontendTopologyBinding(spec.tasks.map((task) => task.id)),
|
|
235
|
+
revision: 0,
|
|
236
|
+
committedFacts: {},
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
: {}),
|
|
183
240
|
};
|
|
184
241
|
initRunBudgetLedger(state, spec.budget);
|
|
185
242
|
return state;
|
|
@@ -301,6 +358,7 @@ export async function runDag(spec, opts) {
|
|
|
301
358
|
activeRunDir,
|
|
302
359
|
completedRunDir,
|
|
303
360
|
pausedRunDir,
|
|
361
|
+
recovery: opts.recovery,
|
|
304
362
|
});
|
|
305
363
|
}
|
|
306
364
|
finally {
|
|
@@ -309,6 +367,16 @@ export async function runDag(spec, opts) {
|
|
|
309
367
|
export async function runDagContinuation(opts) {
|
|
310
368
|
const { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), opts.spec.budget);
|
|
311
369
|
const state = opts.state;
|
|
370
|
+
const canonicalFrontend = isCanonicalFrontendShapeDag(new Map(opts.spec.tasks.map((task) => [task.id, task])));
|
|
371
|
+
if (canonicalFrontend) {
|
|
372
|
+
assertFrontendTopologyBinding(state.frontendShapeState?.frozenTopology, opts.spec.tasks.map((task) => task.id));
|
|
373
|
+
}
|
|
374
|
+
else if (state.frontendShapeState) {
|
|
375
|
+
throw new Error("non-frontend DAG unexpectedly contains frontend topology state");
|
|
376
|
+
}
|
|
377
|
+
if (state.frontendShapeState?.terminalOutcome) {
|
|
378
|
+
throw new Error(`dag run ${state.runId} is terminal due to frontend shape ${state.frontendShapeState.terminalOutcome}`);
|
|
379
|
+
}
|
|
312
380
|
state.status = "running";
|
|
313
381
|
const resumedAt = new Date().toISOString();
|
|
314
382
|
state.runner = {
|
|
@@ -333,6 +401,7 @@ export async function runDagContinuation(opts) {
|
|
|
333
401
|
activeRunDir,
|
|
334
402
|
completedRunDir,
|
|
335
403
|
pausedRunDir,
|
|
404
|
+
recovery: opts.recovery,
|
|
336
405
|
});
|
|
337
406
|
}
|
|
338
407
|
export async function resumeDagRun(opts) {
|
|
@@ -435,9 +504,13 @@ export async function resumeDagRun(opts) {
|
|
|
435
504
|
}
|
|
436
505
|
async function executeDagCheckpoint(input) {
|
|
437
506
|
const { spec, state, ranks, runDir: initialRunDir, cwd, maxConcurrent, completedRunDir, pausedRunDir, } = input;
|
|
507
|
+
bindFrontendShapeStateToRunTopology(state);
|
|
438
508
|
let runDir = initialRunDir;
|
|
439
509
|
let stateWriteQueue = Promise.resolve();
|
|
510
|
+
let shapeTransitionBarrier;
|
|
440
511
|
const persistState = async (options) => {
|
|
512
|
+
if (shapeTransitionBarrier)
|
|
513
|
+
await shapeTransitionBarrier;
|
|
441
514
|
stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, state, options));
|
|
442
515
|
await stateWriteQueue;
|
|
443
516
|
};
|
|
@@ -562,12 +635,50 @@ async function executeDagCheckpoint(input) {
|
|
|
562
635
|
.map((rank) => rank.filter((nodeId) => nodeId === "closeout-pi"))
|
|
563
636
|
.filter((rank) => rank.length > 0)
|
|
564
637
|
: [];
|
|
638
|
+
const canonicalFrontendSource = isCanonicalFrontendShapeDag(tasksById)
|
|
639
|
+
? await loadCanonicalFrontendTaskSourceBinding(cwd, spec)
|
|
640
|
+
: undefined;
|
|
565
641
|
const executeRanks = (selectedRanks) => executeDagRanksOnce({
|
|
566
642
|
state,
|
|
567
643
|
ranks: selectedRanks,
|
|
568
644
|
tasksById,
|
|
569
645
|
maxConcurrent,
|
|
570
646
|
persistState,
|
|
647
|
+
commitFrontendShapeTransition: async (request) => {
|
|
648
|
+
let releaseBarrier;
|
|
649
|
+
shapeTransitionBarrier = new Promise((resolve) => {
|
|
650
|
+
releaseBarrier = resolve;
|
|
651
|
+
});
|
|
652
|
+
try {
|
|
653
|
+
if (!canonicalFrontendSource) {
|
|
654
|
+
throw new Error("frontend shape transition requires canonical task source binding");
|
|
655
|
+
}
|
|
656
|
+
return await commitFrontendShapeTransitionCas({
|
|
657
|
+
state,
|
|
658
|
+
tasksById,
|
|
659
|
+
request,
|
|
660
|
+
taskSourceDigest: canonicalFrontendSource.sourceDigest,
|
|
661
|
+
ensureRecord: async (capsule, terminalOutcome) => {
|
|
662
|
+
await ensureFreshFrontendShapeCapsuleRecord({
|
|
663
|
+
cwd,
|
|
664
|
+
spec,
|
|
665
|
+
canonicalSource: canonicalFrontendSource,
|
|
666
|
+
parentRunId: state.runId,
|
|
667
|
+
terminalOutcome,
|
|
668
|
+
capsule,
|
|
669
|
+
});
|
|
670
|
+
},
|
|
671
|
+
persist: async (nextState) => {
|
|
672
|
+
stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, nextState));
|
|
673
|
+
await stateWriteQueue;
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
releaseBarrier();
|
|
679
|
+
shapeTransitionBarrier = undefined;
|
|
680
|
+
}
|
|
681
|
+
},
|
|
571
682
|
runDir,
|
|
572
683
|
abortSignal: input.abortSignal,
|
|
573
684
|
createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
|
|
@@ -603,6 +714,18 @@ async function executeDagCheckpoint(input) {
|
|
|
603
714
|
});
|
|
604
715
|
const node = state.nodes[nodeId];
|
|
605
716
|
if (node && (node.status === "FINISHED" || node.status === "ERROR")) {
|
|
717
|
+
if (node.status === "FINISHED") {
|
|
718
|
+
if (canonicalFrontendSource) {
|
|
719
|
+
await commitRuntimeFrontendShapeFact({
|
|
720
|
+
state,
|
|
721
|
+
tasksById,
|
|
722
|
+
sourceNodeId: nodeId,
|
|
723
|
+
taskId: canonicalFrontendSource.taskId,
|
|
724
|
+
requirementMarkdown: canonicalFrontendSource.requirementMarkdown,
|
|
725
|
+
constraintMarkdown: canonicalFrontendSource.constraintMarkdown,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
606
729
|
recordFinishedNodeBudget(state, node);
|
|
607
730
|
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
608
731
|
await persistState();
|
|
@@ -612,6 +735,9 @@ async function executeDagCheckpoint(input) {
|
|
|
612
735
|
let pausedByNodeId;
|
|
613
736
|
while (true) {
|
|
614
737
|
pausedByNodeId = await executeRanks(convergenceRanks);
|
|
738
|
+
if (state.frontendShapeState?.terminalOutcome) {
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
615
741
|
if (pausedByNodeId) {
|
|
616
742
|
break;
|
|
617
743
|
}
|
|
@@ -667,7 +793,7 @@ async function executeDagCheckpoint(input) {
|
|
|
667
793
|
runDir = await moveToPausedRunDir(runDir, pausedRunDir);
|
|
668
794
|
}
|
|
669
795
|
else {
|
|
670
|
-
const { recoveryPending } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
|
|
796
|
+
const { recoveryPending, recovery: recoveryTrigger } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
|
|
671
797
|
await finalizeRunOwnedInterrupt({
|
|
672
798
|
runDir,
|
|
673
799
|
state,
|
|
@@ -675,52 +801,56 @@ async function executeDagCheckpoint(input) {
|
|
|
675
801
|
interruptAborted: interruptController.signal.aborted,
|
|
676
802
|
});
|
|
677
803
|
await persistState();
|
|
678
|
-
if (recoveryPending) {
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
804
|
+
if (recoveryPending && input.recovery && recoveryTrigger) {
|
|
805
|
+
// M6 recovery operation controller path: construct/reuse one recovery
|
|
806
|
+
// operation keyed by a deterministic clientRequestId and supervise it
|
|
807
|
+
// to terminal, instead of staging an in-process child run.
|
|
808
|
+
const created = await createRecoveryOperation({
|
|
809
|
+
cwd,
|
|
810
|
+
runDir,
|
|
811
|
+
spec,
|
|
812
|
+
state,
|
|
813
|
+
trigger: recoveryTrigger,
|
|
814
|
+
deps: input.recovery,
|
|
815
|
+
});
|
|
816
|
+
if (created.kind === "conflict") {
|
|
817
|
+
// Same clientRequestId with a different payload → fail closed; never
|
|
818
|
+
// create a second recovery operation (AC-004).
|
|
819
|
+
state.status = "failed";
|
|
820
|
+
state.terminalReason =
|
|
821
|
+
"frontend recovery request-id reuse conflict";
|
|
822
|
+
state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, "auto-recovery-blocked");
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
const { runRecoveryOperationController } = await import("./frontend-recovery-controller.js");
|
|
826
|
+
const result = await runRecoveryOperationController({
|
|
827
|
+
operationId: created.operation.operationId,
|
|
691
828
|
parentRunId: state.runId,
|
|
829
|
+
deps: {
|
|
830
|
+
...input.recovery,
|
|
831
|
+
failureOwner: input.recovery.failureOwner ??
|
|
832
|
+
recoveryTrigger.failureOwner,
|
|
833
|
+
protocolFailureReason: input.recovery.protocolFailureReason ??
|
|
834
|
+
recoveryTrigger.protocolFailureReason,
|
|
835
|
+
recoveryMode: input.recovery.recoveryMode ?? recoveryTrigger.recoveryMode,
|
|
836
|
+
},
|
|
692
837
|
});
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
// back to stale child-staging.
|
|
697
|
-
const parentOnDisk = await readDagRunState(runDir);
|
|
698
|
-
state.frontendRecoveryState = parentOnDisk.frontendRecoveryState;
|
|
699
|
-
const childState = await readDagRunState(staged.childRunDir);
|
|
700
|
-
const childSpec = await readDagRunSpec(staged.childRunDir);
|
|
701
|
-
await runDagContinuation({
|
|
702
|
-
cwd,
|
|
703
|
-
spec: childSpec,
|
|
704
|
-
state: childState,
|
|
705
|
-
runDir: staged.childRunDir,
|
|
706
|
-
maxConcurrent,
|
|
707
|
-
executeNode: input.executeNode,
|
|
708
|
-
observer: input.observer,
|
|
709
|
-
abortSignal,
|
|
710
|
-
});
|
|
838
|
+
state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, result.terminalReason === "succeeded"
|
|
839
|
+
? "recovered"
|
|
840
|
+
: "auto-recovery-blocked");
|
|
711
841
|
}
|
|
712
842
|
}
|
|
713
843
|
await notifyRunObserver(input.observer, "onRunFinish", state);
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
catch (error) {
|
|
720
|
-
console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
|
|
721
|
-
}
|
|
722
|
-
runDir = await moveToCompletedRunDir(runDir, completedRunDir);
|
|
844
|
+
// Parent is always archived to completed/ — no active/ retention for an
|
|
845
|
+
// in-process child CAS anymore.
|
|
846
|
+
try {
|
|
847
|
+
const terminalCheckpoint = await captureWorkspaceCheckpoint(cwd);
|
|
848
|
+
await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL, terminalCheckpoint);
|
|
723
849
|
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
|
|
852
|
+
}
|
|
853
|
+
runDir = await moveToCompletedRunDir(runDir, completedRunDir);
|
|
724
854
|
}
|
|
725
855
|
await relocateRunArtifactPaths({
|
|
726
856
|
runDir,
|
|
@@ -853,39 +983,21 @@ function isSuccessfulConvergenceTerminal(state) {
|
|
|
853
983
|
return true;
|
|
854
984
|
return SUCCESSFUL_CONVERGENCE_TERMINAL_REASONS.has(reason);
|
|
855
985
|
}
|
|
856
|
-
function buildFrontendRecoveryIntent(state, failureSource) {
|
|
857
|
-
const existing = state.frontendRecoveryState;
|
|
858
|
-
const requestId = existing?.requestId ?? randomUUID();
|
|
859
|
-
return {
|
|
860
|
-
schemaVersion: 1,
|
|
861
|
-
phase: "child-staging",
|
|
862
|
-
requestId,
|
|
863
|
-
recoveryRootRunId: existing?.recoveryRootRunId ?? state.runId,
|
|
864
|
-
parentRunId: state.runId,
|
|
865
|
-
...(existing?.childRunId ? { childRunId: existing.childRunId } : {}),
|
|
866
|
-
attemptId: existing?.attemptId ?? `recovery-${requestId}`,
|
|
867
|
-
attemptIndex: existing?.attemptIndex ?? 0,
|
|
868
|
-
continuationCount: existing?.continuationCount ?? 0,
|
|
869
|
-
...(failureSource ? { failureSource } : {}),
|
|
870
|
-
revision: (existing?.revision ?? 0) + 1,
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
986
|
function buildCandidateContractInvalidResult(state, result, artifactHash) {
|
|
874
|
-
const recovery = state.frontendRecoveryState;
|
|
875
987
|
return {
|
|
876
988
|
schemaVersion: 1,
|
|
877
989
|
outcome: "candidate-contract-invalid",
|
|
878
990
|
origin: {
|
|
879
991
|
kind: "frontend-prewrite-gate",
|
|
880
|
-
parentRunId:
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
failedNodeId: result.selectedPlanNodeId,
|
|
992
|
+
parentRunId: state.runId,
|
|
993
|
+
requestId: state.runId,
|
|
994
|
+
failedNodeId: result.failureSource ?? "frontend-plan-pi",
|
|
884
995
|
},
|
|
885
996
|
failureClass: {
|
|
886
997
|
code: "candidate-contract-invalid",
|
|
887
998
|
classification: result.classification,
|
|
888
|
-
reason: result.
|
|
999
|
+
reason: result.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ") ??
|
|
1000
|
+
"candidate contract invalid",
|
|
889
1001
|
},
|
|
890
1002
|
evidenceRefs: [
|
|
891
1003
|
{
|
|
@@ -897,15 +1009,13 @@ function buildCandidateContractInvalidResult(state, result, artifactHash) {
|
|
|
897
1009
|
};
|
|
898
1010
|
}
|
|
899
1011
|
function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
|
|
900
|
-
const recovery = state.frontendRecoveryState;
|
|
901
1012
|
return {
|
|
902
1013
|
schemaVersion: 1,
|
|
903
1014
|
outcome: "auto-recovery-blocked",
|
|
904
1015
|
origin: {
|
|
905
1016
|
kind: "frontend-writer",
|
|
906
|
-
parentRunId:
|
|
907
|
-
|
|
908
|
-
requestId: recovery?.requestId ?? "",
|
|
1017
|
+
parentRunId: state.runId,
|
|
1018
|
+
requestId: state.runId,
|
|
909
1019
|
failedNodeId,
|
|
910
1020
|
},
|
|
911
1021
|
failureClass: {
|
|
@@ -916,63 +1026,78 @@ function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
|
|
|
916
1026
|
evidenceRefs: [],
|
|
917
1027
|
};
|
|
918
1028
|
}
|
|
919
|
-
function
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
failedNodeId: "",
|
|
934
|
-
},
|
|
935
|
-
evidenceRefs: [],
|
|
936
|
-
};
|
|
937
|
-
}
|
|
1029
|
+
function buildRecoveryResultForTrigger(state, trigger, outcome) {
|
|
1030
|
+
return {
|
|
1031
|
+
schemaVersion: 1,
|
|
1032
|
+
outcome,
|
|
1033
|
+
origin: {
|
|
1034
|
+
kind: trigger.failureSource === "frontend-implement-pi"
|
|
1035
|
+
? "frontend-writer"
|
|
1036
|
+
: "frontend-prewrite-gate",
|
|
1037
|
+
parentRunId: state.runId,
|
|
1038
|
+
requestId: trigger.requestId,
|
|
1039
|
+
failedNodeId: trigger.failureSource,
|
|
1040
|
+
},
|
|
1041
|
+
evidenceRefs: [],
|
|
1042
|
+
};
|
|
938
1043
|
}
|
|
939
1044
|
export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
940
1045
|
const repoRoot = cwd ?? path.resolve(runDir, "..", "..", "..", "..");
|
|
1046
|
+
if (state.frontendShapeState?.terminalOutcome &&
|
|
1047
|
+
state.frontendShapeState.transitionCapsule) {
|
|
1048
|
+
state.status = "failed";
|
|
1049
|
+
return { recoveryPending: false };
|
|
1050
|
+
}
|
|
941
1051
|
// Terminal override: a denied frontend writer must never surface as a
|
|
942
1052
|
// mechanical partial_failed just because prewrite FINISHED while the writer
|
|
943
|
-
// was SKIPPED. Fail closed on
|
|
944
|
-
let
|
|
1053
|
+
// was SKIPPED. Fail closed on blocked before aggregation.
|
|
1054
|
+
let recoveryTrigger;
|
|
945
1055
|
const skipFrontendRecovery = state.failureCategory === "controller-interrupted";
|
|
946
1056
|
const hasFrontendWriter = Object.keys(state.nodes).some((id) => FRONTEND_WRITER_NODE_IDS.includes(id));
|
|
947
1057
|
if (hasFrontendWriter && !skipFrontendRecovery) {
|
|
948
1058
|
const admission = await readFrontendPrewriteResult(runDir);
|
|
949
1059
|
if (admission.ok) {
|
|
950
|
-
|
|
1060
|
+
// M8: admission has only admitted|blocked. A blocked admission whose
|
|
1061
|
+
// failureSource is frontend-plan-pi is the contract-invalid analog of
|
|
1062
|
+
// the old retryable-invalid path (candidate continuation may re-plan);
|
|
1063
|
+
// every other blocked admission is terminal.
|
|
1064
|
+
const planContractInvalid = admission.result.classification === "blocked" &&
|
|
1065
|
+
admission.result.failureSource === "frontend-plan-pi";
|
|
1066
|
+
const terminalAdmissionDenial = (admission.result.classification === "blocked" ||
|
|
1067
|
+
admission.result.classification === "stale") &&
|
|
1068
|
+
!planContractInvalid;
|
|
1069
|
+
if (terminalAdmissionDenial) {
|
|
951
1070
|
state.status = "failed";
|
|
952
1071
|
return { recoveryPending: false };
|
|
953
1072
|
}
|
|
954
|
-
if (
|
|
1073
|
+
if (planContractInvalid) {
|
|
955
1074
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
956
1075
|
if (continuationCount >= 1) {
|
|
957
|
-
// Continuation quota exhausted: a second
|
|
1076
|
+
// Continuation quota exhausted: a second contract-invalid candidate
|
|
958
1077
|
// is authoritative candidate-contract-invalid → forced failed.
|
|
959
1078
|
state.frontendRecoveryResult = buildCandidateContractInvalidResult(state, admission.result, admission.artifactHash);
|
|
960
1079
|
state.status = "failed";
|
|
961
1080
|
return { recoveryPending: false };
|
|
962
1081
|
}
|
|
963
|
-
// Root continuation still available:
|
|
964
|
-
//
|
|
965
|
-
//
|
|
966
|
-
|
|
967
|
-
|
|
1082
|
+
// Root continuation still available: emit a recovery trigger for the
|
|
1083
|
+
// M6 operation controller. The parent no longer writes
|
|
1084
|
+
// frontendRecoveryState or stays in active/ for a child CAS.
|
|
1085
|
+
recoveryTrigger = {
|
|
1086
|
+
requestId: state.runId,
|
|
1087
|
+
failureSource: admission.result.failureSource ?? "frontend-plan-pi",
|
|
1088
|
+
failureOwner: "plan",
|
|
1089
|
+
protocolFailureReason: "frontend candidate contract invalid",
|
|
1090
|
+
recoveryMode: "new-dag",
|
|
1091
|
+
};
|
|
968
1092
|
}
|
|
969
1093
|
}
|
|
970
1094
|
}
|
|
971
|
-
// Phase 5: writer transient partial write → rollback + recovery
|
|
972
|
-
// for frontend-implementation writers with a remaining root
|
|
973
|
-
// only when the rollback journal (captured before the
|
|
974
|
-
// cleanly; otherwise the run stays failed with
|
|
975
|
-
|
|
1095
|
+
// Phase 5: writer transient partial write → rollback + recovery trigger.
|
|
1096
|
+
// Only for frontend-implementation writers with a remaining root
|
|
1097
|
+
// continuation, and only when the rollback journal (captured before the
|
|
1098
|
+
// provider call) restores cleanly; otherwise the run stays failed with
|
|
1099
|
+
// auto-recovery-blocked.
|
|
1100
|
+
if (hasFrontendWriter && !recoveryTrigger && !skipFrontendRecovery) {
|
|
976
1101
|
const writerNodeId = FRONTEND_WRITER_NODE_IDS.find((id) => {
|
|
977
1102
|
const node = state.nodes[id];
|
|
978
1103
|
return (node &&
|
|
@@ -990,8 +1115,13 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
990
1115
|
parentRunId: state.runId,
|
|
991
1116
|
});
|
|
992
1117
|
if (rollback.ok) {
|
|
993
|
-
|
|
994
|
-
|
|
1118
|
+
recoveryTrigger = {
|
|
1119
|
+
requestId: state.runId,
|
|
1120
|
+
failureSource: "frontend-implement-pi",
|
|
1121
|
+
failureOwner: "implementation",
|
|
1122
|
+
protocolFailureReason: "frontend writer transient partial write",
|
|
1123
|
+
recoveryMode: "continuation",
|
|
1124
|
+
};
|
|
995
1125
|
}
|
|
996
1126
|
else {
|
|
997
1127
|
state.frontendRecoveryResult = buildWriterPartialWriteBlockedResult(state, writerNodeId, rollback.reason);
|
|
@@ -1001,6 +1131,26 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1001
1131
|
}
|
|
1002
1132
|
}
|
|
1003
1133
|
}
|
|
1134
|
+
// Phase 6: verification failure → automatic recovery (re-implement +
|
|
1135
|
+
// re-verify). The verify shell is terminal by design so the failed run's
|
|
1136
|
+
// evidence stays clean; the M6 recovery operation re-runs implement and
|
|
1137
|
+
// verify in a fresh continuation, guarded by the same continuation quota,
|
|
1138
|
+
// budget fold and failure fingerprint as the other recovery paths.
|
|
1139
|
+
if (hasFrontendWriter && !recoveryTrigger && !skipFrontendRecovery) {
|
|
1140
|
+
const verifyFailed = state.nodes["frontend-verify-shell"]?.status === "ERROR";
|
|
1141
|
+
if (verifyFailed) {
|
|
1142
|
+
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1143
|
+
if (continuationCount < 1) {
|
|
1144
|
+
recoveryTrigger = {
|
|
1145
|
+
requestId: state.runId,
|
|
1146
|
+
failureSource: "frontend-verify-shell",
|
|
1147
|
+
failureOwner: "implementation",
|
|
1148
|
+
protocolFailureReason: "frontend verification failure",
|
|
1149
|
+
recoveryMode: "continuation",
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1004
1154
|
const finishedCount = Object.values(state.nodes).filter((n) => n.status === "FINISHED").length;
|
|
1005
1155
|
const { supersededIds, supersededFailures } = collectSupersededIntermediateFailures(state);
|
|
1006
1156
|
if (state.convergence && supersededFailures.length > 0) {
|
|
@@ -1024,19 +1174,64 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1024
1174
|
else {
|
|
1025
1175
|
state.status = "failed";
|
|
1026
1176
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1177
|
+
return recoveryTrigger
|
|
1178
|
+
? { recoveryPending: true, recovery: recoveryTrigger }
|
|
1179
|
+
: { recoveryPending: false };
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Construct (or reuse) the recovery operation for a failed frontend run.
|
|
1183
|
+
*
|
|
1184
|
+
* The operation is keyed by a deterministic `clientRequestId`
|
|
1185
|
+
* (`dag-recovery-<parentRunId>`), so repeated wakeups of the same parent hit
|
|
1186
|
+
* `OperationStore.create`'s idempotent short-circuit instead of creating a
|
|
1187
|
+
* second recovery run (AC-004). The continuation DagSpec/reset partition is
|
|
1188
|
+
* frozen via `computeFrontendRecoveryPlanForSource` and serialized under the
|
|
1189
|
+
* run dir's bounded `.runtime/` namespace — never the repo root or `.harness`
|
|
1190
|
+
* siblings. `planHash` is the second idempotency dimension of `actionParams`.
|
|
1191
|
+
*/
|
|
1192
|
+
async function createRecoveryOperation(input) {
|
|
1193
|
+
const { cwd, runDir, spec, state, trigger } = input;
|
|
1194
|
+
const { computeFrontendRecoveryPlanForSource } = await import("./frontend-recovery-plan.js");
|
|
1195
|
+
const plan = computeFrontendRecoveryPlanForSource({
|
|
1196
|
+
spec,
|
|
1197
|
+
parentRunId: state.runId,
|
|
1198
|
+
requestId: trigger.requestId,
|
|
1199
|
+
failureSource: trigger.failureSource,
|
|
1200
|
+
});
|
|
1201
|
+
const planHash = createHash("sha256")
|
|
1202
|
+
.update(JSON.stringify(plan))
|
|
1203
|
+
.digest("hex");
|
|
1204
|
+
const continuationPath = path.join(runDir, ".runtime", "frontend-recovery", `${trigger.requestId}.json`);
|
|
1205
|
+
await writeJsonAtomic(continuationPath, {
|
|
1206
|
+
schemaVersion: 1,
|
|
1207
|
+
parentRunId: state.runId,
|
|
1208
|
+
requestId: trigger.requestId,
|
|
1209
|
+
failureSource: trigger.failureSource,
|
|
1210
|
+
recoveryMode: trigger.recoveryMode,
|
|
1211
|
+
plan,
|
|
1212
|
+
});
|
|
1213
|
+
return input.deps.store.create({
|
|
1214
|
+
clientRequestId: `dag-recovery-${state.runId}`,
|
|
1215
|
+
action: "dagRunTask",
|
|
1216
|
+
actionParams: {
|
|
1217
|
+
parentRunId: state.runId,
|
|
1218
|
+
requestId: trigger.requestId,
|
|
1219
|
+
failureSource: trigger.failureSource,
|
|
1220
|
+
failureOwner: trigger.failureOwner,
|
|
1221
|
+
protocolFailureReason: trigger.protocolFailureReason,
|
|
1222
|
+
recoveryMode: trigger.recoveryMode,
|
|
1223
|
+
planHash,
|
|
1224
|
+
},
|
|
1225
|
+
cliArgs: [
|
|
1226
|
+
"dag",
|
|
1227
|
+
"execute",
|
|
1228
|
+
"--dag",
|
|
1229
|
+
continuationPath,
|
|
1230
|
+
"--cwd",
|
|
1231
|
+
cwd,
|
|
1232
|
+
"--json",
|
|
1233
|
+
],
|
|
1234
|
+
});
|
|
1040
1235
|
}
|
|
1041
1236
|
function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
|
|
1042
1237
|
if (rankWriterNodeIds.length <= 1)
|