@tea-agent/loop-agent 0.39.0-beta.0 → 0.39.0-beta.10

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.
Files changed (331) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +248 -95
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +12 -0
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +23 -5
  9. package/dist/cli/program.js +21 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli/update/runtime-activity.js +1 -29
  12. package/dist/cli.js +2 -1
  13. package/dist/commands/client-recovery.js +3 -0
  14. package/dist/commands/dag-follow-up.js +138 -0
  15. package/dist/commands/dag-rerun.js +55 -1
  16. package/dist/commands/init-model-catalog.js +464 -0
  17. package/dist/commands/init-upgrade.js +265 -97
  18. package/dist/commands/init.js +455 -28
  19. package/dist/commands/inspect-next.js +8 -0
  20. package/dist/commands/task-advance.js +19 -0
  21. package/dist/executors/dag-pi-executor.js +2306 -61
  22. package/dist/executors/pi-executor.js +7 -0
  23. package/dist/executors/pi-extension-resolver.js +233 -0
  24. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  25. package/dist/executors/pi-read-budget-policy.js +239 -0
  26. package/dist/executors/pi-sdk-executor.js +180 -33
  27. package/dist/executors/shell-executor.js +1249 -224
  28. package/dist/executors/shell-write-guard.js +14 -0
  29. package/dist/executors/workspace-write-snapshot.js +68 -0
  30. package/dist/infrastructure/harness/atomic-write.js +23 -0
  31. package/dist/shared/dag-failure-category.js +150 -0
  32. package/dist/shared/dag-prompt-override.js +27 -0
  33. package/dist/shared/openspec-spec.js +70 -4
  34. package/dist/shared/operator/capabilities.js +39 -0
  35. package/dist/shared/playwright-cli-command-policy.js +15 -0
  36. package/dist/shared/update/console-notifier.js +100 -0
  37. package/dist/{cli → shared}/update/npm-client.js +70 -15
  38. package/dist/{cli → shared}/update/state.js +41 -13
  39. package/dist/task/config-types.js +50 -5
  40. package/dist/task/contract/apply.js +11 -1
  41. package/dist/task/contract/constants.js +2 -0
  42. package/dist/task/contract/hash.js +3 -0
  43. package/dist/task/contract/observe.js +25 -4
  44. package/dist/task/contract/paths.js +2 -1
  45. package/dist/task/contract/project.js +18 -1
  46. package/dist/task/contract/schema.js +4 -1
  47. package/dist/task/contract/transaction.js +12 -0
  48. package/dist/task/frontend-project-capability.js +199 -18
  49. package/dist/task/source-prepare/completeness.js +188 -0
  50. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  51. package/dist/task/source-prepare/index.js +3 -0
  52. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  53. package/dist/task/source-prepare/ledger-review.js +214 -0
  54. package/dist/task/source-prepare/ledger.js +545 -0
  55. package/dist/task/source-prepare/parse-intent.js +24 -4
  56. package/dist/task/source-prepare/prepare.js +298 -1
  57. package/dist/task/source-prepare/semantic-intake.js +25 -5
  58. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  59. package/dist/task/source-prepare/types.js +22 -0
  60. package/dist/task/source-references.js +22 -1
  61. package/dist/worker/cli.js +18 -2
  62. package/dist/worker/console/chat/chat-event-store.js +62 -4
  63. package/dist/worker/console/chat/context-panel.js +7 -0
  64. package/dist/worker/console/chat/deferred-turn.js +12 -0
  65. package/dist/worker/console/chat/operation-card.js +8 -1
  66. package/dist/worker/console/chat/pi-console-config.js +81 -10
  67. package/dist/worker/console/chat/pi-runtime.js +264 -16
  68. package/dist/worker/console/chat/provider-error.js +98 -0
  69. package/dist/worker/console/chat/routes.js +298 -32
  70. package/dist/worker/console/chat/session-stats.js +179 -0
  71. package/dist/worker/console/chat/session-store.js +93 -63
  72. package/dist/worker/console/chat/todos.js +115 -0
  73. package/dist/worker/console/chat/tool-preview.js +42 -0
  74. package/dist/worker/console/chat/turn-process.js +20 -55
  75. package/dist/worker/console/chat/user-questions.js +266 -0
  76. package/dist/worker/console/chat/workspace-landing.js +14 -0
  77. package/dist/worker/console/console-handoff.js +82 -0
  78. package/dist/worker/console/console-update-and-init.js +115 -0
  79. package/dist/worker/console/console-update-runtime.js +84 -0
  80. package/dist/worker/console/dag-execution-receipt.js +33 -0
  81. package/dist/worker/console/draft-store.js +49 -0
  82. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  83. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  84. package/dist/worker/console/index.js +3 -0
  85. package/dist/worker/console/inspect-split.js +18 -0
  86. package/dist/worker/console/native-directory-picker.js +13 -0
  87. package/dist/worker/console/operation-run-facts.js +19 -3
  88. package/dist/worker/console/operation-store.js +227 -7
  89. package/dist/worker/console/operator-actions.js +388 -0
  90. package/dist/worker/console/operator-surface-health.js +1 -0
  91. package/dist/worker/console/operator-user-error.js +2 -2
  92. package/dist/worker/console/prd-intake-bridge.js +13 -0
  93. package/dist/worker/console/routes.js +355 -3
  94. package/dist/worker/console/security.js +67 -13
  95. package/dist/worker/console/server.js +609 -164
  96. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-BAsjg7ew.js} +1 -1
  97. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-CyIlrwZw.js} +1 -1
  98. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-Bo4tobhH.js} +1 -1
  99. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dfpow9gu.js} +1 -1
  100. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-DPmzG_B4.js} +1 -1
  101. package/dist/worker/console/static/assets/channel-DSrJAn0-.js +1 -0
  102. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-pKQpB1UW.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-5n9YYquZ.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DP3CtA2a.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DYxMJPUy.js} +1 -1
  106. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-CS3D50Io.js} +1 -1
  107. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-gSdDfo5_.js} +1 -1
  108. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-A48q0ZNt.js} +1 -1
  109. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-ow8Mm3DY.js} +1 -1
  110. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-CMKfLWYE.js} +1 -1
  111. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BiTBdQPX.js} +1 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DeMmPl0_.js +1 -0
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DeMmPl0_.js +1 -0
  114. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-Cm7exRz_.js} +1 -1
  115. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-D9O5qGOu.js} +1 -1
  116. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-CkvQnako.js} +1 -1
  117. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-B1rzp7CB.js} +1 -1
  118. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BP07Ipcc.js} +1 -1
  119. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BlPzweQb.js} +1 -1
  120. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-CxPa7n0S.js} +1 -1
  121. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CMTZs53A.js} +1 -1
  122. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-CxG_X52r.js} +1 -1
  123. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D8axtJM0.js} +1 -1
  124. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DIsGzn7b.js} +1 -1
  125. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CU9zkDG0.js} +1 -1
  126. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-DCjTmz5P.js} +1 -1
  127. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BksdmFzg.js} +1 -1
  128. package/dist/worker/console/static/assets/index-B-O5ChYq.css +1 -0
  129. package/dist/worker/console/static/assets/index-CfnY8AQx.js +407 -0
  130. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-CGzzzJtL.js} +1 -1
  131. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BdseB80N.js} +1 -1
  132. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-DM9gsgha.js} +1 -1
  133. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-3q-_9R2b.js} +1 -1
  134. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-B8N-Mxsd.js} +1 -1
  135. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-EPIHUUsu.js} +5 -5
  136. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CxwO12aa.js} +1 -1
  137. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-bPqbVX6Y.js} +1 -1
  138. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-KzqIvzeX.js} +1 -1
  139. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-CyofpWiV.js} +1 -1
  140. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-cTWJvLTP.js} +1 -1
  141. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-BK--q27B.js} +1 -1
  142. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-C0SDOHXK.js} +1 -1
  143. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-Cong6YIo.js} +1 -1
  144. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DUVkgZx8.js} +1 -1
  145. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C7XgxuZg.js} +1 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dzbnd5oX.js +1 -0
  147. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-D9Ayvwcy.js} +2 -2
  148. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BY0hHk6G.js +8 -0
  149. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-Byb000cH.js} +1 -1
  150. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DMLegPt4.js} +1 -1
  151. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-ChMMeOsP.js} +1 -1
  152. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-bEMEPisW.js} +1 -1
  153. package/dist/worker/console/static/index.html +2 -2
  154. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  155. package/dist/worker/console/static-src/app/console-types.js +68 -4
  156. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  157. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  158. package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
  159. package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
  160. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  161. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  162. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  163. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  164. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  165. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  166. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  167. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  168. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  169. package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
  170. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  171. package/dist/worker/console/static-src/operator-chat/useChatStream.js +102 -21
  172. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  173. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  174. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  175. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  176. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  177. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  178. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  179. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  180. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  181. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  182. package/dist/worker/console/workspace-context.js +344 -0
  183. package/dist/worker/console/workspace-registry.js +214 -0
  184. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  185. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  186. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  187. package/dist/worker/observability/init-runtime-activity.js +26 -0
  188. package/dist/worker/observability/read-model.js +22 -0
  189. package/dist/worker/observe/node-input.js +160 -3
  190. package/dist/worker/observe/node-process.js +377 -0
  191. package/dist/worker/observe/routes.js +224 -3
  192. package/dist/worker/observe/server.js +4 -0
  193. package/dist/worker/observe/shell-handler-keys.js +34 -0
  194. package/dist/worker/observe/static/api.js +90 -4
  195. package/dist/worker/observe/static/app.js +18 -70
  196. package/dist/worker/observe/static/constants.js +8 -1
  197. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  198. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  199. package/dist/worker/observe/static/dag-node-purpose.js +208 -0
  200. package/dist/worker/observe/static/format.js +60 -0
  201. package/dist/worker/observe/static/index.html +6 -1
  202. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  203. package/dist/worker/observe/static/markdown-render.js +20 -1
  204. package/dist/worker/observe/static/operator-chrome.css +145 -20
  205. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  206. package/dist/worker/observe/static/operator-chrome.js +330 -96
  207. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  208. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  209. package/dist/worker/observe/static/router.d.ts +46 -0
  210. package/dist/worker/observe/static/router.js +25 -1
  211. package/dist/worker/observe/static/state.d.ts +50 -0
  212. package/dist/worker/observe/static/state.js +184 -1
  213. package/dist/worker/observe/static/styles.css +395 -0
  214. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  215. package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
  216. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  217. package/dist/worker/observe/static/views/dag.js +14 -5
  218. package/dist/worker/observe/static/views/dashboard.js +6 -4
  219. package/dist/worker/observe/static/views/failures.js +5 -2
  220. package/dist/worker/observe/static/views/night.js +2 -2
  221. package/dist/worker/observe/static/views/pool.js +1 -0
  222. package/dist/worker/observe/static/views/run.js +3 -1
  223. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  224. package/dist/worker/observe/static/views/task.js +56 -1
  225. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
  226. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  227. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  228. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  229. package/dist/workflows/dag/contract-output-registry.js +15 -0
  230. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  231. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  232. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  233. package/dist/workflows/dag/failure-category.js +7 -116
  234. package/dist/workflows/dag/frontend-closeout.js +221 -0
  235. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  236. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  237. package/dist/workflows/dag/frontend-implementation-contract.js +534 -178
  238. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  239. package/dist/workflows/dag/frontend-prewrite-gate.js +371 -393
  240. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  241. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  242. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  243. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  244. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  245. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  246. package/dist/workflows/dag/frontend-repair.js +7 -424
  247. package/dist/workflows/dag/frontend-review-context.js +288 -14
  248. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  249. package/dist/workflows/dag/frontend-risk.js +15 -2
  250. package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
  251. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  252. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  253. package/dist/workflows/dag/frontend-shape.js +427 -0
  254. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  255. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  256. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  257. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  258. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  259. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  260. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  261. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  262. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  263. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  264. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  265. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  266. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  267. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  268. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  269. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  270. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  271. package/dist/workflows/dag/frontend-writer-admission.js +272 -0
  272. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  273. package/dist/workflows/dag/init-hybrid.js +984 -618
  274. package/dist/workflows/dag/interrupt-request.js +10 -3
  275. package/dist/workflows/dag/lifecycle.js +3 -3
  276. package/dist/workflows/dag/node-execution.js +468 -34
  277. package/dist/workflows/dag/prompt.js +44 -19
  278. package/dist/workflows/dag/recovery-lease.js +80 -0
  279. package/dist/workflows/dag/report.js +37 -1
  280. package/dist/workflows/dag/rerun-feedback.js +124 -1
  281. package/dist/workflows/dag/rerun-plan.js +144 -5
  282. package/dist/workflows/dag/rerun-run.js +81 -6
  283. package/dist/workflows/dag/rerun-task.js +29 -0
  284. package/dist/workflows/dag/retry-policy.js +318 -8
  285. package/dist/workflows/dag/runner.js +429 -122
  286. package/dist/workflows/dag/scheduler.js +114 -16
  287. package/dist/workflows/dag/structured-output-repair.js +712 -0
  288. package/dist/workflows/dag/types.js +321 -31
  289. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  290. package/dist/workflows/dag/validate.js +92 -32
  291. package/docs/README.md +2 -0
  292. package/docs/architecture/evolution.md +45 -0
  293. package/docs/init-surface.manifest.json +9 -0
  294. package/docs/operations/README.md +1 -1
  295. package/docs/operations/local-development-environment.md +21 -0
  296. package/docs/templates/README.md +2 -1
  297. package/docs/templates/agent-dag-report.schema.json +8 -2
  298. package/docs/templates/agent-dag.schema.json +45 -1
  299. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  300. package/docs/templates/frontend-test-case-checklist.md +2 -2
  301. package/docs/templates/frontend-test-dag.json +8 -10
  302. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  303. package/docs/templates/init-managed-agents.md +13 -3
  304. package/docs/templates/spec-registry.schema.json +45 -0
  305. package/harness.json +3 -3
  306. package/package.json +7 -1
  307. package/scripts/next-info.mjs +356 -0
  308. package/scripts/next-publish-gate.mjs +238 -0
  309. package/scripts/release-source-binding.mjs +251 -0
  310. package/skills/codebase-scout/SKILL.md +1 -1
  311. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  312. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  313. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  314. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  315. package/skills/frontend-design-review/SKILL.md +20 -41
  316. package/skills/frontend-implementation/SKILL.md +3 -4
  317. package/skills/frontend-implementation/references/design-spec.md +15 -10
  318. package/skills/frontend-implementation/references/node-contracts.md +23 -13
  319. package/skills/frontend-review/SKILL.md +22 -14
  320. package/skills/frontend-review/references/review-findings.md +6 -7
  321. package/skills/loop-agent/references/command-reference.md +12 -4
  322. package/skills/loop-agent/references/hybrid-dag.md +8 -1
  323. package/skills/playwright-cli/SKILL.md +23 -1
  324. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  325. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  326. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  327. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  328. package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
  329. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  330. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  331. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -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 { extractTaskSourceOpenspecPaths } from "../../shared/openspec-spec.js";
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, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
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, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
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
- * package.json, so those fixtures retain the historical generic fallback.
1289
- * Real projects never inherit loop-agent's commands when package.json exists.
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
- return genericFallback;
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
- function extractExplicitRequirementIds(...markdownInputs) {
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
- for (const markdown of markdownInputs) {
1440
+ const collect = (markdown, declarationsOnly) => {
1390
1441
  if (!markdown)
1391
- continue;
1392
- for (const match of markdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
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 = extractExplicitRequirementIds(sectionMatch[1]);
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 buildDagSourceBinding(sources) {
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 (sources.referenceDocuments ?? []).slice(0, MAX_INLINE_SOURCE_REFERENCE_DOCUMENTS)) {
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
- // Fact-source roles (requirement/acceptance) carry the fields contracts
1506
- // depend on (AC/scope/columns). Inject them in full so a fixed excerpt
1507
- // cannot silently drop definitions; archival roles stay excerpted.
1508
- const isFactRole = reference.role !== undefined &&
1509
- REQUIREMENT_FACT_ROLES.has(reference.role);
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
- const roleByPath = await loadReferenceRoles(sourceDir);
1564
- return Promise.all(referencePaths.map(async (filePath) => {
1565
- const relative = path
1566
- .relative(sourceDir, filePath)
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
- function resolveFrontendOpenspecGateConfig(sources) {
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 taskSourceCited = extractTaskSourceOpenspecPaths([sources.requirementMarkdown, sources.constraintMarkdown ?? ""].join("\n"));
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
- ? dedupeSorted([...declared, ...taskSourceCited])
2239
- : openspecCandidateSources.scanStrict;
2240
- return {
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:复用仓库既有组件/惯例(规范未点名)→ specReference 可为 null,rationale 说明复用的现有组件与依据。",
2256
- "- decision=new:规范与既有代码均无合适组件 → specReference 必须为 null,rationale 必须说明偏差理由(design-review 审,最终 review 复核)。",
2567
+ "- decision=reuse-existing:仅当该组件/惯例**确实已存在于仓库当前代码**(如复用现有 ActiveRunBadge 的 oc- class 惯例)→ specReference 可为 null,rationale 必须指明复用的具体现有组件/文件与依据。",
2568
+ "- decision=new:任务源/PRD 要求**新增**该组件(仓库当前不存在该组件文件)→ decision 必须为 new,不得标 reuse-existing;specReference 指向任务源 PRD 中定义该组件的 { path, section, line }(path 为 bound PRD 路径,如 .harness/tasks/<taskId>/source/references/prd.md),rationale 说明新增纯展示组件、复用既有 CSS 命名与主题变量约定;design-review 会复核 specReference 可追溯性。",
2257
2569
  "不得静默替换规范组件或自创组件而无偏差声明;spec 已定义该用途组件时不得改选其它组件。",
2258
- "prewrite gate 确定性交叉校验:specReference.path 非法 → component-spec-reference-invalid;未在 openspec-citations 引用块中引用或未真实读取 → component-spec-not-cited;候选桶非空且契约有 UI 可见工作而 uiComponentChoices 缺失/空 → component-choices-missing。",
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
- const classified = capability.designEvidence.classified;
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 conditional
2313
- // revision/final-review branch. The deterministic prewrite gate consumes the
2314
- // surviving plan and design review directly.
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-plan-revision-pi") {
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-prewrite-gate-shell") {
2339
- const gate = task.shell?.frontendPrewriteGate;
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-plan-pi", "frontend-design-review-pi"],
2343
- dependsPolicy: "all",
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
- frontendPrewriteGate: {
2349
- ...gate,
2350
- planFromNodeId: "frontend-plan-pi",
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-prewrite-gate-shell"]) {
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: { type: "implementation-outcome-v1" },
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,150 +2801,63 @@ async function buildFrontendHybridDagFromTask(sources) {
2418
2801
  allowedPaths: taskConfig.allowedPaths,
2419
2802
  complexity: taskConfig.complexity,
2420
2803
  });
2421
- const frontendSourceBinding = buildDagSourceBinding(sources);
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
- "The plan/revision node emits only a fenced json contract — there is no Markdown plan explanation to read. Review these fields:",
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: files, routes, publicApiChanges",
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: implementationSteps[], stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap",
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
- const frontendPlanFieldGuide = [
2522
- "## Compact planner field guide",
2523
- "Emit only editable fields: requirements[], implementationSteps[], targets.routes/publicApiChanges, uiStates[], interactions[], mockApi.strategy/activation/endpoints, verificationTargets[], designEvidence, evidenceGaps[], stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap.",
2524
- "Protected and forbidden: schemaVersion, sourceBinding, riskLevel, targets.files, mockApi.productionDefaultOff, schemaId, targetFiles, requirementCoverage.",
2525
- "Invariants: every requirement has expectedOutcome; every interaction has trigger + expectedBehavior; applicable uiStates have expectedBehavior + implementationTargets + verificationTargetIds; non-applicable uiStates have notApplicableReason; commandLabel is frozen; mockApi production default remains off.",
2526
- "Enums: mock strategy native|browser-intercept|request-adapter|not-needed; uiComponentChoices decision specified|reuse-existing|new. Omit optional empty strings and unchanged revision fields.",
2527
- ].join("\n");
2528
- const sourceContext = [
2529
- buildSourceContextBlock(sources),
2530
- capabilityContextBlock,
2531
- ]
2532
- .filter(Boolean)
2533
- .join("\n\n");
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 (per-AC echo with bad/good examples)`,
2540
- `For each requirement ID below, echo the ID verbatim and confirm: expectedOutcome (user-observable or logic-observable), implementation targets (files), and verification targets (commandLabel + file).`,
2541
- `Do not skip any ID. Use the bad/good patterns below as reference for each field.`,
2542
- ``,
2543
- `Bad example (empty expectedOutcome, empty targets -- REJECTED at contract materialization):`,
2544
- `- AC-001: expectedOutcome="" implementationTargets=[] verificationTargets=[]`,
2545
- ``,
2546
- `Good example (concrete expectedOutcome, real files, real verification targets):`,
2547
- `- AC-001: expectedOutcome="TypeScript compilation exits with code 0 and produces no errors in dist/" implementationTargets=["src/app.tsx"] verificationTargets=["vt-typecheck":"npm run typecheck","tsconfig.json"]`,
2548
- ``,
2549
- ...requirementIds.map((id) => `- ${id}: [expectedOutcome] [implementation files] [verification targets]`),
2550
- ``,
2551
- `Every requirement MUST have a non-empty expectedOutcome. Every interaction MUST have non-empty trigger and expectedBehavior. UI states with applicable=true MUST have non-empty expectedBehavior. Empty strings or omitted fields for these will cause contract rejection.`,
2552
- ].join("\n")
2553
- : "";
2554
- const verificationTargetFileInstruction = [
2555
- `## Verification target file semantics`,
2556
- `verificationTargets[].file is the code file that the target verifies (the file the writer changes), NOT where the command is defined.`,
2557
- `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.`,
2558
- `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.`,
2559
- ].join("\n");
2560
- const mandatorySourceReadInstruction = [
2561
- "## Mandatory full source read before contracting",
2562
- "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.",
2563
- "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.",
2564
- ].join("\n");
2565
2861
  const strategy = resolveDagVerifyStrategy(taskConfig);
2566
2862
  const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
2567
2863
  const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
@@ -2571,15 +2867,15 @@ async function buildFrontendHybridDagFromTask(sources) {
2571
2867
  ? [`See 执行约束.md in task source (${sources.taskId})`]
2572
2868
  : []),
2573
2869
  ...STANDARD_GLOBAL_CONSTRAINTS,
2574
- "Frontend implementation DAGs must pass the effective final design verdict gate before any write node executes; an initial pass uses the original plan, while request-revision selects the read-only revision and final-review branch.",
2575
- "Final design gate pass is the only authorization for frontend implementation writes.",
2576
- "Plan revision remains read-only and never edits business code.",
2577
- "Design revision failures route to replan-and-rerun, never dev-fix.",
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.",
2578
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.",
2579
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.",
2580
2876
  "Mock-backed behavior evidence proves only the documented frontend contract, never real API integration.",
2581
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.",
2582
- "frontend review must block closeout unless review verdict is exactly VERDICT: pass.",
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).",
2583
2879
  `Frontend risk classification: ${frontendRisk.selectedRisk} — ${frontendRisk.reason}`,
2584
2880
  frontendRisk.forceFullGates
2585
2881
  ? "High-risk or supervised: keep full design gates; do not weaken write boundaries."
@@ -2595,7 +2891,14 @@ async function buildFrontendHybridDagFromTask(sources) {
2595
2891
  return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints, blockedReason);
2596
2892
  }
2597
2893
  const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
2598
- const staticFallbackCommands = fallbackVerifyCommands.staticCommands;
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];
2599
2902
  const behaviorFallbackCommands = fallbackVerifyCommands.behaviorCommands;
2600
2903
  const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
2601
2904
  repoRoot: sources.repoRoot,
@@ -2694,11 +2997,65 @@ async function buildFrontendHybridDagFromTask(sources) {
2694
2997
  ...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
2695
2998
  ].join("\n");
2696
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
+ }
2697
3005
  if (frontendSourceMentionsMock(frontendSources) &&
2698
3006
  frontendMockStrategyMustBeNotNeeded(frontendSources)) {
2699
3007
  advisories.push("auto 模式已将 Mock 策略收窄为 not-needed:任务源提到接口/API/Mock 需求,但仓库无确认 Mock 能力或无确定性 Mock 验证命令。若项目规范要求 Mock,请声明 frontendMock.verifyCommands 或 policy:required 后重新生成 DAG。");
2700
3008
  }
2701
- 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
+ : "";
2702
3059
  // Generation-frozen component/theme specification bucket (ADR 0016). Derived
2703
3060
  // from the classified component/theme/rule.components buckets; the prewrite
2704
3061
  // gate consumes it to enforce uiComponentChoices presence and specReference
@@ -2715,7 +3072,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2715
3072
  advisories.push("openspec 策略 cited:契约声明的 requiredReadPaths 与任务源引用均为空,prewrite gate 不强制读取 openspec;如需增强规范门禁,请在 task.json.frontendOpenspec.requiredReadPaths 声明必读路径或在任务源中显式引用 openspec 文件。");
2716
3073
  }
2717
3074
  else {
2718
- advisories.push(`openspec 策略 cited:候选 ${openspecGate.openspecCandidatePaths.length} 个(declared ${openspecGate.openspecCandidateSources.declared.length} / task-source-cited ${openspecGate.openspecCandidateSources.taskSourceCited.length}),plan/review 必须在 openspec-citations 引用块中逐条引用并真实读取。`);
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 事件佐证。`);
2719
3076
  }
2720
3077
  }
2721
3078
  else {
@@ -2753,13 +3110,23 @@ async function buildFrontendHybridDagFromTask(sources) {
2753
3110
  allowedPaths: readOnlyPaths,
2754
3111
  forbiddenPaths,
2755
3112
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
2756
- outputContract: "Markdown contract with Scope, Non-goals, Acceptance Criteria, UI States, Target Runtime Environment, Risks, and Verification Expectations. No file writes.",
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.",
2757
3114
  subtask_prompt: [
2758
- "Read task source and produce a concise frontend implementation contract.",
2759
- "Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations.",
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
+ ] : []),
2760
3128
  "Read-only: do not modify code, docs, artifacts, or repository files.",
2761
- mandatorySourceReadInstruction,
2762
- sourceContext,
3129
+ sourceContexts.contract,
2763
3130
  ].join("\n\n"),
2764
3131
  },
2765
3132
  {
@@ -2769,17 +3136,19 @@ async function buildFrontendHybridDagFromTask(sources) {
2769
3136
  executor: "pi",
2770
3137
  complexity: mapTaskComplexity(taskConfig.complexity),
2771
3138
  writePolicy: "read-only",
3139
+ retryPolicy: FRONTEND_SCOUT_COMPLETENESS_RETRY_POLICY,
2772
3140
  allowedPaths: readOnlyPaths,
2773
3141
  forbiddenPaths,
2774
3142
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
2775
- outputContract: "Markdown scout report with a required TARGET_SURFACE section covering frontend stack, routes, components, styling system, existing design conventions, state/data flow, test entry points, reuse opportunities, and risks. No file writes.",
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.",
2776
3144
  subtask_prompt: [
2777
3145
  "Inspect frontend code, routing, components, styles, package scripts, and tests.",
2778
3146
  "Return code and design observations, existing reuse opportunities, and verification entry points.",
2779
- "Begin with a TARGET_SURFACE section containing exactly these labels: entrypoint, routeOrMount, implementationPaths, testPaths, dataSource, allowedPathConflicts. Use repository-relative POSIX paths. 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.",
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.",
2780
3148
  "Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
2781
3149
  "Read-only: do not modify repository files.",
2782
- sourceContext,
3150
+ sourceContexts.scout,
3151
+ scopedOpenspecContext,
2783
3152
  ].join("\n\n"),
2784
3153
  },
2785
3154
  {
@@ -2789,246 +3158,200 @@ async function buildFrontendHybridDagFromTask(sources) {
2789
3158
  executor: "pi",
2790
3159
  complexity: "MED",
2791
3160
  writePolicy: "read-only",
2792
- outputMode: "structured-required",
2793
- retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
3161
+ retryPolicy: FRONTEND_PLAN_LADDER_RETRY_POLICY,
3162
+ readBudget: {
3163
+ maxFiles: 40,
3164
+ maxBytes: 512 * 1024,
3165
+ maxOperations: 40,
3166
+ onExhaustion: "return-guidance",
3167
+ },
3168
+ allowedPaths: readOnlyPaths,
3169
+ forbiddenPaths,
3170
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
2794
3171
  structuredContractOutput: {
2795
- schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID,
3172
+ schemaId: "frontend-implementation-contract-plan-patch-v1",
2796
3173
  retryOnInvalid: true,
2797
3174
  skeleton: frontendContractSkeleton,
2798
3175
  },
2799
- allowedPaths: readOnlyPaths,
2800
- forbiddenPaths,
2801
- skills: [],
2802
- outputContract: "JSON-only patch output: one-line lead-in, then exactly ONE fenced json object (```json ... ```) containing only the editable RFC 7386 plan patch for the runtime contract skeleton. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. Immediately after it, append exactly one ```openspec-citations``` fenced citation block. 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.",
3176
+ 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.",
2803
3177
  subtask_prompt: [
2804
- "Use frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence to fill the runtime-owned frontend contract skeleton. Return JSON-only output containing only an editable RFC 7386 plan patch. The runtime already owns schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff; omit those protected paths even when their values look obvious.",
2805
- "The patch fields become the complete implementation plan after deterministic merge. Do not produce a separate plan document, prose mirror, or full contract.",
2806
- "Select the Mock / API strategy only in the patch. Encode endpoint/fixture mapping, explicit activation, verification commands, and Real Integration Gap in schema-defined editable fields; productionDefaultOff comes from the protected skeleton and there is no second plan output.",
2807
- "Encode ordered steps (implementationSteps), target files, UI state handling, styling/component strategy (stylingStrategy), interaction notes, Mock/API strategy, dependency policy (dependencyPolicy), deterministic verification entrypoints, Real Integration Gap (realIntegrationGap), and residual risks (residualRisks) into the contract JSON fields. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
2808
- "Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
2809
- "Consume the Scout TARGET_SURFACE evidence before selecting files. Preserve the discovered existing entrypoint and data source. If implementationPaths or testPaths are outside task allowedPaths, record a blocking scope conflict; do not substitute a new page or silently broaden the writeSet.",
2810
- "Output in this exact order: (1) exactly one fenced json object containing the editable plan patch; (2) exactly one openspec-citations citation fenced block appended immediately after it. Do NOT emit protected skeleton fields, a full contract, Markdown plan explanation, raw JSON, or any other fenced block.",
2811
- "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.",
2812
- requirementCoverageInstruction,
2813
- "verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
2814
- verificationTargetFileInstruction,
2815
- "Read-only: do not modify code, docs, artifacts, or repository files.",
2816
- mandatorySourceReadInstruction,
3178
+ "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.",
3179
+ "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.",
3180
+ "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.",
3181
+ `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.`,
3182
+ ...(requiresOpenspecClassification ? ["When a component choice uses an OpenSpec selection, cite that selection; otherwise do not classify unrelated candidates."] : []),
3183
+ "Call finalize_plan exactly once after the necessary typed facts. Return no Markdown narrative.",
3184
+ "READ BUDGET: use injected Contract/Scout facts. Do not search or re-read task sources. If an upstream preview is truncated, you may read only its explicitly listed artifact path. You may also read one or more Scout-confirmed target paths when a specific component/state/test decision needs direct evidence. grep/find/ls are unavailable; a non-Scout repository path is denied. After the read cap, stop and finalize from existing facts or record a genuine evidence gap.",
2817
3185
  fixedVerificationContext,
2818
- sourceContext,
3186
+ scopedOpenspecContext,
2819
3187
  mockContextBlock,
2820
- frontendPlanFieldGuide,
3188
+ frontendContractFieldSummary,
2821
3189
  frontendComponentConformanceInstruction,
2822
- openspecCitationInstruction,
2823
3190
  ].join("\n\n"),
2824
3191
  },
2825
3192
  {
2826
- id: "frontend-design-review-pi",
3193
+ id: "frontend-design-policy-shell",
2827
3194
  depends_on: ["frontend-plan-pi"],
2828
- role: "reviewer",
2829
- executor: "pi",
2830
- complexity: "MED",
3195
+ role: "verifier",
3196
+ executor: "shell",
3197
+ complexity: "LOW",
2831
3198
  writePolicy: "read-only",
2832
- artifactFirstUpstreamNodeIds: ["frontend-plan-pi"],
2833
3199
  allowedPaths: readOnlyPaths,
2834
3200
  forbiddenPaths,
2835
- skills: FRONTEND_DESIGN_REVIEW_SKILLS,
2836
- outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
2837
- outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings, Required Plan Corrections, and Checked Items. No file writes.",
2838
- subtask_prompt: [
2839
- "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.",
2840
- "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2841
- "Request revision 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.",
2842
- "Also request revision 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.",
2843
- "Component selection conformance is a hard blocking condition: VERDICT: request-revision 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; or when any uiComponentChoices specReference.path is not cited in the openspec-citations block or has no successful read event.",
2844
- "Read-only: do not modify repository files.",
2845
- fixedVerificationContext,
2846
- sourceContext,
2847
- frontendContractFieldSummary,
2848
- mockContextBlock,
2849
- ].join("\n\n"),
2850
- },
2851
- {
2852
- id: "frontend-plan-revision-pi",
2853
- depends_on: ["frontend-plan-pi", "frontend-design-review-pi"],
2854
- runIf: "$.nodes['frontend-design-review-pi'].firstVerdictLine == 'VERDICT: request-revision'",
2855
- role: "planner",
2856
- executor: "pi",
2857
- complexity: "MED",
2858
- writePolicy: "read-only",
2859
- artifactFirstUpstreamNodeIds: ["frontend-plan-pi"],
2860
- outputMode: "structured-required",
2861
- retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
2862
- structuredContractOutput: {
2863
- schemaId: "frontend-implementation-contract-revision-patch-v1",
2864
- retryOnInvalid: true,
3201
+ 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).",
3202
+ 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.",
3203
+ shell: {
3204
+ commands: [],
3205
+ frontendDesignPolicy: {
3206
+ schemaVersion: 1,
3207
+ planFromNodeId: "frontend-plan-pi",
3208
+ requiredRequirementIds: requirementIds,
3209
+ allowedMockStrategies: taskConfig.frontendMock?.policy === "disabled" ||
3210
+ frontendMockStrategyMustBeNotNeeded(frontendSources)
3211
+ ? ["not-needed"]
3212
+ : taskConfig.frontendMock?.policy === "required"
3213
+ ? [
3214
+ "native",
3215
+ "browser-intercept",
3216
+ "request-adapter",
3217
+ ]
3218
+ : [
3219
+ "native",
3220
+ "browser-intercept",
3221
+ "request-adapter",
3222
+ "not-needed",
3223
+ ],
3224
+ mockCommandLabels: mockVerifyEvidence?.commandLabels ?? [],
3225
+ artifactName: "frontend-implementation-contract.json",
3226
+ outputDir: "contracts",
3227
+ requireSourceFreshness: true,
3228
+ implementationWriteSet: implementPaths.writeSet,
3229
+ openspecPolicy: openspecGate.openspecPolicy,
3230
+ openspecSpecRoots: taskConfig.frontendOpenspec?.specRoots ?? [
3231
+ ...DEFAULT_FRONTEND_SPEC_ROOTS,
3232
+ ],
3233
+ ...(requiresOpenspecClassification
3234
+ ? { openspecSelectionNodeId: "frontend-contract-pi" }
3235
+ : {}),
3236
+ openspecMandatoryPaths: openspecGate.openspecMandatoryPaths,
3237
+ openspecCandidateSources: {
3238
+ declared: openspecGate.openspecCandidateSources.declared,
3239
+ taskSourceCited: openspecGate.openspecCandidateSources.taskSourceCited,
3240
+ scanStrict: openspecGate.openspecCandidateSources.scanStrict,
3241
+ },
3242
+ openspecCandidatePaths: openspecGate.openspecCandidatePaths,
3243
+ ...(openspecGate.openspecCandidateSnapshots
3244
+ ? {
3245
+ openspecCandidateSnapshots: openspecGate.openspecCandidateSnapshots,
3246
+ }
3247
+ : {}),
3248
+ componentSpecCandidatePaths,
3249
+ allowedDependencies: declaredDependencies,
3250
+ },
3251
+ cwd: ".",
3252
+ timeoutMs: 60000,
2865
3253
  },
2866
- allowedPaths: readOnlyPaths,
2867
- forbiddenPaths,
2868
- skills: [],
2869
- outputContract: "When the initial design review requests revision, return a one-line lead-in followed by 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 after it, append 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.",
2870
- subtask_prompt: [
2871
- "Consume frontend-plan-pi (original contract JSON) and frontend-design-review-pi (first design review findings).",
2872
- "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.",
2873
- "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.",
2874
- requirementCoverageInstruction,
2875
- "Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
2876
- "Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
2877
- "Output in this exact order: (1) exactly one fenced json object containing the merge-patch delta — this is the only plan output the prewrite gate applies on the original contract; (2) exactly one openspec-citations citation fenced block appended immediately after it. Do NOT emit a full contract, Markdown explanation, or prose — the output is JSON-only. Do not emit any raw JSON or JSON objects in prose. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block. Do not include secrets or unsafe paths.",
2878
- "Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the effective (merged) contract; do not reduce behavior semantics to IDs and paths.",
2879
- "verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
2880
- verificationTargetFileInstruction,
2881
- fixedVerificationContext,
2882
- sourceContext,
2883
- frontendPlanFieldGuide,
2884
- mockContextBlock,
2885
- frontendComponentConformanceInstruction,
2886
- openspecCitationInstruction,
2887
- ].join("\n\n"),
2888
3254
  },
2889
3255
  {
2890
- id: "frontend-final-design-review-pi",
2891
- depends_on: [
2892
- "frontend-plan-revision-pi",
2893
- "frontend-plan-pi",
2894
- "frontend-design-review-pi",
2895
- ],
2896
- dependsPolicy: "all-or-condition-skip",
2897
- runIf: "$.nodes['frontend-design-review-pi'].firstVerdictLine == 'VERDICT: request-revision'",
3256
+ id: "frontend-design-review-pi",
3257
+ depends_on: ["frontend-design-policy-shell"],
2898
3258
  role: "reviewer",
2899
3259
  executor: "pi",
2900
3260
  complexity: "MED",
2901
3261
  writePolicy: "read-only",
2902
- artifactFirstUpstreamNodeIds: [
2903
- "frontend-plan-revision-pi",
2904
- "frontend-plan-pi",
2905
- ],
3262
+ readBudget: FRONTEND_READ_BUDGETS.designReview,
3263
+ retryPolicy: DEFAULT_READ_ONLY_PI_RETRY_POLICY,
2906
3264
  allowedPaths: readOnlyPaths,
2907
3265
  forbiddenPaths,
2908
3266
  skills: FRONTEND_DESIGN_REVIEW_SKILLS,
2909
- outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
2910
- 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.",
3267
+ 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.",
2911
3268
  subtask_prompt: [
2912
- "Audit the revised frontend plan before implementation. This node runs only after request-revision and consumes the merge-patch delta from frontend-plan-revision-pi applied on the original frontend-plan-pi contract JSON — there is no separate plan prose.",
2913
- "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2914
- "Verify that every Required Plan Correction from the initial design review has been fully addressed.",
2915
- "Recheck the selected Mock / API strategy, contract-to-fixture mapping, authorized paths/dependencies, explicit activation, production-default-off behavior, behavior verification, and Real Integration Gap. MOCK_STRATEGY: blocked cannot receive VERDICT: pass.",
2916
- "Review every explicit REQ-/BR-/AC- mapping; the downstream prewrite gate also checks identifier retention deterministically.",
2917
- "Request revision if any design gap remains, if corrections are incomplete, or if the revised plan introduces new unaddressed issues.",
2918
- "Also request revision for component selection non-conformance: spec-defined components silently replaced or self-invented without a declared deviation, uiComponentChoices missing for UI-visible work, or a uiComponentChoices specReference.path not cited in the openspec-citations block / not actually read.",
3269
+ "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.",
3270
+ "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.",
3271
+ "request_design_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
3272
+ "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).",
3273
+ "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.",
3274
+ "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.",
3275
+ "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.",
3276
+ "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.",
2919
3277
  "Read-only: do not modify repository files.",
3278
+ "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.",
3279
+ "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.",
2920
3280
  fixedVerificationContext,
2921
- sourceContext,
3281
+ sourceContexts.designReview,
3282
+ scopedOpenspecContext,
2922
3283
  frontendContractFieldSummary,
2923
3284
  mockContextBlock,
2924
3285
  ].join("\n\n"),
2925
3286
  },
2926
3287
  {
2927
- id: "frontend-prewrite-gate-shell",
2928
- depends_on: [
2929
- "frontend-final-design-review-pi",
2930
- "frontend-design-review-pi",
2931
- "frontend-plan-revision-pi",
2932
- "frontend-plan-pi",
2933
- ],
2934
- dependsPolicy: "all-or-condition-skip",
3288
+ id: "frontend-writer-admission-shell",
3289
+ depends_on: ["frontend-design-policy-shell", "frontend-design-review-pi"],
2935
3290
  role: "verifier",
2936
3291
  executor: "shell",
2937
3292
  complexity: "LOW",
2938
3293
  writePolicy: "read-only",
2939
3294
  allowedPaths: readOnlyPaths,
2940
3295
  forbiddenPaths,
2941
- outputContract: "Deterministic prewrite authorization: resolve effective plan/review, require VERDICT: pass, retain every requirement id, validate Mock policy, and materialize the canonical implementation contract.",
2942
- subtask_prompt: "Fail closed unless the effective reviewed plan is source-bound, requirement-complete, Mock-policy compliant, schema-valid, and approved.",
3296
+ 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).",
3297
+ 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.",
2943
3298
  shell: {
2944
3299
  commands: [],
2945
- frontendPrewriteGate: {
3300
+ frontendWriterAdmission: {
2946
3301
  schemaVersion: 1,
2947
- planFromNodeId: "frontend-plan-revision-pi",
2948
- planFallbackFromNodeIds: ["frontend-plan-pi"],
2949
- reviewFromNodeId: "frontend-final-design-review-pi",
2950
- reviewFallbackFromNodeIds: ["frontend-design-review-pi"],
2951
- requiredRequirementIds: requirementIds,
2952
- mockCommandLabels: mockVerifyEvidence?.commandLabels ?? [],
3302
+ designReviewFromNodeId: "frontend-design-review-pi",
3303
+ frozenCommandLabels: [
3304
+ ...staticVerifyEvidence.commandLabels,
3305
+ ...behaviorVerifyEvidence.commandLabels,
3306
+ ...(mockVerifyEvidence?.commandLabels ?? []),
3307
+ ],
2953
3308
  allowedMockStrategies: taskConfig.frontendMock?.policy === "disabled" ||
2954
3309
  frontendMockStrategyMustBeNotNeeded(frontendSources)
2955
3310
  ? ["not-needed"]
2956
3311
  : taskConfig.frontendMock?.policy === "required"
2957
- ? ["native", "browser-intercept", "request-adapter"]
3312
+ ? [
3313
+ "native",
3314
+ "browser-intercept",
3315
+ "request-adapter",
3316
+ ]
2958
3317
  : [
2959
3318
  "native",
2960
3319
  "browser-intercept",
2961
3320
  "request-adapter",
2962
3321
  "not-needed",
2963
3322
  ],
2964
- artifactName: "frontend-implementation-contract.json",
2965
- outputDir: "contracts",
2966
- revisionPatch: true,
2967
- planMdArtifactName: "frontend-plan.md",
2968
- requireSourceFreshness: true,
2969
- implementationWriteSet: implementPaths.writeSet,
2970
- openspecPolicy: openspecGate.openspecPolicy,
2971
- openspecCandidateSources: {
2972
- declared: openspecGate.openspecCandidateSources.declared,
2973
- taskSourceCited: openspecGate.openspecCandidateSources.taskSourceCited,
2974
- scanStrict: openspecGate.openspecCandidateSources.scanStrict,
2975
- },
2976
- openspecCandidatePaths: openspecGate.openspecCandidatePaths,
2977
- componentSpecCandidatePaths,
3323
+ ...(lintShellCommands.length > 0 && lintVerifyEvidence
3324
+ ? {
3325
+ lintCommands: lintShellCommands,
3326
+ lintEvidence: lintVerifyEvidence,
3327
+ }
3328
+ : {}),
2978
3329
  },
2979
3330
  cwd: ".",
2980
3331
  timeoutMs: 60000,
2981
3332
  },
2982
3333
  },
2983
- ...(lintShellCommands.length > 0 && lintVerifyEvidence
2984
- ? [
2985
- {
2986
- id: "frontend-lint-baseline-shell",
2987
- depends_on: ["frontend-prewrite-gate-shell"],
2988
- role: "verifier",
2989
- executor: "shell",
2990
- complexity: "LOW",
2991
- writePolicy: "read-only",
2992
- allowedPaths: readOnlyPaths,
2993
- forbiddenPaths,
2994
- outputContract: "Capture writer-preceding lint output as frontend-lint-baseline-v1 without treating existing lint diagnostics as writer failure.",
2995
- 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.",
2996
- shell: {
2997
- commands: lintShellCommands,
2998
- frontendLintBaseline: {
2999
- schemaVersion: 1,
3000
- lintCommands: lintShellCommands,
3001
- lintEvidence: lintVerifyEvidence,
3002
- },
3003
- cwd: ".",
3004
- timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
3005
- },
3006
- },
3007
- ]
3008
- : []),
3009
3334
  {
3010
3335
  id: implementId,
3011
- depends_on: [
3012
- "frontend-prewrite-gate-shell",
3013
- ...(lintShellCommands.length > 0
3014
- ? ["frontend-lint-baseline-shell"]
3015
- : []),
3016
- ],
3336
+ depends_on: ["frontend-writer-admission-shell"],
3017
3337
  ...buildFrontendWriterNodeDefaults({
3018
3338
  complexity: resolveWriterComplexity(taskConfig),
3019
3339
  writeSet: implementPaths.writeSet,
3020
3340
  allowedPaths: implementPaths.allowedPaths,
3021
3341
  forbiddenPaths,
3342
+ writerOutcomePolicyType: "frontend-facts-v1",
3022
3343
  }),
3023
- outputContract: "First non-empty line must be exactly one of: IMPLEMENTATION_OUTCOME: changed; IMPLEMENTATION_OUTCOME: already-satisfied; IMPLEMENTATION_OUTCOME: blocked. Then a Markdown delivery 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.",
3344
+ 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.",
3024
3345
  subtask_prompt: [
3025
- "Implement against the validated run-owned Frontend Implementation Contract from frontend-prewrite-gate-shell (path/schema/hash). Do not rebuild the contract from Markdown alone.",
3346
+ "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.",
3026
3347
  "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.",
3027
3348
  "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.",
3028
3349
  "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.",
3029
- "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, return IMPLEMENTATION_OUTCOME: blocked instead of reopening broad discovery.",
3350
+ "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.",
3351
+ "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.",
3352
+ "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.",
3030
3353
  "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.",
3031
- "frontend-prewrite-gate-shell confirmed the effective plan/review, requirement coverage, Mock policy, and contract. Stay within writeSet and preserve unrelated files.",
3354
+ "frontend-design-policy-shell materialized and validated the canonical contract; frontend-writer-admission-shell authorized the writeSet. Stay within writeSet and preserve unrelated files.",
3032
3355
  "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.",
3033
3356
  "Do not write root artifacts/** unless explicitly included in writeSet. Do not claim Browser/visual verification.",
3034
3357
  "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.",
@@ -3043,7 +3366,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3043
3366
  .join("\n\n"),
3044
3367
  },
3045
3368
  {
3046
- id: "frontend-verify-assess-shell",
3369
+ id: "frontend-verify-shell",
3047
3370
  depends_on: [implementId],
3048
3371
  role: "verifier",
3049
3372
  executor: "shell",
@@ -3051,8 +3374,8 @@ async function buildFrontendHybridDagFromTask(sources) {
3051
3374
  writePolicy: "read-only",
3052
3375
  allowedPaths: readOnlyPaths,
3053
3376
  forbiddenPaths,
3054
- outputContract: "Run frozen Mock/static/behavior commands, materialize verification trace and repair assessment, and fail closed for non-repairable failures.",
3055
- subtask_prompt: "Execute the frontend verification bundle. Preserve per-command evidence; eligible repairable failures select the bounded repair branch.",
3377
+ 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.",
3378
+ subtask_prompt: "Execute the frontend verification bundle. Preserve per-command evidence; a failure fails this node (terminal) and routes to recovery.",
3056
3379
  shell: {
3057
3380
  commands: [],
3058
3381
  frontendVerificationBundle: {
@@ -3066,7 +3389,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3066
3389
  staticEvidence: staticVerifyEvidence,
3067
3390
  behaviorEvidence: behaviorVerifyEvidence,
3068
3391
  lintBaselineNodeId: lintShellCommands.length > 0
3069
- ? "frontend-lint-baseline-shell"
3392
+ ? "frontend-writer-admission-shell"
3070
3393
  : undefined,
3071
3394
  writerNodeIds: lintShellCommands.length > 0 ? [implementId] : [],
3072
3395
  mode: "initial",
@@ -3075,85 +3398,17 @@ async function buildFrontendHybridDagFromTask(sources) {
3075
3398
  timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
3076
3399
  },
3077
3400
  },
3078
- {
3079
- id: "frontend-repair-pi",
3080
- depends_on: ["frontend-verify-assess-shell", implementId],
3081
- runIf: "$.nodes['frontend-verify-assess-shell'].json.eligible == true",
3082
- ...buildFrontendWriterNodeDefaults({
3083
- complexity: resolveWriterComplexity(taskConfig),
3084
- writeSet: implementPaths.writeSet,
3085
- allowedPaths: implementPaths.allowedPaths,
3086
- forbiddenPaths,
3087
- }),
3088
- 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.",
3089
- subtask_prompt: [
3090
- "Read contracts/frontend-repair-assessment.json and the validated frontend implementation contract.",
3091
- "This node runs only for eligible=true. Apply the smallest fix for the classified repairable failure inside the original implement writeSet only.",
3092
- "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.",
3093
- "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.",
3094
- "Do not re-plan requirements or expand allowed paths. Browser/visual remain not-run.",
3095
- ...(implementPaths.docIndexCompanions.length > 0
3096
- ? [
3097
- `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.`,
3098
- ]
3099
- : []),
3100
- writerDeliveryContract(taskConfig),
3101
- ]
3102
- .filter((value) => Boolean(value))
3103
- .join("\n\n"),
3104
- },
3105
- {
3106
- id: "frontend-reverify-shell",
3107
- depends_on: ["frontend-repair-pi"],
3108
- role: "verifier",
3109
- executor: "shell",
3110
- complexity: "LOW",
3111
- writePolicy: "read-only",
3112
- allowedPaths: readOnlyPaths,
3113
- forbiddenPaths,
3114
- outputContract: "Post-repair Mock/static/behavior re-verification plus refreshed canonical trace; any failure blocks review.",
3115
- subtask_prompt: "Re-run the frozen frontend verification bundle after bounded repair and fail on any command or trace failure.",
3116
- shell: {
3117
- commands: [],
3118
- frontendVerificationBundle: {
3119
- schemaVersion: 1,
3120
- mockCommands: mockShellCommands,
3121
- lintCommands: lintShellCommands,
3122
- staticCommands: staticShellCommands,
3123
- behaviorCommands: behaviorShellCommands,
3124
- mockEvidence: mockVerifyEvidence,
3125
- lintEvidence: lintVerifyEvidence,
3126
- staticEvidence: staticVerifyEvidence,
3127
- behaviorEvidence: behaviorVerifyEvidence,
3128
- lintBaselineNodeId: lintShellCommands.length > 0
3129
- ? "frontend-lint-baseline-shell"
3130
- : undefined,
3131
- writerNodeIds: lintShellCommands.length > 0
3132
- ? [implementId, "frontend-repair-pi"]
3133
- : [],
3134
- mode: "repair",
3135
- },
3136
- cwd: ".",
3137
- timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
3138
- },
3139
- },
3140
3401
  {
3141
3402
  id: "frontend-review-context-shell",
3142
- depends_on: [
3143
- "frontend-reverify-shell",
3144
- "frontend-repair-pi",
3145
- "frontend-verify-assess-shell",
3146
- implementId,
3147
- ],
3148
- dependsPolicy: "all-or-condition-skip",
3403
+ depends_on: ["frontend-verify-shell", implementId],
3149
3404
  role: "verifier",
3150
3405
  executor: "shell",
3151
3406
  complexity: "LOW",
3152
3407
  writePolicy: "read-only",
3153
3408
  allowedPaths: readOnlyPaths,
3154
3409
  forbiddenPaths,
3155
- outputContract: "Canonical frontend review context containing validated contract, lint assessment when configured, effective verification trace, repair assessment, and actual worktree diff.",
3156
- subtask_prompt: "Capture the actual diff and bind it to the effective initial-or-post-repair verification evidence for final review.",
3410
+ outputContract: "Canonical frontend review context containing validated contract, lint assessment when configured, effective verification trace, optional verify-failure facts, and actual worktree diff.",
3411
+ subtask_prompt: "Capture the actual diff and bind it to the effective verification evidence for final review.",
3157
3412
  shell: {
3158
3413
  commands: [],
3159
3414
  frontendReviewContext: { schemaVersion: 1, requireBaseline: true },
@@ -3168,79 +3423,70 @@ async function buildFrontendHybridDagFromTask(sources) {
3168
3423
  executor: "pi",
3169
3424
  complexity: "HIGH",
3170
3425
  writePolicy: "read-only",
3426
+ readBudget: FRONTEND_READ_BUDGETS.finalReview,
3427
+ retryPolicy: FRONTEND_REVIEW_TERMINAL_RETRY_POLICY,
3171
3428
  allowedPaths: readOnlyPaths,
3172
3429
  forbiddenPaths,
3173
3430
  skills: FRONTEND_REVIEW_SKILLS,
3174
- outputContract: 'Structured JSON review verdict only: {"schemaVersion":1,"verdict":"pass|request-revision","findings":[...],"verificationAssessment":"...","uxAssessment":"...","residualRisks":[...]}. No file writes.',
3175
- outputProtocol: REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL,
3431
+ 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.',
3176
3432
  subtask_prompt: [
3177
3433
  "Review the frontend implementation and verification evidence.",
3178
- "Return exactly one final JSON object in this response. Do not repeat it, do not emit a second revision, do not wrap it in Markdown, and do not include prose outside the JSON.",
3179
- 'Required fields: schemaVersion: 1; verdict: "pass" or "request-revision"; findings: array of objects with severity ("Critical" | "Important" | "Minor" | "Info"), optional file, optional positive integer line, issue, and optional requiredChange.',
3180
- 'verdict "request-revision" requires at least one finding. verdict "pass" is invalid if any finding severity is Critical or Important.',
3181
- 'Any Critical or Important finding must force verdict "request-revision".',
3182
- "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.",
3434
+ "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.",
3435
+ "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.",
3436
+ "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.",
3437
+ "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.",
3183
3438
  "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.",
3184
3439
  "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).",
3185
- "The contract embedded in frontend-review-context.json is the effective reviewed plan materialized by the prewrite gate. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, 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.",
3440
+ "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.",
3186
3441
  "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.",
3187
- "Inspect the frontend-verify-assess-shell or selected frontend-reverify-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.",
3442
+ "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.",
3188
3443
  "Distinguish Mock-backed evidence from real API integration evidence and preserve the Real Integration Gap when the backend was not exercised.",
3189
3444
  "Review implementation quality, behavior/state coverage, verification evidence, and maintainability. Read-only: do not modify files.",
3190
3445
  ].join("\n\n"),
3191
3446
  },
3192
3447
  {
3193
- id: "frontend-review-gate-shell",
3194
- depends_on: ["frontend-review-pi"],
3195
- role: "verifier",
3448
+ id: "frontend-closeout-shell",
3449
+ depends_on: ["frontend-review-context-shell", "frontend-review-pi"],
3450
+ role: "closeout",
3196
3451
  executor: "shell",
3197
3452
  complexity: "LOW",
3198
3453
  writePolicy: "read-only",
3199
3454
  allowedPaths: readOnlyPaths,
3200
3455
  forbiddenPaths,
3201
- outputContract: 'Deterministic frontend review verdict gate: exit 0 only when frontend-review-pi emits JSON verdict "pass".',
3202
- subtask_prompt: 'Deterministic gate: block downstream closeout unless frontend-review-pi emitted JSON verdict "pass".',
3456
+ 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.",
3457
+ subtask_prompt: "Render the closeout deterministically from frontend-review-context.json, the verification trace, and the committed typed review terminal fact.",
3203
3458
  shell: {
3204
3459
  commands: [],
3205
- verdictGate: {
3206
- fromNodeId: "frontend-review-pi",
3207
- accept: ["pass"],
3208
- routingAccept: ["request-revision"],
3209
- label: "frontend review",
3210
- source: "json-review-verdict",
3460
+ frontendCloseout: {
3461
+ schemaVersion: 1,
3462
+ reviewFromNodeId: "frontend-review-pi",
3211
3463
  },
3212
3464
  cwd: ".",
3213
3465
  timeoutMs: 60000,
3214
3466
  },
3215
3467
  },
3216
- {
3217
- id: "frontend-closeout-pi",
3218
- depends_on: [
3219
- "frontend-review-context-shell",
3220
- "frontend-review-gate-shell",
3221
- ],
3222
- role: "closeout",
3223
- executor: "pi",
3224
- complexity: "MED",
3225
- writePolicy: "read-only",
3226
- allowedPaths: taskConfig.allowedPaths.length > 0
3227
- ? [...taskConfig.allowedPaths, "docs/**"]
3228
- : ["**", "docs/**"],
3229
- forbiddenPaths,
3230
- skills: FRONTEND_VERIFICATION_SKILLS,
3231
- 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.",
3232
- subtask_prompt: [
3233
- "Return a frontend closeout summary covering Mock decision/strategy/files/verification/production boundary, changes, verification evidence, review result, known risks, and follow-up.",
3234
- "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.",
3235
- "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.",
3236
- `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.`,
3237
- `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.`,
3238
- "Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
3239
- ].join("\n\n"),
3240
- },
3241
3468
  ],
3242
3469
  };
3243
- spec.tasks = pruneFrontendTasksForRisk(spec.tasks, frontendRisk);
3470
+ if (frontendTaskShape.shape === "split-required") {
3471
+ spec.tasks = pruneFrontendTasksForSplitRequired(spec.tasks);
3472
+ }
3473
+ else {
3474
+ const reshapeRaisedTopology = frontendTaskShape.signals.includes("shape-transition-reshape");
3475
+ if (!reshapeRaisedTopology) {
3476
+ spec.tasks = pruneFrontendTasksForRisk(spec.tasks, frontendRisk);
3477
+ }
3478
+ if (frontendTaskShape.shape === "micro") {
3479
+ spec.tasks = pruneFrontendTasksForMicro(spec.tasks);
3480
+ }
3481
+ }
3482
+ spec.advisories = [
3483
+ ...(spec.advisories ?? []),
3484
+ `frontend-task-shape: ${frontendTaskShape.shape} (${frontendTaskShape.reason})`,
3485
+ ];
3486
+ // Freeze the frontend recovery continuation quota from task.json so the
3487
+ // runner can bound M6 auto-recovery without re-reading the task config.
3488
+ const frontendMaxContinuations = sources.taskConfig.frontendRecovery?.maxContinuations ?? 1;
3489
+ spec.frontendRecovery = { maxContinuations: frontendMaxContinuations };
3244
3490
  applyDefaultReadOnlyRetryPolicy(spec);
3245
3491
  parseDagSpec(spec);
3246
3492
  assertValidDagSpec(spec);
@@ -4301,6 +4547,7 @@ async function buildBackendTestGapFillDag(sources) {
4301
4547
  ],
4302
4548
  };
4303
4549
  spec.backendTestLayout = layout;
4550
+ applyBackendTestWorkspaceControl(spec, taskConfig.backendTest?.workspaceControl ?? "git");
4304
4551
  if (sharedSetup) {
4305
4552
  spec.backendTestSharedSetup = { ...sharedSetup };
4306
4553
  }
@@ -4757,6 +5004,7 @@ async function buildBackendTestHybridDag(sources) {
4757
5004
  // against the resolved layout. The default layout is identity, so the
4758
5005
  // packaged template stays byte-identical with the historical contract.
4759
5006
  applyBackendTestLayoutToDagSpec(spec, layout);
5007
+ applyBackendTestWorkspaceControl(spec, taskConfig.backendTest?.workspaceControl ?? "git");
4760
5008
  // Plan B: carry the bound shared-setup document on the spec so runtime
4761
5009
  // validators (N6 markdown case validation) see the same binding as prompts.
4762
5010
  if (intake.sharedSetup) {
@@ -4767,6 +5015,23 @@ async function buildBackendTestHybridDag(sources) {
4767
5015
  assertValidDagSpec(spec);
4768
5016
  return spec;
4769
5017
  }
5018
+ function applyBackendTestWorkspaceControl(spec, control) {
5019
+ if (control !== "filesystem-only") {
5020
+ delete spec.backendTestWorkspaceControl;
5021
+ return;
5022
+ }
5023
+ spec.backendTestWorkspaceControl = control;
5024
+ for (const task of spec.tasks) {
5025
+ if ((task.executor === "pi" && task.toolProfile === "write") ||
5026
+ task.shell?.backendTestPipeline) {
5027
+ task.writeGuardPolicy = "filesystem-only";
5028
+ }
5029
+ const child = task.dynamicExpansion?.childTask;
5030
+ if (child?.executor === "pi" && child.toolProfile === "write") {
5031
+ child.writeGuardPolicy = "filesystem-only";
5032
+ }
5033
+ }
5034
+ }
4770
5035
  /** Rewrite layout-dependent strings across a compiled backend-test DAG spec (plan A). */
4771
5036
  function applyBackendTestLayoutToDagSpec(spec, layout) {
4772
5037
  if (layout.isDefault) {
@@ -4809,11 +5074,57 @@ function applyBackendTestLayoutToDagSpec(spec, layout) {
4809
5074
  spec.successCriteria = spec.successCriteria?.map(rewrite);
4810
5075
  spec.backendTestLayout = layout;
4811
5076
  }
5077
+ /** Rewrite layout-dependent strings across a compiled frontend-test DAG spec. */
5078
+ function applyFrontendTestLayoutToDagSpec(spec, layout) {
5079
+ if (layout.isDefault) {
5080
+ spec.frontendTestLayout = layout;
5081
+ return;
5082
+ }
5083
+ const rewrite = (value) => applyFrontendTestLayoutToText(value, layout);
5084
+ for (const task of spec.tasks) {
5085
+ task.subtask_prompt = rewrite(task.subtask_prompt);
5086
+ if (task.outputContract)
5087
+ task.outputContract = rewrite(task.outputContract);
5088
+ task.writeSet = task.writeSet?.map(rewrite);
5089
+ task.allowedPaths = task.allowedPaths?.map(rewrite);
5090
+ task.forbiddenPaths = task.forbiddenPaths?.map(rewrite);
5091
+ if (task.dynamicExpansion?.childTask) {
5092
+ const child = task.dynamicExpansion.childTask;
5093
+ if (typeof child.subtaskPromptTemplate === "string") {
5094
+ child.subtaskPromptTemplate = rewrite(child.subtaskPromptTemplate);
5095
+ }
5096
+ if (typeof child.outputContract === "string") {
5097
+ child.outputContract = rewrite(child.outputContract);
5098
+ }
5099
+ if (Array.isArray(child.allowedPaths)) {
5100
+ child.allowedPaths = child.allowedPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
5101
+ }
5102
+ if (Array.isArray(child.writeSet)) {
5103
+ child.writeSet = child.writeSet.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
5104
+ }
5105
+ if (Array.isArray(child.forbiddenPaths)) {
5106
+ child.forbiddenPaths = child.forbiddenPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
5107
+ }
5108
+ }
5109
+ if (task.shell?.commands) {
5110
+ task.shell.commands = task.shell.commands.map(rewrite);
5111
+ }
5112
+ }
5113
+ spec.globalConstraints = spec.globalConstraints?.map(rewrite);
5114
+ spec.successCriteria = spec.successCriteria?.map(rewrite);
5115
+ spec.frontendTestLayout = layout;
5116
+ }
5117
+ function allowedPathsCoverFrontendTestRoot(allowedPaths, testRoot) {
5118
+ const probe = `${testRoot}/_layout_probe_`;
5119
+ return allowedPaths.some((pattern) => pathMatchesPattern(testRoot, pattern) ||
5120
+ pathMatchesPattern(probe, pattern));
5121
+ }
4812
5122
  // ---------------------------------------------------------------------------
4813
5123
  // Frontend browser-test RAG DAG template
4814
5124
  // ---------------------------------------------------------------------------
4815
5125
  function buildFrontendTestHybridDag(sources) {
4816
5126
  const rawFrontendTest = sources.taskConfig.frontendTest;
5127
+ const layout = resolveFrontendTestLayout(rawFrontendTest);
4817
5128
  const config = {
4818
5129
  // Default 32: common FE suites cover ~24 AC with multi-dimension cases; 20 caused map maxExpandedNodes failures.
4819
5130
  maxCasesPerBatch: rawFrontendTest?.maxCasesPerBatch ?? 32,
@@ -4835,28 +5146,32 @@ function buildFrontendTestHybridDag(sources) {
4835
5146
  };
4836
5147
  const declaredRequirementIds = buildDagSourceBinding(sources).requirementIds;
4837
5148
  const declaredAcIds = declaredRequirementIds.filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
5149
+ // Optional project-local UI anchor ledger: only mention it in prompts when it
5150
+ // exists so ledger-less projects keep generating without noise.
5151
+ const hasUiAnchorsLedger = Boolean(sources.repoRoot &&
5152
+ existsSync(path.join(sources.repoRoot, layout.ragDir, "ui-anchors.md")));
5153
+ const uiAnchorsLedgerInstruction = hasUiAnchorsLedger
5154
+ ? `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.`
5155
+ : "";
4838
5156
  const reviewMode = config.reviewMode;
4839
5157
  const blockingReview = reviewMode === "blocking";
4840
5158
  const strictOutcomeGate = config.strictOutcomeGate;
4841
5159
  const maxRerunAttempts = config.maxRerunAttempts;
4842
5160
  const enableRetrospect = config.reports?.retrospect === true;
4843
5161
  const enableL5Report = config.reports?.l5 !== false;
4844
- const hasFrontendTestWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "testcase/frontend/**" ||
4845
- pattern === "testcase/**" ||
4846
- pattern === "**");
4847
- if (!hasFrontendTestWriteScope) {
4848
- throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
5162
+ if (!allowedPathsCoverFrontendTestRoot(sources.taskConfig.allowedPaths, layout.testRoot)) {
5163
+ throw new Error(`frontend-test requires task.json allowedPaths to cover "${layout.testRoot}/**" (or an explicit containing glob).`);
4849
5164
  }
4850
5165
  const forbidden = commonForbiddenPaths(sources);
4851
- const ragWriteSet = ["testcase/frontend/rag/**"];
5166
+ const ragWriteSet = [`${layout.ragDir}/**`];
4852
5167
  const caseDraftWriteSet = [
4853
- "testcase/frontend/cases/FE-*.md",
4854
- "testcase/frontend/cases/index.md",
4855
- "testcase/frontend/cases/manifest.draft.json",
5168
+ `${layout.casesDir}/FE-*.md`,
5169
+ `${layout.casesDir}/index.md`,
5170
+ `${layout.casesDir}/manifest.draft.json`,
4856
5171
  ];
4857
5172
  // The shell materializer alone owns the final manifest boundary.
4858
- const casesWriteSet = ["testcase/frontend/cases/**"];
4859
- const evidenceRoot = "testcase/frontend/evidence";
5173
+ const casesWriteSet = [`${layout.casesDir}/**`];
5174
+ const evidenceRoot = layout.evidenceDir;
4860
5175
  const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
4861
5176
  const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
4862
5177
  const checklistScript = [
@@ -4875,6 +5190,8 @@ function buildFrontendTestHybridDag(sources) {
4875
5190
  "const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
4876
5191
  ,
4877
5192
  "const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
5193
+ "const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
5194
+ "const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
4878
5195
  "for(const c of manifest.cases){",
4879
5196
  " const id=c&&c.caseId||'?';",
4880
5197
  " 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-*'});",
@@ -4893,6 +5210,7 @@ function buildFrontendTestHybridDag(sources) {
4893
5210
  " if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
4894
5211
  " if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
4895
5212
  " if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
5213
+ " 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;}",
4896
5214
  " if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
4897
5215
  " }",
4898
5216
  " }",
@@ -4924,6 +5242,8 @@ function buildFrontendTestHybridDag(sources) {
4924
5242
  `const declaredAc=new Set(${declaredAcIdsLiteral});`,
4925
5243
  "const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
4926
5244
  "const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
5245
+ "const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
5246
+ "const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
4927
5247
  "for(const c of manifest.cases){",
4928
5248
  " 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));",
4929
5249
  " if(/^AC-/i.test(c.caseId)) fail('case-id-is-ac','caseId must not be an acceptance id: '+c.caseId);",
@@ -4931,7 +5251,7 @@ function buildFrontendTestHybridDag(sources) {
4931
5251
  " seen.add(c.caseId);",
4932
5252
  " if(typeof c.dimension!=='string'||!dims.has(c.dimension)) fail('invalid-dimension',String(c.dimension));",
4933
5253
  " 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);",
4934
- " 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'); }",
5254
+ " 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'); }",
4935
5255
  " c.casePath='testcase/frontend/cases/'+c.caseId+'.md';",
4936
5256
  " c.evidenceDir='testcase/frontend/evidence/'+c.caseId+'/';",
4937
5257
  " 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)); }",
@@ -4991,15 +5311,11 @@ function buildFrontendTestHybridDag(sources) {
4991
5311
  writeSet: ragWriteSet,
4992
5312
  allowedPaths: [...ragWriteSet],
4993
5313
  forbiddenPaths: forbidden,
4994
- outputContract: "Write testcase/frontend/rag/standard-scenarios.v1.json for generate-time standard scenario coverage.",
4995
- subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package.",
5314
+ 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.",
5315
+ 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.",
4996
5316
  shell: {
4997
- commands: [
4998
- [
4999
- "node -e",
5000
- 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}));}"),
5001
- ].join(" "),
5002
- ],
5317
+ commands: [],
5318
+ frontendTestStandardScenarios: {},
5003
5319
  cwd: ".",
5004
5320
  timeoutMs: 60_000,
5005
5321
  },
@@ -5035,15 +5351,11 @@ function buildFrontendTestHybridDag(sources) {
5035
5351
  writeSet: ragWriteSet,
5036
5352
  allowedPaths: [...ragWriteSet],
5037
5353
  forbiddenPaths: forbidden,
5038
- outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable (node ERROR so generate/map do not run).",
5039
- 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/connection refused/TLS => blockedReason frontend-base-url-unreachable. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.",
5354
+ 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.",
5355
+ 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.",
5040
5356
  shell: {
5041
- commands: [
5042
- [
5043
- "node -e",
5044
- 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);`),
5045
- ].join(" "),
5046
- ],
5357
+ commands: [],
5358
+ frontendTestEnvironmentProbe: {},
5047
5359
  cwd: ".",
5048
5360
  timeoutMs: 60000,
5049
5361
  },
@@ -5064,6 +5376,7 @@ function buildFrontendTestHybridDag(sources) {
5064
5376
  subtask_prompt: [
5065
5377
  "Use skill playwright-cli-case-generator.",
5066
5378
  "Read testcase/frontend/rag/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.",
5379
+ ...(uiAnchorsLedgerInstruction ? [uiAnchorsLedgerInstruction] : []),
5067
5380
  "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.",
5068
5381
  "Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
5069
5382
  "HARD ID CONTRACT (do not confuse these):",
@@ -5223,7 +5536,7 @@ function buildFrontendTestHybridDag(sources) {
5223
5536
  subtaskPromptTemplate: [
5224
5537
  "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.",
5225
5538
  "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?}).",
5226
- "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.",
5539
+ "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.",
5227
5540
  "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).",
5228
5541
  "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}.",
5229
5542
  ].join("\n\n"),
@@ -5246,7 +5559,7 @@ function buildFrontendTestHybridDag(sources) {
5246
5559
  "const manifestPath='testcase/frontend/cases/manifest.json';",
5247
5560
  "if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
5248
5561
  "const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
5249
- "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'});}}",
5562
+ "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'});}}",
5250
5563
  `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}));`,
5251
5564
  ].join("");
5252
5565
  tasks.push({
@@ -5314,6 +5627,7 @@ function buildFrontendTestHybridDag(sources) {
5314
5627
  "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.",
5315
5628
  "Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
5316
5629
  "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.",
5630
+ "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.",
5317
5631
  "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.",
5318
5632
  ].join("\n\n"),
5319
5633
  },
@@ -5449,6 +5763,7 @@ function buildFrontendTestHybridDag(sources) {
5449
5763
  verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
5450
5764
  tasks,
5451
5765
  };
5766
+ applyFrontendTestLayoutToDagSpec(spec, layout);
5452
5767
  applyDefaultReadOnlyRetryPolicy(spec);
5453
5768
  parseDagSpec(spec);
5454
5769
  assertValidDagSpec(spec);
@@ -6511,15 +6826,27 @@ async function buildHybridDagForTemplate(sources, template, options = {}) {
6511
6826
  spec = await buildSupervisedHybridDag(standard, sources);
6512
6827
  }
6513
6828
  applyProjectGovernanceReview(spec, template, sources);
6829
+ // Backend implementation templates only (D1 outer gate): assign the two
6830
+ // Pi extension buckets after all template-specific nodes exist.
6831
+ if (template === "standard-dag" ||
6832
+ template === "review-gated-dag" ||
6833
+ template === "supervised-implementation") {
6834
+ applyBackendPiExtensionBuckets(spec);
6835
+ }
6514
6836
  // New generate path always emits DagSpec v4 + bindings.
6515
6837
  spec.version = 4;
6516
6838
  if (!spec.runtimeContract) {
6517
6839
  spec.runtimeContract = GENERATED_DAG_RUNTIME_CONTRACT;
6518
6840
  }
6519
- spec.sourceBinding = buildDagSourceBinding(sources);
6841
+ spec.sourceBinding = buildDagSourceBinding(sources, template === "frontend-implementation" ? "frontend-implementation" : undefined);
6520
6842
  spec.taskContractBinding = taskContractBinding;
6521
6843
  assertNoGovernanceFlagOnDisallowedTemplate(spec, template);
6522
- parseDagSpec(spec);
6844
+ if (template === "standard-dag" ||
6845
+ template === "review-gated-dag" ||
6846
+ template === "supervised-implementation") {
6847
+ stampTargetTemplateTransientRetryProfile(spec);
6848
+ }
6849
+ spec = parseDagSpec(spec);
6523
6850
  assertValidDagSpec(spec);
6524
6851
  return spec;
6525
6852
  }
@@ -6570,6 +6897,36 @@ export async function buildHybridDagFromTask(sources, options = {}) {
6570
6897
  function cloneTask(task, patch = {}) {
6571
6898
  return { ...task, ...patch };
6572
6899
  }
6900
+ /**
6901
+ * Backend-implementation Pi extension buckets (plan 2026-08-21 D3):
6902
+ * - read side: every non-writer, non-closeout Pi node gets navigation
6903
+ * extensions (pi-codegraph + pi-lens);
6904
+ * - write side: bounded writers get only pi-codegraph (pi-lens registers
6905
+ * ast_grep_replace, which can bypass the writeSet — not loading it is
6906
+ * simpler and safer than filtering after load);
6907
+ * - everything else (closeout, shell, static, other templates) stays closed.
6908
+ * Idempotent: never overwrites an explicit piExtensions a task declares.
6909
+ */
6910
+ function applyBackendPiExtensionBuckets(spec) {
6911
+ for (const task of spec.tasks) {
6912
+ if (task.piExtensions !== undefined)
6913
+ continue;
6914
+ if (task.executor !== "pi")
6915
+ continue;
6916
+ if (task.id === "closeout-pi")
6917
+ continue;
6918
+ task.piExtensions =
6919
+ task.toolProfile === "write" ? ["pi-codegraph"] : ["pi-codegraph", "pi-lens"];
6920
+ }
6921
+ }
6922
+ function stampTargetTemplateTransientRetryProfile(spec) {
6923
+ for (const task of spec.tasks) {
6924
+ if (isTargetTemplateImplementPi(task) ||
6925
+ isCanonicalFinalVerifyShellRetryCandidate(task)) {
6926
+ task.transientRetryProfile = TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE;
6927
+ }
6928
+ }
6929
+ }
6573
6930
  /**
6574
6931
  * Apply default Pi retry policies to generated DAG nodes:
6575
6932
  * - safe read-only planner/scout/reviewer/verifier/supervisor/closeout
@@ -6590,6 +6947,10 @@ function applyDefaultReadOnlyRetryPolicy(spec) {
6590
6947
  : DEFAULT_READ_ONLY_PI_RETRY_POLICY;
6591
6948
  continue;
6592
6949
  }
6950
+ if (isTargetTemplateImplementPi(task)) {
6951
+ task.retryPolicy = TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY;
6952
+ continue;
6953
+ }
6593
6954
  if (isWriterTransportRetryCandidate(task)) {
6594
6955
  task.retryPolicy = WRITER_TRANSPORT_RETRY_POLICY;
6595
6956
  }
@@ -6850,6 +7211,7 @@ function buildReviewGatedHybridDag(standard, sources) {
6850
7211
  }));
6851
7212
  spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewVerdictRecoveryNode(sources), buildReviewGateNode(sources));
6852
7213
  applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
7214
+ stampTargetTemplateTransientRetryProfile(spec);
6853
7215
  applyDefaultReadOnlyRetryPolicy(spec);
6854
7216
  parseDagSpec(spec);
6855
7217
  assertValidDagSpec(spec);
@@ -7181,6 +7543,9 @@ async function buildHardVerifyNode(sources) {
7181
7543
  forbiddenPaths: commonForbiddenPaths(sources),
7182
7544
  outputContract: "Archived hard verification stdout/stderr with exit codes; no worktree writes.",
7183
7545
  subtask_prompt: "Run hard verification after repair round.",
7546
+ transientRetryProfile: sources.verifyCommands && commands.length > 0
7547
+ ? TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE
7548
+ : undefined,
7184
7549
  shell: {
7185
7550
  commands,
7186
7551
  verifyEvidence: buildVerifyEvidence({
@@ -7351,6 +7716,7 @@ async function buildSupervisedHybridDag(standard, sources) {
7351
7716
  ],
7352
7717
  };
7353
7718
  applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
7719
+ stampTargetTemplateTransientRetryProfile(spec);
7354
7720
  applyDefaultReadOnlyRetryPolicy(spec);
7355
7721
  parseDagSpec(spec);
7356
7722
  assertValidDagSpec(spec);