@tea-agent/loop-agent 0.41.1-beta.0 → 0.41.1
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 +47 -267
- package/dist/application/dag/generate-task-dag.js +19 -75
- package/dist/application/task-lifecycle/advance.js +5 -24
- package/dist/application/task-lifecycle/observe.js +17 -171
- package/dist/application/task-lifecycle/plan-transitions.js +7 -42
- package/dist/application/task-lifecycle/recommendations.js +2 -13
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +0 -7
- package/dist/cli/program.js +0 -5
- package/dist/commands/client-recovery.js +0 -3
- package/dist/commands/init.js +1 -27
- package/dist/commands/task-advance.js +0 -19
- package/dist/executors/dag-pi-executor.js +231 -3784
- package/dist/executors/pi-executor.js +4 -15
- package/dist/executors/shell-executor.js +217 -945
- package/dist/executors/shell-write-guard.js +0 -7
- package/dist/shared/dag-failure-category.js +0 -12
- package/dist/shared/openspec-spec.js +4 -70
- package/dist/shared/operator/capabilities.js +0 -7
- package/dist/task/config-types.js +7 -34
- package/dist/task/contract/adopt.js +0 -4
- package/dist/task/contract/import-revision.js +0 -4
- package/dist/task/contract/project.js +0 -3
- package/dist/task/contract/schema.js +1 -2
- package/dist/task/frontend-project-capability.js +20 -203
- package/dist/task/runtime.js +2 -5
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +9 -15
- package/dist/task/source-prepare/prepare.js +1 -89
- package/dist/task/source-prepare/semantic-intake.js +2 -6
- package/dist/task/source-references.js +1 -22
- package/dist/worker/console/chat/chat-ui-policy.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +176 -9
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +42 -22
- package/dist/worker/console/chat/semantic-activity.js +12 -11
- package/dist/worker/console/chat/session-stats.js +96 -0
- 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 +139 -60
- package/dist/worker/console/index.js +0 -3
- package/dist/worker/console/operation-store.js +6 -226
- package/dist/worker/console/operator-actions.js +7 -169
- package/dist/worker/console/pi-plugins.js +3 -1
- package/dist/worker/console/server.js +0 -3
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-D7oU983t.js} +1 -1
- package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-BrKSRdFS.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-CiXV7L56.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-D_Hs_gIG.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-DbM-Lqqh.js} +1 -1
- package/dist/worker/console/static/assets/channel-D1xajGpe.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-D1M8uqJw.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-4D3RFHT6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-CdwpY_n9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-BLeLZ9PG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-CeRNEMvc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-CjkHP8Vy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-B54ZUQY-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-4bKaMVmx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-Cyjfznu_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-CDN-zGFV.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BmuGTfRr.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BmuGTfRr.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-HxJmz411.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-DtSt5K2w.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-VtkSpRmG.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-Bj4DT4du.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-Bpuk6BmP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-BHYGv_yO.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-DEx3WwnT.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-Cj0dS5e8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-B97h8l21.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-DMSFAhBd.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-DhVOCbt6.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-GlakITWL.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-VxPpu9eQ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-Bs_e5DRT.js} +1 -1
- package/dist/worker/console/static/assets/index-BLzcAykx.js +451 -0
- package/dist/worker/console/static/assets/index-Cs9JUSPs.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-BXYuU4Uw.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-C34erCbU.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-DZCAiOU3.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-C9gaTdUm.js} +1 -1
- package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-CvI0Z17U.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-DMMSIx7j.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-BKKP0EO6.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-C3LiFbl5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-Bb3Om2Dz.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-DNthMjzc.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-miFsr4Tv.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-yXUncmn6.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-DLeWKLak.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-CI3he1R3.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-B7jfDkS5.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-BmrZod7J.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-D9ykKEWP.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-CDOlHVSY.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BvPOXALT.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-Cz-XX1gy.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-caV4cI8C.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-dImPlnIO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-CtRYVJcn.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
- package/dist/worker/console/workspace-context.js +1 -111
- package/dist/worker/observability/read-model.js +4 -0
- package/dist/worker/observe/static/dag-node-purpose.js +0 -5
- package/dist/worker/observe/static/views/dag-inspector.js +2 -7
- package/dist/worker/observe/static/views/session-timeline.js +46 -15
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
- package/dist/workflows/dag/frontend-plan-render.js +1 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
- package/dist/workflows/dag/frontend-recovery-run.js +20 -33
- package/dist/workflows/dag/frontend-repair.js +432 -1
- package/dist/workflows/dag/frontend-review-context.js +15 -261
- package/dist/workflows/dag/frontend-verification-trace.js +24 -249
- package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
- package/dist/workflows/dag/init-hybrid.js +494 -685
- package/dist/workflows/dag/interrupt-request.js +0 -7
- package/dist/workflows/dag/node-execution.js +4 -732
- package/dist/workflows/dag/prompt.js +1 -34
- package/dist/workflows/dag/report.js +1 -37
- package/dist/workflows/dag/rerun-feedback.js +1 -256
- package/dist/workflows/dag/rerun-plan.js +2 -10
- package/dist/workflows/dag/rerun-run.js +0 -23
- package/dist/workflows/dag/rerun-task.js +0 -29
- package/dist/workflows/dag/retry-policy.js +104 -214
- package/dist/workflows/dag/runner.js +122 -429
- package/dist/workflows/dag/scheduler.js +16 -114
- package/dist/workflows/dag/types.js +12 -239
- package/dist/workflows/dag/validate.js +12 -28
- package/docs/init-surface.manifest.json +12 -30
- package/docs/skills/vetted-skill-registry.md +2 -4
- package/docs/templates/README.md +0 -2
- package/docs/templates/agent-dag-report.schema.json +2 -8
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +1 -4
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +14 -15
- package/skills/frontend-design-review/SKILL.md +41 -22
- package/skills/frontend-implementation/SKILL.md +52 -0
- package/skills/frontend-implementation/references/code-standards.md +33 -0
- package/skills/frontend-implementation/references/design-spec.md +56 -0
- package/skills/frontend-implementation/references/node-contracts.md +31 -0
- package/skills/frontend-review/SKILL.md +15 -20
- package/skills/frontend-review/references/review-findings.md +7 -6
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +0 -1
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/commands/dag-follow-up.js +0 -138
- package/dist/executors/pi-read-budget-policy.js +0 -239
- package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
- package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
- package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
- package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
- package/dist/worker/console/static-src/active-run-badge.js +0 -17
- package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
- package/dist/workflows/dag/dag-retry-schema.js +0 -138
- package/dist/workflows/dag/frontend-closeout.js +0 -221
- package/dist/workflows/dag/frontend-design-policy.js +0 -400
- package/dist/workflows/dag/frontend-human-decision.js +0 -182
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
- package/dist/workflows/dag/frontend-read-budgets.js +0 -12
- package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
- package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
- package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
- package/dist/workflows/dag/frontend-review-findings.js +0 -270
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
- package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
- package/dist/workflows/dag/frontend-shape-facts.js +0 -409
- package/dist/workflows/dag/frontend-shape.js +0 -427
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
- package/dist/workflows/dag/frontend-split-application-service.js +0 -203
- package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
- package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
- package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
- package/dist/workflows/dag/frontend-writer-admission.js +0 -285
- package/dist/workflows/dag/frontend-writer-status.js +0 -256
- package/dist/workflows/dag/recovery-lease.js +0 -80
- package/docs/templates/frontend-implementation-dag.json +0 -89
- package/docs/templates/spec-registry.schema.json +0 -45
- package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
- package/skills/frontend-contract/SKILL.md +0 -23
- package/skills/frontend-contract/references/contract-protocol.md +0 -34
- package/skills/frontend-plan/SKILL.md +0 -26
- package/skills/frontend-plan/references/decision-contract.md +0 -37
- package/skills/frontend-plan/references/design-decisions.md +0 -17
- package/skills/frontend-scout/SKILL.md +0 -25
- package/skills/frontend-scout/references/design-evidence.md +0 -16
- package/skills/frontend-scout/references/scout-evidence.md +0 -23
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } 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, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
|
|
10
|
+
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readDagRunSpec, readDagRunState, 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,12 +26,7 @@ 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";
|
|
33
29
|
import { parseDagSpec, resolveModelForTask, } from "./types.js";
|
|
34
|
-
import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
|
|
35
30
|
import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
|
|
36
31
|
import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
|
|
37
32
|
import { executeDynamicMapExpansion } from "./dynamic-runtime/map.js";
|
|
@@ -51,51 +46,6 @@ export function toKebabSlug(text, maxLen = 40) {
|
|
|
51
46
|
.replace(/^-|-$/g, "")
|
|
52
47
|
.slice(0, maxLen) || "dag");
|
|
53
48
|
}
|
|
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
|
-
}
|
|
99
49
|
export function extractTaskIdFromDagSpec(spec) {
|
|
100
50
|
if (spec.objective) {
|
|
101
51
|
const match = spec.objective.match(TASK_ID_IN_TEXT_PATTERN);
|
|
@@ -230,15 +180,6 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
230
180
|
...(opts.workerAssociation
|
|
231
181
|
? { workerAssociation: structuredClone(opts.workerAssociation) }
|
|
232
182
|
: {}),
|
|
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
|
-
: {}),
|
|
242
183
|
};
|
|
243
184
|
initRunBudgetLedger(state, spec.budget);
|
|
244
185
|
return state;
|
|
@@ -360,7 +301,6 @@ export async function runDag(spec, opts) {
|
|
|
360
301
|
activeRunDir,
|
|
361
302
|
completedRunDir,
|
|
362
303
|
pausedRunDir,
|
|
363
|
-
recovery: opts.recovery,
|
|
364
304
|
});
|
|
365
305
|
}
|
|
366
306
|
finally {
|
|
@@ -369,16 +309,6 @@ export async function runDag(spec, opts) {
|
|
|
369
309
|
export async function runDagContinuation(opts) {
|
|
370
310
|
const { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), opts.spec.budget);
|
|
371
311
|
const state = opts.state;
|
|
372
|
-
const canonicalFrontend = isCanonicalFrontendShapeDag(new Map(opts.spec.tasks.map((task) => [task.id, task])));
|
|
373
|
-
if (canonicalFrontend) {
|
|
374
|
-
assertFrontendTopologyBinding(state.frontendShapeState?.frozenTopology, opts.spec.tasks.map((task) => task.id));
|
|
375
|
-
}
|
|
376
|
-
else if (state.frontendShapeState) {
|
|
377
|
-
throw new Error("non-frontend DAG unexpectedly contains frontend topology state");
|
|
378
|
-
}
|
|
379
|
-
if (state.frontendShapeState?.terminalOutcome) {
|
|
380
|
-
throw new Error(`dag run ${state.runId} is terminal due to frontend shape ${state.frontendShapeState.terminalOutcome}`);
|
|
381
|
-
}
|
|
382
312
|
state.status = "running";
|
|
383
313
|
const resumedAt = new Date().toISOString();
|
|
384
314
|
state.runner = {
|
|
@@ -403,7 +333,6 @@ export async function runDagContinuation(opts) {
|
|
|
403
333
|
activeRunDir,
|
|
404
334
|
completedRunDir,
|
|
405
335
|
pausedRunDir,
|
|
406
|
-
recovery: opts.recovery,
|
|
407
336
|
});
|
|
408
337
|
}
|
|
409
338
|
export async function resumeDagRun(opts) {
|
|
@@ -506,13 +435,9 @@ export async function resumeDagRun(opts) {
|
|
|
506
435
|
}
|
|
507
436
|
async function executeDagCheckpoint(input) {
|
|
508
437
|
const { spec, state, ranks, runDir: initialRunDir, cwd, maxConcurrent, completedRunDir, pausedRunDir, } = input;
|
|
509
|
-
bindFrontendShapeStateToRunTopology(state);
|
|
510
438
|
let runDir = initialRunDir;
|
|
511
439
|
let stateWriteQueue = Promise.resolve();
|
|
512
|
-
let shapeTransitionBarrier;
|
|
513
440
|
const persistState = async (options) => {
|
|
514
|
-
if (shapeTransitionBarrier)
|
|
515
|
-
await shapeTransitionBarrier;
|
|
516
441
|
stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, state, options));
|
|
517
442
|
await stateWriteQueue;
|
|
518
443
|
};
|
|
@@ -637,50 +562,12 @@ async function executeDagCheckpoint(input) {
|
|
|
637
562
|
.map((rank) => rank.filter((nodeId) => nodeId === "closeout-pi"))
|
|
638
563
|
.filter((rank) => rank.length > 0)
|
|
639
564
|
: [];
|
|
640
|
-
const canonicalFrontendSource = isCanonicalFrontendShapeDag(tasksById)
|
|
641
|
-
? await loadCanonicalFrontendTaskSourceBinding(cwd, spec)
|
|
642
|
-
: undefined;
|
|
643
565
|
const executeRanks = (selectedRanks) => executeDagRanksOnce({
|
|
644
566
|
state,
|
|
645
567
|
ranks: selectedRanks,
|
|
646
568
|
tasksById,
|
|
647
569
|
maxConcurrent,
|
|
648
570
|
persistState,
|
|
649
|
-
commitFrontendShapeTransition: async (request) => {
|
|
650
|
-
let releaseBarrier;
|
|
651
|
-
shapeTransitionBarrier = new Promise((resolve) => {
|
|
652
|
-
releaseBarrier = resolve;
|
|
653
|
-
});
|
|
654
|
-
try {
|
|
655
|
-
if (!canonicalFrontendSource) {
|
|
656
|
-
throw new Error("frontend shape transition requires canonical task source binding");
|
|
657
|
-
}
|
|
658
|
-
return await commitFrontendShapeTransitionCas({
|
|
659
|
-
state,
|
|
660
|
-
tasksById,
|
|
661
|
-
request,
|
|
662
|
-
taskSourceDigest: canonicalFrontendSource.sourceDigest,
|
|
663
|
-
ensureRecord: async (capsule, terminalOutcome) => {
|
|
664
|
-
await ensureFreshFrontendShapeCapsuleRecord({
|
|
665
|
-
cwd,
|
|
666
|
-
spec,
|
|
667
|
-
canonicalSource: canonicalFrontendSource,
|
|
668
|
-
parentRunId: state.runId,
|
|
669
|
-
terminalOutcome,
|
|
670
|
-
capsule,
|
|
671
|
-
});
|
|
672
|
-
},
|
|
673
|
-
persist: async (nextState) => {
|
|
674
|
-
stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, nextState));
|
|
675
|
-
await stateWriteQueue;
|
|
676
|
-
},
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
finally {
|
|
680
|
-
releaseBarrier();
|
|
681
|
-
shapeTransitionBarrier = undefined;
|
|
682
|
-
}
|
|
683
|
-
},
|
|
684
571
|
runDir,
|
|
685
572
|
abortSignal: input.abortSignal,
|
|
686
573
|
createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
|
|
@@ -716,18 +603,6 @@ async function executeDagCheckpoint(input) {
|
|
|
716
603
|
});
|
|
717
604
|
const node = state.nodes[nodeId];
|
|
718
605
|
if (node && (node.status === "FINISHED" || node.status === "ERROR")) {
|
|
719
|
-
if (node.status === "FINISHED") {
|
|
720
|
-
if (canonicalFrontendSource) {
|
|
721
|
-
await commitRuntimeFrontendShapeFact({
|
|
722
|
-
state,
|
|
723
|
-
tasksById,
|
|
724
|
-
sourceNodeId: nodeId,
|
|
725
|
-
taskId: canonicalFrontendSource.taskId,
|
|
726
|
-
requirementMarkdown: canonicalFrontendSource.requirementMarkdown,
|
|
727
|
-
constraintMarkdown: canonicalFrontendSource.constraintMarkdown,
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
606
|
recordFinishedNodeBudget(state, node);
|
|
732
607
|
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
733
608
|
await persistState();
|
|
@@ -737,9 +612,6 @@ async function executeDagCheckpoint(input) {
|
|
|
737
612
|
let pausedByNodeId;
|
|
738
613
|
while (true) {
|
|
739
614
|
pausedByNodeId = await executeRanks(convergenceRanks);
|
|
740
|
-
if (state.frontendShapeState?.terminalOutcome) {
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
615
|
if (pausedByNodeId) {
|
|
744
616
|
break;
|
|
745
617
|
}
|
|
@@ -795,7 +667,7 @@ async function executeDagCheckpoint(input) {
|
|
|
795
667
|
runDir = await moveToPausedRunDir(runDir, pausedRunDir);
|
|
796
668
|
}
|
|
797
669
|
else {
|
|
798
|
-
const { recoveryPending
|
|
670
|
+
const { recoveryPending } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
|
|
799
671
|
await finalizeRunOwnedInterrupt({
|
|
800
672
|
runDir,
|
|
801
673
|
state,
|
|
@@ -803,65 +675,52 @@ async function executeDagCheckpoint(input) {
|
|
|
803
675
|
interruptAborted: interruptController.signal.aborted,
|
|
804
676
|
});
|
|
805
677
|
await persistState();
|
|
806
|
-
if (recoveryPending
|
|
807
|
-
//
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
// Same clientRequestId with a different payload → fail closed; never
|
|
820
|
-
// create a second recovery operation (AC-004).
|
|
821
|
-
state.status = "failed";
|
|
822
|
-
state.terminalReason =
|
|
823
|
-
"frontend recovery request-id reuse conflict";
|
|
824
|
-
state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, "auto-recovery-blocked");
|
|
825
|
-
}
|
|
826
|
-
else if (created.kind === "lease-held") {
|
|
827
|
-
// A different recovery entrance has already durably claimed this
|
|
828
|
-
// parent. Do not "help" by running its operation: that would reopen
|
|
829
|
-
// the exact two-child-DAG race the lease prevents.
|
|
830
|
-
state.status = "failed";
|
|
831
|
-
state.terminalReason =
|
|
832
|
-
"frontend recovery parent lease already held";
|
|
833
|
-
state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, "auto-recovery-blocked");
|
|
834
|
-
}
|
|
835
|
-
else {
|
|
836
|
-
const { runRecoveryOperationController } = await import("./frontend-recovery-controller.js");
|
|
837
|
-
const result = await runRecoveryOperationController({
|
|
838
|
-
operationId: created.operation.operationId,
|
|
678
|
+
if (recoveryPending) {
|
|
679
|
+
// Recovery coordinator (phase 3c AC-2/AC-3): materialize the reserved
|
|
680
|
+
// child and run it synchronously. Dynamic import avoids a static
|
|
681
|
+
// runner ↔ frontend-recovery-run module cycle. stageRecoveryChild
|
|
682
|
+
// locates the parent via the canonical active/ layout, so recovery
|
|
683
|
+
// only fires when this run is still the canonical active run dir
|
|
684
|
+
// (direct runDagContinuation callers using an arbitrary runDir keep
|
|
685
|
+
// the previous scheduler-layer skip semantics).
|
|
686
|
+
const canonicalActiveDir = getDagRunDir(cwd, "active", state.runId);
|
|
687
|
+
if (path.resolve(runDir) === path.resolve(canonicalActiveDir)) {
|
|
688
|
+
const { stageRecoveryChild } = await import("./frontend-recovery-run.js");
|
|
689
|
+
const staged = await stageRecoveryChild({
|
|
690
|
+
cwd,
|
|
839
691
|
parentRunId: state.runId,
|
|
840
|
-
deps: {
|
|
841
|
-
...input.recovery,
|
|
842
|
-
failureOwner: input.recovery.failureOwner ??
|
|
843
|
-
recoveryTrigger.failureOwner,
|
|
844
|
-
protocolFailureReason: input.recovery.protocolFailureReason ??
|
|
845
|
-
recoveryTrigger.protocolFailureReason,
|
|
846
|
-
recoveryMode: input.recovery.recoveryMode ?? recoveryTrigger.recoveryMode,
|
|
847
|
-
},
|
|
848
692
|
});
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
693
|
+
// stageRecoveryChild CAS-advanced the parent's on-disk lineage
|
|
694
|
+
// (child-staging → child-running + childRunId). Refresh the in-memory
|
|
695
|
+
// copy so the terminal persistState below does not regress the parent
|
|
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
|
+
});
|
|
852
711
|
}
|
|
853
712
|
}
|
|
854
713
|
await notifyRunObserver(input.observer, "onRunFinish", state);
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
714
|
+
if (!recoveryPending) {
|
|
715
|
+
try {
|
|
716
|
+
const terminalCheckpoint = await captureWorkspaceCheckpoint(cwd);
|
|
717
|
+
await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL, terminalCheckpoint);
|
|
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);
|
|
863
723
|
}
|
|
864
|
-
runDir = await moveToCompletedRunDir(runDir, completedRunDir);
|
|
865
724
|
}
|
|
866
725
|
await relocateRunArtifactPaths({
|
|
867
726
|
runDir,
|
|
@@ -994,21 +853,39 @@ function isSuccessfulConvergenceTerminal(state) {
|
|
|
994
853
|
return true;
|
|
995
854
|
return SUCCESSFUL_CONVERGENCE_TERMINAL_REASONS.has(reason);
|
|
996
855
|
}
|
|
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
|
+
}
|
|
997
873
|
function buildCandidateContractInvalidResult(state, result, artifactHash) {
|
|
874
|
+
const recovery = state.frontendRecoveryState;
|
|
998
875
|
return {
|
|
999
876
|
schemaVersion: 1,
|
|
1000
877
|
outcome: "candidate-contract-invalid",
|
|
1001
878
|
origin: {
|
|
1002
879
|
kind: "frontend-prewrite-gate",
|
|
1003
|
-
parentRunId: state.runId,
|
|
1004
|
-
|
|
1005
|
-
|
|
880
|
+
parentRunId: recovery?.parentRunId ?? state.runId,
|
|
881
|
+
...(recovery?.childRunId ? { childRunId: recovery.childRunId } : {}),
|
|
882
|
+
requestId: recovery?.requestId ?? "",
|
|
883
|
+
failedNodeId: result.selectedPlanNodeId,
|
|
1006
884
|
},
|
|
1007
885
|
failureClass: {
|
|
1008
886
|
code: "candidate-contract-invalid",
|
|
1009
887
|
classification: result.classification,
|
|
1010
|
-
reason: result.
|
|
1011
|
-
"candidate contract invalid",
|
|
888
|
+
reason: result.failureReason ?? "candidate contract invalid",
|
|
1012
889
|
},
|
|
1013
890
|
evidenceRefs: [
|
|
1014
891
|
{
|
|
@@ -1020,13 +897,15 @@ function buildCandidateContractInvalidResult(state, result, artifactHash) {
|
|
|
1020
897
|
};
|
|
1021
898
|
}
|
|
1022
899
|
function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
|
|
900
|
+
const recovery = state.frontendRecoveryState;
|
|
1023
901
|
return {
|
|
1024
902
|
schemaVersion: 1,
|
|
1025
903
|
outcome: "auto-recovery-blocked",
|
|
1026
904
|
origin: {
|
|
1027
905
|
kind: "frontend-writer",
|
|
1028
|
-
parentRunId: state.runId,
|
|
1029
|
-
|
|
906
|
+
parentRunId: recovery?.parentRunId ?? state.runId,
|
|
907
|
+
...(recovery?.childRunId ? { childRunId: recovery.childRunId } : {}),
|
|
908
|
+
requestId: recovery?.requestId ?? "",
|
|
1030
909
|
failedNodeId,
|
|
1031
910
|
},
|
|
1032
911
|
failureClass: {
|
|
@@ -1037,115 +916,63 @@ function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
|
|
|
1037
916
|
evidenceRefs: [],
|
|
1038
917
|
};
|
|
1039
918
|
}
|
|
1040
|
-
function
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
919
|
+
function settleFrontendRecovery(state, outcome) {
|
|
920
|
+
const recovery = state.frontendRecoveryState;
|
|
921
|
+
if (!recovery)
|
|
922
|
+
return;
|
|
923
|
+
recovery.phase = "settled";
|
|
924
|
+
if (!state.frontendRecoveryResult) {
|
|
925
|
+
state.frontendRecoveryResult = {
|
|
926
|
+
schemaVersion: 1,
|
|
927
|
+
outcome,
|
|
928
|
+
origin: {
|
|
929
|
+
kind: "frontend-prewrite-gate",
|
|
930
|
+
parentRunId: recovery.parentRunId,
|
|
931
|
+
...(recovery.childRunId ? { childRunId: recovery.childRunId } : {}),
|
|
932
|
+
requestId: recovery.requestId,
|
|
933
|
+
failedNodeId: "",
|
|
934
|
+
},
|
|
935
|
+
evidenceRefs: [],
|
|
936
|
+
};
|
|
937
|
+
}
|
|
1054
938
|
}
|
|
1055
|
-
export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd
|
|
939
|
+
export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
1056
940
|
const repoRoot = cwd ?? path.resolve(runDir, "..", "..", "..", "..");
|
|
1057
|
-
if (state.frontendShapeState?.terminalOutcome &&
|
|
1058
|
-
state.frontendShapeState.transitionCapsule) {
|
|
1059
|
-
state.status = "failed";
|
|
1060
|
-
return { recoveryPending: false };
|
|
1061
|
-
}
|
|
1062
941
|
// Terminal override: a denied frontend writer must never surface as a
|
|
1063
942
|
// mechanical partial_failed just because prewrite FINISHED while the writer
|
|
1064
|
-
// was SKIPPED. Fail closed on blocked before aggregation.
|
|
1065
|
-
let
|
|
943
|
+
// was SKIPPED. Fail closed on retryable-invalid/blocked before aggregation.
|
|
944
|
+
let recoveryPending = false;
|
|
1066
945
|
const skipFrontendRecovery = state.failureCategory === "controller-interrupted";
|
|
1067
946
|
const hasFrontendWriter = Object.keys(state.nodes).some((id) => FRONTEND_WRITER_NODE_IDS.includes(id));
|
|
1068
947
|
if (hasFrontendWriter && !skipFrontendRecovery) {
|
|
1069
948
|
const admission = await readFrontendPrewriteResult(runDir);
|
|
1070
949
|
if (admission.ok) {
|
|
1071
|
-
|
|
1072
|
-
// failureSource is frontend-plan-pi is the contract-invalid analog of
|
|
1073
|
-
// the old retryable-invalid path (candidate continuation may re-plan);
|
|
1074
|
-
// every other blocked admission is terminal.
|
|
1075
|
-
const planContractInvalid = admission.result.classification === "blocked" &&
|
|
1076
|
-
admission.result.failureSource === "frontend-plan-pi";
|
|
1077
|
-
// A blocked admission caused by the design review committing
|
|
1078
|
-
// `request_design_changes` is a definite finding (not a protocol or
|
|
1079
|
-
// environment error): the design was reviewed and rejected. It is the
|
|
1080
|
-
// design-review analog of review-request-changes — not terminal, but
|
|
1081
|
-
// re-enters the next plan round with the committed design findings.
|
|
1082
|
-
// The failureSource may be frontend-writer-admission-shell or
|
|
1083
|
-
// frontend-design-policy-shell; the committed design fact is the
|
|
1084
|
-
// authoritative signal.
|
|
1085
|
-
const designRequestFact = await readCommittedDesignRequestFact({
|
|
1086
|
-
runDir,
|
|
1087
|
-
});
|
|
1088
|
-
const designRejected = admission.result.classification === "blocked" &&
|
|
1089
|
-
designRequestFact !== undefined;
|
|
1090
|
-
const terminalAdmissionDenial = (admission.result.classification === "blocked" ||
|
|
1091
|
-
admission.result.classification === "stale") &&
|
|
1092
|
-
!planContractInvalid &&
|
|
1093
|
-
!designRejected;
|
|
1094
|
-
if (terminalAdmissionDenial) {
|
|
950
|
+
if (admission.result.classification === "blocked") {
|
|
1095
951
|
state.status = "failed";
|
|
1096
952
|
return { recoveryPending: false };
|
|
1097
953
|
}
|
|
1098
|
-
if (
|
|
1099
|
-
state.terminalReason = "design-request-changes";
|
|
1100
|
-
try {
|
|
1101
|
-
await writeFrontendReviewFindingsPrd({
|
|
1102
|
-
runDir,
|
|
1103
|
-
parentRunId: state.runId,
|
|
1104
|
-
fact: designRequestFact,
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
catch (error) {
|
|
1108
|
-
console.warn(`[run-dag] warning: failed to write frontend design findings PRD: ${error instanceof Error ? error.message : String(error)}`);
|
|
1109
|
-
}
|
|
1110
|
-
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1111
|
-
if (continuationCount < frontendRecoveryQuota) {
|
|
1112
|
-
recoveryTrigger = {
|
|
1113
|
-
requestId: state.runId,
|
|
1114
|
-
failureSource: "frontend-design-review-pi",
|
|
1115
|
-
failureOwner: classifyReviewIssueCategoryFailureOwner(designRequestFact.issueCategory),
|
|
1116
|
-
protocolFailureReason: `frontend design review request_design_changes (issueCategory=${designRequestFact.issueCategory})`,
|
|
1117
|
-
recoveryMode: "new-dag",
|
|
1118
|
-
};
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
if (planContractInvalid) {
|
|
954
|
+
if (admission.result.classification === "retryable-invalid") {
|
|
1122
955
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1123
|
-
if (continuationCount >=
|
|
1124
|
-
// Continuation quota exhausted: a second
|
|
956
|
+
if (continuationCount >= 1) {
|
|
957
|
+
// Continuation quota exhausted: a second retryable-invalid candidate
|
|
1125
958
|
// is authoritative candidate-contract-invalid → forced failed.
|
|
1126
959
|
state.frontendRecoveryResult = buildCandidateContractInvalidResult(state, admission.result, admission.artifactHash);
|
|
1127
960
|
state.status = "failed";
|
|
1128
961
|
return { recoveryPending: false };
|
|
1129
962
|
}
|
|
1130
|
-
// Root continuation still available:
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
failureSource: admission.result.failureSource ?? "frontend-plan-pi",
|
|
1136
|
-
failureOwner: "plan",
|
|
1137
|
-
protocolFailureReason: "frontend candidate contract invalid",
|
|
1138
|
-
recoveryMode: "new-dag",
|
|
1139
|
-
};
|
|
963
|
+
// Root continuation still available: record the recovery intent and let
|
|
964
|
+
// mechanical aggregation settle partial_failed/failed. The parent keeps
|
|
965
|
+
// phase != settled (recovery in progress) and no final root manifest.
|
|
966
|
+
state.frontendRecoveryState = buildFrontendRecoveryIntent(state);
|
|
967
|
+
recoveryPending = true;
|
|
1140
968
|
}
|
|
1141
969
|
}
|
|
1142
970
|
}
|
|
1143
|
-
// Phase 5: writer transient partial write → rollback + recovery
|
|
1144
|
-
//
|
|
1145
|
-
//
|
|
1146
|
-
//
|
|
1147
|
-
|
|
1148
|
-
if (hasFrontendWriter && !recoveryTrigger && !skipFrontendRecovery) {
|
|
971
|
+
// Phase 5: writer transient partial write → rollback + recovery intent. Only
|
|
972
|
+
// for frontend-implementation writers with a remaining root continuation, and
|
|
973
|
+
// only when the rollback journal (captured before the provider call) restores
|
|
974
|
+
// cleanly; otherwise the run stays failed with auto-recovery-blocked.
|
|
975
|
+
if (hasFrontendWriter && !recoveryPending && !skipFrontendRecovery) {
|
|
1149
976
|
const writerNodeId = FRONTEND_WRITER_NODE_IDS.find((id) => {
|
|
1150
977
|
const node = state.nodes[id];
|
|
1151
978
|
return (node &&
|
|
@@ -1156,20 +983,15 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
|
|
|
1156
983
|
});
|
|
1157
984
|
if (writerNodeId) {
|
|
1158
985
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1159
|
-
if (continuationCount <
|
|
986
|
+
if (continuationCount < 1) {
|
|
1160
987
|
const { rollbackFrontendWriter } = await import("./frontend-writer-recovery.js");
|
|
1161
988
|
const rollback = await rollbackFrontendWriter({
|
|
1162
989
|
cwd: repoRoot,
|
|
1163
990
|
parentRunId: state.runId,
|
|
1164
991
|
});
|
|
1165
992
|
if (rollback.ok) {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
failureSource: "frontend-implement-pi",
|
|
1169
|
-
failureOwner: "implementation",
|
|
1170
|
-
protocolFailureReason: "frontend writer transient partial write",
|
|
1171
|
-
recoveryMode: "continuation",
|
|
1172
|
-
};
|
|
993
|
+
state.frontendRecoveryState = buildFrontendRecoveryIntent(state, "frontend-implement-pi");
|
|
994
|
+
recoveryPending = true;
|
|
1173
995
|
}
|
|
1174
996
|
else {
|
|
1175
997
|
state.frontendRecoveryResult = buildWriterPartialWriteBlockedResult(state, writerNodeId, rollback.reason);
|
|
@@ -1179,64 +1001,6 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
|
|
|
1179
1001
|
}
|
|
1180
1002
|
}
|
|
1181
1003
|
}
|
|
1182
|
-
// Phase 6: verification failure → automatic recovery (re-implement +
|
|
1183
|
-
// re-verify). The verify shell is terminal by design so the failed run's
|
|
1184
|
-
// evidence stays clean; the M6 recovery operation re-runs implement and
|
|
1185
|
-
// verify in a fresh continuation, guarded by the same continuation quota,
|
|
1186
|
-
// budget fold and failure fingerprint as the other recovery paths.
|
|
1187
|
-
if (hasFrontendWriter && !recoveryTrigger && !skipFrontendRecovery) {
|
|
1188
|
-
const verifyFailed = state.nodes["frontend-verify-shell"]?.status === "ERROR";
|
|
1189
|
-
if (verifyFailed) {
|
|
1190
|
-
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1191
|
-
if (continuationCount < frontendRecoveryQuota) {
|
|
1192
|
-
recoveryTrigger = {
|
|
1193
|
-
requestId: state.runId,
|
|
1194
|
-
failureSource: "frontend-verify-shell",
|
|
1195
|
-
failureOwner: "implementation",
|
|
1196
|
-
protocolFailureReason: "frontend verification failure",
|
|
1197
|
-
recoveryMode: "continuation",
|
|
1198
|
-
};
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
// Phase 7: frontend review "request changes" → not passed. When the review
|
|
1203
|
-
// node FINISHED but committed `request_review_changes` (typed terminal
|
|
1204
|
-
// verdict), the implementation was reviewed and rejected. The run must not
|
|
1205
|
-
// surface as finished: terminal aggregation below stays partial_failed, a
|
|
1206
|
-
// terminal reason is recorded, the committed findings are rendered into a
|
|
1207
|
-
// downstream-task PRD draft (run-owned), and a recovery trigger carries the
|
|
1208
|
-
// issueCategory-derived owner so a later DAG round consumes the findings
|
|
1209
|
-
// instead of repeating the previous flow.
|
|
1210
|
-
const reviewRequestFact = await readCommittedReviewRequestFact({
|
|
1211
|
-
runDir,
|
|
1212
|
-
});
|
|
1213
|
-
if (reviewRequestFact) {
|
|
1214
|
-
state.terminalReason = "review-request-changes";
|
|
1215
|
-
try {
|
|
1216
|
-
await writeFrontendReviewFindingsPrd({
|
|
1217
|
-
runDir,
|
|
1218
|
-
parentRunId: state.runId,
|
|
1219
|
-
fact: reviewRequestFact,
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
|
-
catch (error) {
|
|
1223
|
-
// The PRD draft is additive; a write failure must not mask the
|
|
1224
|
-
// not-passed verdict. The terminal aggregation below still applies.
|
|
1225
|
-
console.warn(`[run-dag] warning: failed to write frontend review findings PRD: ${error instanceof Error ? error.message : String(error)}`);
|
|
1226
|
-
}
|
|
1227
|
-
if (!recoveryTrigger && !skipFrontendRecovery) {
|
|
1228
|
-
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1229
|
-
if (continuationCount < frontendRecoveryQuota) {
|
|
1230
|
-
recoveryTrigger = {
|
|
1231
|
-
requestId: state.runId,
|
|
1232
|
-
failureSource: "frontend-review-pi",
|
|
1233
|
-
failureOwner: classifyReviewIssueCategoryFailureOwner(reviewRequestFact.issueCategory),
|
|
1234
|
-
protocolFailureReason: `frontend review request_review_changes (issueCategory=${reviewRequestFact.issueCategory})`,
|
|
1235
|
-
recoveryMode: "new-dag",
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
1004
|
const finishedCount = Object.values(state.nodes).filter((n) => n.status === "FINISHED").length;
|
|
1241
1005
|
const { supersededIds, supersededFailures } = collectSupersededIntermediateFailures(state);
|
|
1242
1006
|
if (state.convergence && supersededFailures.length > 0) {
|
|
@@ -1252,13 +1016,7 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
|
|
|
1252
1016
|
if (effectiveErrorCount === 0 &&
|
|
1253
1017
|
successTerminal &&
|
|
1254
1018
|
finishedCount + conditionSkippedCount + supersededErrorCount === taskCount) {
|
|
1255
|
-
|
|
1256
|
-
// rejected the implementation; the run must never surface as finished
|
|
1257
|
-
// even though every node technically FINISHED.
|
|
1258
|
-
state.status =
|
|
1259
|
-
state.terminalReason === "review-request-changes"
|
|
1260
|
-
? "partial_failed"
|
|
1261
|
-
: "finished";
|
|
1019
|
+
state.status = "finished";
|
|
1262
1020
|
}
|
|
1263
1021
|
else if (finishedCount > 0) {
|
|
1264
1022
|
state.status = "partial_failed";
|
|
@@ -1266,84 +1024,19 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
|
|
|
1266
1024
|
else {
|
|
1267
1025
|
state.status = "failed";
|
|
1268
1026
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
* partition is frozen via `computeFrontendRecoveryPlanForSource` and
|
|
1281
|
-
* serialized under the run dir's bounded `.runtime/` namespace. `planHash` is
|
|
1282
|
-
* the second idempotency dimension of `actionParams`.
|
|
1283
|
-
*/
|
|
1284
|
-
async function createRecoveryOperation(input) {
|
|
1285
|
-
const { cwd, runDir, spec, state, trigger } = input;
|
|
1286
|
-
const recoveryLease = await acquireDagRecoveryLease({
|
|
1287
|
-
cwd,
|
|
1288
|
-
parentRunId: state.runId,
|
|
1289
|
-
requestId: trigger.requestId,
|
|
1290
|
-
kind: "auto-frontend-recovery",
|
|
1291
|
-
});
|
|
1292
|
-
if (recoveryLease.kind === "held") {
|
|
1293
|
-
return { kind: "lease-held", lease: recoveryLease.lease };
|
|
1294
|
-
}
|
|
1295
|
-
const { computeFrontendRecoveryPlanForSource } = await import("./frontend-recovery-plan.js");
|
|
1296
|
-
const plan = computeFrontendRecoveryPlanForSource({
|
|
1297
|
-
spec,
|
|
1298
|
-
parentRunId: state.runId,
|
|
1299
|
-
requestId: trigger.requestId,
|
|
1300
|
-
failureSource: trigger.failureSource,
|
|
1301
|
-
});
|
|
1302
|
-
const planHash = createHash("sha256")
|
|
1303
|
-
.update(JSON.stringify(plan))
|
|
1304
|
-
.digest("hex");
|
|
1305
|
-
const continuationPath = path.join(runDir, ".runtime", "frontend-recovery", `${trigger.requestId}.json`);
|
|
1306
|
-
await writeJsonAtomic(continuationPath, {
|
|
1307
|
-
schemaVersion: 1,
|
|
1308
|
-
parentRunId: state.runId,
|
|
1309
|
-
requestId: trigger.requestId,
|
|
1310
|
-
failureSource: trigger.failureSource,
|
|
1311
|
-
recoveryMode: trigger.recoveryMode,
|
|
1312
|
-
plan,
|
|
1313
|
-
});
|
|
1314
|
-
const created = await input.deps.store.create({
|
|
1315
|
-
clientRequestId: `dag-recovery-${state.runId}`,
|
|
1316
|
-
action: "dagRunTask",
|
|
1317
|
-
actionParams: {
|
|
1318
|
-
parentRunId: state.runId,
|
|
1319
|
-
requestId: trigger.requestId,
|
|
1320
|
-
failureSource: trigger.failureSource,
|
|
1321
|
-
failureOwner: trigger.failureOwner,
|
|
1322
|
-
protocolFailureReason: trigger.protocolFailureReason,
|
|
1323
|
-
recoveryMode: trigger.recoveryMode,
|
|
1324
|
-
planHash,
|
|
1325
|
-
},
|
|
1326
|
-
cliArgs: [
|
|
1327
|
-
"dag",
|
|
1328
|
-
"execute",
|
|
1329
|
-
"--dag",
|
|
1330
|
-
continuationPath,
|
|
1331
|
-
"--cwd",
|
|
1332
|
-
cwd,
|
|
1333
|
-
"--json",
|
|
1334
|
-
],
|
|
1335
|
-
});
|
|
1336
|
-
if (created.kind !== "conflict") {
|
|
1337
|
-
// The durable lease must name the operation before controller execution.
|
|
1338
|
-
// A crash in this narrow window leaves a safely blocked reservation rather
|
|
1339
|
-
// than a second process that can launch another recovery DAG.
|
|
1340
|
-
await bindDagRecoveryLeaseExecution({
|
|
1341
|
-
leasePath: recoveryLease.leasePath,
|
|
1342
|
-
lease: recoveryLease.lease,
|
|
1343
|
-
executionRef: created.operation.operationId,
|
|
1344
|
-
});
|
|
1027
|
+
// Terminal recovery bookkeeping for a child attempt: a child that finished
|
|
1028
|
+
// recovered settles its own lineage; any other child terminal settles as
|
|
1029
|
+
// auto-recovery-blocked (phase 3 performs no further recovery).
|
|
1030
|
+
const recovery = state.frontendRecoveryState;
|
|
1031
|
+
if (recovery && recovery.attemptIndex >= 1) {
|
|
1032
|
+
if (state.status === "finished") {
|
|
1033
|
+
settleFrontendRecovery(state, "recovered");
|
|
1034
|
+
}
|
|
1035
|
+
else if (!state.frontendRecoveryResult) {
|
|
1036
|
+
settleFrontendRecovery(state, "auto-recovery-blocked");
|
|
1037
|
+
}
|
|
1345
1038
|
}
|
|
1346
|
-
return
|
|
1039
|
+
return { recoveryPending };
|
|
1347
1040
|
}
|
|
1348
1041
|
function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
|
|
1349
1042
|
if (rankWriterNodeIds.length <= 1)
|