@tea-agent/loop-agent 0.44.0-next.1 → 0.44.0-next.11
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 +379 -416
- package/dist/application/evaluation/budget.js +21 -0
- package/dist/application/evaluation/corpus-hash.js +10 -15
- package/dist/application/evaluation/corpus.js +2 -1
- package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
- package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
- package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
- package/dist/application/evaluation/frontend-pair-registration.js +143 -0
- package/dist/application/evaluation/frontend-paired-summary.js +150 -0
- package/dist/application/evaluation/frontend-run-observation.js +144 -0
- package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
- package/dist/application/evaluation/types.js +45 -12
- package/dist/application/task-lifecycle/observe.js +26 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +11 -0
- package/dist/cli/program.js +4 -0
- package/dist/commands/task-advance.js +3 -5
- package/dist/commands/task-source-prepare.js +13 -1
- package/dist/executors/dag-pi/guards.js +4 -0
- package/dist/executors/dag-pi/model-config.js +30 -0
- package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
- package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
- package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
- package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
- package/dist/executors/dag-pi/plan/record-tools.js +861 -0
- package/dist/executors/dag-pi/plan/schema.js +129 -0
- package/dist/executors/dag-pi/plan/session.js +1 -0
- package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
- package/dist/executors/dag-pi/sessions/index.js +6 -0
- package/dist/executors/dag-pi/sessions/plan-batches.js +280 -0
- package/dist/executors/dag-pi/sessions/plan-prompts.js +367 -0
- package/dist/executors/dag-pi/sessions/scout-parallel.js +197 -0
- package/dist/executors/dag-pi/sessions/segmented-plan.js +1184 -0
- package/dist/executors/dag-pi/sessions/writer-evidence.js +60 -0
- package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
- package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
- package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
- package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
- package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
- package/dist/executors/dag-pi-executor.js +733 -5782
- package/dist/executors/pi-executor.js +6 -1
- package/dist/executors/pi-sdk-executor.js +112 -19
- package/dist/executors/shell-executor.js +88 -12
- package/dist/executors/shell-presets.js +1 -1
- package/dist/executors/shell-write-guard.js +26 -3
- package/dist/infrastructure/console/operation-store.js +48 -14
- package/dist/infrastructure/evaluation/corpus-store.js +37 -12
- package/dist/shared/file-identity.js +23 -0
- package/dist/shared/git-workspace-status.js +482 -0
- package/dist/shared/operator/capabilities.js +10 -5
- package/dist/shared/path-safety.js +2 -2
- package/dist/shared/wizard-local-telemetry.js +51 -0
- package/dist/task/config-types.js +22 -8
- package/dist/task/contract/project.js +13 -0
- package/dist/task/contract/schema.js +2 -7
- package/dist/task/frontend-preflight.js +1 -1
- package/dist/task/source-prepare/parse-intent.js +124 -5
- package/dist/task/source-prepare/prepare.js +6 -1
- package/dist/task/source-prepare/semantic-intake.js +18 -3
- package/dist/task/source-prepare/source-execution.js +65 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
- package/dist/task/source-prepare/source-provider-budget.js +290 -0
- package/dist/worker/console/chat/assistant-content.js +5 -34
- package/dist/worker/console/chat/chat-event-store.js +62 -10
- package/dist/worker/console/chat/context-insights.js +723 -0
- package/dist/worker/console/chat/conversation-nodes.js +383 -0
- package/dist/worker/console/chat/db-connections.js +854 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
- package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
- package/dist/worker/console/chat/pi-runtime.js +445 -36
- package/dist/worker/console/chat/provider-error.js +0 -12
- package/dist/worker/console/chat/repo-browser.js +52 -4
- package/dist/worker/console/chat/routes.js +577 -72
- package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
- package/dist/worker/console/chat/session-store.js +147 -19
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/chat/timeline-message-order.js +22 -0
- package/dist/worker/console/chat/tools.js +34 -0
- package/dist/worker/console/chat/turn-process.js +237 -66
- package/dist/worker/console/chat/user-turn-outline.js +19 -0
- package/dist/worker/console/console-update-runtime.js +2 -2
- package/dist/worker/console/dag-confirmation.js +3 -0
- package/dist/worker/console/dag-execution-receipt.js +3 -0
- package/dist/worker/console/operator-actions.js +32 -3
- package/dist/worker/console/operator-user-error.js +28 -1
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/prd-intake-bridge.js +5 -12
- package/dist/worker/console/recovery-cta.js +14 -14
- package/dist/worker/console/server.js +5 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
- package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
- package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
- package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
- package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -1
- package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
- package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
- package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
- package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
- package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
- package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
- package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
- package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
- package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
- package/dist/worker/console/workspace-context.js +2 -0
- package/dist/worker/console/workspace-initialization.js +3 -3
- package/dist/worker/observability/read-model.js +23 -2
- package/dist/worker/observe/node-context-insights.js +479 -0
- package/dist/worker/observe/static/console-theme.css +174 -0
- package/dist/worker/observe/static/console-theme.js +36 -0
- package/dist/worker/observe/static/operator-chrome.css +85 -0
- package/dist/worker/observe/static/operator-chrome.js +51 -0
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
- package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
- package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
- package/dist/workflows/dag/budget-enforcement.js +31 -2
- package/dist/workflows/dag/checkpoint.js +686 -0
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-contract-facts.js +11 -8
- package/dist/workflows/dag/frontend-design-policy.js +6 -6
- package/dist/workflows/dag/frontend-durable-tools.js +90 -70
- package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
- package/dist/workflows/dag/frontend-plan-canary.js +66 -16
- package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
- package/dist/workflows/dag/frontend-plan-progress.js +92 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
- package/dist/workflows/dag/frontend-recovery-run.js +51 -46
- package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
- package/dist/workflows/dag/frontend-review-context.js +29 -1
- package/dist/workflows/dag/frontend-review-scopes.js +92 -2
- package/dist/workflows/dag/frontend-risk.js +8 -3
- package/dist/workflows/dag/frontend-root-observation.js +261 -0
- package/dist/workflows/dag/frontend-session-budget.js +52 -39
- package/dist/workflows/dag/frontend-session-context.js +10 -0
- package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
- package/dist/workflows/dag/frontend-verification-trace.js +6 -0
- package/dist/workflows/dag/frontend-writer-admission.js +3 -1
- package/dist/workflows/dag/hybrid/sources.js +1812 -0
- package/dist/workflows/dag/hybrid/templates/backend-test.js +1807 -0
- package/dist/workflows/dag/hybrid/templates/frontend-test.js +702 -0
- package/dist/workflows/dag/hybrid/templates/frontend.js +1529 -0
- package/dist/workflows/dag/hybrid/templates/index.js +8 -0
- package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +449 -0
- package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +495 -0
- package/dist/workflows/dag/hybrid/templates/shared.js +101 -0
- package/dist/workflows/dag/hybrid/templates/standard.js +265 -0
- package/dist/workflows/dag/hybrid/types.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +57 -7106
- package/dist/workflows/dag/node-execution.js +11 -11
- package/dist/workflows/dag/output-protocol.js +1 -0
- package/dist/workflows/dag/rerun-feedback.js +35 -4
- package/dist/workflows/dag/rerun-task.js +62 -66
- package/dist/workflows/dag/runner.js +38 -1159
- package/dist/workflows/dag/terminal-status.js +502 -0
- package/dist/workflows/dag/types.js +15 -8
- package/dist/workflows/loop/benchmark.js +11 -8
- package/docs/README.md +1 -1
- package/docs/architecture/dag-execution.md +1 -1
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/governance/harness-methodology-verification.md +11 -0
- package/docs/templates/backend-test-dag.json +5 -4
- package/docs/templates/frontend-implementation-contract.schema.json +31 -11
- package/package.json +6 -4
- package/skills/frontend-design-review/SKILL.md +8 -11
- package/skills/frontend-design-review/references/review-checklist.md +3 -4
- package/skills/frontend-review/SKILL.md +5 -1
- package/skills/loop-agent/references/command-reference.md +8 -0
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
- package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
- package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/** DAG runner 终态判定、前端恢复策略与恢复操作编排。含取证读取、lease 与 operation 创建;未拆成独立 policy/operation 文件。 */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
|
+
import { FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isConditionSkippedReason, readFrontendPrewriteResult } from "./scheduler.js";
|
|
7
|
+
import { isFrontendWriterTransientPartialWrite } from "./frontend-writer-recovery.js";
|
|
8
|
+
import { classifyReviewIssueCategoryFailureOwner, readCommittedDesignRequestFact, readCommittedReviewRequestFact, writeFrontendReviewFindingsPrd } from "./frontend-review-findings.js";
|
|
9
|
+
import { computeActionPayloadHash } from "../../infrastructure/console/operation-store.js";
|
|
10
|
+
import { classifyFailureOwner, failureOwnerSchema } from "./frontend-recovery-capsule.js";
|
|
11
|
+
import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, claimDagRecoveryLease } from "./recovery-lease.js";
|
|
12
|
+
export function buildCandidateContractInvalidResult(state, result, artifactHash) {
|
|
13
|
+
return {
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
outcome: "candidate-contract-invalid",
|
|
16
|
+
origin: {
|
|
17
|
+
kind: "frontend-prewrite-gate",
|
|
18
|
+
parentRunId: state.runId,
|
|
19
|
+
requestId: state.runId,
|
|
20
|
+
failedNodeId: result.failureSource ?? "frontend-plan-pi",
|
|
21
|
+
},
|
|
22
|
+
failureClass: {
|
|
23
|
+
code: "candidate-contract-invalid",
|
|
24
|
+
classification: result.classification,
|
|
25
|
+
reason: result.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ") ??
|
|
26
|
+
"candidate contract invalid",
|
|
27
|
+
},
|
|
28
|
+
evidenceRefs: [
|
|
29
|
+
{
|
|
30
|
+
runId: state.runId,
|
|
31
|
+
relativePath: FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT,
|
|
32
|
+
sha256: artifactHash,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
|
|
38
|
+
return {
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
outcome: "auto-recovery-blocked",
|
|
41
|
+
origin: {
|
|
42
|
+
kind: "frontend-writer",
|
|
43
|
+
parentRunId: state.runId,
|
|
44
|
+
requestId: state.runId,
|
|
45
|
+
failedNodeId,
|
|
46
|
+
},
|
|
47
|
+
failureClass: {
|
|
48
|
+
code: "writer-transient-partial-write",
|
|
49
|
+
classification: "transient",
|
|
50
|
+
reason,
|
|
51
|
+
},
|
|
52
|
+
evidenceRefs: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function buildRecoveryResultForTrigger(state, trigger, outcome) {
|
|
56
|
+
return {
|
|
57
|
+
schemaVersion: 1,
|
|
58
|
+
outcome,
|
|
59
|
+
origin: {
|
|
60
|
+
kind: trigger.failureSource === "frontend-implement-pi"
|
|
61
|
+
? "frontend-writer"
|
|
62
|
+
: "frontend-prewrite-gate",
|
|
63
|
+
parentRunId: state.runId,
|
|
64
|
+
requestId: trigger.requestId,
|
|
65
|
+
failedNodeId: trigger.failureSource,
|
|
66
|
+
},
|
|
67
|
+
evidenceRefs: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function collectSupersededIntermediateFailures(state) {
|
|
71
|
+
const supersededIds = new Set();
|
|
72
|
+
const supersededFailures = [];
|
|
73
|
+
const soft = state.nodes["soft-verify-shell"];
|
|
74
|
+
if (!soft || soft.status !== "ERROR") {
|
|
75
|
+
return { supersededIds, supersededFailures };
|
|
76
|
+
}
|
|
77
|
+
const repair = state.nodes["repair-pi"];
|
|
78
|
+
const hard = state.nodes["hard-verify-shell"];
|
|
79
|
+
if (repair?.status !== "FINISHED" || hard?.status !== "FINISHED") {
|
|
80
|
+
return { supersededIds, supersededFailures };
|
|
81
|
+
}
|
|
82
|
+
const observesReview = Boolean(state.nodes["review-gate-shell"]);
|
|
83
|
+
const terminalReason = state.convergence?.terminalReason;
|
|
84
|
+
if (observesReview) {
|
|
85
|
+
if (terminalReason !== "review-pass") {
|
|
86
|
+
return { supersededIds, supersededFailures };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (terminalReason && terminalReason !== "hard-verify-pass") {
|
|
90
|
+
return { supersededIds, supersededFailures };
|
|
91
|
+
}
|
|
92
|
+
// Evidence: at least one successful repair pass after soft failure, with hard verify finished in terminal state.
|
|
93
|
+
const hasRepairEvidence = !state.convergence?.enabled ||
|
|
94
|
+
state.convergence.passHistory.some((pass) => pass.reason === "hard-verify-failed" ||
|
|
95
|
+
pass.reason === "review-request-revision" ||
|
|
96
|
+
pass.status === "retrying") ||
|
|
97
|
+
repair.status === "FINISHED";
|
|
98
|
+
if (!hasRepairEvidence) {
|
|
99
|
+
return { supersededIds, supersededFailures };
|
|
100
|
+
}
|
|
101
|
+
supersededIds.add("soft-verify-shell");
|
|
102
|
+
supersededFailures.push({
|
|
103
|
+
nodeId: "soft-verify-shell",
|
|
104
|
+
preservedStatus: "ERROR",
|
|
105
|
+
supersededBy: ["repair-pi", "hard-verify-shell"],
|
|
106
|
+
pass: state.convergence?.currentPass,
|
|
107
|
+
reason: "intermediate-soft-verify-covered-by-repair-and-final-hard",
|
|
108
|
+
});
|
|
109
|
+
return { supersededIds, supersededFailures };
|
|
110
|
+
}
|
|
111
|
+
export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, frontendRecoveryQuota = 1) {
|
|
112
|
+
const repoRoot = cwd ?? path.resolve(runDir, "..", "..", "..", "..");
|
|
113
|
+
if (state.frontendShapeState?.terminalOutcome &&
|
|
114
|
+
state.frontendShapeState.transitionCapsule) {
|
|
115
|
+
state.status = "failed";
|
|
116
|
+
return { recoveryPending: false };
|
|
117
|
+
}
|
|
118
|
+
// Terminal override: a denied frontend writer must never surface as a
|
|
119
|
+
// mechanical partial_failed just because prewrite FINISHED while the writer
|
|
120
|
+
// was SKIPPED. Fail closed on blocked before aggregation.
|
|
121
|
+
let recoveryTrigger;
|
|
122
|
+
const skipFrontendRecovery = state.failureCategory === "controller-interrupted" ||
|
|
123
|
+
state.continuation?.writerTransaction !== undefined;
|
|
124
|
+
const hasFrontendWriter = Object.keys(state.nodes).some((id) => FRONTEND_WRITER_NODE_IDS.includes(id));
|
|
125
|
+
if (hasFrontendWriter && !skipFrontendRecovery) {
|
|
126
|
+
const admission = await readFrontendPrewriteResult(runDir);
|
|
127
|
+
if (admission.ok) {
|
|
128
|
+
// M8: admission uses the accepted/blocked state machine. A blocked admission whose
|
|
129
|
+
// failureSource is frontend-plan-pi is the contract-invalid analog of
|
|
130
|
+
// the old retryable-invalid path (candidate continuation may re-plan);
|
|
131
|
+
// every other blocked admission is terminal.
|
|
132
|
+
const planContractInvalid = admission.result.classification === "blocked" &&
|
|
133
|
+
admission.result.failureSource === "frontend-plan-pi";
|
|
134
|
+
// A blocked admission caused by the design review committing
|
|
135
|
+
// `request_design_changes` is a definite finding (not a protocol or
|
|
136
|
+
// environment error): the design was reviewed and rejected. It is the
|
|
137
|
+
// design-review analog of review-request-changes — not terminal, but
|
|
138
|
+
// re-enters the next plan round with the committed design findings.
|
|
139
|
+
// The failureSource may be frontend-writer-admission-shell or
|
|
140
|
+
// frontend-design-policy-shell; the committed design fact is the
|
|
141
|
+
// authoritative signal.
|
|
142
|
+
const designRequestFact = await readCommittedDesignRequestFact({
|
|
143
|
+
runDir,
|
|
144
|
+
});
|
|
145
|
+
const designRejected = admission.result.classification === "blocked" &&
|
|
146
|
+
designRequestFact !== undefined;
|
|
147
|
+
const terminalAdmissionDenial = (admission.result.classification === "blocked" ||
|
|
148
|
+
admission.result.classification === "stale") &&
|
|
149
|
+
!planContractInvalid &&
|
|
150
|
+
!designRejected;
|
|
151
|
+
if (terminalAdmissionDenial) {
|
|
152
|
+
state.status = "failed";
|
|
153
|
+
return { recoveryPending: false };
|
|
154
|
+
}
|
|
155
|
+
if (designRejected && designRequestFact) {
|
|
156
|
+
state.terminalReason = "design-request-changes";
|
|
157
|
+
try {
|
|
158
|
+
await writeFrontendReviewFindingsPrd({
|
|
159
|
+
runDir,
|
|
160
|
+
parentRunId: state.runId,
|
|
161
|
+
fact: designRequestFact,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.warn(`[run-dag] warning: failed to write frontend design findings PRD: ${error instanceof Error ? error.message : String(error)}`);
|
|
166
|
+
}
|
|
167
|
+
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
168
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
169
|
+
recoveryTrigger = {
|
|
170
|
+
requestId: state.runId,
|
|
171
|
+
// The rejected design is a plan input defect. Reset from the
|
|
172
|
+
// planner so the next child can incorporate the committed
|
|
173
|
+
// findings before design review runs again.
|
|
174
|
+
failureSource: "frontend-design-review-pi",
|
|
175
|
+
failureOwner: classifyReviewIssueCategoryFailureOwner(designRequestFact.issueCategory),
|
|
176
|
+
protocolFailureReason: `frontend design review request_design_changes (issueCategory=${designRequestFact.issueCategory})`,
|
|
177
|
+
recoveryMode: "new-dag",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (planContractInvalid) {
|
|
182
|
+
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
183
|
+
if (continuationCount >= frontendRecoveryQuota) {
|
|
184
|
+
// Continuation quota exhausted: a second contract-invalid candidate
|
|
185
|
+
// is authoritative candidate-contract-invalid → forced failed.
|
|
186
|
+
state.frontendRecoveryResult = buildCandidateContractInvalidResult(state, admission.result, admission.artifactHash);
|
|
187
|
+
state.status = "failed";
|
|
188
|
+
return { recoveryPending: false };
|
|
189
|
+
}
|
|
190
|
+
// Root continuation still available: emit a recovery trigger for the
|
|
191
|
+
// M6 operation controller. The parent no longer writes
|
|
192
|
+
// frontendRecoveryState or stays in active/ for a child CAS.
|
|
193
|
+
recoveryTrigger = {
|
|
194
|
+
requestId: state.runId,
|
|
195
|
+
failureSource: admission.result.failureSource ?? "frontend-plan-pi",
|
|
196
|
+
failureOwner: "plan",
|
|
197
|
+
protocolFailureReason: "frontend candidate contract invalid",
|
|
198
|
+
recoveryMode: "new-dag",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Phase 5: writer transient partial write → rollback + recovery trigger.
|
|
204
|
+
// Only for frontend-implementation writers with a remaining root
|
|
205
|
+
// continuation, and only when the rollback journal (captured before the
|
|
206
|
+
// provider call) restores cleanly; otherwise the run stays failed with
|
|
207
|
+
// auto-recovery-blocked.
|
|
208
|
+
if (hasFrontendWriter && !recoveryTrigger && !skipFrontendRecovery) {
|
|
209
|
+
const writerNodeId = FRONTEND_WRITER_NODE_IDS.find((id) => {
|
|
210
|
+
const node = state.nodes[id];
|
|
211
|
+
return (node &&
|
|
212
|
+
node.status === "ERROR" &&
|
|
213
|
+
isFrontendWriterTransientPartialWrite({
|
|
214
|
+
failureCategory: node.failureCategory,
|
|
215
|
+
}));
|
|
216
|
+
});
|
|
217
|
+
if (writerNodeId) {
|
|
218
|
+
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
219
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
220
|
+
const { rollbackFrontendWriter } = await import("./frontend-writer-recovery.js");
|
|
221
|
+
const rollback = await rollbackFrontendWriter({
|
|
222
|
+
cwd: repoRoot,
|
|
223
|
+
parentRunId: state.runId,
|
|
224
|
+
});
|
|
225
|
+
if (rollback.ok) {
|
|
226
|
+
recoveryTrigger = {
|
|
227
|
+
requestId: state.runId,
|
|
228
|
+
failureSource: "frontend-implement-pi",
|
|
229
|
+
failureOwner: "implementation",
|
|
230
|
+
protocolFailureReason: "frontend writer transient partial write",
|
|
231
|
+
recoveryMode: "continuation",
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
state.frontendRecoveryResult = buildWriterPartialWriteBlockedResult(state, writerNodeId, rollback.reason);
|
|
236
|
+
state.status = "failed";
|
|
237
|
+
return { recoveryPending: false };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Phase 6: verification failure → automatic recovery (re-implement +
|
|
243
|
+
// re-verify). The verify shell is terminal by design so the failed run's
|
|
244
|
+
// evidence stays clean; the M6 recovery operation re-runs implement and
|
|
245
|
+
// verify in a fresh continuation, guarded by the same continuation quota,
|
|
246
|
+
// budget fold and failure fingerprint as the other recovery paths.
|
|
247
|
+
if (hasFrontendWriter && !recoveryTrigger && !skipFrontendRecovery) {
|
|
248
|
+
const verifyFailed = state.nodes["frontend-verify-shell"]?.status === "ERROR";
|
|
249
|
+
if (verifyFailed) {
|
|
250
|
+
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
251
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
252
|
+
let recoveryOwner = classifyFailureOwner({
|
|
253
|
+
nodeId: "frontend-verify-shell",
|
|
254
|
+
rawFailureCategory: state.nodes["frontend-verify-shell"]?.failureCategory,
|
|
255
|
+
});
|
|
256
|
+
try {
|
|
257
|
+
const verifyFailure = JSON.parse(await readFile(path.join(runDir, "contracts/frontend-verify-failure.json"), "utf8"));
|
|
258
|
+
const owner = failureOwnerSchema.safeParse(verifyFailure.failureOwner);
|
|
259
|
+
if (verifyFailure.schemaVersion === 1 &&
|
|
260
|
+
verifyFailure.schemaId === "frontend-verify-failure-v1" &&
|
|
261
|
+
verifyFailure.nodeId === "frontend-verify-shell" &&
|
|
262
|
+
verifyFailure.failureCategory === state.nodes["frontend-verify-shell"]?.failureCategory &&
|
|
263
|
+
owner.success) {
|
|
264
|
+
recoveryOwner = owner.data;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
recoveryOwner = "unknown";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// The node's classified category remains the fallback; never turn
|
|
272
|
+
// unknown/environment/configuration failures into writer work.
|
|
273
|
+
}
|
|
274
|
+
if (recoveryOwner === "implementation")
|
|
275
|
+
recoveryTrigger = {
|
|
276
|
+
requestId: state.runId,
|
|
277
|
+
failureSource: "frontend-verify-shell",
|
|
278
|
+
failureOwner: recoveryOwner,
|
|
279
|
+
protocolFailureReason: "frontend verification failure",
|
|
280
|
+
recoveryMode: "continuation",
|
|
281
|
+
};
|
|
282
|
+
else
|
|
283
|
+
state.terminalReason = recoveryOwner === "environment"
|
|
284
|
+
? "frontend-verification-wait-external"
|
|
285
|
+
: `frontend-verification-${recoveryOwner}-requires-contract-review`;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Phase 7: frontend review "request changes" → not passed. When the review
|
|
290
|
+
// node FINISHED but committed `request_review_changes` (typed terminal
|
|
291
|
+
// verdict), the implementation was reviewed and rejected. The run must not
|
|
292
|
+
// surface as finished: terminal aggregation below stays partial_failed, a
|
|
293
|
+
// terminal reason is recorded, the committed findings are rendered into a
|
|
294
|
+
// downstream-task PRD draft (run-owned), and a recovery trigger carries the
|
|
295
|
+
// issueCategory-derived owner so a later DAG round consumes the findings
|
|
296
|
+
// instead of repeating the previous flow.
|
|
297
|
+
const reviewRequestFact = await readCommittedReviewRequestFact({
|
|
298
|
+
runDir,
|
|
299
|
+
});
|
|
300
|
+
if (reviewRequestFact) {
|
|
301
|
+
state.terminalReason = "review-request-changes";
|
|
302
|
+
try {
|
|
303
|
+
await writeFrontendReviewFindingsPrd({
|
|
304
|
+
runDir,
|
|
305
|
+
parentRunId: state.runId,
|
|
306
|
+
fact: reviewRequestFact,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
// The PRD draft is additive; a write failure must not mask the
|
|
311
|
+
// not-passed verdict. The terminal aggregation below still applies.
|
|
312
|
+
console.warn(`[run-dag] warning: failed to write frontend review findings PRD: ${error instanceof Error ? error.message : String(error)}`);
|
|
313
|
+
}
|
|
314
|
+
if (!recoveryTrigger && !skipFrontendRecovery) {
|
|
315
|
+
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
316
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
317
|
+
recoveryTrigger = {
|
|
318
|
+
requestId: state.runId,
|
|
319
|
+
failureSource: "frontend-review-pi",
|
|
320
|
+
failureOwner: classifyReviewIssueCategoryFailureOwner(reviewRequestFact.issueCategory),
|
|
321
|
+
protocolFailureReason: `frontend review request_review_changes (issueCategory=${reviewRequestFact.issueCategory})`,
|
|
322
|
+
recoveryMode: "new-dag",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const finishedCount = Object.values(state.nodes).filter((n) => n.status === "FINISHED").length;
|
|
328
|
+
const { supersededIds, supersededFailures } = collectSupersededIntermediateFailures(state);
|
|
329
|
+
if (state.convergence && supersededFailures.length > 0) {
|
|
330
|
+
state.convergence.supersededFailures = supersededFailures;
|
|
331
|
+
}
|
|
332
|
+
// Prefer map keys: DagNodeRecord may not always mirror the map key on `.id`.
|
|
333
|
+
const effectiveErrorCount = Object.entries(state.nodes).filter(([nodeId, n]) => n.status === "ERROR" && !supersededIds.has(nodeId)).length;
|
|
334
|
+
const supersededErrorCount = Object.entries(state.nodes).filter(([nodeId, n]) => n.status === "ERROR" && supersededIds.has(nodeId)).length;
|
|
335
|
+
const conditionSkippedCount = Object.values(state.nodes).filter((n) => n.status === "SKIPPED" && isConditionSkippedReason(n.skippedReason)).length;
|
|
336
|
+
const successTerminal = isSuccessfulConvergenceTerminal(state);
|
|
337
|
+
// Superseded intermediate ERRORs remain ERROR in history but count as settled
|
|
338
|
+
// for terminal aggregation only (they do not inflate effectiveErrorCount).
|
|
339
|
+
if (effectiveErrorCount === 0 &&
|
|
340
|
+
successTerminal &&
|
|
341
|
+
finishedCount + conditionSkippedCount + supersededErrorCount === taskCount) {
|
|
342
|
+
// Phase 7 guard: a committed `request_review_changes` means the review
|
|
343
|
+
// rejected the implementation; the run must never surface as finished
|
|
344
|
+
// even though every node technically FINISHED.
|
|
345
|
+
state.status =
|
|
346
|
+
state.terminalReason === "review-request-changes"
|
|
347
|
+
? "partial_failed"
|
|
348
|
+
: "finished";
|
|
349
|
+
}
|
|
350
|
+
else if (finishedCount > 0) {
|
|
351
|
+
state.status = "partial_failed";
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
state.status = "failed";
|
|
355
|
+
}
|
|
356
|
+
return recoveryTrigger
|
|
357
|
+
? { recoveryPending: true, recovery: recoveryTrigger }
|
|
358
|
+
: { recoveryPending: false };
|
|
359
|
+
}
|
|
360
|
+
export function isSuccessfulConvergenceTerminal(state) {
|
|
361
|
+
const reason = state.convergence?.terminalReason;
|
|
362
|
+
if (!reason)
|
|
363
|
+
return true;
|
|
364
|
+
return SUCCESSFUL_CONVERGENCE_TERMINAL_REASONS.has(reason);
|
|
365
|
+
}
|
|
366
|
+
export async function createRecoveryOperation(input) {
|
|
367
|
+
const { cwd, runDir, spec, state, trigger } = input;
|
|
368
|
+
const { computeFrontendRecoveryPlanForSource } = await import("./frontend-recovery-plan.js");
|
|
369
|
+
let plan;
|
|
370
|
+
try {
|
|
371
|
+
plan = computeFrontendRecoveryPlanForSource({
|
|
372
|
+
spec,
|
|
373
|
+
parentRunId: state.runId,
|
|
374
|
+
requestId: trigger.requestId,
|
|
375
|
+
failureSource: trigger.failureSource,
|
|
376
|
+
failureOwner: trigger.failureOwner,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
return { kind: "blocked", reason: error instanceof Error ? error.message : String(error) };
|
|
381
|
+
}
|
|
382
|
+
const recoveryLease = await acquireDagRecoveryLease({
|
|
383
|
+
cwd,
|
|
384
|
+
parentRunId: state.runId,
|
|
385
|
+
requestId: trigger.requestId,
|
|
386
|
+
kind: "auto-frontend-recovery",
|
|
387
|
+
});
|
|
388
|
+
let effectiveLease = recoveryLease;
|
|
389
|
+
let observeOnly = false;
|
|
390
|
+
if (recoveryLease.kind === "held") {
|
|
391
|
+
if (recoveryLease.lease.kind !== "auto-frontend-recovery" ||
|
|
392
|
+
recoveryLease.lease.requestId !== trigger.requestId) {
|
|
393
|
+
return { kind: "lease-held", lease: recoveryLease.lease };
|
|
394
|
+
}
|
|
395
|
+
const claimed = await claimDagRecoveryLease({
|
|
396
|
+
leasePath: recoveryLease.leasePath,
|
|
397
|
+
lease: recoveryLease.lease,
|
|
398
|
+
});
|
|
399
|
+
effectiveLease = claimed;
|
|
400
|
+
if (claimed.kind === "held") {
|
|
401
|
+
if (!claimed.lease.executionRef) {
|
|
402
|
+
return { kind: "lease-held", lease: claimed.lease };
|
|
403
|
+
}
|
|
404
|
+
observeOnly = true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const planHash = createHash("sha256")
|
|
408
|
+
.update(JSON.stringify(plan))
|
|
409
|
+
.digest("hex");
|
|
410
|
+
const continuationPath = path.join(runDir, ".runtime", "frontend-recovery", `${trigger.requestId}.json`);
|
|
411
|
+
await writeJsonAtomic(continuationPath, {
|
|
412
|
+
schemaVersion: 1,
|
|
413
|
+
parentRunId: state.runId,
|
|
414
|
+
requestId: trigger.requestId,
|
|
415
|
+
failureSource: trigger.failureSource,
|
|
416
|
+
failureOwner: plan.failureOwner,
|
|
417
|
+
resetRootNodeId: plan.resetRootNodeId,
|
|
418
|
+
recoveryMode: trigger.recoveryMode,
|
|
419
|
+
planHash,
|
|
420
|
+
plan,
|
|
421
|
+
});
|
|
422
|
+
const operationInput = {
|
|
423
|
+
clientRequestId: `dag-recovery-${state.runId}`,
|
|
424
|
+
action: "dagRunTask",
|
|
425
|
+
actionParams: {
|
|
426
|
+
parentRunId: state.runId,
|
|
427
|
+
requestId: trigger.requestId,
|
|
428
|
+
failureSource: trigger.failureSource,
|
|
429
|
+
failureOwner: plan.failureOwner,
|
|
430
|
+
resetRootNodeId: plan.resetRootNodeId,
|
|
431
|
+
protocolFailureReason: trigger.protocolFailureReason,
|
|
432
|
+
recoveryMode: trigger.recoveryMode,
|
|
433
|
+
planHash,
|
|
434
|
+
},
|
|
435
|
+
cliArgs: [
|
|
436
|
+
"dag",
|
|
437
|
+
"rerun-task",
|
|
438
|
+
"--run-id",
|
|
439
|
+
state.runId,
|
|
440
|
+
"--reason",
|
|
441
|
+
trigger.protocolFailureReason ?? "frontend automatic recovery",
|
|
442
|
+
"--request-id",
|
|
443
|
+
trigger.requestId,
|
|
444
|
+
"--frontend-recovery",
|
|
445
|
+
"--json",
|
|
446
|
+
],
|
|
447
|
+
};
|
|
448
|
+
let created;
|
|
449
|
+
if (effectiveLease.lease.executionRef) {
|
|
450
|
+
// Replay the exact operation bound by the durable lease. A missing record
|
|
451
|
+
// cannot be replaced with a new operation id without violating the fence.
|
|
452
|
+
const existing = await input.deps.store.get(effectiveLease.lease.executionRef);
|
|
453
|
+
if (!existing) {
|
|
454
|
+
throw new Error(`frontend recovery lease operation not found: ${effectiveLease.lease.executionRef}`);
|
|
455
|
+
}
|
|
456
|
+
if (existing.clientRequestId !== operationInput.clientRequestId ||
|
|
457
|
+
existing.action !== operationInput.action ||
|
|
458
|
+
existing.payloadHash !==
|
|
459
|
+
computeActionPayloadHash(operationInput.action, operationInput.actionParams)) {
|
|
460
|
+
throw new Error("frontend recovery lease execution identity drift: referenced operation payload does not match the recovery request");
|
|
461
|
+
}
|
|
462
|
+
created = { kind: "idempotent", operation: existing };
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
created = await input.deps.store.create(operationInput);
|
|
466
|
+
}
|
|
467
|
+
if (created.kind !== "conflict" && !observeOnly) {
|
|
468
|
+
// The durable lease must name the operation before controller execution.
|
|
469
|
+
// This also repairs a lease left behind by a crash before the original bind.
|
|
470
|
+
await bindDagRecoveryLeaseExecution({
|
|
471
|
+
leasePath: effectiveLease.leasePath,
|
|
472
|
+
lease: effectiveLease.lease,
|
|
473
|
+
executionRef: created.operation.operationId,
|
|
474
|
+
});
|
|
475
|
+
// Repair CLI args on both fresh and replayed operations. This closes the
|
|
476
|
+
// crash window after operation creation but before args were persisted.
|
|
477
|
+
await input.deps.store.update(created.operation.operationId, {
|
|
478
|
+
cliArgs: [
|
|
479
|
+
"dag",
|
|
480
|
+
"rerun-task",
|
|
481
|
+
"--run-id",
|
|
482
|
+
state.runId,
|
|
483
|
+
"--reason",
|
|
484
|
+
trigger.protocolFailureReason ?? "frontend automatic recovery",
|
|
485
|
+
"--request-id",
|
|
486
|
+
trigger.requestId,
|
|
487
|
+
"--frontend-recovery",
|
|
488
|
+
"--recovery-operation-id",
|
|
489
|
+
created.operation.operationId,
|
|
490
|
+
"--json",
|
|
491
|
+
],
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
if (created.kind !== "conflict" && observeOnly) {
|
|
495
|
+
return { kind: "observed", operation: created.operation };
|
|
496
|
+
}
|
|
497
|
+
return created;
|
|
498
|
+
}
|
|
499
|
+
export const SUCCESSFUL_CONVERGENCE_TERMINAL_REASONS = new Set([
|
|
500
|
+
"review-pass",
|
|
501
|
+
"hard-verify-pass",
|
|
502
|
+
]);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { frontendPairExecutionBindingSchema } from "../../application/evaluation/frontend-pair-registration.js";
|
|
2
|
+
import { sourceBudgetContractIdentity } from "../../application/evaluation/budget.js";
|
|
3
|
+
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
4
|
+
import { frontendTestObservationSchema } from "./frontend-test-execution-evidence.js";
|
|
5
|
+
import { capabilityBoundarySchema } from "../../task/config-types.js";
|
|
1
6
|
import { z } from "zod";
|
|
2
7
|
import { frontendExecutionPolicySchema } from "../../shared/frontend-execution-policy.js";
|
|
3
8
|
import { DEFAULT_FRONTEND_SPEC_ROOTS, isFrontendSpecFilePath, isValidFrontendSpecRoot, OPENSPEC_SPEC_EXT_RE, } from "../../shared/openspec-spec.js";
|
|
4
|
-
import { campaignBudgetSchema, } from "../../application/evaluation/budget.js";
|
|
9
|
+
import { campaignBudgetSchema, sourceProviderBudgetBindingSchema, } from "../../application/evaluation/budget.js";
|
|
5
10
|
import { assertDagPromptSourceRule } from "./prompt-source.js";
|
|
6
11
|
// Leaf-module import: dagRetryPolicySchema must be defined by a module that
|
|
7
12
|
// never transitively reaches types.ts during init (retry-policy does, via
|
|
@@ -89,6 +94,7 @@ export const dagShellVerifyEvidenceSchema = z.object({
|
|
|
89
94
|
.array(z.object({
|
|
90
95
|
label: z.string(),
|
|
91
96
|
status: z.enum(["ok", "future-delivery-script", "future-test-target"]),
|
|
97
|
+
testObservation: frontendTestObservationSchema.optional(),
|
|
92
98
|
}))
|
|
93
99
|
.default([]),
|
|
94
100
|
/** Effective timeout applied independently to each shell command. */
|
|
@@ -463,13 +469,7 @@ export const dagFrontendDesignPolicySchema = z
|
|
|
463
469
|
.optional(),
|
|
464
470
|
/** Generation-frozen test capability boundary composed into the
|
|
465
471
|
* canonical dependencyPolicy by the runtime. */
|
|
466
|
-
capabilityBoundary:
|
|
467
|
-
.object({
|
|
468
|
-
allowed: z.array(z.string().min(1)).optional(),
|
|
469
|
-
forbidden: z.array(z.string().min(1)).optional(),
|
|
470
|
-
})
|
|
471
|
-
.strict()
|
|
472
|
-
.optional(),
|
|
472
|
+
capabilityBoundary: capabilityBoundarySchema.optional(),
|
|
473
473
|
/** Generation-frozen interaction id vocabulary (plan finalize check). */
|
|
474
474
|
interactionIds: z.array(z.string().min(1)).optional(),
|
|
475
475
|
/** Generation-frozen roots that bound custom normative directories. */
|
|
@@ -1320,6 +1320,7 @@ export const dagEvaluationBindingSchema = z
|
|
|
1320
1320
|
seed: z.number().int().nonnegative(),
|
|
1321
1321
|
split: z.enum(["public", "private", "held_out"]).optional(),
|
|
1322
1322
|
taskRef: z.string().min(1).optional(),
|
|
1323
|
+
frontendPair: frontendPairExecutionBindingSchema.optional(),
|
|
1323
1324
|
})
|
|
1324
1325
|
.strict();
|
|
1325
1326
|
/** Eval Lab Campaign Budget (W3.4); enforced by runner ledger when mode=hard. */
|
|
@@ -1424,6 +1425,7 @@ export const dagSpecSchema = z
|
|
|
1424
1425
|
evaluation: dagEvaluationBindingSchema.optional(),
|
|
1425
1426
|
/** Optional hard/record-only budget; requires version 3 or 4. */
|
|
1426
1427
|
budget: dagBudgetSchema.optional(),
|
|
1428
|
+
sourceProviderBudget: sourceProviderBudgetBindingSchema.optional(),
|
|
1427
1429
|
frontendExecutionPolicy: frontendExecutionPolicySchema.optional(),
|
|
1428
1430
|
sourceBinding: dagSourceBindingSchema.optional(),
|
|
1429
1431
|
/** v4 managed Task Contract binding; only valid on version 4. */
|
|
@@ -1494,6 +1496,9 @@ export const dagSpecSchema = z
|
|
|
1494
1496
|
tasks: z.array(dagTaskSchema).min(1),
|
|
1495
1497
|
})
|
|
1496
1498
|
.superRefine((spec, ctx) => {
|
|
1499
|
+
if (spec.sourceProviderBudget && (!spec.sourceBinding || spec.sourceProviderBudget.ledger.taskId !== spec.sourceBinding.taskId || sha256OfCanonicalJson(spec.sourceProviderBudget.contract ?? null) !== sha256OfCanonicalJson(sourceBudgetContractIdentity(spec.taskContractBinding) ?? null) || spec.budget?.mode !== "hard" || spec.sourceProviderBudget.ledger.maxProviderRequests !== spec.budget?.limits.maxProviderRequests || spec.sourceProviderBudget.sourceBindingSha256 !== sha256OfCanonicalJson(spec.sourceBinding))) {
|
|
1500
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["sourceProviderBudget"], message: "source request budget must match the frozen source and request limit" });
|
|
1501
|
+
}
|
|
1497
1502
|
const supportsV3Fields = spec.version === 3 || spec.version === 4;
|
|
1498
1503
|
validateFrozenArtifactBindings(spec.tasks, ctx);
|
|
1499
1504
|
if (spec.evaluation && !supportsV3Fields) {
|
|
@@ -1618,6 +1623,8 @@ export const frontendRecoveryStateSchema = z
|
|
|
1618
1623
|
* frontend-plan-revision-pi / frontend-prewrite-gate-shell /
|
|
1619
1624
|
* frontend-implement-pi (writer partial write). */
|
|
1620
1625
|
failureSource: z.string().min(1).optional(),
|
|
1626
|
+
failureOwner: z.enum(["contract", "scout", "plan", "implementation", "verification-config", "environment", "scope", "unknown"]).optional(),
|
|
1627
|
+
resetRootNodeId: z.string().min(1).optional(),
|
|
1621
1628
|
revision: z.number().int().nonnegative(),
|
|
1622
1629
|
})
|
|
1623
1630
|
.strict()
|
|
@@ -40,7 +40,7 @@ export const LOOP_BENCHMARK_FIXTURE_SAMPLES = [
|
|
|
40
40
|
regressionPause: false,
|
|
41
41
|
changedPaths: 2,
|
|
42
42
|
repairPasses: 1,
|
|
43
|
-
}, ["./test/dag-runner.test.ts"], "Single repair exhausts before the third successful hard verification."),
|
|
43
|
+
}, ["./test/dag-runner.test.ts", "./test/dag-runner-convergence.test.ts", "./test/dag-runner-artifacts.test.ts", "./test/dag-runner-decisions.test.ts"], "Single repair exhausts before the third successful hard verification."),
|
|
44
44
|
result("3-pass-convergence", {
|
|
45
45
|
pass: true,
|
|
46
46
|
fullVerifyPass: true,
|
|
@@ -51,7 +51,7 @@ export const LOOP_BENCHMARK_FIXTURE_SAMPLES = [
|
|
|
51
51
|
regressionPause: false,
|
|
52
52
|
changedPaths: 2,
|
|
53
53
|
repairPasses: 3,
|
|
54
|
-
}, ["./test/dag-runner.test.ts"], "Convergence re-enters supervisor, repair, and hard verify until pass 3."),
|
|
54
|
+
}, ["./test/dag-runner.test.ts", "./test/dag-runner-convergence.test.ts", "./test/dag-runner-artifacts.test.ts", "./test/dag-runner-decisions.test.ts"], "Convergence re-enters supervisor, repair, and hard verify until pass 3."),
|
|
55
55
|
result("3-pass-convergence+quota", {
|
|
56
56
|
pass: true,
|
|
57
57
|
fullVerifyPass: true,
|
|
@@ -64,6 +64,9 @@ export const LOOP_BENCHMARK_FIXTURE_SAMPLES = [
|
|
|
64
64
|
repairPasses: 3,
|
|
65
65
|
}, [
|
|
66
66
|
"./test/dag-runner.test.ts",
|
|
67
|
+
"./test/dag-runner-convergence.test.ts",
|
|
68
|
+
"./test/dag-runner-artifacts.test.ts",
|
|
69
|
+
"./test/dag-runner-decisions.test.ts",
|
|
67
70
|
"./test/verify-step.test.ts",
|
|
68
71
|
], "Intermediate verification uses quota while final hard gate remains full."),
|
|
69
72
|
]),
|
|
@@ -78,7 +81,7 @@ export const LOOP_BENCHMARK_FIXTURE_SAMPLES = [
|
|
|
78
81
|
regressionPause: false,
|
|
79
82
|
changedPaths: 3,
|
|
80
83
|
repairPasses: 1,
|
|
81
|
-
}, ["./test/dag-runner.test.ts"], "Single repair ends failed without enough evidence to classify regression."),
|
|
84
|
+
}, ["./test/dag-runner.test.ts", "./test/dag-runner-convergence.test.ts", "./test/dag-runner-artifacts.test.ts", "./test/dag-runner-decisions.test.ts"], "Single repair ends failed without enough evidence to classify regression."),
|
|
82
85
|
result("3-pass-convergence", {
|
|
83
86
|
pass: false,
|
|
84
87
|
fullVerifyPass: false,
|
|
@@ -89,7 +92,7 @@ export const LOOP_BENCHMARK_FIXTURE_SAMPLES = [
|
|
|
89
92
|
regressionPause: true,
|
|
90
93
|
changedPaths: 3,
|
|
91
94
|
repairPasses: 2,
|
|
92
|
-
}, ["./test/dag-runner.test.ts"], "Convergence pauses instead of retrying blindly after regression signal."),
|
|
95
|
+
}, ["./test/dag-runner.test.ts", "./test/dag-runner-convergence.test.ts", "./test/dag-runner-artifacts.test.ts", "./test/dag-runner-decisions.test.ts"], "Convergence pauses instead of retrying blindly after regression signal."),
|
|
93
96
|
result("3-pass-convergence+quota", {
|
|
94
97
|
pass: false,
|
|
95
98
|
fullVerifyPass: false,
|
|
@@ -100,7 +103,7 @@ export const LOOP_BENCHMARK_FIXTURE_SAMPLES = [
|
|
|
100
103
|
regressionPause: true,
|
|
101
104
|
changedPaths: 3,
|
|
102
105
|
repairPasses: 2,
|
|
103
|
-
}, ["./test/dag-runner.test.ts"], "Quota reduces intermediate cost but still pauses on regression."),
|
|
106
|
+
}, ["./test/dag-runner.test.ts", "./test/dag-runner-convergence.test.ts", "./test/dag-runner-artifacts.test.ts", "./test/dag-runner-decisions.test.ts"], "Quota reduces intermediate cost but still pauses on regression."),
|
|
104
107
|
]),
|
|
105
108
|
sample("LB-03", "write guard hard failure must not retry", "dag-runtime", [
|
|
106
109
|
result("single-repair", {
|
|
@@ -113,7 +116,7 @@ export const LOOP_BENCHMARK_FIXTURE_SAMPLES = [
|
|
|
113
116
|
regressionPause: false,
|
|
114
117
|
changedPaths: 1,
|
|
115
118
|
repairPasses: 1,
|
|
116
|
-
}, ["./test/dag-runner.test.ts"], "Write-guard failure remains terminal."),
|
|
119
|
+
}, ["./test/dag-runner.test.ts", "./test/dag-runner-convergence.test.ts", "./test/dag-runner-artifacts.test.ts", "./test/dag-runner-decisions.test.ts"], "Write-guard failure remains terminal."),
|
|
117
120
|
result("3-pass-convergence", {
|
|
118
121
|
pass: false,
|
|
119
122
|
fullVerifyPass: false,
|
|
@@ -124,7 +127,7 @@ export const LOOP_BENCHMARK_FIXTURE_SAMPLES = [
|
|
|
124
127
|
regressionPause: false,
|
|
125
128
|
changedPaths: 1,
|
|
126
129
|
repairPasses: 1,
|
|
127
|
-
}, ["./test/dag-runner.test.ts"], "Non-retry class is not re-entered."),
|
|
130
|
+
}, ["./test/dag-runner.test.ts", "./test/dag-runner-convergence.test.ts", "./test/dag-runner-artifacts.test.ts", "./test/dag-runner-decisions.test.ts"], "Non-retry class is not re-entered."),
|
|
128
131
|
result("3-pass-convergence+quota", {
|
|
129
132
|
pass: false,
|
|
130
133
|
fullVerifyPass: false,
|
|
@@ -135,7 +138,7 @@ export const LOOP_BENCHMARK_FIXTURE_SAMPLES = [
|
|
|
135
138
|
regressionPause: false,
|
|
136
139
|
changedPaths: 1,
|
|
137
140
|
repairPasses: 1,
|
|
138
|
-
}, ["./test/dag-runner.test.ts"], "Quota strategy does not change write-guard retry policy."),
|
|
141
|
+
}, ["./test/dag-runner.test.ts", "./test/dag-runner-convergence.test.ts", "./test/dag-runner-artifacts.test.ts", "./test/dag-runner-decisions.test.ts"], "Quota strategy does not change write-guard retry policy."),
|
|
139
142
|
]),
|
|
140
143
|
sample("LB-04", "quota final gate must stay full", "loop-runtime", [
|
|
141
144
|
result("single-repair", {
|
package/docs/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`docs/` 是 loop-agent 源仓的治理文档根目录。新读者不需要先浏览所有文件,先按任务选择入口,再由目录 README 或 `docs/document-catalog.json` 判断文档身份。
|
|
4
4
|
|
|
5
|
-
**维护日:2026-
|
|
5
|
+
**维护日:2026-09-16** — 对齐 `@tea-agent/loop-agent@0.41.1-beta.0`。版本事实以根 `CHANGELOG.md` 与 `package.json` 为准;能力短摘要见 `reports/current-capability-summary.md`。
|
|
6
6
|
|
|
7
7
|
## 我现在要做什么?
|
|
8
8
|
|