@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +349 -97
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +23 -5
- package/dist/cli/program.js +21 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/cli.js +2 -1
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +455 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +2370 -64
- package/dist/executors/pi-executor.js +11 -2
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +74 -28
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/pi-sdk-executor.js +225 -33
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +1258 -224
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/shared/dag-failure-category.js +150 -0
- package/dist/shared/dag-prompt-override.js +27 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +39 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +70 -15
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +50 -5
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +18 -1
- package/dist/task/contract/schema.js +4 -1
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/frontend-project-capability.js +199 -18
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +477 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +298 -1
- package/dist/task/source-prepare/semantic-intake.js +25 -5
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/task/source-references.js +22 -1
- package/dist/worker/cli.js +18 -2
- package/dist/worker/console/chat/chat-event-store.js +75 -5
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/operation-card.js +8 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +395 -59
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/inspect-split.js +18 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/operation-store.js +227 -7
- package/dist/worker/console/operator-actions.js +388 -0
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +355 -3
- package/dist/worker/console/security.js +67 -13
- package/dist/worker/console/server.js +609 -164
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-CXj_GnSb.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BZp6JAp7.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-DfpcEuYU.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-_Gf0xadb.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-Ct2QPCmv.js} +1 -1
- package/dist/worker/console/static/assets/channel-3TxJgYaH.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-BfklkzKl.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-Dy25vZJV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-BFlCRZep.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-op3oVxIE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-C0R7rzF2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-Bt44TCGy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-_uEE_XFx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-C3TOYGZ9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-Cz60oBPV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-8Bik0qis.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-C2QIOA4a.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-CSH_yUUd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-BDLw2XFM.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-CcD9ZtF4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-DqwtkBsI.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BWVEcqsC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-M-WAbtQi.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-DKW7SixP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-PHHPPtrj.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-BQD-B4RX.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-BFvULb51.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-Bw-aXTCb.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BGKw04Qx.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-RqKaPR-I.js} +1 -1
- package/dist/worker/console/static/assets/index-B_D8rbWc.js +407 -0
- package/dist/worker/console/static/assets/index-BdNx6fj0.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-YO5dnzrJ.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-Bszls1DD.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-PhzeaZ09.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-BsjbDoXi.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-0B7NnWKk.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BJr4Fj-q.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-moC4fpGB.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-BEw37-2c.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-Cq6LyasU.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-DUCMcK0D.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-C3upTZm7.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-BI_gMsCW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-YFOIRzfN.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-dOnB7UDD.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-BXUniaIh.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-2tA4wTNu.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D-RJBbb0.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-DO0HkJXC.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DvIOzixv.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-DXDZ0cTj.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-B32Ark0D.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/app/console-types.js +68 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
- package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +183 -48
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
- package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +344 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/materialize/harness-task-lineage.js +5 -2
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observability/read-model.js +22 -0
- package/dist/worker/observe/node-input.js +160 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +224 -3
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +90 -4
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +208 -0
- package/dist/worker/observe/static/format.js +60 -0
- package/dist/worker/observe/static/index.html +6 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -96
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +184 -1
- package/dist/worker/observe/static/styles.css +395 -0
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +14 -5
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/worker/observe/static/views/task.js +56 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
- package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +2 -1
- package/dist/workflows/dag/failure-category.js +7 -116
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +538 -166
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +388 -392
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +288 -14
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +427 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
- package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
- package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
- package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
- package/dist/workflows/dag/frontend-test-html-report.js +3 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
- package/dist/workflows/dag/frontend-test-layout.js +159 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +272 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +1072 -660
- package/dist/workflows/dag/interrupt-request.js +10 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +552 -58
- package/dist/workflows/dag/prompt.js +44 -19
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +124 -1
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +81 -6
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +318 -8
- package/dist/workflows/dag/runner.js +429 -122
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +338 -31
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +92 -32
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +9 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/README.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +63 -1
- package/docs/templates/backend-test-dag.json +265 -237
- package/docs/templates/frontend-implementation-contract.schema.json +684 -24
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +1 -1
- package/package.json +7 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-bounded-implement/SKILL.md +8 -14
- package/skills/frontend-design-review/SKILL.md +20 -41
- package/skills/frontend-implementation/SKILL.md +3 -4
- package/skills/frontend-implementation/references/design-spec.md +15 -10
- package/skills/frontend-implementation/references/node-contracts.md +23 -13
- package/skills/frontend-review/SKILL.md +22 -14
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/loop-agent/references/command-reference.md +12 -4
- package/skills/loop-agent/references/hybrid-dag.md +8 -1
- package/skills/playwright-cli/SKILL.md +23 -1
- package/skills/playwright-cli-case-generator/SKILL.md +29 -8
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
|
+
import { OPENSPEC_SPEC_EXT_RE, isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
6
|
import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
|
|
6
7
|
import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
|
|
7
|
-
import {
|
|
8
|
+
import { checkFrontendSourceFidelityLedger, } from "./frontend-source-fidelity-ledger.js";
|
|
9
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
8
10
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
9
11
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
10
12
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
11
13
|
import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
|
|
12
14
|
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
13
15
|
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
16
|
+
/** Canonical contract serialization must stay at or below this byte size. */
|
|
17
|
+
export const FRONTEND_CONTRACT_MAX_BYTES = 65536;
|
|
14
18
|
/**
|
|
15
19
|
* Machine-readable failure classification for blocked/retryable-invalid
|
|
16
20
|
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
@@ -31,13 +35,22 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
|
31
35
|
"verification-target-outside-write-set",
|
|
32
36
|
"openspec-not-read",
|
|
33
37
|
"candidate-missing-drift",
|
|
34
|
-
"
|
|
35
|
-
"openspec-
|
|
36
|
-
"openspec-citation-not-read",
|
|
38
|
+
"candidate-content-drift",
|
|
39
|
+
"openspec-selection-invalid",
|
|
37
40
|
"component-spec-reference-invalid",
|
|
38
41
|
"component-spec-not-cited",
|
|
42
|
+
"component-spec-choice-unbound",
|
|
39
43
|
"component-choices-missing",
|
|
44
|
+
"contract-too-large",
|
|
40
45
|
"revision-patch-unmergeable",
|
|
46
|
+
// Source fidelity ledger 类目(SOURCE_FIDELITY_*/source-* 稳定错误码)。
|
|
47
|
+
"source-fidelity-ledger-missing",
|
|
48
|
+
"source-fidelity-ledger-stale",
|
|
49
|
+
"source-fidelity-unresolved",
|
|
50
|
+
"source-fidelity-conflict",
|
|
51
|
+
"source-fidelity-high-risk-uncovered",
|
|
52
|
+
"source-fidelity-requirement-not-covered",
|
|
53
|
+
"source-fidelity-source-ref-invalid",
|
|
41
54
|
]);
|
|
42
55
|
export const frontendPrewriteResultV1Schema = z
|
|
43
56
|
.object({
|
|
@@ -125,7 +138,7 @@ export const frontendPrewriteResultV1Schema = z
|
|
|
125
138
|
export const frontendWriterAdmissionSchema = z
|
|
126
139
|
.object({
|
|
127
140
|
schemaVersion: z.literal(1),
|
|
128
|
-
writerNodeId: z.enum(["frontend-implement-pi"
|
|
141
|
+
writerNodeId: z.enum(["frontend-implement-pi"]),
|
|
129
142
|
decision: z.enum(["authorized", "denied"]),
|
|
130
143
|
sourceArtifact: z.string().min(1),
|
|
131
144
|
artifactHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
@@ -154,136 +167,68 @@ async function selectNode(runDir, primary, fallbacks) {
|
|
|
154
167
|
}
|
|
155
168
|
throw new Error(`frontend prewrite gate no FINISHED candidate: ${rejections.join("; ")}`);
|
|
156
169
|
}
|
|
157
|
-
async function
|
|
158
|
-
|
|
170
|
+
async function readFinishedNodeRecord(runDir, nodeId) {
|
|
171
|
+
let record;
|
|
172
|
+
try {
|
|
173
|
+
record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
throw new Error(`frontend prewrite gate cannot parse node record ${nodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
177
|
+
}
|
|
159
178
|
if (record.status !== "FINISHED") {
|
|
160
179
|
throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
|
|
161
180
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// canonical plan appear as two complete JSON contracts to the prewrite gate.
|
|
181
|
+
return record;
|
|
182
|
+
}
|
|
183
|
+
async function readNodeText(runDir, nodeId) {
|
|
184
|
+
const record = await readFinishedNodeRecord(runDir, nodeId);
|
|
167
185
|
const text = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
168
186
|
if (!text)
|
|
169
187
|
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
170
188
|
return text;
|
|
171
189
|
}
|
|
172
|
-
async function
|
|
173
|
-
const record =
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
function resolveRunOwnedArtifact(runDir, candidate, label) {
|
|
177
|
-
const root = path.resolve(runDir);
|
|
178
|
-
const resolved = path.resolve(candidate);
|
|
179
|
-
const relative = path.relative(root, resolved);
|
|
180
|
-
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
181
|
-
throw new Error(`frontend prewrite gate ${label} must be contained by the current run directory`);
|
|
190
|
+
async function readCanonicalContractFromNode(runDir, nodeId) {
|
|
191
|
+
const record = await readFinishedNodeRecord(runDir, nodeId);
|
|
192
|
+
if (!record.structuredArtifactPath) {
|
|
193
|
+
throw new Error(`frontend prewrite gate missing structured artifact for ${nodeId}`);
|
|
182
194
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
throw new Error(`frontend prewrite gate
|
|
195
|
+
const artifactPath = path.isAbsolute(record.structuredArtifactPath)
|
|
196
|
+
? record.structuredArtifactPath
|
|
197
|
+
: path.join(runDir, record.structuredArtifactPath);
|
|
198
|
+
const resolvedRun = path.resolve(runDir);
|
|
199
|
+
const resolvedArtifact = path.resolve(artifactPath);
|
|
200
|
+
const relative = path.relative(resolvedRun, resolvedArtifact);
|
|
201
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
202
|
+
throw new Error(`frontend prewrite gate structured artifact escapes runDir: ${record.structuredArtifactPath}`);
|
|
191
203
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
|
|
204
|
+
const bytes = await readFile(resolvedArtifact);
|
|
205
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
206
|
+
if (record.structuredArtifactSha256 &&
|
|
207
|
+
record.structuredArtifactSha256 !== digest) {
|
|
208
|
+
throw new Error(`frontend prewrite gate structured artifact hash mismatch for ${nodeId}`);
|
|
198
209
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (!payloadText)
|
|
203
|
-
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
204
|
-
return {
|
|
205
|
-
artifactFirst: false,
|
|
206
|
-
payloadText,
|
|
207
|
-
citationsText: payloadText,
|
|
208
|
-
rawCandidateSha256: sha256Hex(record.assistantText ?? record.stdout ?? ""),
|
|
209
|
-
payloadKind: "canonical-contract",
|
|
210
|
-
};
|
|
210
|
+
if (record.structuredArtifactSchemaId &&
|
|
211
|
+
record.structuredArtifactSchemaId !== FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID) {
|
|
212
|
+
throw new Error(`frontend prewrite gate structured artifact schema mismatch for ${nodeId}: ${record.structuredArtifactSchemaId}`);
|
|
211
213
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
214
|
+
const contractText = bytes.toString("utf8");
|
|
215
|
+
let citationsText = "";
|
|
216
|
+
try {
|
|
217
|
+
const citationsRaw = await readFile(path.join(path.dirname(resolvedArtifact), "openspec-citations.json"), "utf8");
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(citationsRaw);
|
|
220
|
+
citationsText =
|
|
221
|
+
typeof parsed.block === "string" ? parsed.block : citationsRaw;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
citationsText = citationsRaw;
|
|
225
|
+
}
|
|
223
226
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
artifactPath: record.structuredArtifactPath,
|
|
228
|
-
expectedSha256: record.structuredArtifactSha256,
|
|
229
|
-
label: `${nodeId} primary payload`,
|
|
230
|
-
}),
|
|
231
|
-
readHashBoundArtifact({
|
|
232
|
-
runDir,
|
|
233
|
-
artifactPath: frontend.citationsArtifactPath,
|
|
234
|
-
expectedSha256: frontend.citationsArtifactSha256,
|
|
235
|
-
label: `${nodeId} citations`,
|
|
236
|
-
}),
|
|
237
|
-
readHashBoundArtifact({
|
|
238
|
-
runDir,
|
|
239
|
-
artifactPath: frontend.rawCandidatePath,
|
|
240
|
-
expectedSha256: frontend.rawCandidateSha256,
|
|
241
|
-
label: `${nodeId} raw candidate`,
|
|
242
|
-
}).then(() => undefined),
|
|
243
|
-
]);
|
|
244
|
-
return {
|
|
245
|
-
artifactFirst: true,
|
|
246
|
-
payloadText,
|
|
247
|
-
citationsText,
|
|
248
|
-
rawCandidateSha256: frontend.rawCandidateSha256,
|
|
249
|
-
payloadKind: frontend.payloadKind,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Same shape predicate as the internal `isContract` inside
|
|
254
|
-
* `extractFrontendImplementationJson` (frontend-implementation-contract.ts):
|
|
255
|
-
* schemaVersion 1 + a plain `targets` object + `requirements` and
|
|
256
|
-
* `verificationTargets` arrays. A merge-patch delta does not carry these
|
|
257
|
-
* fields, so this is the deterministic boundary between a full contract and a
|
|
258
|
-
* patch. Keep the two predicates in sync.
|
|
259
|
-
*/
|
|
260
|
-
function isFullContractShape(value) {
|
|
261
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
262
|
-
return false;
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (error.code !== "ENOENT")
|
|
229
|
+
throw error;
|
|
263
230
|
}
|
|
264
|
-
|
|
265
|
-
const targets = record.targets;
|
|
266
|
-
return (record.schemaVersion === 1 &&
|
|
267
|
-
targets !== null &&
|
|
268
|
-
typeof targets === "object" &&
|
|
269
|
-
!Array.isArray(targets) &&
|
|
270
|
-
Array.isArray(record.requirements) &&
|
|
271
|
-
Array.isArray(record.verificationTargets));
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Shared merge pipeline for revisionPatch mode and content-driven
|
|
275
|
-
* compatibility: read the original contract node, extract the original
|
|
276
|
-
* contract and the effective node's patch, apply the RFC 7386 merge-patch, and
|
|
277
|
-
* serialize deterministically. Throws on any failure; callers map the error to
|
|
278
|
-
* their own classification (retryable-invalid for explicit revisionPatch mode,
|
|
279
|
-
* blocked for content-driven fallback).
|
|
280
|
-
*/
|
|
281
|
-
async function mergeRevisionPatch(runDir, planText, originalNodeId) {
|
|
282
|
-
const originalText = await readNodeText(runDir, originalNodeId);
|
|
283
|
-
const originalValue = extractFrontendImplementationJson(originalText);
|
|
284
|
-
const patchValue = extractFrontendImplementationJson(planText);
|
|
285
|
-
const merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
|
|
286
|
-
return serializeDeterministicJson(merged);
|
|
231
|
+
return { contractText, citationsText, rawSha256: digest };
|
|
287
232
|
}
|
|
288
233
|
function firstNonEmptyVerdictLine(text) {
|
|
289
234
|
const lines = text
|
|
@@ -327,6 +272,7 @@ function isFailedToolResult(event) {
|
|
|
327
272
|
}
|
|
328
273
|
async function collectOpenspecReadPaths(input) {
|
|
329
274
|
const { runDir, planNodeId, reviewNodeId, repoRoot } = input;
|
|
275
|
+
const candidateSet = input.candidatePaths ? new Set(input.candidatePaths) : undefined;
|
|
330
276
|
const matched = new Set();
|
|
331
277
|
for (const nodeId of [planNodeId, reviewNodeId]) {
|
|
332
278
|
const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
|
|
@@ -361,7 +307,7 @@ async function collectOpenspecReadPaths(input) {
|
|
|
361
307
|
: null;
|
|
362
308
|
if (relativePath &&
|
|
363
309
|
!isFailedToolResult(event) &&
|
|
364
|
-
|
|
310
|
+
(candidateSet ? candidateSet.has(relativePath) : OPENSPEC_SPEC_EXT_RE.test(relativePath))) {
|
|
365
311
|
matched.add(relativePath);
|
|
366
312
|
}
|
|
367
313
|
startedReads.delete(event.toolCallId);
|
|
@@ -376,21 +322,6 @@ async function collectOpenspecReadPaths(input) {
|
|
|
376
322
|
}
|
|
377
323
|
return [...matched];
|
|
378
324
|
}
|
|
379
|
-
async function checkOpenspecReadEvidence(input) {
|
|
380
|
-
const { runDir, candidatePaths, planNodeId, reviewNodeId, repoRoot } = input;
|
|
381
|
-
if (candidatePaths.length === 0)
|
|
382
|
-
return [];
|
|
383
|
-
const allReadPaths = await collectOpenspecReadPaths({
|
|
384
|
-
runDir,
|
|
385
|
-
planNodeId,
|
|
386
|
-
reviewNodeId,
|
|
387
|
-
repoRoot,
|
|
388
|
-
});
|
|
389
|
-
const normalizedCandidates = new Set(candidatePaths
|
|
390
|
-
.map((candidate) => toRepoRelativePath(candidate, repoRoot))
|
|
391
|
-
.filter((candidate) => Boolean(candidate && isOpenspecSpecFilePath(candidate))));
|
|
392
|
-
return allReadPaths.filter((readPath) => normalizedCandidates.has(readPath));
|
|
393
|
-
}
|
|
394
325
|
function normalizeCitationPath(rawPath, repoRoot) {
|
|
395
326
|
let candidate = rawPath.trim();
|
|
396
327
|
const hashIndex = candidate.indexOf("#");
|
|
@@ -405,57 +336,132 @@ function normalizeCitationPath(rawPath, repoRoot) {
|
|
|
405
336
|
}
|
|
406
337
|
return candidate;
|
|
407
338
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
339
|
+
async function resolveRequiredOpenspecPaths(input) {
|
|
340
|
+
if (input.policy === "scan-strict")
|
|
341
|
+
return { ok: true, paths: input.candidatePaths };
|
|
342
|
+
// Legacy cited DAGs had no selector: preserve their full candidate must-read
|
|
343
|
+
// semantics instead of silently weakening an existing run.
|
|
344
|
+
if (!input.selectionNodeId)
|
|
345
|
+
return { ok: true, paths: input.candidatePaths };
|
|
346
|
+
// OpenSpec classifications arrive as individual typed `openspec-selection`
|
|
347
|
+
// facts (one path per record_openspec_selection call) so a large candidate
|
|
348
|
+
// set never exceeds a single model output budget. Only positive
|
|
349
|
+
// classifications (required | relevant) are legal facts; candidates that
|
|
350
|
+
// were never mentioned default to irrelevant. Explicit task declarations /
|
|
351
|
+
// source citations (`mandatoryPaths`) are never downgraded: they are
|
|
352
|
+
// always required and always must-read, whether or not the model declared
|
|
353
|
+
// them. Fall back to the legacy single fenced JSON for older runs (which
|
|
354
|
+
// may still carry explicit irrelevant rows).
|
|
355
|
+
let selections;
|
|
356
|
+
let allowIrrelevant = false;
|
|
357
|
+
const selectionNodeId = input.selectionNodeId; // guarded above: no selector -> full candidate semantics
|
|
358
|
+
try {
|
|
359
|
+
const facts = await readCommittedContractFacts(input.runDir, selectionNodeId);
|
|
360
|
+
selections = facts
|
|
361
|
+
.filter((fact) => fact.kind === "openspec-selection")
|
|
362
|
+
.map((fact) => ({
|
|
363
|
+
path: String(fact.path ?? ""),
|
|
364
|
+
disposition: String(fact.disposition ?? ""),
|
|
365
|
+
rationale: String(fact.rationale ?? ""),
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
selections = [];
|
|
370
|
+
}
|
|
371
|
+
if (selections.length === 0) {
|
|
372
|
+
const legacy = await readLegacyOpenspecSelection({
|
|
373
|
+
runDir: input.runDir,
|
|
374
|
+
selectionNodeId,
|
|
375
|
+
candidatePaths: input.candidatePaths,
|
|
376
|
+
mandatoryPaths: input.mandatoryPaths,
|
|
377
|
+
});
|
|
378
|
+
if (!legacy.ok)
|
|
379
|
+
return legacy;
|
|
380
|
+
selections = legacy.selections;
|
|
381
|
+
allowIrrelevant = true;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const byPath = new Map();
|
|
385
|
+
for (const row of selections) {
|
|
386
|
+
const legalDispositions = allowIrrelevant
|
|
387
|
+
? ["required", "relevant", "irrelevant"]
|
|
388
|
+
: ["required", "relevant"];
|
|
389
|
+
if (typeof row.path !== "string" ||
|
|
390
|
+
!legalDispositions.includes(row.disposition) ||
|
|
391
|
+
typeof row.rationale !== "string" ||
|
|
392
|
+
!row.rationale.trim()) {
|
|
393
|
+
throw new Error(`invalid selection row (path+${legalDispositions.join("|")}+non-empty rationale expected): ${JSON.stringify(row)}`);
|
|
439
394
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
unparseable = true;
|
|
443
|
-
continue;
|
|
395
|
+
if (!input.candidatePaths.includes(row.path)) {
|
|
396
|
+
throw new Error(`invalid selection ${row.path}: not a candidate path`);
|
|
444
397
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
398
|
+
if (byPath.has(row.path))
|
|
399
|
+
throw new Error(`duplicate selection ${row.path}`);
|
|
400
|
+
byPath.set(row.path, row.disposition);
|
|
401
|
+
}
|
|
402
|
+
// Only explicitly required/relevant candidates must be read; everything
|
|
403
|
+
// unmentioned defaults to irrelevant. mandatoryPaths are intrinsic:
|
|
404
|
+
// they must be read even if the model omitted them, and they can never
|
|
405
|
+
// be downgraded by a declaration.
|
|
406
|
+
const mustRead = [
|
|
407
|
+
...input.mandatoryPaths,
|
|
408
|
+
...input.candidatePaths.filter((candidate) => byPath.get(candidate) === "required"),
|
|
409
|
+
];
|
|
410
|
+
return { ok: true, paths: [...new Set(mustRead)].sort() };
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
reason: `openspec selector invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function readCommittedContractFacts(runDir, nodeId) {
|
|
420
|
+
const { readTypedEventStoreFromJsonl } = await import("./frontend-typed-event-store.js");
|
|
421
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, nodeId, "contract-typed-facts.jsonl"));
|
|
422
|
+
return records
|
|
423
|
+
.filter((record) => record.phase === "committed")
|
|
424
|
+
.map((record) => record.fact)
|
|
425
|
+
.filter((fact) => Boolean(fact));
|
|
426
|
+
}
|
|
427
|
+
async function readLegacyOpenspecSelection(input) {
|
|
428
|
+
let text;
|
|
429
|
+
try {
|
|
430
|
+
text = await readNodeText(input.runDir, input.selectionNodeId);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
reason: `openspec selector output unreadable: ${error instanceof Error ? error.message : String(error)}`,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const fenced = /```json\s*\n([\s\S]*?)\n```/.exec(text);
|
|
439
|
+
if (!fenced) {
|
|
440
|
+
return {
|
|
441
|
+
ok: false,
|
|
442
|
+
reason: "openspec selector emitted neither typed openspec-selection facts nor a legacy fenced JSON selection object",
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
const parsed = JSON.parse(fenced[1] ?? "");
|
|
447
|
+
if (parsed.schemaVersion !== 1 ||
|
|
448
|
+
parsed.schemaId !== "frontend-openspec-selection-v1" ||
|
|
449
|
+
!Array.isArray(parsed.selections)) {
|
|
450
|
+
throw new Error("schemaVersion/schemaId/selections invalid");
|
|
456
451
|
}
|
|
452
|
+
const rows = parsed.selections.map((row) => ({
|
|
453
|
+
path: String(row.path ?? ""),
|
|
454
|
+
disposition: String(row.disposition ?? ""),
|
|
455
|
+
rationale: String(row.rationale ?? ""),
|
|
456
|
+
}));
|
|
457
|
+
return { ok: true, selections: rows };
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
return {
|
|
461
|
+
ok: false,
|
|
462
|
+
reason: `openspec selector invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
463
|
+
};
|
|
457
464
|
}
|
|
458
|
-
return { blockPresent, unparseable, citations };
|
|
459
465
|
}
|
|
460
466
|
async function finalizePrewrite(input, pending) {
|
|
461
467
|
const sourceBindingSha256 = input.sourceBinding
|
|
@@ -506,13 +512,37 @@ async function finalizePrewrite(input, pending) {
|
|
|
506
512
|
openspecPolicy: pending.openspecPolicy,
|
|
507
513
|
};
|
|
508
514
|
}
|
|
515
|
+
export { checkFrontendSourceFidelityLedger } from "./frontend-source-fidelity-ledger.js";
|
|
509
516
|
export async function runFrontendPrewriteGate(input) {
|
|
510
517
|
const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
|
|
511
518
|
// selectNode throws for a scheduling defect (no FINISHED plan/review). That
|
|
512
519
|
// remains an ERROR and is intentionally not classified into prewrite-result.
|
|
513
520
|
const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
|
|
514
521
|
const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
|
|
515
|
-
|
|
522
|
+
let planArtifact;
|
|
523
|
+
try {
|
|
524
|
+
planArtifact = await readCanonicalContractFromNode(input.runDir, planNodeId);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
return finalizePrewrite(input, {
|
|
528
|
+
planNodeId,
|
|
529
|
+
reviewNodeId,
|
|
530
|
+
verdict: "",
|
|
531
|
+
candidateRawSha256: sha256Hex(""),
|
|
532
|
+
candidateJsonSha256: null,
|
|
533
|
+
canonicalSha256: null,
|
|
534
|
+
normalizationActions: [],
|
|
535
|
+
mockStrategy: undefined,
|
|
536
|
+
artifact: undefined,
|
|
537
|
+
failureReason: error instanceof Error ? error.message : String(error),
|
|
538
|
+
failureCode: "plan-text-unreadable",
|
|
539
|
+
classification: "retryable-invalid",
|
|
540
|
+
openspecReadPaths: [],
|
|
541
|
+
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
542
|
+
openspecPolicy: input.config.openspecPolicy ?? "scan-strict",
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
const candidateRawSha256 = planArtifact.rawSha256;
|
|
516
546
|
const openspecPolicy = input.config.openspecPolicy ?? "scan-strict";
|
|
517
547
|
const basePending = {
|
|
518
548
|
planNodeId,
|
|
@@ -531,21 +561,6 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
531
561
|
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
532
562
|
openspecPolicy,
|
|
533
563
|
};
|
|
534
|
-
let planInput;
|
|
535
|
-
try {
|
|
536
|
-
planInput = await readFrontendPlanNodeInput(input.runDir, planNodeId);
|
|
537
|
-
}
|
|
538
|
-
catch (error) {
|
|
539
|
-
return finalizePrewrite(input, {
|
|
540
|
-
...basePending,
|
|
541
|
-
classification: "retryable-invalid",
|
|
542
|
-
failureReason: error instanceof Error ? error.message : String(error),
|
|
543
|
-
failureCode: "plan-text-unreadable",
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
const planText = planInput.payloadText;
|
|
547
|
-
const citationsText = planInput.citationsText;
|
|
548
|
-
basePending.candidateRawSha256 = planInput.rawCandidateSha256;
|
|
549
564
|
let reviewText;
|
|
550
565
|
try {
|
|
551
566
|
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
@@ -568,90 +583,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
568
583
|
failureCode: "verdict-not-pass",
|
|
569
584
|
});
|
|
570
585
|
}
|
|
571
|
-
|
|
572
|
-
// delta against the original contract node (planFallbackFromNodeIds[0]).
|
|
573
|
-
// Legacy DAGs without revisionPatch (undefined) keep full-contract semantics.
|
|
574
|
-
const revisionPatchMode = input.config.revisionPatch === true &&
|
|
575
|
-
planNodeId === input.config.planFromNodeId &&
|
|
576
|
-
input.config.planFallbackFromNodeIds.length > 0;
|
|
577
|
-
let effectiveContractText = null;
|
|
578
|
-
if (revisionPatchMode) {
|
|
579
|
-
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
580
|
-
try {
|
|
581
|
-
const originalInput = await readFrontendPlanNodeInput(input.runDir, originalNodeId);
|
|
582
|
-
if (planInput.artifactFirst && !originalInput.artifactFirst) {
|
|
583
|
-
throw new Error(`artifact-first revision ${planNodeId} requires artifact-first original contract ${originalNodeId}`);
|
|
584
|
-
}
|
|
585
|
-
if (planInput.artifactFirst) {
|
|
586
|
-
if (planInput.payloadKind !== "merge-patch" ||
|
|
587
|
-
originalInput.payloadKind !== "canonical-contract") {
|
|
588
|
-
throw new Error("artifact-first revision/original payload kinds are invalid");
|
|
589
|
-
}
|
|
590
|
-
effectiveContractText = serializeDeterministicJson(applyFrontendImplementationContractPatch(JSON.parse(originalInput.payloadText), JSON.parse(planInput.payloadText)));
|
|
591
|
-
}
|
|
592
|
-
else {
|
|
593
|
-
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
catch (error) {
|
|
597
|
-
return finalizePrewrite(input, {
|
|
598
|
-
...basePending,
|
|
599
|
-
verdict,
|
|
600
|
-
classification: "retryable-invalid",
|
|
601
|
-
failureReason: `frontend prewrite gate revisionPatch mode requires original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
602
|
-
failureCode: "contract-invalid",
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
// Content-driven compatibility (Bug 1): legacy DAGs without
|
|
608
|
-
// revisionPatch can still receive a merge-patch delta from the revision
|
|
609
|
-
// node. Detect the output shape instead of trusting the DAG flag alone.
|
|
610
|
-
// - Full contract shape → keep legacy full-contract semantics unchanged.
|
|
611
|
-
// - Patch shape with a usable original contract node → auto-merge and
|
|
612
|
-
// run the existing analysis/materialization pipeline.
|
|
613
|
-
// - Patch shape but unmergeable → blocked + revision-patch-unmergeable
|
|
614
|
-
// (no retryable loop).
|
|
615
|
-
let planJsonValue = null;
|
|
616
|
-
try {
|
|
617
|
-
planJsonValue = extractFrontendImplementationJson(planText);
|
|
618
|
-
}
|
|
619
|
-
catch {
|
|
620
|
-
// Not machine-readable JSON: fall back to the legacy full-contract
|
|
621
|
-
// path where analyzeFrontendImplementationContract surfaces the
|
|
622
|
-
// original diagnostic (fail-closed without a new failure mode).
|
|
623
|
-
}
|
|
624
|
-
if (planJsonValue !== null && !isFullContractShape(planJsonValue)) {
|
|
625
|
-
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
626
|
-
if (!originalNodeId || originalNodeId === planNodeId) {
|
|
627
|
-
return finalizePrewrite(input, {
|
|
628
|
-
...basePending,
|
|
629
|
-
verdict,
|
|
630
|
-
classification: "blocked",
|
|
631
|
-
failureReason: "frontend prewrite gate: the effective plan output is a merge-patch delta but no distinct original contract node is available. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.",
|
|
632
|
-
failureCode: "revision-patch-unmergeable",
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
try {
|
|
636
|
-
const originalText = await readNodeText(input.runDir, originalNodeId);
|
|
637
|
-
const originalValue = extractFrontendImplementationJson(originalText);
|
|
638
|
-
if (!isFullContractShape(originalValue)) {
|
|
639
|
-
throw new Error(`original contract node ${originalNodeId} is not a full frontend implementation contract`);
|
|
640
|
-
}
|
|
641
|
-
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
642
|
-
}
|
|
643
|
-
catch (error) {
|
|
644
|
-
return finalizePrewrite(input, {
|
|
645
|
-
...basePending,
|
|
646
|
-
verdict,
|
|
647
|
-
classification: "blocked",
|
|
648
|
-
failureReason: `frontend prewrite gate: the effective plan output is a merge-patch delta but it cannot be merged onto the original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.`,
|
|
649
|
-
failureCode: "revision-patch-unmergeable",
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
const requirementIdSearchText = effectiveContractText ?? planText;
|
|
586
|
+
const requirementIdSearchText = planArtifact.contractText;
|
|
655
587
|
const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
|
|
656
588
|
if (missingIds.length > 0) {
|
|
657
589
|
return finalizePrewrite(input, {
|
|
@@ -699,17 +631,13 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
699
631
|
workspaceRoot,
|
|
700
632
|
});
|
|
701
633
|
}
|
|
702
|
-
// Candidate -> canonical conversion.
|
|
703
|
-
//
|
|
704
|
-
// here would silently fall back to its prose and defeat the integrity gate.
|
|
705
|
-
// Historical records intentionally retain their node-text compatibility path.
|
|
634
|
+
// Candidate -> canonical conversion. The gate consumes the producing node's
|
|
635
|
+
// hash-bound canonical artifact and never re-merges plan/revision stdout.
|
|
706
636
|
let analysis;
|
|
707
637
|
try {
|
|
708
|
-
const rawContractText = effectiveContractText ?? (planInput.artifactFirst ? planText : undefined);
|
|
709
638
|
analysis = await analyzeFrontendImplementationContract({
|
|
710
639
|
runDir: input.runDir,
|
|
711
|
-
|
|
712
|
-
rawContractText,
|
|
640
|
+
rawContractText: planArtifact.contractText,
|
|
713
641
|
sourceBinding: input.sourceBinding,
|
|
714
642
|
});
|
|
715
643
|
}
|
|
@@ -729,6 +657,48 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
729
657
|
}
|
|
730
658
|
throw error;
|
|
731
659
|
}
|
|
660
|
+
// Source fidelity ledger gate (20260824-frontend-source-fidelity-ledger-v1):
|
|
661
|
+
// 在 verdict/missing-requirement-ids/source-binding-not-fresh 之后插入 ledger
|
|
662
|
+
// 检查。缺失即 fail closed 且 writer 不获授权;所有检查通过才继续物化契约。
|
|
663
|
+
if (input.sourceBinding) {
|
|
664
|
+
const ledgerCheck = await checkFrontendSourceFidelityLedger({
|
|
665
|
+
workspaceRoot: workspaceRoot ?? "",
|
|
666
|
+
binding: input.sourceBinding,
|
|
667
|
+
contractRequirements: analysis.canonical.requirements.map((requirement) => ({
|
|
668
|
+
id: requirement.id,
|
|
669
|
+
sourceFragmentIds: requirement.sourceFragmentIds,
|
|
670
|
+
sourceRefs: requirement.sourceRefs,
|
|
671
|
+
})),
|
|
672
|
+
});
|
|
673
|
+
if (!ledgerCheck.ok && ledgerCheck.failureCode) {
|
|
674
|
+
return finalizePrewrite(input, {
|
|
675
|
+
...basePending,
|
|
676
|
+
verdict,
|
|
677
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
678
|
+
normalizationActions: [],
|
|
679
|
+
mockStrategy: analysis.canonical.mockApi.strategy,
|
|
680
|
+
classification: "blocked",
|
|
681
|
+
failureReason: ledgerCheck.failureReason ??
|
|
682
|
+
"frontend source fidelity ledger gate failed",
|
|
683
|
+
failureCode: ledgerCheck.failureCode,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// Size guard: a canonical contract above 64KB is a retryable-invalid defect
|
|
688
|
+
// (compress the contract) rather than a writer authorization failure.
|
|
689
|
+
const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
|
|
690
|
+
if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
|
|
691
|
+
return finalizePrewrite(input, {
|
|
692
|
+
...basePending,
|
|
693
|
+
verdict,
|
|
694
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
695
|
+
normalizationActions: [],
|
|
696
|
+
mockStrategy: analysis.canonical.mockApi.strategy,
|
|
697
|
+
classification: "retryable-invalid",
|
|
698
|
+
failureReason: `frontend prewrite gate: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit. 压缩契约(移除冗余 prose、重复目标或过长描述)后重试。`,
|
|
699
|
+
failureCode: "contract-too-large",
|
|
700
|
+
});
|
|
701
|
+
}
|
|
732
702
|
const mockStrategy = analysis.canonical.mockApi.strategy;
|
|
733
703
|
if (!input.config.allowedMockStrategies.includes(mockStrategy)) {
|
|
734
704
|
const quotedAllowed = input.config.allowedMockStrategies
|
|
@@ -800,14 +770,31 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
800
770
|
// Effective plan/review must actually read the candidate openspec paths when
|
|
801
771
|
// any exist. This fail-closes BEFORE the canonical contract is materialized
|
|
802
772
|
// so a writer is never authorized on top of unread normative evidence.
|
|
803
|
-
const
|
|
773
|
+
const frozenCandidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
774
|
+
const selection = await resolveRequiredOpenspecPaths({
|
|
775
|
+
runDir: input.runDir,
|
|
776
|
+
selectionNodeId: input.config.openspecSelectionNodeId,
|
|
777
|
+
candidatePaths: frozenCandidatePaths,
|
|
778
|
+
mandatoryPaths: input.config.openspecMandatoryPaths ?? [],
|
|
779
|
+
policy: openspecPolicy,
|
|
780
|
+
});
|
|
781
|
+
if (!selection.ok) {
|
|
782
|
+
return finalizePrewrite(input, {
|
|
783
|
+
...basePending, verdict, candidateJsonSha256: analysis.candidateJsonSha256,
|
|
784
|
+
normalizationActions: [], mockStrategy, classification: "retryable-invalid",
|
|
785
|
+
failureReason: `frontend prewrite gate: ${selection.reason}`,
|
|
786
|
+
failureCode: "openspec-selection-invalid",
|
|
787
|
+
openspecCandidatePaths: frozenCandidatePaths,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
const candidatePaths = selection.paths;
|
|
804
791
|
const candidateRoot = workspaceRoot ?? process.cwd();
|
|
805
792
|
// Drift precheck: candidates are frozen at generation time. A candidate that
|
|
806
793
|
// no longer exists on disk was deleted/renamed after generation and is a
|
|
807
794
|
// generation-contract defect, not an "unread" failure.
|
|
808
795
|
const missingCandidates = [];
|
|
809
|
-
if (
|
|
810
|
-
for (const candidate of
|
|
796
|
+
if (frozenCandidatePaths.length > 0) {
|
|
797
|
+
for (const candidate of frozenCandidatePaths) {
|
|
811
798
|
try {
|
|
812
799
|
await stat(path.join(candidateRoot, candidate));
|
|
813
800
|
}
|
|
@@ -830,99 +817,78 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
830
817
|
openspecCandidatePaths: candidatePaths,
|
|
831
818
|
});
|
|
832
819
|
}
|
|
833
|
-
|
|
834
|
-
if (
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
if (!
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
...new Set(parsed.citations
|
|
878
|
-
.filter((citation) => !readSet.has(citation.path))
|
|
879
|
-
.map((citation) => citation.path)),
|
|
880
|
-
];
|
|
881
|
-
if (uncorroborated.length > 0) {
|
|
882
|
-
return finalizePrewrite(input, {
|
|
883
|
-
...basePending,
|
|
884
|
-
verdict,
|
|
885
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
886
|
-
normalizationActions: [],
|
|
887
|
-
mockStrategy,
|
|
888
|
-
classification: "retryable-invalid",
|
|
889
|
-
failureReason: `frontend prewrite gate: these openspec paths were cited in the plan but have no successful read event in the effective plan/review: ${uncorroborated.join(", ")}`,
|
|
890
|
-
failureCode: "openspec-citation-not-read",
|
|
891
|
-
openspecReadPaths,
|
|
892
|
-
openspecCandidatePaths: candidatePaths,
|
|
893
|
-
});
|
|
894
|
-
}
|
|
820
|
+
const frozenSnapshots = input.config.openspecCandidateSnapshots;
|
|
821
|
+
if (frozenSnapshots) {
|
|
822
|
+
const snapshotByPath = new Map(frozenSnapshots.map((snapshot) => [snapshot.path, snapshot.sha256]));
|
|
823
|
+
const incompleteSnapshots = frozenCandidatePaths.filter((candidate) => !snapshotByPath.has(candidate));
|
|
824
|
+
const unexpectedSnapshots = frozenSnapshots
|
|
825
|
+
.map((snapshot) => snapshot.path)
|
|
826
|
+
.filter((candidate) => !frozenCandidatePaths.includes(candidate));
|
|
827
|
+
const changedCandidates = [];
|
|
828
|
+
for (const candidate of frozenCandidatePaths) {
|
|
829
|
+
const expected = snapshotByPath.get(candidate);
|
|
830
|
+
if (!expected)
|
|
831
|
+
continue;
|
|
832
|
+
const actual = createHash("sha256")
|
|
833
|
+
.update(await readFile(path.join(candidateRoot, candidate)))
|
|
834
|
+
.digest("hex");
|
|
835
|
+
if (actual !== expected)
|
|
836
|
+
changedCandidates.push(candidate);
|
|
837
|
+
}
|
|
838
|
+
if (incompleteSnapshots.length > 0 ||
|
|
839
|
+
unexpectedSnapshots.length > 0 ||
|
|
840
|
+
changedCandidates.length > 0) {
|
|
841
|
+
const details = [
|
|
842
|
+
...(incompleteSnapshots.length > 0
|
|
843
|
+
? [`missing snapshots: ${incompleteSnapshots.join(", ")}`]
|
|
844
|
+
: []),
|
|
845
|
+
...(unexpectedSnapshots.length > 0
|
|
846
|
+
? [`unexpected snapshots: ${unexpectedSnapshots.join(", ")}`]
|
|
847
|
+
: []),
|
|
848
|
+
...(changedCandidates.length > 0
|
|
849
|
+
? [`content changed: ${changedCandidates.join(", ")}`]
|
|
850
|
+
: []),
|
|
851
|
+
].join("; ");
|
|
852
|
+
return finalizePrewrite(input, {
|
|
853
|
+
...basePending,
|
|
854
|
+
verdict,
|
|
855
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
856
|
+
normalizationActions: [],
|
|
857
|
+
mockStrategy,
|
|
858
|
+
classification: "blocked",
|
|
859
|
+
failureReason: `frontend prewrite gate blocked: the generation-frozen OpenSpec content snapshot no longer matches the workspace (${details}). Re-read the changed specification and regenerate the DAG.`,
|
|
860
|
+
failureCode: "candidate-content-drift",
|
|
861
|
+
openspecReadPaths: [],
|
|
862
|
+
openspecCandidatePaths: candidatePaths,
|
|
863
|
+
});
|
|
895
864
|
}
|
|
896
|
-
// cited with empty candidates: no read enforcement. The generation step
|
|
897
|
-
// emits an advisory instead of failing the gate.
|
|
898
865
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
}
|
|
866
|
+
let openspecReadPaths;
|
|
867
|
+
openspecReadPaths = await collectOpenspecReadPaths({
|
|
868
|
+
runDir: input.runDir,
|
|
869
|
+
planNodeId,
|
|
870
|
+
reviewNodeId,
|
|
871
|
+
repoRoot: candidateRoot,
|
|
872
|
+
...(candidatePaths.length > 0 ? { candidatePaths } : {}),
|
|
873
|
+
});
|
|
874
|
+
if (candidatePaths.length > 0) {
|
|
875
|
+
const readSet = new Set(openspecReadPaths);
|
|
876
|
+
const unread = candidatePaths.filter((candidate) => !readSet.has(candidate));
|
|
877
|
+
if (unread.length > 0) {
|
|
878
|
+
const governanceRoot = await resolveOpenspecGovernanceRoot(candidateRoot);
|
|
879
|
+
const excludedGovernanceCount = await countExcludedGovernanceSpecFiles(candidateRoot, governanceRoot);
|
|
880
|
+
return finalizePrewrite(input, {
|
|
881
|
+
...basePending,
|
|
882
|
+
verdict,
|
|
883
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
884
|
+
normalizationActions: [],
|
|
885
|
+
mockStrategy,
|
|
886
|
+
classification: "retryable-invalid",
|
|
887
|
+
failureReason: `frontend prewrite gate: effective plan/review did not read these openspec candidate paths: ${unread.join(", ")}. Read each candidate and report applicable rules plus hit path/section/line number, conflicts, and missing specifications, then rerun. 实际成功读取: ${openspecReadPaths.length > 0 ? openspecReadPaths.join(", ") : "(none)"}; 候选总数: ${candidatePaths.length}; 排除治理目录 ${governanceRoot} 下 ${excludedGovernanceCount} 个文件(运行期诊断口径); 候选集在生成期冻结,若候选与本任务无关属于生成期契约配置问题,应修正后重新生成 DAG。`,
|
|
888
|
+
failureCode: "openspec-not-read",
|
|
889
|
+
openspecReadPaths,
|
|
890
|
+
openspecCandidatePaths: candidatePaths,
|
|
891
|
+
});
|
|
926
892
|
}
|
|
927
893
|
}
|
|
928
894
|
// Component selection spec conformance (ADR 0016). Deterministic cross-check
|
|
@@ -944,13 +910,17 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
944
910
|
normalizationActions: [],
|
|
945
911
|
mockStrategy,
|
|
946
912
|
classification: "retryable-invalid",
|
|
947
|
-
failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new
|
|
913
|
+
failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new);specified 必须给 OpenSpec specReference.path/section/line,new 必须给任务源 PRD specReference.path/section/line。",
|
|
948
914
|
failureCode: "component-choices-missing",
|
|
949
915
|
openspecReadPaths,
|
|
950
916
|
openspecCandidatePaths: candidatePaths,
|
|
951
917
|
});
|
|
952
918
|
}
|
|
919
|
+
// Only `specified` choices cite the frozen OpenSpec candidate bucket. A
|
|
920
|
+
// `new` choice instead cites its task-source/PRD declaration and must not be
|
|
921
|
+
// rejected merely because that source is outside the OpenSpec roots.
|
|
953
922
|
const componentSpecReferences = componentChoices
|
|
923
|
+
.filter((choice) => choice.decision === "specified")
|
|
954
924
|
.map((choice) => ({
|
|
955
925
|
raw: choice.specReference?.path ?? "",
|
|
956
926
|
normalized: choice.specReference?.path
|
|
@@ -958,11 +928,37 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
958
928
|
: null,
|
|
959
929
|
}))
|
|
960
930
|
.filter((ref) => ref.raw.length > 0);
|
|
931
|
+
if (componentSpecCandidates.length > 0 && hasUiVisibleWork) {
|
|
932
|
+
const componentCandidateSet = new Set(componentSpecCandidates);
|
|
933
|
+
const hasBoundSpecifiedChoice = componentChoices.some((choice) => {
|
|
934
|
+
if (choice.decision !== "specified" || !choice.specReference?.path) {
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
const normalized = normalizeCitationPath(choice.specReference.path, candidateRoot);
|
|
938
|
+
return Boolean(normalized && componentCandidateSet.has(normalized));
|
|
939
|
+
});
|
|
940
|
+
if (!hasBoundSpecifiedChoice) {
|
|
941
|
+
return finalizePrewrite(input, {
|
|
942
|
+
...basePending,
|
|
943
|
+
verdict,
|
|
944
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
945
|
+
normalizationActions: [],
|
|
946
|
+
mockStrategy,
|
|
947
|
+
classification: "retryable-invalid",
|
|
948
|
+
failureReason: "frontend prewrite gate: UI-visible work has a generation-frozen component/theme specification bucket, but no uiComponentChoices decision=specified is bound to a path in that bucket. At least one declared UI purpose must use and cite the applicable component/theme specification; reuse-existing/new alone cannot bypass the bucket.",
|
|
949
|
+
failureCode: "component-spec-choice-unbound",
|
|
950
|
+
openspecReadPaths,
|
|
951
|
+
openspecCandidatePaths: candidatePaths,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
961
955
|
if (componentSpecReferences.length > 0) {
|
|
962
956
|
const invalidRefs = [
|
|
963
957
|
...new Set(componentSpecReferences
|
|
964
958
|
.filter((ref) => !ref.normalized ||
|
|
965
|
-
!
|
|
959
|
+
!(candidatePaths.length > 0
|
|
960
|
+
? candidatePaths.includes(ref.normalized)
|
|
961
|
+
: isOpenspecSpecFilePath(ref.normalized)))
|
|
966
962
|
.map((ref) => ref.raw)),
|
|
967
963
|
];
|
|
968
964
|
if (invalidRefs.length > 0) {
|
|
@@ -980,13 +976,10 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
980
976
|
});
|
|
981
977
|
}
|
|
982
978
|
if (openspecPolicy === "cited") {
|
|
983
|
-
const parsedCitations = parseOpenspecCitations(citationsText, candidateRoot);
|
|
984
|
-
const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
|
|
985
979
|
const readSet = new Set(openspecReadPaths);
|
|
986
980
|
const uncorroborated = [
|
|
987
981
|
...new Set(componentSpecReferences
|
|
988
982
|
.filter((ref) => !ref.normalized ||
|
|
989
|
-
!citedPaths.has(ref.normalized) ||
|
|
990
983
|
!readSet.has(ref.normalized))
|
|
991
984
|
.map((ref) => ref.raw)),
|
|
992
985
|
];
|
|
@@ -998,7 +991,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
998
991
|
normalizationActions: [],
|
|
999
992
|
mockStrategy,
|
|
1000
993
|
classification: "retryable-invalid",
|
|
1001
|
-
failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths
|
|
994
|
+
failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths are missing a ledger specReference or lack a successful read event: ${uncorroborated.join(", ")}`,
|
|
1002
995
|
failureCode: "component-spec-not-cited",
|
|
1003
996
|
openspecReadPaths,
|
|
1004
997
|
openspecCandidatePaths: candidatePaths,
|
|
@@ -1072,6 +1065,9 @@ export function formatFrontendPrewriteGateStdout(result) {
|
|
|
1072
1065
|
}
|
|
1073
1066
|
lines.push(`Prewrite result: ${result.prewriteResult.path}`);
|
|
1074
1067
|
lines.push(`Openspec policy: ${result.openspecPolicy}`);
|
|
1068
|
+
if (result.failureCode?.startsWith("source-fidelity-")) {
|
|
1069
|
+
lines.push(`Source fidelity ledger: blocked (${result.failureCode})`);
|
|
1070
|
+
}
|
|
1075
1071
|
if (result.failureReason)
|
|
1076
1072
|
lines.push(`Failure reason: ${result.failureReason}`);
|
|
1077
1073
|
if (result.openspecReadPaths.length > 0) {
|