@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,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { access, readdir, readFile, realpath } from "node:fs/promises";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
6
|
import { assertValidDagSpec } from "./validate.js";
|
|
@@ -8,10 +8,11 @@ import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1,
|
|
|
8
8
|
import { bindDagRerunFeedback } from "./rerun-feedback.js";
|
|
9
9
|
import { planMavenVerification, } from "../../verification/maven/index.js";
|
|
10
10
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
11
|
-
import {
|
|
11
|
+
import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, DEFAULT_FRONTEND_SPEC_ROOTS, extractTaskSourceFrontendSpecPaths, } from "../../shared/openspec-spec.js";
|
|
12
|
+
import { readFrontendSpecRegistry, scoreFrontendSpecCandidate, } from "../../task/frontend-project-capability.js";
|
|
12
13
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
13
14
|
import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
|
|
14
|
-
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY,
|
|
15
|
+
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, FRONTEND_SCOUT_COMPLETENESS_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, FRONTEND_PLAN_LADDER_RETRY_POLICY, FRONTEND_REVIEW_TERMINAL_RETRY_POLICY, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY, isCanonicalFinalVerifyShellRetryCandidate, isSafeReadOnlyPiRetryCandidate, isTargetTemplateImplementPi, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
15
16
|
import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
|
|
16
17
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
17
18
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
@@ -19,22 +20,28 @@ import { mergeDocumentIndexCompanions } from "../../governance/document-index-cl
|
|
|
19
20
|
import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
|
|
20
21
|
import { applySddEmbeddedEnhancements, probeRepoLocalSddSkills, } from "./sdd-embedded.js";
|
|
21
22
|
import { discoverProjectGovernancePresence } from "./project-governance-context.js";
|
|
22
|
-
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
23
|
+
import { CANONICAL_TASK_ID_PATTERN, getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
23
24
|
import { materializeTaskReferenceDocs } from "../../task/source-references.js";
|
|
24
|
-
import { REQUIREMENT_FACT_ROLES } from "../../task/source-prepare/artifact-meta.js";
|
|
25
25
|
import { observeTaskContract } from "../../task/contract/observe.js";
|
|
26
|
+
import { extractRequirementFactsFromMarkdown } from "../../task/source-prepare/parse-intent.js";
|
|
27
|
+
import { computeLedgerInputDigest, parseLedgerJson, recoverLedgerInputContract, } from "../../task/source-prepare/ledger.js";
|
|
28
|
+
import { REQUIREMENT_LEDGER_FILE_NAME } from "../../task/contract/constants.js";
|
|
26
29
|
import { dagHasWriterExecution } from "./task-contract-binding.js";
|
|
27
30
|
import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
|
|
28
31
|
import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
29
32
|
import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
|
|
30
33
|
import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
|
|
31
34
|
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
35
|
+
import { applyFrontendTestLayoutToText, resolveFrontendTestLayout, } from "./frontend-test-layout.js";
|
|
32
36
|
import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
|
|
33
37
|
import { buildBackendTestIntakeContext } from "./backend-test-intake-context.js";
|
|
34
38
|
import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
|
|
35
39
|
import { classifyFrontendRisk, } from "./frontend-risk.js";
|
|
36
|
-
import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
|
|
37
|
-
import { buildFrontendImplementationContractSkeleton,
|
|
40
|
+
import { discoverFrontendProjectCapability, resolveFrontendSpecRootAliases, } from "./frontend-project-capability.js";
|
|
41
|
+
import { buildFrontendImplementationContractSkeleton, } from "./frontend-implementation-contract.js";
|
|
42
|
+
import { computeFrontendShapeSourceDigest, parseFrontendShapeTransitionCapsule, resolveFrontendTaskShape, } from "./frontend-shape.js";
|
|
43
|
+
import { discoverLatestCommittedFrontendShapeCapsule } from "./frontend-shape-capsule-store.js";
|
|
44
|
+
import { FRONTEND_NO_VERIFICATION_MARKER_TEXT } from "./frontend-verification-trace.js";
|
|
38
45
|
import { serializeDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
39
46
|
const REQUIREMENT_FILE = "需求.md";
|
|
40
47
|
const CONSTRAINT_FILE = "执行约束.md";
|
|
@@ -100,6 +107,13 @@ const FRONTEND_BOUNDED_IMPLEMENT_SKILLS = ["frontend-bounded-implement"];
|
|
|
100
107
|
const FRONTEND_DESIGN_REVIEW_SKILLS = ["frontend-design-review"];
|
|
101
108
|
const FRONTEND_REVIEW_SKILLS = ["frontend-review"];
|
|
102
109
|
const FRONTEND_VERIFICATION_SKILLS = ["frontend-verification"];
|
|
110
|
+
const FRONTEND_READ_BUDGETS = {
|
|
111
|
+
contract: { maxFiles: 24, maxBytes: 384 * 1024 },
|
|
112
|
+
scout: { maxFiles: 48, maxBytes: 768 * 1024 },
|
|
113
|
+
plan: { maxFiles: 32, maxBytes: 512 * 1024 },
|
|
114
|
+
designReview: { maxFiles: 16, maxBytes: 512 * 1024 },
|
|
115
|
+
finalReview: { maxFiles: 20, maxBytes: 320 * 1024 },
|
|
116
|
+
};
|
|
103
117
|
const IMPLEMENT_WRITESET_PLACEHOLDER = "REPLACE/WITH/NARROW/IMPLEMENT/PATHS/**";
|
|
104
118
|
/**
|
|
105
119
|
* Runtime contract stamped on every newly generated DAG. Capability fields are
|
|
@@ -216,6 +230,28 @@ async function hasDirectDependency(repoRoot, depName) {
|
|
|
216
230
|
return false;
|
|
217
231
|
}
|
|
218
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Collect the project's declared direct dependencies (dependencies +
|
|
235
|
+
* devDependencies names) from package.json. Used to freeze the frontend
|
|
236
|
+
* design-policy allowedDependencies set: the model's dependency policy must
|
|
237
|
+
* not be rejected for mentioning an already-declared dependency, and a
|
|
238
|
+
* dependency not present in the manifest is a genuine unauthorized addition.
|
|
239
|
+
* Returns [] when package.json is unreadable (no allowlist → the dependency
|
|
240
|
+
* check is skipped rather than rejecting prose tokens).
|
|
241
|
+
*/
|
|
242
|
+
async function collectDeclaredDependencies(repoRoot) {
|
|
243
|
+
try {
|
|
244
|
+
const raw = await readFile(path.join(repoRoot, "package.json"), "utf-8");
|
|
245
|
+
const pkg = JSON.parse(raw);
|
|
246
|
+
return [
|
|
247
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
248
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
219
255
|
/** Check whether handler/fixture/bootstrap files exist for known mock frameworks. */
|
|
220
256
|
async function discoverMockHandlerFiles(repoRoot, serviceRoot) {
|
|
221
257
|
const exactCandidates = [
|
|
@@ -1282,11 +1318,15 @@ function deriveParallelScoutPaths(taskConfig) {
|
|
|
1282
1318
|
: allowed,
|
|
1283
1319
|
};
|
|
1284
1320
|
}
|
|
1321
|
+
const FRONTEND_NO_STATIC_VERIFICATION_MARKER = `node -e "console.log('${FRONTEND_NO_VERIFICATION_MARKER_TEXT}; static/behavior verification not-run')"`;
|
|
1285
1322
|
/**
|
|
1286
1323
|
* Resolve frontend verification fallbacks from the target project's own
|
|
1287
1324
|
* package scripts. The DAG builder is also used by unit fixtures without a
|
|
1288
|
-
*
|
|
1289
|
-
*
|
|
1325
|
+
* repoRoot, so those fixtures retain the historical generic fallback. A real
|
|
1326
|
+
* project (repoRoot set) without a readable package.json has no scripts to
|
|
1327
|
+
* invoke; the generic fallback would only inject commands guaranteed to fail
|
|
1328
|
+
* at verify time, so such projects fall through to the tsc probe and may end
|
|
1329
|
+
* up with no fallback commands plus a generation-time advisory.
|
|
1290
1330
|
*/
|
|
1291
1331
|
async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
1292
1332
|
const genericFallback = {
|
|
@@ -1303,10 +1343,8 @@ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
|
1303
1343
|
}
|
|
1304
1344
|
}
|
|
1305
1345
|
catch {
|
|
1306
|
-
|
|
1346
|
+
// No readable manifest: leave scripts unset and keep probing.
|
|
1307
1347
|
}
|
|
1308
|
-
if (!scripts)
|
|
1309
|
-
return genericFallback;
|
|
1310
1348
|
let packageManager = "npm";
|
|
1311
1349
|
for (const [lockfile, manager] of [
|
|
1312
1350
|
["pnpm-lock.yaml", "pnpm"],
|
|
@@ -1383,19 +1421,66 @@ function toDagSourcePath(sources, absolutePath) {
|
|
|
1383
1421
|
absolutePath,
|
|
1384
1422
|
});
|
|
1385
1423
|
}
|
|
1386
|
-
|
|
1424
|
+
/**
|
|
1425
|
+
* Declaration-line shapes that assert a requirement id: list items
|
|
1426
|
+
* ("- AC-1: ..."), numbered/ordered acceptance entries ("1. AC-1 ...",
|
|
1427
|
+
* "1、AC-1"), table rows ("| AC-1 | ..."), and explicit key/value or
|
|
1428
|
+
* heading declarations ("AC-1:...", "id: AC-1"). Ids that merely appear
|
|
1429
|
+
* inside narrative prose (e.g. a fixture run id mentioned in background:
|
|
1430
|
+
* "夹具 run 2026-08-23-req-f03535aa 已存在") are incidental mentions, not
|
|
1431
|
+
* declared requirements, and must not pollute sourceBinding.requirementIds.
|
|
1432
|
+
*/
|
|
1433
|
+
const REQUIREMENT_ID_DECLARATION_LINE = /^\s*(?:[-+*]\s+|\d+[.、))]\s*|\|\s*)?(?:[-A-Z0-9]+\s*[::]\s*)?(?:\*\*)?(?:REQ|BR|AC)-/i;
|
|
1434
|
+
function isRequirementIdDeclarationLine(line) {
|
|
1435
|
+
return REQUIREMENT_ID_DECLARATION_LINE.test(line);
|
|
1436
|
+
}
|
|
1437
|
+
function extractExplicitRequirementIds(requirementMarkdown, ...fallbackMarkdown) {
|
|
1387
1438
|
const ids = [];
|
|
1388
1439
|
const seen = new Set();
|
|
1389
|
-
|
|
1440
|
+
const collect = (markdown, declarationsOnly) => {
|
|
1390
1441
|
if (!markdown)
|
|
1391
|
-
|
|
1392
|
-
|
|
1442
|
+
return;
|
|
1443
|
+
const source = declarationsOnly
|
|
1444
|
+
? markdown
|
|
1445
|
+
.split(/\r?\n/)
|
|
1446
|
+
.filter((line) => isRequirementIdDeclarationLine(line))
|
|
1447
|
+
.join("\n")
|
|
1448
|
+
: markdown;
|
|
1449
|
+
for (const match of source.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1393
1450
|
const id = match[0].toUpperCase();
|
|
1394
1451
|
if (!seen.has(id)) {
|
|
1395
1452
|
seen.add(id);
|
|
1396
1453
|
ids.push(id);
|
|
1397
1454
|
}
|
|
1398
1455
|
}
|
|
1456
|
+
};
|
|
1457
|
+
// The requirement document itself is human-authored acceptance prose:
|
|
1458
|
+
// only declaration lines declare requirements there. If the document has
|
|
1459
|
+
// no declaration-shaped lines at all (minimal free-form tasks like
|
|
1460
|
+
// "covers AC-001"), fall back to its full text so bindings never go
|
|
1461
|
+
// missing for unstructured input. References and constraints are bound
|
|
1462
|
+
// documents (acceptance yaml, analysis docs) whose ids are authoritative
|
|
1463
|
+
// wherever they appear — keep full scan for them.
|
|
1464
|
+
const declarationLines = requirementMarkdown
|
|
1465
|
+
.split(/\r?\n/)
|
|
1466
|
+
.filter((line) => isRequirementIdDeclarationLine(line));
|
|
1467
|
+
collect(requirementMarkdown, declarationLines.length > 0);
|
|
1468
|
+
for (const markdown of fallbackMarkdown) {
|
|
1469
|
+
if (markdown)
|
|
1470
|
+
collect(markdown, false);
|
|
1471
|
+
}
|
|
1472
|
+
return ids;
|
|
1473
|
+
}
|
|
1474
|
+
/** Extract ids from an explicit authoritative section (full scan inside it). */
|
|
1475
|
+
function extractSectionRequirementIds(sectionMarkdown) {
|
|
1476
|
+
const ids = [];
|
|
1477
|
+
const seen = new Set();
|
|
1478
|
+
for (const match of sectionMarkdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1479
|
+
const id = match[0].toUpperCase();
|
|
1480
|
+
if (!seen.has(id)) {
|
|
1481
|
+
seen.add(id);
|
|
1482
|
+
ids.push(id);
|
|
1483
|
+
}
|
|
1399
1484
|
}
|
|
1400
1485
|
return ids;
|
|
1401
1486
|
}
|
|
@@ -1409,7 +1494,7 @@ export function extractTaskScopedRequirementIds(requirementMarkdown, ...fallback
|
|
|
1409
1494
|
// Without it, preserve legacy full-scan behavior (requirement + references).
|
|
1410
1495
|
const sectionMatch = requirementMarkdown.match(/(?:^|\n)##\s*Acceptance References\s*\n([\s\S]*?)(?=\n##\s+|\n#\s+|$)/i);
|
|
1411
1496
|
if (sectionMatch?.[1]) {
|
|
1412
|
-
const fromSection =
|
|
1497
|
+
const fromSection = extractSectionRequirementIds(sectionMatch[1]);
|
|
1413
1498
|
if (fromSection.length > 0)
|
|
1414
1499
|
return fromSection;
|
|
1415
1500
|
}
|
|
@@ -1427,7 +1512,7 @@ export function extractTaskScopedRequirementIds(requirementMarkdown, ...fallback
|
|
|
1427
1512
|
.sort((a, b) => a - b);
|
|
1428
1513
|
return numbered.map((value) => `AC-${value}`);
|
|
1429
1514
|
}
|
|
1430
|
-
function
|
|
1515
|
+
function buildDagSourceBindingBase(sources) {
|
|
1431
1516
|
const sourceEntries = [
|
|
1432
1517
|
{
|
|
1433
1518
|
kind: "requirement",
|
|
@@ -1462,6 +1547,124 @@ function buildDagSourceBinding(sources) {
|
|
|
1462
1547
|
requirementIds: extractTaskScopedRequirementIds(sources.requirementMarkdown, sources.constraintMarkdown, ...(sources.referenceDocuments ?? []).map((reference) => reference.markdown)),
|
|
1463
1548
|
};
|
|
1464
1549
|
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Source binding v2(AC-001 / AC-FIDELITY-REPAIR-001):仅 frontend taskKind 且存在
|
|
1552
|
+
* 可解析 requirement/acceptance 引用时产出 schemaVersion=2 绑定(ledger 指针 +
|
|
1553
|
+
* requirement→fragment 映射全部必填)。绑定完全来自 task advance 已持久化的
|
|
1554
|
+
* source/requirement-ledger.json(intake 侧同一份受管账本的原始字节/hash/inputDigest),
|
|
1555
|
+
* 绝不从 materialized reference path 重建语义等价但 path/hash 不同的 ledger;
|
|
1556
|
+
* 缺失/损坏/漂移 fail closed(SOURCE_FIDELITY_LEDGER_MISSING / _STALE)。
|
|
1557
|
+
*/
|
|
1558
|
+
function buildDagSourceBinding(sources, taskKind) {
|
|
1559
|
+
const base = buildDagSourceBindingBase(sources);
|
|
1560
|
+
if (taskKind !== "frontend-implementation")
|
|
1561
|
+
return base;
|
|
1562
|
+
const ledgerBinding = loadPersistedFrontendSourceBindingLedger(sources);
|
|
1563
|
+
if (!ledgerBinding)
|
|
1564
|
+
return base;
|
|
1565
|
+
// AC-002 / AC-HARD-002:物化 REQ-SRC-* canonical id 确定性并入 v2 requirementIds。
|
|
1566
|
+
// 生成型 REQ-SRC-* id 无法从 markdown 文本导出(必须来自持久化 ledger),此处
|
|
1567
|
+
// 在 required requirement ids 侧确定性注入,确保 prewrite missing-requirement-ids
|
|
1568
|
+
// 不误伤(不要求文本可导出)也不漏检(物化 id 必须被契约覆盖)。
|
|
1569
|
+
const materializedRequirementIds = Object.keys(ledgerBinding.requirementToFragments)
|
|
1570
|
+
.filter((id) => /^REQ-SRC-/.test(id) && !base.requirementIds.includes(id))
|
|
1571
|
+
.sort();
|
|
1572
|
+
return {
|
|
1573
|
+
schemaVersion: 2,
|
|
1574
|
+
taskId: base.taskId,
|
|
1575
|
+
sources: base.sources,
|
|
1576
|
+
requirementIds: [
|
|
1577
|
+
...base.requirementIds,
|
|
1578
|
+
...materializedRequirementIds,
|
|
1579
|
+
],
|
|
1580
|
+
ledgerPath: ledgerBinding.ledgerPath,
|
|
1581
|
+
ledgerSha256: ledgerBinding.ledgerSha256,
|
|
1582
|
+
inputDigest: ledgerBinding.inputDigest,
|
|
1583
|
+
requirementToFragments: ledgerBinding.requirementToFragments,
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* 读取 Task Contract 已持久化的 source/requirement-ledger.json,核验字节 sha256
|
|
1588
|
+
* 与 inputDigest(与当前源输入 digest 一致)后产出 v2 绑定字段。缺失/损坏/漂移
|
|
1589
|
+
* 抛稳定错误(SOURCE_FIDELITY_LEDGER_MISSING / SOURCE_FIDELITY_LEDGER_STALE)。
|
|
1590
|
+
* 新鲜度侧不再"重新推导"权威输入,而是从持久化 ledger 恢复 build 侧的输入合同
|
|
1591
|
+
* (与 ledger build 使用完全相同的输入),避免 contract apply 重投影派生导航视图
|
|
1592
|
+
* 需求.md 导致重抽取漂移误判 stale(AC-DIGEST-001/002/003)。
|
|
1593
|
+
*/
|
|
1594
|
+
function loadPersistedFrontendSourceBindingLedger(sources) {
|
|
1595
|
+
const referenceDocs = (sources.referenceDocuments ?? []).filter((doc) => doc.markdown.trim().length > 0);
|
|
1596
|
+
if (referenceDocs.length === 0)
|
|
1597
|
+
return null;
|
|
1598
|
+
// 存在性守卫(保持既有 v1 回退行为):派生视图无可抽取 requirement/acceptance
|
|
1599
|
+
// 引用时不要求持久化 ledger,直接回退 base binding。此守卫只用于"是否 ledger
|
|
1600
|
+
// 任务"判定,绝不参与 digest 键计算——键输入完全来自持久化 ledger 恢复。
|
|
1601
|
+
const guardFacts = extractRequirementFactsFromMarkdown(sources.requirementMarkdown);
|
|
1602
|
+
if (guardFacts.acceptanceCriteria.length === 0)
|
|
1603
|
+
return null;
|
|
1604
|
+
const ledgerAbsolutePath = path.join(sources.taskDir, "source", REQUIREMENT_LEDGER_FILE_NAME);
|
|
1605
|
+
let raw;
|
|
1606
|
+
try {
|
|
1607
|
+
raw = readFileSync(ledgerAbsolutePath, "utf8");
|
|
1608
|
+
}
|
|
1609
|
+
catch (error) {
|
|
1610
|
+
throw new Error(`SOURCE_FIDELITY_LEDGER_MISSING: frontend task ${sources.taskId} has resolvable requirement/acceptance references but no persisted ${REQUIREMENT_LEDGER_FILE_NAME} at ${ledgerAbsolutePath}; run task advance (intake) to persist the ledger before dag init-hybrid (${error instanceof Error ? error.message : String(error)})`);
|
|
1611
|
+
}
|
|
1612
|
+
const ledgerSha256 = createHash("sha256").update(raw).digest("hex");
|
|
1613
|
+
let ledger;
|
|
1614
|
+
try {
|
|
1615
|
+
ledger = parseLedgerJson(raw);
|
|
1616
|
+
}
|
|
1617
|
+
catch (error) {
|
|
1618
|
+
throw new Error(`SOURCE_FIDELITY_LEDGER_MISSING: persisted ${REQUIREMENT_LEDGER_FILE_NAME} is not a valid v1 ledger: ${error instanceof Error ? error.message : String(error)}`);
|
|
1619
|
+
}
|
|
1620
|
+
// canonical 侧:ledger.canonicalRequirements 按 build 顺序原样保留输入 canonical
|
|
1621
|
+
// requirements(id/text 逐字节),过滤物化 REQ-SRC-*(AC-HARD-002 "物化不入键")
|
|
1622
|
+
// 即精确恢复 digest 输入集;派生导航视图 需求.md 投影不参与键计算。
|
|
1623
|
+
const contract = recoverLedgerInputContract(ledger);
|
|
1624
|
+
if (contract.canonicalRequirements.length === 0) {
|
|
1625
|
+
// AC-HARD-003:存在 v2 ledger 但 canonical requirements 为空时 fail closed,
|
|
1626
|
+
// 绝不回退 v1 base 绑定(buildDagSourceBinding 不降级)。空 canonical 的 ledger
|
|
1627
|
+
// 无法提供 requirement→fragment 映射证据,任何 v1 回退都会静默放行 fail-open。
|
|
1628
|
+
throw new Error(`SOURCE_FIDELITY_LEDGER_STALE: persisted ${REQUIREMENT_LEDGER_FILE_NAME} has an empty canonical requirement set; refusing to fall back to a v1 base binding; re-run task advance (intake) to rebuild the ledger`);
|
|
1629
|
+
}
|
|
1630
|
+
// source 侧:ledger.sourcePaths(build 时权威 sourceDocuments 的 repo-root
|
|
1631
|
+
// relative POSIX 路径集合)作为权威身份集合,对当前 materialized reference 文件
|
|
1632
|
+
// 做交集过滤;内容取当前字节(漂移检测)。权威路径缺失(删除/清空/重命名)→
|
|
1633
|
+
// fail-closed STALE;新增的非权威 role 文件被正确排除(不入键)。
|
|
1634
|
+
const currentByPath = new Map();
|
|
1635
|
+
for (const doc of referenceDocs) {
|
|
1636
|
+
currentByPath.set(toDagSourcePath(sources, doc.path), doc.markdown);
|
|
1637
|
+
}
|
|
1638
|
+
const missingAuthoritativePaths = contract.sourcePaths.filter((sourcePath) => !currentByPath.has(sourcePath));
|
|
1639
|
+
if (missingAuthoritativePaths.length > 0) {
|
|
1640
|
+
throw new Error(`SOURCE_FIDELITY_LEDGER_STALE: persisted ${REQUIREMENT_LEDGER_FILE_NAME} was built from authoritative source documents no longer present (${missingAuthoritativePaths.join(", ")}); re-run task advance (intake) to rebuild the ledger`);
|
|
1641
|
+
}
|
|
1642
|
+
// inputDigest 新鲜度:与当前源输入(canonical repo-root relative identity)一致。
|
|
1643
|
+
// digest 只绑定语义权威输入(规范化源文档 + canonical requirements +
|
|
1644
|
+
// schema/reconciler version);派生导航视图 需求.md 投影不参与键计算。
|
|
1645
|
+
const expectedDigest = computeLedgerInputDigest({
|
|
1646
|
+
sourceDocuments: contract.sourcePaths.map((sourcePath) => ({
|
|
1647
|
+
path: sourcePath,
|
|
1648
|
+
content: currentByPath.get(sourcePath),
|
|
1649
|
+
})),
|
|
1650
|
+
canonicalRequirements: contract.canonicalRequirements,
|
|
1651
|
+
});
|
|
1652
|
+
if (ledger.inputDigest !== expectedDigest) {
|
|
1653
|
+
throw new Error(`SOURCE_FIDELITY_LEDGER_STALE: persisted ${REQUIREMENT_LEDGER_FILE_NAME} inputDigest ${ledger.inputDigest} does not match current source input ${expectedDigest}; re-run task advance (intake) to rebuild the ledger`);
|
|
1654
|
+
}
|
|
1655
|
+
const requirementToFragments = {};
|
|
1656
|
+
for (const requirement of ledger.canonicalRequirements) {
|
|
1657
|
+
if (requirement.sourceFragmentIds.length > 0) {
|
|
1658
|
+
requirementToFragments[requirement.id] = requirement.sourceFragmentIds;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return {
|
|
1662
|
+
ledgerPath: toDagSourcePath(sources, ledgerAbsolutePath),
|
|
1663
|
+
ledgerSha256,
|
|
1664
|
+
inputDigest: ledger.inputDigest,
|
|
1665
|
+
requirementToFragments,
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1465
1668
|
function buildBackendTestAnalysisSourceBindingContract(sources) {
|
|
1466
1669
|
const binding = buildDagSourceBinding(sources);
|
|
1467
1670
|
const requirement = binding.sources.find((source) => source.kind === "requirement");
|
|
@@ -1478,7 +1681,10 @@ function buildBackendTestAnalysisSourceBindingContract(sources) {
|
|
|
1478
1681
|
requirementIds: binding.requirementIds,
|
|
1479
1682
|
};
|
|
1480
1683
|
}
|
|
1481
|
-
function buildSourceContextBlock(sources) {
|
|
1684
|
+
function buildSourceContextBlock(sources, options = {}) {
|
|
1685
|
+
const includeRequirementExcerpt = options.includeRequirementExcerpt ?? true;
|
|
1686
|
+
const includeConstraintExcerpt = options.includeConstraintExcerpt ?? true;
|
|
1687
|
+
const includeReferenceDocuments = options.includeReferenceDocuments ?? true;
|
|
1482
1688
|
const requirementRef = toDagSourcePath(sources, sources.requirementPath);
|
|
1483
1689
|
const requirementExcerpt = excerptMarkdown(sources.requirementMarkdown, {
|
|
1484
1690
|
sourceRef: requirementRef,
|
|
@@ -1487,7 +1693,7 @@ function buildSourceContextBlock(sources) {
|
|
|
1487
1693
|
const parts = [
|
|
1488
1694
|
`## Task source: 需求.md`,
|
|
1489
1695
|
`Bound readPath (use for Pi read-tool calls): ${requirementRef}`,
|
|
1490
|
-
requirementExcerpt.text,
|
|
1696
|
+
...(includeRequirementExcerpt ? [requirementExcerpt.text] : []),
|
|
1491
1697
|
];
|
|
1492
1698
|
if (sources.constraintMarkdown) {
|
|
1493
1699
|
const constraintRef = toDagSourcePath(sources, sources.constraintPath);
|
|
@@ -1495,34 +1701,20 @@ function buildSourceContextBlock(sources) {
|
|
|
1495
1701
|
sourceRef: constraintRef,
|
|
1496
1702
|
});
|
|
1497
1703
|
boundReadPaths.push(`- constraints: ${constraintRef}`);
|
|
1498
|
-
parts.push("## Task source: 执行约束.md", `Bound readPath (use for Pi read-tool calls): ${constraintRef}`, constraintExcerpt.text);
|
|
1704
|
+
parts.push("## Task source: 执行约束.md", `Bound readPath (use for Pi read-tool calls): ${constraintRef}`, ...(includeConstraintExcerpt ? [constraintExcerpt.text] : []));
|
|
1499
1705
|
}
|
|
1500
|
-
for (const reference of (
|
|
1706
|
+
for (const reference of (includeReferenceDocuments
|
|
1707
|
+
? sources.referenceDocuments ?? []
|
|
1708
|
+
: []).slice(0, MAX_INLINE_SOURCE_REFERENCE_DOCUMENTS)) {
|
|
1501
1709
|
const relativePath = path
|
|
1502
1710
|
.relative(path.join(sources.taskDir, "source"), reference.path)
|
|
1503
1711
|
.replaceAll(path.sep, "/");
|
|
1504
1712
|
const referenceRef = toDagSourcePath(sources, reference.path);
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const referenceExcerpt = isFactRole
|
|
1511
|
-
? {
|
|
1512
|
-
text: reference.markdown.trim(),
|
|
1513
|
-
truncated: false,
|
|
1514
|
-
originalChars: reference.markdown.trim().length,
|
|
1515
|
-
maxChars: Number.POSITIVE_INFINITY,
|
|
1516
|
-
}
|
|
1517
|
-
: excerptMarkdown(reference.markdown, {
|
|
1518
|
-
sourceRef: referenceRef,
|
|
1519
|
-
});
|
|
1520
|
-
boundReadPaths.push(`- reference ${relativePath}${isFactRole ? " (full source)" : ""}: ${referenceRef}`);
|
|
1521
|
-
parts.push(`## Task source reference: ${relativePath}`, `Bound readPath (use for Pi read-tool calls): ${referenceRef}`, ...(isFactRole
|
|
1522
|
-
? [
|
|
1523
|
-
`Full source injected (role: ${reference.role}) — complete and authoritative; no excerpt truncation applied.`,
|
|
1524
|
-
]
|
|
1525
|
-
: []), referenceExcerpt.text);
|
|
1713
|
+
const referenceExcerpt = excerptMarkdown(reference.markdown, {
|
|
1714
|
+
sourceRef: referenceRef,
|
|
1715
|
+
});
|
|
1716
|
+
boundReadPaths.push(`- reference ${relativePath}: ${referenceRef}`);
|
|
1717
|
+
parts.push(`## Task source reference: ${relativePath}`, `Bound readPath (use for Pi read-tool calls): ${referenceRef}`, referenceExcerpt.text);
|
|
1526
1718
|
}
|
|
1527
1719
|
parts.push("## Bound source read paths", ...boundReadPaths, "Use these repository-readable paths for any Pi read-tool calls. Bound files under `.harness/tasks/<taskId>/source/**` are read-only inputs: reading them is allowed even though writing `.harness/**` is forbidden.", "Never resolve task-relative citations such as `source/需求.md` or `source/references/*` against the repository root, invent `source/<taskId>/...`, search for substitutes, or fall back to `docs/**` when a bound read fails.", "## Task config summary", `- taskId: ${sources.taskConfig.taskId}`, `- flow: ${sources.taskConfig.flow}`, `- complexity: ${sources.taskConfig.complexity}`, `- contextProfile: ${sources.taskConfig.contextProfile}`, `- allowedPaths: ${sources.taskConfig.allowedPaths.join(", ") || "(none — review before execute)"}`, `- forbiddenPaths: ${sources.taskConfig.forbiddenPaths.join(", ") || "(none)"}`, '- Pi DAG nodes are read-only unless toolProfile="write" is explicitly selected for a bounded writer node.', "- Agent DAG read-only nodes must not write root artifacts/**; root artifacts/ is not a per-node scratchpad.", `- Derived execution contract and immutable references live under the Bound source read paths above (not as repo-root \`source/...\`).`);
|
|
1528
1720
|
if (sources.taskConfig.hardConstraints.length > 0) {
|
|
@@ -1560,49 +1752,10 @@ async function loadMaterializedSourceReferences(sourceDir) {
|
|
|
1560
1752
|
}
|
|
1561
1753
|
await collect(referenceDir);
|
|
1562
1754
|
referencePaths.sort((left, right) => left.localeCompare(right));
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
.replaceAll(path.sep, "/");
|
|
1568
|
-
const role = roleByPath.get(relative);
|
|
1569
|
-
return {
|
|
1570
|
-
path: filePath,
|
|
1571
|
-
markdown: await readFile(filePath, "utf-8"),
|
|
1572
|
-
...(role !== undefined ? { role } : {}),
|
|
1573
|
-
};
|
|
1574
|
-
}));
|
|
1575
|
-
}
|
|
1576
|
-
/**
|
|
1577
|
-
* Reads `source-manifest.json` into a materializedPath → role map so the DAG
|
|
1578
|
-
* generator can tell fact-source references (requirement/acceptance) apart
|
|
1579
|
-
* from archival ones (analysis/clarification/design). A missing or malformed
|
|
1580
|
-
* manifest yields an empty map; such references keep the bounded-excerpt
|
|
1581
|
-
* treatment instead of being injected in full.
|
|
1582
|
-
*/
|
|
1583
|
-
async function loadReferenceRoles(sourceDir) {
|
|
1584
|
-
const roleByPath = new Map();
|
|
1585
|
-
const manifestPath = path.join(sourceDir, "source-manifest.json");
|
|
1586
|
-
let raw;
|
|
1587
|
-
try {
|
|
1588
|
-
raw = await readFile(manifestPath, "utf-8");
|
|
1589
|
-
}
|
|
1590
|
-
catch {
|
|
1591
|
-
return roleByPath;
|
|
1592
|
-
}
|
|
1593
|
-
try {
|
|
1594
|
-
const manifest = JSON.parse(raw);
|
|
1595
|
-
for (const document of manifest.documents ?? []) {
|
|
1596
|
-
if (typeof document.materializedPath === "string" &&
|
|
1597
|
-
typeof document.role === "string") {
|
|
1598
|
-
roleByPath.set(document.materializedPath.replaceAll(path.sep, "/"), document.role);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
catch {
|
|
1603
|
-
// A malformed manifest must never break reference injection.
|
|
1604
|
-
}
|
|
1605
|
-
return roleByPath;
|
|
1755
|
+
return Promise.all(referencePaths.map(async (filePath) => ({
|
|
1756
|
+
path: filePath,
|
|
1757
|
+
markdown: await readFile(filePath, "utf-8"),
|
|
1758
|
+
})));
|
|
1606
1759
|
}
|
|
1607
1760
|
export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
1608
1761
|
const paths = getTaskPaths(repoRoot, taskId);
|
|
@@ -1683,6 +1836,16 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1683
1836
|
catch (error) {
|
|
1684
1837
|
throw new Error(`failed to load verification commands for task "${taskId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1685
1838
|
}
|
|
1839
|
+
const frontendShapeTransitionCapsule = taskConfig.taskKind === "frontend-implementation" && CANONICAL_TASK_ID_PATTERN.test(taskId)
|
|
1840
|
+
? await discoverLatestCommittedFrontendShapeCapsule({
|
|
1841
|
+
repoRoot,
|
|
1842
|
+
taskId,
|
|
1843
|
+
sourceDigest: computeFrontendShapeSourceDigest({
|
|
1844
|
+
requirementMarkdown,
|
|
1845
|
+
constraintMarkdown: constraintMarkdown ?? "",
|
|
1846
|
+
}),
|
|
1847
|
+
})
|
|
1848
|
+
: undefined;
|
|
1686
1849
|
const sources = {
|
|
1687
1850
|
taskId,
|
|
1688
1851
|
repoRoot,
|
|
@@ -1699,6 +1862,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1699
1862
|
verifyCommands,
|
|
1700
1863
|
sddEmbeddedSkills: await probeRepoLocalSddSkills(repoRoot),
|
|
1701
1864
|
projectGovernancePresent: await discoverProjectGovernancePresence(repoRoot),
|
|
1865
|
+
...(frontendShapeTransitionCapsule ? { autoloadedFrontendShapeTransitionCapsule: frontendShapeTransitionCapsule } : {}),
|
|
1702
1866
|
};
|
|
1703
1867
|
return sources;
|
|
1704
1868
|
}
|
|
@@ -1716,7 +1880,9 @@ async function prepareFrontendMockSources(sources, discoveredProjectCapability)
|
|
|
1716
1880
|
}
|
|
1717
1881
|
}
|
|
1718
1882
|
const projectCapability = discoveredProjectCapability ??
|
|
1719
|
-
(await discoverFrontendProjectCapability(repoRoot
|
|
1883
|
+
(await discoverFrontendProjectCapability(repoRoot, {
|
|
1884
|
+
specRoots: sources.taskConfig.frontendOpenspec?.specRoots,
|
|
1885
|
+
}));
|
|
1720
1886
|
const frontendRisk = classifyFrontendRisk({
|
|
1721
1887
|
title: sources.taskConfig.title,
|
|
1722
1888
|
requirementMarkdown: sources.requirementMarkdown,
|
|
@@ -1775,6 +1941,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1775
1941
|
forbiddenPaths,
|
|
1776
1942
|
outputContract: "Archived final shell verification stdout/stderr with exit codes; no worktree writes.",
|
|
1777
1943
|
subtask_prompt: "Run the adapter-resolved final verification commands before read-only verification review.",
|
|
1944
|
+
transientRetryProfile: TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE,
|
|
1778
1945
|
shell: {
|
|
1779
1946
|
commands: verifyShellCommands,
|
|
1780
1947
|
verifyEvidence: buildVerifyEvidence({
|
|
@@ -1826,6 +1993,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1826
1993
|
executor: "pi",
|
|
1827
1994
|
complexity: "MED",
|
|
1828
1995
|
writePolicy: "read-only",
|
|
1996
|
+
readBudget: FRONTEND_READ_BUDGETS.contract,
|
|
1829
1997
|
allowedPaths: taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"],
|
|
1830
1998
|
forbiddenPaths,
|
|
1831
1999
|
outputContract: "Plain Markdown implementation contract (10 lines or fewer); no file writes.",
|
|
@@ -1842,6 +2010,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1842
2010
|
executor: "pi",
|
|
1843
2011
|
complexity: scoutComplexity,
|
|
1844
2012
|
writePolicy: "read-only",
|
|
2013
|
+
readBudget: FRONTEND_READ_BUDGETS.scout,
|
|
1845
2014
|
allowedPaths: scoutPaths.srcPaths,
|
|
1846
2015
|
forbiddenPaths,
|
|
1847
2016
|
outputContract: "Plain Markdown source reconnaissance summary; no file writes.",
|
|
@@ -1858,6 +2027,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1858
2027
|
executor: "pi",
|
|
1859
2028
|
complexity: scoutComplexity,
|
|
1860
2029
|
writePolicy: "read-only",
|
|
2030
|
+
readBudget: FRONTEND_READ_BUDGETS.plan,
|
|
1861
2031
|
allowedPaths: scoutPaths.testPaths,
|
|
1862
2032
|
forbiddenPaths,
|
|
1863
2033
|
outputContract: "Plain Markdown test coverage reconnaissance summary; no file writes.",
|
|
@@ -1895,6 +2065,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1895
2065
|
allowedPaths: implementPaths.allowedPaths,
|
|
1896
2066
|
forbiddenPaths,
|
|
1897
2067
|
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
2068
|
+
transientRetryProfile: TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE,
|
|
1898
2069
|
subtask_prompt: [
|
|
1899
2070
|
"Implement the approved plan with minimal focused changes.",
|
|
1900
2071
|
"Stay within writeSet. Do not write root artifacts/** unless artifacts paths are explicitly declared in writeSet.",
|
|
@@ -1954,6 +2125,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1954
2125
|
],
|
|
1955
2126
|
};
|
|
1956
2127
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
2128
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
1957
2129
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
1958
2130
|
parseDagSpec(spec);
|
|
1959
2131
|
assertValidDagSpec(spec);
|
|
@@ -2222,40 +2394,180 @@ function frontendMockStrategyMustBeNotNeeded(sources) {
|
|
|
2222
2394
|
capabilityStatus === "ambiguous" ||
|
|
2223
2395
|
!hasDeterministicMockVerification));
|
|
2224
2396
|
}
|
|
2225
|
-
|
|
2397
|
+
/**
|
|
2398
|
+
* 候选规范懒加载:prompt 只内联与任务相关的候选,不全部塞给模型。
|
|
2399
|
+
*
|
|
2400
|
+
* - mandatory(任务源显式声明/引用)始终保留——模型必须知道它们存在。
|
|
2401
|
+
* - scan-strict 候选按「路径段是否命中任务源关键词」过滤:列表页任务只
|
|
2402
|
+
* 带出列表相关组件/页面规范,创建页规范不进 prompt。
|
|
2403
|
+
* - runtime 的选型/read 门禁仍消费完整 openspecCandidatePaths(prewrite
|
|
2404
|
+
* gate 用全量);这里只缩小 prompt 体积,不改变门禁语义。未提及的候选
|
|
2405
|
+
* 由 runtime 默认 irrelevant,模型无需枚举。
|
|
2406
|
+
* - 真正读取发生在 plan 侧:模型对 required 选型调用 read 工具,prewrite
|
|
2407
|
+
* 门禁验证 read 事件——按需读取,不预加载正文。
|
|
2408
|
+
*/
|
|
2409
|
+
export function filterRelevantOpenspecCandidates(input) {
|
|
2410
|
+
const mandatory = new Set(input.mandatoryPaths);
|
|
2411
|
+
const relevant = [];
|
|
2412
|
+
for (const candidate of input.candidates) {
|
|
2413
|
+
if (mandatory.has(candidate)) {
|
|
2414
|
+
relevant.push(candidate);
|
|
2415
|
+
continue;
|
|
2416
|
+
}
|
|
2417
|
+
if (openspecCandidateMatchesSource(candidate, input.sourceMarkdown)) {
|
|
2418
|
+
relevant.push(candidate);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
// 保持候选发现顺序(确定性);mandatory 与相关候选都按原始顺序出现。
|
|
2422
|
+
const maxCandidates = input.maxCandidates ?? 24;
|
|
2423
|
+
if (relevant.length <= maxCandidates)
|
|
2424
|
+
return relevant;
|
|
2425
|
+
const mandatoryRelevant = relevant.filter((candidate) => mandatory.has(candidate));
|
|
2426
|
+
const optionalRelevant = relevant.filter((candidate) => !mandatory.has(candidate));
|
|
2427
|
+
return [
|
|
2428
|
+
...mandatoryRelevant,
|
|
2429
|
+
...optionalRelevant.slice(0, Math.max(0, maxCandidates - mandatoryRelevant.length)),
|
|
2430
|
+
];
|
|
2431
|
+
}
|
|
2432
|
+
function buildFrontendSpecCandidateSummaries(input) {
|
|
2433
|
+
const mandatory = new Set(input.mandatoryPaths);
|
|
2434
|
+
return input.paths
|
|
2435
|
+
.map((candidate) => scoreFrontendSpecCandidate(candidate, "", {
|
|
2436
|
+
registryMode: input.registryModes.get(candidate),
|
|
2437
|
+
taskRelated: mandatory.has(candidate) || openspecCandidateMatchesSource(candidate, input.sourceMarkdown),
|
|
2438
|
+
}))
|
|
2439
|
+
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* 候选路径是否与任务源相关:从任务源提取文件名/路径 token(去扩展名、
|
|
2443
|
+
* 去连字符/下划线),若候选的路径段包含任一 token 即视为相关。纯字符串
|
|
2444
|
+
* 判定,无 IO;找不到 token 时保守保留(避免漏掉模型可能需要的规范)。
|
|
2445
|
+
*/
|
|
2446
|
+
function openspecCandidateMatchesSource(candidate, sourceMarkdown) {
|
|
2447
|
+
const sourceTokens = extractOpenspecSourceTokens(sourceMarkdown);
|
|
2448
|
+
if (sourceTokens.size === 0)
|
|
2449
|
+
return true;
|
|
2450
|
+
const candidateLower = candidate.toLowerCase().replace(/\\/g, "/");
|
|
2451
|
+
const candidateSegments = candidateLower.split("/");
|
|
2452
|
+
const candidateBasename = candidateSegments[candidateSegments.length - 1] ?? "";
|
|
2453
|
+
for (const token of sourceTokens) {
|
|
2454
|
+
if (candidateBasename.includes(token) ||
|
|
2455
|
+
candidateLower.includes(`/${token}/`) ||
|
|
2456
|
+
candidateLower.includes(`${token}.`)) {
|
|
2457
|
+
return true;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
return false;
|
|
2461
|
+
}
|
|
2462
|
+
/** 从任务源 markdown 提取显著 token:反引号代码段、路径、组件/页面名。 */
|
|
2463
|
+
function extractOpenspecSourceTokens(markdown) {
|
|
2464
|
+
const tokens = new Set();
|
|
2465
|
+
const push = (raw) => {
|
|
2466
|
+
const cleaned = raw
|
|
2467
|
+
.trim()
|
|
2468
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "")
|
|
2469
|
+
.toLowerCase();
|
|
2470
|
+
if (cleaned.length >= 2 && cleaned.length <= 40)
|
|
2471
|
+
tokens.add(cleaned);
|
|
2472
|
+
};
|
|
2473
|
+
// 反引号内联代码(文件名/组件名)
|
|
2474
|
+
for (const match of markdown.matchAll(/`([^`\n]+)`/g)) {
|
|
2475
|
+
push(match[1] ?? "");
|
|
2476
|
+
}
|
|
2477
|
+
// markdown 链接文本
|
|
2478
|
+
for (const match of markdown.matchAll(/\[([^\]]+)\]\([^)]+\)/g)) {
|
|
2479
|
+
push(match[1] ?? "");
|
|
2480
|
+
}
|
|
2481
|
+
// 路径 token(openspec/ 或 xxx/xxx.md)
|
|
2482
|
+
for (const match of markdown.matchAll(/(?:[A-Za-z0-9_-]+\/)+[A-Za-z0-9_.-]+/g)) {
|
|
2483
|
+
const segments = (match[0] ?? "").split("/");
|
|
2484
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
2485
|
+
push(basename.replace(/\.[a-z0-9]+$/i, ""));
|
|
2486
|
+
for (const segment of segments)
|
|
2487
|
+
push(segment);
|
|
2488
|
+
}
|
|
2489
|
+
return tokens;
|
|
2490
|
+
}
|
|
2491
|
+
async function resolveFrontendOpenspecGateConfig(sources) {
|
|
2226
2492
|
const taskConfig = sources.taskConfig;
|
|
2227
2493
|
const policy = taskConfig.frontendOpenspec?.policy ?? "cited";
|
|
2228
2494
|
const declared = taskConfig.frontendOpenspec?.requiredReadPaths ?? [];
|
|
2229
|
-
const
|
|
2495
|
+
const governanceRoot = sources.frontendProjectCapability?.openspecDiscovery?.governanceRoot ??
|
|
2496
|
+
DEFAULT_OPENSPEC_GOVERNANCE_ROOT;
|
|
2497
|
+
const specRoots = taskConfig.frontendOpenspec?.specRoots ?? DEFAULT_FRONTEND_SPEC_ROOTS;
|
|
2498
|
+
const rootAliases = sources.repoRoot
|
|
2499
|
+
? await resolveFrontendSpecRootAliases(sources.repoRoot, [...specRoots])
|
|
2500
|
+
: [];
|
|
2501
|
+
const remappedSourceMarkdown = rootAliases.reduce((markdown, { alias, logicalRoot }) => markdown.replace(new RegExp(`(^|[^A-Za-z0-9_.-])${alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`, "g"), `$1${logicalRoot}/`), [sources.requirementMarkdown, sources.constraintMarkdown ?? ""].join("\n"));
|
|
2502
|
+
const taskSourceCited = extractTaskSourceFrontendSpecPaths(remappedSourceMarkdown, specRoots, governanceRoot);
|
|
2230
2503
|
const scanStrict = sources.frontendProjectCapability?.designEvidence.normativePaths ?? [];
|
|
2504
|
+
const registry = sources.repoRoot
|
|
2505
|
+
? await readFrontendSpecRegistry(sources.repoRoot)
|
|
2506
|
+
: null;
|
|
2507
|
+
const registryModes = new Map();
|
|
2508
|
+
for (const root of registry?.roots ?? []) {
|
|
2509
|
+
if (root.scope && !root.scope.includes("frontend"))
|
|
2510
|
+
continue;
|
|
2511
|
+
if (!root.mode)
|
|
2512
|
+
continue;
|
|
2513
|
+
for (const candidate of scanStrict) {
|
|
2514
|
+
if (candidate === root.path || candidate.startsWith(`${root.path}/`)) {
|
|
2515
|
+
registryModes.set(candidate, root.mode);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2231
2519
|
const dedupeSorted = (paths) => [...new Set(paths)].sort();
|
|
2232
2520
|
const openspecCandidateSources = {
|
|
2233
2521
|
declared: dedupeSorted(declared),
|
|
2234
2522
|
taskSourceCited: dedupeSorted(taskSourceCited),
|
|
2235
2523
|
scanStrict: dedupeSorted(scanStrict),
|
|
2236
2524
|
};
|
|
2525
|
+
// Candidate discovery is frozen independently from the eventual must-read
|
|
2526
|
+
// set. The selector may mark candidates irrelevant, while explicit task
|
|
2527
|
+
// declarations/source citations are never allowed to be downgraded.
|
|
2528
|
+
const openspecMandatoryPaths = dedupeSorted([
|
|
2529
|
+
...declared,
|
|
2530
|
+
...taskSourceCited,
|
|
2531
|
+
]);
|
|
2532
|
+
// `cited` must stay demand-driven: a normative registry makes a path
|
|
2533
|
+
// discoverable, not implicitly task-mandatory. Only scan-strict may offer
|
|
2534
|
+
// the global discovery set to the bounded task-relevance selector.
|
|
2237
2535
|
const openspecCandidatePaths = policy === "cited"
|
|
2238
|
-
?
|
|
2239
|
-
:
|
|
2240
|
-
|
|
2536
|
+
? openspecMandatoryPaths
|
|
2537
|
+
: dedupeSorted([
|
|
2538
|
+
...openspecCandidateSources.scanStrict,
|
|
2539
|
+
...openspecMandatoryPaths,
|
|
2540
|
+
]);
|
|
2541
|
+
const result = {
|
|
2241
2542
|
openspecPolicy: policy,
|
|
2242
2543
|
openspecCandidatePaths,
|
|
2243
2544
|
openspecCandidateSources,
|
|
2545
|
+
openspecMandatoryPaths,
|
|
2546
|
+
openspecCandidateSummaries: buildFrontendSpecCandidateSummaries({
|
|
2547
|
+
paths: openspecCandidatePaths,
|
|
2548
|
+
mandatoryPaths: openspecMandatoryPaths,
|
|
2549
|
+
registryModes,
|
|
2550
|
+
sourceMarkdown: [sources.requirementMarkdown, sources.constraintMarkdown ?? ""].join("\n"),
|
|
2551
|
+
}),
|
|
2244
2552
|
};
|
|
2553
|
+
if (sources.repoRoot && openspecCandidatePaths.length > 0) {
|
|
2554
|
+
result.openspecCandidateSnapshots = await Promise.all(openspecCandidatePaths.map(async (candidate) => ({
|
|
2555
|
+
path: candidate,
|
|
2556
|
+
sha256: createHash("sha256")
|
|
2557
|
+
.update(await readFile(path.join(sources.repoRoot, candidate)))
|
|
2558
|
+
.digest("hex"),
|
|
2559
|
+
})));
|
|
2560
|
+
}
|
|
2561
|
+
return result;
|
|
2245
2562
|
}
|
|
2246
|
-
const openspecCitationInstruction = [
|
|
2247
|
-
"OpenSpec 引用块(citation block):在 fenced json 契约块之后,追加**恰好一个** ```openspec-citations 围栏代码块(三反引号 + openspec-citations)。",
|
|
2248
|
-
'该块内每行一个 JSON 对象 {"path":"<repo 相对 openspec 路径>","section":"<命中章节或空串>","line":<int 或 null>},必须逐条列出你在本计划中实际读取并应用的每个 openspec 规范文件。',
|
|
2249
|
-
"prewrite gate 会用真实 read 事件核验每条引用:引用存在但无成功 read 事件 → openspec-citation-not-read;契约冻结的必读候选未被引用 → openspec-not-cited;两者都 fail-closed。不要引用未读取的路径。",
|
|
2250
|
-
].join("\n");
|
|
2251
2563
|
const frontendComponentConformanceInstruction = [
|
|
2252
2564
|
"## Component Selection conformance (uiComponentChoices; hard rule)",
|
|
2253
2565
|
"每个 UI 用途必须在契约的 uiComponentChoices[] 中声明组件选型:{ purpose, component, decision, specReference, rationale }。",
|
|
2254
2566
|
"- decision=specified:前端规范(候选组件/主题桶 + 任务源显式引用)已定义该用途组件 → 必须使用该组件,并给精确 specReference { path, section, line }(path 必须是 openspec/ai_workspace 受支持规范路径)。",
|
|
2255
|
-
"- decision=reuse-existing
|
|
2256
|
-
"- decision=new
|
|
2567
|
+
"- decision=reuse-existing:仅当该组件/惯例**确实已存在于仓库当前代码**(如复用现有 ActiveRunBadge 的 oc- class 惯例)→ specReference 可为 null,rationale 必须指明复用的具体现有组件/文件与依据。",
|
|
2568
|
+
"- decision=new:任务源/PRD 要求**新增**该组件(仓库当前不存在该组件文件)→ decision 必须为 new,不得标 reuse-existing;调用 record_component_choice 时传 sourceRequirementIds(关联的 frozen requirement ID),runtime 会从 source-fidelity ledger 物化精确的任务源 PRD { path, section, line }。不要读取 PRD 或手填/猜测 specReference;rationale 说明新增纯展示组件、复用既有 CSS 命名与主题变量约定。",
|
|
2257
2569
|
"不得静默替换规范组件或自创组件而无偏差声明;spec 已定义该用途组件时不得改选其它组件。",
|
|
2258
|
-
"prewrite gate
|
|
2570
|
+
"prewrite gate 确定性交叉校验:仅 decision=specified 的 specReference 必须是候选 OpenSpec 路径、在 typed decision ledger 中声明且有成功 read 事件;decision=new 的 PRD 引用走任务源可追溯性审查,不得按 OpenSpec 候选拒绝。候选桶非空且契约有 UI 可见工作而 uiComponentChoices 缺失/空 → component-choices-missing。",
|
|
2259
2571
|
].join("\n");
|
|
2260
2572
|
function resolveFrontendCapabilityContextBlock(sources) {
|
|
2261
2573
|
const risk = sources.frontendRisk;
|
|
@@ -2269,34 +2581,7 @@ function resolveFrontendCapabilityContextBlock(sources) {
|
|
|
2269
2581
|
}
|
|
2270
2582
|
if (capability) {
|
|
2271
2583
|
parts.push("", capability.adapterGuidance);
|
|
2272
|
-
|
|
2273
|
-
const bucketLines = [];
|
|
2274
|
-
const pushBucket = (label, paths) => {
|
|
2275
|
-
if (paths.length > 0)
|
|
2276
|
-
bucketLines.push(`${label}: ${paths.join(", ")}`);
|
|
2277
|
-
};
|
|
2278
|
-
pushBucket("schemas", classified.schemas);
|
|
2279
|
-
pushBucket("code-template", classified.codeTemplate);
|
|
2280
|
-
pushBucket("rule.api", classified.rule.api);
|
|
2281
|
-
pushBucket("rule.mock", classified.rule.mock);
|
|
2282
|
-
pushBucket("rule.router", classified.rule.router);
|
|
2283
|
-
pushBucket("rule.hooks", classified.rule.hooks);
|
|
2284
|
-
pushBucket("rule.utils", classified.rule.utils);
|
|
2285
|
-
pushBucket("rule.components", classified.rule.components);
|
|
2286
|
-
pushBucket("rule.other", classified.rule.other);
|
|
2287
|
-
pushBucket("theme", classified.theme);
|
|
2288
|
-
pushBucket("component", classified.component);
|
|
2289
|
-
pushBucket("ui-other", classified.uiOther);
|
|
2290
|
-
pushBucket("advisory-other", classified.advisoryOther);
|
|
2291
|
-
parts.push("## Classified openspec specification paths (role semantics)");
|
|
2292
|
-
if (bucketLines.length > 0) {
|
|
2293
|
-
parts.push(...bucketLines);
|
|
2294
|
-
}
|
|
2295
|
-
else {
|
|
2296
|
-
parts.push("(no openspec specification paths discovered — greenfield)");
|
|
2297
|
-
}
|
|
2298
|
-
parts.push("component / theme / rule.components 是「组件/主题规范」必读语义桶:规范已定义某用途组件时必须使用它(uiComponentChoices 用 decision=specified + 精确 path/section/line),不得静默替换为自认更合适的组件;无规范定义时才允许 reuse-existing 或 new(new 必须声明偏差 rationale)。这些路径在生成期冻结为 prewrite gate 的 componentSpecCandidatePaths。");
|
|
2299
|
-
parts.push("Each consuming node MUST report in its output: applicable rules, the hit path/section/line number for every applied specification, and any conflicts or missing specifications. Missing or conflicting required specifications must fail closed rather than silently substituting nearby repository conventions.");
|
|
2584
|
+
parts.push("Task-relevant OpenSpec candidates are injected separately as a bounded Top-K index. The deterministic prewrite gate retains the complete frozen candidate set; unlisted candidates are neither silently required nor evidence of a missing specification.");
|
|
2300
2585
|
parts.push(`A11y capability: ${capability.a11y.status}` +
|
|
2301
2586
|
(capability.a11y.tools.length
|
|
2302
2587
|
? ` (${capability.a11y.tools.join(", ")})`
|
|
@@ -2305,17 +2590,102 @@ function resolveFrontendCapabilityContextBlock(sources) {
|
|
|
2305
2590
|
}
|
|
2306
2591
|
return parts.join("\n");
|
|
2307
2592
|
}
|
|
2593
|
+
function pruneFrontendTasksForMicro(tasks) {
|
|
2594
|
+
// Micro topology (7 nodes): contract-import-shell → writer-admission →
|
|
2595
|
+
// implement → verify → review-context → review → closeout. Drops scout/plan/
|
|
2596
|
+
// design-policy/design-review and replaces the model contract node with a
|
|
2597
|
+
// deterministic contract-import shell (§7.3: no model, no requirement
|
|
2598
|
+
// rewrites — micro consumes a pre-validated managed Contract).
|
|
2599
|
+
const drop = new Set([
|
|
2600
|
+
"frontend-contract-pi",
|
|
2601
|
+
"frontend-scout-pi",
|
|
2602
|
+
"frontend-plan-pi",
|
|
2603
|
+
"frontend-design-policy-shell",
|
|
2604
|
+
"frontend-design-review-pi",
|
|
2605
|
+
]);
|
|
2606
|
+
const contractPi = tasks.find((task) => task.id === "frontend-contract-pi");
|
|
2607
|
+
const contractImportShell = contractPi
|
|
2608
|
+
? {
|
|
2609
|
+
id: "frontend-contract-import-shell",
|
|
2610
|
+
role: "verifier",
|
|
2611
|
+
executor: "shell",
|
|
2612
|
+
complexity: "LOW",
|
|
2613
|
+
writePolicy: "read-only",
|
|
2614
|
+
depends_on: [],
|
|
2615
|
+
allowedPaths: contractPi.allowedPaths,
|
|
2616
|
+
forbiddenPaths: contractPi.forbiddenPaths,
|
|
2617
|
+
subtask_prompt: "Deterministic managed-Contract import + source/binding/schema freshness re-validation; no model, no requirement rewrite.",
|
|
2618
|
+
shell: {
|
|
2619
|
+
commands: [],
|
|
2620
|
+
frontendContractImport: {
|
|
2621
|
+
schemaVersion: 1,
|
|
2622
|
+
artifactName: "frontend-task-contract.json",
|
|
2623
|
+
outputDir: "contracts",
|
|
2624
|
+
requireSourceFreshness: true,
|
|
2625
|
+
},
|
|
2626
|
+
cwd: ".",
|
|
2627
|
+
timeoutMs: 60000,
|
|
2628
|
+
},
|
|
2629
|
+
}
|
|
2630
|
+
: undefined;
|
|
2631
|
+
const filtered = [
|
|
2632
|
+
...(contractImportShell ? [contractImportShell] : []),
|
|
2633
|
+
...tasks.filter((task) => !drop.has(task.id)),
|
|
2634
|
+
];
|
|
2635
|
+
const byId = new Map(filtered.map((task) => [task.id, task]));
|
|
2636
|
+
const remap = (deps) => {
|
|
2637
|
+
if (!deps)
|
|
2638
|
+
return [];
|
|
2639
|
+
const next = [];
|
|
2640
|
+
for (const dep of deps) {
|
|
2641
|
+
if (drop.has(dep))
|
|
2642
|
+
continue;
|
|
2643
|
+
if (byId.has(dep))
|
|
2644
|
+
next.push(dep);
|
|
2645
|
+
}
|
|
2646
|
+
return [...new Set(next)];
|
|
2647
|
+
};
|
|
2648
|
+
return filtered.map((task) => {
|
|
2649
|
+
const depends_on = remap(task.depends_on);
|
|
2650
|
+
if (task.id === "frontend-writer-admission-shell") {
|
|
2651
|
+
const admission = task.shell?.frontendWriterAdmission;
|
|
2652
|
+
return {
|
|
2653
|
+
...task,
|
|
2654
|
+
depends_on: ["frontend-contract-import-shell"],
|
|
2655
|
+
shell: admission
|
|
2656
|
+
? {
|
|
2657
|
+
...task.shell,
|
|
2658
|
+
commands: task.shell?.commands ?? [],
|
|
2659
|
+
frontendWriterAdmission: {
|
|
2660
|
+
...admission,
|
|
2661
|
+
designReviewFromNodeId: undefined,
|
|
2662
|
+
},
|
|
2663
|
+
}
|
|
2664
|
+
: task.shell,
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
return { ...task, depends_on };
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
function pruneFrontendTasksForSplitRequired(tasks) {
|
|
2671
|
+
const writerChain = new Set([
|
|
2672
|
+
"frontend-writer-admission-shell",
|
|
2673
|
+
"frontend-implement-pi",
|
|
2674
|
+
"frontend-verify-shell",
|
|
2675
|
+
"frontend-review-context-shell",
|
|
2676
|
+
"frontend-review-pi",
|
|
2677
|
+
"frontend-closeout-shell",
|
|
2678
|
+
]);
|
|
2679
|
+
return tasks.filter((task) => !writerChain.has(task.id));
|
|
2680
|
+
}
|
|
2308
2681
|
function pruneFrontendTasksForRisk(tasks, risk) {
|
|
2309
2682
|
if (risk.forceFullGates || risk.selectedRisk !== "small") {
|
|
2310
2683
|
return tasks;
|
|
2311
2684
|
}
|
|
2312
|
-
// Small topology keeps one design review and removes only the
|
|
2313
|
-
//
|
|
2314
|
-
//
|
|
2315
|
-
const drop = new Set([
|
|
2316
|
-
"frontend-plan-revision-pi",
|
|
2317
|
-
"frontend-final-design-review-pi",
|
|
2318
|
-
]);
|
|
2685
|
+
// Small topology keeps one design review and removes only the redundant
|
|
2686
|
+
// design-review node; the deterministic design-policy and writer-admission
|
|
2687
|
+
// shells consume the surviving plan directly.
|
|
2688
|
+
const drop = new Set(["frontend-design-review-pi"]);
|
|
2319
2689
|
const filtered = tasks.filter((task) => !drop.has(task.id));
|
|
2320
2690
|
const byId = new Map(filtered.map((task) => [task.id, task]));
|
|
2321
2691
|
const remap = (deps) => {
|
|
@@ -2323,11 +2693,8 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
2323
2693
|
return [];
|
|
2324
2694
|
const next = [];
|
|
2325
2695
|
for (const dep of deps) {
|
|
2326
|
-
if (dep === "frontend-
|
|
2327
|
-
if (byId.has("frontend-plan-pi"))
|
|
2328
|
-
next.push("frontend-plan-pi");
|
|
2696
|
+
if (dep === "frontend-design-review-pi")
|
|
2329
2697
|
continue;
|
|
2330
|
-
}
|
|
2331
2698
|
if (byId.has(dep) || dep === "frontend-implement-pi")
|
|
2332
2699
|
next.push(dep);
|
|
2333
2700
|
}
|
|
@@ -2335,30 +2702,27 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
2335
2702
|
};
|
|
2336
2703
|
return filtered.map((task) => {
|
|
2337
2704
|
const depends_on = remap(task.depends_on);
|
|
2338
|
-
if (task.id === "frontend-
|
|
2339
|
-
|
|
2705
|
+
if (task.id === "frontend-writer-admission-shell") {
|
|
2706
|
+
// Small topology: no design review node, so the admission shell drops
|
|
2707
|
+
// the design-review dependency and the verdict requirement.
|
|
2708
|
+
const admission = task.shell?.frontendWriterAdmission;
|
|
2340
2709
|
return {
|
|
2341
2710
|
...task,
|
|
2342
|
-
depends_on: ["frontend-
|
|
2343
|
-
|
|
2344
|
-
shell: gate
|
|
2711
|
+
depends_on: ["frontend-design-policy-shell"],
|
|
2712
|
+
shell: admission
|
|
2345
2713
|
? {
|
|
2346
2714
|
...task.shell,
|
|
2347
2715
|
commands: task.shell?.commands ?? [],
|
|
2348
|
-
|
|
2349
|
-
...
|
|
2350
|
-
|
|
2351
|
-
planFallbackFromNodeIds: [],
|
|
2352
|
-
reviewFromNodeId: "frontend-design-review-pi",
|
|
2353
|
-
reviewFallbackFromNodeIds: [],
|
|
2354
|
-
revisionPatch: false,
|
|
2716
|
+
frontendWriterAdmission: {
|
|
2717
|
+
...admission,
|
|
2718
|
+
designReviewFromNodeId: undefined,
|
|
2355
2719
|
},
|
|
2356
2720
|
}
|
|
2357
2721
|
: task.shell,
|
|
2358
2722
|
};
|
|
2359
2723
|
}
|
|
2360
2724
|
if (task.id === "frontend-implement-pi") {
|
|
2361
|
-
for (const need of ["frontend-
|
|
2725
|
+
for (const need of ["frontend-writer-admission-shell"]) {
|
|
2362
2726
|
if (byId.has(need) && !depends_on.includes(need))
|
|
2363
2727
|
depends_on.push(need);
|
|
2364
2728
|
}
|
|
@@ -2383,9 +2747,23 @@ function buildFrontendWriterNodeDefaults(input) {
|
|
|
2383
2747
|
allowedPaths: input.allowedPaths,
|
|
2384
2748
|
forbiddenPaths: input.forbiddenPaths,
|
|
2385
2749
|
skills: FRONTEND_BOUNDED_IMPLEMENT_SKILLS,
|
|
2386
|
-
writerOutcomePolicy: {
|
|
2750
|
+
writerOutcomePolicy: {
|
|
2751
|
+
type: input.writerOutcomePolicyType ?? "implementation-outcome-v1",
|
|
2752
|
+
},
|
|
2387
2753
|
};
|
|
2388
2754
|
}
|
|
2755
|
+
function resolveFrontendShapeCapsuleGenerationInput(sources) {
|
|
2756
|
+
const explicit = sources.frontendShapeTransitionCapsule == null
|
|
2757
|
+
? undefined
|
|
2758
|
+
: parseFrontendShapeTransitionCapsule(sources.frontendShapeTransitionCapsule);
|
|
2759
|
+
const autoloaded = sources.autoloadedFrontendShapeTransitionCapsule == null
|
|
2760
|
+
? undefined
|
|
2761
|
+
: parseFrontendShapeTransitionCapsule(sources.autoloadedFrontendShapeTransitionCapsule);
|
|
2762
|
+
if (explicit && autoloaded && explicit.capsuleDigest !== autoloaded.capsuleDigest) {
|
|
2763
|
+
throw new Error("explicit frontend shape transition capsule conflicts with runtime autoload");
|
|
2764
|
+
}
|
|
2765
|
+
return explicit ?? autoloaded;
|
|
2766
|
+
}
|
|
2389
2767
|
async function buildFrontendHybridDagFromTask(sources) {
|
|
2390
2768
|
const { taskConfig } = sources;
|
|
2391
2769
|
const mockCapability = sources.frontendMockCapability ?? {
|
|
@@ -2410,6 +2788,11 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2410
2788
|
const implementId = frontendImplementationNodeId();
|
|
2411
2789
|
const mockContextBlock = resolveFrontendMockContextBlock(frontendSources);
|
|
2412
2790
|
const capabilityContextBlock = resolveFrontendCapabilityContextBlock(frontendSources);
|
|
2791
|
+
// Freeze the design-policy dependency allowlist from the project manifest:
|
|
2792
|
+
// already-declared deps are authorized; anything else in the model's
|
|
2793
|
+
// dependency policy that looks like a package name is an unauthorized new
|
|
2794
|
+
// dependency (fail-closed at the policy shell and the plan pre-check).
|
|
2795
|
+
const declaredDependencies = await collectDeclaredDependencies(sources.repoRoot ?? process.cwd());
|
|
2413
2796
|
const frontendRisk = frontendSources.frontendRisk ??
|
|
2414
2797
|
classifyFrontendRisk({
|
|
2415
2798
|
title: taskConfig.title,
|
|
@@ -2418,145 +2801,63 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2418
2801
|
allowedPaths: taskConfig.allowedPaths,
|
|
2419
2802
|
complexity: taskConfig.complexity,
|
|
2420
2803
|
});
|
|
2421
|
-
const
|
|
2804
|
+
const frontendTaskShape = resolveFrontendTaskShape({
|
|
2805
|
+
complexity: taskConfig.complexity,
|
|
2806
|
+
allowedPaths: taskConfig.allowedPaths,
|
|
2807
|
+
requirementMarkdown: sources.requirementMarkdown,
|
|
2808
|
+
constraintMarkdown: sources.constraintMarkdown ?? undefined,
|
|
2809
|
+
splitSignal: { splitRequired: false, runtimeSupportsSplit: true },
|
|
2810
|
+
taskId: sources.taskId,
|
|
2811
|
+
sourceDigest: computeFrontendShapeSourceDigest({
|
|
2812
|
+
requirementMarkdown: sources.requirementMarkdown,
|
|
2813
|
+
constraintMarkdown: sources.constraintMarkdown ?? "",
|
|
2814
|
+
}),
|
|
2815
|
+
shapeTransitionCapsule: resolveFrontendShapeCapsuleGenerationInput(sources),
|
|
2816
|
+
});
|
|
2817
|
+
const frontendSourceBinding = buildDagSourceBinding(sources, taskConfig.taskKind);
|
|
2422
2818
|
const frontendContractSkeleton = buildFrontendImplementationContractSkeleton({
|
|
2423
2819
|
sourceBinding: frontendSourceBinding,
|
|
2424
2820
|
riskLevel: frontendRisk.selectedRisk,
|
|
2425
2821
|
targetFiles: implementPaths.writeSet,
|
|
2426
2822
|
});
|
|
2427
|
-
const frontendContractSchemaBlock = (() => {
|
|
2428
|
-
const schema = loadFrontendImplementationContractJsonSchema();
|
|
2429
|
-
return [
|
|
2430
|
-
`## Final ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative after runtime merge)`,
|
|
2431
|
-
schema,
|
|
2432
|
-
"",
|
|
2433
|
-
"## Runtime contract skeleton (deterministic and protected)",
|
|
2434
|
-
JSON.stringify(frontendContractSkeleton),
|
|
2435
|
-
"",
|
|
2436
|
-
"The initial planner emits an editable RFC 7386 patch against this skeleton. It MUST omit schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime merges and validates the final contract, then replaces the planner output with canonical full-contract JSON for downstream review.",
|
|
2437
|
-
"",
|
|
2438
|
-
"## Forbidden fields (these are NOT in the schema; do not emit)",
|
|
2439
|
-
"- schemaId",
|
|
2440
|
-
"- targetFiles",
|
|
2441
|
-
"- requirementCoverage",
|
|
2442
|
-
"",
|
|
2443
|
-
"## Critical rules",
|
|
2444
|
-
"- verificationTargets is a TOP-LEVEL required array",
|
|
2445
|
-
"- uiStates items use name/applicable/expectedBehavior/implementationTargets/verificationTargetIds/notApplicableReason",
|
|
2446
|
-
"- Use uiStates: [] for frontend logic changes with no user-visible UI state. Do not invent UI states.",
|
|
2447
|
-
"- For applicable=true, provide non-empty expectedBehavior plus non-empty implementationTargets and verificationTargetIds. For applicable=false, provide non-empty notApplicableReason and omit expectedBehavior instead of emitting an empty string.",
|
|
2448
|
-
"- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
|
|
2449
|
-
"- All implementation files, verification files, symbols, and commands must be discovered from the current target workspace and current task. Never copy paths, symbols, or commands from the loop-agent repository, an example task, or prior run output.",
|
|
2450
|
-
"- Use relative POSIX paths rooted at the target workspace. Do not assume a particular src/test directory layout; preserve the target project's actual app/, packages/, spec/, __tests__, or other layout.",
|
|
2451
|
-
"",
|
|
2452
|
-
"## Bad / Good contract field examples",
|
|
2453
|
-
"",
|
|
2454
|
-
"### verificationTargets - BAD (invented commandLabel, missing file):",
|
|
2455
|
-
'{"id":"vt-1","type":"static","commandLabel":"lint","file":"","requirementIds":["AC-001"],"uiStates":[]} <-- REJECTED: commandLabel not in frozen command set; empty file path',
|
|
2456
|
-
"",
|
|
2457
|
-
'### verificationTargets - GOOD (real frozen label, real file):',
|
|
2458
|
-
'{"id":"vt-1","type":"static","commandLabel":"npm run typecheck","file":"tsconfig.json","requirementIds":["AC-001"],"uiStates":[]} <-- Matches frozen command set; real file path',
|
|
2459
|
-
"",
|
|
2460
|
-
"### requirements - BAD (missing expectedOutcome):",
|
|
2461
|
-
'{"id":"AC-001","expectedOutcome":"","implementationTargets":["src/app.tsx"],"verificationTargetIds":["vt-1"]} <-- REJECTED: empty expectedOutcome',
|
|
2462
|
-
"",
|
|
2463
|
-
"### requirements - GOOD (concrete expectedOutcome):",
|
|
2464
|
-
'{"id":"AC-001","expectedOutcome":"TypeScript compilation exits with code 0 and produces no errors in dist/","implementationTargets":["src/app.tsx"],"verificationTargetIds":["vt-1"]}',
|
|
2465
|
-
"",
|
|
2466
|
-
"### interactions - BAD (empty trigger/expectedBehavior):",
|
|
2467
|
-
'{"name":"save-click","trigger":"","expectedBehavior":"","implementationTargets":["src/button.tsx"],"verificationTargetIds":["vt-3"]} <-- REJECTED: empty trigger and expectedBehavior',
|
|
2468
|
-
"",
|
|
2469
|
-
"### interactions - GOOD:",
|
|
2470
|
-
'{"name":"save-click","trigger":"User clicks the Save button in the editor toolbar","expectedBehavior":"POST /api/save is called with editor content; success toast appears; button enters disabled+spinner state until response","implementationTargets":["src/editor/save-button.tsx"],"verificationTargetIds":["vt-3"]}',
|
|
2471
|
-
"",
|
|
2472
|
-
"### uiStates - BAD (applicable=true but missing expectedBehavior):",
|
|
2473
|
-
'{"name":"loading","applicable":true,"expectedBehavior":"","implementationTargets":[],"verificationTargetIds":[]} <-- REJECTED: applicable UI state requires non-empty expectedBehavior, implementationTargets, and verificationTargetIds',
|
|
2474
|
-
"",
|
|
2475
|
-
"### uiStates - GOOD (applicable=true with complete fields):",
|
|
2476
|
-
'{"name":"loading","applicable":true,"expectedBehavior":"Skeleton placeholder visible while data fetches; aria-busy=true on the list container","implementationTargets":["src/dashboard/list-view.tsx"],"verificationTargetIds":["vt-3"]}',
|
|
2477
|
-
"",
|
|
2478
|
-
"### uiStates - BAD (applicable=false without notApplicableReason):",
|
|
2479
|
-
'{"name":"dark-mode","applicable":false} <-- REJECTED: non-applicable UI state requires notApplicableReason',
|
|
2480
|
-
"",
|
|
2481
|
-
"### uiStates - GOOD (applicable=false with reason):",
|
|
2482
|
-
'{"name":"dark-mode","applicable":false,"notApplicableReason":"Dark mode toggle is out of scope for this task; only light theme is targeted"}',
|
|
2483
|
-
"",
|
|
2484
|
-
"### mockApi.endpoints - BAD (strategy=native but empty endpoints):",
|
|
2485
|
-
'{"strategy":"native","productionDefaultOff":true,"activation":"env flag","endpoints":[]} <-- REJECTED: native strategy requires at least one endpoint with method, path, fixture, and consumer',
|
|
2486
|
-
"",
|
|
2487
|
-
"### mockApi.endpoints - GOOD (strategy=native with complete endpoint):",
|
|
2488
|
-
'{"strategy":"native","productionDefaultOff":true,"activation":"VITE_ENABLE_MOCK=true","endpoints":[{"method":"GET","path":"/api/users","fixture":"mocks/fixtures/users.json","consumer":"src/api/users.ts"}]}',
|
|
2489
|
-
"",
|
|
2490
|
-
"### optional plan fields - GOOD (all optional; omit when absent):",
|
|
2491
|
-
'{"implementationSteps":["confirm contract","sync tests"],"stylingStrategy":"reuse existing design tokens","dependencyPolicy":"no new runtime deps","residualRisks":["browser a11y not-run"],"realIntegrationGap":"FE-TEST owns live HTTP"}',
|
|
2492
|
-
"",
|
|
2493
|
-
"### optional plan fields - BAD (present-but-empty strings are rejected):",
|
|
2494
|
-
'{"stylingStrategy":"","dependencyPolicy":""} <-- REJECTED: optional string fields must be non-empty when present; omit them instead',
|
|
2495
|
-
"",
|
|
2496
|
-
"### uiComponentChoices - GOOD (specified with precise spec hit):",
|
|
2497
|
-
'{"purpose":"primary action button","component":"Button","decision":"specified","specReference":{"path":"openspec/schemas/button.md","section":"Variants","line":12},"rationale":"spec mandates Button for primary actions"}',
|
|
2498
|
-
"",
|
|
2499
|
-
"### uiComponentChoices - GOOD (new with deviation rationale; specReference null):",
|
|
2500
|
-
'{"purpose":"loading skeleton","component":"SkeletonCard","decision":"new","specReference":null,"rationale":"no spec or existing component covers skeleton; deviation pending design-review approval"}',
|
|
2501
|
-
"",
|
|
2502
|
-
"### uiComponentChoices - BAD (specified without specReference, or new with specReference):",
|
|
2503
|
-
'{"purpose":"primary action","component":"MyButton","decision":"specified","specReference":null,"rationale":"..."} <-- REJECTED: specified requires specReference',
|
|
2504
|
-
'{"purpose":"primary action","component":"MyButton","decision":"new","specReference":{"path":"openspec/schemas/button.md","section":"","line":null},"rationale":"..."} <-- REJECTED: new must not carry specReference',
|
|
2505
|
-
].join("\n");
|
|
2506
|
-
})();
|
|
2507
2823
|
const frontendContractFieldSummary = [
|
|
2508
2824
|
"## Contract field summary (authoritative JSON; no plan prose)",
|
|
2509
|
-
"
|
|
2825
|
+
"frontend-plan-pi records typed facts; frontend-design-policy-shell applies the runtime skeleton, validates, and materializes the canonical full contract JSON supplied here. There is no separate plan prose authority. Review these fields:",
|
|
2510
2826
|
"- requirements[]: id, expectedOutcome, implementationTargets, verificationTargetIds, evidenceGap",
|
|
2511
2827
|
"- uiStates[]: name, applicable, expectedBehavior, implementationTargets, verificationTargetIds, notApplicableReason",
|
|
2512
2828
|
"- interactions[]: name, trigger, expectedBehavior, implementationTargets, verificationTargetIds",
|
|
2513
|
-
"- targets:
|
|
2829
|
+
"- targets: routes, publicApiChanges (files are runtime-owned)",
|
|
2514
2830
|
"- mockApi: strategy, productionDefaultOff, activation, endpoints[]",
|
|
2515
2831
|
"- verificationTargets[]: id, type, commandLabel, file, symbol, requirementIds, uiStates",
|
|
2516
|
-
"- designEvidence: source, paths, conflicts; evidenceGaps[]",
|
|
2517
|
-
"- optional:
|
|
2832
|
+
"- designEvidence: source, paths, conflicts; evidenceGaps[] (optional)",
|
|
2833
|
+
"- optional: stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap",
|
|
2518
2834
|
"- uiComponentChoices[]: purpose, component, decision (specified|reuse-existing|new), specReference { path, section, line } | null, rationale",
|
|
2519
2835
|
"Do not require or read a separate plan prose section; the contract JSON is the only plan surface.",
|
|
2520
2836
|
].join("\n");
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2837
|
+
// Do not carry every attachment through the whole frontend pipeline. The
|
|
2838
|
+
// contract node is the sole requirements/materials synthesis point; scout
|
|
2839
|
+
// and plan only need the canonical request plus constraints, while design
|
|
2840
|
+
// review consumes the materialized contract and only needs source provenance.
|
|
2841
|
+
// This prevents attachment content from accumulating on later review calls.
|
|
2842
|
+
const sourceContexts = {
|
|
2843
|
+
contract: [buildSourceContextBlock(sources), capabilityContextBlock]
|
|
2844
|
+
.filter(Boolean)
|
|
2845
|
+
.join("\n\n"),
|
|
2846
|
+
scout: [
|
|
2847
|
+
buildSourceContextBlock(sources, { includeReferenceDocuments: false }),
|
|
2848
|
+
capabilityContextBlock,
|
|
2849
|
+
]
|
|
2850
|
+
.filter(Boolean)
|
|
2851
|
+
.join("\n\n"),
|
|
2852
|
+
designReview: buildSourceContextBlock(sources, {
|
|
2853
|
+
includeRequirementExcerpt: false,
|
|
2854
|
+
includeConstraintExcerpt: false,
|
|
2855
|
+
includeReferenceDocuments: false,
|
|
2856
|
+
}),
|
|
2857
|
+
};
|
|
2534
2858
|
const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
|
|
2535
2859
|
mockCapability.verifyCommands.length > 0;
|
|
2536
2860
|
const requirementIds = frontendSourceBinding.requirementIds;
|
|
2537
|
-
const requirementCoverageInstruction = requirementIds.length > 0
|
|
2538
|
-
? [
|
|
2539
|
-
"## Requirement Coverage",
|
|
2540
|
-
`Cover each frozen ID exactly once in requirements[]: ${requirementIds.join(", ")}. Each item needs a concrete expectedOutcome, implementationTargets, and verificationTargetIds.`,
|
|
2541
|
-
"Interactions need trigger + expectedBehavior; applicable uiStates need expectedBehavior + implementationTargets + verificationTargetIds. Do not use empty strings.",
|
|
2542
|
-
].join("\n")
|
|
2543
|
-
: "";
|
|
2544
|
-
const verificationTargetFileInstruction = [
|
|
2545
|
-
`## Verification target file semantics`,
|
|
2546
|
-
`verificationTargets[].file is the code file that the target verifies (the file the writer changes), NOT where the command is defined.`,
|
|
2547
|
-
`Non-static targets (type unit/component/integration/mock) MUST set file to a concrete code file inside the implementation writeSet (task allowedPaths); the prewrite gate rejects any non-static target whose file falls outside the writeSet.`,
|
|
2548
|
-
`Command-level checks that run project-wide (all tests, typecheck, build, governance) MUST use type "static" and must NOT be bound as non-static targets with file=package.json/tsconfig.json/vite.config.ts/scripts/*. Static targets are exempt from the writeSet containment check.`,
|
|
2549
|
-
].join("\n");
|
|
2550
|
-
const mandatorySourceReadInstruction = [
|
|
2551
|
-
"## Mandatory full source read before contracting",
|
|
2552
|
-
"Before producing this contract/plan, use the Pi read tool to read the FULL bound source files (需求.md, 执行约束.md, and every `references/*` Bound readPath listed above) — the inline copies above may be truncated excerpts, and requirement/acceptance references are authoritative only in their full form.",
|
|
2553
|
-
"Do not drop scope fields, acceptance criteria, non-goals, UI states, or column/field definitions that exist in the full sources but are absent from the inline excerpts; if a field appears in the full source, it belongs in the contract.",
|
|
2554
|
-
].join("\n");
|
|
2555
|
-
const plannerSourceReadInstruction = [
|
|
2556
|
-
"## Source-read policy",
|
|
2557
|
-
"Use the injected task sources plus frontend-contract-pi and frontend-scout-pi as the primary evidence. Do not repeat reads that those facts already settle.",
|
|
2558
|
-
"Read a bound source only to resolve a missing or conflicting field; read every applicable OpenSpec candidate directly before citing it. Never infer omitted task fields from a preview.",
|
|
2559
|
-
].join("\n");
|
|
2560
2861
|
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
2561
2862
|
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
2562
2863
|
const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
|
|
@@ -2566,15 +2867,15 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2566
2867
|
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
2567
2868
|
: []),
|
|
2568
2869
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
2569
|
-
"Frontend
|
|
2570
|
-
"
|
|
2571
|
-
"
|
|
2572
|
-
"
|
|
2870
|
+
"Frontend design policy (frontend-design-policy-shell) and writer admission (frontend-writer-admission-shell) are the only write authorization; the writer runs only after both materialized the canonical contract and admitted a concrete writeSet.",
|
|
2871
|
+
"Design review verdict is consumed only as admission data input; it never drives branch selection.",
|
|
2872
|
+
"Verification failure is terminal for the run: frontend-verify-shell fails closed, routes to recovery, and never selects a same-run repair branch.",
|
|
2873
|
+
"Frontend review is decided exclusively by the committed typed terminal tools (approve_review / request_review_changes); response-text JSON verdicts and first-line VERDICT markers carry no control-flow weight.",
|
|
2573
2874
|
"Frontend planning must consume the read-only Mock assessment strategy produced after scouting; MOCK_STRATEGY: blocked must not pass the deterministic Mock contract gate.",
|
|
2574
2875
|
"Mock implementations must preserve the real request path as the default, require explicit test/dev activation, and never rely on commenting out the real request.",
|
|
2575
2876
|
"Mock-backed behavior evidence proves only the documented frontend contract, never real API integration.",
|
|
2576
2877
|
"frontend-implementation DAGs must complete deterministic static verification before final review. Behavior verification is also required when the task declares a behavior entrypoint or the implementation contract contains a non-static verification target; static-only contracts must map every target to the declared static entrypoint.",
|
|
2577
|
-
"
|
|
2878
|
+
"Frontend closeout renders only from committed facts; a weak status (failed / not-run / baseline-debt / mock-backed / pending) can never be rewritten into a stronger one (passed / real-integrated).",
|
|
2578
2879
|
`Frontend risk classification: ${frontendRisk.selectedRisk} — ${frontendRisk.reason}`,
|
|
2579
2880
|
frontendRisk.forceFullGates
|
|
2580
2881
|
? "High-risk or supervised: keep full design gates; do not weaken write boundaries."
|
|
@@ -2590,7 +2891,14 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2590
2891
|
return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints, blockedReason);
|
|
2591
2892
|
}
|
|
2592
2893
|
const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
|
|
2593
|
-
|
|
2894
|
+
// The verification bundle schema requires at least one static command. When
|
|
2895
|
+
// a real project exposes no usable verification command at all, run an
|
|
2896
|
+
// explicit no-op marker instead of a command that is guaranteed to fail:
|
|
2897
|
+
// the trace then records not-run honestly and the advisory asks the task
|
|
2898
|
+
// to declare verification commands and regenerate.
|
|
2899
|
+
const staticFallbackCommands = fallbackVerifyCommands.staticCommands.length > 0
|
|
2900
|
+
? fallbackVerifyCommands.staticCommands
|
|
2901
|
+
: [FRONTEND_NO_STATIC_VERIFICATION_MARKER];
|
|
2594
2902
|
const behaviorFallbackCommands = fallbackVerifyCommands.behaviorCommands;
|
|
2595
2903
|
const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
|
|
2596
2904
|
repoRoot: sources.repoRoot,
|
|
@@ -2689,11 +2997,65 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2689
2997
|
...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
|
|
2690
2998
|
].join("\n");
|
|
2691
2999
|
const advisories = [];
|
|
3000
|
+
if (!hasDeclaredFrontendVerification &&
|
|
3001
|
+
fallbackVerifyCommands.staticCommands.length === 0 &&
|
|
3002
|
+
fallbackVerifyCommands.behaviorCommands.length === 0) {
|
|
3003
|
+
advisories.push("未发现可用的前端验证命令:目标项目没有可读取的 package.json scripts,也未探测到本地 TypeScript,verify 节点将没有静态/行为命令可执行。请在 task.json --verify 或执行约束.md 的验证约束中显式声明命令(例如 node --check src/app.js),然后重新生成 DAG。");
|
|
3004
|
+
}
|
|
2692
3005
|
if (frontendSourceMentionsMock(frontendSources) &&
|
|
2693
3006
|
frontendMockStrategyMustBeNotNeeded(frontendSources)) {
|
|
2694
3007
|
advisories.push("auto 模式已将 Mock 策略收窄为 not-needed:任务源提到接口/API/Mock 需求,但仓库无确认 Mock 能力或无确定性 Mock 验证命令。若项目规范要求 Mock,请声明 frontendMock.verifyCommands 或 policy:required 后重新生成 DAG。");
|
|
2695
3008
|
}
|
|
2696
|
-
const openspecGate = resolveFrontendOpenspecGateConfig(sources);
|
|
3009
|
+
const openspecGate = await resolveFrontendOpenspecGateConfig(sources);
|
|
3010
|
+
const requiresOpenspecClassification = openspecGate.openspecPolicy === "cited" &&
|
|
3011
|
+
openspecGate.openspecCandidatePaths.length > 0;
|
|
3012
|
+
// Prompt 内联的候选只保留与任务相关的子集:mandatory(任务显式声明/
|
|
3013
|
+
// 引用)始终保留;scan-strict 候选按路径段是否命中任务源关键词过滤。
|
|
3014
|
+
// runtime 的选型/read 门禁仍消费完整 openspecCandidatePaths——这里只
|
|
3015
|
+
// 减小 prompt 体积,不改变门禁语义;未提及的候选 runtime 默认 irrelevant。
|
|
3016
|
+
const promptCandidatePaths = filterRelevantOpenspecCandidates({
|
|
3017
|
+
candidates: openspecGate.openspecCandidateSummaries.map((candidate) => candidate.path),
|
|
3018
|
+
mandatoryPaths: openspecGate.openspecMandatoryPaths,
|
|
3019
|
+
sourceMarkdown: [
|
|
3020
|
+
sources.requirementMarkdown,
|
|
3021
|
+
sources.constraintMarkdown ?? "",
|
|
3022
|
+
].join("\n"),
|
|
3023
|
+
maxCandidates: 24,
|
|
3024
|
+
});
|
|
3025
|
+
const candidateByPath = new Map(openspecGate.openspecCandidateSummaries.map((candidate) => [
|
|
3026
|
+
candidate.path,
|
|
3027
|
+
candidate,
|
|
3028
|
+
]));
|
|
3029
|
+
// Preserve the filter's mandatory/relevance order. Sorting then slicing here
|
|
3030
|
+
// used to be able to drop a mandatory path after it passed the Top-K filter.
|
|
3031
|
+
const promptCandidates = promptCandidatePaths.flatMap((candidatePath) => {
|
|
3032
|
+
const candidate = candidateByPath.get(candidatePath);
|
|
3033
|
+
return candidate
|
|
3034
|
+
? [
|
|
3035
|
+
{
|
|
3036
|
+
path: candidate.path,
|
|
3037
|
+
kind: candidate.kind,
|
|
3038
|
+
score: candidate.score,
|
|
3039
|
+
reasons: candidate.reasons,
|
|
3040
|
+
source: candidate.source,
|
|
3041
|
+
},
|
|
3042
|
+
]
|
|
3043
|
+
: [];
|
|
3044
|
+
});
|
|
3045
|
+
const openspecSelectionContext = JSON.stringify({
|
|
3046
|
+
schemaVersion: 1,
|
|
3047
|
+
schemaId: "frontend-openspec-selection-v1",
|
|
3048
|
+
candidates: promptCandidates,
|
|
3049
|
+
mandatoryPaths: openspecGate.openspecMandatoryPaths,
|
|
3050
|
+
});
|
|
3051
|
+
const scopedOpenspecContext = promptCandidates.length > 0
|
|
3052
|
+
? [
|
|
3053
|
+
"## Task-relevant OpenSpec Top-K (generation frozen)",
|
|
3054
|
+
`Policy: ${openspecGate.openspecPolicy}. This is the bounded prompt index; the deterministic gate retains ${openspecGate.openspecCandidatePaths.length} frozen candidates.`,
|
|
3055
|
+
...promptCandidates.map((candidate) => `- ${openspecGate.openspecMandatoryPaths.includes(candidate.path) ? "mandatory" : "candidate"}: ${candidate.path} (${candidate.kind}; ${candidate.source})`),
|
|
3056
|
+
"Apply or cite only paths relevant to the concrete contract. Report applied rules with path/section/line and surface conflicts or missing specifications; unlisted candidates default to irrelevant unless the deterministic gate requires them.",
|
|
3057
|
+
].join("\n")
|
|
3058
|
+
: "";
|
|
2697
3059
|
// Generation-frozen component/theme specification bucket (ADR 0016). Derived
|
|
2698
3060
|
// from the classified component/theme/rule.components buckets; the prewrite
|
|
2699
3061
|
// gate consumes it to enforce uiComponentChoices presence and specReference
|
|
@@ -2710,7 +3072,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2710
3072
|
advisories.push("openspec 策略 cited:契约声明的 requiredReadPaths 与任务源引用均为空,prewrite gate 不强制读取 openspec;如需增强规范门禁,请在 task.json.frontendOpenspec.requiredReadPaths 声明必读路径或在任务源中显式引用 openspec 文件。");
|
|
2711
3073
|
}
|
|
2712
3074
|
else {
|
|
2713
|
-
advisories.push(`openspec 策略 cited:候选 ${openspecGate.openspecCandidatePaths.length} 个(declared ${openspecGate.openspecCandidateSources.declared.length} / task-source-cited ${openspecGate.openspecCandidateSources.taskSourceCited.length}),plan
|
|
3075
|
+
advisories.push(`openspec 策略 cited:候选 ${openspecGate.openspecCandidatePaths.length} 个(declared ${openspecGate.openspecCandidateSources.declared.length} / task-source-cited ${openspecGate.openspecCandidateSources.taskSourceCited.length}),plan 必须在 typed decision ledger 的 uiComponentChoices.specReference 中声明并通过真实 read 事件佐证。`);
|
|
2714
3076
|
}
|
|
2715
3077
|
}
|
|
2716
3078
|
else {
|
|
@@ -2748,13 +3110,23 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2748
3110
|
allowedPaths: readOnlyPaths,
|
|
2749
3111
|
forbiddenPaths,
|
|
2750
3112
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2751
|
-
outputContract: "Markdown contract with
|
|
3113
|
+
outputContract: "Typed requirement facts plus a concise Markdown contract. Submit through the incremental typed tools record_requirement / record_constraint / record_evidence_expectation / record_handoff_intent / record_open_question / record_split_proposal / record_openspec_selection, then call finalize_contract exactly once. Requirements use stable REQ/BR/AC identifiers with source spans and a disposition (explicit | repository-resolvable | assumption | blocking); each requirement registers evidence expectations across static/behavior/Mock/real-integration (required | optional | not-applicable), and UI-visible or interactive requirements register a non-blocking frontend-test handoff intent. End finalize_contract with a single contract disposition of ready | ready-with-assumptions | blocked. Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations; do not fix target files, components, or implementation methods as requirements. When OpenSpec candidates exist, classify only the ones you actually use: call record_openspec_selection once per required/relevant path; never enumerate irrelevant candidates (unmentioned defaults to irrelevant) and never emit a fenced selection JSON. No file writes.",
|
|
2752
3114
|
subtask_prompt: [
|
|
2753
|
-
"Read task source and produce a concise frontend implementation contract.",
|
|
2754
|
-
"
|
|
3115
|
+
"Read task source and produce a concise frontend implementation contract as typed requirement facts plus narrative Markdown.",
|
|
3116
|
+
"Assign each requirement the SAME id as the ledger canonical requirement it covers (sourceBinding.requirementIds, e.g. AC-001) — do NOT invent new REQ/BR prefixed ids for canonical requirements: the compiled contract must match the ledger canonical requirement ids exactly or schema validation rejects it (unknown requirement id). Requirements use the canonical id with a source span (task-source section or repository file:line). Label each requirement's disposition as explicit | repository-resolvable | assumption | blocking; a blocking requirement must name its owner (human-decision or external-state) and evidence refs.",
|
|
3117
|
+
"Source fidelity ledger: when the DAG sourceBinding carries a requirement→fragment mapping (requirementToFragments, e.g. REQ-SRC-* ids from the managed ledger), each record_requirement MUST declare the fragments that requirement is bound to: set sourceFragmentIds to the mapped fragment ids (the authoritative provenance evidence the design policy verifies). sourceRefs (fragment→path display refs) are optional — declare them only when you have the exact path from the materialized source; otherwise omit them rather than inventing paths. Declare exactly what the ledger binds — do not invent ids, do not omit them, and do not re-derive them from prose. A requirement that the ledger binds but the contract omits (or fabricates) fails writer admission.",
|
|
3118
|
+
"Register evidence expectations for each requirement across static, behavior, Mock, and real integration as required | optional | not-applicable; required must follow from user requirements, task risk, or project governance, never from model convenience. For UI-visible or interactive requirements, register a non-blocking frontend-test handoff intent.",
|
|
3119
|
+
"Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations. Do not fix target files, components, styling, or implementation methods as requirements; leave those to Scout and Plan.",
|
|
3120
|
+
"If the task is too large for one bounded writer, record a task split proposal instead of silently widening scope.",
|
|
3121
|
+
"End the contract with a single disposition: ready, ready-with-assumptions (bounded assumptions that do not change product behavior), or blocked.",
|
|
3122
|
+
scopedOpenspecContext,
|
|
3123
|
+
...(requiresOpenspecClassification ? [
|
|
3124
|
+
"Classify OpenSpec candidates incrementally while contracting — only the ones you actually use. Call record_openspec_selection once per path with disposition required (must be read and cited by the plan) or relevant (may inform planning). Never call it for irrelevant candidates and never list them: candidates you do not mention are treated as irrelevant by the runtime. Explicit task declarations / source citations are already required and must-read regardless; you never need to re-declare them.",
|
|
3125
|
+
"Mandatory paths are enforced by the runtime from the frozen task configuration — do not enumerate them, do not downgrade them.",
|
|
3126
|
+
openspecSelectionContext,
|
|
3127
|
+
] : []),
|
|
2755
3128
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2756
|
-
|
|
2757
|
-
sourceContext,
|
|
3129
|
+
sourceContexts.contract,
|
|
2758
3130
|
].join("\n\n"),
|
|
2759
3131
|
},
|
|
2760
3132
|
{
|
|
@@ -2764,17 +3136,19 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2764
3136
|
executor: "pi",
|
|
2765
3137
|
complexity: mapTaskComplexity(taskConfig.complexity),
|
|
2766
3138
|
writePolicy: "read-only",
|
|
3139
|
+
retryPolicy: FRONTEND_SCOUT_COMPLETENESS_RETRY_POLICY,
|
|
2767
3140
|
allowedPaths: readOnlyPaths,
|
|
2768
3141
|
forbiddenPaths,
|
|
2769
3142
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2770
|
-
outputContract: "Markdown scout report
|
|
3143
|
+
outputContract: "Markdown scout report covering target surface and design evidence (frontend stack, routes, components, styling system, existing design conventions, state/data flow, test entry points, reuse opportunities, risks). Submit through the incremental evidence tools record_target_surface / record_design_evidence. A complete target surface is mandatory before Plan; if it cannot be proven, commit blocked with unresolved paths so this Scout node retries rather than shifting discovery to Plan. No fixed TARGET_SURFACE section title is required — target surface and design evidence are reported as committed typed facts. No file writes.",
|
|
2771
3144
|
subtask_prompt: [
|
|
2772
3145
|
"Inspect frontend code, routing, components, styles, package scripts, and tests.",
|
|
2773
3146
|
"Return code and design observations, existing reuse opportunities, and verification entry points.",
|
|
2774
|
-
"
|
|
3147
|
+
"Report target surface and design evidence as facts (no fixed section title required): completeness, entrypoint, routeOrMount, implementationPaths, testPaths, dataSource, allowedPathConflicts, unresolvedPaths. Use repository-relative POSIX paths. A complete surface must name at least one proven entrypoint, implementation path, or test path and set unresolvedPaths to []; if any target ownership remains unknown, commit completeness=blocked with every unresolved path instead of guessing. implementationPaths and testPaths must name the existing files/directories that actually own the requested behavior; allowedPathConflicts must list every discovered path not covered by task allowedPaths, or [] when none exists.",
|
|
2775
3148
|
"Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
|
|
2776
3149
|
"Read-only: do not modify repository files.",
|
|
2777
|
-
|
|
3150
|
+
sourceContexts.scout,
|
|
3151
|
+
scopedOpenspecContext,
|
|
2778
3152
|
].join("\n\n"),
|
|
2779
3153
|
},
|
|
2780
3154
|
{
|
|
@@ -2784,246 +3158,194 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2784
3158
|
executor: "pi",
|
|
2785
3159
|
complexity: "MED",
|
|
2786
3160
|
writePolicy: "read-only",
|
|
2787
|
-
|
|
2788
|
-
|
|
3161
|
+
retryPolicy: FRONTEND_PLAN_LADDER_RETRY_POLICY,
|
|
3162
|
+
allowedPaths: readOnlyPaths,
|
|
3163
|
+
forbiddenPaths,
|
|
3164
|
+
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2789
3165
|
structuredContractOutput: {
|
|
2790
|
-
schemaId:
|
|
3166
|
+
schemaId: "frontend-implementation-contract-plan-patch-v1",
|
|
2791
3167
|
retryOnInvalid: true,
|
|
2792
3168
|
skeleton: frontendContractSkeleton,
|
|
2793
3169
|
},
|
|
2794
|
-
|
|
2795
|
-
forbiddenPaths,
|
|
2796
|
-
skills: [],
|
|
2797
|
-
outputContract: "JSON-only patch output: exactly ONE fenced json object (```json ... ```) containing only the editable RFC 7386 plan patch for the runtime contract skeleton, immediately followed by exactly one ```openspec-citations``` fenced citation block. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime applies the patch, validates it, and promotes canonical full-contract JSON for downstream review. Do NOT emit a full contract, Markdown plan explanation, raw JSON, or any other fenced block. No file writes.",
|
|
3170
|
+
outputContract: "Typed decision patch only: map frozen requirements to implementation/verification targets and select the needed component, state, data/Mock, styling, and dependency decisions. Use only the record_* tools needed to express those decisions, then call finalize_plan exactly once. Contract owns requirement semantics; Scout owns repository discovery; deterministic runtime owns schema, protected fields, path containment, and command validation. No Markdown narrative or file writes.",
|
|
2798
3171
|
subtask_prompt: [
|
|
2799
|
-
"
|
|
2800
|
-
"
|
|
2801
|
-
"
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
"
|
|
2805
|
-
"
|
|
2806
|
-
"Each requirement must state its user-observable or logic-observable expectedOutcome. Each interaction must state its trigger and expectedBehavior. IDs plus file paths are not sufficient behavior semantics.",
|
|
2807
|
-
requirementCoverageInstruction,
|
|
2808
|
-
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2809
|
-
verificationTargetFileInstruction,
|
|
2810
|
-
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2811
|
-
plannerSourceReadInstruction,
|
|
3172
|
+
"Plan only the delta between the frozen frontend-contract-pi facts and frontend-scout-pi target surface. Do not reinterpret the task, repeat requirements, search the repository, or choose implementation order.",
|
|
3173
|
+
"Record only: requirement-to-file/verification coverage; component/styling choices; applicable UI state and interaction behavior; data/Mock strategy; and a dependency policy or genuine evidence gap. Reuse Scout paths. If scope is missing, record a blocking gap instead of inventing a path.",
|
|
3174
|
+
"Use the typed tool schemas as the field contract. Runtime owns schemaVersion, sourceBinding, riskLevel, targets.files, mockApi.productionDefaultOff, aliases, command allowlisting, path containment, and final validation; do not restate those rules or emit a full JSON contract.",
|
|
3175
|
+
`Cover each frozen requirement ID exactly once: ${requirementIds.join(", ") || "(none)"}. Bind every verification target to a listed frozen command and a Scout-confirmed file. Use static targets for project-wide commands.`,
|
|
3176
|
+
...(requiresOpenspecClassification ? ["When a component choice uses an OpenSpec selection, cite that selection; otherwise do not classify unrelated candidates."] : []),
|
|
3177
|
+
"Call finalize_plan exactly once after the necessary typed facts. Return no Markdown narrative.",
|
|
3178
|
+
"TOOL-ONLY PLAN: Do not read Contract/Scout stdout, task sources, or Scout-confirmed target files. Contract and Scout already own evidence discovery; use the injected upstream facts, record a genuine evidence gap when those facts are insufficient, and start committing record_* facts immediately. For decision=new, pass sourceRequirementIds to record_component_choice; runtime derives the exact PRD citation from the frozen ledger.",
|
|
2812
3179
|
fixedVerificationContext,
|
|
2813
|
-
|
|
3180
|
+
scopedOpenspecContext,
|
|
2814
3181
|
mockContextBlock,
|
|
2815
|
-
|
|
3182
|
+
frontendContractFieldSummary,
|
|
2816
3183
|
frontendComponentConformanceInstruction,
|
|
2817
|
-
openspecCitationInstruction,
|
|
2818
3184
|
].join("\n\n"),
|
|
2819
3185
|
},
|
|
2820
3186
|
{
|
|
2821
|
-
id: "frontend-design-
|
|
3187
|
+
id: "frontend-design-policy-shell",
|
|
2822
3188
|
depends_on: ["frontend-plan-pi"],
|
|
2823
|
-
role: "
|
|
2824
|
-
executor: "
|
|
2825
|
-
complexity: "
|
|
3189
|
+
role: "verifier",
|
|
3190
|
+
executor: "shell",
|
|
3191
|
+
complexity: "LOW",
|
|
2826
3192
|
writePolicy: "read-only",
|
|
2827
|
-
artifactFirstUpstreamNodeIds: ["frontend-plan-pi"],
|
|
2828
3193
|
allowedPaths: readOnlyPaths,
|
|
2829
3194
|
forbiddenPaths,
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
3195
|
+
outputContract: "Deterministic design policy: materialize the canonical frontend implementation contract from the plan patch, enforce requirement-id retention, Mock strategy/frozen-command binding, writeSet containment, source freshness, and openspec invariants, then evaluate the design policy (write policy result).",
|
|
3196
|
+
subtask_prompt: "Materialize the canonical contract and fail closed unless the deterministic design policy approves. The design verdict is not available at this stage; the writer admission shell enforces it.",
|
|
3197
|
+
shell: {
|
|
3198
|
+
commands: [],
|
|
3199
|
+
frontendDesignPolicy: {
|
|
3200
|
+
schemaVersion: 1,
|
|
3201
|
+
planFromNodeId: "frontend-plan-pi",
|
|
3202
|
+
requiredRequirementIds: requirementIds,
|
|
3203
|
+
allowedMockStrategies: taskConfig.frontendMock?.policy === "disabled" ||
|
|
3204
|
+
frontendMockStrategyMustBeNotNeeded(frontendSources)
|
|
3205
|
+
? ["not-needed"]
|
|
3206
|
+
: taskConfig.frontendMock?.policy === "required"
|
|
3207
|
+
? [
|
|
3208
|
+
"native",
|
|
3209
|
+
"browser-intercept",
|
|
3210
|
+
"request-adapter",
|
|
3211
|
+
]
|
|
3212
|
+
: [
|
|
3213
|
+
"native",
|
|
3214
|
+
"browser-intercept",
|
|
3215
|
+
"request-adapter",
|
|
3216
|
+
"not-needed",
|
|
3217
|
+
],
|
|
3218
|
+
mockCommandLabels: mockVerifyEvidence?.commandLabels ?? [],
|
|
3219
|
+
artifactName: "frontend-implementation-contract.json",
|
|
3220
|
+
outputDir: "contracts",
|
|
3221
|
+
requireSourceFreshness: true,
|
|
3222
|
+
implementationWriteSet: implementPaths.writeSet,
|
|
3223
|
+
openspecPolicy: openspecGate.openspecPolicy,
|
|
3224
|
+
openspecSpecRoots: taskConfig.frontendOpenspec?.specRoots ?? [
|
|
3225
|
+
...DEFAULT_FRONTEND_SPEC_ROOTS,
|
|
3226
|
+
],
|
|
3227
|
+
...(requiresOpenspecClassification
|
|
3228
|
+
? { openspecSelectionNodeId: "frontend-contract-pi" }
|
|
3229
|
+
: {}),
|
|
3230
|
+
openspecMandatoryPaths: openspecGate.openspecMandatoryPaths,
|
|
3231
|
+
openspecCandidateSources: {
|
|
3232
|
+
declared: openspecGate.openspecCandidateSources.declared,
|
|
3233
|
+
taskSourceCited: openspecGate.openspecCandidateSources.taskSourceCited,
|
|
3234
|
+
scanStrict: openspecGate.openspecCandidateSources.scanStrict,
|
|
3235
|
+
},
|
|
3236
|
+
openspecCandidatePaths: openspecGate.openspecCandidatePaths,
|
|
3237
|
+
...(openspecGate.openspecCandidateSnapshots
|
|
3238
|
+
? {
|
|
3239
|
+
openspecCandidateSnapshots: openspecGate.openspecCandidateSnapshots,
|
|
3240
|
+
}
|
|
3241
|
+
: {}),
|
|
3242
|
+
componentSpecCandidatePaths,
|
|
3243
|
+
allowedDependencies: declaredDependencies,
|
|
3244
|
+
},
|
|
3245
|
+
cwd: ".",
|
|
3246
|
+
timeoutMs: 60000,
|
|
2860
3247
|
},
|
|
2861
|
-
allowedPaths: readOnlyPaths,
|
|
2862
|
-
forbiddenPaths,
|
|
2863
|
-
skills: [],
|
|
2864
|
-
outputContract: "When the initial design review requests revision, return exactly ONE fenced json object (```json ... ```) containing an RFC 7386 merge-patch delta against the original frontend-implementation-contract-v1 (only the fields you change; null deletes a key; arrays and scalars replace; plain objects merge recursively), immediately followed by exactly one ```openspec-citations``` fenced citation block. Do NOT emit a full contract, Markdown explanation, or prose — the output is JSON-only; the gate applies the patch on the original contract and renders plan.md deterministically. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block or raw JSON. No file writes.",
|
|
2865
|
-
subtask_prompt: [
|
|
2866
|
-
"Consume the hash-bound frontend-plan-pi contract artifact and frontend-design-review-pi findings. Read the artifact only for fields affected by a Required Plan Correction.",
|
|
2867
|
-
"This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce an RFC 7386 merge-patch delta against the original contract JSON that addresses every Required Plan Correction from the design findings.",
|
|
2868
|
-
"The patch delta may update editable contract fields such as requirements, implementationSteps, targets.routes/publicApiChanges, uiStates, interactions, mockApi.strategy/activation/endpoints, dependencyPolicy, stylingStrategy, uiComponentChoices, verificationTargets, evidenceGaps, residualRisks, and realIntegrationGap. It must not modify protected schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff. Only include fields you change; omit unchanged fields (the gate applies the patch on the original contract). null deletes a key; arrays and scalars replace; plain objects merge recursively.",
|
|
2869
|
-
"Only include changed fields. For a changed requirement, preserve its expectedOutcome, implementationTargets, and verificationTargetIds; do not resend unchanged requirements.",
|
|
2870
|
-
"Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
|
|
2871
|
-
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
2872
|
-
"Output exactly two adjacent blocks: (1) one fenced json object containing the merge-patch delta; (2) one openspec-citations citation block. Do NOT emit a full contract, Markdown explanation, or prose. Do not emit raw JSON or JSON objects in prose, secrets, or unsafe paths.",
|
|
2873
|
-
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the effective (merged) contract; do not reduce behavior semantics to IDs and paths.",
|
|
2874
|
-
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2875
|
-
verificationTargetFileInstruction,
|
|
2876
|
-
fixedVerificationContext,
|
|
2877
|
-
sourceContext,
|
|
2878
|
-
frontendPlanFieldGuide,
|
|
2879
|
-
mockContextBlock,
|
|
2880
|
-
frontendComponentConformanceInstruction,
|
|
2881
|
-
openspecCitationInstruction,
|
|
2882
|
-
].join("\n\n"),
|
|
2883
3248
|
},
|
|
2884
3249
|
{
|
|
2885
|
-
id: "frontend-
|
|
2886
|
-
depends_on: [
|
|
2887
|
-
"frontend-plan-revision-pi",
|
|
2888
|
-
"frontend-plan-pi",
|
|
2889
|
-
"frontend-design-review-pi",
|
|
2890
|
-
],
|
|
2891
|
-
dependsPolicy: "all-or-condition-skip",
|
|
2892
|
-
runIf: "$.nodes['frontend-design-review-pi'].firstVerdictLine == 'VERDICT: request-revision'",
|
|
3250
|
+
id: "frontend-design-review-pi",
|
|
3251
|
+
depends_on: ["frontend-design-policy-shell"],
|
|
2893
3252
|
role: "reviewer",
|
|
2894
3253
|
executor: "pi",
|
|
2895
3254
|
complexity: "MED",
|
|
2896
3255
|
writePolicy: "read-only",
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
"frontend-plan-pi",
|
|
2900
|
-
],
|
|
3256
|
+
readBudget: FRONTEND_READ_BUDGETS.designReview,
|
|
3257
|
+
retryPolicy: DEFAULT_READ_ONLY_PI_RETRY_POLICY,
|
|
2901
3258
|
allowedPaths: readOnlyPaths,
|
|
2902
3259
|
forbiddenPaths,
|
|
2903
3260
|
skills: FRONTEND_DESIGN_REVIEW_SKILLS,
|
|
2904
|
-
|
|
2905
|
-
outputContract: "For the effective frontend plan, return plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings and Checked Items. No file writes.",
|
|
3261
|
+
outputContract: "Authoritative typed design terminal via approve_design / request_design_changes tools. No JSON verdict; the committed typed design fact is the only authority. No file writes.",
|
|
2906
3262
|
subtask_prompt: [
|
|
2907
|
-
"Audit the
|
|
2908
|
-
"
|
|
2909
|
-
"
|
|
2910
|
-
"
|
|
2911
|
-
"
|
|
2912
|
-
"
|
|
2913
|
-
"
|
|
3263
|
+
"Audit the frontend plan before implementation. frontend-plan-pi is emitted to you as canonical full-contract JSON after the runtime applied and validated the planner's editable patch against its protected skeleton; there is no separate plan prose.",
|
|
3264
|
+
"Your authoritative terminal verdict is exactly one committed typed tool call: approve_design or request_design_changes. Call exactly one of them; after calling one, do not call the other.",
|
|
3265
|
+
"request_design_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
|
|
3266
|
+
"Your verdict is consumed only as deterministic data input by frontend-writer-admission-shell; it no longer drives any branch or gate. request_design_changes blocks writer admission (terminal).",
|
|
3267
|
+
"Request design changes when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence. A static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint. not-needed otherwise requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case the plan must preserve the real request path and record the Real Integration Gap.",
|
|
3268
|
+
"Also request design changes for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
|
|
3269
|
+
"Component selection conformance is a hard blocking condition: request_design_changes when the frontend spec (component/theme/rule.components bucket) already defines a component for a purpose but the plan selects another or self-invents one without a declared deviation; when uiComponentChoices is missing/empty for UI-visible work while the frozen component/theme bucket is non-empty; when a decision=specified specReference.path is missing a ledger OpenSpec reference or successful read event; or when a decision=new component lacks a traceable task-source/PRD specReference. A PRD reference for decision=new is not an OpenSpec citation and must not be rejected merely for lacking an OpenSpec read event.",
|
|
3270
|
+
"You must NOT make authoritative assertions about the execution result of frozen verification commands (typecheck/test/build/lint/etc.). Predicting that a command will necessarily pass or fail, or declaring an acceptance criterion unreachable on that basis, is out of your authority: command results are deterministically established by frontend-verify-shell. Any concern about verification feasibility must be recorded only as a non-blocking verification concern in findings (severity must not be Critical, and it must never be the sole fatal basis for request_design_changes). Only semantic design defects (component selection, state flow, interaction contract, or conflicts with the specification) may be Critical. A pure command-will-fail prediction must not be classified as contract-requirement-gap.",
|
|
2914
3271
|
"Read-only: do not modify repository files.",
|
|
3272
|
+
"LARGE-FILE AUDIT (avoid full reads): style/theme audit files can be large (e.g. styles.css is often hundreds of KB). Prefer grep to locate the exact rules/variables you must verify (e.g. grep the oc- class, is-* modifier, or --oc- theme variables with their line numbers), then read only the narrow line range when surrounding context is needed. Do not read a large style/test file in full — a single full read can exhaust the read budget and fail the attempt.",
|
|
3273
|
+
"Canonical contract JSON: frontend-design-policy-shell prints its absolute output path in the upstream output (Contract: /abs/path/.harness/dag-runs/.../contracts/frontend-implementation-contract.json). Read exactly that path when you need the full contract — never resolve a bare contracts/... path against the repository root (it does not exist there), and do not hunt for substitutes. Implementation target files inside the writeSet (e.g. TaskSourceBadge.tsx or its test file) are created later by the implement node: do not read them and do not treat their absence as a design defect.",
|
|
2915
3274
|
fixedVerificationContext,
|
|
2916
|
-
|
|
3275
|
+
sourceContexts.designReview,
|
|
3276
|
+
scopedOpenspecContext,
|
|
2917
3277
|
frontendContractFieldSummary,
|
|
2918
3278
|
mockContextBlock,
|
|
2919
3279
|
].join("\n\n"),
|
|
2920
3280
|
},
|
|
2921
3281
|
{
|
|
2922
|
-
id: "frontend-
|
|
2923
|
-
depends_on: [
|
|
2924
|
-
"frontend-final-design-review-pi",
|
|
2925
|
-
"frontend-design-review-pi",
|
|
2926
|
-
"frontend-plan-revision-pi",
|
|
2927
|
-
"frontend-plan-pi",
|
|
2928
|
-
],
|
|
2929
|
-
dependsPolicy: "all-or-condition-skip",
|
|
3282
|
+
id: "frontend-writer-admission-shell",
|
|
3283
|
+
depends_on: ["frontend-design-policy-shell", "frontend-design-review-pi"],
|
|
2930
3284
|
role: "verifier",
|
|
2931
3285
|
executor: "shell",
|
|
2932
3286
|
complexity: "LOW",
|
|
2933
3287
|
writePolicy: "read-only",
|
|
2934
3288
|
allowedPaths: readOnlyPaths,
|
|
2935
3289
|
forbiddenPaths,
|
|
2936
|
-
outputContract: "Deterministic
|
|
2937
|
-
subtask_prompt: "Fail closed unless the
|
|
3290
|
+
outputContract: "Deterministic writer admission: require design review verdict pass, freeze the pre-writer worktree/lint baselines, derive the concrete writeSet + admission digest, and write contracts/frontend-writer-admission-result.json (schemaId frontend-writer-admission-shell-v1).",
|
|
3291
|
+
subtask_prompt: "Fail closed unless the design review approved and the deterministic admission derived a concrete, non-empty writeSet. The admission result is the only write authorization.",
|
|
2938
3292
|
shell: {
|
|
2939
3293
|
commands: [],
|
|
2940
|
-
|
|
3294
|
+
frontendWriterAdmission: {
|
|
2941
3295
|
schemaVersion: 1,
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3296
|
+
designReviewFromNodeId: "frontend-design-review-pi",
|
|
3297
|
+
frozenCommandLabels: [
|
|
3298
|
+
...staticVerifyEvidence.commandLabels,
|
|
3299
|
+
...behaviorVerifyEvidence.commandLabels,
|
|
3300
|
+
...(mockVerifyEvidence?.commandLabels ?? []),
|
|
3301
|
+
],
|
|
2948
3302
|
allowedMockStrategies: taskConfig.frontendMock?.policy === "disabled" ||
|
|
2949
3303
|
frontendMockStrategyMustBeNotNeeded(frontendSources)
|
|
2950
3304
|
? ["not-needed"]
|
|
2951
3305
|
: taskConfig.frontendMock?.policy === "required"
|
|
2952
|
-
? [
|
|
3306
|
+
? [
|
|
3307
|
+
"native",
|
|
3308
|
+
"browser-intercept",
|
|
3309
|
+
"request-adapter",
|
|
3310
|
+
]
|
|
2953
3311
|
: [
|
|
2954
3312
|
"native",
|
|
2955
3313
|
"browser-intercept",
|
|
2956
3314
|
"request-adapter",
|
|
2957
3315
|
"not-needed",
|
|
2958
3316
|
],
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
openspecPolicy: openspecGate.openspecPolicy,
|
|
2966
|
-
openspecCandidateSources: {
|
|
2967
|
-
declared: openspecGate.openspecCandidateSources.declared,
|
|
2968
|
-
taskSourceCited: openspecGate.openspecCandidateSources.taskSourceCited,
|
|
2969
|
-
scanStrict: openspecGate.openspecCandidateSources.scanStrict,
|
|
2970
|
-
},
|
|
2971
|
-
openspecCandidatePaths: openspecGate.openspecCandidatePaths,
|
|
2972
|
-
componentSpecCandidatePaths,
|
|
3317
|
+
...(lintShellCommands.length > 0 && lintVerifyEvidence
|
|
3318
|
+
? {
|
|
3319
|
+
lintCommands: lintShellCommands,
|
|
3320
|
+
lintEvidence: lintVerifyEvidence,
|
|
3321
|
+
}
|
|
3322
|
+
: {}),
|
|
2973
3323
|
},
|
|
2974
3324
|
cwd: ".",
|
|
2975
3325
|
timeoutMs: 60000,
|
|
2976
3326
|
},
|
|
2977
3327
|
},
|
|
2978
|
-
...(lintShellCommands.length > 0 && lintVerifyEvidence
|
|
2979
|
-
? [
|
|
2980
|
-
{
|
|
2981
|
-
id: "frontend-lint-baseline-shell",
|
|
2982
|
-
depends_on: ["frontend-prewrite-gate-shell"],
|
|
2983
|
-
role: "verifier",
|
|
2984
|
-
executor: "shell",
|
|
2985
|
-
complexity: "LOW",
|
|
2986
|
-
writePolicy: "read-only",
|
|
2987
|
-
allowedPaths: readOnlyPaths,
|
|
2988
|
-
forbiddenPaths,
|
|
2989
|
-
outputContract: "Capture writer-preceding lint output as frontend-lint-baseline-v1 without treating existing lint diagnostics as writer failure.",
|
|
2990
|
-
subtask_prompt: "Run the frozen lint commands read-only. Preserve raw output and mark the baseline unavailable on timeout, execution failure, unparseable output, or worktree mutation.",
|
|
2991
|
-
shell: {
|
|
2992
|
-
commands: lintShellCommands,
|
|
2993
|
-
frontendLintBaseline: {
|
|
2994
|
-
schemaVersion: 1,
|
|
2995
|
-
lintCommands: lintShellCommands,
|
|
2996
|
-
lintEvidence: lintVerifyEvidence,
|
|
2997
|
-
},
|
|
2998
|
-
cwd: ".",
|
|
2999
|
-
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3000
|
-
},
|
|
3001
|
-
},
|
|
3002
|
-
]
|
|
3003
|
-
: []),
|
|
3004
3328
|
{
|
|
3005
3329
|
id: implementId,
|
|
3006
|
-
depends_on: [
|
|
3007
|
-
"frontend-prewrite-gate-shell",
|
|
3008
|
-
...(lintShellCommands.length > 0
|
|
3009
|
-
? ["frontend-lint-baseline-shell"]
|
|
3010
|
-
: []),
|
|
3011
|
-
],
|
|
3330
|
+
depends_on: ["frontend-writer-admission-shell"],
|
|
3012
3331
|
...buildFrontendWriterNodeDefaults({
|
|
3013
3332
|
complexity: resolveWriterComplexity(taskConfig),
|
|
3014
3333
|
writeSet: implementPaths.writeSet,
|
|
3015
3334
|
allowedPaths: implementPaths.allowedPaths,
|
|
3016
3335
|
forbiddenPaths,
|
|
3336
|
+
writerOutcomePolicyType: "frontend-facts-v1",
|
|
3017
3337
|
}),
|
|
3018
|
-
outputContract: "
|
|
3338
|
+
outputContract: "The implementation status is derived by the executor from mechanical facts (write-tool events, run delta, write guard, requirement coverage, focused-check), not from any IMPLEMENTATION_OUTCOME first line. Deliver a Markdown summary with Contract Ref (path/schema/hash), Changed Files, Requirements Implemented, UI States, Tests Changed, Verification Attempts, Deviations, and Residual Risks. Follow fixed stages: contract confirm → tests → component/state → API/Mock → focused checks → diff cleanup.",
|
|
3019
3339
|
subtask_prompt: [
|
|
3020
|
-
"Implement against the validated run-owned Frontend Implementation Contract
|
|
3340
|
+
"Implement against the validated run-owned Frontend Implementation Contract materialized by frontend-design-policy-shell (path/schema/hash) and authorized by frontend-writer-admission-shell. Do not rebuild the contract from Markdown alone.",
|
|
3021
3341
|
"The canonical contract already contains the approved requirement, target-file, UI-state, verification, design, and Mock/API decisions. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, or design-review prose, and do not repeat broad repository research. Inspect only contract target files and directly related local code needed to implement them.",
|
|
3022
3342
|
"Execute in fixed stages and report each in the delivery summary: (1) Contract confirm, (2) Tests sync, (3) Component/UI state implementation, (4) API/Mock wiring per contract.mockApi, (5) Focused checks behind frozen entrypoints only, (6) Diff cleanup.",
|
|
3023
3343
|
"Map every requirement id, expectedOutcome, interaction trigger/expectedBehavior, and applicable UI state from the contract to concrete files. Do not invent shell verification commands; only frozen static/behavior entrypoints will run.",
|
|
3024
|
-
"Begin implementation after the contract and its target files are confirmed. Do not spend the turn collecting optional context. If the canonical contract lacks behavior needed to edit safely,
|
|
3344
|
+
"Begin implementation after the contract and its target files are confirmed. Do not spend the turn collecting optional context. If the canonical contract lacks behavior needed to edit safely, stop and state the blocking reason in the summary instead of reopening broad discovery.",
|
|
3345
|
+
"Your implementation status is derived by the executor from mechanical facts (persisted write-tool events, run delta, write guard, requirement coverage, focused-check failures), never from any IMPLEMENTATION_OUTCOME first line. Do not emit an IMPLEMENTATION_OUTCOME first line.",
|
|
3346
|
+
"The node runs a bounded micro-loop: after each write attempt the executor re-runs frozen focused checks and records a per-round diff checkpoint; the write guard stays active every round. Only repair local issues attributable to the current diff (syntax/type/import/format/unit-assert/obvious omission). Never change requirements, design, writeSet, or verification strictness inside the loop.",
|
|
3025
3347
|
"Implement only the approved Mock strategy carried by the validated contract. Preserve the real request path as the default, require explicit test/dev activation, and never comment out or replace the real request with inline data.",
|
|
3026
|
-
"frontend-
|
|
3348
|
+
"frontend-design-policy-shell materialized and validated the canonical contract; frontend-writer-admission-shell authorized the writeSet. Stay within writeSet and preserve unrelated files.",
|
|
3027
3349
|
"For native, browser-intercept, or request-adapter, implement contract-aligned fixtures/states and a dev/test-only activation boundary in this same writer. For not-needed, do not add Mock files or a framework and state the positive reason.",
|
|
3028
3350
|
"Do not write root artifacts/** unless explicitly included in writeSet. Do not claim Browser/visual verification.",
|
|
3029
3351
|
"Edit existing files with the structured edit/write tools. NEVER rewrite Markdown (or any file with quoting/backticks/indentation-sensitive content) via bash sed/awk/echo redirection: escaping mistakes silently corrupt the file and self-repair loops burn the run.",
|
|
@@ -3038,7 +3360,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3038
3360
|
.join("\n\n"),
|
|
3039
3361
|
},
|
|
3040
3362
|
{
|
|
3041
|
-
id: "frontend-verify-
|
|
3363
|
+
id: "frontend-verify-shell",
|
|
3042
3364
|
depends_on: [implementId],
|
|
3043
3365
|
role: "verifier",
|
|
3044
3366
|
executor: "shell",
|
|
@@ -3046,8 +3368,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3046
3368
|
writePolicy: "read-only",
|
|
3047
3369
|
allowedPaths: readOnlyPaths,
|
|
3048
3370
|
forbiddenPaths,
|
|
3049
|
-
outputContract: "Run frozen Mock/static/behavior commands
|
|
3050
|
-
subtask_prompt: "Execute the frontend verification bundle. Preserve per-command evidence;
|
|
3371
|
+
outputContract: "Run frozen Mock/static/behavior commands and materialize the verification trace. Any failure is terminal: no same-run repair branch, failure ownership facts are materialized for recovery.",
|
|
3372
|
+
subtask_prompt: "Execute the frontend verification bundle. Preserve per-command evidence; a failure fails this node (terminal) and routes to recovery.",
|
|
3051
3373
|
shell: {
|
|
3052
3374
|
commands: [],
|
|
3053
3375
|
frontendVerificationBundle: {
|
|
@@ -3061,7 +3383,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3061
3383
|
staticEvidence: staticVerifyEvidence,
|
|
3062
3384
|
behaviorEvidence: behaviorVerifyEvidence,
|
|
3063
3385
|
lintBaselineNodeId: lintShellCommands.length > 0
|
|
3064
|
-
? "frontend-
|
|
3386
|
+
? "frontend-writer-admission-shell"
|
|
3065
3387
|
: undefined,
|
|
3066
3388
|
writerNodeIds: lintShellCommands.length > 0 ? [implementId] : [],
|
|
3067
3389
|
mode: "initial",
|
|
@@ -3070,85 +3392,17 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3070
3392
|
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3071
3393
|
},
|
|
3072
3394
|
},
|
|
3073
|
-
{
|
|
3074
|
-
id: "frontend-repair-pi",
|
|
3075
|
-
depends_on: ["frontend-verify-assess-shell", implementId],
|
|
3076
|
-
runIf: "$.nodes['frontend-verify-assess-shell'].json.eligible == true",
|
|
3077
|
-
...buildFrontendWriterNodeDefaults({
|
|
3078
|
-
complexity: resolveWriterComplexity(taskConfig),
|
|
3079
|
-
writeSet: implementPaths.writeSet,
|
|
3080
|
-
allowedPaths: implementPaths.allowedPaths,
|
|
3081
|
-
forbiddenPaths,
|
|
3082
|
-
}),
|
|
3083
|
-
outputContract: "First non-empty line must be exactly one of: IMPLEMENTATION_OUTCOME: changed; IMPLEMENTATION_OUTCOME: already-satisfied; IMPLEMENTATION_OUTCOME: blocked. Then a repair summary for an eligible repairable assessment. Must not expand writeSet, re-interpret requirements, skip tests, or enable Mock by default.",
|
|
3084
|
-
subtask_prompt: [
|
|
3085
|
-
"Read contracts/frontend-repair-assessment.json and the validated frontend implementation contract.",
|
|
3086
|
-
"This node runs only for eligible=true. Apply the smallest fix for the classified repairable failure inside the original implement writeSet only.",
|
|
3087
|
-
"The repair assessment and canonical implementation contract are complete inputs for this phase. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, or design-review prose, and do not repeat repository-wide discovery.",
|
|
3088
|
-
"Do not change lint/type/test config, do not add .skip/.only, do not comment out real requests, do not default-enable Mock, do not add dependencies.",
|
|
3089
|
-
"Do not re-plan requirements or expand allowed paths. Browser/visual remain not-run.",
|
|
3090
|
-
...(implementPaths.docIndexCompanions.length > 0
|
|
3091
|
-
? [
|
|
3092
|
-
`Doc index sync is MANDATORY: ${implementPaths.docIndexCompanions.join(", ")} are catalog index files for this writeSet. When your repair adds, renames, or removes any indexed file, update ${implementPaths.docIndexCompanions.join(" and ")} in the same run; verification runs check-doc-index and fails the run on a missing index entry.`,
|
|
3093
|
-
]
|
|
3094
|
-
: []),
|
|
3095
|
-
writerDeliveryContract(taskConfig),
|
|
3096
|
-
]
|
|
3097
|
-
.filter((value) => Boolean(value))
|
|
3098
|
-
.join("\n\n"),
|
|
3099
|
-
},
|
|
3100
|
-
{
|
|
3101
|
-
id: "frontend-reverify-shell",
|
|
3102
|
-
depends_on: ["frontend-repair-pi"],
|
|
3103
|
-
role: "verifier",
|
|
3104
|
-
executor: "shell",
|
|
3105
|
-
complexity: "LOW",
|
|
3106
|
-
writePolicy: "read-only",
|
|
3107
|
-
allowedPaths: readOnlyPaths,
|
|
3108
|
-
forbiddenPaths,
|
|
3109
|
-
outputContract: "Post-repair Mock/static/behavior re-verification plus refreshed canonical trace; any failure blocks review.",
|
|
3110
|
-
subtask_prompt: "Re-run the frozen frontend verification bundle after bounded repair and fail on any command or trace failure.",
|
|
3111
|
-
shell: {
|
|
3112
|
-
commands: [],
|
|
3113
|
-
frontendVerificationBundle: {
|
|
3114
|
-
schemaVersion: 1,
|
|
3115
|
-
mockCommands: mockShellCommands,
|
|
3116
|
-
lintCommands: lintShellCommands,
|
|
3117
|
-
staticCommands: staticShellCommands,
|
|
3118
|
-
behaviorCommands: behaviorShellCommands,
|
|
3119
|
-
mockEvidence: mockVerifyEvidence,
|
|
3120
|
-
lintEvidence: lintVerifyEvidence,
|
|
3121
|
-
staticEvidence: staticVerifyEvidence,
|
|
3122
|
-
behaviorEvidence: behaviorVerifyEvidence,
|
|
3123
|
-
lintBaselineNodeId: lintShellCommands.length > 0
|
|
3124
|
-
? "frontend-lint-baseline-shell"
|
|
3125
|
-
: undefined,
|
|
3126
|
-
writerNodeIds: lintShellCommands.length > 0
|
|
3127
|
-
? [implementId, "frontend-repair-pi"]
|
|
3128
|
-
: [],
|
|
3129
|
-
mode: "repair",
|
|
3130
|
-
},
|
|
3131
|
-
cwd: ".",
|
|
3132
|
-
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3133
|
-
},
|
|
3134
|
-
},
|
|
3135
3395
|
{
|
|
3136
3396
|
id: "frontend-review-context-shell",
|
|
3137
|
-
depends_on: [
|
|
3138
|
-
"frontend-reverify-shell",
|
|
3139
|
-
"frontend-repair-pi",
|
|
3140
|
-
"frontend-verify-assess-shell",
|
|
3141
|
-
implementId,
|
|
3142
|
-
],
|
|
3143
|
-
dependsPolicy: "all-or-condition-skip",
|
|
3397
|
+
depends_on: ["frontend-verify-shell", implementId],
|
|
3144
3398
|
role: "verifier",
|
|
3145
3399
|
executor: "shell",
|
|
3146
3400
|
complexity: "LOW",
|
|
3147
3401
|
writePolicy: "read-only",
|
|
3148
3402
|
allowedPaths: readOnlyPaths,
|
|
3149
3403
|
forbiddenPaths,
|
|
3150
|
-
outputContract: "Canonical frontend review context containing validated contract, lint assessment when configured, effective verification trace,
|
|
3151
|
-
subtask_prompt: "Capture the actual diff and bind it to the effective
|
|
3404
|
+
outputContract: "Canonical frontend review context containing validated contract, lint assessment when configured, effective verification trace, optional verify-failure facts, and actual worktree diff.",
|
|
3405
|
+
subtask_prompt: "Capture the actual diff and bind it to the effective verification evidence for final review.",
|
|
3152
3406
|
shell: {
|
|
3153
3407
|
commands: [],
|
|
3154
3408
|
frontendReviewContext: { schemaVersion: 1, requireBaseline: true },
|
|
@@ -3163,79 +3417,70 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3163
3417
|
executor: "pi",
|
|
3164
3418
|
complexity: "HIGH",
|
|
3165
3419
|
writePolicy: "read-only",
|
|
3420
|
+
readBudget: FRONTEND_READ_BUDGETS.finalReview,
|
|
3421
|
+
retryPolicy: FRONTEND_REVIEW_TERMINAL_RETRY_POLICY,
|
|
3166
3422
|
allowedPaths: readOnlyPaths,
|
|
3167
3423
|
forbiddenPaths,
|
|
3168
3424
|
skills: FRONTEND_REVIEW_SKILLS,
|
|
3169
|
-
outputContract: '
|
|
3170
|
-
outputProtocol: REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL,
|
|
3425
|
+
outputContract: 'Authoritative typed review terminal via approve_review / request_review_changes tools. No JSON verdict is required in the response text; the typed terminal fact is the only authority. No file writes.',
|
|
3171
3426
|
subtask_prompt: [
|
|
3172
3427
|
"Review the frontend implementation and verification evidence.",
|
|
3173
|
-
"
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
"Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, frontend lint assessment when lint is configured, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
|
|
3428
|
+
"Your authoritative terminal verdict is exactly one committed typed tool call: approve_review or request_review_changes. Call it once and do not call the other afterwards.",
|
|
3429
|
+
"approve_review means the implementation passes; it must not carry Critical or Important findings. request_review_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
|
|
3430
|
+
"Do NOT emit an equivalent JSON verdict in the response text: the committed typed terminal fact is the only authority and no branch or gate reads response-text JSON verdicts.",
|
|
3431
|
+
"Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, frontend lint assessment when lint is configured, the effective verification trace, and the run-owned actual diff. Then read diff.reviewSummaryPath. The full artifacts/diff_patch.patch is retained only as audit evidence: do NOT read it in full. For semantic review, read only the named per-file diff fragment in the summary/index (in part order when needed) and then the current source file when necessary. Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
|
|
3178
3432
|
"Treat lint status exactly as passed | baseline-debt | failed | unavailable. baseline-debt may continue only with intact evidence and zero diagnostics on writer-changed files; report the tolerated debt count and never rewrite it as lint passed. Typecheck, build, and test still require successful final exits.",
|
|
3179
3433
|
"Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
|
|
3180
|
-
"The contract embedded in frontend-review-context.json is the effective
|
|
3434
|
+
"The contract embedded in frontend-review-context.json is the effective plan materialized by frontend-design-policy-shell. Do not re-open task sources, OpenSpec, AI workspace, design-review, writer summary, or verification node prose. Inspect only the canonical review context, its bound diff, and diff-referenced files when semantic review requires source code.",
|
|
3181
3435
|
"Treat a commented-out real request, default-enabled Mock, production entrypoint importing test mocks, API/fixture contract drift, unauthorized Mock dependency/path, or missing behavior evidence for the selected strategy as at least Important. Mock strategies require Mock-backed evidence. not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case verify that the real request remains the default and the Real Integration Gap is preserved.",
|
|
3182
|
-
"Inspect the frontend-verify-
|
|
3436
|
+
"Inspect the frontend-verify-shell evidence in the review context directly, including the production/default-real-path static check, and require Mock activation to be off for that check.",
|
|
3183
3437
|
"Distinguish Mock-backed evidence from real API integration evidence and preserve the Real Integration Gap when the backend was not exercised.",
|
|
3184
3438
|
"Review implementation quality, behavior/state coverage, verification evidence, and maintainability. Read-only: do not modify files.",
|
|
3185
3439
|
].join("\n\n"),
|
|
3186
3440
|
},
|
|
3187
3441
|
{
|
|
3188
|
-
id: "frontend-
|
|
3189
|
-
depends_on: ["frontend-review-pi"],
|
|
3190
|
-
role: "
|
|
3442
|
+
id: "frontend-closeout-shell",
|
|
3443
|
+
depends_on: ["frontend-review-context-shell", "frontend-review-pi"],
|
|
3444
|
+
role: "closeout",
|
|
3191
3445
|
executor: "shell",
|
|
3192
3446
|
complexity: "LOW",
|
|
3193
3447
|
writePolicy: "read-only",
|
|
3194
3448
|
allowedPaths: readOnlyPaths,
|
|
3195
3449
|
forbiddenPaths,
|
|
3196
|
-
outputContract:
|
|
3197
|
-
subtask_prompt:
|
|
3450
|
+
outputContract: "Deterministic closeout rendered from committed facts only: coverage matrix, lint status, integration facts, typed review verdict, cumulative diff, browser/visual not-run, risks, and follow-up. No model summaries are re-interpreted.",
|
|
3451
|
+
subtask_prompt: "Render the closeout deterministically from frontend-review-context.json, the verification trace, and the committed typed review terminal fact.",
|
|
3198
3452
|
shell: {
|
|
3199
3453
|
commands: [],
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
routingAccept: ["request-revision"],
|
|
3204
|
-
label: "frontend review",
|
|
3205
|
-
source: "json-review-verdict",
|
|
3454
|
+
frontendCloseout: {
|
|
3455
|
+
schemaVersion: 1,
|
|
3456
|
+
reviewFromNodeId: "frontend-review-pi",
|
|
3206
3457
|
},
|
|
3207
3458
|
cwd: ".",
|
|
3208
3459
|
timeoutMs: 60000,
|
|
3209
3460
|
},
|
|
3210
3461
|
},
|
|
3211
|
-
{
|
|
3212
|
-
id: "frontend-closeout-pi",
|
|
3213
|
-
depends_on: [
|
|
3214
|
-
"frontend-review-context-shell",
|
|
3215
|
-
"frontend-review-gate-shell",
|
|
3216
|
-
],
|
|
3217
|
-
role: "closeout",
|
|
3218
|
-
executor: "pi",
|
|
3219
|
-
complexity: "MED",
|
|
3220
|
-
writePolicy: "read-only",
|
|
3221
|
-
allowedPaths: taskConfig.allowedPaths.length > 0
|
|
3222
|
-
? [...taskConfig.allowedPaths, "docs/**"]
|
|
3223
|
-
: ["**", "docs/**"],
|
|
3224
|
-
forbiddenPaths,
|
|
3225
|
-
skills: FRONTEND_VERIFICATION_SKILLS,
|
|
3226
|
-
outputContract: "Markdown closeout summary with Changes, Mock Decision / Strategy / Files / Verification / Production Boundary, Verification Evidence, Review Result, Frontend Status, Real Integration Status, Known Risks, and Follow-up. No file writes.",
|
|
3227
|
-
subtask_prompt: [
|
|
3228
|
-
"Return a frontend closeout summary covering Mock decision/strategy/files/verification/production boundary, changes, verification evidence, review result, known risks, and follow-up.",
|
|
3229
|
-
"Use only the canonical frontend-review-context.json plus the final frontend review gate verdict. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, design-review, writer, repair, or verification prose, and do not perform new repository research during closeout.",
|
|
3230
|
-
"Include a coverage matrix for each requirement id, applicable UI state, and verification target/check with status passed|failed|not-run|blocked|unavailable. Report lint separately as passed|baseline-debt|failed|unavailable; baseline-debt is explicit debt, not passed. Always state Browser accessibility verification: not-run and Visual regression: not-run. Use contracts/frontend-review-context.json and the effective frontend-verify-assess-shell or frontend-reverify-shell facts; do not invent Browser evidence from component tests.",
|
|
3231
|
-
`When only Mock-backed evidence passed, state exactly Frontend status: mock-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task to create/run after backend readiness. This follow-up is not auto-created or auto-executed. Never describe Mock evidence as real API integration.`,
|
|
3232
|
-
`When Mock was skipped in auto mode and no real API evidence passed, state exactly Frontend status: locally-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task when backend readiness matters.`,
|
|
3233
|
-
"Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
|
|
3234
|
-
].join("\n\n"),
|
|
3235
|
-
},
|
|
3236
3462
|
],
|
|
3237
3463
|
};
|
|
3238
|
-
|
|
3464
|
+
if (frontendTaskShape.shape === "split-required") {
|
|
3465
|
+
spec.tasks = pruneFrontendTasksForSplitRequired(spec.tasks);
|
|
3466
|
+
}
|
|
3467
|
+
else {
|
|
3468
|
+
const reshapeRaisedTopology = frontendTaskShape.signals.includes("shape-transition-reshape");
|
|
3469
|
+
if (!reshapeRaisedTopology) {
|
|
3470
|
+
spec.tasks = pruneFrontendTasksForRisk(spec.tasks, frontendRisk);
|
|
3471
|
+
}
|
|
3472
|
+
if (frontendTaskShape.shape === "micro") {
|
|
3473
|
+
spec.tasks = pruneFrontendTasksForMicro(spec.tasks);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
spec.advisories = [
|
|
3477
|
+
...(spec.advisories ?? []),
|
|
3478
|
+
`frontend-task-shape: ${frontendTaskShape.shape} (${frontendTaskShape.reason})`,
|
|
3479
|
+
];
|
|
3480
|
+
// Freeze the frontend recovery continuation quota from task.json so the
|
|
3481
|
+
// runner can bound M6 auto-recovery without re-reading the task config.
|
|
3482
|
+
const frontendMaxContinuations = sources.taskConfig.frontendRecovery?.maxContinuations ?? 1;
|
|
3483
|
+
spec.frontendRecovery = { maxContinuations: frontendMaxContinuations };
|
|
3239
3484
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
3240
3485
|
parseDagSpec(spec);
|
|
3241
3486
|
assertValidDagSpec(spec);
|
|
@@ -3836,9 +4081,10 @@ export function applyBackendTestLayoutToText(text, layout) {
|
|
|
3836
4081
|
/**
|
|
3837
4082
|
* Builds the `node -e` command for the backend-test module manifest shell.
|
|
3838
4083
|
* The inline JS mirrors `extractModuleStemsFromReadme` so the map_agent
|
|
3839
|
-
* shard set deterministically matches the
|
|
3840
|
-
* Gate trusts: only
|
|
3841
|
-
* relative links `[label](./<stem>.md)` count
|
|
4084
|
+
* shard set deterministically matches the plan index the Completeness
|
|
4085
|
+
* Gate trusts: only `testcase/md/<stem>.md` path mentions and canonical
|
|
4086
|
+
* relative links `[label](./<stem>.md)` count. Adjacent table cells such as
|
|
4087
|
+
* Case Range or pytest assets are never interpreted as module stems. The same
|
|
3842
4088
|
* `looksLikeValidModuleStem` filter and `normalizeBackendTestModuleStem`
|
|
3843
4089
|
* normalization. Output is exactly one trailing JSON line `{modules:[{stem}]}`
|
|
3844
4090
|
* that `parseJsonFromText` accepts after shell command echoes.
|
|
@@ -3852,17 +4098,18 @@ function buildBackendTestModuleManifestShellCommand(layout) {
|
|
|
3852
4098
|
// mdDir/testPrefix are injected as JSON literals so the same extractor
|
|
3853
4099
|
// works for any configured backendTest layout (plan A).
|
|
3854
4100
|
const mdDirLiteral = JSON.stringify(layout.markdownDir);
|
|
3855
|
-
const testPrefixLiteral = JSON.stringify(`${layout.scriptDir}/test_`);
|
|
3856
4101
|
const escOpen = String.fromCharCode(92, 91); // \[
|
|
3857
4102
|
const escClose = String.fromCharCode(92, 93); // \]
|
|
3858
4103
|
const escBslash = String.fromCharCode(92, 92); // \\
|
|
3859
|
-
const script = `const fs=require('fs');
|
|
4104
|
+
const script = `const fs=require('fs'),path=require('path'),crypto=require('crypto');
|
|
3860
4105
|
const mdDir=${mdDirLiteral};
|
|
3861
|
-
const testPrefix=${testPrefixLiteral};
|
|
3862
4106
|
const esc=s=>s.replace(/[${escOpen}${escClose}{}()*+?^$|${escBslash}]/g,'${escBslash}$&');
|
|
3863
4107
|
const rxMdPath=new RegExp(esc(mdDir)+'${escBslash}/([A-Za-z0-9_.-]+)${escBslash}.md','g');
|
|
3864
|
-
const
|
|
3865
|
-
|
|
4108
|
+
const runDir=process.env.HARNESS_DAG_RUN_DIR||'';
|
|
4109
|
+
if(!runDir){process.stderr.write('missing HARNESS_DAG_RUN_DIR for backend-test Markdown plan artifact\\n');process.exit(2);}
|
|
4110
|
+
const planPath=path.join(runDir,'generate-backend-md-plan-pi','plan.md');
|
|
4111
|
+
if(!fs.existsSync(planPath)){process.stderr.write('missing backend-test Markdown plan artifact: '+planPath+'\\n');process.exit(2);}
|
|
4112
|
+
const readme=fs.readFileSync(planPath,'utf8');
|
|
3866
4113
|
const norm=s=>String(s).toLowerCase().replace(/[^a-z0-9]+/g,'_').replace(/^_+|_+$/g,'').replace(/_+/g,'_');
|
|
3867
4114
|
const bt=String.fromCharCode(96);
|
|
3868
4115
|
const stripBackticks=s=>s.split(bt).join('');
|
|
@@ -3879,15 +4126,17 @@ const lines=section.split('\\n').filter(l=>l.includes('|'));
|
|
|
3879
4126
|
for(const line of lines){
|
|
3880
4127
|
const bare=stripBackticks(line);
|
|
3881
4128
|
for(const m of bare.matchAll(rxMdPath)){raw.push(m[1]);}
|
|
3882
|
-
for(const m of bare.matchAll(rxTableRow)){if(valid(m[1])||invalidReason(m[1])!=='invalid-syntax')raw.push(m[1]);}
|
|
3883
4129
|
}
|
|
3884
4130
|
for(const m of section.matchAll(rxRelLink)){raw.push(m[1]);}
|
|
3885
4131
|
const invalid=[];for(const r of raw){const reason=invalidReason(r);if(reason)invalid.push({stem:norm(r),reason});}
|
|
3886
4132
|
if(invalid.length){for(const item of invalid)process.stderr.write(item.reason+': '+item.stem+'; use a stable business resource/domain stem\\n');process.exit(2);}
|
|
3887
4133
|
const seen=new Set();const modules=[];
|
|
3888
4134
|
for(const r of raw){const st=norm(r);if(valid(r)&&!seen.has(st)){seen.add(st);modules.push({stem:st});}}
|
|
4135
|
+
if(modules.length===0){process.stderr.write('empty-module-index: require at least one stable business module\\n');process.exit(2);}
|
|
3889
4136
|
if(modules.length>8){process.stderr.write('excessive-module-count: '+modules.length+' > 8; merge by the smallest stable business resource/domain set\\n');process.exit(2);}
|
|
3890
|
-
process.
|
|
4137
|
+
const planReadPath=path.relative(process.cwd(),planPath).split(path.sep).join('/');
|
|
4138
|
+
const planSha256=crypto.createHash('sha256').update(readme).digest('hex');
|
|
4139
|
+
process.stdout.write(JSON.stringify({modules:modules.map(item=>({...item,planReadPath})),planReadPath,planSha256}));
|
|
3891
4140
|
`;
|
|
3892
4141
|
const encoded = Buffer.from(script, "utf8").toString("base64");
|
|
3893
4142
|
return `node -e "eval(Buffer.from('${encoded}','base64').toString('utf8'))"`;
|
|
@@ -4296,6 +4545,7 @@ async function buildBackendTestGapFillDag(sources) {
|
|
|
4296
4545
|
],
|
|
4297
4546
|
};
|
|
4298
4547
|
spec.backendTestLayout = layout;
|
|
4548
|
+
applyBackendTestWorkspaceControl(spec, taskConfig.backendTest?.workspaceControl ?? "git");
|
|
4299
4549
|
if (sharedSetup) {
|
|
4300
4550
|
spec.backendTestSharedSetup = { ...sharedSetup };
|
|
4301
4551
|
}
|
|
@@ -4369,35 +4619,37 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4369
4619
|
const generateMdPlan = {
|
|
4370
4620
|
id: "generate-backend-md-plan-pi",
|
|
4371
4621
|
depends_on: [environment.id],
|
|
4372
|
-
role: "
|
|
4622
|
+
role: "planner",
|
|
4373
4623
|
executor: "pi",
|
|
4374
|
-
toolProfile: "
|
|
4624
|
+
toolProfile: "read-only",
|
|
4375
4625
|
complexity: "MED",
|
|
4376
|
-
writePolicy: "
|
|
4377
|
-
|
|
4378
|
-
|
|
4626
|
+
writePolicy: "read-only",
|
|
4627
|
+
allowedPaths: [],
|
|
4628
|
+
readSet: [
|
|
4629
|
+
toDagSourcePath(sources, sources.requirementPath),
|
|
4630
|
+
...(sources.constraintMarkdown
|
|
4631
|
+
? [toDagSourcePath(sources, sources.constraintPath)]
|
|
4632
|
+
: []),
|
|
4633
|
+
...intake.referenceIndex.map((entry) => entry.readPath),
|
|
4634
|
+
".harness/dag-runs/**/reports/backend-test-environment.md",
|
|
4635
|
+
],
|
|
4379
4636
|
forbiddenPaths: forbidden,
|
|
4380
|
-
|
|
4381
|
-
type: "implementation-outcome-v1",
|
|
4382
|
-
requireChangedFiles: true,
|
|
4383
|
-
},
|
|
4384
|
-
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
4385
|
-
outputContract: "Write a Chinese, human-readable testcase/md/README.md as the single Markdown-first entry page with Coverage Scope, Coverage Matrix and a machine-parseable module index. Do not write module case cards here; do not execute pytest or modify production code/config.",
|
|
4637
|
+
outputContract: "Return a Chinese, human-readable Markdown-first plan with Coverage Scope, Coverage Matrix, Scenario Partitions when applicable, and a machine-parseable Module Index. The runtime persists it as a run-owned Harness artifact; do not write testcase/md/README.md, execute pytest, or modify project files.",
|
|
4386
4638
|
subtask_prompt: [
|
|
4387
|
-
"This is a required
|
|
4639
|
+
"This is a required plan-generation node. Read only the strict read set and return the complete Markdown plan in the assistant response. The runtime persists the response as a run-owned Harness artifact named generate-backend-md-plan-pi/plan.md. Do not write testcase/md/README.md or any project file; module case cards are written by downstream sharded nodes.",
|
|
4388
4640
|
"Output budget protocol (hard, max output <=16K per turn): Never paste full Matrix, case bodies, or source text into assistant chat. README holds only Scope+Matrix+module index; never inline full case bodies. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.",
|
|
4389
|
-
"
|
|
4390
|
-
"Read the upstream environment report. Generate the Markdown-first backend test
|
|
4641
|
+
"Return the complete plan as plain Markdown. Do not emit JSON or code fences. The plan must contain the exact ## Coverage Scope, ## Coverage Matrix and ## Module Index sections required by the downstream manifest.",
|
|
4642
|
+
"Read the upstream environment report only through the strict read set. Generate the Markdown-first backend test plan; it will be persisted under the current DAG run's Harness artifacts, not under testcase/md/.",
|
|
4391
4643
|
"Write human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.",
|
|
4392
|
-
"Create
|
|
4393
|
-
"Before the Coverage Matrix, write a mandatory machine-readable `## Coverage Scope` section in
|
|
4644
|
+
"Create the concise plan entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in the plan artifact.",
|
|
4645
|
+
"Before the Coverage Matrix, write a mandatory machine-readable `## Coverage Scope` section in the plan artifact using exactly `| Field | Value |`, immediately followed by the separator row `|---|---|`, and these six unique rows: `Change Classification`, `Coverage Policy`, `Affected Operations`, `Affected Rule Keys`, `Regression Floor`, `Scope Evidence`. Always set `Change Classification` to `new-operation` and `Coverage Policy` to `full-contract`; do NOT reason about whether operations are new or existing. Cover all in-scope rules from the requirement document at full depth; treat the product requirement as the coverage baseline and use API contract evidence (fields/status/enum/boundary/format) to supplement scenario dimensions. Scope is limited to operations/rules the requirement document (or its referenced API contract) explicitly describes; do not expand to unrelated operations that the requirement does not mention. List affected operations exactly as `METHOD /path`, stable rule keys separated by semicolons, and precise source pointers as Scope Evidence.",
|
|
4394
4646
|
"Coverage depth is full over the in-scope rules: fully cover every documented status, request/response field rule, requiredness, enum, boundary, format, auth and business state of each affected operation the requirement describes, but do not re-test unrelated operations the requirement does not mention. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT.",
|
|
4395
|
-
"Before writing cases, build the mandatory machine-readable Coverage Matrix inside
|
|
4647
|
+
"Before writing cases, build the mandatory machine-readable Coverage Matrix inside the plan artifact itself. Its section heading line must be exactly `## Coverage Matrix` with no numeric prefix/suffix; never place the canonical Matrix only in a module file. Use this exact header: `| Rule Key | Priority | Source | Endpoint/Field | Dimension | Rule | Required Test Points | Case IDs | Status |`. Every data row must contain exactly 9 pipe-delimited cells and must never omit `Dimension`; use concise dimensions such as requirement, operation, response-status, requiredness, enum, boundary, format, business-state or error. Use only P0/P1/P2 and COVERED/PARTIAL/GAP/CONFLICT. Use stable `TP-<UPPERCASE-HYPHENATED-ID>` test points separated by semicolons.",
|
|
4396
4648
|
"Each Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.",
|
|
4397
4649
|
"Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
|
|
4398
4650
|
"For uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.",
|
|
4399
|
-
"Mandatory module index: include a `## Module Index` table in
|
|
4400
|
-
"Scenario Partitions (query/filter axes): for every affected GET/list operation, declare one row per enum or classification axis used for filtering (query/path parameters such as type/status/category). Add a mandatory machine-readable `## Scenario Partitions` section after the Coverage Matrix using exactly `| Partition ID | Operation | Axis | Domain | Required Slots | Expected by Slot | Bind Rule |` with the separator row. Partition ID is a stable `SP-<OPERATION>-<AXIS>` token; Domain must copy the legal values verbatim from the bound OpenAPI enum or requirement sentence (never guess); Required Slots writes `each-value` plus `omitted` only when the parameter is optional; Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Only GET/list query or path parameters whose bound source documents a finite enum or classification set may become a Scenario Partition. Do not create partitions for free-form strings, primary keys, required-or-optional-only parameters, or boundary/format-only axes. If an axis has no finite legal-value domain, do not declare a Partition row and do not invent NOT-IN-SET cases. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
|
|
4651
|
+
"Mandatory module index: include a `## Module Index` table in the plan artifact that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output protocol, keep the total module count at the smallest safe value, and never exceed 8 modules. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Pure hexadecimal/hash-like opaque stems such as `a401606` or `deadbeef` are forbidden. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix and never defines module files. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
4652
|
+
"Scenario Partitions (query/filter axes): for every affected GET/list operation, declare one row per enum or classification axis used for filtering (query/path parameters such as type/status/category). Add a mandatory machine-readable `## Scenario Partitions` section after the Coverage Matrix using exactly `| Partition ID | Operation | Axis | Domain | Required Slots | Expected by Slot | Bind Rule |` with the separator row. Partition ID is a stable `SP-<OPERATION>-<AXIS>` token; Domain must copy the legal values verbatim from the bound OpenAPI enum or requirement sentence (never guess), using bare semicolon-separated identifier values inside the single table cell (for example `ACTIVE; ARCHIVED`, with no Markdown backticks or prose); Required Slots writes `each-value` plus `omitted` only when the parameter is optional; Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Only GET/list query or path parameters whose bound source documents a finite enum or classification set may become a Scenario Partition. Do not create partitions for free-form strings, primary keys, required-or-optional-only parameters, or boundary/format-only axes. If an axis has no finite legal-value domain, do not declare a Partition row and do not invent NOT-IN-SET cases. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
|
|
4401
4653
|
"Before finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
4402
4654
|
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
4403
4655
|
intake.boundedSourceContext,
|
|
@@ -4416,8 +4668,8 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4416
4668
|
writePolicy: "read-only",
|
|
4417
4669
|
allowedPaths: ro,
|
|
4418
4670
|
forbiddenPaths: forbidden,
|
|
4419
|
-
outputContract: "Stdout JSON {modules:[{stem}]} parsed from
|
|
4420
|
-
subtask_prompt: "Parse
|
|
4671
|
+
outputContract: "Stdout JSON {modules:[{stem,planReadPath}],planReadPath,planSha256} parsed from the run-owned generate-backend-md-plan-pi/plan.md artifact using the same module-stem extractor as the Completeness Gate.",
|
|
4672
|
+
subtask_prompt: "Parse only $HARNESS_DAG_RUN_DIR/generate-backend-md-plan-pi/plan.md and emit exactly one trailing JSON line {modules:[{stem,planReadPath}],planReadPath,planSha256}. No file writes and no testcase/md/README.md fallback.",
|
|
4421
4673
|
shell: {
|
|
4422
4674
|
commands: [buildBackendTestModuleManifestShellCommand(layout)],
|
|
4423
4675
|
cwd: ".",
|
|
@@ -4458,6 +4710,15 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4458
4710
|
allowedPaths: ["testcase/md/{{item.stem}}.md"],
|
|
4459
4711
|
forbiddenPaths: forbidden,
|
|
4460
4712
|
writeSet: ["testcase/md/{{item.stem}}.md"],
|
|
4713
|
+
readSet: [
|
|
4714
|
+
"{{item.planReadPath}}",
|
|
4715
|
+
toDagSourcePath(sources, sources.requirementPath),
|
|
4716
|
+
...(sources.constraintMarkdown
|
|
4717
|
+
? [toDagSourcePath(sources, sources.constraintPath)]
|
|
4718
|
+
: []),
|
|
4719
|
+
...intake.referenceIndex.map((entry) => entry.readPath),
|
|
4720
|
+
`${layout.markdownDir}/{{item.stem}}.md`,
|
|
4721
|
+
],
|
|
4461
4722
|
writerOutcomePolicy: {
|
|
4462
4723
|
type: "implementation-outcome-v1",
|
|
4463
4724
|
requireChangedFiles: true,
|
|
@@ -4465,7 +4726,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4465
4726
|
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
4466
4727
|
outputContract: "Write exactly one Chinese module Markdown case-card file testcase/md/<stem>.md with BE-<MODULE>-<NNN> cases and the seven required h3 sections; keep machine IDs/literals exact and do not execute pytest or modify production code/config or the README.",
|
|
4467
4728
|
subtaskPromptTemplate: [
|
|
4468
|
-
"This is a required file-generation node for exactly one Markdown module.
|
|
4729
|
+
"This is a required file-generation node for exactly one Markdown module. Read the upstream run-owned Markdown plan artifact at `{{item.planReadPath}}` (Coverage Scope + Coverage Matrix + Module Index) and the bounded references, then immediately use write tools to create the single file testcase/md/{{item.stem}}.md. Do not read or recreate testcase/md/README.md. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists. Do not modify any other module file.",
|
|
4469
4730
|
"Output budget protocol (hard, max output <=16K per turn): Never paste full Matrix, other modules' case bodies, or source text into assistant chat. Each write/edit tool call touches at most one file (this module). Compact tables/lists are required; omitting required sections or in-scope variants is forbidden. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.",
|
|
4470
4731
|
"The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the module file has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.",
|
|
4471
4732
|
"Write human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.",
|
|
@@ -4487,28 +4748,37 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4487
4748
|
};
|
|
4488
4749
|
const reviewCases = {
|
|
4489
4750
|
id: "review-and-revise-backend-md-cases-pi",
|
|
4490
|
-
depends_on: [generateMdCasesMap.id],
|
|
4751
|
+
depends_on: [generateMdCasesMap.id, materializeMdManifest.id],
|
|
4491
4752
|
role: "implementer",
|
|
4492
4753
|
executor: "pi",
|
|
4493
4754
|
toolProfile: "write",
|
|
4494
4755
|
complexity: "MED",
|
|
4495
4756
|
writePolicy: "exclusive",
|
|
4496
|
-
writeSet: [
|
|
4497
|
-
|
|
4498
|
-
|
|
4757
|
+
writeSet: [`${layout.markdownDir}/**`],
|
|
4758
|
+
readSet: [
|
|
4759
|
+
".harness/dag-runs/**/generate-backend-md-plan-pi/plan.md",
|
|
4760
|
+
toDagSourcePath(sources, sources.requirementPath),
|
|
4761
|
+
...(sources.constraintMarkdown
|
|
4762
|
+
? [toDagSourcePath(sources, sources.constraintPath)]
|
|
4763
|
+
: []),
|
|
4764
|
+
...intake.referenceIndex.map((entry) => entry.readPath),
|
|
4765
|
+
`${layout.markdownDir}/*.md`,
|
|
4766
|
+
],
|
|
4767
|
+
allowedPaths: Array.from(new Set([...ro, `${layout.markdownDir}/**`])),
|
|
4768
|
+
forbiddenPaths: Array.from(new Set([...forbidden, layout.readmePath])),
|
|
4499
4769
|
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
4500
4770
|
outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked. Perform exactly one bounded incremental synchronization of testcase/md/** against all bound source references; preserve valid Cases and report a concise summary.",
|
|
4501
4771
|
subtask_prompt: [
|
|
4502
|
-
"Perform one gap-targeted synchronization, not a full-suite rewrite or stylistic review. Start from explicit bound source IDs/error codes/DTO fields/normative quoted rules and the
|
|
4503
|
-
"Output budget protocol: never dump full Matrix/case bodies into assistant chat. Inspect
|
|
4772
|
+
"Perform one gap-targeted synchronization, not a full-suite rewrite or stylistic review. Read the immutable run-owned Markdown plan from the direct upstream manifest's planReadPath. Start from explicit bound source IDs/error codes/DTO fields/normative quoted rules and the plan Coverage Matrix; open and edit only modules that own a missing or conflicting rule. Never create or edit testcase/md/README.md and never modify the run-owned plan artifact. Preserve unrelated valid modules byte-for-byte and avoid optional wording cleanup.",
|
|
4773
|
+
"Output budget protocol: never dump full Matrix/case bodies into assistant chat. Inspect the immutable run-owned plan first, build a concise target list from its Matrix and Module Index, then read/write only target modules one file per tool call. Do not traverse every module when the Matrix and source token inventory show no gap; return `already-satisfied`. When adding omitted in-scope cases, keep every required section. Do not bulk-delete in-scope cases to save tokens.",
|
|
4504
4774
|
"For every variant Test Point, ensure the Markdown scenario intent is machine-checkable and located inside that same Case body/自动化映射, never in a file-level appendix, implementation-details block, or another Case. Use an exact transport target: `场景意图: <TP-ID>; operation=<METHOD /path>; target=<body.field|query.field|path.field|header.field|request>; intent=<empty|missing|null|min-1|min|max|max+1|pattern-invalid|enum-invalid|wrong-type|nominal-operation|custom-literal:V>; bound=<n optional>; example=<optional>; expectedCode=<optional>`. Never use vague targets such as field=resource/health. Keep pytest params aligned to the exact target. For intent=missing/empty/default-omit, pytest may use `_OMIT` or delete the key; for intent=enum-invalid use a concrete invalid enum literal (for example `UNKNOWN_STATUS`), never `_OMIT`/missing-key; for trim/padded samples use `custom-literal:trim` or a real padded string, not a bare token like `filter-active` when the intent is `custom-literal:ACTIVE`.",
|
|
4505
4775
|
"Treat the requirement document as the coverage baseline; scope is limited to operations/rules it (or its referenced API contract) describes, and API contract evidence supplements scenario dimensions. For every in-scope operation, check applicable lifecycle/uniqueness states (including deleted-existing when in scope), valid enum values, bounded invalid classes, min-1/min/nominal/max/max+1, allowed/forbidden format classes, required/null/missing/wrong-type semantics, status/error codes, auth and state transitions. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT. Directly add in-scope omissions; reject scope expansion to operations absent from the requirement document; undefined impact remains GAP/CONFLICT rather than invented behavior.",
|
|
4506
|
-
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Require the exact `## Coverage Scope` Field/Value table with the `|---|---|` separator row, a valid classification-policy pair, non-empty Affected Operations/Rule Keys/Scope Evidence, and the classification-specific Regression Floor. Require the exact unnumbered `## Coverage Matrix` heading in
|
|
4776
|
+
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Require the exact `## Coverage Scope` Field/Value table with the `|---|---|` separator row, a valid classification-policy pair, non-empty Affected Operations/Rule Keys/Scope Evidence, and the classification-specific Regression Floor. Require the exact unnumbered `## Coverage Matrix` heading in the immutable run-owned plan artifact, exact headers, exactly 9 cells in every data row (including a non-empty Dimension), deterministic OpenAPI Rule Keys for every in-scope affected operation, exactly one Matrix row per Rule Key (merge multi-dimension product rows), and bidirectional Matrix Rule/Test Point ↔ Case bindings. Never describe affected-scope coverage as whole-API completeness. Every explicit AC ID must appear in at least one Case `验收标准`; every explicit in-scope AC/REQ/BR Rule Key cited by a Case must have exactly one Coverage Matrix row, and no Case may cite a source Rule Key omitted from the Matrix. Every Matrix Case ID must share at least one of that row's Required Test Points and the Case must cite that Rule Key. Perform an explicit execution-redundancy review: merge checkpoint-only parameter rows, repeated default/read-back assertions, DELETE status/body/follow-up-read checks, response schema/Content-Type checks, PUT full-update/timestamp checks, repeated list setup and identical null/empty inputs when endpoint, input partition, precondition state and expected outcome are the same. Preserve separate POST/PUT, boundary, enum, wrong-type, role/tenant and distinct business-state variants. Directly repair malformed headings/rows/keys and binding modes rather than merely commenting on them. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, a `### 操作步骤` section that contains only a table without any numbered executable line, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), dropped exact `### 操作步骤`/`### 预期结果` headings, and missing or drifted script/function mapping where it can be derived.",
|
|
4507
4777
|
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`; `BE-RN-011A` must be renumbered or merged) consistently across headings/index/mappings, fix automation mappings so each automatable case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol (evidence-only meta cases may keep `脚本/primary symbol=无` with empty variants), assign every Test Point exactly one of `变体测试点`/`场景断言测试点`/`横切证据测试点`, then perform an exact-set check: each Case's `### 测试点` set must equal (not merely contain) the union of those three binding lists; delete stale/legacy aliases and ensure every binding-list Test Point is present, expand every variant parameter row into its own atomic TP ID, make every non-cross-cutting TP Case-specific and owned by exactly one Case, require every primary symbol to start with the canonical Case prefix, ensure every explicit AC ID appears in an applicable Case `验收标准`, merge execution duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Remove every credential/header value, placeholder, fake token and anti-example from Markdown. Sensitive key names may remain only as a plain list; values must be described as runtime-only and omitted, with no colon/value pair or literal example anywhere, including details blocks and explanatory text. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. Recalculate predicted collected items as `sum(max(1, variant count per Case))`; when the task declares a budget, directly merge redundant journeys/reclassify same-request checkpoints until the prediction is within budget, while preserving all required coverage. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
|
|
4508
4778
|
"This is the single Markdown incremental synchronization round. Read every authoritative reference index entry whose role hints include acceptance-criteria, api-contract, data-contract or business-rule; do not rely on the derived PRD as a complete inventory. Preserve every explicit AC/REQ/BR ID, every documented HTTP/business error code, every DTO/JSON field, enum value, boundary, format, nested shape, transaction/state/idempotency/uniqueness/auth/tenant/cross-field rule. For each natural-language normative business rule preserved as required scope, include its exact source sentence without paraphrase together with source path and line/heading anchor so the deterministic ledger can verify quote/hash provenance. Ensure every Case declares exactly `Payload Contract: none` or the three labels `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; every label must occupy its own machine-readable list line, and a Case must never concatenate target/setup operations or multiple `Payload Contract` tokens onto one line, and explanatory prose/details must not repeat any `Payload Contract:` token; never infer missing keys or enum values. A target GET/DELETE operation with no request body must remain `Payload Contract: none` even when its setup journey performs POST/PUT with a DTO; setup payloads never redefine the target Case payload contract. Add only missing Matrix rows/Test Points/Cases/assertions or repair exact drift; do not rewrite already-valid unrelated modules. Work gap-targeted: inspect source anchors and affected modules first, leave unrelated valid modules byte-stable, and return `already-satisfied` without restating the full suite when no gap exists.",
|
|
4509
4779
|
"For affected API fields, use one valid nominal payload plus atomic required/missing/null/empty/wrong-type, every documented enum value plus bounded invalid classes, documented min-1/min/nominal/max/max+1, formats and nested object/array constraints. Do not generate a Cartesian product or invent undocumented constraints. Do not invent a concrete identifier type when the source only requires presence; for a missing-resource 404 path with unspecified identifier syntax/type, synchronize the Case to a create-delete-derived valid identifier journey rather than an arbitrary UUID/text placeholder.",
|
|
4510
|
-
"Scenario Partitions synchronization: when
|
|
4511
|
-
"Before returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in
|
|
4780
|
+
"Scenario Partitions synchronization: when the run-owned plan declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Directly add missing slot rows/Cases. Record an illegal plan Partition row that has no source-backed finite domain as GAP/CONFLICT and remove only its derived `TP-SP-*` slots/Cases from target modules; never modify the immutable plan artifact. Never delete a legal source-backed partition or drop its complement slot to force coverage green. When the bound source does not document the complement expectation, keep the slot with GAP expected instead of guessing. Body-field validation enums (`TP-<FIELD>-ENUM-*`) are NOT partitions — do not add partition rows for them.",
|
|
4781
|
+
"Before returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in the run-owned plan or an applicable module Case. If a fact cannot be safely automated, retain it as GAP/CONFLICT with its exact source pointer instead of dropping it. Return already-satisfied only when no target file needs an incremental edit.",
|
|
4512
4782
|
"Read only indexed source paths. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
|
|
4513
4783
|
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
4514
4784
|
intake.boundedSourceContext,
|
|
@@ -4557,8 +4827,8 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4557
4827
|
writePolicy: "read-only",
|
|
4558
4828
|
allowedPaths: ro,
|
|
4559
4829
|
forbiddenPaths: forbidden,
|
|
4560
|
-
outputContract: "Stdout JSON {modules:[{stem}]} parsed from
|
|
4561
|
-
subtask_prompt: "Parse
|
|
4830
|
+
outputContract: "Stdout JSON {modules:[{stem,planReadPath}],planReadPath,planSha256} parsed from the run-owned Markdown plan artifact, so the pytest map shard set and every child planReadPath deterministically match the Markdown map manifest.",
|
|
4831
|
+
subtask_prompt: "Parse only the run-owned generate-backend-md-plan-pi/plan.md artifact and emit exactly one trailing JSON line {modules:[{stem,planReadPath}],planReadPath,planSha256}. Reuse the same plan-derived Module Index contract as the Markdown manifest; do not search for or fall back to testcase/**/README.md. No file writes.",
|
|
4562
4832
|
shell: {
|
|
4563
4833
|
commands: [buildBackendTestModuleManifestShellCommand(layout)],
|
|
4564
4834
|
cwd: ".",
|
|
@@ -4613,7 +4883,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4613
4883
|
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
4614
4884
|
outputContract: "Write exactly one pytest module file testcase/test_<stem>.py whose actual test function region contains the exact Case ID, preferably in the function name or docstring. testcase/md/<module>.md (excluding README.md) maps one-to-one to testcase/test_<module>.py; never merge or split modules. No JSON and no pytest execution.",
|
|
4615
4885
|
subtaskPromptTemplate: [
|
|
4616
|
-
"Convert the single Markdown module testcase/md/{{item.stem}}.md into one self-contained pytest module. Before writing, also read
|
|
4886
|
+
"Convert the single Markdown module testcase/md/{{item.stem}}.md into one self-contained pytest module. Before writing, also read the run-owned Markdown plan artifact at `{{item.planReadPath}}` and use its explicit API target/environment table as the authoritative fallback base URL for every module. A task/Markdown `API_BASE_URL` target takes precedence over project README dev-server URLs; never infer a backend API fallback from a frontend/Vite port such as localhost:3000. After reading the module Markdown, the run-owned plan artifact, and the bounded pytest config/conftest, immediately use write tools to create the single file testcase/test_{{item.stem}}.py. Define any bounded HTTP client fixture, request logging/redaction/truncation helper and payload builders needed by this module inside that same file; do not import generated testcase/**/helpers/** or testcase/**/factories/** assets. Do not end after analysis or planning. Do not modify Markdown, conftest, helpers/factories, or any other module's pytest script.",
|
|
4617
4887
|
"Output budget protocol (hard, max output <=16K per turn): Write exactly one test_{{item.stem}}.py. Never paste full Python modules into assistant chat. Do not merge or split modules. Do not reduce params/assertions/skips to fit. If OUTPUT_LIMIT_RECOVERY is injected, continue only listed missing/broken scripts.",
|
|
4618
4888
|
"Align every variant pytest.param payload with the Markdown scenario intent (empty/missing/null/length/pattern/enum/wrong-type/nominal). Prefer literal payloads over Faker for intent-critical fields so pre-execution scenario-param checks can verify them. Hard contract: intent=enum-invalid MUST pass a concrete invalid value literal (string/number/boolean), never `_OMIT`/None/missing key; intent=missing/empty may use `_OMIT` or delete the key; intent=custom-literal:trim|whitespace-padded requires a leading/trailing whitespace string with non-empty trimmed content (all-whitespace belongs to empty/whitespace-only, not trim); intent=custom-literal:ACTIVE|ARCHIVED requires the exact enum string, never descriptive tokens like filter-active; intent=max/min/max+1 should pass a repeated-string length expression, a bare length number N, or a helper named _*_LEN{N} / _*_MAX_LENGTH / _*_OVER_LENGTH — never a bare 1 for oversize. Hard contract: request payload dicts may only contain DTO field keys from Payload Allowed Paths; never put expect/expected/echo_* helper keys inside the JSON body dict. Path/query/header identifiers and scenario-control metadata (including `id`, expected codes, and selector labels) must stay in separate pytest parameters and helper arguments; never merge them into a DTO patch or JSON body unless that exact path is allowed by the Markdown payload contract. Normalize the configured API base URL with `rstrip(\"/\")` (or equivalently join exactly one slash) before appending endpoint paths; generated requests must never contain a `//api/...` path. When the bound source documents a concrete non-secret local API URL, generated clients must use it as the fallback in `os.environ.get(\"API_BASE_URL\", \"<documented-url>\")`; do not require an otherwise-uninjected environment variable or fail setup solely because it is absent. Missing-field helpers must remove keys idempotently with `payload.pop(field, None)`, never `del payload[field]`, because optional fields may already be absent.",
|
|
4619
4889
|
"For every response contract that requires an object or pagination envelope, first assert that each envelope/data value is a dict and that required keys exist, then index fields and assert values. Never let an incidental KeyError or list/string TypeError stand in for the explicit response-shape contract failure.",
|
|
@@ -4629,6 +4899,13 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4629
4899
|
},
|
|
4630
4900
|
};
|
|
4631
4901
|
const collectionAssess = shellNode("assess-backend-pytest-collection-shell", [generatePytestCasesMap.id], "markdown-collection-assess", "Resolve final Markdown-mapped scripts before any business test body execution. Run scenario-param assess/at-most-one deterministic repair first, then pytest --collect-only and a no-business-body pytest --setup-plan fixture-resolution preflight. A safe missing mapped script, generated-local syntax/import defect, or generated fixture dependency/plugin-registration defect is REPAIRABLE; dependency, third-party plugin, production-module, environment, safety and unknown failures remain blocked. Materialize hash-bound collection-v3 facts with repairPaths and fixtureResolutionStatus.", "Run-owned reports/backend-test-pytest-collection-initial.md and contracts/backend-test-pytest-collection-initial.json (collection-v3) with bounded collection/fixture diagnostics, repairPaths, asset hashes, collected item IDs and deterministic repair eligibility.", [], 120000);
|
|
4902
|
+
// N10 owns the deterministic scenario-param rewrite that may update mapped
|
|
4903
|
+
// generated test modules before collection. Declare that bounded shell write
|
|
4904
|
+
// authority so filesystem-only workspace control does not misclassify the
|
|
4905
|
+
// intentional repair as an out-of-bounds mutation.
|
|
4906
|
+
collectionAssess.writePolicy = "exclusive";
|
|
4907
|
+
collectionAssess.writeSet = [applyLayout("testcase/**/test_*.py")];
|
|
4908
|
+
collectionAssess.allowedPaths = Array.from(new Set([...ro, ...collectionAssess.writeSet]));
|
|
4632
4909
|
const repairPytest = {
|
|
4633
4910
|
id: "repair-backend-pytest-collection-pi",
|
|
4634
4911
|
depends_on: [collectionAssess.id],
|
|
@@ -4638,8 +4915,24 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4638
4915
|
toolProfile: "write",
|
|
4639
4916
|
complexity: "MED",
|
|
4640
4917
|
writePolicy: "exclusive",
|
|
4641
|
-
writeSet: [
|
|
4642
|
-
|
|
4918
|
+
writeSet: Array.from(new Set([
|
|
4919
|
+
applyLayout("testcase/**/test_*.py"),
|
|
4920
|
+
`${layout.testRoot}/helpers/**/*.py`,
|
|
4921
|
+
`${layout.testRoot}/factories/**/*.py`,
|
|
4922
|
+
`${layout.testRoot}/__*_helpers.py`,
|
|
4923
|
+
`${layout.scriptDir}/helpers/**/*.py`,
|
|
4924
|
+
`${layout.scriptDir}/factories/**/*.py`,
|
|
4925
|
+
`${layout.scriptDir}/__*_helpers.py`,
|
|
4926
|
+
])),
|
|
4927
|
+
readSet: [
|
|
4928
|
+
".harness/dag-runs/**/reports/backend-test-pytest-collection-initial.md",
|
|
4929
|
+
".harness/dag-runs/**/reports/backend-test-markdown-pytest-correspondence-initial.md",
|
|
4930
|
+
".harness/dag-runs/**/generate-backend-md-plan-pi/plan.md",
|
|
4931
|
+
`${layout.markdownDir}/**`,
|
|
4932
|
+
`${layout.testRoot}/**/*.py`,
|
|
4933
|
+
],
|
|
4934
|
+
contextBudget: { maxTurns: 24, maxTokens: 80_000 },
|
|
4935
|
+
allowedPaths: Array.from(new Set([...ro, `${layout.testRoot}/**`])),
|
|
4643
4936
|
forbiddenPaths: Array.from(new Set([
|
|
4644
4937
|
...forbidden,
|
|
4645
4938
|
"testcase/md/**",
|
|
@@ -4652,7 +4945,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4652
4945
|
outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|blocked, followed by a concise repair summary. This node runs only for REPAIRABLE initial facts, so already-satisfied is invalid and a successful outcome requires a non-empty bounded diff. Modify only generated pytest scripts/helpers/factories and preserve every Markdown Case, Test Point, primary symbol and assertion meaning.",
|
|
4653
4946
|
subtask_prompt: [
|
|
4654
4947
|
"Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution. Treat any upstream line such as `Repair paths: testcase/test_x.py` as complete authoritative repairPaths evidence. Directly read and edit that testcase path; do not search for separate root-level `contracts/**`, guess a DAG run directory, or require another report artifact. If the read tool successfully returns the testcase file, the path exists—continue the bounded repair and never later claim that file is absent.",
|
|
4655
|
-
"Initial status REPAIRABLE means at least one listed finding remains: `already-satisfied` is forbidden, and you must produce a non-empty bounded diff on repairPaths before returning `IMPLEMENTATION_OUTCOME: changed`. Fix only readiness-proven generated testcase-local defects on initial facts repairPaths: create exact safe missing mapped test_*.py paths, repair syntax/import/symbol/decorator/parameterization, close generated fixture dependencies/plugin registration, and repair initial Markdown-to-pytest correspondence findings (
|
|
4948
|
+
"Initial status REPAIRABLE means at least one listed finding remains: `already-satisfied` is forbidden, and you must produce a non-empty bounded diff on repairPaths before returning `IMPLEMENTATION_OUTCOME: changed`. Fix only readiness-proven generated testcase-local defects on initial facts repairPaths: create exact safe missing mapped test_*.py paths, repair syntax/import/symbol/decorator/parameterization, close generated fixture dependencies/plugin registration, and repair initial Markdown-to-pytest correspondence findings. Use this deterministic repair map instead of reading analyzer implementation: findings about `Case-ID`, `Assertion-Test-Points`, or `Cross-Cutting-Test-Points` are fixed by editing the declared primary symbol docstring metadata lines; variant binding findings are fixed in the literal direct `pytest.param(..., id=\"TP-...\")` row; primary-symbol cardinality/name findings are fixed in the function name or duplicate primary symbols; script mismatch is fixed only on the authoritative assessment repairPaths; payload findings are fixed in request payload construction. Do not read controller `src/**` or inspect JS/TS analyzer code. Do not search for `testcase/**/README.md`. Never invent a business pytest symbol for evidence-only Markdown Cases that declare `脚本/primary symbol=无` with empty variants. For fixture defects inspect both provider and importer listed by repairPaths; fix ScopeMismatch by aligning fixture scopes or inlining request-scoped values so module fixtures never depend on function fixtures; when a shared fixture depends on sibling fixtures, register the whole provider module through an exact pytest_plugins declaration rather than importing only the outer fixture. Do not create unrelated pytest scripts.",
|
|
4656
4949
|
"This is the single pytest incremental synchronization round. The `Findings` in `reports/backend-test-pytest-collection-initial.md` are the mandatory repair checklist: resolve every repairable listed finding on every authoritative `Repair paths` file before considering any other advisory evidence, and never substitute an unrelated scenario-param cleanup for a listed correspondence/collection defect. For every assessment-listed path, compare the effective Markdown Case/Test Points/test data and its `Payload Contract`/`Payload Required Paths`/`Payload Allowed Paths`/`Payload Enum` labels with the generated module. Incrementally add or repair only missing symbols, params, assertions and payload builders. Repair every assessment-listed missing nested path, unexpected key and enum mismatch; preserve exact DTO keys, nested shapes, enum/boundary literals, operation transport and business preconditions; remove guessed replacement keys only when the effective Markdown proves the exact contract. Keep path/query/header identifiers and scenario-control metadata separate from DTO patches and JSON bodies; an `id` used for a path target must be passed to the request path/helper, never inserted into a body patch unless `id` is explicitly listed in Payload Allowed Paths. Flatten every variant into a literal direct `pytest.param(..., id=\"TP-...\")` row; replace `_post_case`/`_put_case` or other parameter-row factories because correspondence and scenario readiness require the actual row values and IDs to be statically visible. Also repair helper call sites to match their defined return signatures; do not tuple-unpack a helper that returns one scalar value.",
|
|
4657
4950
|
"Preserve final testcase/md/** semantics, every Case ID, Rule/Test Point binding, primary symbol, parameter ID, expected status/body/schema assertion, HTTP logging, redaction and truncation behavior.",
|
|
4658
4951
|
"Use local edit only on assessment-listed paths; keep summaries short; never rewrite unrelated modules.",
|
|
@@ -4752,6 +5045,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4752
5045
|
// against the resolved layout. The default layout is identity, so the
|
|
4753
5046
|
// packaged template stays byte-identical with the historical contract.
|
|
4754
5047
|
applyBackendTestLayoutToDagSpec(spec, layout);
|
|
5048
|
+
applyBackendTestWorkspaceControl(spec, taskConfig.backendTest?.workspaceControl ?? "git");
|
|
4755
5049
|
// Plan B: carry the bound shared-setup document on the spec so runtime
|
|
4756
5050
|
// validators (N6 markdown case validation) see the same binding as prompts.
|
|
4757
5051
|
if (intake.sharedSetup) {
|
|
@@ -4762,6 +5056,23 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4762
5056
|
assertValidDagSpec(spec);
|
|
4763
5057
|
return spec;
|
|
4764
5058
|
}
|
|
5059
|
+
function applyBackendTestWorkspaceControl(spec, control) {
|
|
5060
|
+
if (control !== "filesystem-only") {
|
|
5061
|
+
delete spec.backendTestWorkspaceControl;
|
|
5062
|
+
return;
|
|
5063
|
+
}
|
|
5064
|
+
spec.backendTestWorkspaceControl = control;
|
|
5065
|
+
for (const task of spec.tasks) {
|
|
5066
|
+
if ((task.executor === "pi" && task.toolProfile === "write") ||
|
|
5067
|
+
task.shell?.backendTestPipeline) {
|
|
5068
|
+
task.writeGuardPolicy = "filesystem-only";
|
|
5069
|
+
}
|
|
5070
|
+
const child = task.dynamicExpansion?.childTask;
|
|
5071
|
+
if (child?.executor === "pi" && child.toolProfile === "write") {
|
|
5072
|
+
child.writeGuardPolicy = "filesystem-only";
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
}
|
|
4765
5076
|
/** Rewrite layout-dependent strings across a compiled backend-test DAG spec (plan A). */
|
|
4766
5077
|
function applyBackendTestLayoutToDagSpec(spec, layout) {
|
|
4767
5078
|
if (layout.isDefault) {
|
|
@@ -4804,11 +5115,57 @@ function applyBackendTestLayoutToDagSpec(spec, layout) {
|
|
|
4804
5115
|
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
4805
5116
|
spec.backendTestLayout = layout;
|
|
4806
5117
|
}
|
|
5118
|
+
/** Rewrite layout-dependent strings across a compiled frontend-test DAG spec. */
|
|
5119
|
+
function applyFrontendTestLayoutToDagSpec(spec, layout) {
|
|
5120
|
+
if (layout.isDefault) {
|
|
5121
|
+
spec.frontendTestLayout = layout;
|
|
5122
|
+
return;
|
|
5123
|
+
}
|
|
5124
|
+
const rewrite = (value) => applyFrontendTestLayoutToText(value, layout);
|
|
5125
|
+
for (const task of spec.tasks) {
|
|
5126
|
+
task.subtask_prompt = rewrite(task.subtask_prompt);
|
|
5127
|
+
if (task.outputContract)
|
|
5128
|
+
task.outputContract = rewrite(task.outputContract);
|
|
5129
|
+
task.writeSet = task.writeSet?.map(rewrite);
|
|
5130
|
+
task.allowedPaths = task.allowedPaths?.map(rewrite);
|
|
5131
|
+
task.forbiddenPaths = task.forbiddenPaths?.map(rewrite);
|
|
5132
|
+
if (task.dynamicExpansion?.childTask) {
|
|
5133
|
+
const child = task.dynamicExpansion.childTask;
|
|
5134
|
+
if (typeof child.subtaskPromptTemplate === "string") {
|
|
5135
|
+
child.subtaskPromptTemplate = rewrite(child.subtaskPromptTemplate);
|
|
5136
|
+
}
|
|
5137
|
+
if (typeof child.outputContract === "string") {
|
|
5138
|
+
child.outputContract = rewrite(child.outputContract);
|
|
5139
|
+
}
|
|
5140
|
+
if (Array.isArray(child.allowedPaths)) {
|
|
5141
|
+
child.allowedPaths = child.allowedPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
5142
|
+
}
|
|
5143
|
+
if (Array.isArray(child.writeSet)) {
|
|
5144
|
+
child.writeSet = child.writeSet.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
5145
|
+
}
|
|
5146
|
+
if (Array.isArray(child.forbiddenPaths)) {
|
|
5147
|
+
child.forbiddenPaths = child.forbiddenPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
if (task.shell?.commands) {
|
|
5151
|
+
task.shell.commands = task.shell.commands.map(rewrite);
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
spec.globalConstraints = spec.globalConstraints?.map(rewrite);
|
|
5155
|
+
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
5156
|
+
spec.frontendTestLayout = layout;
|
|
5157
|
+
}
|
|
5158
|
+
function allowedPathsCoverFrontendTestRoot(allowedPaths, testRoot) {
|
|
5159
|
+
const probe = `${testRoot}/_layout_probe_`;
|
|
5160
|
+
return allowedPaths.some((pattern) => pathMatchesPattern(testRoot, pattern) ||
|
|
5161
|
+
pathMatchesPattern(probe, pattern));
|
|
5162
|
+
}
|
|
4807
5163
|
// ---------------------------------------------------------------------------
|
|
4808
5164
|
// Frontend browser-test RAG DAG template
|
|
4809
5165
|
// ---------------------------------------------------------------------------
|
|
4810
5166
|
function buildFrontendTestHybridDag(sources) {
|
|
4811
5167
|
const rawFrontendTest = sources.taskConfig.frontendTest;
|
|
5168
|
+
const layout = resolveFrontendTestLayout(rawFrontendTest);
|
|
4812
5169
|
const config = {
|
|
4813
5170
|
// Default 32: common FE suites cover ~24 AC with multi-dimension cases; 20 caused map maxExpandedNodes failures.
|
|
4814
5171
|
maxCasesPerBatch: rawFrontendTest?.maxCasesPerBatch ?? 32,
|
|
@@ -4830,28 +5187,32 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4830
5187
|
};
|
|
4831
5188
|
const declaredRequirementIds = buildDagSourceBinding(sources).requirementIds;
|
|
4832
5189
|
const declaredAcIds = declaredRequirementIds.filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
|
|
5190
|
+
// Optional project-local UI anchor ledger: only mention it in prompts when it
|
|
5191
|
+
// exists so ledger-less projects keep generating without noise.
|
|
5192
|
+
const hasUiAnchorsLedger = Boolean(sources.repoRoot &&
|
|
5193
|
+
existsSync(path.join(sources.repoRoot, layout.ragDir, "ui-anchors.md")));
|
|
5194
|
+
const uiAnchorsLedgerInstruction = hasUiAnchorsLedger
|
|
5195
|
+
? `Also read ${layout.ragDir}/ui-anchors.md (UI anchor ledger). Every passing find assertion in generated cases must quote a ledger row whose 状态 is 有效 for the matching page x state section; rows marked 不可断言/失效 must not be used as passing assertions. When an AC names a control absent from the ledger section for that state, emit a blocked case note (blockedReason unique-control-unavailable) instead of exploratory find steps, and follow ledger 备注 alternatives (split-node short literals) exactly.`
|
|
5196
|
+
: "";
|
|
4833
5197
|
const reviewMode = config.reviewMode;
|
|
4834
5198
|
const blockingReview = reviewMode === "blocking";
|
|
4835
5199
|
const strictOutcomeGate = config.strictOutcomeGate;
|
|
4836
5200
|
const maxRerunAttempts = config.maxRerunAttempts;
|
|
4837
5201
|
const enableRetrospect = config.reports?.retrospect === true;
|
|
4838
5202
|
const enableL5Report = config.reports?.l5 !== false;
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
pattern === "**");
|
|
4842
|
-
if (!hasFrontendTestWriteScope) {
|
|
4843
|
-
throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
|
|
5203
|
+
if (!allowedPathsCoverFrontendTestRoot(sources.taskConfig.allowedPaths, layout.testRoot)) {
|
|
5204
|
+
throw new Error(`frontend-test requires task.json allowedPaths to cover "${layout.testRoot}/**" (or an explicit containing glob).`);
|
|
4844
5205
|
}
|
|
4845
5206
|
const forbidden = commonForbiddenPaths(sources);
|
|
4846
|
-
const ragWriteSet = [
|
|
5207
|
+
const ragWriteSet = [`${layout.ragDir}/**`];
|
|
4847
5208
|
const caseDraftWriteSet = [
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
5209
|
+
`${layout.casesDir}/FE-*.md`,
|
|
5210
|
+
`${layout.casesDir}/index.md`,
|
|
5211
|
+
`${layout.casesDir}/manifest.draft.json`,
|
|
4851
5212
|
];
|
|
4852
5213
|
// The shell materializer alone owns the final manifest boundary.
|
|
4853
|
-
const casesWriteSet = [
|
|
4854
|
-
const evidenceRoot =
|
|
5214
|
+
const casesWriteSet = [`${layout.casesDir}/**`];
|
|
5215
|
+
const evidenceRoot = layout.evidenceDir;
|
|
4855
5216
|
const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
|
|
4856
5217
|
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
4857
5218
|
const checklistScript = [
|
|
@@ -4870,6 +5231,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4870
5231
|
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
|
|
4871
5232
|
,
|
|
4872
5233
|
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
5234
|
+
"const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
|
|
5235
|
+
"const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
|
|
4873
5236
|
"for(const c of manifest.cases){",
|
|
4874
5237
|
" const id=c&&c.caseId||'?';",
|
|
4875
5238
|
" if(typeof c.caseId!=='string'||!caseIdRe.test(c.caseId))issues.push({ruleId:'case-id-shape',caseId:id,detail:'caseId must be FE-<FEATURE>-<NNN>-<dimension>, never AC-FE-*'});",
|
|
@@ -4888,6 +5251,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4888
5251
|
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
4889
5252
|
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
4890
5253
|
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
5254
|
+
" if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))){issues.push({ruleId:'ac-id-truncated',caseId:id,detail:ac+' is a truncated acceptance id (missing feature prefix or sequence number)'});continue;}",
|
|
4891
5255
|
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
4892
5256
|
" }",
|
|
4893
5257
|
" }",
|
|
@@ -4919,6 +5283,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4919
5283
|
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
4920
5284
|
"const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
|
|
4921
5285
|
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
5286
|
+
"const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
|
|
5287
|
+
"const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
|
|
4922
5288
|
"for(const c of manifest.cases){",
|
|
4923
5289
|
" if(!c||typeof c.caseId!=='string'||!/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/.test(c.caseId)) fail('case-id-shape','caseId must be FE-*, never AC-FE-*: '+String(c&&c.caseId));",
|
|
4924
5290
|
" if(/^AC-/i.test(c.caseId)) fail('case-id-is-ac','caseId must not be an acceptance id: '+c.caseId);",
|
|
@@ -4926,7 +5292,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4926
5292
|
" seen.add(c.caseId);",
|
|
4927
5293
|
" if(typeof c.dimension!=='string'||!dims.has(c.dimension)) fail('invalid-dimension',String(c.dimension));",
|
|
4928
5294
|
" if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) fail('ac-mapping','invalid acIds for '+c.caseId);",
|
|
4929
|
-
" for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
5295
|
+
" for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))) fail('ac-id-truncated','acIds entry is a truncated acceptance id (missing feature prefix or sequence number): '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
4930
5296
|
" c.casePath='testcase/frontend/cases/'+c.caseId+'.md';",
|
|
4931
5297
|
" c.evidenceDir='testcase/frontend/evidence/'+c.caseId+'/';",
|
|
4932
5298
|
" for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) fail('unsafe-path',k+': '+String(v)); }",
|
|
@@ -4986,15 +5352,11 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4986
5352
|
writeSet: ragWriteSet,
|
|
4987
5353
|
allowedPaths: [...ragWriteSet],
|
|
4988
5354
|
forbiddenPaths: forbidden,
|
|
4989
|
-
outputContract: "Write testcase/frontend/rag/standard-scenarios.v1.json for generate-time standard scenario coverage.",
|
|
4990
|
-
subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package.",
|
|
5355
|
+
outputContract: "Write testcase/frontend/rag/standard-scenarios.v1.json for generate-time standard scenario coverage. Copy docs/templates or harness.json governanceRoot templates (including ai_workspace/loop-agent/templates) when present; otherwise write the minimal STD-FE-SMOKE-ENTRY fallback.",
|
|
5356
|
+
subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package from docs/templates, governanceRoot/templates, or the init-projected ai_workspace/loop-agent/templates path.",
|
|
4991
5357
|
shell: {
|
|
4992
|
-
commands: [
|
|
4993
|
-
|
|
4994
|
-
"node -e",
|
|
4995
|
-
JSON.stringify("const fs=require('fs'),path=require('path');const dest='testcase/frontend/rag/standard-scenarios.v1.json';const candidates=[path.join('docs','templates','frontend-test-standard-scenarios.v1.json')];let src=null;for(const c of candidates){if(fs.existsSync(c)){src=c;break;}}fs.mkdirSync(path.dirname(dest),{recursive:true});if(src){fs.copyFileSync(src,dest);process.stdout.write(JSON.stringify({status:'copied',from:src,to:dest}));}else{const minimal={schemaVersion:1,id:'frontend-test-standard-scenarios-v1',scenarios:[{id:'STD-FE-SMOKE-ENTRY',title:'入口可打开',category:'smoke',priority:'must',testPoints:['open'],minCases:1}]};fs.writeFileSync(dest,JSON.stringify(minimal,null,2)+'\n');process.stdout.write(JSON.stringify({status:'fallback',to:dest}));}"),
|
|
4996
|
-
].join(" "),
|
|
4997
|
-
],
|
|
5358
|
+
commands: [],
|
|
5359
|
+
frontendTestStandardScenarios: {},
|
|
4998
5360
|
cwd: ".",
|
|
4999
5361
|
timeoutMs: 60_000,
|
|
5000
5362
|
},
|
|
@@ -5030,15 +5392,11 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5030
5392
|
writeSet: ragWriteSet,
|
|
5031
5393
|
allowedPaths: [...ragWriteSet],
|
|
5032
5394
|
forbiddenPaths: forbidden,
|
|
5033
|
-
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable (
|
|
5034
|
-
subtask_prompt: "Parse the resolved absolute baseUrl from testcase/frontend/rag/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. 4xx/5xx/DNS/timeout/
|
|
5395
|
+
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable with errorClass (connection-refused / dns-unresolved / connect-timeout / http-N / curl-exit-N). Node ERROR so generate/map do not run. Does not start the app.",
|
|
5396
|
+
subtask_prompt: "Parse the resolved absolute baseUrl from testcase/frontend/rag/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. Connection refused (curl 7) records errorClass=connection-refused and tells the operator to start the local app (scripts/serve.sh or npm start) then rerun from this node. 4xx/5xx/DNS/timeout/TLS => blockedReason frontend-base-url-unreachable with a distinct errorClass. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.",
|
|
5035
5397
|
shell: {
|
|
5036
|
-
commands: [
|
|
5037
|
-
|
|
5038
|
-
"node -e",
|
|
5039
|
-
JSON.stringify(`const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const patterns=[/baseUrl\\s*[:=]\\s*["'\\x60]?((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/base[-_ ]url\\s*[:=]\\s*["'\\x60]?((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/playwright-cli open --browser=chrome\\s+((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\}\\],"']*)/i];let baseUrl=null;for(const re of patterns){const m=s.match(re);if(m){baseUrl=m[1];break;}}if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl from context.md');baseUrl=baseUrl.replace(/[)\\}\\],."'\\x60]+$/,'');const sourceMatch=s.match(/baseUrlSource\\s*[:=]\\s*([^\\r\\n]+)/i);const baseUrlSource=sourceMatch?sourceMatch[1].trim():'context.md';let parsed;try{parsed=new URL(baseUrl);}catch(_){throw new Error('baseUrl must be absolute http(s): '+baseUrl);}if((parsed.protocol!=='http:'&&parsed.protocol!=='https:')||parsed.username||parsed.password||parsed.search||parsed.hash)throw new Error('unsafe baseUrl from context.md: '+redactUrl(baseUrl));if(/(?:^|\\.)(?:www\\.)?[^.]*(?:prod|production)/i.test(parsed.hostname))throw new Error('production URL forbidden: '+redactUrl(baseUrl));baseUrl=parsed.toString();const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrl:safe,baseUrlRedacted:safe,baseUrlSource,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated contextBaseUrl='+safe+' source='+baseUrlSource+' probe=reachable method='+used+' httpStatus='+statusNum);`),
|
|
5040
|
-
].join(" "),
|
|
5041
|
-
],
|
|
5398
|
+
commands: [],
|
|
5399
|
+
frontendTestEnvironmentProbe: {},
|
|
5042
5400
|
cwd: ".",
|
|
5043
5401
|
timeoutMs: 60000,
|
|
5044
5402
|
},
|
|
@@ -5059,6 +5417,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5059
5417
|
subtask_prompt: [
|
|
5060
5418
|
"Use skill playwright-cli-case-generator.",
|
|
5061
5419
|
"Read testcase/frontend/rag/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.",
|
|
5420
|
+
...(uiAnchorsLedgerInstruction ? [uiAnchorsLedgerInstruction] : []),
|
|
5062
5421
|
"Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and the draft case paths testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/manifest.draft.json. Write only those same draft paths. Do not write testcase/frontend/cases/manifest.json.",
|
|
5063
5422
|
"Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
|
|
5064
5423
|
"HARD ID CONTRACT (do not confuse these):",
|
|
@@ -5218,7 +5577,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5218
5577
|
subtaskPromptTemplate: [
|
|
5219
5578
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
|
|
5220
5579
|
"Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
|
|
5221
|
-
"1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
5580
|
+
"1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. Unique-control early stop: when an AC names a specific control that is absent from the first post-navigation snapshot after reaching the required state (and no setup-required gate appeared), do ONE find with the case's literal as evidence; if it returns 0 matches, record status=blocked with blockedReason unique-control-unavailable citing that single find - do NOT re-explore with alternative literals, repeated snapshots, navigation detours, or rerun loops. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
5222
5581
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
|
|
5223
5582
|
"Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
|
|
5224
5583
|
].join("\n\n"),
|
|
@@ -5241,7 +5600,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5241
5600
|
"const manifestPath='testcase/frontend/cases/manifest.json';",
|
|
5242
5601
|
"if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
|
|
5243
5602
|
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
|
|
5244
|
-
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
5603
|
+
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(r.status==='failed')reason='failed-retry';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||reason==='failed-retry'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
5245
5604
|
`fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
|
|
5246
5605
|
].join("");
|
|
5247
5606
|
tasks.push({
|
|
@@ -5309,6 +5668,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5309
5668
|
"RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite the authoritative execution.md and case-result.json; its final status replaces the earlier case result.",
|
|
5310
5669
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
|
|
5311
5670
|
"Read the concrete baseUrl from testcase/frontend/rag/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.",
|
|
5671
|
+
"Unique-control early stop: if the first post-navigation snapshot lacks the control an AC names (and no setup-required gate appeared), one find returning 0 matches is sufficient evidence - write status=blocked blockedReason unique-control-unavailable instead of exploratory retries.",
|
|
5312
5672
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}. Write fixed sections `### 执行摘要` and `### 实际执行步骤` to execution.md when available.",
|
|
5313
5673
|
].join("\n\n"),
|
|
5314
5674
|
},
|
|
@@ -5444,6 +5804,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5444
5804
|
verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
|
|
5445
5805
|
tasks,
|
|
5446
5806
|
};
|
|
5807
|
+
applyFrontendTestLayoutToDagSpec(spec, layout);
|
|
5447
5808
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
5448
5809
|
parseDagSpec(spec);
|
|
5449
5810
|
assertValidDagSpec(spec);
|
|
@@ -6506,15 +6867,27 @@ async function buildHybridDagForTemplate(sources, template, options = {}) {
|
|
|
6506
6867
|
spec = await buildSupervisedHybridDag(standard, sources);
|
|
6507
6868
|
}
|
|
6508
6869
|
applyProjectGovernanceReview(spec, template, sources);
|
|
6870
|
+
// Backend implementation templates only (D1 outer gate): assign the two
|
|
6871
|
+
// Pi extension buckets after all template-specific nodes exist.
|
|
6872
|
+
if (template === "standard-dag" ||
|
|
6873
|
+
template === "review-gated-dag" ||
|
|
6874
|
+
template === "supervised-implementation") {
|
|
6875
|
+
applyBackendPiExtensionBuckets(spec);
|
|
6876
|
+
}
|
|
6509
6877
|
// New generate path always emits DagSpec v4 + bindings.
|
|
6510
6878
|
spec.version = 4;
|
|
6511
6879
|
if (!spec.runtimeContract) {
|
|
6512
6880
|
spec.runtimeContract = GENERATED_DAG_RUNTIME_CONTRACT;
|
|
6513
6881
|
}
|
|
6514
|
-
spec.sourceBinding = buildDagSourceBinding(sources);
|
|
6882
|
+
spec.sourceBinding = buildDagSourceBinding(sources, template === "frontend-implementation" ? "frontend-implementation" : undefined);
|
|
6515
6883
|
spec.taskContractBinding = taskContractBinding;
|
|
6516
6884
|
assertNoGovernanceFlagOnDisallowedTemplate(spec, template);
|
|
6517
|
-
|
|
6885
|
+
if (template === "standard-dag" ||
|
|
6886
|
+
template === "review-gated-dag" ||
|
|
6887
|
+
template === "supervised-implementation") {
|
|
6888
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
6889
|
+
}
|
|
6890
|
+
spec = parseDagSpec(spec);
|
|
6518
6891
|
assertValidDagSpec(spec);
|
|
6519
6892
|
return spec;
|
|
6520
6893
|
}
|
|
@@ -6565,6 +6938,36 @@ export async function buildHybridDagFromTask(sources, options = {}) {
|
|
|
6565
6938
|
function cloneTask(task, patch = {}) {
|
|
6566
6939
|
return { ...task, ...patch };
|
|
6567
6940
|
}
|
|
6941
|
+
/**
|
|
6942
|
+
* Backend-implementation Pi extension buckets (plan 2026-08-21 D3):
|
|
6943
|
+
* - read side: every non-writer, non-closeout Pi node gets navigation
|
|
6944
|
+
* extensions (pi-codegraph + pi-lens);
|
|
6945
|
+
* - write side: bounded writers get only pi-codegraph (pi-lens registers
|
|
6946
|
+
* ast_grep_replace, which can bypass the writeSet — not loading it is
|
|
6947
|
+
* simpler and safer than filtering after load);
|
|
6948
|
+
* - everything else (closeout, shell, static, other templates) stays closed.
|
|
6949
|
+
* Idempotent: never overwrites an explicit piExtensions a task declares.
|
|
6950
|
+
*/
|
|
6951
|
+
function applyBackendPiExtensionBuckets(spec) {
|
|
6952
|
+
for (const task of spec.tasks) {
|
|
6953
|
+
if (task.piExtensions !== undefined)
|
|
6954
|
+
continue;
|
|
6955
|
+
if (task.executor !== "pi")
|
|
6956
|
+
continue;
|
|
6957
|
+
if (task.id === "closeout-pi")
|
|
6958
|
+
continue;
|
|
6959
|
+
task.piExtensions =
|
|
6960
|
+
task.toolProfile === "write" ? ["pi-codegraph"] : ["pi-codegraph", "pi-lens"];
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6963
|
+
function stampTargetTemplateTransientRetryProfile(spec) {
|
|
6964
|
+
for (const task of spec.tasks) {
|
|
6965
|
+
if (isTargetTemplateImplementPi(task) ||
|
|
6966
|
+
isCanonicalFinalVerifyShellRetryCandidate(task)) {
|
|
6967
|
+
task.transientRetryProfile = TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE;
|
|
6968
|
+
}
|
|
6969
|
+
}
|
|
6970
|
+
}
|
|
6568
6971
|
/**
|
|
6569
6972
|
* Apply default Pi retry policies to generated DAG nodes:
|
|
6570
6973
|
* - safe read-only planner/scout/reviewer/verifier/supervisor/closeout
|
|
@@ -6585,6 +6988,10 @@ function applyDefaultReadOnlyRetryPolicy(spec) {
|
|
|
6585
6988
|
: DEFAULT_READ_ONLY_PI_RETRY_POLICY;
|
|
6586
6989
|
continue;
|
|
6587
6990
|
}
|
|
6991
|
+
if (isTargetTemplateImplementPi(task)) {
|
|
6992
|
+
task.retryPolicy = TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY;
|
|
6993
|
+
continue;
|
|
6994
|
+
}
|
|
6588
6995
|
if (isWriterTransportRetryCandidate(task)) {
|
|
6589
6996
|
task.retryPolicy = WRITER_TRANSPORT_RETRY_POLICY;
|
|
6590
6997
|
}
|
|
@@ -6845,6 +7252,7 @@ function buildReviewGatedHybridDag(standard, sources) {
|
|
|
6845
7252
|
}));
|
|
6846
7253
|
spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewVerdictRecoveryNode(sources), buildReviewGateNode(sources));
|
|
6847
7254
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
7255
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
6848
7256
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
6849
7257
|
parseDagSpec(spec);
|
|
6850
7258
|
assertValidDagSpec(spec);
|
|
@@ -7176,6 +7584,9 @@ async function buildHardVerifyNode(sources) {
|
|
|
7176
7584
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
7177
7585
|
outputContract: "Archived hard verification stdout/stderr with exit codes; no worktree writes.",
|
|
7178
7586
|
subtask_prompt: "Run hard verification after repair round.",
|
|
7587
|
+
transientRetryProfile: sources.verifyCommands && commands.length > 0
|
|
7588
|
+
? TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE
|
|
7589
|
+
: undefined,
|
|
7179
7590
|
shell: {
|
|
7180
7591
|
commands,
|
|
7181
7592
|
verifyEvidence: buildVerifyEvidence({
|
|
@@ -7346,6 +7757,7 @@ async function buildSupervisedHybridDag(standard, sources) {
|
|
|
7346
7757
|
],
|
|
7347
7758
|
};
|
|
7348
7759
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
7760
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
7349
7761
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
7350
7762
|
parseDagSpec(spec);
|
|
7351
7763
|
assertValidDagSpec(spec);
|