@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,8 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
|
-
import { writeDagNodeJsonArtifact, writeDagNodeTextArtifact, } from "../infrastructure/harness/artifact-store.js";
|
|
6
|
+
import { writeDagNodeJsonArtifact, writeDagNodeTextArtifact, writeDagRunJsonArtifact, } from "../infrastructure/harness/artifact-store.js";
|
|
6
7
|
import { captureWorkspaceCheckpoint } from "../workflows/dag/workspace-checkpoint.js";
|
|
7
8
|
import { truncateOutput } from "../shared/output-truncation.js";
|
|
8
9
|
import { processTreeSpawnOptions, terminateProcessTree, } from "./process-tree.js";
|
|
@@ -14,20 +15,32 @@ import { buildRequirementCoverageGateShellCommand, expandShellPreset, buildVerdi
|
|
|
14
15
|
import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend-test-analysis-contract.js";
|
|
15
16
|
import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
|
|
16
17
|
import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
|
|
18
|
+
import { assertFrontendSourceBindingFresh, buildContractFromCommittedFacts, deterministicSha256, frontendImplementationContractSchema, serializeDeterministicJson, sha256Hex, writeFrontendImplementationContractArtifact, } from "../workflows/dag/frontend-implementation-contract.js";
|
|
19
|
+
import { checkFrontendSourceFidelityLedger } from "../workflows/dag/frontend-source-fidelity-ledger.js";
|
|
17
20
|
import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
|
|
21
|
+
import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
|
|
22
|
+
import { probeFrontendTestEnvironment } from "../workflows/dag/frontend-test-environment-probe.js";
|
|
18
23
|
import { renderFrontendTestL5Report } from "../workflows/dag/frontend-test-l5-report.js";
|
|
19
24
|
import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-case-checklist.js";
|
|
20
25
|
import { materializeFrontendTestCaseManifest } from "../workflows/dag/frontend-test-case-manifest.js";
|
|
21
26
|
import { renderFrontendTestHtmlReport } from "../workflows/dag/frontend-test-html-report.js";
|
|
22
27
|
import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
|
|
23
|
-
import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
28
|
+
import { captureFrontendWorktreeBaseline, formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
|
|
29
|
+
import { readTypedEventStoreFromJsonl } from "../workflows/dag/frontend-typed-event-store.js";
|
|
30
|
+
import { classifyFrontendFailure, } from "../workflows/dag/frontend-repair.js";
|
|
31
|
+
const FRONTEND_CONTRACT_MAX_BYTES = 65536;
|
|
32
|
+
import { evaluateFrontendDesignPolicy, } from "../workflows/dag/frontend-design-policy.js";
|
|
33
|
+
import { deriveFrontendWriterAdmissionShell, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, frontendWriterAdmissionResultV1Schema, } from "../workflows/dag/frontend-writer-admission.js";
|
|
34
|
+
import { deriveFrontendIntegrationFacts, buildFrontendTestHandoff, frontendCloseoutFactsSchema, renderFrontendCloseout, } from "../workflows/dag/frontend-closeout.js";
|
|
35
|
+
import { buildFrontendHumanDecisionEnvelope, evaluateHighAdmissionReceipt, routeVerificationConfig, } from "../workflows/dag/frontend-human-decision.js";
|
|
36
|
+
import { classifyFailureOwner, classifyIssueCategoryRestartPhase, classifyRestartPhase, } from "../workflows/dag/frontend-recovery-capsule.js";
|
|
37
|
+
import { pathMatchesPattern } from "../shared/git-progress.js";
|
|
26
38
|
import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from "../workflows/dag/frontend-review-context.js";
|
|
27
39
|
import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
|
|
28
40
|
import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
|
|
29
41
|
import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
|
|
30
42
|
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
43
|
+
import { frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
|
|
31
44
|
import { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
|
|
32
45
|
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
33
46
|
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
|
|
@@ -38,6 +51,7 @@ import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
|
38
51
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
39
52
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
40
53
|
import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
54
|
+
import { captureWorkspaceWriteSnapshot, diffWorkspaceWriteSnapshots, } from "./workspace-write-snapshot.js";
|
|
41
55
|
import { parseAndValidateFinalWriteSetApproval, } from "../workflows/dag/node-execution.js";
|
|
42
56
|
import { buildShellProcessEnv } from "./shell-verification.js";
|
|
43
57
|
import { readRunState } from "../workflows/dag/run-store.js";
|
|
@@ -121,6 +135,39 @@ function shouldReuseSuccessfulShellCommands(shell) {
|
|
|
121
135
|
const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
|
|
122
136
|
const SUMMARY_STDOUT_MAX = 4_000;
|
|
123
137
|
const SUMMARY_STDERR_MAX = 2_000;
|
|
138
|
+
const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
|
|
139
|
+
const TEST_TIMEOUT_MARKERS = [
|
|
140
|
+
/Test timed out in \d+ms/i,
|
|
141
|
+
/Timeout of \d+ms exceeded/i,
|
|
142
|
+
];
|
|
143
|
+
const DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS = [
|
|
144
|
+
/AssertionError/i,
|
|
145
|
+
/expected:?\s/i,
|
|
146
|
+
/received:?\s/i,
|
|
147
|
+
/Snapshot /i,
|
|
148
|
+
/toMatchSnapshot/i,
|
|
149
|
+
/SyntaxError/i,
|
|
150
|
+
/TS\d{4}/,
|
|
151
|
+
/error TS/i,
|
|
152
|
+
/eslint/i,
|
|
153
|
+
/compile error/i,
|
|
154
|
+
/Failed to compile/i,
|
|
155
|
+
];
|
|
156
|
+
function isIdentifiedTestCommand(command) {
|
|
157
|
+
return /(?:^|[\s"'`])(?:npx\s+)?vitest(?:\s|$)|(?:^|[\s"'`])npm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])pnpm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])yarn(?:\.cmd)?\s+test(?:\s|$)/i.test(command);
|
|
158
|
+
}
|
|
159
|
+
function classifyShellCommandFailure(input) {
|
|
160
|
+
if (input.processCategory !== "nonzero-exit")
|
|
161
|
+
return input.processCategory;
|
|
162
|
+
if (!isIdentifiedTestCommand(input.command))
|
|
163
|
+
return "nonzero-exit";
|
|
164
|
+
const sample = `${input.stdout}\n${input.stderr}`.slice(0, TEST_TIMEOUT_CLASSIFY_MAX_CHARS);
|
|
165
|
+
const hasTimeoutMarker = TEST_TIMEOUT_MARKERS.some((marker) => marker.test(sample));
|
|
166
|
+
if (!hasTimeoutMarker)
|
|
167
|
+
return "nonzero-exit";
|
|
168
|
+
const mixedDeterministicFailure = DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS.some((marker) => marker.test(sample));
|
|
169
|
+
return mixedDeterministicFailure ? "nonzero-exit" : "test-timeout";
|
|
170
|
+
}
|
|
124
171
|
const NODE_STDOUT_MAX = 12_000;
|
|
125
172
|
const NODE_STDERR_MAX = 8_000;
|
|
126
173
|
const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
|
|
@@ -327,16 +374,21 @@ export async function executeShellCommand(input) {
|
|
|
327
374
|
});
|
|
328
375
|
});
|
|
329
376
|
child.on("close", (code) => {
|
|
330
|
-
let
|
|
377
|
+
let processCategory = "nonzero-exit";
|
|
331
378
|
if (timedOut) {
|
|
332
|
-
|
|
379
|
+
processCategory = "timeout";
|
|
333
380
|
}
|
|
334
381
|
else if (code === 0) {
|
|
335
|
-
|
|
382
|
+
processCategory = "success";
|
|
336
383
|
}
|
|
337
384
|
finish({
|
|
338
385
|
exitCode: code,
|
|
339
|
-
failureCategory
|
|
386
|
+
failureCategory: classifyShellCommandFailure({
|
|
387
|
+
command: input.command,
|
|
388
|
+
processCategory,
|
|
389
|
+
stdout: formatBoundedOutput(stdout),
|
|
390
|
+
stderr: formatBoundedOutput(stderr),
|
|
391
|
+
}),
|
|
340
392
|
ok: code === 0 && !timedOut,
|
|
341
393
|
});
|
|
342
394
|
});
|
|
@@ -498,13 +550,22 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
498
550
|
const started = Date.now();
|
|
499
551
|
// Plan A: prefer the frozen layout carried by the compiled spec; fall back
|
|
500
552
|
// to the historical default for hand-written legacy DAGs.
|
|
501
|
-
const
|
|
553
|
+
const baseBackendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
502
554
|
? {
|
|
503
555
|
testRoot: meta.spec.backendTestLayout.testRoot,
|
|
504
556
|
markdownRel: meta.spec.backendTestLayout.markdownRel,
|
|
505
557
|
scriptRel: meta.spec.backendTestLayout.scriptRel,
|
|
506
558
|
}
|
|
507
559
|
: undefined);
|
|
560
|
+
const markdownPlanArtifact = path.join(meta.runDir, "generate-backend-md-plan-pi", "plan.md");
|
|
561
|
+
const backendLayout = existsSync(markdownPlanArtifact)
|
|
562
|
+
? {
|
|
563
|
+
...baseBackendLayout,
|
|
564
|
+
readmePath: path
|
|
565
|
+
.relative(input.cwd, markdownPlanArtifact)
|
|
566
|
+
.replaceAll(path.sep, "/"),
|
|
567
|
+
}
|
|
568
|
+
: baseBackendLayout;
|
|
508
569
|
try {
|
|
509
570
|
if (!pipeline)
|
|
510
571
|
throw new Error("missing backend-test pipeline id");
|
|
@@ -874,6 +935,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
874
935
|
effective = await materializeEffectiveBackendPytestCollection({
|
|
875
936
|
workspaceRoot: input.cwd,
|
|
876
937
|
initial,
|
|
938
|
+
layout: backendLayout,
|
|
877
939
|
});
|
|
878
940
|
}
|
|
879
941
|
else if (initial.status === "REPAIRABLE") {
|
|
@@ -933,6 +995,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
933
995
|
workspaceRoot: input.cwd,
|
|
934
996
|
initial,
|
|
935
997
|
final: finalFacts,
|
|
998
|
+
layout: backendLayout,
|
|
936
999
|
});
|
|
937
1000
|
}
|
|
938
1001
|
else {
|
|
@@ -995,6 +1058,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
995
1058
|
scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
|
|
996
1059
|
eligibility,
|
|
997
1060
|
eligibilityInputHashes,
|
|
1061
|
+
layout: backendLayout,
|
|
998
1062
|
});
|
|
999
1063
|
return {
|
|
1000
1064
|
ok: true,
|
|
@@ -1018,7 +1082,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1018
1082
|
else if (pipeline === "markdown-traceability") {
|
|
1019
1083
|
let report;
|
|
1020
1084
|
try {
|
|
1021
|
-
report = await validateBackendMarkdownTraceability(input.cwd);
|
|
1085
|
+
report = await validateBackendMarkdownTraceability(input.cwd, backendLayout);
|
|
1022
1086
|
}
|
|
1023
1087
|
catch (error) {
|
|
1024
1088
|
report = `# Backend Test Traceability\n\n## Status\n\nFAIL\n\n## Findings\n\n- Traceability validation could not complete: ${error instanceof Error ? error.message : String(error)}\n`;
|
|
@@ -1103,7 +1167,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1103
1167
|
}
|
|
1104
1168
|
else if (pipeline === "markdown-execute-html") {
|
|
1105
1169
|
const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
|
|
1106
|
-
await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
|
|
1170
|
+
await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness, backendLayout);
|
|
1107
1171
|
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
1108
1172
|
let eligibleItemIds = executionReadiness.eligibleItemIds;
|
|
1109
1173
|
// Plan D gap-fill executeScope=affected: when a gap plan exists in
|
|
@@ -1678,6 +1742,33 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1678
1742
|
}
|
|
1679
1743
|
}
|
|
1680
1744
|
async function executeBackendTestPipelineWithWriteGuard(input, meta) {
|
|
1745
|
+
if (meta.spec.backendTestWorkspaceControl === "filesystem-only") {
|
|
1746
|
+
const paths = input.task.writeSet ?? input.task.allowedPaths;
|
|
1747
|
+
let beforeSnapshot;
|
|
1748
|
+
try {
|
|
1749
|
+
beforeSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
|
|
1750
|
+
}
|
|
1751
|
+
catch (error) {
|
|
1752
|
+
return { ok: false, stdout: "", stderr: `workspace filesystem baseline unavailable: ${error instanceof Error ? error.message : String(error)}`, failureCategory: "write-guard", durationMs: 0 };
|
|
1753
|
+
}
|
|
1754
|
+
const result = await executeBackendTestPipeline(input, meta);
|
|
1755
|
+
try {
|
|
1756
|
+
const afterSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
|
|
1757
|
+
const guard = validateShellWriteGuard({
|
|
1758
|
+
changedPaths: diffWorkspaceWriteSnapshots(beforeSnapshot, afterSnapshot),
|
|
1759
|
+
writePolicy: input.task.writePolicy,
|
|
1760
|
+
writeSet: input.task.writeSet,
|
|
1761
|
+
allowedPaths: input.task.allowedPaths,
|
|
1762
|
+
forbiddenPaths: input.task.forbiddenPaths,
|
|
1763
|
+
});
|
|
1764
|
+
if (!guard.ok)
|
|
1765
|
+
return { ...result, ok: false, stderr: [result.stderr, `filesystem write guard failed: ${guard.violations.join(", ")}`].filter(Boolean).join("\n\n"), failureCategory: "write-guard" };
|
|
1766
|
+
return result;
|
|
1767
|
+
}
|
|
1768
|
+
catch (error) {
|
|
1769
|
+
return { ...result, ok: false, stderr: [result.stderr, `filesystem write guard failed: ${error instanceof Error ? error.message : String(error)}`].filter(Boolean).join("\n\n"), failureCategory: "write-guard" };
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1681
1772
|
let beforeStatus;
|
|
1682
1773
|
try {
|
|
1683
1774
|
beforeStatus = await readGitStatusPorcelain(input.cwd);
|
|
@@ -1740,6 +1831,45 @@ async function executePipelineCommands(input, meta, overrideCommands) {
|
|
|
1740
1831
|
}
|
|
1741
1832
|
return results;
|
|
1742
1833
|
}
|
|
1834
|
+
/** A+B (AC-008): derive unresolved verification entrypoints from committed plan
|
|
1835
|
+
* facts. A verification target is unresolved when its commandLabel does not
|
|
1836
|
+
* match any frozen command label from the verify shell evidence bundle.
|
|
1837
|
+
* Returns `{ ref, digest }` entries; a missing/unreadable plan ledger fails
|
|
1838
|
+
* closed to [] so the router restarts the contract instead of inventing refs. */
|
|
1839
|
+
async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandLabels) {
|
|
1840
|
+
try {
|
|
1841
|
+
const { restorePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
1842
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
|
|
1843
|
+
const patch = restorePlanPatchFromCommittedFacts(records);
|
|
1844
|
+
const targets = patch
|
|
1845
|
+
?.verificationTargets;
|
|
1846
|
+
if (!Array.isArray(targets))
|
|
1847
|
+
return [];
|
|
1848
|
+
const frozen = new Set(frozenCommandLabels
|
|
1849
|
+
.map((label) => label.trim().toLowerCase())
|
|
1850
|
+
.filter(Boolean));
|
|
1851
|
+
const refs = [];
|
|
1852
|
+
for (const item of targets) {
|
|
1853
|
+
const target = item;
|
|
1854
|
+
const commandLabel = typeof target.commandLabel === "string" ? target.commandLabel : "";
|
|
1855
|
+
if (commandLabel &&
|
|
1856
|
+
frozen.has(commandLabel.trim().toLowerCase())) {
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
const file = typeof target.file === "string" ? target.file : "";
|
|
1860
|
+
const symbol = typeof target.symbol === "string" ? target.symbol : "";
|
|
1861
|
+
const id = typeof target.id === "string" ? target.id : "";
|
|
1862
|
+
const ref = file || symbol || id;
|
|
1863
|
+
if (!ref)
|
|
1864
|
+
continue;
|
|
1865
|
+
refs.push({ ref, digest: sha256Hex(ref) });
|
|
1866
|
+
}
|
|
1867
|
+
return refs;
|
|
1868
|
+
}
|
|
1869
|
+
catch {
|
|
1870
|
+
return [];
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1743
1873
|
async function executeFrontendVerificationBundle(input, meta) {
|
|
1744
1874
|
const started = Date.now();
|
|
1745
1875
|
const shell = input.task.shell;
|
|
@@ -1894,75 +2024,505 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
1894
2024
|
catch (error) {
|
|
1895
2025
|
traceError = error instanceof Error ? error : new Error(String(error));
|
|
1896
2026
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2027
|
+
const verifyFailed = firstFailure !== undefined || lintSyntheticFailure !== undefined || traceError !== undefined;
|
|
2028
|
+
if (verifyFailed) {
|
|
2029
|
+
// M8: verify failure is TERMINAL. There is no same-run repair branch and
|
|
2030
|
+
// no frontend-repair-assessment.json; we only materialize failure
|
|
2031
|
+
// ownership facts (frontend-repair classification + recovery capsule
|
|
2032
|
+
// ownership) for the operation controller / recovery run.
|
|
2033
|
+
const failedNodeId = input.task.id;
|
|
2034
|
+
const failureCategory = firstFailure?.failureCategory ??
|
|
2035
|
+
lintSyntheticFailure?.failureCategory ??
|
|
2036
|
+
(traceError ? "invalid-output" : undefined) ??
|
|
2037
|
+
"invalid-output";
|
|
2038
|
+
const failureClass = classifyFrontendFailure({
|
|
2039
|
+
nodeId: failedNodeId,
|
|
2040
|
+
failureCategory,
|
|
2041
|
+
stdout: firstFailure?.stdout,
|
|
2042
|
+
stderr: firstFailure?.stderr ?? traceError?.message,
|
|
2043
|
+
});
|
|
2044
|
+
// A verify failure means the writer's candidate failed verification, so
|
|
2045
|
+
// the recovery ownership maps to the implement phase (frontend-recovery
|
|
2046
|
+
// capsule classification, fail-closed to implement-pi).
|
|
2047
|
+
const classifiedOwner = classifyFailureOwner({
|
|
2048
|
+
failureSource: "frontend-verify-shell",
|
|
2049
|
+
nodeId: failedNodeId,
|
|
2050
|
+
rawFailureCategory: failureCategory,
|
|
2051
|
+
});
|
|
2052
|
+
const failureOwner = classifiedOwner === "unknown" ? "implement-pi" : classifiedOwner;
|
|
2053
|
+
const restartPhase = classifyRestartPhase(failureOwner);
|
|
2054
|
+
// A+B (AC-008): verification-config routing. Produce structured
|
|
2055
|
+
// unresolved entrypoint refs from committed plan facts and the frozen
|
|
2056
|
+
// command labels; a non-empty unresolved set routes to
|
|
2057
|
+
// verification-config-decision and materializes the frozen
|
|
2058
|
+
// verification-config envelope. An empty/untyped signal fails closed to
|
|
2059
|
+
// contract-restart. The decision is recorded so the recovery controller
|
|
2060
|
+
// never re-classifies the same signal twice.
|
|
2061
|
+
let verificationConfigRoute;
|
|
2062
|
+
if (failureOwner === "verification-config") {
|
|
2063
|
+
const frozenCommandLabels = [
|
|
2064
|
+
...(bundle.staticEvidence.commandLabels ?? []),
|
|
2065
|
+
...(bundle.behaviorEvidence.commandLabels ?? []),
|
|
2066
|
+
...(bundle.mockEvidence?.commandLabels ?? []),
|
|
2067
|
+
];
|
|
2068
|
+
const unresolvedEntrypointRefs = await deriveUnresolvedVerificationEntrypointRefs(meta.runDir, frozenCommandLabels);
|
|
2069
|
+
verificationConfigRoute = routeVerificationConfig({
|
|
2070
|
+
unresolvedEntrypointRefs: unresolvedEntrypointRefs.map((ref) => ref.ref),
|
|
2071
|
+
});
|
|
2072
|
+
const allowlistedResolvers = [
|
|
2073
|
+
...new Set(frozenCommandLabels
|
|
2074
|
+
.map((label) => label.trim())
|
|
2075
|
+
.filter(Boolean)),
|
|
2076
|
+
].sort();
|
|
2077
|
+
if (verificationConfigRoute.decision ===
|
|
2078
|
+
"verification-config-decision" &&
|
|
2079
|
+
unresolvedEntrypointRefs.length > 0 &&
|
|
2080
|
+
allowlistedResolvers.length > 0) {
|
|
2081
|
+
const envelope = buildFrontendHumanDecisionEnvelope({
|
|
2082
|
+
operationId: input.task.id,
|
|
2083
|
+
runId: meta.runId,
|
|
2084
|
+
operationRevision: 1,
|
|
2085
|
+
targetDigest: sha256Hex(frozenCommandLabels.slice().sort().join("\n")),
|
|
2086
|
+
staleOnTargetDigestChange: true,
|
|
2087
|
+
kind: "verification-config",
|
|
2088
|
+
resumeAction: "select-verification-config",
|
|
2089
|
+
payload: {
|
|
2090
|
+
unresolvedEntrypointRefs,
|
|
2091
|
+
allowlistedResolvers,
|
|
2092
|
+
},
|
|
2093
|
+
});
|
|
2094
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verification-config-envelope.json", envelope);
|
|
2095
|
+
}
|
|
1912
2096
|
}
|
|
2097
|
+
const verifyFailure = {
|
|
2098
|
+
schemaVersion: 1,
|
|
2099
|
+
schemaId: "frontend-verify-failure-v1",
|
|
2100
|
+
nodeId: failedNodeId,
|
|
2101
|
+
failureCategory,
|
|
2102
|
+
failureClass,
|
|
2103
|
+
failureOwner,
|
|
2104
|
+
restartPhase,
|
|
2105
|
+
...(verificationConfigRoute
|
|
2106
|
+
? { verificationConfigRoute }
|
|
2107
|
+
: {}),
|
|
2108
|
+
commandResults,
|
|
2109
|
+
failedAt: new Date().toISOString(),
|
|
2110
|
+
};
|
|
2111
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verify-failure.json", verifyFailure);
|
|
1913
2112
|
return {
|
|
1914
|
-
ok:
|
|
1915
|
-
stdout:
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2113
|
+
ok: false,
|
|
2114
|
+
stdout: summarizeCommandResults(results).stdout,
|
|
2115
|
+
stderr: firstFailure?.stderr ||
|
|
2116
|
+
lintSyntheticFailure?.stderr ||
|
|
2117
|
+
traceError?.message ||
|
|
2118
|
+
"frontend verification failed",
|
|
2119
|
+
failureCategory,
|
|
1920
2120
|
durationMs: Date.now() - started,
|
|
1921
2121
|
...{ commandResults },
|
|
1922
2122
|
};
|
|
1923
2123
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
2124
|
+
// A+B (AC-008): when lint is `require-clean` but the assessment proves
|
|
2125
|
+
// baseline-debt, materialize the frozen `baseline-debt-acceptance` envelope
|
|
2126
|
+
// whose only positive resume is `accept-baseline-debt`. Evidence stays
|
|
2127
|
+
// `baseline-debt` and is never rewritten to `passed`.
|
|
2128
|
+
if (lintAssessment?.status === "baseline-debt") {
|
|
2129
|
+
const baselineEvidenceRefs = (lintAssessment.rawEvidenceRefs ?? [])
|
|
2130
|
+
.filter((ref) => ref?.path && ref?.sha256)
|
|
2131
|
+
.map((ref) => ({ ref: ref.path, digest: ref.sha256 }));
|
|
2132
|
+
if (baselineEvidenceRefs.length > 0) {
|
|
2133
|
+
const envelope = buildFrontendHumanDecisionEnvelope({
|
|
2134
|
+
operationId: input.task.id,
|
|
2135
|
+
runId: meta.runId,
|
|
2136
|
+
operationRevision: 1,
|
|
2137
|
+
targetDigest: lintAssessment.commandIdentity.sha256,
|
|
2138
|
+
staleOnTargetDigestChange: true,
|
|
2139
|
+
kind: "baseline-debt-acceptance",
|
|
2140
|
+
resumeAction: "accept-baseline-debt",
|
|
2141
|
+
payload: {
|
|
2142
|
+
baselineEvidenceRefs,
|
|
2143
|
+
candidateDigest: lintAssessment.commandIdentity.sha256,
|
|
2144
|
+
commandDigest: lintAssessment.commandIdentity.sha256,
|
|
2145
|
+
resultingPolicy: "allow-non-regression",
|
|
2146
|
+
},
|
|
2147
|
+
});
|
|
2148
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-baseline-debt-acceptance-envelope.json", envelope);
|
|
2149
|
+
}
|
|
1942
2150
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
}
|
|
2151
|
+
return {
|
|
2152
|
+
ok: true,
|
|
2153
|
+
stdout: lintAssessment?.status === "baseline-debt"
|
|
2154
|
+
? "Frontend verification bundle: pass with lint baseline-debt"
|
|
2155
|
+
: "Frontend verification bundle: pass",
|
|
2156
|
+
stderr: "",
|
|
2157
|
+
failureCategory: "success",
|
|
2158
|
+
durationMs: Date.now() - started,
|
|
2159
|
+
...{ commandResults },
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
/* ------------------------------------------------------------------ *
|
|
2163
|
+
* M8 deterministic frontend shell nodes: design policy, writer *
|
|
2164
|
+
* admission, closeout. These replace the prewrite-gate authorization *
|
|
2165
|
+
* path and the Pi closeout node. *
|
|
2166
|
+
* ------------------------------------------------------------------ */
|
|
2167
|
+
async function readRunDirJson(runDir, relativePath) {
|
|
2168
|
+
return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
|
|
2169
|
+
}
|
|
2170
|
+
/** Committed typed review terminal kinds from the review node's typed event
|
|
2171
|
+
* store (AC-003: typed terminal tools are the only authoritative verdict). */
|
|
2172
|
+
async function collectReviewTerminalKinds(runDir, reviewNodeId) {
|
|
2173
|
+
try {
|
|
2174
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
|
|
2175
|
+
return records
|
|
2176
|
+
.filter((record) => record.phase === "committed")
|
|
2177
|
+
.map((record) => {
|
|
2178
|
+
const kind = record.fact.kind;
|
|
2179
|
+
return typeof kind === "string" ? kind : "";
|
|
2180
|
+
})
|
|
2181
|
+
.filter(Boolean);
|
|
2182
|
+
}
|
|
2183
|
+
catch {
|
|
2184
|
+
return [];
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
/** Committed typed design terminal kinds from the design review node's typed
|
|
2188
|
+
* event store (M8: the committed approve_design / request_design_changes fact
|
|
2189
|
+
* is the only authoritative verdict; the legacy first-line VERDICT text is
|
|
2190
|
+
* never read for writer admission). Fail-closed on a missing/unreadable store. */
|
|
2191
|
+
async function collectDesignTerminalKinds(runDir, designNodeId) {
|
|
2192
|
+
try {
|
|
2193
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
|
|
2194
|
+
return records
|
|
2195
|
+
.filter((record) => record.phase === "committed")
|
|
2196
|
+
.map((record) => {
|
|
2197
|
+
const kind = record.fact.kind;
|
|
2198
|
+
return typeof kind === "string" ? kind : "";
|
|
2199
|
+
})
|
|
2200
|
+
.filter(Boolean);
|
|
2201
|
+
}
|
|
2202
|
+
catch {
|
|
2203
|
+
return [];
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
/** A+B (AC-009): deterministic §4.10 restart phase for a committed
|
|
2207
|
+
* `request_review_changes` fact. The typed issueCategory is mapped through the
|
|
2208
|
+
* frozen classifier; `unknown` (or an unreadable store) fails closed to
|
|
2209
|
+
* `undefined` so the runtime never trusts an untyped category. */
|
|
2210
|
+
async function collectReviewRestartPhase(runDir, reviewNodeId) {
|
|
2211
|
+
try {
|
|
2212
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
|
|
2213
|
+
const request = records.find((record) => record.phase === "committed" &&
|
|
2214
|
+
record.fact.kind === "request_review_changes");
|
|
2215
|
+
const issueCategory = request?.fact
|
|
2216
|
+
?.issueCategory;
|
|
2217
|
+
return typeof issueCategory === "string"
|
|
2218
|
+
? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
|
|
2219
|
+
: undefined;
|
|
2220
|
+
}
|
|
2221
|
+
catch {
|
|
2222
|
+
return undefined;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
/** A+B (AC-009): deterministic §4.10 restart phase for a committed
|
|
2226
|
+
* `request_design_changes` fact. Mirrors the review collector. */
|
|
2227
|
+
async function collectDesignRestartPhase(runDir, designNodeId) {
|
|
2228
|
+
try {
|
|
2229
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
|
|
2230
|
+
const request = records.find((record) => record.phase === "committed" &&
|
|
2231
|
+
record.fact.kind === "request_design_changes");
|
|
2232
|
+
const issueCategory = request?.fact
|
|
2233
|
+
?.issueCategory;
|
|
2234
|
+
return typeof issueCategory === "string"
|
|
2235
|
+
? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
|
|
2236
|
+
: undefined;
|
|
2237
|
+
}
|
|
2238
|
+
catch {
|
|
2239
|
+
return undefined;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* Resolve the deterministic runtime skeleton for the frontend plan patch from
|
|
2244
|
+
* the DAG spec. The plan task carries it via `structuredContractOutput.skeleton`
|
|
2245
|
+
* (the only schema-recognized persistent channel); absence is a fail-closed
|
|
2246
|
+
* generation/configuration defect, never a reason to fall back to text.
|
|
2247
|
+
*/
|
|
2248
|
+
function resolveFrontendContractSkeleton(spec, planNodeId) {
|
|
2249
|
+
const planTask = spec.tasks.find((task) => task.id === planNodeId);
|
|
2250
|
+
const skeleton = planTask?.structuredContractOutput?.skeleton;
|
|
2251
|
+
if (!skeleton || Object.keys(skeleton).length === 0) {
|
|
2252
|
+
throw new Error(`frontend design policy blocked: missing runtime contract skeleton for ${planNodeId}`);
|
|
2253
|
+
}
|
|
2254
|
+
return skeleton;
|
|
2255
|
+
}
|
|
2256
|
+
async function collectFrontendSourceFreshness(input) {
|
|
2257
|
+
const entries = [];
|
|
2258
|
+
for (const source of input.sourceBinding?.sources ?? []) {
|
|
2259
|
+
entries.push({ path: source.path, sha256: source.sha256, fresh: true });
|
|
2260
|
+
}
|
|
2261
|
+
for (const evidencePath of input.contract.designEvidence.paths) {
|
|
2262
|
+
if (entries.some((entry) => entry.path === evidencePath))
|
|
2263
|
+
continue;
|
|
2264
|
+
try {
|
|
2265
|
+
const bytes = await readFile(path.join(input.workspaceRoot, evidencePath));
|
|
2266
|
+
entries.push({
|
|
2267
|
+
path: evidencePath,
|
|
2268
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
2269
|
+
fresh: true,
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
catch {
|
|
2273
|
+
entries.push({
|
|
2274
|
+
path: evidencePath,
|
|
2275
|
+
sha256: "0".repeat(64),
|
|
2276
|
+
fresh: false,
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
return entries;
|
|
2281
|
+
}
|
|
2282
|
+
/** R3/R4 openspec invariants: frozen candidates must exist and content
|
|
2283
|
+
* snapshots must not have drifted. Citation is enforced upstream by the
|
|
2284
|
+
* prewrite gate read-event evidence and ledger specReference conformance. */
|
|
2285
|
+
async function checkFrontendOpenspecFacts(input) {
|
|
2286
|
+
const frozenCandidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
2287
|
+
const candidateRoot = input.workspaceRoot;
|
|
2288
|
+
const missingCandidates = [];
|
|
2289
|
+
for (const candidate of frozenCandidatePaths) {
|
|
2290
|
+
try {
|
|
2291
|
+
await stat(path.join(candidateRoot, candidate));
|
|
2292
|
+
}
|
|
2293
|
+
catch {
|
|
2294
|
+
missingCandidates.push(candidate);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
if (missingCandidates.length > 0) {
|
|
2298
|
+
throw new Error(`frontend design policy blocked: openspec candidate paths frozen at generation time no longer exist on disk: ${missingCandidates.join(", ")}`);
|
|
2299
|
+
}
|
|
2300
|
+
const snapshots = input.config.openspecCandidateSnapshots;
|
|
2301
|
+
if (snapshots && snapshots.length > 0) {
|
|
2302
|
+
for (const snapshot of snapshots) {
|
|
2303
|
+
try {
|
|
2304
|
+
const bytes = await readFile(path.join(candidateRoot, snapshot.path));
|
|
2305
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
2306
|
+
if (actual !== snapshot.sha256) {
|
|
2307
|
+
throw new Error(`frontend design policy blocked: openspec candidate content hash drifted: ${snapshot.path}`);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
catch (error) {
|
|
2311
|
+
if (error instanceof Error &&
|
|
2312
|
+
error.message.startsWith("frontend design policy blocked")) {
|
|
2313
|
+
throw error;
|
|
2314
|
+
}
|
|
2315
|
+
throw new Error(`frontend design policy blocked: openspec candidate unreadable: ${snapshot.path}`);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
1952
2318
|
}
|
|
2319
|
+
}
|
|
2320
|
+
async function executeFrontendDesignPolicy(input, meta) {
|
|
2321
|
+
const started = Date.now();
|
|
2322
|
+
const shell = input.task.shell;
|
|
2323
|
+
const config = shell.frontendDesignPolicy;
|
|
1953
2324
|
try {
|
|
1954
|
-
|
|
2325
|
+
// 1. Materialize the canonical contract from committed typed facts. After
|
|
2326
|
+
// M9a the plan node's assistantText is a pure Markdown narrative; the
|
|
2327
|
+
// compile authority is the committed plan ledger ⊕ runtime skeleton.
|
|
2328
|
+
// Fail closed on missing committed facts — never fall back to text.
|
|
2329
|
+
const { readCommittedOriginFacts, checkCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
2330
|
+
// A+B (AC-001/003): fail-closed committed contract/scout typed facts.
|
|
2331
|
+
// Missing/uncommitted facts block here; never fall back to text parsing.
|
|
2332
|
+
const committedContractFacts = await readCommittedOriginFacts(meta.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
|
|
2333
|
+
const contractCheck = checkCommittedOriginFacts({
|
|
2334
|
+
records: committedContractFacts,
|
|
2335
|
+
origin: "contract",
|
|
2336
|
+
});
|
|
2337
|
+
if (!contractCheck.ok) {
|
|
2338
|
+
return {
|
|
2339
|
+
ok: false,
|
|
2340
|
+
stdout: "",
|
|
2341
|
+
stderr: `frontend design policy blocked: ${contractCheck.reason}`,
|
|
2342
|
+
failureCategory: "invalid-output",
|
|
2343
|
+
durationMs: Date.now() - started,
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
const committedScoutFacts = await readCommittedOriginFacts(meta.runDir, "frontend-scout-pi", "scout-typed-facts.jsonl");
|
|
2347
|
+
const scoutCheck = checkCommittedOriginFacts({
|
|
2348
|
+
records: committedScoutFacts,
|
|
2349
|
+
origin: "scout",
|
|
2350
|
+
});
|
|
2351
|
+
if (!scoutCheck.ok) {
|
|
2352
|
+
return {
|
|
2353
|
+
ok: false,
|
|
2354
|
+
stdout: "",
|
|
2355
|
+
stderr: `frontend design policy blocked: ${scoutCheck.reason}`,
|
|
2356
|
+
failureCategory: "invalid-output",
|
|
2357
|
+
durationMs: Date.now() - started,
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
const skeleton = resolveFrontendContractSkeleton(meta.spec, config.planFromNodeId);
|
|
2361
|
+
const assembled = await buildContractFromCommittedFacts({
|
|
2362
|
+
runDir: meta.runDir,
|
|
2363
|
+
planNodeId: config.planFromNodeId,
|
|
2364
|
+
contractNodeId: "frontend-contract-pi",
|
|
2365
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
2366
|
+
skeleton,
|
|
2367
|
+
});
|
|
2368
|
+
if (!assembled.ok) {
|
|
2369
|
+
return {
|
|
2370
|
+
ok: false,
|
|
2371
|
+
stdout: "",
|
|
2372
|
+
stderr: `frontend design policy blocked: ${assembled.reason}`,
|
|
2373
|
+
failureCategory: "invalid-output",
|
|
2374
|
+
durationMs: Date.now() - started,
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
const contract = assembled.canonical;
|
|
2378
|
+
const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(contract), "utf8");
|
|
2379
|
+
if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
|
|
2380
|
+
return {
|
|
2381
|
+
ok: false,
|
|
2382
|
+
stdout: "",
|
|
2383
|
+
stderr: `frontend design policy blocked: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit`,
|
|
2384
|
+
failureCategory: "invalid-output",
|
|
2385
|
+
durationMs: Date.now() - started,
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
// 2. Requirement id retention (R11): search the canonical contract
|
|
2389
|
+
// serialization instead of the plan node narrative.
|
|
2390
|
+
const contractText = serializeDeterministicJson(contract);
|
|
2391
|
+
const missingIds = config.requiredRequirementIds.filter((id) => !contractText.includes(id));
|
|
2392
|
+
if (missingIds.length > 0) {
|
|
2393
|
+
return {
|
|
2394
|
+
ok: false,
|
|
2395
|
+
stdout: "",
|
|
2396
|
+
stderr: `frontend design policy blocked: missing requirement ids: ${missingIds.join(", ")}`,
|
|
2397
|
+
failureCategory: "invalid-output",
|
|
2398
|
+
durationMs: Date.now() - started,
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
// 3. Mock strategy allowed + frozen command binding (R1).
|
|
2402
|
+
const mockStrategy = contract.mockApi.strategy;
|
|
2403
|
+
if (!config.allowedMockStrategies.includes(mockStrategy)) {
|
|
2404
|
+
return {
|
|
2405
|
+
ok: false,
|
|
2406
|
+
stdout: "",
|
|
2407
|
+
stderr: `frontend contract mock strategy "${mockStrategy}" is incompatible with the generated allowed strategies [${config.allowedMockStrategies.join(", ")}]`,
|
|
2408
|
+
failureCategory: "invalid-output",
|
|
2409
|
+
durationMs: Date.now() - started,
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
if (mockStrategy !== "not-needed" && config.mockCommandLabels.length === 0) {
|
|
2413
|
+
return {
|
|
2414
|
+
ok: false,
|
|
2415
|
+
stdout: "",
|
|
2416
|
+
stderr: `frontend design policy blocked: contract selected Mock strategy ${mockStrategy} but the DAG has no authorized Mock verification commands`,
|
|
2417
|
+
failureCategory: "invalid-output",
|
|
2418
|
+
durationMs: Date.now() - started,
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
// 4. writeSet containment.
|
|
2422
|
+
if (config.implementationWriteSet) {
|
|
2423
|
+
const writeSet = new Set(config.implementationWriteSet);
|
|
2424
|
+
const uncoveredTargets = contract.targets.files.filter((target) => ![...writeSet].some((pattern) => pathMatchesPattern(target, pattern) || target === pattern));
|
|
2425
|
+
if (uncoveredTargets.length > 0) {
|
|
2426
|
+
return {
|
|
2427
|
+
ok: false,
|
|
2428
|
+
stdout: "",
|
|
2429
|
+
stderr: `frontend design policy blocked: contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`,
|
|
2430
|
+
failureCategory: "invalid-output",
|
|
2431
|
+
durationMs: Date.now() - started,
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
const uncoveredVTs = contract.verificationTargets
|
|
2435
|
+
.filter((vt) => vt.type !== "static")
|
|
2436
|
+
.filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
|
|
2437
|
+
if (uncoveredVTs.length > 0) {
|
|
2438
|
+
return {
|
|
2439
|
+
ok: false,
|
|
2440
|
+
stdout: "",
|
|
2441
|
+
stderr: `frontend design policy blocked: verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`,
|
|
2442
|
+
failureCategory: "invalid-output",
|
|
2443
|
+
durationMs: Date.now() - started,
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
// 5. Source freshness (R4) + freshness entries.
|
|
2448
|
+
if (config.requireSourceFreshness) {
|
|
2449
|
+
if (!meta.spec.sourceBinding) {
|
|
2450
|
+
throw new Error("frontend design policy requires sourceBinding for freshness check");
|
|
2451
|
+
}
|
|
2452
|
+
await assertFrontendSourceBindingFresh({
|
|
2453
|
+
workspaceRoot: input.cwd,
|
|
2454
|
+
binding: meta.spec.sourceBinding,
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
// Byte freshness is necessary but not sufficient: require the same ledger
|
|
2458
|
+
// semantic checks as the shadow prewrite gate before production writer
|
|
2459
|
+
// admission can be materialized.
|
|
2460
|
+
if (meta.spec.sourceBinding) {
|
|
2461
|
+
const ledgerCheck = await checkFrontendSourceFidelityLedger({
|
|
2462
|
+
workspaceRoot: input.cwd,
|
|
2463
|
+
binding: meta.spec.sourceBinding,
|
|
2464
|
+
contractRequirements: contract.requirements.map((requirement) => ({
|
|
2465
|
+
id: requirement.id,
|
|
2466
|
+
sourceFragmentIds: requirement.sourceFragmentIds,
|
|
2467
|
+
sourceRefs: requirement.sourceRefs,
|
|
2468
|
+
})),
|
|
2469
|
+
});
|
|
2470
|
+
if (!ledgerCheck.ok) {
|
|
2471
|
+
return {
|
|
2472
|
+
ok: false,
|
|
2473
|
+
stdout: "",
|
|
2474
|
+
stderr: `frontend design policy blocked: ${ledgerCheck.failureReason ?? "source fidelity ledger validation failed"}`,
|
|
2475
|
+
failureCategory: "invalid-output",
|
|
2476
|
+
durationMs: Date.now() - started,
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
const sourceFreshness = await collectFrontendSourceFreshness({
|
|
2481
|
+
workspaceRoot: input.cwd,
|
|
2482
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
2483
|
+
contract,
|
|
2484
|
+
});
|
|
2485
|
+
// 6. Openspec invariants (R3): frozen candidates exist and snapshots have
|
|
2486
|
+
// not drifted. Citation is superseded by prewrite-gate read evidence.
|
|
2487
|
+
await checkFrontendOpenspecFacts({
|
|
2488
|
+
workspaceRoot: input.cwd,
|
|
2489
|
+
config,
|
|
2490
|
+
});
|
|
2491
|
+
// 7. Deterministic design policy (M8 D2: designApproval is optional here;
|
|
2492
|
+
// the design verdict is enforced by the writer-admission shell).
|
|
2493
|
+
const policyResult = evaluateFrontendDesignPolicy({
|
|
2494
|
+
contract,
|
|
2495
|
+
allowedMockStrategies: config.allowedMockStrategies,
|
|
2496
|
+
sourceFreshness,
|
|
2497
|
+
componentSpecCandidatePaths: config.openspecCandidatePaths ??
|
|
2498
|
+
config.componentSpecCandidatePaths ??
|
|
2499
|
+
[],
|
|
2500
|
+
allowedDependencies: config.allowedDependencies,
|
|
2501
|
+
writeSetPatterns: config.implementationWriteSet ?? [],
|
|
2502
|
+
});
|
|
2503
|
+
// 8. Write canonical contract + policy result.
|
|
2504
|
+
await writeFrontendImplementationContractArtifact({
|
|
1955
2505
|
runDir: meta.runDir,
|
|
1956
|
-
|
|
2506
|
+
outputDir: config.outputDir,
|
|
2507
|
+
artifactName: config.artifactName,
|
|
2508
|
+
canonical: contract,
|
|
1957
2509
|
});
|
|
1958
|
-
await
|
|
2510
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-design-policy-result.json", policyResult);
|
|
2511
|
+
if (policyResult.classification === "blocked") {
|
|
2512
|
+
return {
|
|
2513
|
+
ok: false,
|
|
2514
|
+
stdout: "",
|
|
2515
|
+
stderr: `frontend design policy blocked: ${policyResult.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
|
|
2516
|
+
failureCategory: "invalid-output",
|
|
2517
|
+
durationMs: Date.now() - started,
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
1959
2520
|
return {
|
|
1960
2521
|
ok: true,
|
|
1961
|
-
stdout:
|
|
2522
|
+
stdout: `Frontend design policy: approved findings=0\nContract: ${path.resolve(meta.runDir, config.outputDir, config.artifactName)}`,
|
|
1962
2523
|
stderr: "",
|
|
1963
2524
|
failureCategory: "success",
|
|
1964
2525
|
durationMs: Date.now() - started,
|
|
1965
|
-
...{ commandResults },
|
|
1966
2526
|
};
|
|
1967
2527
|
}
|
|
1968
2528
|
catch (error) {
|
|
@@ -1972,30 +2532,163 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
1972
2532
|
stderr: error instanceof Error ? error.message : String(error),
|
|
1973
2533
|
failureCategory: "invalid-output",
|
|
1974
2534
|
durationMs: Date.now() - started,
|
|
1975
|
-
...{ commandResults },
|
|
1976
2535
|
};
|
|
1977
2536
|
}
|
|
1978
2537
|
}
|
|
1979
|
-
async function
|
|
2538
|
+
async function executeFrontendWriterAdmission(input, meta) {
|
|
1980
2539
|
const started = Date.now();
|
|
2540
|
+
const shell = input.task.shell;
|
|
2541
|
+
const config = shell.frontendWriterAdmission;
|
|
1981
2542
|
try {
|
|
1982
|
-
|
|
1983
|
-
|
|
2543
|
+
// 1. Design verdict pass is a hard admission condition (M8 D2). In small
|
|
2544
|
+
// topology the design review node is absent and the check is vacuous.
|
|
2545
|
+
// The verdict is read from the committed typed design terminal fact, not
|
|
2546
|
+
// from the legacy first-line VERDICT text protocol (AC-003/AC-004).
|
|
2547
|
+
let verdictPass = true;
|
|
2548
|
+
let designRestartPhase;
|
|
2549
|
+
if (config.designReviewFromNodeId) {
|
|
2550
|
+
const designKinds = await collectDesignTerminalKinds(meta.runDir, config.designReviewFromNodeId);
|
|
2551
|
+
verdictPass =
|
|
2552
|
+
designKinds.length === 1 && designKinds[0] === "approve_design";
|
|
2553
|
+
// A+B (AC-009): request_design_changes issueCategory maps to the
|
|
2554
|
+
// deterministic §4.10 restart phase; unknown fails closed to undefined.
|
|
2555
|
+
designRestartPhase = verdictPass
|
|
2556
|
+
? undefined
|
|
2557
|
+
: await collectDesignRestartPhase(meta.runDir, config.designReviewFromNodeId);
|
|
2558
|
+
}
|
|
2559
|
+
// 2. Read the canonical contract + design-policy result.
|
|
2560
|
+
const contractRaw = await readRunDirJson(meta.runDir, "contracts/frontend-implementation-contract.json");
|
|
2561
|
+
const contract = frontendImplementationContractSchema.parse(contractRaw);
|
|
2562
|
+
const policyRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-design-policy-result.json"));
|
|
2563
|
+
if (policyRaw.schemaId !== "frontend-design-policy-v1") {
|
|
2564
|
+
throw new Error("frontend writer admission: design policy result artifact invalid");
|
|
2565
|
+
}
|
|
2566
|
+
const designPolicyResult = {
|
|
2567
|
+
schemaId: "frontend-design-policy-v1",
|
|
2568
|
+
classification: policyRaw.classification === "approved" ? "approved" : "blocked",
|
|
2569
|
+
findings: policyRaw.findings ?? [],
|
|
2570
|
+
digest: "",
|
|
2571
|
+
};
|
|
2572
|
+
// 3. Freeze the pre-writer worktree baseline (cumulative diff baseline).
|
|
2573
|
+
await captureFrontendWorktreeBaseline({
|
|
2574
|
+
runDir: meta.runDir,
|
|
1984
2575
|
workspaceRoot: input.cwd,
|
|
1985
|
-
declaredAcIds,
|
|
1986
2576
|
});
|
|
1987
|
-
|
|
2577
|
+
const baselineRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-worktree-baseline.json"));
|
|
2578
|
+
const baseline = (baselineRaw.files ?? [])
|
|
2579
|
+
.map((file) => ({
|
|
2580
|
+
path: file,
|
|
2581
|
+
sha256: baselineRaw.hashes?.[file] ?? null,
|
|
2582
|
+
}))
|
|
2583
|
+
.filter((entry) => entry.sha256 !== null);
|
|
2584
|
+
// 4. Writer-preceding lint baseline (R5) when lint is configured.
|
|
2585
|
+
let lintResults = [];
|
|
2586
|
+
if ((config.lintCommands?.length ?? 0) > 0) {
|
|
2587
|
+
for (const command of config.lintCommands) {
|
|
2588
|
+
const commandNumber = lintResults.length + 1;
|
|
2589
|
+
const result = await executeShellCommand({
|
|
2590
|
+
command,
|
|
2591
|
+
cwd: input.cwd,
|
|
2592
|
+
timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
|
|
2593
|
+
envAllowlist: shell.envAllowlist,
|
|
2594
|
+
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
2595
|
+
reportActivity: input.reportActivity,
|
|
2596
|
+
outputArtifacts: {
|
|
2597
|
+
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
2598
|
+
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
2599
|
+
},
|
|
2600
|
+
});
|
|
2601
|
+
lintResults.push(result);
|
|
2602
|
+
}
|
|
2603
|
+
await materializeFrontendLintBaseline({
|
|
2604
|
+
runDir: meta.runDir,
|
|
2605
|
+
workspaceRoot: input.cwd,
|
|
2606
|
+
commands: config.lintCommands,
|
|
2607
|
+
results: lintResults,
|
|
2608
|
+
worktreeChanged: false,
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
// 5. Derive admission (writeSet + digests + findings). Glob entries in
|
|
2612
|
+
// the contract targets (e.g. `src/**` inherited from the skeleton) are
|
|
2613
|
+
// expanded against the concrete workspace files so the derived writeSet
|
|
2614
|
+
// contains only real paths the writer can touch.
|
|
2615
|
+
const workspaceFiles = await collectWorkspaceFiles(input.cwd);
|
|
2616
|
+
const admission = deriveFrontendWriterAdmissionShell({
|
|
2617
|
+
contract,
|
|
2618
|
+
designPolicyResult,
|
|
2619
|
+
baseline,
|
|
2620
|
+
frozenCommandLabels: config.frozenCommandLabels,
|
|
2621
|
+
allowedMockStrategies: config.allowedMockStrategies,
|
|
2622
|
+
workspaceFiles,
|
|
2623
|
+
});
|
|
2624
|
+
// 6. Design verdict enforcement: request-revision is a blocked admission.
|
|
2625
|
+
const failureSource = policyRaw.classification === "blocked"
|
|
2626
|
+
? "frontend-design-policy-shell"
|
|
2627
|
+
: "frontend-writer-admission-shell";
|
|
2628
|
+
const findings = verdictPass
|
|
2629
|
+
? admission.findings
|
|
2630
|
+
: [
|
|
2631
|
+
...admission.findings,
|
|
2632
|
+
{
|
|
2633
|
+
code: "design-approval-request-revision",
|
|
2634
|
+
message: designRestartPhase !== undefined
|
|
2635
|
+
? `design review typed terminal verdict is not approve_design (restartPhase=${designRestartPhase})`
|
|
2636
|
+
: "design review typed terminal verdict is not approve_design",
|
|
2637
|
+
},
|
|
2638
|
+
];
|
|
2639
|
+
const classification = findings.length === 0 && admission.classification === "accepted"
|
|
2640
|
+
? admission.classification
|
|
2641
|
+
: findings.length === 0 &&
|
|
2642
|
+
admission.classification === "requires-human-approval"
|
|
2643
|
+
? "requires-human-approval"
|
|
2644
|
+
: "blocked";
|
|
2645
|
+
const result = frontendWriterAdmissionResultV1Schema.parse({
|
|
2646
|
+
schemaVersion: 1,
|
|
2647
|
+
schemaId: "frontend-writer-admission-shell-v1",
|
|
2648
|
+
classification,
|
|
2649
|
+
writeSet: admission.writeSet,
|
|
2650
|
+
writeSetDigest: admission.writeSetDigest,
|
|
2651
|
+
baselineDigest: admission.baselineDigest,
|
|
2652
|
+
admissionDigest: admission.admissionDigest,
|
|
2653
|
+
...(admission.baselineDebtPolicy !== undefined
|
|
2654
|
+
? { baselineDebtPolicy: admission.baselineDebtPolicy }
|
|
2655
|
+
: {}),
|
|
2656
|
+
findings,
|
|
2657
|
+
failureSource,
|
|
2658
|
+
});
|
|
2659
|
+
await writeDagRunJsonArtifact(meta.runDir, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, result);
|
|
2660
|
+
// A+B (AC-007): High topology materializes a `high-admission-receipt-v1`
|
|
2661
|
+
// catalog whose candidate digest must match the admission digest before
|
|
2662
|
+
// the gate can transition from `requires-human-approval` to `accepted`.
|
|
2663
|
+
// Any digest drift resolves to `stale` (never silently accepted).
|
|
2664
|
+
if (classification === "requires-human-approval") {
|
|
2665
|
+
const receipt = evaluateHighAdmissionReceipt({
|
|
2666
|
+
candidateDigest: result.admissionDigest,
|
|
2667
|
+
admissionDigest: result.admissionDigest,
|
|
2668
|
+
});
|
|
2669
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-high-admission-receipt.json", receipt);
|
|
2670
|
+
if (receipt.status !== "accepted") {
|
|
2671
|
+
return {
|
|
2672
|
+
ok: false,
|
|
2673
|
+
stdout: "",
|
|
2674
|
+
stderr: `frontend writer admission stale: high-admission receipt candidate digest drifted from admission digest`,
|
|
2675
|
+
failureCategory: "invalid-output",
|
|
2676
|
+
durationMs: Date.now() - started,
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
if (classification === "blocked") {
|
|
1988
2681
|
return {
|
|
1989
2682
|
ok: false,
|
|
1990
2683
|
stdout: "",
|
|
1991
|
-
stderr: `frontend
|
|
2684
|
+
stderr: `frontend writer admission ${classification}: ${findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
|
|
1992
2685
|
failureCategory: "invalid-output",
|
|
1993
2686
|
durationMs: Date.now() - started,
|
|
1994
2687
|
};
|
|
1995
2688
|
}
|
|
1996
2689
|
return {
|
|
1997
2690
|
ok: true,
|
|
1998
|
-
stdout: `
|
|
2691
|
+
stdout: `Frontend writer admission: ${classification} writeSet=${result.writeSet.length} writeSetDigest=${result.writeSetDigest}`,
|
|
1999
2692
|
stderr: "",
|
|
2000
2693
|
failureCategory: "success",
|
|
2001
2694
|
durationMs: Date.now() - started,
|
|
@@ -2011,16 +2704,40 @@ async function executeFrontendTestCaseChecklist(input, meta) {
|
|
|
2011
2704
|
};
|
|
2012
2705
|
}
|
|
2013
2706
|
}
|
|
2014
|
-
async function
|
|
2707
|
+
async function executeFrontendContractImport(input, meta) {
|
|
2015
2708
|
const started = Date.now();
|
|
2709
|
+
const shell = input.task.shell;
|
|
2710
|
+
const config = shell.frontendContractImport;
|
|
2016
2711
|
try {
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
}
|
|
2712
|
+
const binding = meta.spec.sourceBinding;
|
|
2713
|
+
if (!binding) {
|
|
2714
|
+
throw new Error("frontend contract-import requires a DAG sourceBinding to re-validate freshness");
|
|
2715
|
+
}
|
|
2716
|
+
if (config.requireSourceFreshness) {
|
|
2717
|
+
await assertFrontendSourceBindingFresh({
|
|
2718
|
+
workspaceRoot: input.cwd,
|
|
2719
|
+
binding,
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
const digest = createHash("sha256")
|
|
2723
|
+
.update(JSON.stringify({
|
|
2724
|
+
taskId: binding.taskId,
|
|
2725
|
+
sources: binding.sources,
|
|
2726
|
+
artifactName: config.artifactName,
|
|
2727
|
+
}))
|
|
2728
|
+
.digest("hex");
|
|
2729
|
+
const facts = {
|
|
2730
|
+
schemaVersion: 1,
|
|
2731
|
+
schemaId: "frontend-contract-import-v1",
|
|
2732
|
+
artifactName: config.artifactName,
|
|
2733
|
+
sourceBindingTaskId: binding.taskId,
|
|
2734
|
+
sourceCount: binding.sources.length,
|
|
2735
|
+
fresh: true,
|
|
2736
|
+
digest,
|
|
2737
|
+
};
|
|
2021
2738
|
return {
|
|
2022
2739
|
ok: true,
|
|
2023
|
-
stdout:
|
|
2740
|
+
stdout: JSON.stringify(facts, null, 2),
|
|
2024
2741
|
stderr: "",
|
|
2025
2742
|
failureCategory: "success",
|
|
2026
2743
|
durationMs: Date.now() - started,
|
|
@@ -2036,12 +2753,185 @@ async function executeFrontendTestL5Report(input, meta) {
|
|
|
2036
2753
|
};
|
|
2037
2754
|
}
|
|
2038
2755
|
}
|
|
2039
|
-
async function
|
|
2756
|
+
async function executeFrontendCloseout(input, meta) {
|
|
2757
|
+
const started = Date.now();
|
|
2758
|
+
const shell = input.task.shell;
|
|
2759
|
+
const config = shell.frontendCloseout;
|
|
2760
|
+
try {
|
|
2761
|
+
const context = (await readRunDirJson(meta.runDir, "contracts/frontend-review-context.json"));
|
|
2762
|
+
const contract = frontendImplementationContractSchema.parse(context.contract);
|
|
2763
|
+
const trace = context.verificationTrace;
|
|
2764
|
+
const verificationTargets = (trace?.targets ?? []).map((target) => ({
|
|
2765
|
+
status: target.status === "ok" ? "ok" : "failed",
|
|
2766
|
+
}));
|
|
2767
|
+
const mockEvidenceBound = contract.mockApi.strategy !== "not-needed" &&
|
|
2768
|
+
typeof trace?.mockCommandNodeId === "string" &&
|
|
2769
|
+
trace.mockCommandNodeId.length > 0 &&
|
|
2770
|
+
Array.isArray(trace?.mockCommandLabels) &&
|
|
2771
|
+
trace.mockCommandLabels.length > 0 &&
|
|
2772
|
+
Array.isArray(trace?.mockCommandTexts) &&
|
|
2773
|
+
trace.mockCommandTexts.length === trace.mockCommandLabels.length;
|
|
2774
|
+
const integrationFacts = deriveFrontendIntegrationFacts({
|
|
2775
|
+
mockStrategy: contract.mockApi.strategy,
|
|
2776
|
+
realApiEvidence: { present: false, passed: false },
|
|
2777
|
+
mockEvidenceBound,
|
|
2778
|
+
verificationTargets,
|
|
2779
|
+
});
|
|
2780
|
+
// Typed review terminal (AC-003) from the committed typed event store.
|
|
2781
|
+
const typedKinds = await collectReviewTerminalKinds(meta.runDir, config.reviewFromNodeId);
|
|
2782
|
+
const typedVerdict = typedKinds.length === 1 &&
|
|
2783
|
+
(typedKinds[0] === "approve_review" || typedKinds[0] === "request_review_changes")
|
|
2784
|
+
? typedKinds[0]
|
|
2785
|
+
: null;
|
|
2786
|
+
// A+B (AC-009): request_review_changes issueCategory maps to the
|
|
2787
|
+
// deterministic §4.10 restart phase. Recorded as a run-owned audit
|
|
2788
|
+
// artifact so the frozen closeout schema is never widened; `unknown`
|
|
2789
|
+
// fails closed to no restart phase.
|
|
2790
|
+
const reviewRestartPhase = typedVerdict === "request_review_changes"
|
|
2791
|
+
? await collectReviewRestartPhase(meta.runDir, config.reviewFromNodeId)
|
|
2792
|
+
: undefined;
|
|
2793
|
+
if (reviewRestartPhase !== undefined) {
|
|
2794
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-review-restart-phase.json", {
|
|
2795
|
+
schemaVersion: 1,
|
|
2796
|
+
nodeId: config.reviewFromNodeId,
|
|
2797
|
+
restartPhase: reviewRestartPhase,
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
const facts = frontendCloseoutFactsSchema.parse({
|
|
2801
|
+
schemaVersion: 1,
|
|
2802
|
+
schemaId: "frontend-closeout-facts-v1",
|
|
2803
|
+
coverageMatrix: verificationTargets.map((target, index) => ({
|
|
2804
|
+
verificationTargetId: trace?.targets?.[index]?.id ?? `vt-${index}`,
|
|
2805
|
+
status: target.status === "ok" ? "passed" : "failed",
|
|
2806
|
+
})),
|
|
2807
|
+
lintStatus: "unavailable",
|
|
2808
|
+
integrationFacts,
|
|
2809
|
+
reviewApproval: {
|
|
2810
|
+
typedVerdict,
|
|
2811
|
+
legacyVerdict: null,
|
|
2812
|
+
match: true,
|
|
2813
|
+
},
|
|
2814
|
+
cumulativeDiff: {
|
|
2815
|
+
cumulativeDigest: (typeof context.verificationTrace === "object" &&
|
|
2816
|
+
context.verificationTrace !== null &&
|
|
2817
|
+
"schemaId" in context.verificationTrace
|
|
2818
|
+
? deterministicSha256({ verificationTrace: context.verificationTrace })
|
|
2819
|
+
: deterministicSha256({})),
|
|
2820
|
+
empty: false,
|
|
2821
|
+
},
|
|
2822
|
+
notRun: { browser: "not-run", visual: "not-run" },
|
|
2823
|
+
pendingRealIntegration: integrationFacts !== "real-api",
|
|
2824
|
+
});
|
|
2825
|
+
const rendered = renderFrontendCloseout(facts);
|
|
2826
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-closeout-facts.json", facts);
|
|
2827
|
+
// A+B (AC-010): materialize the deterministic frontend-test handoff.
|
|
2828
|
+
// browser/visual/runtime-a11y are always `not-run` and the handoff is
|
|
2829
|
+
// always `deferred-to-frontend-test`, so it never blocks implementation
|
|
2830
|
+
// and can never be rewritten into a passed result.
|
|
2831
|
+
const handoffTaskId = meta.spec.sourceBinding?.taskId ?? input.task.id;
|
|
2832
|
+
const sourceDigest = meta.spec.sourceBinding
|
|
2833
|
+
? createHash("sha256")
|
|
2834
|
+
.update(JSON.stringify({
|
|
2835
|
+
taskId: meta.spec.sourceBinding.taskId,
|
|
2836
|
+
sources: meta.spec.sourceBinding.sources,
|
|
2837
|
+
}))
|
|
2838
|
+
.digest("hex")
|
|
2839
|
+
: deterministicSha256({});
|
|
2840
|
+
const handoff = buildFrontendTestHandoff({
|
|
2841
|
+
taskId: handoffTaskId,
|
|
2842
|
+
sourceDigest,
|
|
2843
|
+
candidateHash: facts.cumulativeDiff.cumulativeDigest,
|
|
2844
|
+
});
|
|
2845
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-test-handoff.json", handoff);
|
|
2846
|
+
await writeDagNodeTextArtifact(meta.runDir, input.task.id, "frontend-closeout.md", rendered);
|
|
2847
|
+
return {
|
|
2848
|
+
ok: true,
|
|
2849
|
+
stdout: `Frontend closeout rendered\n${rendered.split("\n").slice(0, 8).join("\n")}`,
|
|
2850
|
+
stderr: "",
|
|
2851
|
+
failureCategory: "success",
|
|
2852
|
+
durationMs: Date.now() - started,
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
catch (error) {
|
|
2856
|
+
return {
|
|
2857
|
+
ok: false,
|
|
2858
|
+
stdout: "",
|
|
2859
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2860
|
+
failureCategory: "invalid-output",
|
|
2861
|
+
durationMs: Date.now() - started,
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
async function executeFrontendTestCaseChecklist(input, meta) {
|
|
2866
|
+
const started = Date.now();
|
|
2867
|
+
try {
|
|
2868
|
+
const layout = frontendTestLayoutFromSpec(meta.spec);
|
|
2869
|
+
const declaredAcIds = (meta.spec.sourceBinding?.requirementIds ?? []).filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
|
|
2870
|
+
const result = await validateFrontendCaseChecklist({
|
|
2871
|
+
workspaceRoot: input.cwd,
|
|
2872
|
+
declaredAcIds,
|
|
2873
|
+
layout,
|
|
2874
|
+
});
|
|
2875
|
+
if (result.issues.length) {
|
|
2876
|
+
return {
|
|
2877
|
+
ok: false,
|
|
2878
|
+
stdout: "",
|
|
2879
|
+
stderr: `frontend-test checklist blocked: ${JSON.stringify(result.issues)}`,
|
|
2880
|
+
failureCategory: "invalid-output",
|
|
2881
|
+
durationMs: Date.now() - started,
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
return {
|
|
2885
|
+
ok: true,
|
|
2886
|
+
stdout: `frontend-test checklist ok cases=${result.caseCount} source=${path.basename(result.manifestPath)}`,
|
|
2887
|
+
stderr: "",
|
|
2888
|
+
failureCategory: "success",
|
|
2889
|
+
durationMs: Date.now() - started,
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
catch (error) {
|
|
2893
|
+
return {
|
|
2894
|
+
ok: false,
|
|
2895
|
+
stdout: "",
|
|
2896
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2897
|
+
failureCategory: "invalid-output",
|
|
2898
|
+
durationMs: Date.now() - started,
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
async function executeFrontendTestL5Report(input, meta) {
|
|
2903
|
+
const started = Date.now();
|
|
2904
|
+
try {
|
|
2905
|
+
const output = await renderFrontendTestL5Report({
|
|
2906
|
+
workspaceRoot: input.cwd,
|
|
2907
|
+
runDir: meta.runDir,
|
|
2908
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2909
|
+
});
|
|
2910
|
+
return {
|
|
2911
|
+
ok: true,
|
|
2912
|
+
stdout: `Frontend L-5 report: ${output.htmlPath}\nMarkdown: ${output.markdownPath}\nStatus: ${output.metrics.status}`,
|
|
2913
|
+
stderr: "",
|
|
2914
|
+
failureCategory: "success",
|
|
2915
|
+
durationMs: Date.now() - started,
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
catch (error) {
|
|
2919
|
+
return {
|
|
2920
|
+
ok: false,
|
|
2921
|
+
stdout: "",
|
|
2922
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2923
|
+
failureCategory: "invalid-output",
|
|
2924
|
+
durationMs: Date.now() - started,
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
async function executeFrontendTestHtmlReport(input, meta) {
|
|
2040
2929
|
const started = Date.now();
|
|
2041
2930
|
try {
|
|
2042
2931
|
const output = await renderFrontendTestHtmlReport({
|
|
2043
2932
|
workspaceRoot: input.cwd,
|
|
2044
2933
|
runDir: meta.runDir,
|
|
2934
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2045
2935
|
});
|
|
2046
2936
|
return {
|
|
2047
2937
|
ok: true,
|
|
@@ -2061,11 +2951,60 @@ async function executeFrontendTestHtmlReport(input, meta) {
|
|
|
2061
2951
|
};
|
|
2062
2952
|
}
|
|
2063
2953
|
}
|
|
2064
|
-
async function
|
|
2954
|
+
async function executeFrontendTestStandardScenarios(input, meta) {
|
|
2955
|
+
const started = Date.now();
|
|
2956
|
+
try {
|
|
2957
|
+
const result = await copyFrontendTestStandardScenarios({
|
|
2958
|
+
workspaceRoot: input.cwd,
|
|
2959
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2960
|
+
});
|
|
2961
|
+
return {
|
|
2962
|
+
ok: true,
|
|
2963
|
+
stdout: JSON.stringify(result),
|
|
2964
|
+
stderr: "",
|
|
2965
|
+
failureCategory: "success",
|
|
2966
|
+
durationMs: Date.now() - started,
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
catch (error) {
|
|
2970
|
+
return {
|
|
2971
|
+
ok: false,
|
|
2972
|
+
stdout: "",
|
|
2973
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2974
|
+
failureCategory: "invalid-output",
|
|
2975
|
+
durationMs: Date.now() - started,
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
async function executeFrontendTestEnvironmentProbe(input, meta) {
|
|
2980
|
+
const started = Date.now();
|
|
2981
|
+
const result = await probeFrontendTestEnvironment({
|
|
2982
|
+
workspaceRoot: input.cwd,
|
|
2983
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2984
|
+
});
|
|
2985
|
+
if (result.ok) {
|
|
2986
|
+
return {
|
|
2987
|
+
ok: true,
|
|
2988
|
+
stdout: result.stdout,
|
|
2989
|
+
stderr: "",
|
|
2990
|
+
failureCategory: "success",
|
|
2991
|
+
durationMs: Date.now() - started,
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
return {
|
|
2995
|
+
ok: false,
|
|
2996
|
+
stdout: "",
|
|
2997
|
+
stderr: result.stderr,
|
|
2998
|
+
failureCategory: "nonzero-exit",
|
|
2999
|
+
durationMs: Date.now() - started,
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
async function executeFrontendTestEvidenceValidation(input, meta) {
|
|
2065
3003
|
const started = Date.now();
|
|
2066
3004
|
try {
|
|
2067
3005
|
const result = await validateFrontendCaseEvidence({
|
|
2068
3006
|
workspaceRoot: input.cwd,
|
|
3007
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2069
3008
|
});
|
|
2070
3009
|
const output = `frontend case evidence validation cases=${result.cases} findings=${result.issues.length}${result.issues.length ? ` issues=${JSON.stringify(result.issues)}` : ""}`;
|
|
2071
3010
|
if (result.hardFail) {
|
|
@@ -2104,6 +3043,7 @@ async function executeFrontendTestCaseManifest(input, meta) {
|
|
|
2104
3043
|
const checklist = await validateFrontendCaseChecklist({
|
|
2105
3044
|
workspaceRoot: input.cwd,
|
|
2106
3045
|
declaredAcIds,
|
|
3046
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2107
3047
|
});
|
|
2108
3048
|
if (checklist.issues.length) {
|
|
2109
3049
|
return {
|
|
@@ -2118,6 +3058,7 @@ async function executeFrontendTestCaseManifest(input, meta) {
|
|
|
2118
3058
|
workspaceRoot: input.cwd,
|
|
2119
3059
|
maxCases: gate.maxCases,
|
|
2120
3060
|
declaredAcIds,
|
|
3061
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2121
3062
|
});
|
|
2122
3063
|
return {
|
|
2123
3064
|
ok: true,
|
|
@@ -2156,6 +3097,7 @@ async function executeFrontendTestResultFinalize(input, meta) {
|
|
|
2156
3097
|
try {
|
|
2157
3098
|
const evidence = await validateFrontendCaseEvidence({
|
|
2158
3099
|
workspaceRoot: input.cwd,
|
|
3100
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2159
3101
|
});
|
|
2160
3102
|
advisoryStdout = `frontend case evidence validation cases=${evidence.cases} findings=${evidence.issues.length}${evidence.issues.length ? ` issues=${JSON.stringify(evidence.issues)}` : ""}`;
|
|
2161
3103
|
// Advisory findings must not block materialize; only hardFail (path escape) does.
|
|
@@ -2174,6 +3116,7 @@ async function executeFrontendTestResultFinalize(input, meta) {
|
|
|
2174
3116
|
artifactName: "frontend-test-result.json",
|
|
2175
3117
|
outputDir: "contracts",
|
|
2176
3118
|
sourceBinding: meta.spec.sourceBinding,
|
|
3119
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2177
3120
|
});
|
|
2178
3121
|
return {
|
|
2179
3122
|
ok: true,
|
|
@@ -2212,12 +3155,14 @@ async function executeFrontendTestReports(input, meta) {
|
|
|
2212
3155
|
const l5 = await renderFrontendTestL5Report({
|
|
2213
3156
|
workspaceRoot: input.cwd,
|
|
2214
3157
|
runDir: meta.runDir,
|
|
3158
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2215
3159
|
});
|
|
2216
3160
|
lines.push(`Frontend L-5 report: ${l5.htmlPath}\nMarkdown: ${l5.markdownPath}\nStatus: ${l5.metrics.status}`);
|
|
2217
3161
|
}
|
|
2218
3162
|
const report = await renderFrontendTestHtmlReport({
|
|
2219
3163
|
workspaceRoot: input.cwd,
|
|
2220
3164
|
runDir: meta.runDir,
|
|
3165
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2221
3166
|
});
|
|
2222
3167
|
lines.push(`Frontend test report: ${report.htmlPath}\nMarkdown: ${report.markdownPath}\nOutcome: ${report.outcome}\nCases: ${report.caseCount}`);
|
|
2223
3168
|
return {
|
|
@@ -2238,69 +3183,76 @@ async function executeFrontendTestReports(input, meta) {
|
|
|
2238
3183
|
};
|
|
2239
3184
|
}
|
|
2240
3185
|
}
|
|
2241
|
-
async function
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
|
|
3186
|
+
async function writeShellPresetProcessReceipt(input) {
|
|
3187
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
|
|
3188
|
+
schemaVersion: 1,
|
|
3189
|
+
kind: "shell-preset",
|
|
3190
|
+
preset: input.preset,
|
|
3191
|
+
ok: input.ok,
|
|
3192
|
+
durationMs: input.durationMs,
|
|
3193
|
+
steps: input.steps,
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
async function ensureShellPresetProcessReceipt(input) {
|
|
3197
|
+
const receiptPath = path.join(input.runDir, input.nodeId, "process-receipt.json");
|
|
3198
|
+
if (existsSync(receiptPath))
|
|
3199
|
+
return;
|
|
3200
|
+
if (existsSync(path.join(input.runDir, input.nodeId, "shell-command-receipt.json"))) {
|
|
3201
|
+
return;
|
|
2252
3202
|
}
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
3203
|
+
const detail = input.result.ok ? input.result.stdout : input.result.stderr;
|
|
3204
|
+
await writeShellPresetProcessReceipt({
|
|
3205
|
+
runDir: input.runDir,
|
|
3206
|
+
nodeId: input.nodeId,
|
|
3207
|
+
preset: input.preset,
|
|
3208
|
+
ok: input.result.ok,
|
|
3209
|
+
durationMs: input.result.durationMs,
|
|
3210
|
+
steps: [
|
|
3211
|
+
{
|
|
3212
|
+
id: "run",
|
|
3213
|
+
label: input.preset,
|
|
3214
|
+
ok: input.result.ok,
|
|
3215
|
+
detail: detail?.slice(0, 8 * 1024),
|
|
3216
|
+
durationMs: input.result.durationMs,
|
|
2265
3217
|
},
|
|
2266
|
-
|
|
2267
|
-
}
|
|
2268
|
-
let afterStatus;
|
|
2269
|
-
try {
|
|
2270
|
-
afterStatus = await readGitStatusPorcelain(input.cwd);
|
|
2271
|
-
}
|
|
2272
|
-
catch {
|
|
2273
|
-
afterStatus = undefined;
|
|
2274
|
-
}
|
|
2275
|
-
try {
|
|
2276
|
-
const materialized = await materializeFrontendLintBaseline({
|
|
2277
|
-
runDir: meta.runDir,
|
|
2278
|
-
workspaceRoot: input.cwd,
|
|
2279
|
-
commands: baseline.lintCommands,
|
|
2280
|
-
results,
|
|
2281
|
-
worktreeChanged: beforeStatus === undefined ||
|
|
2282
|
-
afterStatus === undefined ||
|
|
2283
|
-
beforeStatus !== afterStatus,
|
|
2284
|
-
});
|
|
2285
|
-
return {
|
|
2286
|
-
ok: true,
|
|
2287
|
-
stdout: `Frontend lint baseline: ${materialized.artifact.status}\nArtifact: ${materialized.ref.path}\nDiagnostics: ${materialized.artifact.diagnostics.length}`,
|
|
2288
|
-
stderr: materialized.artifact.reason ?? "",
|
|
2289
|
-
failureCategory: "success",
|
|
2290
|
-
durationMs: Date.now() - started,
|
|
2291
|
-
};
|
|
2292
|
-
}
|
|
2293
|
-
catch (error) {
|
|
2294
|
-
return {
|
|
2295
|
-
ok: false,
|
|
2296
|
-
stdout: "",
|
|
2297
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
2298
|
-
failureCategory: "invalid-output",
|
|
2299
|
-
durationMs: Date.now() - started,
|
|
2300
|
-
};
|
|
2301
|
-
}
|
|
3218
|
+
],
|
|
3219
|
+
});
|
|
2302
3220
|
}
|
|
2303
|
-
|
|
3221
|
+
function namedShellHandler(shell) {
|
|
3222
|
+
if (!shell)
|
|
3223
|
+
return null;
|
|
3224
|
+
const record = shell;
|
|
3225
|
+
for (const key of [
|
|
3226
|
+
"finalWriteSetApprovalGate",
|
|
3227
|
+
"frontendTestStandardScenarios",
|
|
3228
|
+
"frontendTestEnvironmentProbe",
|
|
3229
|
+
"frontendBrowserToolPreflight",
|
|
3230
|
+
"frontendPrewriteGate",
|
|
3231
|
+
"frontendLintBaseline",
|
|
3232
|
+
"frontendVerificationBundle",
|
|
3233
|
+
"frontendReviewContext",
|
|
3234
|
+
"frontendTestCaseChecklist",
|
|
3235
|
+
"frontendTestCaseManifest",
|
|
3236
|
+
"frontendTestEvidenceValidation",
|
|
3237
|
+
"frontendTestResultFinalize",
|
|
3238
|
+
"frontendTestL5Report",
|
|
3239
|
+
"frontendTestHtmlReport",
|
|
3240
|
+
"frontendTestReports",
|
|
3241
|
+
"backendTestPipeline",
|
|
3242
|
+
"jsonArtifactGate",
|
|
3243
|
+
"projectGovernanceGate",
|
|
3244
|
+
"requirementCoverageGate",
|
|
3245
|
+
"verdictGate",
|
|
3246
|
+
"verifyEvidence",
|
|
3247
|
+
"repairArtifactGate",
|
|
3248
|
+
"jacocoCoverage",
|
|
3249
|
+
]) {
|
|
3250
|
+
if (record[key] != null)
|
|
3251
|
+
return key;
|
|
3252
|
+
}
|
|
3253
|
+
return null;
|
|
3254
|
+
}
|
|
3255
|
+
async function executeDagShellNodeInner(input, meta) {
|
|
2304
3256
|
const shell = input.task.shell;
|
|
2305
3257
|
if (shell?.finalWriteSetApprovalGate) {
|
|
2306
3258
|
const started = Date.now();
|
|
@@ -2339,30 +3291,91 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2339
3291
|
};
|
|
2340
3292
|
}
|
|
2341
3293
|
}
|
|
3294
|
+
if (shell?.frontendTestStandardScenarios) {
|
|
3295
|
+
return executeFrontendTestStandardScenarios(input, meta);
|
|
3296
|
+
}
|
|
3297
|
+
if (shell?.frontendTestEnvironmentProbe) {
|
|
3298
|
+
return executeFrontendTestEnvironmentProbe(input, meta);
|
|
3299
|
+
}
|
|
2342
3300
|
if (shell?.frontendBrowserToolPreflight) {
|
|
2343
3301
|
const started = Date.now();
|
|
3302
|
+
const steps = [];
|
|
3303
|
+
const finish = async (result) => {
|
|
3304
|
+
await writeShellPresetProcessReceipt({
|
|
3305
|
+
runDir: meta.runDir,
|
|
3306
|
+
nodeId: input.task.id,
|
|
3307
|
+
preset: "frontendBrowserToolPreflight",
|
|
3308
|
+
ok: result.ok,
|
|
3309
|
+
durationMs: result.durationMs,
|
|
3310
|
+
steps: steps.length > 0
|
|
3311
|
+
? steps
|
|
3312
|
+
: [
|
|
3313
|
+
{
|
|
3314
|
+
id: "run",
|
|
3315
|
+
label: "浏览器工具能力探测",
|
|
3316
|
+
ok: result.ok,
|
|
3317
|
+
detail: result.ok ? result.stdout : result.stderr,
|
|
3318
|
+
durationMs: result.durationMs,
|
|
3319
|
+
},
|
|
3320
|
+
],
|
|
3321
|
+
});
|
|
3322
|
+
return result;
|
|
3323
|
+
};
|
|
2344
3324
|
try {
|
|
2345
3325
|
if (resolvePiBackend() === "cli-only") {
|
|
3326
|
+
steps.push({
|
|
3327
|
+
id: "pi-backend",
|
|
3328
|
+
label: "Pi SDK 后端可用",
|
|
3329
|
+
ok: false,
|
|
3330
|
+
detail: "CODE_AGENT_PI_BACKEND=cli-only",
|
|
3331
|
+
});
|
|
2346
3332
|
throw new Error("browser-command-capability-unavailable: CODE_AGENT_PI_BACKEND=cli-only rejects SDK-only playwright_cli");
|
|
2347
3333
|
}
|
|
3334
|
+
steps.push({
|
|
3335
|
+
id: "pi-backend",
|
|
3336
|
+
label: "Pi SDK 后端可用",
|
|
3337
|
+
ok: true,
|
|
3338
|
+
});
|
|
2348
3339
|
const sdkCapability = await checkPiSdkCustomToolCapability(input.cwd);
|
|
2349
3340
|
if (!sdkCapability.ok) {
|
|
3341
|
+
steps.push({
|
|
3342
|
+
id: "sdk-custom-tool",
|
|
3343
|
+
label: "Pi SDK custom-tool 能力",
|
|
3344
|
+
ok: false,
|
|
3345
|
+
detail: sdkCapability.detail,
|
|
3346
|
+
});
|
|
2350
3347
|
throw new Error(`browser-command-capability-unavailable: ${sdkCapability.detail}`);
|
|
2351
3348
|
}
|
|
3349
|
+
steps.push({
|
|
3350
|
+
id: "sdk-custom-tool",
|
|
3351
|
+
label: "Pi SDK custom-tool 能力",
|
|
3352
|
+
ok: true,
|
|
3353
|
+
});
|
|
3354
|
+
const layout = frontendTestLayoutFromSpec(meta.spec);
|
|
2352
3355
|
const toolProbe = await createPlaywrightCliTool({
|
|
2353
3356
|
repoRoot: input.cwd,
|
|
2354
3357
|
runDir: meta.runDir,
|
|
2355
3358
|
nodeId: input.task.id,
|
|
2356
3359
|
caseId: "frontend-browser-preflight",
|
|
2357
|
-
evidenceDir:
|
|
2358
|
-
// Capability-only registration probe. The tested URL is selected later by
|
|
2359
|
-
// retrieve-frontend-test-context-pi and validated by the environment shell.
|
|
3360
|
+
evidenceDir: `${layout.evidenceDir}/frontend-browser-preflight`,
|
|
2360
3361
|
baseUrl: "http://localhost:5173/",
|
|
2361
|
-
inputRoot:
|
|
3362
|
+
inputRoot: layout.fixturesDir,
|
|
3363
|
+
layout,
|
|
2362
3364
|
});
|
|
2363
3365
|
if (toolProbe.name !== "playwright_cli") {
|
|
3366
|
+
steps.push({
|
|
3367
|
+
id: "structured-tool",
|
|
3368
|
+
label: "注册 playwright_cli",
|
|
3369
|
+
ok: false,
|
|
3370
|
+
detail: toolProbe.name,
|
|
3371
|
+
});
|
|
2364
3372
|
throw new Error("browser-command-capability-unavailable: structured playwright_cli tool registration failed");
|
|
2365
3373
|
}
|
|
3374
|
+
steps.push({
|
|
3375
|
+
id: "structured-tool",
|
|
3376
|
+
label: "注册 playwright_cli",
|
|
3377
|
+
ok: true,
|
|
3378
|
+
});
|
|
2366
3379
|
const launcher = resolvePlaywrightCliLauncher(input.cwd);
|
|
2367
3380
|
const result = await defaultPlaywrightCliCommandRunner({
|
|
2368
3381
|
executable: launcher.executable,
|
|
@@ -2375,8 +3388,19 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2375
3388
|
if (result.exitCode !== 0 ||
|
|
2376
3389
|
result.timedOut ||
|
|
2377
3390
|
!["open", "close", "find", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
|
|
3391
|
+
steps.push({
|
|
3392
|
+
id: "playwright-cli-help",
|
|
3393
|
+
label: "playwright-cli --help 合同",
|
|
3394
|
+
ok: false,
|
|
3395
|
+
detail: help.slice(0, 8 * 1024),
|
|
3396
|
+
});
|
|
2378
3397
|
throw new Error("playwright-cli-contract-incompatible");
|
|
2379
3398
|
}
|
|
3399
|
+
steps.push({
|
|
3400
|
+
id: "playwright-cli-help",
|
|
3401
|
+
label: "playwright-cli --help 合同",
|
|
3402
|
+
ok: true,
|
|
3403
|
+
});
|
|
2380
3404
|
const capability = {
|
|
2381
3405
|
schemaVersion: 1,
|
|
2382
3406
|
status: "ready",
|
|
@@ -2387,54 +3411,35 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2387
3411
|
targetUrlAuthority: "retrieve-context-and-environment-probe",
|
|
2388
3412
|
};
|
|
2389
3413
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "frontend-browser-capability.json", capability);
|
|
2390
|
-
|
|
3414
|
+
steps.push({
|
|
3415
|
+
id: "capability-artifact",
|
|
3416
|
+
label: "写出 frontend-browser-capability.json",
|
|
3417
|
+
ok: true,
|
|
3418
|
+
detail: JSON.stringify(capability, null, 2),
|
|
3419
|
+
});
|
|
3420
|
+
return await finish({
|
|
2391
3421
|
ok: true,
|
|
2392
3422
|
stdout: JSON.stringify(capability),
|
|
2393
3423
|
stderr: "",
|
|
2394
3424
|
failureCategory: "success",
|
|
2395
3425
|
durationMs: Date.now() - started,
|
|
2396
|
-
};
|
|
3426
|
+
});
|
|
2397
3427
|
}
|
|
2398
3428
|
catch (error) {
|
|
2399
|
-
return {
|
|
3429
|
+
return await finish({
|
|
2400
3430
|
ok: false,
|
|
2401
3431
|
stdout: "",
|
|
2402
3432
|
stderr: `playwright-cli-unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
2403
3433
|
failureCategory: "tool-policy",
|
|
2404
3434
|
durationMs: Date.now() - started,
|
|
2405
|
-
};
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
if (shell?.frontendPrewriteGate) {
|
|
2409
|
-
const started = Date.now();
|
|
2410
|
-
try {
|
|
2411
|
-
const result = await runFrontendPrewriteGate({
|
|
2412
|
-
runDir: meta.runDir,
|
|
2413
|
-
config: shell.frontendPrewriteGate,
|
|
2414
|
-
sourceBinding: meta.spec.sourceBinding,
|
|
2415
|
-
workspaceRoot: input.cwd,
|
|
2416
|
-
repoRoot: input.cwd,
|
|
2417
3435
|
});
|
|
2418
|
-
return {
|
|
2419
|
-
ok: true,
|
|
2420
|
-
stdout: formatFrontendPrewriteGateStdout(result),
|
|
2421
|
-
stderr: "",
|
|
2422
|
-
failureCategory: "success",
|
|
2423
|
-
durationMs: Date.now() - started,
|
|
2424
|
-
};
|
|
2425
|
-
}
|
|
2426
|
-
catch (error) {
|
|
2427
|
-
return {
|
|
2428
|
-
ok: false,
|
|
2429
|
-
stdout: "",
|
|
2430
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
2431
|
-
failureCategory: "invalid-output",
|
|
2432
|
-
durationMs: Date.now() - started,
|
|
2433
|
-
};
|
|
2434
3436
|
}
|
|
2435
3437
|
}
|
|
2436
|
-
if (shell?.
|
|
2437
|
-
return
|
|
3438
|
+
if (shell?.frontendDesignPolicy) {
|
|
3439
|
+
return executeFrontendDesignPolicy(input, meta);
|
|
3440
|
+
}
|
|
3441
|
+
if (shell?.frontendWriterAdmission) {
|
|
3442
|
+
return executeFrontendWriterAdmission(input, meta);
|
|
2438
3443
|
}
|
|
2439
3444
|
if (shell?.frontendVerificationBundle) {
|
|
2440
3445
|
return executeFrontendVerificationBundle(input, meta);
|
|
@@ -2465,6 +3470,12 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2465
3470
|
};
|
|
2466
3471
|
}
|
|
2467
3472
|
}
|
|
3473
|
+
if (shell?.frontendContractImport) {
|
|
3474
|
+
return executeFrontendContractImport(input, meta);
|
|
3475
|
+
}
|
|
3476
|
+
if (shell?.frontendCloseout) {
|
|
3477
|
+
return executeFrontendCloseout(input, meta);
|
|
3478
|
+
}
|
|
2468
3479
|
if (shell?.frontendTestCaseChecklist) {
|
|
2469
3480
|
return executeFrontendTestCaseChecklist(input, meta);
|
|
2470
3481
|
}
|
|
@@ -2472,7 +3483,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2472
3483
|
return executeFrontendTestCaseManifest(input, meta);
|
|
2473
3484
|
}
|
|
2474
3485
|
if (shell?.frontendTestEvidenceValidation) {
|
|
2475
|
-
return executeFrontendTestEvidenceValidation(input);
|
|
3486
|
+
return executeFrontendTestEvidenceValidation(input, meta);
|
|
2476
3487
|
}
|
|
2477
3488
|
if (shell?.frontendTestResultFinalize) {
|
|
2478
3489
|
return executeFrontendTestResultFinalize(input, meta);
|
|
@@ -2579,6 +3590,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2579
3590
|
artifactName: gate.artifactName,
|
|
2580
3591
|
outputDir: gate.outputDir,
|
|
2581
3592
|
sourceBinding: meta.spec.sourceBinding,
|
|
3593
|
+
layout: frontendTestLayoutFromSpec(meta.spec),
|
|
2582
3594
|
});
|
|
2583
3595
|
break;
|
|
2584
3596
|
default:
|
|
@@ -2686,53 +3698,23 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2686
3698
|
}
|
|
2687
3699
|
if (shell?.commands?.length === 1 &&
|
|
2688
3700
|
shell.commands[0] === "frontend-failure-assess-gate") {
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
stdout: formatFrontendFailureAssessStdout(result),
|
|
2697
|
-
stderr: "",
|
|
2698
|
-
failureCategory: "success",
|
|
2699
|
-
durationMs: Date.now() - started,
|
|
2700
|
-
};
|
|
2701
|
-
}
|
|
2702
|
-
catch (error) {
|
|
2703
|
-
return {
|
|
2704
|
-
ok: false,
|
|
2705
|
-
stdout: "",
|
|
2706
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
2707
|
-
failureCategory: "invalid-output",
|
|
2708
|
-
durationMs: Date.now() - started,
|
|
2709
|
-
};
|
|
2710
|
-
}
|
|
3701
|
+
return {
|
|
3702
|
+
ok: false,
|
|
3703
|
+
stdout: "",
|
|
3704
|
+
stderr: "frontend-failure-assess-gate legacy command removed in M8: verify failure is terminal and materializes failure ownership facts in the verification bundle itself",
|
|
3705
|
+
failureCategory: "invalid-output",
|
|
3706
|
+
durationMs: 0,
|
|
3707
|
+
};
|
|
2711
3708
|
}
|
|
2712
3709
|
if (shell?.commands?.length === 1 &&
|
|
2713
3710
|
shell.commands[0] === "frontend-repair-contract-gate") {
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
stdout: formatFrontendRepairContractStdout(result.assessment),
|
|
2722
|
-
stderr: "",
|
|
2723
|
-
failureCategory: "success",
|
|
2724
|
-
durationMs: Date.now() - started,
|
|
2725
|
-
};
|
|
2726
|
-
}
|
|
2727
|
-
catch (error) {
|
|
2728
|
-
return {
|
|
2729
|
-
ok: false,
|
|
2730
|
-
stdout: "",
|
|
2731
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
2732
|
-
failureCategory: "invalid-output",
|
|
2733
|
-
durationMs: Date.now() - started,
|
|
2734
|
-
};
|
|
2735
|
-
}
|
|
3711
|
+
return {
|
|
3712
|
+
ok: false,
|
|
3713
|
+
stdout: "",
|
|
3714
|
+
stderr: "frontend-repair-contract-gate legacy command removed in M8: no same-run repair branch exists in the new topology",
|
|
3715
|
+
failureCategory: "invalid-output",
|
|
3716
|
+
durationMs: 0,
|
|
3717
|
+
};
|
|
2736
3718
|
}
|
|
2737
3719
|
if (shell?.projectGovernanceGate) {
|
|
2738
3720
|
const started = Date.now();
|
|
@@ -2824,6 +3806,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2824
3806
|
const cwd = resolveShellCwd(input.cwd, shell.cwd);
|
|
2825
3807
|
const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
|
|
2826
3808
|
const failFast = shell.failFast !== false;
|
|
3809
|
+
const attemptNumber = input.attempt ?? 1;
|
|
2827
3810
|
const results = [];
|
|
2828
3811
|
const executedCommandKeys = new Set();
|
|
2829
3812
|
let beforeStatus;
|
|
@@ -2919,8 +3902,8 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2919
3902
|
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
2920
3903
|
reportActivity: input.reportActivity,
|
|
2921
3904
|
outputArtifacts: {
|
|
2922
|
-
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
2923
|
-
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
3905
|
+
stdoutPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stdout.txt`),
|
|
3906
|
+
stderrPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stderr.txt`),
|
|
2924
3907
|
},
|
|
2925
3908
|
});
|
|
2926
3909
|
results.push(result);
|
|
@@ -2979,6 +3962,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2979
3962
|
schemaVersion: 1,
|
|
2980
3963
|
runId: meta.runId,
|
|
2981
3964
|
nodeId: input.task.id,
|
|
3965
|
+
attempt: attemptNumber,
|
|
2982
3966
|
workspaceFingerprint: workspaceFingerprint ?? null,
|
|
2983
3967
|
failFast,
|
|
2984
3968
|
commands: results.map((result) => ({
|
|
@@ -2997,6 +3981,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2997
3981
|
subchecks: parseCheckRepoSubcheckDurations(result),
|
|
2998
3982
|
})),
|
|
2999
3983
|
};
|
|
3984
|
+
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, path.posix.join(`attempt-${attemptNumber}`, "shell-command-receipt.json"), receipt);
|
|
3000
3985
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "shell-command-receipt.json", receipt);
|
|
3001
3986
|
const summaryMarkdown = buildShellResultSummaryMarkdown({
|
|
3002
3987
|
nodeId: input.task.id,
|
|
@@ -3048,8 +4033,57 @@ export async function executeDagShellNode(input, meta) {
|
|
|
3048
4033
|
stdout: `${recordPrefix}${reuseNote}${aggregate.stdout}`,
|
|
3049
4034
|
stderr: stderrParts.filter(Boolean).join("\n\n"),
|
|
3050
4035
|
failureCategory,
|
|
4036
|
+
...(firstFailure?.failureCategory
|
|
4037
|
+
? { rawFailureCategory: firstFailure.failureCategory }
|
|
4038
|
+
: {}),
|
|
3051
4039
|
durationMs: Date.now() - started,
|
|
3052
4040
|
// extra field consumed by node-execution if supported; also embedded in stdout marker
|
|
3053
4041
|
...{ commandResults },
|
|
3054
4042
|
};
|
|
3055
4043
|
}
|
|
4044
|
+
/**
|
|
4045
|
+
* Enumerate the workspace's concrete file paths for glob writeSet expansion.
|
|
4046
|
+
*
|
|
4047
|
+
* Uses `git ls-files -co --exclude-standard` (tracked + untracked, excluding
|
|
4048
|
+
* gitignored) so the expanded writeSet reflects real files the writer can
|
|
4049
|
+
* touch. Falls back to [] when the workspace is not a git repository or git
|
|
4050
|
+
* fails — in that case glob entries fail closed as non-concrete paths.
|
|
4051
|
+
*/
|
|
4052
|
+
async function collectWorkspaceFiles(workspaceRoot) {
|
|
4053
|
+
try {
|
|
4054
|
+
const files = await new Promise((resolve, reject) => {
|
|
4055
|
+
const child = spawn("git", ["ls-files", "-co", "--exclude-standard", "-z"], { cwd: workspaceRoot, stdio: ["ignore", "pipe", "ignore"] });
|
|
4056
|
+
const chunks = [];
|
|
4057
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
4058
|
+
child.on("error", reject);
|
|
4059
|
+
child.on("close", (code) => {
|
|
4060
|
+
if (code === 0) {
|
|
4061
|
+
resolve(Buffer.concat(chunks)
|
|
4062
|
+
.toString("utf8")
|
|
4063
|
+
.split("\0")
|
|
4064
|
+
.filter(Boolean));
|
|
4065
|
+
}
|
|
4066
|
+
else {
|
|
4067
|
+
reject(new Error(`git ls-files exited ${String(code)}`));
|
|
4068
|
+
}
|
|
4069
|
+
});
|
|
4070
|
+
});
|
|
4071
|
+
return files.map((file) => file.replace(/\\/g, "/")).sort();
|
|
4072
|
+
}
|
|
4073
|
+
catch {
|
|
4074
|
+
return [];
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
export async function executeDagShellNode(input, meta) {
|
|
4078
|
+
const handler = namedShellHandler(input.task.shell);
|
|
4079
|
+
const result = await executeDagShellNodeInner(input, meta);
|
|
4080
|
+
if (handler) {
|
|
4081
|
+
await ensureShellPresetProcessReceipt({
|
|
4082
|
+
runDir: meta.runDir,
|
|
4083
|
+
nodeId: input.task.id,
|
|
4084
|
+
preset: handler,
|
|
4085
|
+
result,
|
|
4086
|
+
});
|
|
4087
|
+
}
|
|
4088
|
+
return result;
|
|
4089
|
+
}
|