@tea-agent/loop-agent 0.43.0-next.9 → 0.43.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/AGENTS.md +1 -1
- package/CHANGELOG.md +31 -2309
- package/README.md +7 -0
- package/dist/application/dag/args.js +8 -0
- package/dist/application/task-lifecycle/advance.js +38 -7
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +2 -0
- package/dist/cli/program.js +3 -1
- package/dist/cli/update/init-surface-notifier.js +2 -0
- package/dist/cli/update/policy.js +2 -0
- package/dist/commands/client-recovery.js +20 -55
- package/dist/commands/cursor-prompt.js +2 -0
- package/dist/commands/dag-approve.js +2 -0
- package/dist/commands/dag-final-verification.js +2 -0
- package/dist/commands/dag-init-hybrid.js +2 -0
- package/dist/commands/dag-reconcile-tasks.js +2 -0
- package/dist/commands/dag-reject.js +2 -0
- package/dist/commands/dag-report.js +2 -0
- package/dist/commands/dag-rerun.js +2 -0
- package/dist/commands/dag-resume.js +2 -0
- package/dist/commands/dag-workflow-compile.js +2 -0
- package/dist/commands/dag-workflow-plan.js +2 -0
- package/dist/commands/dag-workflow-validate.js +2 -0
- package/dist/commands/delegate.js +2 -0
- package/dist/commands/examples.js +2 -0
- package/dist/commands/import-prd.js +2 -0
- package/dist/commands/init-model-catalog.js +2 -0
- package/dist/commands/init.js +19 -13
- package/dist/commands/instructions.js +2 -0
- package/dist/commands/knowledge.js +2 -0
- package/dist/commands/loop-benchmark.js +2 -0
- package/dist/commands/pi-prompt.js +2 -0
- package/dist/commands/pi-reuse-benchmark.js +2 -0
- package/dist/commands/promote-run.js +2 -0
- package/dist/commands/stats.js +1 -1
- package/dist/commands/study-init.js +2 -0
- package/dist/commands/task-advance.js +34 -0
- package/dist/commands/task-source-prepare.js +2 -0
- package/dist/commands/worktree-create.js +2 -0
- package/dist/commands/worktree-remove.js +2 -0
- package/dist/executors/dag-pi-executor.js +1650 -198
- package/dist/executors/pi-executor.js +85 -3
- package/dist/executors/pi-sdk-executor.js +18 -0
- package/dist/executors/shell-executor.js +93 -7
- package/dist/executors/shell-verification.js +3 -0
- package/dist/executors/shell-write-guard.js +27 -32
- package/dist/governance/exec-plans.js +6 -3
- package/dist/infrastructure/console/app-data.js +6 -0
- package/dist/shared/artifacts-core.js +2 -0
- package/dist/shared/backend-dogfood-preflight.js +47 -0
- package/dist/shared/dag-failure-category.js +3 -0
- package/dist/shared/git-progress.js +2 -0
- package/dist/shared/one-shot-prompt-args.js +2 -0
- package/dist/shared/operator/capabilities.js +180 -0
- package/dist/shared/package-metadata.js +6 -4
- package/dist/shared/pi-context-pressure/extension.js +26 -16
- package/dist/shared/pi-context-pressure/sift-bridge.js +1 -1
- package/dist/shared/pi-provider-recovery.js +182 -0
- package/dist/shared/pi-retry-settings.js +40 -0
- package/dist/shared/reference-context.js +2 -0
- package/dist/task/config-types.js +29 -0
- package/dist/task/contract/project.js +9 -0
- package/dist/task/contract/recover.js +6 -0
- package/dist/task/contract/schema.js +17 -0
- package/dist/task/source-prepare/artifact-meta.js +9 -2
- package/dist/task/source-prepare/build-draft.js +60 -0
- package/dist/task/source-prepare/fragment-inventory.js +4 -1
- package/dist/task/source-prepare/parse-intent.js +36 -11
- package/dist/task/task-demand-routing.js +5 -2
- package/dist/worker/console/chat/browser-automation.js +523 -0
- package/dist/worker/console/chat/browser-policy.js +6 -36
- package/dist/worker/console/chat/browser-routes.js +188 -56
- package/dist/worker/console/chat/explore-tools.js +5 -2
- package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +184 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +94 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
- package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
- package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
- package/dist/worker/console/chat/pi-runtime.js +208 -1138
- package/dist/worker/console/chat/routes.js +27 -7
- package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
- package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
- package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +1 -1
- package/dist/worker/console/chat/session-mode-view.js +5 -4
- package/dist/worker/console/chat/session-mode.js +18 -12
- package/dist/worker/console/chat/session-store.js +42 -9
- package/dist/worker/console/chat/tool-preview.js +115 -0
- package/dist/worker/console/chat/tools.js +10 -0
- package/dist/worker/console/chat/turn-order.js +13 -0
- package/dist/worker/console/chat/turn-process.js +32 -30
- package/dist/worker/console/console-update-and-init.js +1 -19
- package/dist/worker/console/console-update-runtime.js +5 -20
- package/dist/worker/console/operator-actions.js +63 -1
- package/dist/worker/console/prd-intake-bridge.js +54 -1
- package/dist/worker/console/routes.js +8 -0
- package/dist/worker/console/scheduled-goal-host.js +98 -0
- package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
- package/dist/worker/console/server.js +24 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-D_fNbWc_.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
- package/dist/worker/console/static/assets/{arc-_HuqLW6m.js → arc-eoQiMvuk.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Djy2qY1m.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-bB-1TSyY.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5Hmghrz.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
- package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Duyl39oy.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-x6mdnSyf.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-WlBuFj7W.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CTF429HX.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dwd6r-SF.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C-Br82D1.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CuGp0DeE.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CJhFX6Lh.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DpGOa_jT.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DXJaTynd.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CRBZWjE4.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BdXcxtlb.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-07UKsOts.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-D38p9__S.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-nSiBNNjH.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BFOTot5D.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DypKaZ6k.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-B6yoOQeW.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BHYS6viX.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-D2VD5Snj.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-E98mWxN1.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CT1zxhqP.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-6GcngNcy.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCL6NvKe.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
- package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
- package/dist/worker/console/static/assets/index-Cdkvw_H6.js +469 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C0627e-9.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DPDPv9HL.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D3OHdWiO.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DI88zD7n.js → linear-BDZ9riWi.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-p2l8IrWY.js → mermaid.core-7pKqYtpZ.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-B1bAX-oS.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DwOnWxdD.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-xRkTXnJC.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-_9UC5Zio.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CgEUirWJ.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Czbpwiyz.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-_j7_KiqY.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BNJa6Hyi.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DedpKtOc.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BBvthr7c.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Ds7umgEM.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKa-p1nF.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LoUNR56o.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bqbl6ym7.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B8dUg4lf.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +24 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
- package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +192 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +9 -1
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +15 -5
- package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +92 -38
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +145 -70
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +38 -55
- package/dist/worker/console/static-src/operator-chat/useComposer.js +182 -69
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +2 -0
- package/dist/worker/console/static-src/prd-file-import.js +3 -1
- package/dist/worker/console/workspace-context.js +22 -0
- package/dist/worker/console/workspace-initialization.js +209 -0
- package/dist/worker/delivery/git-transaction.js +2 -0
- package/dist/worker/feature/fullstack-validate.js +2 -0
- package/dist/worker/loop-agent/loop-agent-client.js +8 -2
- package/dist/worker/materialize/harness-task-materializer.js +4 -1
- package/dist/worker/metrics/projector.js +1 -1
- package/dist/worker/observability/event-store.js +4 -1
- package/dist/worker/observability/read-model.js +4 -1
- package/dist/worker/observe/node-transparency.js +23 -1
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/operator-chrome.js +3 -1
- package/dist/worker/observe/static/styles.css +7 -4
- package/dist/worker/observe/static/views/dag-inspector.js +5 -19
- package/dist/worker/outcomes/declared-artifacts.js +2 -0
- package/dist/worker/preflight.js +3 -0
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
- package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
- package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
- package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
- package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
- package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
- package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
- package/dist/worker/task-spec/validate.js +4 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
- package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
- package/dist/workflows/dag/dag-retry-schema.js +11 -0
- package/dist/workflows/dag/decision-envelope.js +2 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +2 -2
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-committed-facts.js +461 -0
- package/dist/workflows/dag/frontend-durable-tools.js +15 -3
- package/dist/workflows/dag/frontend-implementation-contract.js +369 -12
- package/dist/workflows/dag/frontend-plan-canary.js +53 -0
- package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
- package/dist/workflows/dag/frontend-plan-render.js +0 -2
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
- package/dist/workflows/dag/frontend-recovery-run.js +57 -0
- package/dist/workflows/dag/frontend-repair.js +55 -76
- package/dist/workflows/dag/frontend-review-context.js +42 -68
- package/dist/workflows/dag/frontend-review-scopes.js +2 -2
- package/dist/workflows/dag/frontend-risk.js +92 -10
- package/dist/workflows/dag/frontend-session-budget.js +117 -3
- package/dist/workflows/dag/frontend-shape.js +11 -55
- package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
- package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
- package/dist/workflows/dag/frontend-verification-trace.js +33 -54
- package/dist/workflows/dag/frontend-writer-admission.js +3 -47
- package/dist/workflows/dag/frontend-writer-status.js +0 -23
- package/dist/workflows/dag/init-hybrid.js +47 -19
- package/dist/workflows/dag/interrupt-request.js +2 -0
- package/dist/workflows/dag/lifecycle.js +11 -2
- package/dist/workflows/dag/node-execution.js +50 -13
- package/dist/workflows/dag/reconcile-run.js +2 -0
- package/dist/workflows/dag/repair-artifact.js +3 -1
- package/dist/workflows/dag/report.js +2 -0
- package/dist/workflows/dag/rerun-plan.js +10 -0
- package/dist/workflows/dag/rerun-task.js +79 -1
- package/dist/workflows/dag/retry-policy.js +18 -0
- package/dist/workflows/dag/scheduler.js +2 -4
- package/dist/workflows/dag/types.js +44 -6
- package/dist/workflows/dag/validate.js +4 -0
- package/docs/README.md +1 -0
- package/docs/architecture/runtime-boundaries.md +3 -3
- package/docs/governance/README.md +1 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +14 -10
- package/docs/templates/frontend-implementation-contract.schema.json +0 -7
- package/package.json +9 -3
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/references/contract-protocol.md +3 -1
- package/skills/frontend-plan/SKILL.md +8 -6
- package/skills/frontend-plan/references/decision-contract.md +3 -3
- package/skills/frontend-review/SKILL.md +18 -28
- package/skills/frontend-review/references/review-findings.md +8 -3
- package/dist/commands/new-task.js +0 -5
- package/dist/commands/status.js +0 -57
- package/dist/commands/task-contract.js +0 -269
- package/dist/shared/prompts.js +0 -26
- package/dist/task/config.js +0 -2
- package/dist/task/contract/validate-draft.js +0 -106
- package/dist/task/goal.js +0 -3
- package/dist/task/index.js +0 -12
- package/dist/task/lifecycle.js +0 -1
- package/dist/task/operator/capabilities.js +0 -6
- package/dist/task/operator/envelope.js +0 -2
- package/dist/task/operator/index.js +0 -5
- package/dist/task/operator/registry.js +0 -2
- package/dist/task/operator/types.js +0 -1
- package/dist/task/paths.js +0 -1
- package/dist/task/source-state.js +0 -1
- package/dist/task/subagent-guidance.js +0 -1
- package/dist/worker/console/chat/sift-bridge.js +0 -1
- package/dist/worker/console/index.js +0 -20
- package/dist/worker/console/static/assets/channel-C0A6UEIa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +0 -1
- package/dist/worker/console/static/assets/index-24jp3aBh.js +0 -468
- package/dist/worker/console/static/assets/index-CY6y6I8M.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +0 -8
- package/dist/worker/scheduler/index.js +0 -25
- package/dist/workflows/dag/facts.js +0 -4
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
- package/dist/workflows/dag/index.js +0 -6
- package/dist/workflows/dynamic/index.js +0 -7
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { collectFrontendPlanMissingFacts, collectFrontendPlanPhaseMissingFacts, committedFactFromPlanRecord, planFactStringList, planFactScopeIntersects } from "../workflows/dag/frontend-plan-completeness.js";
|
|
2
|
-
export { collectFrontendPlanMissingFacts, collectFrontendPlanPhaseMissingFacts } from "../workflows/dag/frontend-plan-completeness.js";
|
|
3
1
|
import { classifyFrontendPlanRecovery } from "../workflows/dag/frontend-plan-recovery-policy.js";
|
|
4
2
|
import { collectFrontendExecutionGroups, frontendExecutionSchema } from "../workflows/dag/frontend-execution-groups.js";
|
|
5
3
|
import { FRONTEND_SCOPE_TARGET_BYTES, packFrontendInputUnits, parseFrontendInputBlock, projectFrontendContractPrompt, projectFrontendInputScope } from "../workflows/dag/frontend-input-projection.js";
|
|
@@ -282,7 +280,17 @@ export const WRITER_BUDGET_EXHAUSTED_CATEGORY = "writer-budget-exhausted";
|
|
|
282
280
|
export const WRITER_TOKEN_BUDGET = 2_000_000;
|
|
283
281
|
export function allowsMissingChangedWriterOutcomeRecovery(task) {
|
|
284
282
|
return (isBackendTestCompletenessRetryCandidate(task) ||
|
|
285
|
-
isBackendTestPytestCollectionRepairOutcomeRecoveryCandidate(task)
|
|
283
|
+
isBackendTestPytestCollectionRepairOutcomeRecoveryCandidate(task) ||
|
|
284
|
+
// N5 can persist a valid incremental edit but omit the prose status line.
|
|
285
|
+
// Recover only the missing marker with a measured non-empty diff; the
|
|
286
|
+
// existing write guard and downstream backend validation still apply.
|
|
287
|
+
(task.id === "review-and-revise-backend-md-cases-pi" &&
|
|
288
|
+
task.executor === "pi" &&
|
|
289
|
+
task.role === "implementer" &&
|
|
290
|
+
task.toolProfile === "write" &&
|
|
291
|
+
task.writePolicy === "exclusive" &&
|
|
292
|
+
(task.writeSet?.length ?? 0) > 0 &&
|
|
293
|
+
task.writerOutcomePolicy?.type === "implementation-outcome-v1"));
|
|
286
294
|
}
|
|
287
295
|
function readWriterThinkingExhaustionEvidence(result) {
|
|
288
296
|
const wider = result;
|
|
@@ -458,12 +466,16 @@ export function resolveDagPiStepName(task) {
|
|
|
458
466
|
export const FRONTEND_CONTRACT_RECORD_TOOL_NAMES = [
|
|
459
467
|
"record_requirement",
|
|
460
468
|
"record_constraint",
|
|
469
|
+
// Model-owned execution grouping on already-confirmed canonical requirements.
|
|
470
|
+
"record_requirement_execution",
|
|
461
471
|
"record_evidence_expectation",
|
|
462
472
|
"record_handoff_intent",
|
|
463
473
|
"record_open_question",
|
|
464
474
|
"record_split_proposal",
|
|
465
475
|
"record_ui_state",
|
|
466
476
|
"record_required_deliverables",
|
|
477
|
+
// OpenSpec disposition selection when the task declares spec roots.
|
|
478
|
+
"record_openspec_selection",
|
|
467
479
|
"complete_contract_scope",
|
|
468
480
|
];
|
|
469
481
|
export const FRONTEND_CONTRACT_TERMINAL_TOOL_NAMES = [
|
|
@@ -492,6 +504,27 @@ export const FRONTEND_PLAN_RECORD_TOOL_NAMES = [
|
|
|
492
504
|
];
|
|
493
505
|
export const FRONTEND_PLAN_TERMINAL_TOOL_NAMES = ["finalize_plan"];
|
|
494
506
|
export const FRONTEND_PLAN_ADOPT_TOOL_NAMES = ["adopt_staged_fact"];
|
|
507
|
+
/** Experimental decision-path Plan toolset (HARNESS_FRONTEND_PLAN_DECISION=1).
|
|
508
|
+
* These are the tool names `createFrontendPlanDecisionTools` registers as
|
|
509
|
+
* custom tools; the SDK activates a custom tool only when its name is in the
|
|
510
|
+
* session allowlist returned by `resolveDagPiToolNames`, so the decision path
|
|
511
|
+
* must return this exact set instead of the relationship-path plan tools.
|
|
512
|
+
* `finalize_decision` is finalize-loop-owned: the model calls it in-session
|
|
513
|
+
* after committing facts and repairs any pre-validation findings until it
|
|
514
|
+
* succeeds; the executor still invokes it after the session (an identical
|
|
515
|
+
* replay of a successful terminal) as the backstop. It is listed so durable
|
|
516
|
+
* activation/consistency checks see the full toolset. */
|
|
517
|
+
export const FRONTEND_DECISION_PLAN_TOOL_NAMES = [
|
|
518
|
+
"record_module_placement",
|
|
519
|
+
"record_reuse_decision",
|
|
520
|
+
"record_state_ownership",
|
|
521
|
+
"record_data_flow",
|
|
522
|
+
"retract_data_flow",
|
|
523
|
+
"record_api_mock_boundary",
|
|
524
|
+
"record_verification_focus",
|
|
525
|
+
"record_dependency",
|
|
526
|
+
"finalize_decision",
|
|
527
|
+
];
|
|
495
528
|
/** M5: `frontend-review-pi` emits its authoritative terminal verdict through
|
|
496
529
|
* committed typed tools instead of the legacy JSON verdict parse. */
|
|
497
530
|
export function isFrontendReviewTypedTerminalNode(task) {
|
|
@@ -549,9 +582,28 @@ export async function acceptCommittedTypedTerminalFact(runDir, nodeId) {
|
|
|
549
582
|
if (!binding)
|
|
550
583
|
return false;
|
|
551
584
|
try {
|
|
552
|
-
const { readCommittedOriginFacts } = await import("../workflows/dag/frontend-
|
|
553
|
-
|
|
554
|
-
|
|
585
|
+
const { readCommittedOriginFacts } = await import("../workflows/dag/frontend-committed-facts.js");
|
|
586
|
+
// The experimental decision Plan path commits its terminal into
|
|
587
|
+
// plan-decision-facts.jsonl (kind finalize_decision) instead of the
|
|
588
|
+
// relationship ledger plan-typed-facts.jsonl (kind finalize_plan). Accept
|
|
589
|
+
// either ledger for the plan node so a successful decision finalize with
|
|
590
|
+
// no trailing narrative text is not reclassified as empty-output.
|
|
591
|
+
const candidates = [binding];
|
|
592
|
+
if (nodeId === "frontend-plan-pi") {
|
|
593
|
+
candidates.push({ file: "plan-decision-facts.jsonl", kind: "finalize_decision" });
|
|
594
|
+
}
|
|
595
|
+
for (const candidate of candidates) {
|
|
596
|
+
try {
|
|
597
|
+
const records = await readCommittedOriginFacts(runDir, nodeId, candidate.file);
|
|
598
|
+
if (records.some((record) => record.fact.kind === candidate.kind)) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// Missing alternative ledger is expected on the relationship path.
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return false;
|
|
555
607
|
}
|
|
556
608
|
catch {
|
|
557
609
|
return false;
|
|
@@ -561,6 +613,10 @@ export function resolveDagPiToolNames(task) {
|
|
|
561
613
|
if (isFrontendReviewTypedTerminalNode(task)) {
|
|
562
614
|
return [
|
|
563
615
|
...DAG_PI_READONLY_TOOLS,
|
|
616
|
+
// complete_review_scope comes from createFrontendReviewScopeProtocol;
|
|
617
|
+
// the Pi SDK activates custom tools only when their name is in this
|
|
618
|
+
// allowlist, and omitting it silently drops the checkpoint tool the
|
|
619
|
+
// segmented runner instructs the model to call before any terminal.
|
|
564
620
|
"complete_review_scope",
|
|
565
621
|
"record_review_finding",
|
|
566
622
|
"approve_review",
|
|
@@ -570,6 +626,7 @@ export function resolveDagPiToolNames(task) {
|
|
|
570
626
|
if (isFrontendDesignTypedTerminalNode(task)) {
|
|
571
627
|
return [
|
|
572
628
|
...DAG_PI_READONLY_TOOLS,
|
|
629
|
+
// Same scope-checkpoint activation rule as the review node above.
|
|
573
630
|
"complete_review_scope",
|
|
574
631
|
"record_design_finding",
|
|
575
632
|
"approve_design",
|
|
@@ -590,6 +647,16 @@ export function resolveDagPiToolNames(task) {
|
|
|
590
647
|
return [...DAG_PI_READONLY_TOOLS, ...FRONTEND_SCOUT_EVIDENCE_TOOL_NAMES];
|
|
591
648
|
}
|
|
592
649
|
if (isFrontendPlanLedgerNode(task)) {
|
|
650
|
+
if (process.env.HARNESS_FRONTEND_PLAN_DECISION === "1") {
|
|
651
|
+
// Experimental decision path: expose exactly the decision toolset that
|
|
652
|
+
// createFrontendPlanDecisionTools registers. Relationship-path plan
|
|
653
|
+
// tools (record_route_selection … finalize_plan/adopt_staged_fact) are
|
|
654
|
+
// not present in that toolset, so returning them here would leave the
|
|
655
|
+
// decision tools unactivated (Pi SDK activates custom tools only when
|
|
656
|
+
// their name is in this allowlist) — the planner then cannot record
|
|
657
|
+
// module placements and finalize always reports unplaced-requirement.
|
|
658
|
+
return [...FRONTEND_DECISION_PLAN_TOOL_NAMES];
|
|
659
|
+
}
|
|
593
660
|
return [
|
|
594
661
|
// Plan is a decision-only node. Contract/scout own source and repository
|
|
595
662
|
// discovery; omitting read tools prevents a planner from spending its
|
|
@@ -1116,6 +1183,112 @@ async function loadContractRequirementInheritance(runDir) {
|
|
|
1116
1183
|
}
|
|
1117
1184
|
return byId;
|
|
1118
1185
|
}
|
|
1186
|
+
export async function resolveFrontendDecisionAuthority(input) {
|
|
1187
|
+
const inheritance = await loadContractRequirementInheritance(input.runDir);
|
|
1188
|
+
if (inheritance.size === 0)
|
|
1189
|
+
return undefined;
|
|
1190
|
+
const requirements = [...inheritance].map(([id, record]) => ({ id, expectedOutcome: record.text ?? id }));
|
|
1191
|
+
const executionGroups = collectFrontendExecutionGroups([...inheritance].map(([id, record]) => ({ id, execution: record.execution })));
|
|
1192
|
+
const behaviorGroups = executionGroups
|
|
1193
|
+
.filter(group => group.kind === "behavior")
|
|
1194
|
+
.map(group => ({ id: group.id, requirementIds: group.requirementIds, minimumEvidenceLevel: "mounted" }));
|
|
1195
|
+
// Contract `execution` is model-authored and optional (record_requirement
|
|
1196
|
+
// accepts it only when the contract model decided to attach a group). The
|
|
1197
|
+
// decision authority must be deterministic, so every requirement that did not
|
|
1198
|
+
// receive a behavior execution group still gets its own fallback group —
|
|
1199
|
+
// otherwise a contract run without execution metadata leaves all requirements
|
|
1200
|
+
// unplaced (r3: every attempt failed unplaced-requirement because behavior
|
|
1201
|
+
// groups were empty and the model's placement ids were unknown-behavior-group).
|
|
1202
|
+
{
|
|
1203
|
+
const groupedRequirementIds = new Set(behaviorGroups.flatMap(group => group.requirementIds));
|
|
1204
|
+
for (const requirement of requirements) {
|
|
1205
|
+
if (!groupedRequirementIds.has(requirement.id)) {
|
|
1206
|
+
behaviorGroups.push({
|
|
1207
|
+
id: requirement.id,
|
|
1208
|
+
requirementIds: [requirement.id],
|
|
1209
|
+
minimumEvidenceLevel: "mounted",
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
// The frozen command directory (friendly ids like
|
|
1215
|
+
// verify-repo-quick-verification-2) is the vocabulary both the model and the
|
|
1216
|
+
// relationship plan reference. Freeze those ids here — never the opaque
|
|
1217
|
+
// canonicalKeys of the verification bundle (they encode cwd+args and are not
|
|
1218
|
+
// the ids `record_verification_focus` accepts; freezing them makes every
|
|
1219
|
+
// finalize fail with unknown-command for a model that correctly follows the
|
|
1220
|
+
// PRD/H-C vocabulary).
|
|
1221
|
+
const { deriveFrontendVerifyCommandDirectoryFromRun, deriveRequirementOwnershipFromRun } = await import("../workflows/dag/frontend-implementation-contract.js");
|
|
1222
|
+
const verifyDirectory = await deriveFrontendVerifyCommandDirectoryFromRun(input.runDir);
|
|
1223
|
+
const requirementOwnership = await deriveRequirementOwnershipFromRun(input.runDir);
|
|
1224
|
+
const { deriveFrontendInteractionIdsFromRun, deriveFrontendAllowedMockStrategiesFromRun } = await import("../workflows/dag/frontend-implementation-contract.js");
|
|
1225
|
+
const interactionIds = await deriveFrontendInteractionIdsFromRun(input.runDir);
|
|
1226
|
+
const allowedMockStrategies = await deriveFrontendAllowedMockStrategiesFromRun(input.runDir);
|
|
1227
|
+
const commandIds = new Set();
|
|
1228
|
+
for (const entry of verifyDirectory)
|
|
1229
|
+
commandIds.add(entry.commandId);
|
|
1230
|
+
const requirementSha256 = input.sourceBinding?.schemaVersion === 2
|
|
1231
|
+
? (input.sourceBinding.inputDigest ?? "")
|
|
1232
|
+
: "";
|
|
1233
|
+
if (!/^[a-f0-9]{64}$/.test(requirementSha256))
|
|
1234
|
+
return undefined;
|
|
1235
|
+
return {
|
|
1236
|
+
sourceBinding: { requirementSha256 },
|
|
1237
|
+
observedRequirementSha256: requirementSha256,
|
|
1238
|
+
requirements,
|
|
1239
|
+
behaviorGroups,
|
|
1240
|
+
allowedPaths: input.task.allowedPaths ?? [],
|
|
1241
|
+
forbiddenPaths: input.task.forbiddenPaths ?? [],
|
|
1242
|
+
commands: [...commandIds].map(id => ({ id })),
|
|
1243
|
+
requirementOwnership,
|
|
1244
|
+
interactionIds: interactionIds.length > 0 ? interactionIds : undefined,
|
|
1245
|
+
allowedMockStrategies: allowedMockStrategies.length > 0 ? allowedMockStrategies : undefined,
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
/** Decision-path Plan prompt. The relationship `subtask_prompt`
|
|
1249
|
+
* (record_state_registry / record_plan_verification_target / finalize_plan) must
|
|
1250
|
+
* NOT be reused: it names tools the decision toolset does not expose and never
|
|
1251
|
+
* states the behavior-group ids the decision schema requires. This prompt
|
|
1252
|
+
* injects the frozen authority so the model can fill `modulePlacements[].id` and
|
|
1253
|
+
* `verificationFocus[].behaviorGroupId` correctly. */
|
|
1254
|
+
export function buildFrontendDecisionPlanPrompt(authority) {
|
|
1255
|
+
return [
|
|
1256
|
+
"DECISION PLAN (experimental): record semantic decisions only. The runtime expands behavior-group membership and derives the per-AC relationship table; do not write requirement-to-file coverage yourself.",
|
|
1257
|
+
"Frozen requirements (cover each exactly once through its behavior group):",
|
|
1258
|
+
JSON.stringify(authority.requirements),
|
|
1259
|
+
"Behavior groups — use these exact ids for modulePlacements[].id and verificationFocus[].behaviorGroupId:",
|
|
1260
|
+
JSON.stringify(authority.behaviorGroups),
|
|
1261
|
+
"Frozen verification command ids — use one for verificationFocus[].commandId:",
|
|
1262
|
+
JSON.stringify(authority.commands.map(command => command.id)),
|
|
1263
|
+
[
|
|
1264
|
+
"Record with these typed tools (each takes an `entry` field):",
|
|
1265
|
+
'- record_module_placement: {"id": "<behavior group id>", "paths": ["<repo-relative file>"]}',
|
|
1266
|
+
'- record_state_ownership: {"state": "<name>", "owner": "<behavior group id>", "applicable": true, "expectedBehavior": "<behavior>"} (use notApplicableReason for a non-applicable state)',
|
|
1267
|
+
'- record_verification_focus: {"id": "VT-...", "behaviorGroupId": "<behavior group id>", "file": "<test file>", "commandId": "<frozen command id>", "evidenceLevel": "unit|mounted|real-integration"}',
|
|
1268
|
+
'- record_reuse_decision: {"symbol": "<component>", "decision": "reuse-existing|new", "evidence": ["<path>"], "purpose": "<state or interaction name it covers>", "covers": ["<extra ids>"]} (set stylingStrategy once on the first row when the task mandates a style contract)',
|
|
1269
|
+
'- record_api_mock_boundary: {"boundary": "<METHOD /path>", "mode": "real|mock|not-needed", "evidence": "<why>"} (boundary is the identity: same boundary + replace:true flips its mode; real/mock also carry fixture + consumer)',
|
|
1270
|
+
'- record_data_flow: {"interaction": "<name>", "source": "<behavior group id>", "trigger": "<event>", "expectedBehavior": "<behavior>"}',
|
|
1271
|
+
'- record_dependency: {"name": "<dependency or none>"}',
|
|
1272
|
+
].join("\n"),
|
|
1273
|
+
"Every frozen requirement must belong to a behavior group, and every behavior group needs a module placement; a requirement with no placement fails finalize.",
|
|
1274
|
+
...(authority.interactionIds?.length ? [
|
|
1275
|
+
`Frozen interaction id vocabulary — record_data_flow interaction names MUST be exactly these ids (renames are deterministic finalize rejects): [${authority.interactionIds.join(", ")}].`,
|
|
1276
|
+
] : []),
|
|
1277
|
+
...(authority.allowedMockStrategies?.length ? [
|
|
1278
|
+
`Frozen Mock strategy allowance — the contract mockApi.strategy must be one of [${authority.allowedMockStrategies.join(", ")}] (mode real→native, mock→browser-intercept, all boundaries not-needed→not-needed).${authority.allowedMockStrategies.length === 1 && authority.allowedMockStrategies[0] === "not-needed" ? ` This run allows ONLY "not-needed": record every remote boundary with mode "not-needed" (the run has no deterministic Mock verification; preserve the real integration gap) — mode "mock" or "real" is a deterministic finalize reject.` : ""}`,
|
|
1279
|
+
] : []),
|
|
1280
|
+
...(authority.requirementOwnership?.length ? [
|
|
1281
|
+
"Requirement-file ownership is machine-enforced at finalize (violations are deterministic rejects, not review opinions):",
|
|
1282
|
+
...authority.requirementOwnership.map(entry => [
|
|
1283
|
+
`- ${entry.requirementIds.join(", ")}:`,
|
|
1284
|
+
entry.implementationFiles?.length ? ` implementation files allowed: [${entry.implementationFiles.join(", ")}]` : "",
|
|
1285
|
+
entry.verificationFiles?.length ? ` verification files allowed: [${entry.verificationFiles.join(", ")}]` : "",
|
|
1286
|
+
].filter(Boolean).join("\n")),
|
|
1287
|
+
] : []),
|
|
1288
|
+
"The requirements, execution constraints and prohibited wordings in the supplied input are authoritative review criteria, not suggestions: a component the task mandates by name must be recorded as its own decision=new reuse decision (never merged into an existing component), implementationTargets must only reference requirement-authorized files, and stylingStrategy must restate the mandated style contract verbatim without promising anything the input forbids.",
|
|
1289
|
+
"When all required facts are committed, call finalize_decision. On a failed finalize receipt, fix the listed record_* facts (resubmit a corrected row with the same identity and replace:true to replace it) and call finalize_decision again until it succeeds. Return no Markdown narrative.",
|
|
1290
|
+
].join("\n\n");
|
|
1291
|
+
}
|
|
1119
1292
|
/**
|
|
1120
1293
|
* Resolve task-source citations from the source-fidelity ledger before the
|
|
1121
1294
|
* planner starts. The planner names a frozen requirement id and one of its
|
|
@@ -1273,7 +1446,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1273
1446
|
]);
|
|
1274
1447
|
const { loadTypedEventStore, readCommittedEvents, writeTypedEventStoreJsonl, } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
1275
1448
|
const { adoptStagedFact, adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
1276
|
-
const { assemblePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-
|
|
1449
|
+
const { assemblePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-committed-facts.js");
|
|
1277
1450
|
let store = input.store;
|
|
1278
1451
|
const attemptId = input.attemptId;
|
|
1279
1452
|
let activeRequirementScope = [];
|
|
@@ -1289,7 +1462,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1289
1462
|
description: "Optional. When omitted, the runtime derives it from the contract requirement. Prefer omitting it to keep this tool call small.",
|
|
1290
1463
|
})),
|
|
1291
1464
|
implementationTargets: stringArray,
|
|
1292
|
-
verificationTargetIds: stringArray,
|
|
1465
|
+
verificationTargetIds: Type.Optional(stringArray),
|
|
1293
1466
|
evidenceGap: Type.Optional(Type.Object({
|
|
1294
1467
|
requirementId: optionalString,
|
|
1295
1468
|
description: Type.String({}),
|
|
@@ -1403,7 +1576,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1403
1576
|
});
|
|
1404
1577
|
async function adoptPlanFact(kind, requestId, fact) {
|
|
1405
1578
|
// A+B (AC-005): a provider-capability fact kind reaching the plan ledger
|
|
1406
|
-
// is out of route — it belongs to the
|
|
1579
|
+
// is out of route — it belongs to the provider capability channel,
|
|
1407
1580
|
// not the plan decision ledger. Route it through the frozen seven-kind
|
|
1408
1581
|
// matrix and fail closed to `unsupported-provider-capability` instead of
|
|
1409
1582
|
// silently widening the plan catalog.
|
|
@@ -1953,7 +2126,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1953
2126
|
const recordPlanRequirementTool = defineTool({
|
|
1954
2127
|
name: "record_plan_requirement",
|
|
1955
2128
|
label: "record_plan_requirement",
|
|
1956
|
-
description: "Commit one plan requirement entry (origin=plan plan-requirement fact). Call once per requirement. For a correction, re-submit the same id with replace=true; the ledger compiles the latest replacement. Entry carries id, implementationTargets,
|
|
2129
|
+
description: "Commit one plan requirement entry (origin=plan plan-requirement fact). Call once per requirement. Verification targets are authoritative in record_plan_verification_target; omit verificationTargetIds here unless repairing legacy input. For a correction, re-submit the same id with replace=true; the ledger compiles the latest replacement. Entry carries id, implementationTargets, and optional expectedOutcome (omit it — the runtime derives the outcome text from the contract requirement). IMPORTANT: batch up to 4 record_* calls per assistant message; never batch more than 4 — a larger single message risks output truncation under a small output window. Example: {\"entry\": {\"id\": \"<AC-XXX>\", \"implementationTargets\": [\"<deliverable file>\"]}}",
|
|
1957
2130
|
promptSnippet: "Commit 1-4 plan requirement entries (up to 4 per message).",
|
|
1958
2131
|
parameters: Type.Object({
|
|
1959
2132
|
entry: requirementSchema,
|
|
@@ -2047,7 +2220,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2047
2220
|
const recordPlanGroupCoverageTool = defineTool({
|
|
2048
2221
|
name: "record_plan_group_coverage", label: "record_plan_group_coverage",
|
|
2049
2222
|
description: "Submit shared implementation/verification references for one declared execution group. Runtime expands to every canonical member and retains its full outcome and source bindings. A shared VT must actually verify each independent condition. Use per-requirement records for differences; never create UI for constraints or exclusions. replace:true explicitly revises the group.",
|
|
2050
|
-
parameters: Type.Object({ id: Type.String({ minLength: 1 }), implementationTargets: stringArray, verificationTargetIds: stringArray, replace: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
2223
|
+
parameters: Type.Object({ id: Type.String({ minLength: 1 }), implementationTargets: stringArray, verificationTargetIds: Type.Optional(stringArray), replace: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
2051
2224
|
async execute(callId, params, signal, onUpdate, ctx) {
|
|
2052
2225
|
const group = executionGroups.find(g => g.id === params.id && g.kind !== "unclassified");
|
|
2053
2226
|
if (!group || (activeRequirementScope.length && group.requirementIds.some(id => !activeRequirementScope.includes(id))))
|
|
@@ -2056,11 +2229,11 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2056
2229
|
for (const id of group.requirementIds) {
|
|
2057
2230
|
const existing = readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "plan-requirement" && r.fact.entry?.id === id).at(-1)?.fact.entry;
|
|
2058
2231
|
if (existing && !params.replace) {
|
|
2059
|
-
if (JSON.stringify(existing.implementationTargets) !== JSON.stringify(params.implementationTargets)
|
|
2232
|
+
if (JSON.stringify(existing.implementationTargets) !== JSON.stringify(params.implementationTargets))
|
|
2060
2233
|
return planToolReceipt({ ok: false, kind: "plan-requirement", code: "FACT_IDENTITY_CONFLICT", error: `${id}: existing coverage differs; use replace:true to revise explicitly` });
|
|
2061
2234
|
continue;
|
|
2062
2235
|
}
|
|
2063
|
-
last = await recordPlanRequirementTool.execute(`${callId}:${id}`, { entry: { id, implementationTargets: params.implementationTargets, verificationTargetIds: params.verificationTargetIds }, ...(params.replace ? { replace: true } : {}) }, signal, onUpdate, ctx);
|
|
2236
|
+
last = await recordPlanRequirementTool.execute(`${callId}:${id}`, { entry: { id, implementationTargets: params.implementationTargets, ...(params.verificationTargetIds ? { verificationTargetIds: params.verificationTargetIds } : {}) }, ...(params.replace ? { replace: true } : {}) }, signal, onUpdate, ctx);
|
|
2064
2237
|
if (!last.details?.ok)
|
|
2065
2238
|
return last;
|
|
2066
2239
|
}
|
|
@@ -2070,7 +2243,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2070
2243
|
const recordPlanVerificationTargetTool = defineTool({
|
|
2071
2244
|
name: "record_plan_verification_target",
|
|
2072
2245
|
label: "record_plan_verification_target",
|
|
2073
|
-
description: "Commit one plan verification target entry (origin=plan plan-verification-target fact). Reference a frozen verification command by commandId (see the frozen command directory in your prompt: static commands are project-wide checks traced by file and command only; behavior commands
|
|
2246
|
+
description: "Commit one plan verification target entry (origin=plan plan-verification-target fact). Reference a frozen verification command by commandId (see the frozen command directory in your prompt: static commands are project-wide checks traced by file and command only; behavior commands bind an existing affected test file; preserve its test names). For behavior targets, call once per distinct behavior, not mechanically once per requirement: one target may cover multiple related requirementIds. For a correction, re-submit the same id with replace=true; the ledger compiles the latest replacement. A target id identifies a contract entry only; never require it in test names. Results bind by frozen command and test file. Entry carries id, commandId, file, requirementIds, and uiStates; optional scope (unit | component | integration) is display-only. Free-form symbol text is not accepted. IMPORTANT: batch up to 4 record_* calls per assistant message; never batch more than 4 — a larger single message risks output truncation under a small output window. Example: {\"entry\": {\"id\": \"VT-DASHBOARD-SHELL\", \"commandId\": \"<frozen behavior command id>\", \"file\": \"<test file>\", \"requirementIds\": [\"AC-001\", \"AC-002\"], \"uiStates\": []}}" +
|
|
2074
2247
|
(input.canonicalVerificationTargetIds &&
|
|
2075
2248
|
input.canonicalVerificationTargetIds.length > 0
|
|
2076
2249
|
? ` Frozen canonical behavior target ids (use exactly for behavior targets): ${input.canonicalVerificationTargetIds.join(", ")}.`
|
|
@@ -2549,6 +2722,9 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2549
2722
|
finalizePlanTool,
|
|
2550
2723
|
],
|
|
2551
2724
|
});
|
|
2725
|
+
const durableFinalizePlanTool = durable.customTools.find((tool) => typeof tool === "object" && tool !== null && tool.name === "finalize_plan");
|
|
2726
|
+
if (!durableFinalizePlanTool)
|
|
2727
|
+
throw new Error("frontend plan durable finalize tool unavailable");
|
|
2552
2728
|
return {
|
|
2553
2729
|
customTools: [...durable.customTools, { ...readPlanFactsTool, execute: async (...args) => { await durable.flush(); return readPlanFactsTool.execute(...args); } }],
|
|
2554
2730
|
adoptCommittedFacts: async (records) => durable.commitExternal(async () => {
|
|
@@ -2617,6 +2793,17 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2617
2793
|
];
|
|
2618
2794
|
},
|
|
2619
2795
|
flush: durable.flush,
|
|
2796
|
+
finalizePlan: (params) => durableFinalizePlanTool.execute(`${attemptId}:auto-finalize-plan`, {
|
|
2797
|
+
...(params?.residualRisks
|
|
2798
|
+
? { residualRisks: [...params.residualRisks] }
|
|
2799
|
+
: {}),
|
|
2800
|
+
...(params?.realIntegrationGap
|
|
2801
|
+
? { realIntegrationGap: params.realIntegrationGap }
|
|
2802
|
+
: {}),
|
|
2803
|
+
}, undefined, undefined,
|
|
2804
|
+
// The wrapper passes this through to the typed tool; finalize itself
|
|
2805
|
+
// does not inspect the extension context.
|
|
2806
|
+
{}),
|
|
2620
2807
|
committedFactCount: () => readCommittedEvents(store, attemptId).length,
|
|
2621
2808
|
committedRequirementIds: () => {
|
|
2622
2809
|
const ids = new Set();
|
|
@@ -2633,6 +2820,674 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2633
2820
|
committedFacts: () => readCommittedEvents(store, attemptId),
|
|
2634
2821
|
};
|
|
2635
2822
|
}
|
|
2823
|
+
/** Translate derived-patch validation findings into decision-channel
|
|
2824
|
+
* vocabulary. Each zod issue path names a COMPILED patch array, but the model
|
|
2825
|
+
* authored record_* facts — so quote the offending entry's identity and name
|
|
2826
|
+
* the tool that owns it (r14: "uiComponentChoices.0.specReference.section:
|
|
2827
|
+
* Required" is unactionable when the model has never heard of specReference). */
|
|
2828
|
+
export function translateDecisionPatchFindings(message, decision) {
|
|
2829
|
+
const body = message.replace(/^invalid-output:\s*/, "");
|
|
2830
|
+
return body
|
|
2831
|
+
.split(/;\s*/)
|
|
2832
|
+
.map(issue => {
|
|
2833
|
+
const choice = issue.match(/uiComponentChoices\.(\d+)\.(.+)/);
|
|
2834
|
+
if (choice) {
|
|
2835
|
+
const row = decision.reuseDecisions[Number(choice[1])];
|
|
2836
|
+
const base = row
|
|
2837
|
+
? `record_reuse_decision purpose "${row.purpose}" (component ${row.symbol}, decision ${row.decision}): ${issue}`
|
|
2838
|
+
: `record_reuse_decision row ${choice[1]}: ${issue}`;
|
|
2839
|
+
const field = choice[2];
|
|
2840
|
+
return field?.startsWith("specReference")
|
|
2841
|
+
? `${base} — pass specSection (the AC id / task-source section it implements) on that entry, or use decision "reuse-existing"`
|
|
2842
|
+
: base;
|
|
2843
|
+
}
|
|
2844
|
+
const focus = issue.match(/verificationTargets\.(\d+)\.(.+)/);
|
|
2845
|
+
if (focus) {
|
|
2846
|
+
const row = decision.verificationFocus[Number(focus[1])];
|
|
2847
|
+
if (!row)
|
|
2848
|
+
return `record_verification_focus row ${focus[1]}: ${issue}`;
|
|
2849
|
+
return `record_verification_focus ${row.id}: ${issue} — behavior targets derive uiStates from record_state_ownership rows whose owner equals behaviorGroupId "${row.behaviorGroupId}"; record one for this group if missing`;
|
|
2850
|
+
}
|
|
2851
|
+
const state = issue.match(/uiStates\.(\d+)\.(.+)/);
|
|
2852
|
+
if (state) {
|
|
2853
|
+
const row = decision.stateOwnership[Number(state[1])];
|
|
2854
|
+
return row
|
|
2855
|
+
? `record_state_ownership "${row.state}" (owner ${row.owner}): ${issue}`
|
|
2856
|
+
: `record_state_ownership row ${state[1]}: ${issue}`;
|
|
2857
|
+
}
|
|
2858
|
+
const interaction = issue.match(/interactions\.(\d+)\.(.+)/);
|
|
2859
|
+
if (interaction) {
|
|
2860
|
+
const row = decision.dataFlows[Number(interaction[1])];
|
|
2861
|
+
return row
|
|
2862
|
+
? `record_data_flow "${row.interaction}": ${issue}`
|
|
2863
|
+
: `record_data_flow row ${interaction[1]}: ${issue}`;
|
|
2864
|
+
}
|
|
2865
|
+
const requirement = issue.match(/requirements\.(\d+)\.(.+)/);
|
|
2866
|
+
if (requirement) {
|
|
2867
|
+
return `record_module_placement row ${requirement[1]}: ${issue}`;
|
|
2868
|
+
}
|
|
2869
|
+
return issue;
|
|
2870
|
+
})
|
|
2871
|
+
.join("; ");
|
|
2872
|
+
}
|
|
2873
|
+
/** Read the recovery child's read-only parent decision snapshot (written by
|
|
2874
|
+
* the recovery continuation). Absent file → undefined (fresh plan). */
|
|
2875
|
+
export async function readParentDecisionSnapshot(runDir, nodeId) {
|
|
2876
|
+
const snapshotPath = path.join(runDir, nodeId, "parent-decision-snapshot.json");
|
|
2877
|
+
let raw;
|
|
2878
|
+
try {
|
|
2879
|
+
raw = await readFile(snapshotPath, "utf8");
|
|
2880
|
+
}
|
|
2881
|
+
catch (error) {
|
|
2882
|
+
if (error.code === "ENOENT")
|
|
2883
|
+
return undefined;
|
|
2884
|
+
throw error;
|
|
2885
|
+
}
|
|
2886
|
+
const parsed = JSON.parse(raw);
|
|
2887
|
+
if (!Array.isArray(parsed.facts))
|
|
2888
|
+
throw new Error(`parent decision snapshot malformed: ${snapshotPath}`);
|
|
2889
|
+
return parsed;
|
|
2890
|
+
}
|
|
2891
|
+
const decisionKindToToolName = (kind) => `record_${kind.replaceAll("-", "_")}`;
|
|
2892
|
+
/** D (local design repair): replay the parent's committed decision facts
|
|
2893
|
+
* through the child's durable tools — the normal commit entry with the
|
|
2894
|
+
* child's own binding; the finalize terminal is never inherited. Idempotent:
|
|
2895
|
+
* stable per-fact callIds make retries replay receipts instead of duplicating. */
|
|
2896
|
+
export async function replayParentDecisionSnapshot(snapshot, tools) {
|
|
2897
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
2898
|
+
for (const [index, fact] of snapshot.facts.entries()) {
|
|
2899
|
+
const toolName = decisionKindToToolName(fact.kind);
|
|
2900
|
+
const tool = byName.get(toolName);
|
|
2901
|
+
if (!tool) {
|
|
2902
|
+
throw new Error(`parent decision replay: no tool for fact kind "${fact.kind}"`);
|
|
2903
|
+
}
|
|
2904
|
+
const receipt = await tool.execute(`parent-replay:${index}`, {
|
|
2905
|
+
entry: fact.entry,
|
|
2906
|
+
});
|
|
2907
|
+
if (receipt.details?.ok !== true) {
|
|
2908
|
+
throw new Error(`parent decision replay failed for ${toolName} (identity ${JSON.stringify(fact.entry.purpose ??
|
|
2909
|
+
fact.entry.state ??
|
|
2910
|
+
fact.entry.interaction ??
|
|
2911
|
+
fact.entry.id ??
|
|
2912
|
+
fact.entry.boundary ??
|
|
2913
|
+
"?")}): ${receipt.details?.error ?? "unknown error"}`);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
export async function createFrontendPlanDecisionTools(input) {
|
|
2918
|
+
const [{ Type }, { defineTool }, { assembleDecisionContractFromFacts, assembleExperimentPlanContract, buildFrontendPlanRelationshipPatch }] = await Promise.all([
|
|
2919
|
+
import("typebox"),
|
|
2920
|
+
import("@earendil-works/pi-coding-agent"),
|
|
2921
|
+
import("../workflows/dag/frontend-plan-decision-contract.js"),
|
|
2922
|
+
]);
|
|
2923
|
+
const { readCommittedEvents } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
2924
|
+
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
2925
|
+
const { createDurableFrontendTools } = await import("../workflows/dag/frontend-durable-tools.js");
|
|
2926
|
+
let store = input.store;
|
|
2927
|
+
const { attemptId, authority } = input;
|
|
2928
|
+
const optionalStringArray = Type.Optional(Type.Array(Type.String({ minLength: 1 })));
|
|
2929
|
+
const optionalString = Type.Optional(Type.String({ minLength: 1 }));
|
|
2930
|
+
// In-session pre-validation of the derived relationship patch. The
|
|
2931
|
+
// post-session bridge runs the identical analysis; running it first inside
|
|
2932
|
+
// finalize_decision turns findings into a bounded in-session correction
|
|
2933
|
+
// (fix facts, finalize again) instead of an attempt burn. Skipped when the
|
|
2934
|
+
// caller does not supply the runtime skeleton/sourceBinding.
|
|
2935
|
+
let requiredDeliverablesCache;
|
|
2936
|
+
const prevalidateDerivedPatch = async () => {
|
|
2937
|
+
if (!input.skeleton || !input.sourceBinding)
|
|
2938
|
+
return { ok: true, canonicalSha256: "", canonical: {} };
|
|
2939
|
+
const committed = readCommittedEvents(store, attemptId);
|
|
2940
|
+
const decision = assembleDecisionContractFromFacts(committed.map(record => record.fact));
|
|
2941
|
+
requiredDeliverablesCache ??= (async () => {
|
|
2942
|
+
const map = new Map();
|
|
2943
|
+
try {
|
|
2944
|
+
const { readTypedEventStoreFromJsonl } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
2945
|
+
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl"));
|
|
2946
|
+
for (const record of [...contractRecords].reverse()) {
|
|
2947
|
+
const fact = record.fact;
|
|
2948
|
+
if (!fact || fact.origin !== "contract" || fact.kind !== "required-deliverables")
|
|
2949
|
+
continue;
|
|
2950
|
+
for (const item of Array.isArray(fact.items) ? fact.items : []) {
|
|
2951
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
2952
|
+
continue;
|
|
2953
|
+
const requirementId = item.requirementId;
|
|
2954
|
+
const file = item.path;
|
|
2955
|
+
if (typeof requirementId !== "string" || typeof file !== "string")
|
|
2956
|
+
continue;
|
|
2957
|
+
const paths = map.get(requirementId) ?? [];
|
|
2958
|
+
paths.push(file);
|
|
2959
|
+
map.set(requirementId, paths);
|
|
2960
|
+
}
|
|
2961
|
+
break;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
catch {
|
|
2965
|
+
// Missing/unreadable contract ledger → no deliverable obligations.
|
|
2966
|
+
}
|
|
2967
|
+
return map;
|
|
2968
|
+
})();
|
|
2969
|
+
const concreteWriteSet = [
|
|
2970
|
+
...new Set([
|
|
2971
|
+
...decision.modulePlacements.flatMap(item => item.paths),
|
|
2972
|
+
...decision.verificationFocus.map(item => item.file),
|
|
2973
|
+
]),
|
|
2974
|
+
];
|
|
2975
|
+
const { applyFrontendContractMergePatch, analyzeFrontendPlanPatchCandidate, FrontendContractFailure, PlanPolicyPrecheckFailure, serializeDeterministicJson, } = await import("../workflows/dag/frontend-implementation-contract.js");
|
|
2976
|
+
try {
|
|
2977
|
+
const patch = buildFrontendPlanRelationshipPatch({
|
|
2978
|
+
decision,
|
|
2979
|
+
authority,
|
|
2980
|
+
concreteWriteSet,
|
|
2981
|
+
requiredDeliverables: await requiredDeliverablesCache,
|
|
2982
|
+
});
|
|
2983
|
+
const merged = applyFrontendContractMergePatch(input.skeleton, patch);
|
|
2984
|
+
const analysis = await analyzeFrontendPlanPatchCandidate({
|
|
2985
|
+
runDir: input.runDir,
|
|
2986
|
+
rawContractText: serializeDeterministicJson(merged),
|
|
2987
|
+
sourceBinding: input.sourceBinding,
|
|
2988
|
+
});
|
|
2989
|
+
return {
|
|
2990
|
+
ok: true,
|
|
2991
|
+
canonicalSha256: createHash("sha256")
|
|
2992
|
+
.update(serializeDeterministicJson(analysis.canonical))
|
|
2993
|
+
.digest("hex"),
|
|
2994
|
+
canonical: analysis.canonical,
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
catch (error) {
|
|
2998
|
+
if (error instanceof PlanPolicyPrecheckFailure) {
|
|
2999
|
+
// Template the fix: every uncovered interaction/state maps to a
|
|
3000
|
+
// fill-in record_reuse_decision row (the decision channel's
|
|
3001
|
+
// component-choice tool).
|
|
3002
|
+
const suggestions = error.findings
|
|
3003
|
+
.filter(finding => finding.code === "ui-design-coverage-missing" && finding.path)
|
|
3004
|
+
.map(finding => ({
|
|
3005
|
+
tool: "record_reuse_decision",
|
|
3006
|
+
args: {
|
|
3007
|
+
entry: {
|
|
3008
|
+
symbol: "<name the existing or new component>",
|
|
3009
|
+
decision: "reuse-existing",
|
|
3010
|
+
evidence: ["<existing repo file that proves this reuse>"],
|
|
3011
|
+
purpose: finding.path,
|
|
3012
|
+
},
|
|
3013
|
+
},
|
|
3014
|
+
}));
|
|
3015
|
+
const suggestionBlock = suggestions.length > 0
|
|
3016
|
+
? ` Suggested record_* calls (copy, fill component, submit): ${JSON.stringify(suggestions)}`
|
|
3017
|
+
: "";
|
|
3018
|
+
return {
|
|
3019
|
+
ok: false,
|
|
3020
|
+
error: `finalize_decision pre-validation failed (fix the listed plan facts, then call finalize_decision again): ${error.message}${suggestionBlock}`,
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
// FrontendContractFailure messages carry zod issue paths and are
|
|
3024
|
+
// translated into record_* vocabulary; the bridge compiler throws
|
|
3025
|
+
// decision-state-unbindable / decision-styling-strategy-conflict
|
|
3026
|
+
// diagnostics that are already model-fixable as written.
|
|
3027
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3028
|
+
const translated = error instanceof FrontendContractFailure
|
|
3029
|
+
? translateDecisionPatchFindings(message, decision)
|
|
3030
|
+
: message;
|
|
3031
|
+
return {
|
|
3032
|
+
ok: false,
|
|
3033
|
+
error: `finalize_decision pre-validation failed (fix the listed facts with the named record_* tools — resubmit a corrected row with the same identity and replace:true to replace it — then call finalize_decision again): ${translated}`,
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
};
|
|
3037
|
+
const planToolReceipt = (details) => ({
|
|
3038
|
+
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
3039
|
+
details,
|
|
3040
|
+
});
|
|
3041
|
+
async function adoptDecisionFact(kind, requestId, fact) {
|
|
3042
|
+
try {
|
|
3043
|
+
const staged = stageTypedEventFact({
|
|
3044
|
+
store,
|
|
3045
|
+
requestId,
|
|
3046
|
+
attemptId,
|
|
3047
|
+
fact: fact,
|
|
3048
|
+
});
|
|
3049
|
+
const committed = await adoptTypedEventFact({
|
|
3050
|
+
store,
|
|
3051
|
+
requestId,
|
|
3052
|
+
attemptId,
|
|
3053
|
+
fact: fact,
|
|
3054
|
+
eventId: staged.eventId,
|
|
3055
|
+
expectedRevision: store.revision,
|
|
3056
|
+
});
|
|
3057
|
+
return { ok: true, kind, eventId: committed.eventId, revision: committed.revision, error: "" };
|
|
3058
|
+
}
|
|
3059
|
+
catch (error) {
|
|
3060
|
+
return {
|
|
3061
|
+
ok: false,
|
|
3062
|
+
kind,
|
|
3063
|
+
code: error?.code,
|
|
3064
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
const record = (name, label, description, entrySchema, gate) => defineTool({
|
|
3069
|
+
name,
|
|
3070
|
+
label,
|
|
3071
|
+
description,
|
|
3072
|
+
promptSnippet: `Record ${label}.`,
|
|
3073
|
+
parameters: Type.Object({ entry: entrySchema }, { additionalProperties: false }),
|
|
3074
|
+
async execute(_toolCallId, params) {
|
|
3075
|
+
const entry = params?.entry;
|
|
3076
|
+
const kind = name.replace("record_", "").replace(/_/g, "-");
|
|
3077
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
3078
|
+
return planToolReceipt({ ok: false, kind, error: `${name} requires a non-empty entry object` });
|
|
3079
|
+
}
|
|
3080
|
+
if (gate) {
|
|
3081
|
+
const rejected = await gate(entry);
|
|
3082
|
+
if (rejected)
|
|
3083
|
+
return rejected;
|
|
3084
|
+
}
|
|
3085
|
+
const result = await adoptDecisionFact(kind, `${attemptId}:${name}:${randomUUID()}`, { kind, origin: "plan", entry });
|
|
3086
|
+
return planToolReceipt(result);
|
|
3087
|
+
},
|
|
3088
|
+
});
|
|
3089
|
+
const recordModulePlacementTool = record("record_module_placement", "module placement", "Record one module placement: which behavior-group/module id maps to which concrete files. Example: {\"entry\": {\"id\": \"page\", \"paths\": [\"src/page.tsx\"]}}", Type.Object({ id: Type.String({ minLength: 1 }), paths: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }) }, { additionalProperties: false }));
|
|
3090
|
+
const recordReuseDecisionTool = record("record_reuse_decision", "reuse decision", "Record one component decision: which component serves which UI state/interaction (purpose). decision \"reuse-existing\" = an existing repo component (evidence[0] = the repo file that proves it); \"specified\" = the frontend spec mandates it; \"new\" = the task source mandates a bounded addition — specified/new require specSection naming the spec/task-source declaration they implement (e.g. an AC id). Set stylingStrategy once (on the first call) when the task mandates a style contract. Resubmit the same purpose with replace:true to correct a row. Example: {\"entry\": {\"symbol\": \"Spinner\", \"decision\": \"reuse-existing\", \"evidence\": [\"src/ui/Spinner.tsx\"], \"purpose\": \"loading\"}} or {\"entry\": {\"symbol\": \"SmokeCounter\", \"decision\": \"new\", \"evidence\": [\"需求.md\"], \"purpose\": \"increment\", \"specSection\": \"AC-FE-002\", \"stylingStrategy\": \"compact, stable, animation-free single card\"}}", Type.Object({
|
|
3091
|
+
symbol: Type.String({ minLength: 1 }),
|
|
3092
|
+
decision: Type.Union([Type.Literal("specified"), Type.Literal("reuse-existing"), Type.Literal("new")]),
|
|
3093
|
+
evidence: Type.Array(Type.String()),
|
|
3094
|
+
specSection: Type.Optional(Type.String({ minLength: 1 })),
|
|
3095
|
+
purpose: Type.String({ minLength: 1 }),
|
|
3096
|
+
covers: optionalStringArray,
|
|
3097
|
+
rationale: optionalString,
|
|
3098
|
+
stylingStrategy: optionalString,
|
|
3099
|
+
}, { additionalProperties: false }), async (entry) => {
|
|
3100
|
+
// specified/new compile into a canonical specReference whose .section
|
|
3101
|
+
// the contract schema mandates; without the section the row can only
|
|
3102
|
+
// fail at finalize, so demand it here where the model can still act.
|
|
3103
|
+
if (entry.decision === "specified") {
|
|
3104
|
+
const evidenceList = Array.isArray(entry.evidence) ? entry.evidence : [];
|
|
3105
|
+
const evidencePath = typeof evidenceList[0] === "string" ? evidenceList[0] : "";
|
|
3106
|
+
const candidates = input.componentSpecCandidatePaths ?? [];
|
|
3107
|
+
if (candidates.length === 0) {
|
|
3108
|
+
return planToolReceipt({
|
|
3109
|
+
ok: false,
|
|
3110
|
+
kind: "reuse-decision",
|
|
3111
|
+
error: `record_reuse_decision specified-requires-spec-candidates: decision "specified" means a declared component spec mandates this choice, but this task declares no component spec candidates — use decision "new" (task-source-mandated addition, evidence[0] = the task-source file) or "reuse-existing"`,
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
if (evidencePath && !candidates.includes(evidencePath)) {
|
|
3115
|
+
return planToolReceipt({
|
|
3116
|
+
ok: false,
|
|
3117
|
+
kind: "reuse-decision",
|
|
3118
|
+
error: `record_reuse_decision specified-spec-reference-outside-candidates: evidence[0] "${evidencePath}" is not a declared component spec candidate; declared candidates: [${candidates.join(", ")}]`,
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
if ((entry.decision === "specified" || entry.decision === "new") &&
|
|
3123
|
+
!(typeof entry.specSection === "string" && entry.specSection.trim())) {
|
|
3124
|
+
return planToolReceipt({
|
|
3125
|
+
ok: false,
|
|
3126
|
+
kind: "reuse-decision",
|
|
3127
|
+
error: `record_reuse_decision spec-section-required: decision "${entry.decision}" must name the spec/task-source declaration it implements via specSection (e.g. an AC id); use decision "reuse-existing" for existing repo conventions. Example: {"entry": {"symbol": "SmokeCounter", "decision": "new", "evidence": ["src/components/SmokeCounter.tsx"], "purpose": "render the counter card", "specSection": "AC-FE-001"}}`,
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
return undefined;
|
|
3131
|
+
});
|
|
3132
|
+
const recordStateOwnershipTool = record("record_state_ownership", "state ownership", "Record one UI state and its owner, applicability and expected behavior. owner must equal the behaviorGroupId of the record_verification_focus entries it supports; every behavior group referenced by a verification focus needs at least one state row or finalize fails. Resubmit the same state name with replace:true to correct a row. Example: {\"entry\": {\"state\": \"loading\", \"owner\": \"page\", \"applicable\": true, \"expectedBehavior\": \"render loading\"}}", Type.Object({
|
|
3133
|
+
state: Type.String({ minLength: 1 }),
|
|
3134
|
+
owner: Type.String({ minLength: 1 }),
|
|
3135
|
+
applicable: Type.Boolean(),
|
|
3136
|
+
expectedBehavior: optionalString,
|
|
3137
|
+
notApplicableReason: optionalString,
|
|
3138
|
+
}, { additionalProperties: false }));
|
|
3139
|
+
const frozenInteractionIds = authority.interactionIds ?? [];
|
|
3140
|
+
const recordDataFlowTool = record("record_data_flow", "data flow", "Record one interaction data flow: source behavior group, trigger and expected behavior. One row per interaction name; resubmit the same interaction name to replace it. When a frozen interaction id vocabulary is declared, the interaction name MUST be one of the declared ids — wrong-named records are rejected at submission and stale ones are retracted via retract_data_flow. Example: {\"entry\": {\"interaction\": \"load\", \"source\": \"page\", \"trigger\": \"submit query\", \"expectedBehavior\": \"show loading then results\"}}", Type.Object({
|
|
3141
|
+
interaction: Type.String({ minLength: 1 }),
|
|
3142
|
+
source: Type.String({ minLength: 1 }),
|
|
3143
|
+
trigger: Type.String({ minLength: 1 }),
|
|
3144
|
+
expectedBehavior: Type.String({ minLength: 1 }),
|
|
3145
|
+
}, { additionalProperties: false }), frozenInteractionIds.length > 0
|
|
3146
|
+
? async (entry) => {
|
|
3147
|
+
const name = typeof entry.interaction === "string" ? entry.interaction : "";
|
|
3148
|
+
if (name && !frozenInteractionIds.includes(name)) {
|
|
3149
|
+
return planToolReceipt({
|
|
3150
|
+
ok: false,
|
|
3151
|
+
kind: "data-flow",
|
|
3152
|
+
code: "INTERACTION_ID_NOT_DECLARED",
|
|
3153
|
+
error: `interaction "${name}" is not in the frozen vocabulary; allowed: [${frozenInteractionIds.join(", ")}]. Submit the declared id. To remove an already-recorded wrong interaction, call retract_data_flow with {"interaction": "${name}"}.`,
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
return undefined;
|
|
3157
|
+
}
|
|
3158
|
+
: undefined);
|
|
3159
|
+
const retractDataFlowTool = defineTool({
|
|
3160
|
+
name: "retract_data_flow",
|
|
3161
|
+
label: "retract data flow",
|
|
3162
|
+
description: "Retract a previously recorded interaction data flow (e.g. one recorded under a wrong interaction id). The retracted record and every fact keyed to that interaction name stop participating in the compiled plan. Declared vocabulary ids cannot be retracted. Example: {\"interaction\": \"increment-counter\", \"reason\": \"recorded under a wrong id\"}",
|
|
3163
|
+
promptSnippet: "Retract one interaction data flow by interaction name.",
|
|
3164
|
+
parameters: Type.Object({
|
|
3165
|
+
interaction: Type.String({ minLength: 1 }),
|
|
3166
|
+
reason: Type.Optional(Type.String({ minLength: 1 })),
|
|
3167
|
+
}, { additionalProperties: false }),
|
|
3168
|
+
async execute(_toolCallId, params) {
|
|
3169
|
+
const interaction = typeof params?.interaction === "string" ? params.interaction : "";
|
|
3170
|
+
if (!interaction) {
|
|
3171
|
+
return planToolReceipt({ ok: false, kind: "data-flow", error: "retract_data_flow requires a non-empty interaction" });
|
|
3172
|
+
}
|
|
3173
|
+
if (frozenInteractionIds.includes(interaction)) {
|
|
3174
|
+
return planToolReceipt({ ok: false, kind: "data-flow", error: `interaction "${interaction}" is a declared vocabulary id and cannot be retracted` });
|
|
3175
|
+
}
|
|
3176
|
+
const result = await adoptDecisionFact("data-flow", `${attemptId}:retract_data_flow:${randomUUID()}`, { kind: "data-flow", origin: "plan", entry: { interaction, retracted: true, ...(typeof params?.reason === "string" ? { reason: params.reason } : {}) } });
|
|
3177
|
+
return planToolReceipt(result);
|
|
3178
|
+
},
|
|
3179
|
+
});
|
|
3180
|
+
const recordApiMockBoundaryTool = record("record_api_mock_boundary", "API/Mock boundary", "Record one API boundary and its mode (real/mock/not-needed) with evidence. The boundary is the stable identity: resubmit the same boundary with replace:true to change its mode (e.g. mock→real); the old decision is replaced, not appended. Real/mock endpoints require fixture (the test fixture path) and consumer (the implementation file that calls the API) — design policy rejects endpoints without them. Example: {\"entry\": {\"boundary\": \"GET /items\", \"mode\": \"mock\", \"evidence\": \"fixture only\", \"fixture\": \"test/fixtures/items.ts\", \"consumer\": \"src/page.tsx\"}}", Type.Object({
|
|
3181
|
+
boundary: Type.String({ minLength: 1 }),
|
|
3182
|
+
mode: Type.Union([Type.Literal("real"), Type.Literal("mock"), Type.Literal("not-needed")]),
|
|
3183
|
+
evidence: Type.String({ minLength: 1 }),
|
|
3184
|
+
fixture: optionalString,
|
|
3185
|
+
consumer: optionalString,
|
|
3186
|
+
}, { additionalProperties: false }), async (entry) => {
|
|
3187
|
+
// Design policy hard-requires fixture + consumer on every derived
|
|
3188
|
+
// endpoint (non-not-needed strategies); demand them here where the
|
|
3189
|
+
// model can still act instead of failing the compile post-session.
|
|
3190
|
+
if ((entry.mode === "real" || entry.mode === "mock") &&
|
|
3191
|
+
(!(typeof entry.fixture === "string" && entry.fixture.trim()) ||
|
|
3192
|
+
!(typeof entry.consumer === "string" && entry.consumer.trim()))) {
|
|
3193
|
+
return planToolReceipt({
|
|
3194
|
+
ok: false,
|
|
3195
|
+
kind: "api-mock-boundary",
|
|
3196
|
+
error: `record_api_mock_boundary fixture-and-consumer-required: ${entry.mode} boundary "${entry.boundary}" needs fixture (the test fixture path) and consumer (the implementation file that calls the API). Example: {"entry": {"boundary": "${entry.boundary}", "mode": "${entry.mode}", "evidence": "<why>", "fixture": "test/fixtures/items.ts", "consumer": "src/page.tsx"}}`,
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
return undefined;
|
|
3200
|
+
});
|
|
3201
|
+
const recordVerificationFocusTool = record("record_verification_focus", "verification focus", "Record one behavior-group verification focus: which test file/command proves which behavior group at what evidence level. Every behaviorGroupId must have at least one record_state_ownership row whose owner equals it — declare one UI state per behavior group before finalizing. Example: {\"entry\": {\"id\": \"VT-1\", \"behaviorGroupId\": \"page\", \"file\": \"test/page.test.tsx\", \"commandId\": \"test\", \"evidenceLevel\": \"mounted\"}}", Type.Object({
|
|
3202
|
+
id: Type.String({ minLength: 1 }),
|
|
3203
|
+
behaviorGroupId: Type.String({ minLength: 1 }),
|
|
3204
|
+
file: Type.String({ minLength: 1 }),
|
|
3205
|
+
commandId: Type.String({ minLength: 1 }),
|
|
3206
|
+
evidenceLevel: Type.Union([Type.Literal("unit"), Type.Literal("mounted"), Type.Literal("real-integration")]),
|
|
3207
|
+
boundary: optionalString,
|
|
3208
|
+
}, { additionalProperties: false }), async (entry) => {
|
|
3209
|
+
// Mirror the relationship record_plan_verification_target boundary so
|
|
3210
|
+
// protocol errors surface in-node (bounded correction) instead of
|
|
3211
|
+
// burning attempts on an immutable committed fact that finalize rejects.
|
|
3212
|
+
// Duplicate/identity enforcement lives in the durable wrapper (FACT_IDENTITY_CONFLICT
|
|
3213
|
+
// unless replace:true) and the compile collapses same-id records to the
|
|
3214
|
+
// latest replacement, so no duplicate gate is needed here.
|
|
3215
|
+
const verificationCommandId = typeof entry.commandId === "string" ? entry.commandId.trim() : "";
|
|
3216
|
+
if (!verificationCommandId) {
|
|
3217
|
+
return planToolReceipt({
|
|
3218
|
+
ok: false,
|
|
3219
|
+
kind: "verification-focus",
|
|
3220
|
+
error: `record_verification_focus entry.commandId is required (received ${JSON.stringify(entry.commandId ?? null)}); pick one id from the frozen command directory in your prompt`,
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
const { deriveFrontendVerifyCommandDirectoryFromRun, isFrontendTestFilePath } = await import("../workflows/dag/frontend-implementation-contract.js");
|
|
3224
|
+
const verifyDirectory = await deriveFrontendVerifyCommandDirectoryFromRun(input.runDir);
|
|
3225
|
+
if (verifyDirectory.length > 0) {
|
|
3226
|
+
const directoryEntry = verifyDirectory.find((candidate) => candidate.commandId === verificationCommandId);
|
|
3227
|
+
if (!directoryEntry) {
|
|
3228
|
+
return planToolReceipt({
|
|
3229
|
+
ok: false,
|
|
3230
|
+
kind: "verification-focus",
|
|
3231
|
+
error: `record_verification_focus verification-target-unknown-command: unknown commandId "${verificationCommandId}"; available frozen commands: [${verifyDirectory.map((candidate) => `${candidate.commandId} (${candidate.mode}: ${candidate.label})`).join(", ")}]`,
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
3234
|
+
if (directoryEntry.mode === "behavior" &&
|
|
3235
|
+
typeof entry.file === "string" &&
|
|
3236
|
+
!isFrontendTestFilePath(entry.file)) {
|
|
3237
|
+
return planToolReceipt({
|
|
3238
|
+
ok: false,
|
|
3239
|
+
kind: "verification-focus",
|
|
3240
|
+
error: `record_verification_focus verification-target-phase-mismatch: behavior command "${directoryEntry.label}" (${directoryEntry.commandId}) must bind a test file (__tests__/, tests?/, e2e/, cypress/, *.test.*, *.spec.*, *.cy.*); received file "${entry.file}"`,
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
// A behavior verification target derives its contract uiStates solely
|
|
3245
|
+
// from the committed state-ownership rows whose owner equals its
|
|
3246
|
+
// behaviorGroupId, and the relationship patch rejects a behavior
|
|
3247
|
+
// target with empty uiStates whenever the plan declares any state or
|
|
3248
|
+
// interaction. That rejection currently surfaces only in the
|
|
3249
|
+
// post-session bridge and burns the whole attempt, so enforce the
|
|
3250
|
+
// pairing here as a bounded in-session correction. A ledger with no
|
|
3251
|
+
// ownership and no data-flow facts stays exempt (pure-logic plan),
|
|
3252
|
+
// mirroring the contract-level exemption.
|
|
3253
|
+
const committedLedger = readCommittedEvents(store, attemptId);
|
|
3254
|
+
const ownershipRows = committedLedger.filter((event) => event.fact.kind === "state-ownership");
|
|
3255
|
+
const hasInteractionFacts = committedLedger.some((event) => event.fact.kind === "data-flow");
|
|
3256
|
+
if ((ownershipRows.length > 0 || hasInteractionFacts) &&
|
|
3257
|
+
!ownershipRows.some((event) => event.fact.entry?.owner ===
|
|
3258
|
+
entry.behaviorGroupId)) {
|
|
3259
|
+
return planToolReceipt({
|
|
3260
|
+
ok: false,
|
|
3261
|
+
kind: "verification-focus",
|
|
3262
|
+
error: `record_verification_focus undeclared-behavior-group-state: behavior group "${entry.behaviorGroupId}" (verification target ${entry.id}) has no record_state_ownership row; finalize derives each behavior target's UI states from ownership rows whose owner equals the behaviorGroupId, so record at least one for this group: {"state": "<name>", "owner": "${entry.behaviorGroupId}", "applicable": true, "expectedBehavior": "<what the state does>"}`,
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
return undefined;
|
|
3266
|
+
});
|
|
3267
|
+
const recordDependencyTool = record("record_dependency", "dependency", "Record one dependency name. Example: {\"entry\": {\"name\": \"none\"}}", Type.Object({ name: Type.String({ minLength: 1 }) }, { additionalProperties: false }));
|
|
3268
|
+
const finalizeDecisionTool = defineTool({
|
|
3269
|
+
name: "finalize_decision",
|
|
3270
|
+
label: "finalize_decision",
|
|
3271
|
+
description: "Compile the committed decision facts into a decision contract, pre-validate the derived canonical contract, and commit the terminal. Fails closed on any safety finding or validation finding; correct the reported record_* facts and call finalize_decision again.",
|
|
3272
|
+
promptSnippet: "Compile and finalize the committed decision facts.",
|
|
3273
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
3274
|
+
async execute(_toolCallId) {
|
|
3275
|
+
const committed = readCommittedEvents(store, attemptId);
|
|
3276
|
+
const decision = assembleDecisionContractFromFacts(committed.map(record => record.fact));
|
|
3277
|
+
const expectedWriteSet = [
|
|
3278
|
+
...decision.modulePlacements.flatMap(item => item.paths),
|
|
3279
|
+
...decision.verificationFocus.map(item => item.file),
|
|
3280
|
+
];
|
|
3281
|
+
const derived = assembleExperimentPlanContract({
|
|
3282
|
+
decision,
|
|
3283
|
+
authority,
|
|
3284
|
+
concreteWriteSet: expectedWriteSet,
|
|
3285
|
+
});
|
|
3286
|
+
if (derived.findings.length > 0) {
|
|
3287
|
+
return planToolReceipt({
|
|
3288
|
+
ok: false,
|
|
3289
|
+
kind: "finalize_decision",
|
|
3290
|
+
code: "DECISION_SAFETY_FINDINGS",
|
|
3291
|
+
error: `decision safety projection failed: ${derived.findings.join("; ")}`,
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
3294
|
+
const prevalidation = await prevalidateDerivedPatch();
|
|
3295
|
+
if (!prevalidation.ok) {
|
|
3296
|
+
return planToolReceipt({
|
|
3297
|
+
ok: false,
|
|
3298
|
+
kind: "finalize_decision",
|
|
3299
|
+
code: "DECISION_PREVALIDATION_FAILED",
|
|
3300
|
+
error: prevalidation.error,
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
// Recovery-mode guard: a repair child that finalizes a plan identical
|
|
3304
|
+
// to its parent's has not performed the repair the findings demand —
|
|
3305
|
+
// the identical plan is exactly what admission rejected. Both hashes
|
|
3306
|
+
// use the same deterministic serializer over the canonical contract,
|
|
3307
|
+
// so only a real semantic change passes.
|
|
3308
|
+
// Recovery-mode guard: a repair child must demonstrably close the
|
|
3309
|
+
// parent findings before the run re-enters design review.
|
|
3310
|
+
if (input.parentDecisionSnapshot) {
|
|
3311
|
+
const { readParentCanonical, parentCanonicalContentSha256 } = await import("../workflows/dag/frontend-implementation-contract.js");
|
|
3312
|
+
const { extractRepairAssertions, evaluateRepairClosure, decisionIdentitySha256 } = await import("../workflows/dag/frontend-plan-decision-contract.js");
|
|
3313
|
+
const parentRunDir = path.join(input.runDir, "..", input.parentDecisionSnapshot.parentRunId);
|
|
3314
|
+
const parentCanonical = await readParentCanonical(parentRunDir);
|
|
3315
|
+
// 1. identical-plan guard. Decisions, not prose: a rationale-only
|
|
3316
|
+
// rewrite is not a repair (smoke r27 cleared a raw byte-hash guard
|
|
3317
|
+
// by editing two rationale strings and changing nothing else). The
|
|
3318
|
+
// byte hash stays as the fallback when the parent canonical cannot
|
|
3319
|
+
// be read.
|
|
3320
|
+
const parentSha = await parentCanonicalContentSha256(parentRunDir);
|
|
3321
|
+
const parentDecisions = parentCanonical
|
|
3322
|
+
? decisionIdentitySha256(parentCanonical)
|
|
3323
|
+
: undefined;
|
|
3324
|
+
const childDecisions = decisionIdentitySha256(prevalidation.canonical);
|
|
3325
|
+
if (parentDecisions
|
|
3326
|
+
? parentDecisions === childDecisions
|
|
3327
|
+
: parentSha !== undefined && parentSha === prevalidation.canonicalSha256) {
|
|
3328
|
+
return planToolReceipt({
|
|
3329
|
+
ok: false,
|
|
3330
|
+
kind: "finalize_decision",
|
|
3331
|
+
code: "DECISION_REPAIR_NO_CHANGE",
|
|
3332
|
+
error: "recovery finalize blocked: every structured decision row is identical to the parent run's (rationale-only edits do not count as a repair), but the review findings require changes. Correct the flagged rows (same identity, replace:true), then call finalize_decision again.",
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
// 2. per-finding closure: every deterministic assertion extracted
|
|
3336
|
+
// from the parent findings must hold on the child canonical.
|
|
3337
|
+
const findings = input.parentDecisionSnapshot.designFindings ?? [];
|
|
3338
|
+
if (parentCanonical && findings.length > 0) {
|
|
3339
|
+
const assertions = extractRepairAssertions(findings, parentCanonical);
|
|
3340
|
+
const closure = evaluateRepairClosure(assertions, prevalidation.canonical, parentCanonical);
|
|
3341
|
+
if (!closure.closed) {
|
|
3342
|
+
return planToolReceipt({
|
|
3343
|
+
ok: false,
|
|
3344
|
+
kind: "finalize_decision",
|
|
3345
|
+
code: "DECISION_REPAIR_NOT_CLOSED",
|
|
3346
|
+
error: `recovery finalize blocked: ${closure.unmet.length} finding(s) are still not closed — ${closure.unmet.join("; ")}. Correct the flagged rows (same identity, replace:true), then call finalize_decision again.`,
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
const result = await adoptDecisionFact("finalize_decision", `${attemptId}:finalize_decision:${randomUUID()}`, { kind: "finalize_decision", origin: "plan", findings: [] });
|
|
3352
|
+
return planToolReceipt(result);
|
|
3353
|
+
},
|
|
3354
|
+
});
|
|
3355
|
+
const durable = await createDurableFrontendTools({
|
|
3356
|
+
file: path.join(input.runDir, input.nodeId, "plan-decision-facts.jsonl"),
|
|
3357
|
+
attemptId,
|
|
3358
|
+
store: input.store,
|
|
3359
|
+
setWorkingStore: next => { store = next; },
|
|
3360
|
+
binding: { authority },
|
|
3361
|
+
tools: [
|
|
3362
|
+
recordModulePlacementTool,
|
|
3363
|
+
recordReuseDecisionTool,
|
|
3364
|
+
recordStateOwnershipTool,
|
|
3365
|
+
recordDataFlowTool,
|
|
3366
|
+
retractDataFlowTool,
|
|
3367
|
+
recordApiMockBoundaryTool,
|
|
3368
|
+
recordVerificationFocusTool,
|
|
3369
|
+
recordDependencyTool,
|
|
3370
|
+
finalizeDecisionTool,
|
|
3371
|
+
],
|
|
3372
|
+
});
|
|
3373
|
+
const durableFinalizeDecisionTool = durable.customTools.find((tool) => typeof tool === "object" && tool !== null && tool.name === "finalize_decision");
|
|
3374
|
+
if (!durableFinalizeDecisionTool)
|
|
3375
|
+
throw new Error("frontend plan decision durable finalize tool unavailable");
|
|
3376
|
+
return {
|
|
3377
|
+
customTools: durable.customTools,
|
|
3378
|
+
flush: durable.flush,
|
|
3379
|
+
committedFacts: () => readCommittedEvents(store, attemptId),
|
|
3380
|
+
finalizeDecision: () => durableFinalizeDecisionTool.execute(`${attemptId}:auto-finalize-decision`, {}, undefined, undefined, {}),
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
/**
|
|
3384
|
+
* Decision → relationship bridge: after a successful decision `finalize`
|
|
3385
|
+
* (kind finalize_decision committed in plan-decision-facts.jsonl) the plan
|
|
3386
|
+
* node ALSO publishes the relationship-shaped plan ledger
|
|
3387
|
+
* (`<runDir>/<nodeId>/plan-typed-facts.jsonl`) carrying a committed
|
|
3388
|
+
* `target-surface` patch fact + a `finalize_plan` terminal fact derived from
|
|
3389
|
+
* the committed decision facts. The unchanged R1 self-check
|
|
3390
|
+
* (`validateFrontendPlanPatchNodeOutput`) and every downstream shell
|
|
3391
|
+
* (`buildContractFromCommittedFacts`) read ONLY that ledger, so without this
|
|
3392
|
+
* bridge every decision run fails deterministically with "frontend plan
|
|
3393
|
+
* ledger missing" → invalid-output on all attempts.
|
|
3394
|
+
*
|
|
3395
|
+
* The patch is compiled by `buildFrontendPlanRelationshipPatch` and front-loads
|
|
3396
|
+
* the exact validation the relationship finalize receipt runs (merge onto the
|
|
3397
|
+
* runtime skeleton + analyzeFrontendPlanPatchCandidate). When the derived
|
|
3398
|
+
* canonical fails, this returns a fixable error and the executor turns it into
|
|
3399
|
+
* an invalid-output so the node retry ladder restarts the session with the
|
|
3400
|
+
* diagnostics instead of writing a ledger that R1 would reject.
|
|
3401
|
+
*/
|
|
3402
|
+
export async function bridgeFrontendPlanDecisionToRelationshipLedger(input) {
|
|
3403
|
+
const { assembleDecisionContractFromFacts, buildFrontendPlanRelationshipPatch, } = await import("../workflows/dag/frontend-plan-decision-contract.js");
|
|
3404
|
+
const committed = input.facts
|
|
3405
|
+
.filter(record => record.phase === "committed")
|
|
3406
|
+
.map(record => record.fact);
|
|
3407
|
+
const decision = assembleDecisionContractFromFacts(committed);
|
|
3408
|
+
const concreteWriteSet = [
|
|
3409
|
+
...new Set([
|
|
3410
|
+
...decision.modulePlacements.flatMap(item => item.paths),
|
|
3411
|
+
...decision.verificationFocus.map(item => item.file),
|
|
3412
|
+
]),
|
|
3413
|
+
];
|
|
3414
|
+
// Contract-declared deliverable obligations (the same ledger the analyzer
|
|
3415
|
+
// extracts `requiredDeliverables` from): bind each path to its requirement
|
|
3416
|
+
// so the derived canonical contract plans every contract-mandated file.
|
|
3417
|
+
const requiredDeliverables = new Map();
|
|
3418
|
+
try {
|
|
3419
|
+
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl"));
|
|
3420
|
+
for (const record of [...contractRecords].reverse()) {
|
|
3421
|
+
const fact = record.fact;
|
|
3422
|
+
if (!fact || fact.origin !== "contract" || fact.kind !== "required-deliverables")
|
|
3423
|
+
continue;
|
|
3424
|
+
for (const item of Array.isArray(fact.items) ? fact.items : []) {
|
|
3425
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
3426
|
+
continue;
|
|
3427
|
+
const requirementId = item.requirementId;
|
|
3428
|
+
const file = item.path;
|
|
3429
|
+
if (typeof requirementId !== "string" || typeof file !== "string")
|
|
3430
|
+
continue;
|
|
3431
|
+
const paths = requiredDeliverables.get(requirementId) ?? [];
|
|
3432
|
+
paths.push(file);
|
|
3433
|
+
requiredDeliverables.set(requirementId, paths);
|
|
3434
|
+
}
|
|
3435
|
+
break;
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
catch {
|
|
3439
|
+
// Missing/unreadable contract ledger → no deliverable obligations to merge.
|
|
3440
|
+
}
|
|
3441
|
+
if (!input.skeleton || !input.sourceBinding) {
|
|
3442
|
+
return { ok: false, error: "frontend decision bridge requires the runtime skeleton and sourceBinding" };
|
|
3443
|
+
}
|
|
3444
|
+
let patch;
|
|
3445
|
+
try {
|
|
3446
|
+
const { analyzeFrontendPlanPatchCandidate, applyFrontendContractMergePatch, serializeDeterministicJson, } = await import("../workflows/dag/frontend-implementation-contract.js");
|
|
3447
|
+
patch = buildFrontendPlanRelationshipPatch({
|
|
3448
|
+
decision,
|
|
3449
|
+
authority: input.authority,
|
|
3450
|
+
concreteWriteSet,
|
|
3451
|
+
requiredDeliverables,
|
|
3452
|
+
});
|
|
3453
|
+
const merged = applyFrontendContractMergePatch(input.skeleton, patch);
|
|
3454
|
+
await analyzeFrontendPlanPatchCandidate({
|
|
3455
|
+
runDir: input.runDir,
|
|
3456
|
+
rawContractText: serializeDeterministicJson(merged),
|
|
3457
|
+
sourceBinding: input.sourceBinding,
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
catch (error) {
|
|
3461
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
3462
|
+
return {
|
|
3463
|
+
ok: false,
|
|
3464
|
+
// This path should be rare now that finalize_decision pre-validates
|
|
3465
|
+
// the same patch in-session; keep the retry-prompt diagnostics in
|
|
3466
|
+
// decision-channel vocabulary regardless.
|
|
3467
|
+
error: `frontend decision → relationship patch failed pre-validation (correct the reported decision facts, then the retry ladder restarts the session): ${translateDecisionPatchFindings(raw, decision)}`,
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
const { createTypedEventStore, stageTypedEventRecord, commitTypedEventRecord, writeTypedEventStoreJsonl } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
3471
|
+
const file = path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl");
|
|
3472
|
+
const store = createTypedEventStore();
|
|
3473
|
+
const adopt = (kind, patchValue, revision) => {
|
|
3474
|
+
const eventId = `decision-bridge:${input.attemptId}:${kind}`;
|
|
3475
|
+
const fact = { kind, origin: "plan", patch: patchValue };
|
|
3476
|
+
stageTypedEventRecord(store, {
|
|
3477
|
+
eventId,
|
|
3478
|
+
requestId: eventId,
|
|
3479
|
+
attemptId: input.attemptId,
|
|
3480
|
+
fact,
|
|
3481
|
+
});
|
|
3482
|
+
return commitTypedEventRecord(store, eventId, revision);
|
|
3483
|
+
};
|
|
3484
|
+
adopt("target-surface", patch, 1);
|
|
3485
|
+
adopt("finalize_plan", patch, 2);
|
|
3486
|
+
// The ledger is derived wholesale from the committed decision facts, so a
|
|
3487
|
+
// re-run rewrites it atomically (no incremental append semantics needed).
|
|
3488
|
+
await writeTypedEventStoreJsonl(file, store.records.filter(record => record.phase === "committed"));
|
|
3489
|
+
return { ok: true, patch };
|
|
3490
|
+
}
|
|
2636
3491
|
function isRecordObject(value) {
|
|
2637
3492
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2638
3493
|
}
|
|
@@ -2846,6 +3701,52 @@ export async function createFrontendContractTools(input) {
|
|
|
2846
3701
|
return receipt(result);
|
|
2847
3702
|
},
|
|
2848
3703
|
});
|
|
3704
|
+
// Requirement identity/text/source are runtime-owned and seeded before the
|
|
3705
|
+
// model starts. Execution grouping is still a model decision, so expose it
|
|
3706
|
+
// as a small typed update instead of forcing the model to re-submit the same
|
|
3707
|
+
// requirement just to attach execution metadata.
|
|
3708
|
+
const recordRequirementExecutionTool = defineTool({
|
|
3709
|
+
name: "record_requirement_execution",
|
|
3710
|
+
label: "record_requirement_execution",
|
|
3711
|
+
description: "Attach execution ownership to already-confirmed canonical requirements.",
|
|
3712
|
+
promptSnippet: "Record execution group metadata for confirmed requirements.",
|
|
3713
|
+
parameters: Type.Object({
|
|
3714
|
+
requirementIds: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, uniqueItems: true }),
|
|
3715
|
+
execution: Type.Object({
|
|
3716
|
+
groupId: Type.String({ minLength: 1 }),
|
|
3717
|
+
kind: Type.Union([Type.Literal("behavior"), Type.Literal("constraint"), Type.Literal("exclusion")]),
|
|
3718
|
+
summary: Type.String({ minLength: 1 }),
|
|
3719
|
+
}, { additionalProperties: false }),
|
|
3720
|
+
}, { additionalProperties: false }),
|
|
3721
|
+
async execute(callId, params) {
|
|
3722
|
+
const ids = params.requirementIds;
|
|
3723
|
+
if (activeScope && ids.some((id) => !activeScope?.has(id)))
|
|
3724
|
+
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "execution metadata references a requirement outside the active contract scope" });
|
|
3725
|
+
const canonical = input.canonicalRequirements;
|
|
3726
|
+
const unknown = ids.filter((id) => !canonical?.has(id));
|
|
3727
|
+
if (unknown.length > 0)
|
|
3728
|
+
return receipt({ ok: false, code: "CONTRACT_REFERENCE_UNKNOWN", error: `record_requirement_execution references unknown canonical requirements: ${unknown.join(", ")}` });
|
|
3729
|
+
const execution = frontendExecutionSchema.parse(params.execution);
|
|
3730
|
+
const existing = readCommittedEvents(store, attemptId)
|
|
3731
|
+
.filter((record) => record.fact.kind === "requirement")
|
|
3732
|
+
.map((record) => ({ id: String(record.fact.id), execution: record.fact.execution }))
|
|
3733
|
+
.filter((unit) => !ids.includes(unit.id));
|
|
3734
|
+
try {
|
|
3735
|
+
collectFrontendExecutionGroups([...existing, ...ids.map((id) => ({ id, execution }))]);
|
|
3736
|
+
}
|
|
3737
|
+
catch (error) {
|
|
3738
|
+
return receipt({ ok: false, code: "EXECUTION_GROUP_CONFLICT", error: error instanceof Error ? error.message : String(error) });
|
|
3739
|
+
}
|
|
3740
|
+
let result = { ok: true };
|
|
3741
|
+
for (const id of ids) {
|
|
3742
|
+
const requirement = canonical.get(id);
|
|
3743
|
+
result = await adoptContractFact("requirement", { kind: "requirement", origin: "contract", disposition: "explicit", id, text: requirement.text, sourceFragmentIds: requirement.sourceFragmentIds, execution });
|
|
3744
|
+
if (result.ok !== true)
|
|
3745
|
+
return receipt(result);
|
|
3746
|
+
}
|
|
3747
|
+
return receipt({ ...result, requestId: callId });
|
|
3748
|
+
},
|
|
3749
|
+
});
|
|
2849
3750
|
// Authoritative UI state declarations: the contract node extracts the
|
|
2850
3751
|
// PRD/reference UI-state table into structured facts so the planner binds
|
|
2851
3752
|
// uiStates to declared ids instead of inventing names (dogfood
|
|
@@ -3059,6 +3960,7 @@ export async function createFrontendContractTools(input) {
|
|
|
3059
3960
|
tools: [
|
|
3060
3961
|
...recordTools,
|
|
3061
3962
|
recordRequirementTool,
|
|
3963
|
+
recordRequirementExecutionTool,
|
|
3062
3964
|
recordEvidenceExpectationTool,
|
|
3063
3965
|
recordUiStateTool,
|
|
3064
3966
|
recordRequiredDeliverablesTool,
|
|
@@ -3067,9 +3969,23 @@ export async function createFrontendContractTools(input) {
|
|
|
3067
3969
|
finalizeContractTool,
|
|
3068
3970
|
],
|
|
3069
3971
|
});
|
|
3972
|
+
const durableRecordRequirementTool = durable.customTools.find((tool) => typeof tool === "object" && tool !== null && tool.name === "record_requirement");
|
|
3973
|
+
if (!durableRecordRequirementTool)
|
|
3974
|
+
throw new Error("frontend contract durable requirement tool unavailable");
|
|
3070
3975
|
return {
|
|
3071
3976
|
customTools: durable.customTools,
|
|
3072
3977
|
inputRequirements: () => [...(input.canonicalRequirements ?? [])].map(([id, value]) => ({ id, text: value.text, sourceFragmentIds: [...value.sourceFragmentIds] })),
|
|
3978
|
+
seedCanonicalRequirements: async () => {
|
|
3979
|
+
for (const id of input.canonicalRequirements?.keys() ?? []) {
|
|
3980
|
+
if (committedRequirementIds().has(id))
|
|
3981
|
+
continue;
|
|
3982
|
+
const receipt = await durableRecordRequirementTool.execute(`${attemptId}:seed-requirement:${id}`, { id }, undefined, undefined, {});
|
|
3983
|
+
if (receipt.details?.ok !== true) {
|
|
3984
|
+
throw new Error(`runtime requirement seed rejected for ${id}`);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
await durable.flush();
|
|
3988
|
+
},
|
|
3073
3989
|
completedScopeRequirementIds,
|
|
3074
3990
|
setActiveRequirementScope: ids => { activeScope = ids === null ? null : new Set(ids); },
|
|
3075
3991
|
committedRequirementIds,
|
|
@@ -3251,6 +4167,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3251
4167
|
entrypoint: optionalString,
|
|
3252
4168
|
routeOrMount: optionalString,
|
|
3253
4169
|
implementationPaths: stringArray,
|
|
4170
|
+
proposedPaths: Type.Optional(stringArray),
|
|
3254
4171
|
testPaths: stringArray,
|
|
3255
4172
|
dataSource: optionalString,
|
|
3256
4173
|
allowedPathConflicts: stringArray,
|
|
@@ -3260,12 +4177,18 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3260
4177
|
if (input.requirementIds && (!activeScope?.length || params.scopeId !== scopeIdentity(activeScope)))
|
|
3261
4178
|
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "Use exactly the runtime Scout scopeId; discovery may complete only the supplied obligations" });
|
|
3262
4179
|
const implementationPaths = params?.implementationPaths ?? [];
|
|
4180
|
+
const proposedPaths = params?.proposedPaths ?? [];
|
|
3263
4181
|
const testPaths = params?.testPaths ?? [];
|
|
3264
4182
|
const pathEvidence = await enrichScoutPathEvidence([
|
|
3265
4183
|
...(params?.entrypoint ? [params.entrypoint] : []),
|
|
3266
4184
|
...implementationPaths,
|
|
3267
4185
|
...testPaths,
|
|
3268
4186
|
]);
|
|
4187
|
+
for (const proposedPath of proposedPaths) {
|
|
4188
|
+
if (!pathEvidence.some((item) => item.path === proposedPath)) {
|
|
4189
|
+
pathEvidence.push({ path: proposedPath, sha256: "0".repeat(64), fresh: false, sourceDeclared: false, proposed: true });
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
3269
4192
|
const surface = {
|
|
3270
4193
|
kind: "target-surface",
|
|
3271
4194
|
origin: "scout",
|
|
@@ -3273,6 +4196,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3273
4196
|
entrypoint: params?.entrypoint ?? "",
|
|
3274
4197
|
routeOrMount: params?.routeOrMount ?? "",
|
|
3275
4198
|
implementationPaths,
|
|
4199
|
+
...(proposedPaths.length > 0 ? { proposedPaths } : {}),
|
|
3276
4200
|
testPaths,
|
|
3277
4201
|
dataSource: params?.dataSource ?? "",
|
|
3278
4202
|
allowedPathConflicts: params?.allowedPathConflicts ?? [],
|
|
@@ -3281,7 +4205,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3281
4205
|
...(pathEvidence.length > 0 ? { pathEvidence } : {}),
|
|
3282
4206
|
};
|
|
3283
4207
|
if (activeScope) {
|
|
3284
|
-
const { readCompleteScoutTargetSurface } = await import("../workflows/dag/frontend-
|
|
4208
|
+
const { readCompleteScoutTargetSurface } = await import("../workflows/dag/frontend-committed-facts.js");
|
|
3285
4209
|
if (surface.completeness === "complete") {
|
|
3286
4210
|
const check = readCompleteScoutTargetSurface([{ phase: "committed", fact: surface }]);
|
|
3287
4211
|
if (!check.ok)
|
|
@@ -3295,7 +4219,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3295
4219
|
const surfaces = [...new Set(input.requirementIds.map(id => current.get(id)))].map(f => f.surface);
|
|
3296
4220
|
const union = (key) => [...new Set(surfaces.flatMap(s => Array.isArray(s[key]) ? s[key] : []))];
|
|
3297
4221
|
const entries = [...new Set(surfaces.map(s => String(s.entrypoint ?? "")).filter(Boolean))];
|
|
3298
|
-
return receipt(await adoptScoutFact("target-surface", { kind: "target-surface", origin: "scout", completeness: "complete", entrypoint: entries[0] ?? "", implementationPaths: [...new Set([...entries, ...union("implementationPaths")])], testPaths: union("testPaths"), allowedPathConflicts: union("allowedPathConflicts"), unresolvedPaths: union("unresolvedPaths"), routeOrMount: [...new Set(surfaces.map(s => s.routeOrMount).filter(Boolean))].join("\n"), dataSource: [...new Set(surfaces.map(s => s.dataSource).filter(Boolean))].join("\n"), pathEvidence: surfaces.flatMap(s => s.pathEvidence ?? []), ...(hasSourceDeclarations ? { sourceDeclaredPaths } : {}) }));
|
|
4222
|
+
return receipt(await adoptScoutFact("target-surface", { kind: "target-surface", origin: "scout", completeness: "complete", entrypoint: entries[0] ?? "", implementationPaths: [...new Set([...entries, ...union("implementationPaths")])], proposedPaths: union("proposedPaths"), testPaths: union("testPaths"), allowedPathConflicts: union("allowedPathConflicts"), unresolvedPaths: union("unresolvedPaths"), routeOrMount: [...new Set(surfaces.map(s => s.routeOrMount).filter(Boolean))].join("\n"), dataSource: [...new Set(surfaces.map(s => s.dataSource).filter(Boolean))].join("\n"), pathEvidence: surfaces.flatMap(s => s.pathEvidence ?? []), ...(hasSourceDeclarations ? { sourceDeclaredPaths } : {}) }));
|
|
3299
4223
|
}
|
|
3300
4224
|
return receipt(saved);
|
|
3301
4225
|
}
|
|
@@ -3605,21 +4529,15 @@ const DEFAULT_DAG_PI_WRITE_GUARD_DEPENDENCIES = {
|
|
|
3605
4529
|
readGitStatusPorcelain,
|
|
3606
4530
|
recoverRootNulArtifact,
|
|
3607
4531
|
};
|
|
3608
|
-
/**
|
|
3609
|
-
*
|
|
3610
|
-
* facts from session events, parse the legacy JSON verdict from the response
|
|
3611
|
-
* text, compare them (audit-only), and persist the audit artifact. Missing
|
|
3612
|
-
* typed terminal facts fail the node closed (AC-001); a shadow mismatch never
|
|
3613
|
-
* blocks the node.
|
|
4532
|
+
/** Validate the typed review terminal and complete review scopes. The typed
|
|
4533
|
+
* terminal is authoritative; legacy JSON verdict parsing is intentionally gone.
|
|
3614
4534
|
*/
|
|
3615
|
-
async function
|
|
4535
|
+
async function validateFrontendReviewTerminal(input) {
|
|
3616
4536
|
// A provider/executor failure (in particular context-overflow) is already
|
|
3617
4537
|
// authoritative. Do not rewrite it to review-terminal-missing merely
|
|
3618
4538
|
// because no terminal tool could be submitted after the failed call.
|
|
3619
4539
|
if (!input.mapped.ok)
|
|
3620
4540
|
return input.mapped;
|
|
3621
|
-
const { compareTypedReviewToLegacyJsonVerdict } = await import("../workflows/dag/frontend-review-context.js");
|
|
3622
|
-
const { parseJsonReviewVerdict } = await import("../workflows/dag/output-protocol.js");
|
|
3623
4541
|
let typedKinds = [];
|
|
3624
4542
|
try {
|
|
3625
4543
|
await input.tools?.flush();
|
|
@@ -3629,43 +4547,6 @@ async function runFrontendReviewTerminalShadow(input) {
|
|
|
3629
4547
|
catch (error) {
|
|
3630
4548
|
return { ...input.mapped, ok: false, failureCategory: "frontend-ledger-invalid", stderr: `FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${error instanceof Error ? error.message : String(error)}` };
|
|
3631
4549
|
}
|
|
3632
|
-
let legacyVerdict;
|
|
3633
|
-
try {
|
|
3634
|
-
const parsed = parseJsonReviewVerdict(input.mapped.assistantText ?? input.mapped.stdout);
|
|
3635
|
-
if (parsed.ok)
|
|
3636
|
-
legacyVerdict = parsed.verdict;
|
|
3637
|
-
}
|
|
3638
|
-
catch {
|
|
3639
|
-
legacyVerdict = undefined;
|
|
3640
|
-
}
|
|
3641
|
-
let comparison;
|
|
3642
|
-
try {
|
|
3643
|
-
comparison = compareTypedReviewToLegacyJsonVerdict({
|
|
3644
|
-
typedKinds,
|
|
3645
|
-
legacyVerdict,
|
|
3646
|
-
});
|
|
3647
|
-
}
|
|
3648
|
-
catch (error) {
|
|
3649
|
-
comparison = {
|
|
3650
|
-
typedVerdict: undefined,
|
|
3651
|
-
legacyVerdict: undefined,
|
|
3652
|
-
match: false,
|
|
3653
|
-
reason: `typed review equivalence comparison crashed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3654
|
-
};
|
|
3655
|
-
}
|
|
3656
|
-
// The durable ledger was validated above; this artifact is audit-only.
|
|
3657
|
-
try {
|
|
3658
|
-
await writeDagNodeJsonArtifact(input.meta.runDir, input.task.id, "fact-review-status.json", {
|
|
3659
|
-
schemaVersion: 1,
|
|
3660
|
-
nodeId: input.task.id,
|
|
3661
|
-
typedKinds,
|
|
3662
|
-
legacyVerdict,
|
|
3663
|
-
comparison,
|
|
3664
|
-
});
|
|
3665
|
-
}
|
|
3666
|
-
catch {
|
|
3667
|
-
// best-effort audit artifact
|
|
3668
|
-
}
|
|
3669
4550
|
if (typedKinds.length === 0) {
|
|
3670
4551
|
return {
|
|
3671
4552
|
...input.mapped,
|
|
@@ -3681,16 +4562,8 @@ async function runFrontendReviewTerminalShadow(input) {
|
|
|
3681
4562
|
}
|
|
3682
4563
|
return input.mapped;
|
|
3683
4564
|
}
|
|
3684
|
-
/**
|
|
3685
|
-
|
|
3686
|
-
* design terminal facts from session events, flush them, and persist the audit
|
|
3687
|
-
* artifact. The design review's legacy output was a first-line
|
|
3688
|
-
* `VERDICT: pass|request-revision` text protocol (not a JSON verdict), so
|
|
3689
|
-
* there is no JSON equivalence comparison here. Missing typed terminal facts
|
|
3690
|
-
* fail the node closed; writer admission later reads the committed
|
|
3691
|
-
* `design-typed-facts.jsonl` as the only authoritative verdict.
|
|
3692
|
-
*/
|
|
3693
|
-
async function runFrontendDesignTerminalShadow(input) {
|
|
4565
|
+
/** Validate the typed design terminal and complete design scopes. */
|
|
4566
|
+
async function validateFrontendDesignTerminal(input) {
|
|
3694
4567
|
// See the review counterpart above: a failed provider call cannot be
|
|
3695
4568
|
// diagnosed as an omitted terminal tool call.
|
|
3696
4569
|
if (!input.mapped.ok)
|
|
@@ -3704,17 +4577,6 @@ async function runFrontendDesignTerminalShadow(input) {
|
|
|
3704
4577
|
catch (error) {
|
|
3705
4578
|
return { ...input.mapped, ok: false, failureCategory: "frontend-ledger-invalid", stderr: `FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${error instanceof Error ? error.message : String(error)}` };
|
|
3706
4579
|
}
|
|
3707
|
-
// The durable ledger was validated above; this artifact is audit-only.
|
|
3708
|
-
try {
|
|
3709
|
-
await writeDagNodeJsonArtifact(input.meta.runDir, input.task.id, "fact-design-status.json", {
|
|
3710
|
-
schemaVersion: 1,
|
|
3711
|
-
nodeId: input.task.id,
|
|
3712
|
-
typedKinds,
|
|
3713
|
-
});
|
|
3714
|
-
}
|
|
3715
|
-
catch {
|
|
3716
|
-
// best-effort audit artifact
|
|
3717
|
-
}
|
|
3718
4580
|
if (typedKinds.length === 0) {
|
|
3719
4581
|
return {
|
|
3720
4582
|
...input.mapped,
|
|
@@ -3744,7 +4606,7 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3744
4606
|
]),
|
|
3745
4607
|
instruction: [
|
|
3746
4608
|
"PLAN PHASE — requirement coverage only.",
|
|
3747
|
-
"Your ONLY job: for every frozen requirement, emit record_plan_requirement (requirement → implementation files) and record_plan_verification_target facts (verification target bound to requirement ids and files). Group related requirements under one non-static behavior target when one observable test behavior proves them together; do not mechanically create one target per requirement.
|
|
4609
|
+
"Your ONLY job: for every frozen requirement, emit record_plan_requirement (requirement → implementation files) and record_plan_verification_target facts (verification target bound to requirement ids and files). Group related requirements under one non-static behavior target when one observable test behavior proves them together; do not mechanically create one target per requirement. Target ids identify contract entries, not test-title markers. Reuse affected existing test files and their names; do not add tests or rename titles just to carry generated ids. Never submit prose as a symbol. Do NOT record components, UI states, mock, dependency, or routes — a follow-up session owns those.",
|
|
3748
4610
|
"A coverage session is complete only when EVERY requirement assigned to this session (the full inventory, or the exact COVERAGE BATCH / shard list when present) has committed coverage facts: a record_plan_requirement entry plus verification targets, or a committed evidence gap. Keep committing in batches of up to 4 record_* calls per assistant message until then; do not write a concluding summary while any assigned requirement is still uncommitted — an early stop strands the remainder into a MISSING-FACT repair session and doubles the sessions needed.",
|
|
3749
4611
|
"If a requirement genuinely cannot have a verification target, record a non-empty record_plan_evidence_gap. Do not call finalize_plan; it is not available in this phase.",
|
|
3750
4612
|
].join(" "),
|
|
@@ -3797,15 +4659,185 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3797
4659
|
"Record only dependency policy and design-evidence conflicts. Do not record route, Mock/data, or requirement-local UX facts. Do not call finalize_plan.",
|
|
3798
4660
|
].join(" "),
|
|
3799
4661
|
},
|
|
3800
|
-
{
|
|
3801
|
-
id: "finalize",
|
|
3802
|
-
toolNames: null,
|
|
3803
|
-
instruction: [
|
|
3804
|
-
"PLAN PHASE — finalize.",
|
|
3805
|
-
"All record_* tools are available only for a narrowly named correction if the finalize receipt reports missing or invalid facts. Otherwise call finalize_plan exactly once with no extra fields.",
|
|
3806
|
-
].join(" "),
|
|
3807
|
-
},
|
|
3808
4662
|
];
|
|
4663
|
+
function committedFactFromPlanRecord(value) {
|
|
4664
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4665
|
+
return undefined;
|
|
4666
|
+
const record = value;
|
|
4667
|
+
if (record.phase !== undefined && record.phase !== "committed")
|
|
4668
|
+
return undefined;
|
|
4669
|
+
const fact = record.fact;
|
|
4670
|
+
return fact && typeof fact === "object" && !Array.isArray(fact)
|
|
4671
|
+
? fact
|
|
4672
|
+
: typeof record.kind === "string"
|
|
4673
|
+
? record
|
|
4674
|
+
: undefined;
|
|
4675
|
+
}
|
|
4676
|
+
function planFactStringList(value) {
|
|
4677
|
+
if (!Array.isArray(value))
|
|
4678
|
+
return [];
|
|
4679
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
4680
|
+
}
|
|
4681
|
+
function planFactScopeIntersects(fact, requirementIds) {
|
|
4682
|
+
return planFactStringList(fact.scopeRequirementIds).some((id) => requirementIds.has(id));
|
|
4683
|
+
}
|
|
4684
|
+
/** Compute the authoritative coverage queue from the committed plan ledger. */
|
|
4685
|
+
export function collectFrontendPlanMissingFacts(input) {
|
|
4686
|
+
const requirements = new Map();
|
|
4687
|
+
const standaloneEvidenceGaps = new Set();
|
|
4688
|
+
const verificationTargetIds = new Set();
|
|
4689
|
+
const verificationTargetRequirements = new Map();
|
|
4690
|
+
for (const value of input.committedFacts) {
|
|
4691
|
+
const fact = committedFactFromPlanRecord(value);
|
|
4692
|
+
if (!fact || fact.origin !== "plan")
|
|
4693
|
+
continue;
|
|
4694
|
+
if (fact.kind === "plan-requirement" && fact.entry && typeof fact.entry === "object") {
|
|
4695
|
+
const entry = fact.entry;
|
|
4696
|
+
if (typeof entry.id === "string" && entry.id.trim())
|
|
4697
|
+
requirements.set(entry.id, entry);
|
|
4698
|
+
}
|
|
4699
|
+
if (fact.kind === "plan-verification-target" && fact.entry && typeof fact.entry === "object") {
|
|
4700
|
+
const entry = fact.entry;
|
|
4701
|
+
const id = entry.id;
|
|
4702
|
+
if (typeof id === "string" && id.trim()) {
|
|
4703
|
+
verificationTargetIds.add(id);
|
|
4704
|
+
verificationTargetRequirements.set(id, new Set(Array.isArray(entry.requirementIds)
|
|
4705
|
+
? entry.requirementIds.filter((value) => typeof value === "string")
|
|
4706
|
+
: []));
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
if (fact.kind === "plan-evidence-gap" && fact.entry && typeof fact.entry === "object") {
|
|
4710
|
+
const entry = fact.entry;
|
|
4711
|
+
const requirementId = entry.requirementId;
|
|
4712
|
+
const description = entry.description;
|
|
4713
|
+
if (typeof requirementId === "string" && requirementId.trim() && typeof description === "string" && description.trim()) {
|
|
4714
|
+
standaloneEvidenceGaps.add(requirementId);
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
}
|
|
4718
|
+
const missing = [];
|
|
4719
|
+
for (const id of input.requirementIds) {
|
|
4720
|
+
const entry = requirements.get(id);
|
|
4721
|
+
if (!entry) {
|
|
4722
|
+
missing.push({
|
|
4723
|
+
kind: "plan-requirement",
|
|
4724
|
+
id,
|
|
4725
|
+
requirementIds: [id],
|
|
4726
|
+
reason: `requirement ${id} has no committed plan-requirement fact`,
|
|
4727
|
+
});
|
|
4728
|
+
continue;
|
|
4729
|
+
}
|
|
4730
|
+
// Verification targets are the single authoritative direction. The
|
|
4731
|
+
// legacy requirement-side list is accepted only as a fallback while
|
|
4732
|
+
// resuming older ledgers; new plans derive it from target.requirementIds.
|
|
4733
|
+
const derivedTargetIds = [...verificationTargetRequirements.entries()]
|
|
4734
|
+
.filter(([, requirementIds]) => requirementIds.has(id))
|
|
4735
|
+
.map(([targetId]) => targetId);
|
|
4736
|
+
const legacyTargetIds = Array.isArray(entry.verificationTargetIds)
|
|
4737
|
+
? entry.verificationTargetIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
4738
|
+
: [];
|
|
4739
|
+
const targetIds = derivedTargetIds.length > 0 ? derivedTargetIds : legacyTargetIds;
|
|
4740
|
+
const gap = entry.evidenceGap && typeof entry.evidenceGap === "object"
|
|
4741
|
+
? entry.evidenceGap
|
|
4742
|
+
: undefined;
|
|
4743
|
+
const hasEvidenceGap = (typeof gap?.description === "string" && gap.description.trim().length > 0) ||
|
|
4744
|
+
standaloneEvidenceGaps.has(id);
|
|
4745
|
+
if (targetIds.length === 0 && !hasEvidenceGap) {
|
|
4746
|
+
missing.push({
|
|
4747
|
+
kind: "plan-verification-target",
|
|
4748
|
+
requirementIds: [id],
|
|
4749
|
+
reason: `requirement ${id} declares neither a verification target nor a non-empty evidenceGap`,
|
|
4750
|
+
});
|
|
4751
|
+
continue;
|
|
4752
|
+
}
|
|
4753
|
+
for (const targetId of targetIds) {
|
|
4754
|
+
if (!verificationTargetIds.has(targetId) ||
|
|
4755
|
+
!verificationTargetRequirements.get(targetId)?.has(id)) {
|
|
4756
|
+
missing.push({
|
|
4757
|
+
kind: "plan-verification-target",
|
|
4758
|
+
id: targetId,
|
|
4759
|
+
requirementIds: [id],
|
|
4760
|
+
reason: `requirement ${id} references verification target ${targetId}, but that target is not committed`,
|
|
4761
|
+
});
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
return missing;
|
|
4766
|
+
}
|
|
4767
|
+
/** Completeness checks for phases whose facts are committed incrementally. */
|
|
4768
|
+
export function collectFrontendPlanPhaseMissingFacts(input) {
|
|
4769
|
+
const facts = input.committedFacts
|
|
4770
|
+
.map(committedFactFromPlanRecord)
|
|
4771
|
+
.filter((fact) => Boolean(fact && fact.origin === "plan"));
|
|
4772
|
+
if (input.phase === "ux-registry") {
|
|
4773
|
+
return facts.some((fact) => fact.kind === "state-registry")
|
|
4774
|
+
? []
|
|
4775
|
+
: [
|
|
4776
|
+
{
|
|
4777
|
+
kind: "state-registry",
|
|
4778
|
+
requirementIds: [...input.requirementIds],
|
|
4779
|
+
reason: "global UX vocabulary phase has no committed state-registry fact",
|
|
4780
|
+
},
|
|
4781
|
+
];
|
|
4782
|
+
}
|
|
4783
|
+
if (input.phase === "ux-local") {
|
|
4784
|
+
const missing = [];
|
|
4785
|
+
// Evaluate each behaviour requirement independently. A fact scoped to AC-1
|
|
4786
|
+
// must not accidentally satisfy AC-2 merely because both ids share one
|
|
4787
|
+
// UX session; shared facts remain valid when they explicitly list both ids.
|
|
4788
|
+
for (const requirementId of input.requirementIds) {
|
|
4789
|
+
if (!input.behaviorRequiredRequirementIds?.includes(requirementId))
|
|
4790
|
+
continue;
|
|
4791
|
+
const scopedFacts = facts.filter((fact) => planFactScopeIntersects(fact, new Set([requirementId])));
|
|
4792
|
+
const hasChoice = scopedFacts.some((fact) => fact.kind === "component-choice" &&
|
|
4793
|
+
Array.isArray(fact.uiComponentChoices) &&
|
|
4794
|
+
fact.uiComponentChoices.length > 0);
|
|
4795
|
+
const canonicalStateFlow = collectCanonicalStateFlowNames(scopedFacts);
|
|
4796
|
+
const hasStateFlow = canonicalStateFlow.uiStateNames.size > 0 ||
|
|
4797
|
+
canonicalStateFlow.interactionNames.size > 0;
|
|
4798
|
+
if (!hasChoice) {
|
|
4799
|
+
missing.push({
|
|
4800
|
+
kind: "component-choice",
|
|
4801
|
+
requirementIds: [requirementId],
|
|
4802
|
+
reason: "behaviour-required UX slice has no committed component-choice fact",
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4805
|
+
if (!hasStateFlow) {
|
|
4806
|
+
missing.push({
|
|
4807
|
+
kind: "state-flow",
|
|
4808
|
+
requirementIds: [requirementId],
|
|
4809
|
+
reason: "behaviour-required UX slice has no committed state-flow fact",
|
|
4810
|
+
});
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
return missing;
|
|
4814
|
+
}
|
|
4815
|
+
const hasMockApi = facts.some((fact) => fact.kind === "mock-api");
|
|
4816
|
+
const allInteractions = collectCanonicalStateFlowNames(input.committedFacts).interactionNames;
|
|
4817
|
+
const liveInteractions = new Set([...collectCanonicalStateFlowNames(facts.filter(f => !planFactStringList(f.scopeRequirementIds).length || planFactScopeIntersects(f, new Set(input.requirementIds)))).interactionNames].filter(name => allInteractions.has(name)));
|
|
4818
|
+
const coveredInteractions = new Set(facts
|
|
4819
|
+
.filter((fact) => fact.kind === "data-flow")
|
|
4820
|
+
.flatMap((fact) => planFactStringList(fact.interactions)));
|
|
4821
|
+
const missing = [];
|
|
4822
|
+
if (!hasMockApi) {
|
|
4823
|
+
missing.push({
|
|
4824
|
+
kind: "mock-api",
|
|
4825
|
+
requirementIds: [...input.requirementIds],
|
|
4826
|
+
reason: "global Mock/data phase has no committed mock-api fact",
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4829
|
+
for (const interaction of liveInteractions) {
|
|
4830
|
+
if (coveredInteractions.has(interaction))
|
|
4831
|
+
continue;
|
|
4832
|
+
missing.push({
|
|
4833
|
+
kind: "data-flow",
|
|
4834
|
+
id: interaction,
|
|
4835
|
+
requirementIds: [...input.requirementIds],
|
|
4836
|
+
reason: `interaction ${interaction} has no committed data-flow fact`,
|
|
4837
|
+
});
|
|
4838
|
+
}
|
|
4839
|
+
return missing;
|
|
4840
|
+
}
|
|
3809
4841
|
/** Estimate calls conservatively: requirement + one VT, with a second VT
|
|
3810
4842
|
* reserved for behaviour-required requirements. Explicit declarations win. */
|
|
3811
4843
|
export function estimateFrontendPlanRequirementRecordCalls(fact) {
|
|
@@ -4032,6 +5064,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
4032
5064
|
? { routeOrMount: compactPromptString(surface.routeOrMount, 180) }
|
|
4033
5065
|
: {}),
|
|
4034
5066
|
implementationPaths: compactPromptStringArray(surface.implementationPaths),
|
|
5067
|
+
proposedPaths: compactPromptStringArray(surface.proposedPaths),
|
|
4035
5068
|
testPaths: compactPromptStringArray(surface.testPaths),
|
|
4036
5069
|
...(compactPromptString(surface.dataSource, 180)
|
|
4037
5070
|
? { dataSource: compactPromptString(surface.dataSource, 180) }
|
|
@@ -4263,7 +5296,7 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4263
5296
|
continue;
|
|
4264
5297
|
}
|
|
4265
5298
|
if (fact.kind === "target-surface") {
|
|
4266
|
-
compactFacts.push({ kind: fact.kind, routes: compactPromptStringArray(fact.routes, 24, 180) });
|
|
5299
|
+
compactFacts.push({ kind: fact.kind, routes: compactPromptStringArray(fact.routes, 24, 180), implementationPaths: compactPromptStringArray(fact.implementationPaths, 24, 180), proposedPaths: compactPromptStringArray(fact.proposedPaths, 24, 180), testPaths: compactPromptStringArray(fact.testPaths, 24, 180) });
|
|
4267
5300
|
}
|
|
4268
5301
|
}
|
|
4269
5302
|
if (compactFacts.length === 0)
|
|
@@ -4311,19 +5344,21 @@ export async function runFrontendReviewSegmentedSessions(input) {
|
|
|
4311
5344
|
protocol.setActiveScope(scopes.map(s => s.id));
|
|
4312
5345
|
const finalScope = input.inventory.scopes.every(s => protocol.completedScopeIds().has(s.id) || scopes.some(current => current.id === s.id));
|
|
4313
5346
|
const customTools = finalScope ? input.customTools : input.customTools.filter(t => !terminalKinds.includes(String(t.name)));
|
|
4314
|
-
const prompt = `${input.basePrompt}\n<frontend_review_scope>\n${JSON.stringify({ semantics: "full", inventoryDigest: input.inventory.digest, scopes, previouslyCompleted: [...protocol.completedScopeIds()], savedFindings: protocol.committedFacts().filter(r => String(r.fact.kind).endsWith("-finding")).map(r => ({ id: r.fact.id, finding: r.fact.finding })) })}\n</frontend_review_scope>\nReview the complete supplied scopes, saving each finding immediately. Call complete_review_scope for each exact id only after all its independent permissions, thresholds, errors and evidence have been checked. ${finalScope ? "After every scope is complete, make one independent overall approve/request_changes decision; persisted findings cannot be omitted." : "More scopes remain. Do not finalize or reread already completed scopes unless resolving a cross-scope issue."}`;
|
|
5347
|
+
const prompt = `${input.basePrompt}\n<frontend_review_authority>\nThe Contract acceptance criteria, constraints, required deliverables, UI-state declarations, and verification expectations in the supplied input are authoritative. Review the actual diff and evidence against those facts; do not replace them with a Plan-derived interpretation.\n</frontend_review_authority>\n<frontend_review_scope>\n${JSON.stringify({ semantics: "full", inventoryDigest: input.inventory.digest, scopes, previouslyCompleted: [...protocol.completedScopeIds()], savedFindings: protocol.committedFacts().filter(r => String(r.fact.kind).endsWith("-finding")).map(r => ({ id: r.fact.id, finding: r.fact.finding })) })}\n</frontend_review_scope>\nReview the complete supplied scopes, saving each finding immediately. Call complete_review_scope for each exact id only after all its independent permissions, thresholds, errors and evidence have been checked. ${finalScope ? "After every scope is complete, make one independent overall approve/request_changes decision; persisted findings cannot be omitted." : "More scopes remain. Do not finalize or reread already completed scopes unless resolving a cross-scope issue."}`;
|
|
4315
5348
|
if (Buffer.byteLength(prompt) > targetBytes && scopes.length > 1) {
|
|
4316
5349
|
const at = Math.ceil(scopes.length / 2);
|
|
4317
5350
|
queue.splice(index, 1, { scopes: scopes.slice(0, at), repairs: 0 }, { scopes: scopes.slice(at), repairs: 0 });
|
|
4318
5351
|
index--;
|
|
4319
5352
|
continue;
|
|
4320
5353
|
}
|
|
5354
|
+
const completionInstruction = `${item.repairs ? "REPAIR: the prior session did not commit all required checkpoints/verdict. Do not repeat the review narrative. " : ""}Complete these exact runtime scope IDs using complete_review_scope: ${JSON.stringify(scopes.map(scope => scope.id))}. These are scope IDs, not node IDs. ${finalScope ? `After those checkpoints succeed, call exactly one terminal tool: ${terminalKinds.join(" or ")}. A prose conclusion is not a committed verdict.` : "More scopes remain; do not submit an overall verdict yet."}`;
|
|
5355
|
+
const userMessage = `${input.sessionOptions.userMessage}\n\n${completionInstruction}`;
|
|
4321
5356
|
if (++calls > 128)
|
|
4322
5357
|
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_RECOVERY_EXHAUSTED: session quota reached" };
|
|
4323
|
-
last = await observeFrontendSession({ ...input.observation, phase: `${input.phase}/scope`, scopeIds: scopes.map(s => s.id), prompt, userMessage
|
|
5358
|
+
last = await observeFrontendSession({ ...input.observation, phase: `${input.phase}/scope`, scopeIds: scopes.map(s => s.id), prompt, userMessage, customTools,
|
|
4324
5359
|
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${calls}.json` : undefined,
|
|
4325
5360
|
committedCount: () => protocol.committedFacts().length, durableCommittedCount: () => protocol.committedFacts().length,
|
|
4326
|
-
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
5361
|
+
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, userMessage, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
4327
5362
|
await input.tools.flush();
|
|
4328
5363
|
await input.inventory.validate();
|
|
4329
5364
|
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
@@ -4339,8 +5374,30 @@ export async function runFrontendReviewSegmentedSessions(input) {
|
|
|
4339
5374
|
index--;
|
|
4340
5375
|
continue;
|
|
4341
5376
|
}
|
|
4342
|
-
if (!last.ok && !capacity)
|
|
5377
|
+
if (!last.ok && !capacity) {
|
|
5378
|
+
// Terminal-only completion: this protocol finishes through durable
|
|
5379
|
+
// tools (complete_review_scope -> approve_review/request_review_changes),
|
|
5380
|
+
// so the model can end its turn with no closing prose and the step
|
|
5381
|
+
// classifies as empty-output even though the review is complete
|
|
5382
|
+
// (smoke r27/r31/r32 committed the terminal and still reported
|
|
5383
|
+
// empty-output; the node then replayed the committed fact in an
|
|
5384
|
+
// extra attempt). Accept the segment here instead, so a completed
|
|
5385
|
+
// review neither spends a replay attempt nor depends on the retry
|
|
5386
|
+
// ladder, and a misclassified category (smoke r28 read a finding id
|
|
5387
|
+
// containing UNAUTHORIZED as an auth error, which is not retryable)
|
|
5388
|
+
// can no longer turn a committed review into a node failure.
|
|
5389
|
+
if (last.failureCategory === "empty-output" && terminal() && !missing.length) {
|
|
5390
|
+
try {
|
|
5391
|
+
await input.tools.flush();
|
|
5392
|
+
protocol.assertComplete();
|
|
5393
|
+
return { ...last, ok: true, failureCategory: "success" };
|
|
5394
|
+
}
|
|
5395
|
+
catch {
|
|
5396
|
+
// Unfinished scope checkpoints still fail this segment.
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
4343
5399
|
return last;
|
|
5400
|
+
}
|
|
4344
5401
|
if (missing.length || finalScope && !terminal()) {
|
|
4345
5402
|
if (item.repairs >= 1)
|
|
4346
5403
|
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_SCOPE_INCOMPLETE: required checkpoint or verdict missing" };
|
|
@@ -4409,11 +5466,15 @@ export async function runFrontendScoutSegmentedSessions(input) {
|
|
|
4409
5466
|
}
|
|
4410
5467
|
}
|
|
4411
5468
|
await input.tools.flush();
|
|
4412
|
-
const { readCompleteScoutTargetSurface } = await import("../workflows/dag/frontend-
|
|
5469
|
+
const { readCompleteScoutTargetSurface } = await import("../workflows/dag/frontend-committed-facts.js");
|
|
4413
5470
|
const closure = readCompleteScoutTargetSurface(input.tools.committedFacts());
|
|
4414
5471
|
return closure.ok ? { ...last, ok: true, failureCategory: "success" } : { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: closure.reason };
|
|
4415
5472
|
}
|
|
4416
5473
|
export async function runFrontendContractSegmentedSessions(input) {
|
|
5474
|
+
// Requirement identity, text, and source fragments are frozen in the
|
|
5475
|
+
// runtime ledger. Seed those facts once; model sessions spend their budget
|
|
5476
|
+
// on execution grouping, evidence expectations, and genuine decisions.
|
|
5477
|
+
await input.tools.seedCanonicalRequirements();
|
|
4417
5478
|
// Build from the frozen runtime inventory if the caller has not rendered it yet.
|
|
4418
5479
|
const basePrompt = parseFrontendInputBlock(input.basePrompt, "contract") ? input.basePrompt : input.basePrompt +
|
|
4419
5480
|
`\n<frontend_contract_input>\nFrozen complete source obligations.\n${JSON.stringify({ requirements: input.tools.inputRequirements() })}\n</frontend_contract_input>`;
|
|
@@ -4438,13 +5499,14 @@ export async function runFrontendContractSegmentedSessions(input) {
|
|
|
4438
5499
|
const groupIndex = collectFrontendExecutionGroups(input.tools.committedFacts().filter(r => r.fact.kind === "requirement").map(r => ({ id: String(r.fact.id), execution: r.fact.execution })));
|
|
4439
5500
|
const shared = input.tools.committedFacts().filter(r => !["requirement", "contract-finalized", "contract-scope-completed"].includes(String(r.fact.kind))).map(r => r.fact);
|
|
4440
5501
|
const prompt = projectFrontendContractPrompt(basePrompt, scopeIds) +
|
|
4441
|
-
`\nCONTRACT SCOPE: analyze only ${scopeIds.join(", ") || "(all scopes complete; verify global facts and terminal)"}.
|
|
5502
|
+
`\nCONTRACT SCOPE: analyze only ${scopeIds.join(", ") || "(all scopes complete; verify global facts and terminal)"}. Requirement identity/text/source fragments are already committed by runtime; do not call record_requirement. Use record_requirement_execution for model-owned execution grouping, then submit decisions and evidence records. Call complete_contract_scope after ALL decisions for this scope. ` +
|
|
4442
5503
|
(finalScope ? "After complete scope coverage and source-bound deliverables, call finalize_contract. Correct rejected calls and retry." : "Do not finalize; subsequent complete scopes remain.") +
|
|
4443
5504
|
`\n<committed_contract_facts>\n${JSON.stringify({ facts: shared, executionGroups: groupIndex })}\n</committed_contract_facts>`;
|
|
4444
5505
|
const customTools = input.tools.customTools.filter(t => finalScope || t.name !== "finalize_contract");
|
|
4445
5506
|
invocation += 1;
|
|
4446
5507
|
if (invocation > maxSessions)
|
|
4447
5508
|
return { ...last, ok: false, failureCategory: "invalid-output", stderr: "CONTRACT_RECOVERY_EXHAUSTED: session quota exceeded" };
|
|
5509
|
+
const committedBefore = input.tools.committedFacts().length;
|
|
4448
5510
|
last = await observeFrontendSession({
|
|
4449
5511
|
...input.observation, phase: "contract/scope", scopeIds, prompt, userMessage: input.sessionOptions.userMessage, customTools,
|
|
4450
5512
|
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${invocation}.json` : undefined,
|
|
@@ -4476,8 +5538,17 @@ export async function runFrontendContractSegmentedSessions(input) {
|
|
|
4476
5538
|
index -= 1;
|
|
4477
5539
|
break;
|
|
4478
5540
|
}
|
|
4479
|
-
|
|
5541
|
+
// A session that ends with blank assistant text but committed new
|
|
5542
|
+
// facts is not a node failure: small-output models legitimately
|
|
5543
|
+
// stop after their tool calls. Continue so the scope/repair checks
|
|
5544
|
+
// below decide, instead of burning a full node retry.
|
|
5545
|
+
const committedFactsOnlySuccess = !(last.assistantText ?? "").trim() &&
|
|
5546
|
+
!last.stderr.trim() &&
|
|
5547
|
+
input.tools.committedFacts().length > committedBefore;
|
|
5548
|
+
if (!last.ok && !committedFactsOnlySuccess)
|
|
4480
5549
|
return last;
|
|
5550
|
+
if (committedFactsOnlySuccess)
|
|
5551
|
+
last = { ...last, ok: true, failureCategory: "success" };
|
|
4481
5552
|
const missing = scopeIds.filter(id => !input.tools.completedScopeRequirementIds().has(id));
|
|
4482
5553
|
if (!missing.length && (!finalScope || terminal()))
|
|
4483
5554
|
break;
|
|
@@ -4512,16 +5583,18 @@ function buildFrontendPlanWorkload(input) {
|
|
|
4512
5583
|
const workCost = (ids) => 1 + ids.reduce((total, id) => total + Math.max(1, (input.requirementCosts?.get(id) ?? 2) - 1), 0);
|
|
4513
5584
|
const policy = input.sessionOptions.frontendExecutionPolicy;
|
|
4514
5585
|
const targetBytes = policy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
4515
|
-
const userMessageBytes = Buffer.byteLength(input.sessionOptions.userMessage ?? "");
|
|
4516
5586
|
const buildWorkBatches = (ids) => {
|
|
4517
5587
|
const work = workGroups.flatMap((g, index) => {
|
|
4518
5588
|
const members = g.requirementIds.filter(id => ids.includes(id));
|
|
4519
5589
|
return members.length ? [{ id: `${index}:${g.id}`, requirementIds: members,
|
|
4520
5590
|
requirements: members.map(id => fullUnits.get(id) ?? { id }), estimatedCalls: workCost(members) }] : [];
|
|
4521
5591
|
});
|
|
4522
|
-
|
|
5592
|
+
// Pack complete work units, not the repeated request scaffold. Deducting
|
|
5593
|
+
// fixed context/tools can leave a one-byte budget and force one AC per
|
|
5594
|
+
// session without reducing that overhead. The SDK checks the actual
|
|
5595
|
+
// request against model capacity; capacity recovery splits unfinished work.
|
|
4523
5596
|
return packFrontendInputUnits(work, {
|
|
4524
|
-
targetBytes
|
|
5597
|
+
targetBytes,
|
|
4525
5598
|
maxUnits: policy?.maxScopeUnits ?? 4,
|
|
4526
5599
|
maxCost: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS, cost: g => g.estimatedCalls,
|
|
4527
5600
|
}).map(batch => batch.flatMap(g => g.requirementIds));
|
|
@@ -4533,11 +5606,31 @@ function buildFrontendPlanWorkload(input) {
|
|
|
4533
5606
|
frozenInput, workGroups, buildWorkBatches,
|
|
4534
5607
|
compactEligible: input.requirementCosts !== undefined &&
|
|
4535
5608
|
workGroups.length <= FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS &&
|
|
4536
|
-
Buffer.byteLength(input.basePrompt) + userMessageBytes + Buffer.byteLength(JSON.stringify(input.allTools)) <= targetBytes * 2 &&
|
|
4537
5609
|
workGroups.reduce((total, group) => total + workCost(group.requirementIds), 0) <= FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS &&
|
|
4538
5610
|
countFrontendPlanTargetSurfaces(input.basePrompt) === 1,
|
|
4539
5611
|
};
|
|
4540
5612
|
}
|
|
5613
|
+
/**
|
|
5614
|
+
* Requirement ids the plan coverage layout must own, in first-commit order.
|
|
5615
|
+
*
|
|
5616
|
+
* The contract ledger is append-only and `record_requirement` is incremental, so
|
|
5617
|
+
* the same id can legitimately be committed more than once. Deduplicating here is
|
|
5618
|
+
* load-bearing: `buildFrontendPlanWorkload` seeds one `unclassified` work group per
|
|
5619
|
+
* list entry without re-checking membership, so a repeated id produced two groups,
|
|
5620
|
+
* `buildWorkBatches` emitted the id twice, and the coverage-layout validator then
|
|
5621
|
+
* rejected the layout the runtime had derived itself (members 8 !== owners 7) as
|
|
5622
|
+
* FRONTEND_PLAN_LAYOUT_INVALID with failureCategory tool-policy - killing the run
|
|
5623
|
+
* at the plan node over a completely ordinary ledger.
|
|
5624
|
+
*/
|
|
5625
|
+
export function collectFrontendPlanRequirementIds(contractFacts) {
|
|
5626
|
+
return [
|
|
5627
|
+
...new Set(contractFacts
|
|
5628
|
+
.filter((record) => record.fact?.kind ===
|
|
5629
|
+
"requirement")
|
|
5630
|
+
.map((record) => record.fact?.id)
|
|
5631
|
+
.filter((id) => typeof id === "string")),
|
|
5632
|
+
];
|
|
5633
|
+
}
|
|
4541
5634
|
const frontendPlanCoverageLayoutSchema = z.object({
|
|
4542
5635
|
schemaVersion: z.literal(1),
|
|
4543
5636
|
bindingSha256: z.string(),
|
|
@@ -4601,12 +5694,11 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4601
5694
|
const uxRegistrySegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "ux-registry");
|
|
4602
5695
|
const uxSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "ux-local");
|
|
4603
5696
|
const globalMockDataSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "global-mock-data");
|
|
4604
|
-
const finalizeSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "finalize");
|
|
4605
5697
|
const allRequirementIds = input.requirementIds ?? [];
|
|
4606
5698
|
const buildPhasePrompt = (segment, missing = [], scopeIds = allRequirementIds) => {
|
|
4607
5699
|
const compact = allRequirementIds.length > 0
|
|
4608
5700
|
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, scopeIds, {
|
|
4609
|
-
includeRequirementText: segment.id === "global-mock-data",
|
|
5701
|
+
includeRequirementText: segment.id === "global-mock-data" || segment.id === "finalize",
|
|
4610
5702
|
includeVerificationTargets: false,
|
|
4611
5703
|
includeDesignEvidence: segment.id === "global-dependency-deviation" || segment.id === "finalize",
|
|
4612
5704
|
includeChecklist: false,
|
|
@@ -4727,7 +5819,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4727
5819
|
return [
|
|
4728
5820
|
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, allRequirementIds),
|
|
4729
5821
|
"PLAN PHASE — compact local planning for a small frontend request.",
|
|
4730
|
-
"Review all listed requirements together and record_state_registry first with one global UX vocabulary. Then record every plan-requirement and verification-target fact, followed by
|
|
5822
|
+
"Review all listed requirements together and record_state_registry first with one global UX vocabulary. Then record every plan-requirement and verification-target fact, followed by component/state-flow facts and the minimal route, Mock/data, dependency, and design-deviation policy facts needed by the observable behavior. Do not read the repository or task source; use only the committed input above. Do not call finalize_plan in this session.",
|
|
4731
5823
|
"TOOL-FIRST: your first assistant actions must be record_* tool calls, at most 2-3 facts per message. Do not draft the whole analysis before recording; if a fact is uncertain, record it with an evidence gap instead of reasoning longer.",
|
|
4732
5824
|
ledger,
|
|
4733
5825
|
...(missing.length > 0
|
|
@@ -4738,8 +5830,6 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4738
5830
|
: []),
|
|
4739
5831
|
].filter(Boolean).join("\n\n");
|
|
4740
5832
|
};
|
|
4741
|
-
const compactFinalizeInstruction = "This is a small-request compact pass. Reconcile the committed local facts with route, data-flow, Mock/API, dependency and deviation policy, then call finalize_plan; correct rejected facts and retry until exactly one successful terminal commit.";
|
|
4742
|
-
const buildCompactFinalizePrompt = (missing = []) => [buildPhasePrompt(finalizeSegment, missing), compactFinalizeInstruction].join("\n\n");
|
|
4743
5833
|
const mapPlannerExhaustion = (r, committedAnyFacts) => isPlannerThinkingExhausted(r, committedAnyFacts)
|
|
4744
5834
|
? {
|
|
4745
5835
|
...r,
|
|
@@ -4782,12 +5872,80 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4782
5872
|
tokensUsed: 0,
|
|
4783
5873
|
};
|
|
4784
5874
|
}
|
|
5875
|
+
const compiledInput = parseFrontendInputBlock(input.basePrompt, "plan")?.payload;
|
|
5876
|
+
const fullUnits = new Map(compiledInput?.requirements.map(r => [r.id, r]) ?? []);
|
|
5877
|
+
const workCost = (ids) => 1 + ids.reduce((total, id) => total + Math.max(1, (input.requirementCosts?.get(id) ?? 2) - 1), 0);
|
|
4785
5878
|
const { workGroups, buildWorkBatches, compactEligible } = buildFrontendPlanWorkload({
|
|
4786
5879
|
basePrompt: input.basePrompt, requirementIds: allRequirementIds,
|
|
4787
5880
|
requirementCosts: input.requirementCosts, sessionOptions: input.sessionOptions,
|
|
4788
|
-
coverageTools: input.segmentCustomTools(coverageSegment.toolNames),
|
|
4789
|
-
allTools: input.segmentCustomTools(null),
|
|
4790
5881
|
});
|
|
5882
|
+
// Data decisions are grouped by observable data domain. Requirements that
|
|
5883
|
+
// mention the same endpoint/resource or execution group share one session;
|
|
5884
|
+
// unrelated domains remain isolated. This prevents the old requirement-by-
|
|
5885
|
+
// requirement repetition while preserving the packer's size bound.
|
|
5886
|
+
const buildDataBatches = (ids) => {
|
|
5887
|
+
const selected = new Set(ids);
|
|
5888
|
+
const domains = new Map();
|
|
5889
|
+
for (const [index, group] of workGroups.entries()) {
|
|
5890
|
+
const members = group.requirementIds.filter((id) => selected.has(id));
|
|
5891
|
+
if (!members.length)
|
|
5892
|
+
continue;
|
|
5893
|
+
const texts = members.map((id) => String(fullUnits.get(id)?.text ?? ""));
|
|
5894
|
+
const endpoint = texts
|
|
5895
|
+
.map((text) => text.match(/\b(?:GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s,;.)]+)/i)?.[1])
|
|
5896
|
+
.find(Boolean);
|
|
5897
|
+
const resource = endpoint
|
|
5898
|
+
? endpoint.split("/").filter(Boolean).slice(0, 2).join("/")
|
|
5899
|
+
: undefined;
|
|
5900
|
+
const domain = resource ? `endpoint:${resource}` : `group:${group.id}`;
|
|
5901
|
+
const current = domains.get(domain);
|
|
5902
|
+
if (current) {
|
|
5903
|
+
current.requirementIds.push(...members);
|
|
5904
|
+
current.requirements.push(...members.map((id) => fullUnits.get(id) ?? { id }));
|
|
5905
|
+
current.estimatedCalls = workCost(current.requirementIds);
|
|
5906
|
+
}
|
|
5907
|
+
else {
|
|
5908
|
+
domains.set(domain, {
|
|
5909
|
+
id: `${index}:${domain}`,
|
|
5910
|
+
requirementIds: [...members],
|
|
5911
|
+
requirements: members.map((id) => fullUnits.get(id) ?? { id }),
|
|
5912
|
+
estimatedCalls: workCost(members),
|
|
5913
|
+
});
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
const work = [...domains.values()];
|
|
5917
|
+
const scaffoldBytes = Buffer.byteLength(input.basePrompt.replace(/<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/, JSON.stringify({ ...compiledInput, requirements: [] }))) + Buffer.byteLength(JSON.stringify(input.segmentCustomTools(globalMockDataSegment.toolNames)));
|
|
5918
|
+
return packFrontendInputUnits(work, {
|
|
5919
|
+
targetBytes: Math.max(1, (input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES) - scaffoldBytes),
|
|
5920
|
+
maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits ?? 4,
|
|
5921
|
+
maxCost: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS,
|
|
5922
|
+
cost: (group) => group.estimatedCalls,
|
|
5923
|
+
}).map((batch) => batch.flatMap((group) => group.requirementIds));
|
|
5924
|
+
};
|
|
5925
|
+
// Dependency/deviation is optional policy. For a pure local UI request with
|
|
5926
|
+
// no dependency, package, design-conflict, or OpenSpec signal, opening a
|
|
5927
|
+
// dedicated model session only produces an empty policy fact. Keep the
|
|
5928
|
+
// session when the prompt or ledger contains any such signal so this is a
|
|
5929
|
+
// conservative skip, not a blanket removal of the gate.
|
|
5930
|
+
const dependencyDeviationNeeded = /(?:dependenc|package\.json|npm\s+(?:install|i)|yarn\s+add|pnpm\s+add|openspec|design\s+conflict|规范冲突|依赖)/i.test(input.basePrompt) ||
|
|
5931
|
+
Boolean(input.committedFacts?.().some((record) => {
|
|
5932
|
+
const fact = committedFactFromPlanRecord(record);
|
|
5933
|
+
return fact?.kind === "dependency" || fact?.kind === "design-deviation";
|
|
5934
|
+
}));
|
|
5935
|
+
const globalPolicyToolNames = new Set([
|
|
5936
|
+
"record_route_selection",
|
|
5937
|
+
"record_dependency",
|
|
5938
|
+
"record_design_deviation",
|
|
5939
|
+
"adopt_staged_fact",
|
|
5940
|
+
]);
|
|
5941
|
+
const globalPolicyPrompt = [
|
|
5942
|
+
"PLAN PHASE — global implementation policy.",
|
|
5943
|
+
"Use the Scout target surface to record the selected route(s), then record dependency policy and design-evidence conflicts only when they are relevant. Do not record requirement-local UX or Mock/data facts. Do not call finalize_plan.",
|
|
5944
|
+
dependencyDeviationNeeded
|
|
5945
|
+
? "Dependency/design policy signals are present; inspect them and commit the minimal policy facts needed."
|
|
5946
|
+
: "No dependency/design-conflict signal was found; do not invent a policy fact.",
|
|
5947
|
+
].join(" ");
|
|
5948
|
+
let globalPolicyQueued = false;
|
|
4791
5949
|
// Small, single-surface requests do not benefit from six isolated Pi
|
|
4792
5950
|
// sessions. Keep the typed ledger as the authority, but let one local
|
|
4793
5951
|
// session establish requirement/UX facts and one final session establish
|
|
@@ -4805,6 +5963,12 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4805
5963
|
"record_state_registry",
|
|
4806
5964
|
"record_component_choice",
|
|
4807
5965
|
"record_state_flow",
|
|
5966
|
+
"record_route_selection",
|
|
5967
|
+
"record_data_flow",
|
|
5968
|
+
"record_mock_api",
|
|
5969
|
+
"record_mock_endpoint",
|
|
5970
|
+
"record_dependency",
|
|
5971
|
+
"record_design_deviation",
|
|
4808
5972
|
"adopt_staged_fact",
|
|
4809
5973
|
]);
|
|
4810
5974
|
queue.push({
|
|
@@ -4813,11 +5977,6 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4813
5977
|
requirementSlice: [...allRequirementIds],
|
|
4814
5978
|
prompt: buildCompactLocalPrompt(),
|
|
4815
5979
|
});
|
|
4816
|
-
queue.push({
|
|
4817
|
-
id: "finalize",
|
|
4818
|
-
toolNames: null,
|
|
4819
|
-
prompt: buildCompactFinalizePrompt(),
|
|
4820
|
-
});
|
|
4821
5980
|
}
|
|
4822
5981
|
else if (!requirementIdsProvided) {
|
|
4823
5982
|
queue.push({
|
|
@@ -4856,10 +6015,25 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4856
6015
|
prompt: slice.length ? buildUxPrompt(slice) : buildPhasePrompt(uxSegment),
|
|
4857
6016
|
}));
|
|
4858
6017
|
for (const segment of FRONTEND_PLAN_SEGMENTS) {
|
|
4859
|
-
if (["coverage", "ux-registry", "ux-local"
|
|
6018
|
+
if (["coverage", "ux-registry", "ux-local"].includes(segment.id))
|
|
6019
|
+
continue;
|
|
6020
|
+
if (segment.id === "global-dependency-deviation" && !dependencyDeviationNeeded)
|
|
6021
|
+
continue;
|
|
6022
|
+
if (segment.id === "global-route") {
|
|
6023
|
+
if (globalPolicyQueued)
|
|
6024
|
+
continue;
|
|
6025
|
+
globalPolicyQueued = true;
|
|
6026
|
+
queue.push({
|
|
6027
|
+
id: "global-policy",
|
|
6028
|
+
toolNames: globalPolicyToolNames,
|
|
6029
|
+
prompt: `${buildPhasePrompt(segment)}\n\n${globalPolicyPrompt}`,
|
|
6030
|
+
});
|
|
6031
|
+
continue;
|
|
6032
|
+
}
|
|
6033
|
+
if (segment.id === "global-dependency-deviation")
|
|
4860
6034
|
continue;
|
|
4861
6035
|
if (segment.id === "global-mock-data" && requirementIdsProvided) {
|
|
4862
|
-
|
|
6036
|
+
buildDataBatches(allRequirementIds).forEach((slice, i) => queue.push({ id: `global-mock-data-${i + 1}`, toolNames: segment.toolNames, requirementSlice: slice, prompt: buildPhasePrompt(segment, [], slice) }));
|
|
4863
6037
|
}
|
|
4864
6038
|
else {
|
|
4865
6039
|
queue.push({
|
|
@@ -4869,11 +6043,6 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4869
6043
|
});
|
|
4870
6044
|
}
|
|
4871
6045
|
}
|
|
4872
|
-
queue.push({
|
|
4873
|
-
id: finalizeSegment.id,
|
|
4874
|
-
toolNames: finalizeSegment.toolNames,
|
|
4875
|
-
prompt: buildPhasePrompt(finalizeSegment),
|
|
4876
|
-
});
|
|
4877
6046
|
}
|
|
4878
6047
|
let accumulated;
|
|
4879
6048
|
let index = 0;
|
|
@@ -4881,6 +6050,8 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4881
6050
|
let lastDurableCount = input.committedFactCount();
|
|
4882
6051
|
while (index < queue.length) {
|
|
4883
6052
|
const session = queue[index];
|
|
6053
|
+
if (!session)
|
|
6054
|
+
break;
|
|
4884
6055
|
if (input.attempt > 1 && input.committedFacts && session.id === "ux-registry" &&
|
|
4885
6056
|
collectFrontendPlanPhaseMissingFacts({ phase: "ux-registry", requirementIds: allRequirementIds, committedFacts: input.committedFacts() }).length === 0) {
|
|
4886
6057
|
index += 1;
|
|
@@ -4917,6 +6088,32 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4917
6088
|
index += 1;
|
|
4918
6089
|
continue;
|
|
4919
6090
|
}
|
|
6091
|
+
// Resume only unfinished UX scopes. The registry and local decisions are
|
|
6092
|
+
// durable facts; replaying a completed scope wastes a model session and
|
|
6093
|
+
// can make a previously valid shared decision look like a duplicate.
|
|
6094
|
+
if (session.id === "ux-registry" &&
|
|
6095
|
+
input.committedFacts &&
|
|
6096
|
+
!collectFrontendPlanPhaseMissingFacts({
|
|
6097
|
+
phase: "ux-registry",
|
|
6098
|
+
requirementIds: allRequirementIds,
|
|
6099
|
+
committedFacts: input.committedFacts(),
|
|
6100
|
+
}).length) {
|
|
6101
|
+
index += 1;
|
|
6102
|
+
continue;
|
|
6103
|
+
}
|
|
6104
|
+
if (session.id.startsWith("ux-local-") &&
|
|
6105
|
+
session.requirementSlice &&
|
|
6106
|
+
(input.behaviorRequiredRequirementIds?.length ?? 0) > 0 &&
|
|
6107
|
+
input.committedFacts &&
|
|
6108
|
+
!collectFrontendPlanPhaseMissingFacts({
|
|
6109
|
+
phase: "ux-local",
|
|
6110
|
+
requirementIds: session.requirementSlice,
|
|
6111
|
+
committedFacts: input.committedFacts(),
|
|
6112
|
+
behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds,
|
|
6113
|
+
}).length) {
|
|
6114
|
+
index += 1;
|
|
6115
|
+
continue;
|
|
6116
|
+
}
|
|
4920
6117
|
let prompt = session.prompt;
|
|
4921
6118
|
if (session.coverageOnly && session.coverageSlice) {
|
|
4922
6119
|
const promptSlice = [...new Set([...remaining, ...preexistingMissing.flatMap(item => item.requirementIds)])];
|
|
@@ -4936,15 +6133,16 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4936
6133
|
: buildPhasePrompt(uxSegment, session.missingFacts);
|
|
4937
6134
|
}
|
|
4938
6135
|
else {
|
|
4939
|
-
// Global phases
|
|
6136
|
+
// Global phases consume the latest committed ledger;
|
|
4940
6137
|
// constructing their prompt only when the session starts prevents a
|
|
4941
6138
|
// stale queue entry from dropping facts written by earlier phases.
|
|
4942
6139
|
const segment = FRONTEND_PLAN_SEGMENTS.find((candidate) => candidate.id === session.id || (candidate.id === "global-mock-data" && session.id.startsWith("global-mock-data-")));
|
|
4943
|
-
if (
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
6140
|
+
if (session.id === "global-policy") {
|
|
6141
|
+
const routePrompt = buildPhasePrompt(FRONTEND_PLAN_SEGMENTS.find((candidate) => candidate.id === "global-route"));
|
|
6142
|
+
prompt = `${routePrompt}\n\n${globalPolicyPrompt}`;
|
|
6143
|
+
}
|
|
6144
|
+
else if (segment) {
|
|
6145
|
+
prompt = buildPhasePrompt(segment, session.missingFacts, session.requirementSlice ?? allRequirementIds);
|
|
4948
6146
|
}
|
|
4949
6147
|
}
|
|
4950
6148
|
const atomicFocus = session.atomicRecovery ? session.missingFacts?.[0] : undefined;
|
|
@@ -4971,17 +6169,6 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4971
6169
|
"state-flow": "record_state_flow", "data-flow": "record_data_flow", "mock-api": "record_mock_api",
|
|
4972
6170
|
};
|
|
4973
6171
|
const customTools = input.segmentCustomTools(atomicFocus ? new Set([atomicTools[atomicFocus.kind]]) : session.toolNames);
|
|
4974
|
-
const scopeForPacking = session.coverageSlice ?? session.requirementSlice;
|
|
4975
|
-
const envelopeBytes = Buffer.byteLength(prompt) + Buffer.byteLength(input.sessionOptions.userMessage ?? "") + Buffer.byteLength(JSON.stringify(customTools));
|
|
4976
|
-
if (envelopeBytes > (input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES) && scopeForPacking && session.id !== "compact-local") {
|
|
4977
|
-
const groups = workGroups.map(g => g.requirementIds.filter(id => scopeForPacking.includes(id))).filter(g => g.length);
|
|
4978
|
-
if (groups.length > 1) {
|
|
4979
|
-
const half = Math.ceil(groups.length / 2);
|
|
4980
|
-
queue.splice(index, 1, ...[groups.slice(0, half).flat(), groups.slice(half).flat()].map(slice => ({ ...session, ...(session.coverageOnly ? { coverageSlice: slice } : { requirementSlice: slice }) })));
|
|
4981
|
-
invocationCount -= 1;
|
|
4982
|
-
continue;
|
|
4983
|
-
}
|
|
4984
|
-
}
|
|
4985
6172
|
const result = await observeFrontendSession({
|
|
4986
6173
|
...input.observation, phase: `plan/${session.id}`, scopeIds: session.requirementSlice ?? session.coverageSlice ?? allRequirementIds,
|
|
4987
6174
|
prompt, userMessage: input.sessionOptions.userMessage, customTools, committedCount: input.committedFactCount, durableCommittedCount: () => lastDurableCount,
|
|
@@ -5013,8 +6200,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
5013
6200
|
? combineSequentialPiResults(accumulated, result)
|
|
5014
6201
|
: result;
|
|
5015
6202
|
const committedAfter = input.committedFactCount();
|
|
5016
|
-
const committedFactsOnlySuccess =
|
|
5017
|
-
!(result.assistantText ?? "").trim() &&
|
|
6203
|
+
const committedFactsOnlySuccess = !(result.assistantText ?? "").trim() &&
|
|
5018
6204
|
!result.stderr.trim() &&
|
|
5019
6205
|
!result.timedOut &&
|
|
5020
6206
|
committedAfter > committedBefore;
|
|
@@ -5094,16 +6280,14 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
5094
6280
|
? buildCompactLocalPrompt(missing)
|
|
5095
6281
|
: isUxRegistrySession
|
|
5096
6282
|
? buildUxRegistryPrompt(missing)
|
|
5097
|
-
:
|
|
5098
|
-
?
|
|
5099
|
-
:
|
|
5100
|
-
? buildUxPrompt(session.requirementSlice ?? [], missing)
|
|
5101
|
-
: buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
|
|
6283
|
+
: isUxLocalSession
|
|
6284
|
+
? buildUxPrompt(session.requirementSlice ?? [], missing)
|
|
6285
|
+
: buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
|
|
5102
6286
|
const recovery = classifyFrontendPlanRecovery({ ...result, stopReason: readWriterThinkingExhaustionEvidence(result).stopReason });
|
|
5103
6287
|
if (recovery === "stop")
|
|
5104
6288
|
return { ...result, ok: false };
|
|
5105
6289
|
if ((recovery === "output" || (session.atomicRecovery && result.ok)) && input.committedFacts &&
|
|
5106
|
-
!(missingPhaseFacts.length === 1 && missingPhaseFacts[0]
|
|
6290
|
+
!(missingPhaseFacts.length === 1 && missingPhaseFacts[0]?.kind === "plan-requirement") &&
|
|
5107
6291
|
(session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data"))) {
|
|
5108
6292
|
if (missingPhaseFacts.length === 0) {
|
|
5109
6293
|
// A complete validated slice does not need a successful prose turn.
|
|
@@ -5329,6 +6513,91 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
5329
6513
|
failureCategory: "invalid-output",
|
|
5330
6514
|
};
|
|
5331
6515
|
}
|
|
6516
|
+
if (!input.parallelCoverageOnly && input.finalizePlan) {
|
|
6517
|
+
// Preserve optional closeout context if an earlier correction or future
|
|
6518
|
+
// ledger producer committed it. The old model-only finalize session was
|
|
6519
|
+
// the only writer of these fields; runtime-owned finalization must not
|
|
6520
|
+
// silently erase them when they are already available.
|
|
6521
|
+
const optionalPlanFields = {};
|
|
6522
|
+
for (const record of input.committedFacts?.() ?? []) {
|
|
6523
|
+
const fact = committedFactFromPlanRecord(record);
|
|
6524
|
+
if (!fact)
|
|
6525
|
+
continue;
|
|
6526
|
+
if (Array.isArray(fact.residualRisks)) {
|
|
6527
|
+
optionalPlanFields.residualRisks = fact.residualRisks.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
6528
|
+
}
|
|
6529
|
+
if (typeof fact.realIntegrationGap === "string" && fact.realIntegrationGap.trim()) {
|
|
6530
|
+
optionalPlanFields.realIntegrationGap = fact.realIntegrationGap;
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
6533
|
+
let finalizeResult = await input.finalizePlan(optionalPlanFields);
|
|
6534
|
+
const finalizeDetails = finalizeResult && typeof finalizeResult === "object"
|
|
6535
|
+
? (finalizeResult.details ?? finalizeResult)
|
|
6536
|
+
: finalizeResult;
|
|
6537
|
+
if (!finalizeDetails ||
|
|
6538
|
+
typeof finalizeDetails !== "object" ||
|
|
6539
|
+
finalizeDetails.ok !== true) {
|
|
6540
|
+
// Normal plans never open a finalize model session. Keep the old
|
|
6541
|
+
// correction/recovery semantic only for a rejected deterministic
|
|
6542
|
+
// compile: give the planner one bounded repair pass, then retry the
|
|
6543
|
+
// same runtime authority. The correction prompt explicitly forbids
|
|
6544
|
+
// calling finalize_plan, so terminal ownership remains deterministic.
|
|
6545
|
+
const correctionPrompt = [
|
|
6546
|
+
input.basePrompt,
|
|
6547
|
+
"PLAN FINALIZE CORRECTION — the runtime compile rejected the committed ledger.",
|
|
6548
|
+
`Runtime error: ${String(finalizeDetails?.error ?? "plan finalize rejected")}`,
|
|
6549
|
+
"Repair only the reported facts with the typed record_* tools. Do not call finalize_plan; the runtime will retry it after this correction.",
|
|
6550
|
+
input.committedFacts ? compactFrontendPlanLedgerContext({ committedFacts: input.committedFacts(), requirementIds: input.requirementIds ?? [], kinds: ["plan-requirement", "plan-verification-target", "state-registry", "component-choice", "state-flow", "data-flow", "mock-api", "design-deviation", "dependency", "target-surface"] }) : "",
|
|
6551
|
+
].filter(Boolean).join("\n\n");
|
|
6552
|
+
input.setActiveRequirementScope?.([]);
|
|
6553
|
+
const correctionTools = input.segmentCustomTools(null);
|
|
6554
|
+
const correction = await input.piStepFn({
|
|
6555
|
+
...input.sessionOptions,
|
|
6556
|
+
prompt: correctionPrompt,
|
|
6557
|
+
writerToolPolicy: { requireSdk: true, customTools: correctionTools },
|
|
6558
|
+
});
|
|
6559
|
+
try {
|
|
6560
|
+
await input.flushLedger();
|
|
6561
|
+
}
|
|
6562
|
+
catch { /* node-level flush retries below */ }
|
|
6563
|
+
accumulated = accumulated
|
|
6564
|
+
? combineSequentialPiResults(accumulated, correction)
|
|
6565
|
+
: correction;
|
|
6566
|
+
if (correction.ok) {
|
|
6567
|
+
finalizeResult = await input.finalizePlan(optionalPlanFields);
|
|
6568
|
+
}
|
|
6569
|
+
const retriedDetails = finalizeResult && typeof finalizeResult === "object"
|
|
6570
|
+
? (finalizeResult.details ?? finalizeResult)
|
|
6571
|
+
: finalizeResult;
|
|
6572
|
+
if (retriedDetails && typeof retriedDetails === "object" && retriedDetails.ok === true) {
|
|
6573
|
+
return mapPlannerExhaustion(accumulated ?? { ok: true, stdout: "", stderr: "", assistantText: "", command: [], durationMs: 0, exitCode: 0, failureCategory: "success", modelDisplay: "runtime-finalize", parsedEvents: 0, timedOut: false, attemptedModels: [], fallbackUsed: false, tokensUsed: 0 }, true);
|
|
6574
|
+
}
|
|
6575
|
+
const detail = retriedDetails && typeof retriedDetails === "object"
|
|
6576
|
+
? String(retriedDetails.error ?? "plan finalize rejected")
|
|
6577
|
+
: "plan finalize rejected";
|
|
6578
|
+
return {
|
|
6579
|
+
...(accumulated ?? {
|
|
6580
|
+
ok: false,
|
|
6581
|
+
stdout: "",
|
|
6582
|
+
stderr: "",
|
|
6583
|
+
assistantText: "",
|
|
6584
|
+
command: [],
|
|
6585
|
+
durationMs: 0,
|
|
6586
|
+
exitCode: null,
|
|
6587
|
+
failureCategory: "invalid-output",
|
|
6588
|
+
modelDisplay: "unknown",
|
|
6589
|
+
parsedEvents: 0,
|
|
6590
|
+
timedOut: false,
|
|
6591
|
+
attemptedModels: [],
|
|
6592
|
+
fallbackUsed: false,
|
|
6593
|
+
tokensUsed: 0,
|
|
6594
|
+
}),
|
|
6595
|
+
ok: false,
|
|
6596
|
+
stderr: `${accumulated?.stderr ?? ""}\nfrontend plan deterministic finalize failed: ${detail}`.trim(),
|
|
6597
|
+
failureCategory: "invalid-output",
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6600
|
+
}
|
|
5332
6601
|
return mapPlannerExhaustion(accumulated ?? {
|
|
5333
6602
|
ok: false,
|
|
5334
6603
|
stdout: "",
|
|
@@ -5622,6 +6891,10 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5622
6891
|
let reviewInventory;
|
|
5623
6892
|
let designInventory;
|
|
5624
6893
|
let planLedgerTools;
|
|
6894
|
+
let decisionPlanTools;
|
|
6895
|
+
let parentDecisionSnapshot;
|
|
6896
|
+
let decisionPlanAuthority;
|
|
6897
|
+
const useDecisionPlan = process.env.HARNESS_FRONTEND_PLAN_DECISION === "1";
|
|
5625
6898
|
let contractTools;
|
|
5626
6899
|
let scoutEvidenceTools;
|
|
5627
6900
|
let scoutSourceDeclaredPaths;
|
|
@@ -5721,9 +6994,23 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5721
6994
|
runDir: meta.runDir,
|
|
5722
6995
|
nodeId: input.task.id,
|
|
5723
6996
|
});
|
|
6997
|
+
// Pre-session protocol self-check: an absent index silently disables
|
|
6998
|
+
// the scope-checkpoint protocol (inventory-less terminals approve
|
|
6999
|
+
// vacuously), so a broken upstream must fail here, before any model
|
|
7000
|
+
// call, instead of degrading the review.
|
|
7001
|
+
if (!reviewInventory) {
|
|
7002
|
+
return {
|
|
7003
|
+
ok: false,
|
|
7004
|
+
stdout: "",
|
|
7005
|
+
stderr: "pi review terminal tool policy unavailable before Pi execution: contracts/frontend-implementation-contract.index.json missing; the review-context gate must materialize the canonical contract index before this node starts",
|
|
7006
|
+
failureCategory: "tool-policy",
|
|
7007
|
+
durationMs: Date.now() - started,
|
|
7008
|
+
};
|
|
7009
|
+
}
|
|
5724
7010
|
writerToolPolicy = {
|
|
5725
7011
|
requireSdk: true,
|
|
5726
7012
|
customTools: reviewTerminalTools.customTools,
|
|
7013
|
+
requireAllowlistedCustomTools: true,
|
|
5727
7014
|
};
|
|
5728
7015
|
}
|
|
5729
7016
|
catch (error) {
|
|
@@ -5748,9 +7035,22 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5748
7035
|
runDir: meta.runDir,
|
|
5749
7036
|
nodeId: input.task.id,
|
|
5750
7037
|
});
|
|
7038
|
+
// Same pre-session self-check as the review terminal above: without
|
|
7039
|
+
// the index the scope checkpoints disappear and the design verdict
|
|
7040
|
+
// loses its binding to the reviewed slices.
|
|
7041
|
+
if (!designInventory) {
|
|
7042
|
+
return {
|
|
7043
|
+
ok: false,
|
|
7044
|
+
stdout: "",
|
|
7045
|
+
stderr: "pi design terminal tool policy unavailable before Pi execution: contracts/frontend-implementation-contract.index.json missing; the design-policy shell must materialize the canonical contract index before this node starts",
|
|
7046
|
+
failureCategory: "tool-policy",
|
|
7047
|
+
durationMs: Date.now() - started,
|
|
7048
|
+
};
|
|
7049
|
+
}
|
|
5751
7050
|
writerToolPolicy = {
|
|
5752
7051
|
requireSdk: true,
|
|
5753
7052
|
customTools: designTerminalTools.customTools,
|
|
7053
|
+
requireAllowlistedCustomTools: true,
|
|
5754
7054
|
};
|
|
5755
7055
|
}
|
|
5756
7056
|
catch (error) {
|
|
@@ -5781,6 +7081,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5781
7081
|
writerToolPolicy = {
|
|
5782
7082
|
requireSdk: true,
|
|
5783
7083
|
customTools: contractTools.customTools,
|
|
7084
|
+
requireAllowlistedCustomTools: true,
|
|
5784
7085
|
};
|
|
5785
7086
|
}
|
|
5786
7087
|
catch (error) {
|
|
@@ -5814,6 +7115,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5814
7115
|
writerToolPolicy = {
|
|
5815
7116
|
requireSdk: true,
|
|
5816
7117
|
customTools: scoutEvidenceTools.customTools,
|
|
7118
|
+
requireAllowlistedCustomTools: true,
|
|
5817
7119
|
};
|
|
5818
7120
|
}
|
|
5819
7121
|
catch (error) {
|
|
@@ -5830,30 +7132,66 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5830
7132
|
try {
|
|
5831
7133
|
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
5832
7134
|
const store = createTypedEventStore();
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
runDir: meta.runDir,
|
|
5837
|
-
nodeId: input.task.id,
|
|
5838
|
-
skeleton: input.task.structuredContractOutput?.skeleton,
|
|
5839
|
-
sourceBinding: meta.spec.sourceBinding,
|
|
5840
|
-
writeSetPatterns: input.task.writeSet,
|
|
5841
|
-
componentNewSourceReferences: await resolveFrontendPlanNewComponentSourceReferences({
|
|
5842
|
-
cwd: input.cwd,
|
|
7135
|
+
if (useDecisionPlan) {
|
|
7136
|
+
const authority = await resolveFrontendDecisionAuthority({
|
|
7137
|
+
runDir: meta.runDir,
|
|
5843
7138
|
sourceBinding: meta.spec.sourceBinding,
|
|
5844
|
-
|
|
5845
|
-
|
|
7139
|
+
task: input.task,
|
|
7140
|
+
});
|
|
7141
|
+
if (!authority)
|
|
7142
|
+
throw new Error("frontend decision authority unavailable (missing contract inheritance or source binding)");
|
|
7143
|
+
decisionPlanAuthority = authority;
|
|
7144
|
+
if (useDecisionPlan) {
|
|
7145
|
+
// Recovery child: read the parent snapshot before the tools so
|
|
7146
|
+
// the replay and the identical-plan finalize guard share it.
|
|
7147
|
+
parentDecisionSnapshot = await readParentDecisionSnapshot(meta.runDir, input.task.id);
|
|
7148
|
+
}
|
|
7149
|
+
decisionPlanTools = await createFrontendPlanDecisionTools({
|
|
7150
|
+
attemptId: `${meta.runId}:${input.task.id}`,
|
|
7151
|
+
store,
|
|
5846
7152
|
runDir: meta.runDir,
|
|
5847
|
-
|
|
5848
|
-
|
|
7153
|
+
nodeId: input.task.id,
|
|
7154
|
+
authority,
|
|
7155
|
+
...(useDecisionPlan && parentDecisionSnapshot
|
|
7156
|
+
? { parentDecisionSnapshot }
|
|
7157
|
+
: {}),
|
|
7158
|
+
componentSpecCandidatePaths: meta.spec.tasks.find((task) => task.id === "frontend-design-policy-shell")?.shell?.frontendDesignPolicy?.componentSpecCandidatePaths ?? [],
|
|
7159
|
+
skeleton: input.task.structuredContractOutput?.skeleton,
|
|
7160
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
7161
|
+
});
|
|
7162
|
+
writerToolPolicy = {
|
|
7163
|
+
requireSdk: true,
|
|
7164
|
+
customTools: decisionPlanTools.customTools,
|
|
7165
|
+
requireAllowlistedCustomTools: true,
|
|
7166
|
+
};
|
|
7167
|
+
}
|
|
7168
|
+
else {
|
|
7169
|
+
planLedgerTools = await createFrontendPlanLedgerTools({
|
|
7170
|
+
attemptId: `${meta.runId}:${input.task.id}`,
|
|
7171
|
+
store,
|
|
5849
7172
|
runDir: meta.runDir,
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
7173
|
+
nodeId: input.task.id,
|
|
7174
|
+
skeleton: input.task.structuredContractOutput?.skeleton,
|
|
7175
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
7176
|
+
writeSetPatterns: input.task.writeSet,
|
|
7177
|
+
componentNewSourceReferences: await resolveFrontendPlanNewComponentSourceReferences({
|
|
7178
|
+
cwd: input.cwd,
|
|
7179
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
7180
|
+
}),
|
|
7181
|
+
declaredUiStateIds: await resolveFrontendDeclaredUiStateIds({
|
|
7182
|
+
runDir: meta.runDir,
|
|
7183
|
+
}),
|
|
7184
|
+
canonicalVerificationTargetIds: await resolveFrontendCanonicalVerificationTargetIds({
|
|
7185
|
+
runDir: meta.runDir,
|
|
7186
|
+
}),
|
|
7187
|
+
workspaceRoot: input.cwd,
|
|
7188
|
+
});
|
|
7189
|
+
writerToolPolicy = {
|
|
7190
|
+
requireSdk: true,
|
|
7191
|
+
customTools: planLedgerTools.customTools,
|
|
7192
|
+
requireAllowlistedCustomTools: true,
|
|
7193
|
+
};
|
|
7194
|
+
}
|
|
5857
7195
|
}
|
|
5858
7196
|
catch (error) {
|
|
5859
7197
|
return {
|
|
@@ -5872,6 +7210,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5872
7210
|
budget: input.task.readBudget,
|
|
5873
7211
|
});
|
|
5874
7212
|
writerToolPolicy = {
|
|
7213
|
+
...(writerToolPolicy ?? {}),
|
|
5875
7214
|
requireSdk: true,
|
|
5876
7215
|
customTools: [
|
|
5877
7216
|
...(writerToolPolicy?.customTools ?? []),
|
|
@@ -6016,6 +7355,136 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6016
7355
|
},
|
|
6017
7356
|
});
|
|
6018
7357
|
}
|
|
7358
|
+
else if (isFrontendPlanLedgerNode(input.task) && decisionPlanTools) {
|
|
7359
|
+
// Experimental decision path: one session records semantic decisions;
|
|
7360
|
+
// the runtime finalizes and derives the safety projection afterwards.
|
|
7361
|
+
// Use a decision-specific prompt: the relationship subtask_prompt names
|
|
7362
|
+
// tools this toolset does not expose and never states behavior-group ids,
|
|
7363
|
+
// so reusing it leaves every requirement unplaced.
|
|
7364
|
+
// D (local design repair): a recovery child replays the parent run's
|
|
7365
|
+
// committed decision facts through its own durable tools BEFORE the model
|
|
7366
|
+
// session starts. The model then only corrects the rows the design review
|
|
7367
|
+
// flagged (same identity + replace:true) instead of re-deriving the plan.
|
|
7368
|
+
if (parentDecisionSnapshot && parentDecisionSnapshot.facts.length > 0) {
|
|
7369
|
+
try {
|
|
7370
|
+
await replayParentDecisionSnapshot(parentDecisionSnapshot, decisionPlanTools.customTools);
|
|
7371
|
+
}
|
|
7372
|
+
catch (error) {
|
|
7373
|
+
return {
|
|
7374
|
+
ok: false,
|
|
7375
|
+
stdout: "",
|
|
7376
|
+
stderr: `parent decision replay failed before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
7377
|
+
failureCategory: "invalid-output",
|
|
7378
|
+
durationMs: Date.now() - started,
|
|
7379
|
+
};
|
|
7380
|
+
}
|
|
7381
|
+
}
|
|
7382
|
+
const decisionPrompt = decisionPlanAuthority
|
|
7383
|
+
? (() => {
|
|
7384
|
+
const base = buildFrontendDecisionPlanPrompt(decisionPlanAuthority);
|
|
7385
|
+
// Preserve node-execution's retry guidance (appended to
|
|
7386
|
+
// input.prompt as a <retry_instruction> tail) so a failed
|
|
7387
|
+
// decision attempt tells the next session exactly which
|
|
7388
|
+
// decision facts to repair instead of replaying the identical
|
|
7389
|
+
// under-declared plan. The relationship body of input.prompt is
|
|
7390
|
+
// deliberately dropped (its tools do not exist in this toolset).
|
|
7391
|
+
if ((input.attempt ?? 1) > 1) {
|
|
7392
|
+
const marker = input.prompt.indexOf("<retry_instruction>");
|
|
7393
|
+
if (marker >= 0) {
|
|
7394
|
+
const tail = input.prompt.slice(marker).trim();
|
|
7395
|
+
if (tail.length > 0) {
|
|
7396
|
+
return `${base}\n\n${tail}`;
|
|
7397
|
+
}
|
|
7398
|
+
}
|
|
7399
|
+
}
|
|
7400
|
+
if (parentDecisionSnapshot && parentDecisionSnapshot.facts.length > 0) {
|
|
7401
|
+
const identityOf = (entry) => String(entry.purpose ??
|
|
7402
|
+
entry.state ??
|
|
7403
|
+
entry.interaction ??
|
|
7404
|
+
entry.id ??
|
|
7405
|
+
entry.boundary ??
|
|
7406
|
+
"?");
|
|
7407
|
+
const ledger = parentDecisionSnapshot.facts
|
|
7408
|
+
.map((fact) => `- ${fact.kind}: ${identityOf(fact.entry)}`)
|
|
7409
|
+
.join("\n");
|
|
7410
|
+
return `${base}\n\n<parent_decision_repair>\nYour ledger is pre-loaded with the parent run's committed decision facts (replayed by the runtime before this session):\n${ledger}\nThe <rerun_feedback> findings name the rows that blocked admission. Correct ONLY those rows: resubmit the corrected entry with the SAME natural identity and replace:true. Do not restate unrelated rows and do not add rows the findings do not require. Then call finalize_decision.\n</parent_decision_repair>`;
|
|
7411
|
+
}
|
|
7412
|
+
return base;
|
|
7413
|
+
})()
|
|
7414
|
+
: input.prompt;
|
|
7415
|
+
const decisionResult = await piStepFn({
|
|
7416
|
+
...piSessionOptions,
|
|
7417
|
+
prompt: decisionPrompt,
|
|
7418
|
+
writerToolPolicy: { requireSdk: true, customTools: decisionPlanTools.customTools },
|
|
7419
|
+
});
|
|
7420
|
+
try {
|
|
7421
|
+
await decisionPlanTools.flush();
|
|
7422
|
+
}
|
|
7423
|
+
catch { /* node-level flush retries below */ }
|
|
7424
|
+
const finalizeResult = await decisionPlanTools.finalizeDecision();
|
|
7425
|
+
const finalizeDetails = finalizeResult && typeof finalizeResult === "object"
|
|
7426
|
+
? (finalizeResult.details ?? finalizeResult)
|
|
7427
|
+
: finalizeResult;
|
|
7428
|
+
if (finalizeDetails && typeof finalizeDetails === "object" && finalizeDetails.ok === true) {
|
|
7429
|
+
// Bridge the finalized decisions into the relationship-shaped plan
|
|
7430
|
+
// ledger. The node-level R1 self-check and all downstream shells
|
|
7431
|
+
// compile ONLY plan-typed-facts.jsonl; without this write the whole
|
|
7432
|
+
// decision run fails "frontend plan ledger missing" at R1 on every
|
|
7433
|
+
// attempt. Validation failures surface as invalid-output so the
|
|
7434
|
+
// retry ladder restarts the session with the diagnostics.
|
|
7435
|
+
try {
|
|
7436
|
+
const bridge = await bridgeFrontendPlanDecisionToRelationshipLedger({
|
|
7437
|
+
runDir: meta.runDir,
|
|
7438
|
+
nodeId: input.task.id,
|
|
7439
|
+
attemptId: `${meta.runId}:${input.task.id}`,
|
|
7440
|
+
facts: decisionPlanTools.committedFacts(),
|
|
7441
|
+
authority: decisionPlanAuthority,
|
|
7442
|
+
skeleton: input.task.structuredContractOutput?.skeleton,
|
|
7443
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
7444
|
+
});
|
|
7445
|
+
if (!bridge.ok) {
|
|
7446
|
+
throw new Error(bridge.error);
|
|
7447
|
+
}
|
|
7448
|
+
result = decisionResult;
|
|
7449
|
+
}
|
|
7450
|
+
catch (error) {
|
|
7451
|
+
result = {
|
|
7452
|
+
ok: false,
|
|
7453
|
+
stdout: decisionResult.stdout ?? "",
|
|
7454
|
+
stderr: `${decisionResult.stderr ?? ""}\n${error instanceof Error ? error.message : String(error)}`.trim(),
|
|
7455
|
+
failureCategory: "invalid-output",
|
|
7456
|
+
durationMs: Date.now() - started,
|
|
7457
|
+
modelDisplay: decisionResult.modelDisplay,
|
|
7458
|
+
parsedEvents: decisionResult.parsedEvents,
|
|
7459
|
+
timedOut: decisionResult.timedOut,
|
|
7460
|
+
attemptedModels: decisionResult.attemptedModels,
|
|
7461
|
+
fallbackUsed: decisionResult.fallbackUsed,
|
|
7462
|
+
tokensUsed: decisionResult.tokensUsed,
|
|
7463
|
+
assistantText: decisionResult.assistantText ?? "",
|
|
7464
|
+
command: decisionResult.command ?? [],
|
|
7465
|
+
exitCode: decisionResult.exitCode,
|
|
7466
|
+
};
|
|
7467
|
+
}
|
|
7468
|
+
}
|
|
7469
|
+
else {
|
|
7470
|
+
result = {
|
|
7471
|
+
ok: false,
|
|
7472
|
+
stdout: decisionResult.stdout ?? "",
|
|
7473
|
+
stderr: `${decisionResult.stderr ?? ""}\nfrontend decision finalize failed: ${String(finalizeDetails?.error ?? "decision finalize rejected")}`.trim(),
|
|
7474
|
+
failureCategory: "invalid-output",
|
|
7475
|
+
durationMs: Date.now() - started,
|
|
7476
|
+
modelDisplay: decisionResult.modelDisplay,
|
|
7477
|
+
parsedEvents: decisionResult.parsedEvents,
|
|
7478
|
+
timedOut: decisionResult.timedOut,
|
|
7479
|
+
attemptedModels: decisionResult.attemptedModels,
|
|
7480
|
+
fallbackUsed: decisionResult.fallbackUsed,
|
|
7481
|
+
tokensUsed: decisionResult.tokensUsed,
|
|
7482
|
+
assistantText: decisionResult.assistantText ?? "",
|
|
7483
|
+
command: decisionResult.command ?? [],
|
|
7484
|
+
exitCode: decisionResult.exitCode,
|
|
7485
|
+
};
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
6019
7488
|
else if (isFrontendPlanLedgerNode(input.task) && planLedgerTools) {
|
|
6020
7489
|
// Frontend-only split: independent coverage map sessions feed a single
|
|
6021
7490
|
// reducer (UX decisions -> global policy -> finalize), mirroring the
|
|
@@ -6025,7 +7494,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6025
7494
|
const planRequirementCosts = new Map();
|
|
6026
7495
|
const behaviorRequiredRequirementIds = [];
|
|
6027
7496
|
try {
|
|
6028
|
-
const { readCommittedOriginFacts } = await import("../workflows/dag/frontend-
|
|
7497
|
+
const { readCommittedOriginFacts } = await import("../workflows/dag/frontend-committed-facts.js");
|
|
6029
7498
|
const contractFacts = await readCommittedOriginFacts(meta.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
|
|
6030
7499
|
// Only requirement facts: the contract ledger also carries
|
|
6031
7500
|
// constraints (CON-*), evidence expectations (EV-*), handoff
|
|
@@ -6033,11 +7502,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6033
7502
|
// (SPLIT-*) that all have ids — feeding those into the coverage
|
|
6034
7503
|
// batches made the model record non-frozen plan-requirement ids
|
|
6035
7504
|
// that finalize's canonical-coverage gate then rejected (r-ext2).
|
|
6036
|
-
planRequirementIds = contractFacts
|
|
6037
|
-
.filter((record) => record.fact
|
|
6038
|
-
?.kind === "requirement")
|
|
6039
|
-
.map((record) => record.fact?.id)
|
|
6040
|
-
.filter((id) => typeof id === "string");
|
|
7505
|
+
planRequirementIds = collectFrontendPlanRequirementIds(contractFacts);
|
|
6041
7506
|
for (const fact of resolveFrontendContractRequirements(contractFacts.map((record) => record.fact))) {
|
|
6042
7507
|
if (fact.evidence.behavior === "required")
|
|
6043
7508
|
behaviorRequiredRequirementIds.push(fact.id);
|
|
@@ -6078,6 +7543,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6078
7543
|
: {}),
|
|
6079
7544
|
committedRequirementIds: () => options.ledgerTools.committedRequirementIds(),
|
|
6080
7545
|
committedFacts: () => options.ledgerTools.committedFacts(),
|
|
7546
|
+
finalizePlan: (params) => options.ledgerTools.finalizePlan(params),
|
|
6081
7547
|
behaviorRequiredRequirementIds,
|
|
6082
7548
|
setActiveRequirementScope: (requirementIds) => options.ledgerTools.setActiveRequirementScope(requirementIds),
|
|
6083
7549
|
segmentCustomTools: (toolNames) => toolNames === null
|
|
@@ -6088,12 +7554,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6088
7554
|
flushLedger: () => options.ledgerTools.flush(),
|
|
6089
7555
|
});
|
|
6090
7556
|
if (planRequirementIds.length > 1) {
|
|
6091
|
-
const coverageTools = FRONTEND_PLAN_SEGMENTS.find(segment => segment.id === "coverage").toolNames;
|
|
6092
7557
|
const workload = buildFrontendPlanWorkload({
|
|
6093
7558
|
basePrompt: input.prompt, requirementIds: planRequirementIds,
|
|
6094
7559
|
requirementCosts: planRequirementCosts, sessionOptions: piSessionOptions,
|
|
6095
|
-
allTools: planLedgerTools.customTools,
|
|
6096
|
-
coverageTools: planLedgerTools.customTools.filter(tool => isRecordObject(tool) && coverageTools.has(String(tool.name))),
|
|
6097
7560
|
});
|
|
6098
7561
|
let coverageBatches;
|
|
6099
7562
|
try {
|
|
@@ -6383,12 +7846,12 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6383
7846
|
if (readBudgetIssues.length > 0) {
|
|
6384
7847
|
const hasTypedFrontendTerminal = isFrontendReviewTypedTerminalNode(input.task) ||
|
|
6385
7848
|
isFrontendDesignTypedTerminalNode(input.task);
|
|
6386
|
-
// A stale/generated DAG may still carry
|
|
7849
|
+
// A stale/generated DAG may still carry an older read budget. Once a
|
|
6387
7850
|
// design/review node has a successful typed terminal, telemetry is
|
|
6388
|
-
// diagnostic only; the terminal
|
|
7851
|
+
// diagnostic only; the typed terminal validation below still fails closed when the
|
|
6389
7852
|
// fact is missing or conflicting.
|
|
6390
7853
|
if (mapped.ok && hasTypedFrontendTerminal) {
|
|
6391
|
-
// Continue to the typed terminal
|
|
7854
|
+
// Continue to the typed terminal validation below.
|
|
6392
7855
|
}
|
|
6393
7856
|
else {
|
|
6394
7857
|
// Read-budget telemetry is diagnostic only when the provider/executor
|
|
@@ -6414,7 +7877,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6414
7877
|
}
|
|
6415
7878
|
}
|
|
6416
7879
|
if (isFrontendReviewTypedTerminalNode(input.task)) {
|
|
6417
|
-
return await
|
|
7880
|
+
return await validateFrontendReviewTerminal({
|
|
6418
7881
|
task: input.task,
|
|
6419
7882
|
meta,
|
|
6420
7883
|
mapped,
|
|
@@ -6422,7 +7885,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6422
7885
|
});
|
|
6423
7886
|
}
|
|
6424
7887
|
if (isFrontendDesignTypedTerminalNode(input.task)) {
|
|
6425
|
-
return await
|
|
7888
|
+
return await validateFrontendDesignTerminal({
|
|
6426
7889
|
task: input.task,
|
|
6427
7890
|
meta,
|
|
6428
7891
|
mapped,
|
|
@@ -6445,7 +7908,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6445
7908
|
return { ...mapped, ok: false, failureCategory: mapped.ok ? "frontend-ledger-invalid" : mapped.failureCategory, stderr: `${mapped.stderr}\nFRONTEND_LEDGER_INTEGRITY_INVALID: ${error instanceof Error ? error.message : String(error)}` };
|
|
6446
7909
|
}
|
|
6447
7910
|
if (mapped.ok) {
|
|
6448
|
-
const { checkCommittedOriginFacts, readCommittedOriginFacts } = await import("../workflows/dag/frontend-
|
|
7911
|
+
const { checkCommittedOriginFacts, readCommittedOriginFacts } = await import("../workflows/dag/frontend-committed-facts.js");
|
|
6449
7912
|
const scoutFacts = await readCommittedOriginFacts(meta.runDir, input.task.id, "scout-typed-facts.jsonl");
|
|
6450
7913
|
const completeness = checkCommittedOriginFacts({
|
|
6451
7914
|
records: scoutFacts,
|
|
@@ -6677,15 +8140,15 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6677
8140
|
let factDerivedStatus;
|
|
6678
8141
|
if (mapped.ok && input.task.writerOutcomePolicy) {
|
|
6679
8142
|
if (isFrontendFactsWriter(input.task)) {
|
|
6680
|
-
// AC-001/AC-003: facts-derived status. The
|
|
6681
|
-
//
|
|
8143
|
+
// AC-001/AC-003: facts-derived status. The response text is never
|
|
8144
|
+
// consulted for frontend writer completion.
|
|
6682
8145
|
try {
|
|
6683
8146
|
const [writerStatus, contractModule, traceModule] = await Promise.all([
|
|
6684
8147
|
import("../workflows/dag/frontend-writer-status.js"),
|
|
6685
8148
|
import("../workflows/dag/frontend-implementation-contract.js"),
|
|
6686
8149
|
import("../workflows/dag/frontend-verification-trace.js"),
|
|
6687
8150
|
]);
|
|
6688
|
-
const { collectFrontendWriterFacts, deriveFrontendWriterStatus, computeFailureFingerprint,
|
|
8151
|
+
const { collectFrontendWriterFacts, deriveFrontendWriterStatus, computeFailureFingerprint, } = writerStatus;
|
|
6689
8152
|
const contractPath = path.join(meta.runDir, "contracts", "frontend-implementation-contract.json");
|
|
6690
8153
|
const contractRaw = JSON.parse(await readFile(contractPath, "utf8"));
|
|
6691
8154
|
const parsedContract = contractModule.frontendImplementationContractSchema.safeParse(contractRaw);
|
|
@@ -6716,19 +8179,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6716
8179
|
wallTimeMs: Date.now() - started,
|
|
6717
8180
|
rounds: 1,
|
|
6718
8181
|
writeAttempts: attempt,
|
|
6719
|
-
firstLineText: mapped.assistantText,
|
|
6720
8182
|
});
|
|
6721
8183
|
const derived = deriveFrontendWriterStatus(facts);
|
|
6722
8184
|
factDerivedStatus = derived.status;
|
|
6723
|
-
const legacyValidation = validateWriterImplementationOutcome(mapped.assistantText || mapped.stdout, changeManifestChangedFiles ?? [], {
|
|
6724
|
-
requireChangedFiles: false,
|
|
6725
|
-
allowMissingChangedOutcomeWhenDiffPresent: false,
|
|
6726
|
-
});
|
|
6727
|
-
const legacyOutcome = legacyValidation.ok
|
|
6728
|
-
? legacyValidation.outcome
|
|
6729
|
-
: legacyValidation.reason.includes("blocked")
|
|
6730
|
-
? "blocked"
|
|
6731
|
-
: "missing";
|
|
6732
8185
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "fact-implementation-status.json", {
|
|
6733
8186
|
schemaVersion: 1,
|
|
6734
8187
|
nodeId: input.task.id,
|
|
@@ -6745,7 +8198,6 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
6745
8198
|
rounds: facts.rounds,
|
|
6746
8199
|
writeAttempts: facts.writeAttempts,
|
|
6747
8200
|
failureFingerprint: computeFailureFingerprint(facts),
|
|
6748
|
-
shadow: compareFactStatusToLegacyOutcome(derived.status, legacyOutcome),
|
|
6749
8201
|
});
|
|
6750
8202
|
if (derived.status !== "changed" &&
|
|
6751
8203
|
derived.status !== "already-satisfied") {
|