@tea-agent/loop-agent 0.41.0-next.3 → 0.41.1-beta.0
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 +264 -32
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/task-lifecycle/advance.js +24 -5
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +3784 -231
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +945 -217
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +7 -0
- package/dist/task/config-types.js +34 -7
- package/dist/task/contract/adopt.js +4 -0
- package/dist/task/contract/import-revision.js +4 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +203 -20
- package/dist/task/runtime.js +5 -2
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +15 -9
- package/dist/task/source-prepare/prepare.js +89 -1
- package/dist/task/source-prepare/semantic-intake.js +6 -2
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/pi-runtime.js +2 -3
- package/dist/worker/console/chat/routes.js +3 -12
- package/dist/worker/console/chat/semantic-activity.js +11 -12
- package/dist/worker/console/chat/session-stats.js +0 -96
- package/dist/worker/console/chat/turn-process.js +4 -28
- 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 +169 -7
- package/dist/worker/console/pi-plugins.js +1 -3
- package/dist/worker/console/server.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DwVJxUxz.js → abnfDiagram-N423BO3Z-Bho4we4c.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DqfCcXdu.js → arc-TBDkTeK0.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-qMVtggRv.js → architectureDiagram-T3A2C74G-BF_fZrn2.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-ECrV38dh.js → blockDiagram-VBNYF7ZC-CCnPgUe2.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DBaaLqQv.js → c4Diagram-5PPSVZJV-BCYZJLAT.js} +1 -1
- package/dist/worker/console/static/assets/channel-bnXW0U3C.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BvfNHkAz.js → chunk-2GRJ4B5K-D8K6adhU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5StiIbc.js → chunk-2Q5K7J3B-DkT1Q78b.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-HabGiJkS.js → chunk-5RXB4S5H-DuhvpIr7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BiTg127i.js → chunk-5VM5RSS4-BnK6dC49.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CZsXBPuR.js → chunk-6Q2QTUOP-DPz__z7r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D-r1MxLK.js → chunk-GF5L2VYU-0kkppM2j.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DQxb9plr.js → chunk-JWPE2WC7-BB41iK5t.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMndGvCT.js → chunk-KBJHAD2P-CiVrxKHU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ByL4BgOJ.js → chunk-RYQCIY6F-C2h9Xje1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVe1b1gL.js → chunk-XXDRQBXY-3iOSHdbM.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BAyGzN8j.js → cose-bilkent-JH36ORCC-BN3A7tkk.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C5yVYU7E.js → cynefin-VYW2F7L2-CTuBy0Or.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-x-MwsEm9.js → cynefinDiagram-MW4NZA55-wb6tUlMN.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CT5Tle5F.js → dagre-VZM6K2ZE-GP1Ivx8B.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BARERhlS.js → diagram-7IWD3JNH-ejrcK6L0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BwfUz0VM.js → diagram-B4RE2ZJO-YFkCCvUU.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Mk4r2WvJ.js → diagram-LBJQPF4R-B4rrvIaL.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-KUrxuBs0.js → diagram-Q27KOJAE-C-YIZcUf.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CjZOceZn.js → diagram-UB23O5K3-BPF5wcL1.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-rnwUCiEI.js → ebnfDiagram-BXEA7PRR-CU5SzXjO.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DM4j23FM.js → erDiagram-JOGREHBK-CpL414DE.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DD5HdDEs.js → flowDiagram-UKHOOZJN-MrhLzNau.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CIAGSF47.js → ganttDiagram-PKOTCBZU-Bg4qFuUo.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DbwFbVEL.js → gitGraphDiagram-DS77QQ5N-ColxSXLR.js} +1 -1
- package/dist/worker/console/static/assets/index-D3CC4eUz.css +1 -0
- package/dist/worker/console/static/assets/index-DLt4ZFvD.js +437 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-B-Pb0T66.js → infoDiagram-6WML65LV-Czvtm6gc.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B9Unk-YX.js → ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C2F3JkPt.js → journeyDiagram-NVQOT4AX-DWYZcvph.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Bl8-Xcoz.js → kanban-definition-27J2QSJJ-KTLZRUjx.js} +1 -1
- package/dist/worker/console/static/assets/{linear-yK4uZmC5.js → linear-_Exn7bfl.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Bpa6PmuE.js → mermaid.core-Cxg8kX8V.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BfGRoCSe.js → mindmap-definition-FAOFIHXS-oKrlo2Bt.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bz6_HtST.js → pegDiagram-VL7TDLO6-XAeteiNd.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-i0Umbsby.js → pieDiagram-7S7Q4E2Y-BTf1K-lp.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DaVQBobe.js → quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DMTGnSq7.js → railroadDiagram-AXF67PYL-BVbKtZhW.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-2ao_DaSq.js → requirementDiagram-LRYGKXZP-C5e4JFcr.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-2SG1lTB9.js → sankeyDiagram-W5VNT64P-Cz30hc29.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BQYOLybR.js → sequenceDiagram-SI44F4Z6-DS33n6d0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C8g3yjuI.js → sizeCapture-X5ZJPWSS-46w7v-9T.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-q_F8nAQG.js → stateDiagram-OKZ733FA-pbcD5SsZ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-8LrWpH2s.js → swimlanes-SLNWSIFB-DuivYBSj.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BQoY3qzl.js → timeline-definition-Z64GVDOM-Cb9VZVIl.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BDu1UjSp.js → vennDiagram-T6HMQDX7-D2-mfOgR.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C48M7Y0a.js → wardleyDiagram-T6FBY63Y-DeS2aC76.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YU6FTl_F.js → xychartDiagram-ELKLHX3M-WGsrFjxH.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/app/useRunProgress.js +2 -9
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +0 -6
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +3 -28
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +0 -106
- package/dist/worker/console/workspace-context.js +111 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observability/read-model.js +0 -4
- package/dist/worker/observe/static/dag-node-purpose.js +5 -0
- package/dist/worker/observe/static/views/dag-inspector.js +7 -2
- package/dist/worker/observe/static/views/session-timeline.js +15 -46
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +932 -162
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-read-budgets.js +12 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +1 -432
- package/dist/workflows/dag/frontend-review-context.js +261 -15
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +427 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +249 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +285 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +685 -494
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +732 -4
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +256 -1
- package/dist/workflows/dag/rerun-plan.js +10 -2
- package/dist/workflows/dag/rerun-run.js +23 -0
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +214 -104
- package/dist/workflows/dag/runner.js +429 -122
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/types.js +239 -12
- package/dist/workflows/dag/validate.js +28 -12
- package/docs/init-surface.manifest.json +30 -12
- package/docs/skills/vetted-skill-registry.md +4 -2
- package/docs/templates/README.md +2 -0
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +4 -1
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +15 -14
- package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
- package/skills/frontend-contract/SKILL.md +23 -0
- package/skills/frontend-contract/references/contract-protocol.md +34 -0
- package/skills/frontend-design-review/SKILL.md +22 -41
- package/skills/frontend-plan/SKILL.md +26 -0
- package/skills/frontend-plan/references/decision-contract.md +37 -0
- package/skills/frontend-plan/references/design-decisions.md +17 -0
- package/skills/frontend-review/SKILL.md +20 -15
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/frontend-scout/SKILL.md +25 -0
- package/skills/frontend-scout/references/design-evidence.md +16 -0
- package/skills/frontend-scout/references/scout-evidence.md +23 -0
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/channel-CL1NRAmS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/index-BfleX-qj.css +0 -1
- package/dist/worker/console/static/assets/index-Bih9w9h9.js +0 -451
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrxGBDJu.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BhpDxzAf.js +0 -8
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +0 -22
- package/skills/frontend-implementation/SKILL.md +0 -52
- package/skills/frontend-implementation/references/code-standards.md +0 -33
- package/skills/frontend-implementation/references/design-spec.md +0 -56
- package/skills/frontend-implementation/references/node-contracts.md +0 -31
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
3
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
4
4
|
import { hostname } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -7,7 +7,7 @@ import { isHardBudgetBreached, resolveEffectiveMaxConcurrent, } from "../../appl
|
|
|
7
7
|
import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
|
|
8
8
|
import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
|
|
9
9
|
import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
|
|
10
|
-
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun,
|
|
10
|
+
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
|
|
11
11
|
import { markInterruptNeedsReconcile, mergeAbortSignals, readInterruptRequest, runnerIdentityFromState, settleDagInterrupt, startInterruptWatcher, } from "./interrupt-request.js";
|
|
12
12
|
import { moveToCompletedRunDir, moveToPausedRunDir, prepareRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
|
|
13
13
|
import { evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
@@ -26,7 +26,12 @@ import { runConvergencePassController, } from "./convergence/controller.js";
|
|
|
26
26
|
import { executeDagRanksOnce, FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isConditionSkippedReason, readFrontendPrewriteResult, } from "./scheduler.js";
|
|
27
27
|
import { topoSortToRanks } from "./topo.js";
|
|
28
28
|
import { isFrontendWriterTransientPartialWrite } from "./frontend-writer-recovery.js";
|
|
29
|
+
import { classifyReviewIssueCategoryFailureOwner, readCommittedDesignRequestFact, readCommittedReviewRequestFact, writeFrontendReviewFindingsPrd, } from "./frontend-review-findings.js";
|
|
30
|
+
import { bindFrontendShapeStateToRunTopology, commitFrontendShapeTransitionCas, commitRuntimeFrontendShapeFact, isCanonicalFrontendShapeDag, } from "./frontend-shape-facts.js";
|
|
31
|
+
import { assertFrontendTopologyBinding, buildFrontendTopologyBinding, computeFrontendShapeSourceDigest, } from "./frontend-shape.js";
|
|
32
|
+
import { ensureFrontendShapeCapsuleRecord } from "./frontend-shape-capsule-store.js";
|
|
29
33
|
import { parseDagSpec, resolveModelForTask, } from "./types.js";
|
|
34
|
+
import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
|
|
30
35
|
import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
|
|
31
36
|
import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
|
|
32
37
|
import { executeDynamicMapExpansion } from "./dynamic-runtime/map.js";
|
|
@@ -46,6 +51,51 @@ export function toKebabSlug(text, maxLen = 40) {
|
|
|
46
51
|
.replace(/^-|-$/g, "")
|
|
47
52
|
.slice(0, maxLen) || "dag");
|
|
48
53
|
}
|
|
54
|
+
export async function loadCanonicalFrontendTaskSourceBinding(cwd, spec) {
|
|
55
|
+
const binding = spec.sourceBinding;
|
|
56
|
+
if (!binding?.taskId)
|
|
57
|
+
throw new Error("canonical frontend DAG requires sourceBinding.taskId");
|
|
58
|
+
const requirement = binding.sources.find((source) => source.kind === "requirement");
|
|
59
|
+
if (!requirement)
|
|
60
|
+
throw new Error("canonical frontend DAG requires a bound requirement source");
|
|
61
|
+
const constraint = binding.sources.find((source) => source.kind === "constraint");
|
|
62
|
+
const readFreshSource = async (source) => {
|
|
63
|
+
let bytes;
|
|
64
|
+
try {
|
|
65
|
+
bytes = await readFile(path.resolve(cwd, source.path));
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`canonical frontend ${source.kind} source freshness read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
69
|
+
}
|
|
70
|
+
const actualSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
71
|
+
if (actualSha256 !== source.sha256) {
|
|
72
|
+
throw new Error(`canonical frontend ${source.kind} source sha256 drifted: expected ${source.sha256}, received ${actualSha256}`);
|
|
73
|
+
}
|
|
74
|
+
return bytes.toString("utf8");
|
|
75
|
+
};
|
|
76
|
+
const requirementMarkdown = await readFreshSource(requirement);
|
|
77
|
+
const constraintMarkdown = constraint ? await readFreshSource(constraint) : "";
|
|
78
|
+
return {
|
|
79
|
+
taskId: binding.taskId,
|
|
80
|
+
requirementMarkdown,
|
|
81
|
+
constraintMarkdown,
|
|
82
|
+
sourceDigest: computeFrontendShapeSourceDigest({ requirementMarkdown, constraintMarkdown }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function ensureFreshFrontendShapeCapsuleRecord(input) {
|
|
86
|
+
const freshSource = await loadCanonicalFrontendTaskSourceBinding(input.cwd, input.spec);
|
|
87
|
+
if (freshSource.taskId !== input.canonicalSource.taskId ||
|
|
88
|
+
freshSource.sourceDigest !== input.canonicalSource.sourceDigest) {
|
|
89
|
+
throw new Error("canonical frontend task source freshness binding drifted before transition commit");
|
|
90
|
+
}
|
|
91
|
+
await (input.ensureRecord ?? ensureFrontendShapeCapsuleRecord)({
|
|
92
|
+
repoRoot: input.cwd,
|
|
93
|
+
taskId: input.canonicalSource.taskId,
|
|
94
|
+
parentRunId: input.parentRunId,
|
|
95
|
+
terminalOutcome: input.terminalOutcome,
|
|
96
|
+
capsule: input.capsule,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
49
99
|
export function extractTaskIdFromDagSpec(spec) {
|
|
50
100
|
if (spec.objective) {
|
|
51
101
|
const match = spec.objective.match(TASK_ID_IN_TEXT_PATTERN);
|
|
@@ -180,6 +230,15 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
180
230
|
...(opts.workerAssociation
|
|
181
231
|
? { workerAssociation: structuredClone(opts.workerAssociation) }
|
|
182
232
|
: {}),
|
|
233
|
+
...(isCanonicalFrontendShapeDag(new Map(spec.tasks.map((task) => [task.id, task])))
|
|
234
|
+
? {
|
|
235
|
+
frontendShapeState: {
|
|
236
|
+
frozenTopology: buildFrontendTopologyBinding(spec.tasks.map((task) => task.id)),
|
|
237
|
+
revision: 0,
|
|
238
|
+
committedFacts: {},
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
: {}),
|
|
183
242
|
};
|
|
184
243
|
initRunBudgetLedger(state, spec.budget);
|
|
185
244
|
return state;
|
|
@@ -301,6 +360,7 @@ export async function runDag(spec, opts) {
|
|
|
301
360
|
activeRunDir,
|
|
302
361
|
completedRunDir,
|
|
303
362
|
pausedRunDir,
|
|
363
|
+
recovery: opts.recovery,
|
|
304
364
|
});
|
|
305
365
|
}
|
|
306
366
|
finally {
|
|
@@ -309,6 +369,16 @@ export async function runDag(spec, opts) {
|
|
|
309
369
|
export async function runDagContinuation(opts) {
|
|
310
370
|
const { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), opts.spec.budget);
|
|
311
371
|
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
|
+
}
|
|
312
382
|
state.status = "running";
|
|
313
383
|
const resumedAt = new Date().toISOString();
|
|
314
384
|
state.runner = {
|
|
@@ -333,6 +403,7 @@ export async function runDagContinuation(opts) {
|
|
|
333
403
|
activeRunDir,
|
|
334
404
|
completedRunDir,
|
|
335
405
|
pausedRunDir,
|
|
406
|
+
recovery: opts.recovery,
|
|
336
407
|
});
|
|
337
408
|
}
|
|
338
409
|
export async function resumeDagRun(opts) {
|
|
@@ -435,9 +506,13 @@ export async function resumeDagRun(opts) {
|
|
|
435
506
|
}
|
|
436
507
|
async function executeDagCheckpoint(input) {
|
|
437
508
|
const { spec, state, ranks, runDir: initialRunDir, cwd, maxConcurrent, completedRunDir, pausedRunDir, } = input;
|
|
509
|
+
bindFrontendShapeStateToRunTopology(state);
|
|
438
510
|
let runDir = initialRunDir;
|
|
439
511
|
let stateWriteQueue = Promise.resolve();
|
|
512
|
+
let shapeTransitionBarrier;
|
|
440
513
|
const persistState = async (options) => {
|
|
514
|
+
if (shapeTransitionBarrier)
|
|
515
|
+
await shapeTransitionBarrier;
|
|
441
516
|
stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, state, options));
|
|
442
517
|
await stateWriteQueue;
|
|
443
518
|
};
|
|
@@ -562,12 +637,50 @@ async function executeDagCheckpoint(input) {
|
|
|
562
637
|
.map((rank) => rank.filter((nodeId) => nodeId === "closeout-pi"))
|
|
563
638
|
.filter((rank) => rank.length > 0)
|
|
564
639
|
: [];
|
|
640
|
+
const canonicalFrontendSource = isCanonicalFrontendShapeDag(tasksById)
|
|
641
|
+
? await loadCanonicalFrontendTaskSourceBinding(cwd, spec)
|
|
642
|
+
: undefined;
|
|
565
643
|
const executeRanks = (selectedRanks) => executeDagRanksOnce({
|
|
566
644
|
state,
|
|
567
645
|
ranks: selectedRanks,
|
|
568
646
|
tasksById,
|
|
569
647
|
maxConcurrent,
|
|
570
648
|
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
|
+
},
|
|
571
684
|
runDir,
|
|
572
685
|
abortSignal: input.abortSignal,
|
|
573
686
|
createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
|
|
@@ -603,6 +716,18 @@ async function executeDagCheckpoint(input) {
|
|
|
603
716
|
});
|
|
604
717
|
const node = state.nodes[nodeId];
|
|
605
718
|
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
|
+
}
|
|
606
731
|
recordFinishedNodeBudget(state, node);
|
|
607
732
|
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
608
733
|
await persistState();
|
|
@@ -612,6 +737,9 @@ async function executeDagCheckpoint(input) {
|
|
|
612
737
|
let pausedByNodeId;
|
|
613
738
|
while (true) {
|
|
614
739
|
pausedByNodeId = await executeRanks(convergenceRanks);
|
|
740
|
+
if (state.frontendShapeState?.terminalOutcome) {
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
615
743
|
if (pausedByNodeId) {
|
|
616
744
|
break;
|
|
617
745
|
}
|
|
@@ -667,7 +795,7 @@ async function executeDagCheckpoint(input) {
|
|
|
667
795
|
runDir = await moveToPausedRunDir(runDir, pausedRunDir);
|
|
668
796
|
}
|
|
669
797
|
else {
|
|
670
|
-
const { recoveryPending } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
|
|
798
|
+
const { recoveryPending, recovery: recoveryTrigger } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd, spec.frontendRecovery?.maxContinuations ?? 1);
|
|
671
799
|
await finalizeRunOwnedInterrupt({
|
|
672
800
|
runDir,
|
|
673
801
|
state,
|
|
@@ -675,52 +803,65 @@ async function executeDagCheckpoint(input) {
|
|
|
675
803
|
interruptAborted: interruptController.signal.aborted,
|
|
676
804
|
});
|
|
677
805
|
await persistState();
|
|
678
|
-
if (recoveryPending) {
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
806
|
+
if (recoveryPending && input.recovery && recoveryTrigger) {
|
|
807
|
+
// M6 recovery operation controller path: construct/reuse one recovery
|
|
808
|
+
// operation keyed by a deterministic clientRequestId and supervise it
|
|
809
|
+
// to terminal, instead of staging an in-process child run.
|
|
810
|
+
const created = await createRecoveryOperation({
|
|
811
|
+
cwd,
|
|
812
|
+
runDir,
|
|
813
|
+
spec,
|
|
814
|
+
state,
|
|
815
|
+
trigger: recoveryTrigger,
|
|
816
|
+
deps: input.recovery,
|
|
817
|
+
});
|
|
818
|
+
if (created.kind === "conflict") {
|
|
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,
|
|
691
839
|
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
|
+
},
|
|
692
848
|
});
|
|
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
|
-
});
|
|
849
|
+
state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, result.terminalReason === "succeeded"
|
|
850
|
+
? "recovered"
|
|
851
|
+
: "auto-recovery-blocked");
|
|
711
852
|
}
|
|
712
853
|
}
|
|
713
854
|
await notifyRunObserver(input.observer, "onRunFinish", state);
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
runDir = await moveToCompletedRunDir(runDir, completedRunDir);
|
|
855
|
+
// Parent is always archived to completed/ — no active/ retention for an
|
|
856
|
+
// in-process child CAS anymore.
|
|
857
|
+
try {
|
|
858
|
+
const terminalCheckpoint = await captureWorkspaceCheckpoint(cwd);
|
|
859
|
+
await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL, terminalCheckpoint);
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
|
|
723
863
|
}
|
|
864
|
+
runDir = await moveToCompletedRunDir(runDir, completedRunDir);
|
|
724
865
|
}
|
|
725
866
|
await relocateRunArtifactPaths({
|
|
726
867
|
runDir,
|
|
@@ -853,39 +994,21 @@ function isSuccessfulConvergenceTerminal(state) {
|
|
|
853
994
|
return true;
|
|
854
995
|
return SUCCESSFUL_CONVERGENCE_TERMINAL_REASONS.has(reason);
|
|
855
996
|
}
|
|
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
997
|
function buildCandidateContractInvalidResult(state, result, artifactHash) {
|
|
874
|
-
const recovery = state.frontendRecoveryState;
|
|
875
998
|
return {
|
|
876
999
|
schemaVersion: 1,
|
|
877
1000
|
outcome: "candidate-contract-invalid",
|
|
878
1001
|
origin: {
|
|
879
1002
|
kind: "frontend-prewrite-gate",
|
|
880
|
-
parentRunId:
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
failedNodeId: result.selectedPlanNodeId,
|
|
1003
|
+
parentRunId: state.runId,
|
|
1004
|
+
requestId: state.runId,
|
|
1005
|
+
failedNodeId: result.failureSource ?? "frontend-plan-pi",
|
|
884
1006
|
},
|
|
885
1007
|
failureClass: {
|
|
886
1008
|
code: "candidate-contract-invalid",
|
|
887
1009
|
classification: result.classification,
|
|
888
|
-
reason: result.
|
|
1010
|
+
reason: result.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ") ??
|
|
1011
|
+
"candidate contract invalid",
|
|
889
1012
|
},
|
|
890
1013
|
evidenceRefs: [
|
|
891
1014
|
{
|
|
@@ -897,15 +1020,13 @@ function buildCandidateContractInvalidResult(state, result, artifactHash) {
|
|
|
897
1020
|
};
|
|
898
1021
|
}
|
|
899
1022
|
function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
|
|
900
|
-
const recovery = state.frontendRecoveryState;
|
|
901
1023
|
return {
|
|
902
1024
|
schemaVersion: 1,
|
|
903
1025
|
outcome: "auto-recovery-blocked",
|
|
904
1026
|
origin: {
|
|
905
1027
|
kind: "frontend-writer",
|
|
906
|
-
parentRunId:
|
|
907
|
-
|
|
908
|
-
requestId: recovery?.requestId ?? "",
|
|
1028
|
+
parentRunId: state.runId,
|
|
1029
|
+
requestId: state.runId,
|
|
909
1030
|
failedNodeId,
|
|
910
1031
|
},
|
|
911
1032
|
failureClass: {
|
|
@@ -916,63 +1037,115 @@ function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
|
|
|
916
1037
|
evidenceRefs: [],
|
|
917
1038
|
};
|
|
918
1039
|
}
|
|
919
|
-
function
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
failedNodeId: "",
|
|
934
|
-
},
|
|
935
|
-
evidenceRefs: [],
|
|
936
|
-
};
|
|
937
|
-
}
|
|
1040
|
+
function buildRecoveryResultForTrigger(state, trigger, outcome) {
|
|
1041
|
+
return {
|
|
1042
|
+
schemaVersion: 1,
|
|
1043
|
+
outcome,
|
|
1044
|
+
origin: {
|
|
1045
|
+
kind: trigger.failureSource === "frontend-implement-pi"
|
|
1046
|
+
? "frontend-writer"
|
|
1047
|
+
: "frontend-prewrite-gate",
|
|
1048
|
+
parentRunId: state.runId,
|
|
1049
|
+
requestId: trigger.requestId,
|
|
1050
|
+
failedNodeId: trigger.failureSource,
|
|
1051
|
+
},
|
|
1052
|
+
evidenceRefs: [],
|
|
1053
|
+
};
|
|
938
1054
|
}
|
|
939
|
-
export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
1055
|
+
export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, frontendRecoveryQuota = 1) {
|
|
940
1056
|
const repoRoot = cwd ?? path.resolve(runDir, "..", "..", "..", "..");
|
|
1057
|
+
if (state.frontendShapeState?.terminalOutcome &&
|
|
1058
|
+
state.frontendShapeState.transitionCapsule) {
|
|
1059
|
+
state.status = "failed";
|
|
1060
|
+
return { recoveryPending: false };
|
|
1061
|
+
}
|
|
941
1062
|
// Terminal override: a denied frontend writer must never surface as a
|
|
942
1063
|
// mechanical partial_failed just because prewrite FINISHED while the writer
|
|
943
|
-
// was SKIPPED. Fail closed on
|
|
944
|
-
let
|
|
1064
|
+
// was SKIPPED. Fail closed on blocked before aggregation.
|
|
1065
|
+
let recoveryTrigger;
|
|
945
1066
|
const skipFrontendRecovery = state.failureCategory === "controller-interrupted";
|
|
946
1067
|
const hasFrontendWriter = Object.keys(state.nodes).some((id) => FRONTEND_WRITER_NODE_IDS.includes(id));
|
|
947
1068
|
if (hasFrontendWriter && !skipFrontendRecovery) {
|
|
948
1069
|
const admission = await readFrontendPrewriteResult(runDir);
|
|
949
1070
|
if (admission.ok) {
|
|
950
|
-
|
|
1071
|
+
// M8: admission has only admitted|blocked. A blocked admission whose
|
|
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) {
|
|
951
1095
|
state.status = "failed";
|
|
952
1096
|
return { recoveryPending: false };
|
|
953
1097
|
}
|
|
954
|
-
if (
|
|
1098
|
+
if (designRejected && designRequestFact) {
|
|
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) {
|
|
955
1122
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
956
|
-
if (continuationCount >=
|
|
957
|
-
// Continuation quota exhausted: a second
|
|
1123
|
+
if (continuationCount >= frontendRecoveryQuota) {
|
|
1124
|
+
// Continuation quota exhausted: a second contract-invalid candidate
|
|
958
1125
|
// is authoritative candidate-contract-invalid → forced failed.
|
|
959
1126
|
state.frontendRecoveryResult = buildCandidateContractInvalidResult(state, admission.result, admission.artifactHash);
|
|
960
1127
|
state.status = "failed";
|
|
961
1128
|
return { recoveryPending: false };
|
|
962
1129
|
}
|
|
963
|
-
// Root continuation still available:
|
|
964
|
-
//
|
|
965
|
-
//
|
|
966
|
-
|
|
967
|
-
|
|
1130
|
+
// Root continuation still available: emit a recovery trigger for the
|
|
1131
|
+
// M6 operation controller. The parent no longer writes
|
|
1132
|
+
// frontendRecoveryState or stays in active/ for a child CAS.
|
|
1133
|
+
recoveryTrigger = {
|
|
1134
|
+
requestId: state.runId,
|
|
1135
|
+
failureSource: admission.result.failureSource ?? "frontend-plan-pi",
|
|
1136
|
+
failureOwner: "plan",
|
|
1137
|
+
protocolFailureReason: "frontend candidate contract invalid",
|
|
1138
|
+
recoveryMode: "new-dag",
|
|
1139
|
+
};
|
|
968
1140
|
}
|
|
969
1141
|
}
|
|
970
1142
|
}
|
|
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
|
-
|
|
1143
|
+
// Phase 5: writer transient partial write → rollback + recovery trigger.
|
|
1144
|
+
// Only for frontend-implementation writers with a remaining root
|
|
1145
|
+
// continuation, and only when the rollback journal (captured before the
|
|
1146
|
+
// provider call) restores cleanly; otherwise the run stays failed with
|
|
1147
|
+
// auto-recovery-blocked.
|
|
1148
|
+
if (hasFrontendWriter && !recoveryTrigger && !skipFrontendRecovery) {
|
|
976
1149
|
const writerNodeId = FRONTEND_WRITER_NODE_IDS.find((id) => {
|
|
977
1150
|
const node = state.nodes[id];
|
|
978
1151
|
return (node &&
|
|
@@ -983,15 +1156,20 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
983
1156
|
});
|
|
984
1157
|
if (writerNodeId) {
|
|
985
1158
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
986
|
-
if (continuationCount <
|
|
1159
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
987
1160
|
const { rollbackFrontendWriter } = await import("./frontend-writer-recovery.js");
|
|
988
1161
|
const rollback = await rollbackFrontendWriter({
|
|
989
1162
|
cwd: repoRoot,
|
|
990
1163
|
parentRunId: state.runId,
|
|
991
1164
|
});
|
|
992
1165
|
if (rollback.ok) {
|
|
993
|
-
|
|
994
|
-
|
|
1166
|
+
recoveryTrigger = {
|
|
1167
|
+
requestId: state.runId,
|
|
1168
|
+
failureSource: "frontend-implement-pi",
|
|
1169
|
+
failureOwner: "implementation",
|
|
1170
|
+
protocolFailureReason: "frontend writer transient partial write",
|
|
1171
|
+
recoveryMode: "continuation",
|
|
1172
|
+
};
|
|
995
1173
|
}
|
|
996
1174
|
else {
|
|
997
1175
|
state.frontendRecoveryResult = buildWriterPartialWriteBlockedResult(state, writerNodeId, rollback.reason);
|
|
@@ -1001,6 +1179,64 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1001
1179
|
}
|
|
1002
1180
|
}
|
|
1003
1181
|
}
|
|
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
|
+
}
|
|
1004
1240
|
const finishedCount = Object.values(state.nodes).filter((n) => n.status === "FINISHED").length;
|
|
1005
1241
|
const { supersededIds, supersededFailures } = collectSupersededIntermediateFailures(state);
|
|
1006
1242
|
if (state.convergence && supersededFailures.length > 0) {
|
|
@@ -1016,7 +1252,13 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1016
1252
|
if (effectiveErrorCount === 0 &&
|
|
1017
1253
|
successTerminal &&
|
|
1018
1254
|
finishedCount + conditionSkippedCount + supersededErrorCount === taskCount) {
|
|
1019
|
-
|
|
1255
|
+
// Phase 7 guard: a committed `request_review_changes` means the review
|
|
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";
|
|
1020
1262
|
}
|
|
1021
1263
|
else if (finishedCount > 0) {
|
|
1022
1264
|
state.status = "partial_failed";
|
|
@@ -1024,19 +1266,84 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1024
1266
|
else {
|
|
1025
1267
|
state.status = "failed";
|
|
1026
1268
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1269
|
+
return recoveryTrigger
|
|
1270
|
+
? { recoveryPending: true, recovery: recoveryTrigger }
|
|
1271
|
+
: { recoveryPending: false };
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Construct (or reuse) the recovery operation for a failed frontend run.
|
|
1275
|
+
*
|
|
1276
|
+
* A parent-run atomic lease is acquired before `OperationStore.create`, then
|
|
1277
|
+
* its operation id is persisted into that lease before the controller may run.
|
|
1278
|
+
* `clientRequestId` remains a useful store-level replay key, but is not a
|
|
1279
|
+
* cross-process recovery admission boundary. The continuation DagSpec/reset
|
|
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
|
+
});
|
|
1038
1345
|
}
|
|
1039
|
-
return
|
|
1346
|
+
return created;
|
|
1040
1347
|
}
|
|
1041
1348
|
function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
|
|
1042
1349
|
if (rankWriterNodeIds.length <= 1)
|