@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
@@ -8,6 +8,15 @@ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-sto
8
8
  import { findPackageRoot } from "../../shared/package-metadata.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
10
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
11
+ import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
12
+ // Cycle-safe value imports: the check functions are hoisted declarations used
13
+ // only at call time, so the contract module and the policy module can depend
14
+ // on each other without module-init ordering issues.
15
+ import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
16
+ import { isDagSourceBindingV2 } from "./types.js";
17
+ import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
18
+ import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
19
+ import { isConfigurationVerificationFile, verificationSymbolMatchesContent, } from "./frontend-verification-trace.js";
11
20
  export const frontendNormalizationActionSchema = z.enum([
12
21
  "remove-trailing-commas",
13
22
  "strip-comments",
@@ -41,9 +50,6 @@ export function serializeDeterministicJson(value) {
41
50
  export function deterministicSha256(value) {
42
51
  return sha256Hex(serializeDeterministicJson(value));
43
52
  }
44
- /** Provider-safe limits for the two planner outputs. Larger work must split. */
45
- export const FRONTEND_PLAN_RESPONSE_MAX_BYTES = 14 * 1024;
46
- export const FRONTEND_CANONICAL_CONTRACT_MAX_BYTES = 16 * 1024;
47
53
  /** A contract materialization failure carries its classification plus the
48
54
  * audit context collected up to the failure point, so the prewrite gate can
49
55
  * persist a complete frontend-prewrite-result-v1 even when the canonical
@@ -105,6 +111,68 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
105
111
  }
106
112
  return [...labels];
107
113
  }
114
+ /**
115
+ * Extract the design-policy allowedDependencies set from the DAG run spec
116
+ * (run.json), mirroring `deriveFrozenCommandLabelsFromRun`. Used by the plan
117
+ * node's front-loaded policy pre-check so dependency declarations can be
118
+ * corrected through the retry ladder before the policy shell (the authority)
119
+ * re-runs the same check on committed facts.
120
+ */
121
+ async function deriveAllowedDependenciesFromRun(runDir) {
122
+ const specPath = path.join(runDir, "run.json");
123
+ let raw;
124
+ try {
125
+ raw = await readFile(specPath, "utf8");
126
+ }
127
+ catch {
128
+ return [];
129
+ }
130
+ let spec;
131
+ try {
132
+ spec = JSON.parse(raw);
133
+ }
134
+ catch {
135
+ return [];
136
+ }
137
+ const deps = new Set();
138
+ for (const task of spec.tasks ?? []) {
139
+ const allowed = task.shell?.frontendDesignPolicy?.allowedDependencies;
140
+ if (allowed) {
141
+ for (const dep of allowed)
142
+ deps.add(dep);
143
+ }
144
+ }
145
+ return [...deps];
146
+ }
147
+ /** Read the frozen implementation writeSet patterns from run.json so the plan
148
+ * validator can front-load checkTargetPaths (write-set-too-large) before the
149
+ * design-policy shell terminates the run. */
150
+ async function deriveWriteSetFromRun(runDir) {
151
+ const specPath = path.join(runDir, "run.json");
152
+ let raw;
153
+ try {
154
+ raw = await readFile(specPath, "utf8");
155
+ }
156
+ catch {
157
+ return [];
158
+ }
159
+ let spec;
160
+ try {
161
+ spec = JSON.parse(raw);
162
+ }
163
+ catch {
164
+ return [];
165
+ }
166
+ const writeSet = new Set();
167
+ for (const task of spec.tasks ?? []) {
168
+ const patterns = task.shell?.frontendDesignPolicy?.implementationWriteSet;
169
+ if (patterns) {
170
+ for (const pattern of patterns)
171
+ writeSet.add(pattern);
172
+ }
173
+ }
174
+ return [...writeSet];
175
+ }
108
176
  export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
109
177
  export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
110
178
  /**
@@ -172,7 +240,6 @@ export function loadFrontendImplementationContractJsonSchema(startDir = path.dir
172
240
  "mockApi",
173
241
  "designEvidence",
174
242
  "verificationTargets",
175
- "evidenceGaps",
176
243
  ];
177
244
  const actualRequired = Array.isArray(schema.required) ? schema.required : [];
178
245
  const missing = expectedRequired.filter((key) => !actualRequired.includes(key));
@@ -221,13 +288,13 @@ const gap = z
221
288
  * `specReference` is required (path/section/line hit).
222
289
  * - `reuse-existing`: reuse an existing repo component/convention not named by
223
290
  * the spec → `specReference` may be null, rationale explains the basis.
224
- * - `new`: neither spec nor existing code fits → `specReference` must be null,
225
- * rationale must declare the deviation (design-review approves it).
291
+ * - `new`: the task source mandates a component absent from the repository →
292
+ * `specReference` is required and points at that task-source declaration;
293
+ * rationale explains the bounded addition.
226
294
  *
227
- * `specReference` is nullable + optional because `stripNullValuesDeep` strips
228
- * null object values before validation, so a `new` choice's `specReference:
229
- * null` becomes an absent field; the superRefine below treats absent and null
230
- * identically.
295
+ * `specReference` is nullable + optional at the wire boundary because
296
+ * `stripNullValuesDeep` removes model-emitted nulls before validation. The
297
+ * decision-specific requirements are enforced by the superRefine below.
231
298
  */
232
299
  const uiComponentSpecReferenceSchema = z
233
300
  .object({
@@ -242,7 +309,11 @@ const uiComponentChoiceSchema = z
242
309
  component: z.string().min(1),
243
310
  decision: z.enum(["specified", "reuse-existing", "new"]),
244
311
  specReference: uiComponentSpecReferenceSchema.nullable().optional(),
245
- rationale: z.string().min(1),
312
+ // Optional: the decision enum already carries the semantic (specified →
313
+ // specReference, new → task-source traceability, reuse-existing → no
314
+ // rationale needed).
315
+ // Omit for reuse-existing to keep plan output small.
316
+ rationale: z.string().min(1).optional(),
246
317
  })
247
318
  .strict();
248
319
  const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
@@ -271,7 +342,12 @@ function stripNullValuesDeep(value) {
271
342
  return value;
272
343
  }
273
344
  export const frontendImplementationContractSchema = z
274
- .preprocess(stripNullValuesDeep, z
345
+ .preprocess((value) => {
346
+ const record = asRecord(value);
347
+ if (!record)
348
+ return stripNullValuesDeep(value);
349
+ return stripNullValuesDeep(record);
350
+ }, z
275
351
  .object({
276
352
  schemaVersion: z.literal(1),
277
353
  sourceBinding: z
@@ -281,8 +357,33 @@ export const frontendImplementationContractSchema = z
281
357
  requirementSha256: z.string().regex(/^[a-f0-9]{64}$/),
282
358
  referencePaths: z.array(safePath),
283
359
  requirementIds: z.array(id),
360
+ // Source binding v2 (frontend): bound ledger pointer + fragment map.
361
+ ledgerPath: safePath.optional(),
362
+ ledgerSha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
363
+ inputDigest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
364
+ requirementToFragments: z
365
+ .record(z.string(), z.array(z.string()))
366
+ .optional(),
284
367
  })
285
- .strict(),
368
+ .strict()
369
+ .superRefine((value, ctx) => {
370
+ // v2 all-or-none:任一 ledger 字段出现则四项必须齐全,防止 v1+optional 冒充 v2。
371
+ const anyLedgerField = value.ledgerPath !== undefined ||
372
+ value.ledgerSha256 !== undefined ||
373
+ value.inputDigest !== undefined ||
374
+ value.requirementToFragments !== undefined;
375
+ const allLedgerFields = value.ledgerPath !== undefined &&
376
+ value.ledgerSha256 !== undefined &&
377
+ value.inputDigest !== undefined &&
378
+ value.requirementToFragments !== undefined;
379
+ if (anyLedgerField && !allLedgerFields) {
380
+ ctx.addIssue({
381
+ code: z.ZodIssueCode.custom,
382
+ path: ["sourceBinding"],
383
+ message: "source binding v2 ledger fields (ledgerPath/ledgerSha256/inputDigest/requirementToFragments) must be provided together; partial or optional v1+ledger bindings are rejected",
384
+ });
385
+ }
386
+ }),
286
387
  riskLevel: z.enum(["small", "standard", "high-risk"]),
287
388
  targets: z
288
389
  .object({
@@ -298,6 +399,10 @@ export const frontendImplementationContractSchema = z
298
399
  implementationTargets: z.array(safePath),
299
400
  verificationTargetIds: z.array(z.string().min(1)),
300
401
  evidenceGap: gap.optional(),
402
+ // Source fidelity ledger binding (AC-005/AC-006): 每个 requirement
403
+ // 携带来源 fragment 证据,prewrite 门禁校验其真实性与覆盖。
404
+ sourceFragmentIds: z.array(z.string().min(1)).optional(),
405
+ sourceRefs: z.array(safePath).optional(),
301
406
  })
302
407
  .strict()).min(1),
303
408
  uiStates: z.array(z
@@ -370,7 +475,7 @@ export const frontendImplementationContractSchema = z
370
475
  uiStates: z.array(z.string().min(1)),
371
476
  })
372
477
  .strict()).min(1),
373
- evidenceGaps: z.array(gap),
478
+ evidenceGaps: z.array(gap).optional(),
374
479
  implementationSteps: z.array(z.string().min(1)).optional(),
375
480
  stylingStrategy: z.string().min(1).optional(),
376
481
  uiComponentChoices: uiComponentChoicesSchema.optional(),
@@ -434,6 +539,32 @@ export const frontendImplementationContractSchema = z
434
539
  message: `unknown verification target ${targetId}`,
435
540
  path: ["requirements"],
436
541
  });
542
+ // Source fidelity ledger binding (AC-005/AC-006): 绑定携带
543
+ // requirementToFragments(ledger v2)时,每个 requirement 必须携带
544
+ // 非空 sourceFragmentIds,否则 fail closed(防引用伪造/缺失)。
545
+ // sourceRefs(fragment→path 的派生展示)不设硬门槛:模型从 binding
546
+ // 只能拿到 fragment id 拿不到权威 path,强制只会诱导编造;防伪造的
547
+ // 真实性校验由 fidelity gate 按 sourceFragmentIds 执行(存在性 +
548
+ // 与 ledger boundFragments 一致)。
549
+ const boundFragments = value.sourceBinding.requirementToFragments?.[requirement.id];
550
+ if (value.sourceBinding.ledgerPath &&
551
+ (!requirement.sourceFragmentIds ||
552
+ requirement.sourceFragmentIds.length === 0)) {
553
+ ctx.addIssue({
554
+ code: "custom",
555
+ message: `requirement ${requirement.id} must carry sourceFragmentIds when a source fidelity ledger is bound`,
556
+ path: ["requirements", requirement.id, "sourceFragmentIds"],
557
+ });
558
+ }
559
+ else if (boundFragments !== undefined &&
560
+ (!requirement.sourceFragmentIds ||
561
+ requirement.sourceFragmentIds.length === 0)) {
562
+ ctx.addIssue({
563
+ code: "custom",
564
+ message: `requirement ${requirement.id} is bound to ledger fragments ${boundFragments.join(", ")} but declares none`,
565
+ path: ["requirements", requirement.id, "sourceFragmentIds"],
566
+ });
567
+ }
437
568
  }
438
569
  for (const state of value.uiStates) {
439
570
  if (state.applicable &&
@@ -463,10 +594,10 @@ export const frontendImplementationContractSchema = z
463
594
  }
464
595
  }
465
596
  else if (choice.decision === "new") {
466
- if (choice.specReference) {
597
+ if (!choice.specReference || !choice.specReference.path) {
467
598
  ctx.addIssue({
468
599
  code: "custom",
469
- message: "new component choice must not carry a specReference",
600
+ message: "new component choice requires a task-source specReference.path",
470
601
  path: ["uiComponentChoices", index, "specReference"],
471
602
  });
472
603
  }
@@ -570,6 +701,29 @@ export async function assertFrontendSourceBindingFresh(input) {
570
701
  throw new Error(`frontend source binding is stale: ${source.path}`);
571
702
  }
572
703
  }
704
+ // Source binding v2 ledger 新鲜度(AC-FIDELITY-REPAIR-001/002):v2 绑定携带的
705
+ // ledger 文件必须存在且 sha256 与绑定一致,否则 fail closed;v1 绑定无此路径。
706
+ if (isDagSourceBindingV2(input.binding)) {
707
+ const absolute = resolveDagTaskSourcePath({
708
+ workspaceRoot: input.workspaceRoot,
709
+ taskId: input.binding.taskId,
710
+ sourcePath: input.binding.ledgerPath,
711
+ });
712
+ let content;
713
+ try {
714
+ content = await readFile(absolute);
715
+ const info = await stat(absolute);
716
+ if (!info.isFile())
717
+ throw new Error("not a regular file");
718
+ }
719
+ catch (error) {
720
+ throw new Error(`frontend source fidelity ledger unavailable: ${input.binding.ledgerPath}: ${error instanceof Error ? error.message : String(error)}`);
721
+ }
722
+ const actual = createHash("sha256").update(content).digest("hex");
723
+ if (actual !== input.binding.ledgerSha256) {
724
+ throw new Error(`frontend source fidelity ledger is stale or tampered: ${input.binding.ledgerPath} (bound ${input.binding.ledgerSha256}, observed ${actual})`);
725
+ }
726
+ }
573
727
  }
574
728
  const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
575
729
  const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
@@ -744,7 +898,7 @@ export function canonicalFrontendContractSourceBinding(binding) {
744
898
  const requirement = binding.sources.find((source) => source.kind === "requirement");
745
899
  if (!requirement)
746
900
  throw new Error("frontend implementation contract gate requires a requirement source in DAG sourceBinding");
747
- return {
901
+ const sourceBinding = {
748
902
  taskId: binding.taskId,
749
903
  requirementPath: requirement.path,
750
904
  requirementSha256: requirement.sha256,
@@ -754,6 +908,15 @@ export function canonicalFrontendContractSourceBinding(binding) {
754
908
  .sort(),
755
909
  requirementIds: binding.requirementIds.map(canonicalizeRequirementId),
756
910
  };
911
+ // Source binding v2(schemaVersion=2 强制字段):透传 ledger 指针与
912
+ // requirement→fragment 映射。v1 绑定不得以 optional 字段冒充 v2。
913
+ if (isDagSourceBindingV2(binding)) {
914
+ sourceBinding.ledgerPath = binding.ledgerPath;
915
+ sourceBinding.ledgerSha256 = binding.ledgerSha256;
916
+ sourceBinding.inputDigest = binding.inputDigest;
917
+ sourceBinding.requirementToFragments = binding.requirementToFragments;
918
+ }
919
+ return sourceBinding;
757
920
  }
758
921
  function asRecord(value) {
759
922
  return value && typeof value === "object" && !Array.isArray(value)
@@ -1082,6 +1245,40 @@ function looksLikeStrictFrontendContract(value) {
1082
1245
  asRecord(record.mockApi) !== null &&
1083
1246
  asRecord(record.designEvidence) !== null);
1084
1247
  }
1248
+ /**
1249
+ * Single shared completion layer for required-with-fallback contract fields.
1250
+ *
1251
+ * Structural invariant: the coerce normalizer has strict-shape and free-form
1252
+ * branches that reshape differently, but every required key that carries a
1253
+ * deterministic fallback (currently `designEvidence`) must be completed in
1254
+ * exactly ONE place — here — so the branches can never drift again (the
1255
+ * designEvidence Required and invented-uiStates failures both came from
1256
+ * branch-local rules diverging). This layer never invents values that later
1257
+ * refinements reject: it only fills documented fallbacks for keys already
1258
+ * present-but-partial, and is idempotent for already-complete inputs.
1259
+ */
1260
+ export function completeCanonicalRequiredFields(value) {
1261
+ const record = asRecord(value);
1262
+ if (!record)
1263
+ return value;
1264
+ const designEvidence = asRecord(record.designEvidence);
1265
+ if (!designEvidence)
1266
+ return value;
1267
+ const source = asString(designEvidence.source);
1268
+ const paths = Array.isArray(designEvidence.paths);
1269
+ const conflicts = Array.isArray(designEvidence.conflicts);
1270
+ if (source && paths && conflicts)
1271
+ return value;
1272
+ return {
1273
+ ...record,
1274
+ designEvidence: {
1275
+ ...designEvidence,
1276
+ ...(source ? {} : { source: "repository-fallback+task-source" }),
1277
+ ...(paths ? {} : { paths: [] }),
1278
+ ...(conflicts ? {} : { conflicts: [] }),
1279
+ },
1280
+ };
1281
+ }
1085
1282
  /**
1086
1283
  * Coerce common free-form plan JSON into frontend-implementation-contract-v1.
1087
1284
  * Near-schema payloads are left untouched so unknown-key fail-closed still holds.
@@ -1186,6 +1383,9 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1186
1383
  ...strictRecord,
1187
1384
  requirements,
1188
1385
  ...(strictMockApi ? { mockApi: { ...strictMockApi, endpoints: mockEndpoints } } : {}),
1386
+ // Required-with-fallback keys (designEvidence etc.) are completed
1387
+ // by the single shared `completeCanonicalRequiredFields` layer in
1388
+ // the analyze pipeline — never per-branch.
1189
1389
  };
1190
1390
  }
1191
1391
  }
@@ -1234,9 +1434,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1234
1434
  : typeRaw.includes("type")
1235
1435
  ? "static"
1236
1436
  : "unit";
1237
- const commandLabel = asString(vt.commandLabel) ||
1238
- asString(vt.command) ||
1239
- (type === "static" ? "npm run typecheck" : "npm run test:unit:fe");
1437
+ const commandLabel = asString(vt.commandLabel) || asString(vt.command);
1240
1438
  const file = asString(vt.file) ||
1241
1439
  (Array.isArray(vt.symbols) ? targetFiles.find((p) => p.includes("__tests__")) : "") ||
1242
1440
  targetFiles.find((p) => p.includes("__tests__")) ||
@@ -1252,7 +1450,11 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1252
1450
  requirementIds: requirementIds.length > 0
1253
1451
  ? requirementIds
1254
1452
  : [...canonicalBinding.requirementIds],
1255
- uiStates: uiStateNames.length > 0 ? uiStateNames : ["success", "error"],
1453
+ // Never invent UI state references: "success" is not a canonical
1454
+ // state name, so a defaulted ["success","error"] was always rejected
1455
+ // by the schema's unknown-uiState refinement — a verification
1456
+ // target that declares no UI binding stays unbound ([]).
1457
+ uiStates: uiStateNames,
1256
1458
  });
1257
1459
  }
1258
1460
  if (verificationTargets.length === 0) {
@@ -1287,6 +1489,14 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1287
1489
  verificationTargetIds: verificationTargetIds.length > 0
1288
1490
  ? verificationTargetIds
1289
1491
  : defaultVerificationIds,
1492
+ // Source fidelity provenance 透传:兼容分支重建 requirement 时必须
1493
+ // 保留已声明的 sourceFragmentIds/sourceRefs(strict 分支通过
1494
+ // `...requirement` 保留,两边必须一致),否则 ledger-bound 契约在
1495
+ // designEvidence 缺失等情况下走兼容分支时丢失防伪造证据。
1496
+ ...(Array.isArray(req.sourceFragmentIds)
1497
+ ? { sourceFragmentIds: req.sourceFragmentIds }
1498
+ : {}),
1499
+ ...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
1290
1500
  };
1291
1501
  // Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
1292
1502
  if (gapText) {
@@ -1488,7 +1698,13 @@ export async function analyzeFrontendImplementationContract(input) {
1488
1698
  rawOutput = input.rawContractText;
1489
1699
  }
1490
1700
  else if (input.fromNodeId !== undefined) {
1491
- const record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
1701
+ let record;
1702
+ try {
1703
+ record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
1704
+ }
1705
+ catch (error) {
1706
+ throw new Error(`cannot parse node record ${input.fromNodeId}: ${error instanceof Error ? error.message : String(error)}`);
1707
+ }
1492
1708
  rawOutput = record.assistantText ?? record.stdout ?? "";
1493
1709
  }
1494
1710
  else {
@@ -1596,15 +1812,16 @@ export async function analyzeFrontendImplementationContract(input) {
1596
1812
  // There is exactly one post-security candidate. A fallback candidate would
1597
1813
  // allow malformed raw fields to bypass the boundary checks above.
1598
1814
  const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
1815
+ const completedContract = completeCanonicalRequiredFields(normalizedContract);
1599
1816
  const candidate = deriveFrontendVerificationCoverage({
1600
- ...(asRecord(normalizedContract) ?? parsed),
1817
+ ...(asRecord(completedContract) ?? parsed),
1601
1818
  sourceBinding: canonicalBinding,
1602
1819
  }, canonicalBinding);
1603
1820
  const result = frontendImplementationContractSchema.safeParse(candidate);
1604
1821
  if (!result.success)
1605
1822
  fail("retryable-invalid", `invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, candidateJsonSha256);
1606
1823
  const blockingGaps = [
1607
- ...result.data.evidenceGaps,
1824
+ ...(result.data.evidenceGaps ?? []),
1608
1825
  ...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
1609
1826
  ].filter((item) => item.blocking);
1610
1827
  if (blockingGaps.length > 0)
@@ -1613,7 +1830,7 @@ export async function analyzeFrontendImplementationContract(input) {
1613
1830
  .join(", ")}`, candidateJsonSha256);
1614
1831
  for (const requirementId of canonicalBinding.requirementIds)
1615
1832
  if (!result.data.requirements.some((item) => item.id === requirementId) &&
1616
- !result.data.evidenceGaps.some((item) => item.requirementId === requirementId))
1833
+ !(result.data.evidenceGaps ?? []).some((item) => item.requirementId === requirementId))
1617
1834
  fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
1618
1835
  return {
1619
1836
  canonical: result.data,
@@ -1637,6 +1854,40 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
1637
1854
  "/targets/files",
1638
1855
  "/mockApi/productionDefaultOff",
1639
1856
  ];
1857
+ const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
1858
+ "target-surface",
1859
+ "component-choice",
1860
+ "state-flow",
1861
+ "data-flow",
1862
+ "mock-api",
1863
+ "design-deviation",
1864
+ "dependency",
1865
+ "plan-requirement",
1866
+ "plan-verification-target",
1867
+ "plan-evidence-gap",
1868
+ ];
1869
+ /**
1870
+ * Restore the editable RFC 7386 plan patch from a committed plan ledger.
1871
+ * This is the single compile authority for the frontend implementation
1872
+ * contract: the tool payload (`patch` on any committed plan fact) wins over
1873
+ * any narrative JSON in the plan node text.
1874
+ */
1875
+ export function restorePlanPatchFromCommittedFacts(records) {
1876
+ const planFacts = records
1877
+ .filter((record) => record.phase === "committed")
1878
+ .map((record) => record.fact)
1879
+ .filter((fact) => fact.origin === "plan" &&
1880
+ typeof fact.kind === "string" &&
1881
+ PLAN_LEDGER_FACT_KINDS_FOR_COMPILE.includes(fact.kind));
1882
+ if (planFacts.length === 0)
1883
+ return undefined;
1884
+ for (const fact of planFacts) {
1885
+ const patch = fact.patch;
1886
+ if (isPlainObject(patch))
1887
+ return patch;
1888
+ }
1889
+ return undefined;
1890
+ }
1640
1891
  function frontendPlanPatchProtectedPathViolations(patch) {
1641
1892
  const record = asRecord(patch);
1642
1893
  if (!record)
@@ -1666,34 +1917,34 @@ function workspaceRootFromDagRunDir(runDir) {
1666
1917
  const joined = segments.slice(0, harnessIndex).join(path.sep);
1667
1918
  return joined || path.parse(absolute).root;
1668
1919
  }
1669
- function verificationSymbolMatchesFile(symbol, content) {
1670
- if (symbol.trim().toLowerCase() === "all describe blocks")
1671
- return content.includes("describe(");
1672
- const symbols = symbol.includes(" / ")
1673
- ? symbol
1674
- .split(/[((]/, 1)[0]
1675
- .split("/")
1676
- .map((item) => item.trim())
1677
- .filter(Boolean)
1678
- : [symbol];
1679
- return symbols.every((item) => {
1680
- const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
1681
- return [item, describeTitle, describeTitle ? "describe(" : undefined]
1682
- .filter((candidate) => Boolean(candidate))
1683
- .some((candidate) => content.includes(candidate));
1684
- });
1685
- }
1686
1920
  async function dropUnresolvedVerificationSymbols(input) {
1687
1921
  const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
1688
1922
  const targets = asRecord(input.contract)?.verificationTargets;
1689
1923
  if (!workspaceRoot || !Array.isArray(targets))
1690
- return false;
1691
- let changed = false;
1924
+ return [];
1925
+ const actions = [];
1692
1926
  for (const targetValue of targets) {
1693
1927
  const target = asRecord(targetValue);
1694
1928
  const file = asString(target?.file);
1695
1929
  const symbol = asString(target?.symbol);
1696
- if (!target || !file || !symbol)
1930
+ if (!target || !file)
1931
+ continue;
1932
+ // Static targets (project-wide commands such as typecheck/build) verify
1933
+ // by command exit code, never by a per-file symbol. Strip any symbol
1934
+ // the planner attached — including fabricated placeholders like "null"
1935
+ // — so the plan shape check and the verify trace gate never evaluate
1936
+ // it. This is the root fix for the recurring fabricated-symbol error
1937
+ // class: the field was allowed for static, the prompt did not forbid
1938
+ // it, and models habitually fill it; the compiler removes it instead.
1939
+ if (target.type === "static") {
1940
+ if (symbol) {
1941
+ delete target.symbol;
1942
+ if (!actions.includes("strip-static-verification-symbols"))
1943
+ actions.push("strip-static-verification-symbols");
1944
+ }
1945
+ continue;
1946
+ }
1947
+ if (!symbol)
1697
1948
  continue;
1698
1949
  const absolute = path.resolve(workspaceRoot, file);
1699
1950
  const relative = path.relative(workspaceRoot, absolute);
@@ -1702,6 +1953,10 @@ async function dropUnresolvedVerificationSymbols(input) {
1702
1953
  relative.includes("..")) {
1703
1954
  continue;
1704
1955
  }
1956
+ // Configuration files (tsconfig.json, vite.config.*, ...) do not expose
1957
+ // source symbols; the trace gate skips them, so the drop must too.
1958
+ if (isConfigurationVerificationFile(file))
1959
+ continue;
1705
1960
  let content;
1706
1961
  try {
1707
1962
  content = await readFile(absolute, "utf8");
@@ -1711,60 +1966,78 @@ async function dropUnresolvedVerificationSymbols(input) {
1711
1966
  // changing the optional symbol during candidate normalization.
1712
1967
  continue;
1713
1968
  }
1714
- if (verificationSymbolMatchesFile(symbol, content))
1969
+ if (verificationSymbolMatchesContent(symbol, content))
1715
1970
  continue;
1716
1971
  delete target.symbol;
1717
- changed = true;
1718
- }
1719
- return changed;
1720
- }
1721
- function extractSingleOpenspecCitationsBlock(text) {
1722
- const blocks = text.match(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g) ?? [];
1723
- if (blocks.length !== 1) {
1724
- throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
1972
+ if (!actions.includes("drop-unresolved-verification-symbols"))
1973
+ actions.push("drop-unresolved-verification-symbols");
1725
1974
  }
1726
- return blocks[0];
1727
- }
1728
- /**
1729
- * Frontend plan/revision output has two JSON-bearing regions: the primary
1730
- * payload and line-oriented OpenSpec citations. Keep their parsing domains
1731
- * separate so a citation row can never become a competing contract candidate.
1732
- * A bare JSON payload is retained only for historical node-output compatibility.
1733
- */
1734
- export function extractFrontendPlanPayload(text) {
1735
- const citationsBlock = extractSingleOpenspecCitationsBlock(text);
1736
- return {
1737
- payload: extractFrontendPlanPrimaryPayload(text.replace(citationsBlock, "")),
1738
- citationsBlock,
1739
- };
1740
- }
1741
- function extractFrontendPlanPrimaryPayload(text) {
1742
- const fences = [...text.matchAll(/```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi)];
1743
- if (fences.length > 1) {
1744
- throw new Error(`frontend plan output must contain exactly one primary fenced json payload (found ${fences.length})`);
1745
- }
1746
- if (fences.length === 1) {
1747
- try {
1748
- return JSON.parse(fences[0][1]);
1749
- }
1750
- catch (error) {
1751
- throw new Error(`frontend plan primary json fence is invalid: ${error instanceof Error ? error.message : String(error)}`);
1752
- }
1753
- }
1754
- return extractFrontendImplementationJson(text);
1975
+ return actions;
1755
1976
  }
1756
1977
  async function writeFrontendPlanCandidateRaw(input) {
1757
1978
  const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
1758
- await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text);
1979
+ await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text.endsWith("\n") ? input.text : `${input.text}\n`);
1759
1980
  return relativePath;
1760
1981
  }
1761
1982
  /**
1762
1983
  * Compile the planner's editable RFC 7386 patch against the deterministic
1763
1984
  * runtime skeleton. Every attempt is preserved under contracts/candidates so
1764
1985
  * format/schema failures can be inspected and replayed without trusting model
1765
- * prose. A successful result returns canonical full-contract text for all
1986
+ * prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
1766
1987
  * downstream review and prewrite nodes.
1767
1988
  */
1989
+ /**
1990
+ * Placeholder tokens that are never valid verification symbols ("null" is the
1991
+ * classic model artifact: it serializes a JS null into the symbol string).
1992
+ */
1993
+ const VERIFICATION_SYMBOL_PLACEHOLDERS = new Set([
1994
+ "null",
1995
+ "undefined",
1996
+ "none",
1997
+ "n/a",
1998
+ "na",
1999
+ "tbd",
2000
+ "todo",
2001
+ "...",
2002
+ "?",
2003
+ "-",
2004
+ ]);
2005
+ /** An over-long symbol can never resolve to a real declaration. */
2006
+ const VERIFICATION_SYMBOL_MAX_CHARS = 60;
2007
+ /**
2008
+ * Deterministic shape check for a verification target symbol. Catches obvious
2009
+ * model fabrication (placeholder tokens, prose fragments) at plan time so the
2010
+ * plan retry ladder can fix the facts in-node instead of the verify trace gate
2011
+ * failing at the very end of the run.
2012
+ *
2013
+ * Deliberately conservative: bare test titles without punctuation ("renders
2014
+ * loading") and `describe(...)` / `it(...)` forms stay valid — a real symbol
2015
+ * may be a function name, a dotted path, or a describe/it title.
2016
+ */
2017
+ function isSuspiciousVerificationSymbol(symbol) {
2018
+ const trimmed = symbol.trim();
2019
+ if (!trimmed)
2020
+ return false;
2021
+ if (VERIFICATION_SYMBOL_PLACEHOLDERS.has(trimmed.toLowerCase()))
2022
+ return true;
2023
+ if (trimmed.length > VERIFICATION_SYMBOL_MAX_CHARS)
2024
+ return true;
2025
+ // Deliberately conservative: bare test titles with punctuation (e.g.
2026
+ // "adds +1 button") are valid describe/it-style symbols and must pass;
2027
+ // the length + placeholder rules above already catch obvious fabrication
2028
+ // ("null", over-long prose fragments). Everything else is left to the
2029
+ // deterministic drop/trace resolvability check.
2030
+ return false;
2031
+ }
2032
+ function assertVerificationSymbolShapes(contract) {
2033
+ for (const target of contract.verificationTargets) {
2034
+ if (!target.symbol)
2035
+ continue;
2036
+ if (isSuspiciousVerificationSymbol(target.symbol)) {
2037
+ throw new Error(`frontend plan verification target "${target.id}" declares a fabricated symbol "${target.symbol}" for ${target.file}; fix it to a real symbol present in that file or leave the symbol empty (the deterministic trace gate verifies symbol resolvability after verification)`);
2038
+ }
2039
+ }
2040
+ }
1768
2041
  export async function validateFrontendPlanPatchNodeOutput(input) {
1769
2042
  const nodeId = input.nodeId ?? "frontend-plan-pi";
1770
2043
  const attempt = input.attempt ?? 1;
@@ -1796,28 +2069,23 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1796
2069
  });
1797
2070
  };
1798
2071
  try {
1799
- if (Buffer.byteLength(input.text, "utf8") > FRONTEND_PLAN_RESPONSE_MAX_BYTES) {
1800
- throw new Error(`frontend plan response exceeds the ${FRONTEND_PLAN_RESPONSE_MAX_BYTES}-byte limit`);
1801
- }
1802
2072
  if (!input.sourceBinding)
1803
2073
  throw new Error("frontend plan patch validator requires DAG sourceBinding");
1804
2074
  const skeleton = input.structuredContractOutput?.skeleton;
1805
2075
  if (!skeleton)
1806
2076
  throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
1807
- const patch = extractFrontendPlanPrimaryPayload(input.text);
2077
+ const committed = await readTypedEventStoreFromJsonl(path.join(input.runDir, nodeId, "plan-typed-facts.jsonl"));
2078
+ const patch = restorePlanPatchFromCommittedFacts(committed);
1808
2079
  if (!isPlainObject(patch))
1809
- throw new Error("frontend plan patch must extract to one JSON object");
2080
+ throw new Error("frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority");
1810
2081
  extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
1811
2082
  const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
1812
2083
  extractedPatchSha256 = extractedArtifact.sha256;
1813
- const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
1814
- const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
1815
- await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
1816
2084
  const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1817
2085
  if (protectedViolations.length > 0)
1818
2086
  throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
1819
2087
  const merged = applyFrontendContractMergePatch(skeleton, patch);
1820
- const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
2088
+ const symbolNormalizationActions = await dropUnresolvedVerificationSymbols({
1821
2089
  runDir: input.runDir,
1822
2090
  contract: merged,
1823
2091
  });
@@ -1826,10 +2094,24 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1826
2094
  rawContractText: serializeDeterministicJson(merged),
1827
2095
  sourceBinding: input.sourceBinding,
1828
2096
  });
1829
- if (Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8") >
1830
- FRONTEND_CANONICAL_CONTRACT_MAX_BYTES) {
1831
- throw new Error(`frontend canonical contract exceeds the ${FRONTEND_CANONICAL_CONTRACT_MAX_BYTES}-byte limit; split the task`);
1832
- }
2097
+ // Front-load the verification-symbol shape check so fabricated symbols
2098
+ // are fixed by the plan retry ladder in-node instead of failing the
2099
+ // verify trace gate at the end of the run.
2100
+ assertVerificationSymbolShapes(analysis.canonical);
2101
+ // Front-load the plan-attributable design-policy checks so the §5.1
2102
+ // retry ladder can fix these facts in-node with diagnostics instead of
2103
+ // the run terminating at the policy shell. The policy shell remains the
2104
+ // final authority and re-runs the identical checks on committed facts.
2105
+ const policyPreFindings = [
2106
+ ...checkUiDesignCoverage(analysis.canonical),
2107
+ ...checkUiStateAttribution(analysis.canonical),
2108
+ ...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
2109
+ ...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
2110
+ ];
2111
+ if (policyPreFindings.length > 0)
2112
+ throw new Error(`frontend plan policy pre-check failed (fix these plan facts, then re-commit and finalize): ${policyPreFindings
2113
+ .map((finding) => `${finding.code}: ${finding.message}`)
2114
+ .join("; ")}`);
1833
2115
  const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
1834
2116
  await writeReport({
1835
2117
  classification: "accepted-normalized",
@@ -1837,47 +2119,66 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1837
2119
  normalizedContractSha256: normalizedArtifact.sha256,
1838
2120
  normalizationActions: [
1839
2121
  "apply-runtime-skeleton",
1840
- ...(droppedUnresolvedSymbols
1841
- ? ["drop-unresolved-verification-symbols"]
1842
- : []),
2122
+ ...symbolNormalizationActions,
1843
2123
  ...analysis.normalizationActions,
1844
2124
  ],
1845
2125
  errors: [],
1846
2126
  });
2127
+ const canonicalRel = canonicalContractRel(nodeId);
2128
+ const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalRel, analysis.canonical);
2129
+ await persistRuntimeSkeleton({
2130
+ runDir: input.runDir,
2131
+ nodeId,
2132
+ skeleton,
2133
+ });
2134
+ const artifact = {
2135
+ path: canonicalArtifact.path,
2136
+ sha256: canonicalArtifact.sha256,
2137
+ schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
2138
+ };
2139
+ // Human-readable plan summary appended after the artifact pointer. The
2140
+ // pointer remains the machine authority (path/schema/sha256); the
2141
+ // rendered summary is a deterministic view of the canonical contract so
2142
+ // Console/reviewers can read the plan without opening the JSON. It never
2143
+ // participates in downstream validation.
2144
+ const planSummary = renderFrontendPlanMarkdown(analysis.canonical);
1847
2145
  return {
1848
2146
  ok: true,
1849
2147
  contract: analysis.canonical,
1850
- structuredArtifact: {
1851
- path: normalizedArtifact.path,
1852
- sha256: normalizedArtifact.sha256,
1853
- schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
1854
- },
1855
- frontendPlanOutput: {
1856
- schemaVersion: 1,
1857
- payloadKind: "canonical-contract",
1858
- citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
1859
- citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
1860
- rawCandidatePath: path.join(input.runDir, rawPath),
1861
- rawCandidateSha256: sha256Hex(input.text),
1862
- },
1863
- normalizedText: [
1864
- "Frontend implementation contract compiled from the runtime skeleton.",
1865
- "",
1866
- "```json",
1867
- serializeDeterministicJson(analysis.canonical),
1868
- "```",
1869
- citationsBlock,
1870
- ].join("\n"),
2148
+ artifact,
2149
+ normalizedText: `${formatStructuredArtifactPointer(artifact)}\n\n${planSummary}`,
1871
2150
  };
1872
2151
  }
1873
2152
  catch (error) {
1874
2153
  const reason = error instanceof Error ? error.message : String(error);
2154
+ const classified = classifyStructuredContractFailure({
2155
+ reason,
2156
+ kind: error instanceof FrontendContractFailure ? error.kind : undefined,
2157
+ });
1875
2158
  await writeReport({
1876
- classification: "retryable-invalid",
2159
+ classification: classified.classification,
1877
2160
  normalizationActions: [],
1878
- errors: [{ path: "$", code: "invalid-output", message: reason }],
2161
+ errors: classified.errors,
1879
2162
  });
1880
- return { ok: false, reason };
2163
+ return {
2164
+ ok: false,
2165
+ reason,
2166
+ classification: classified.classification,
2167
+ errors: classified.errors,
2168
+ };
2169
+ }
2170
+ }
2171
+ export async function readFrontendCanonicalCandidate(runDir, nodeId) {
2172
+ const candidatePath = path.join(runDir, canonicalContractRel(nodeId));
2173
+ try {
2174
+ const raw = await readFile(candidatePath, "utf8");
2175
+ const parsed = frontendImplementationContractSchema.safeParse(JSON.parse(raw));
2176
+ return parsed.success ? parsed.data : undefined;
2177
+ }
2178
+ catch (error) {
2179
+ if (error.code === "ENOENT")
2180
+ return undefined;
2181
+ return undefined;
1881
2182
  }
1882
2183
  }
1883
2184
  /**
@@ -1898,85 +2199,140 @@ export async function validateFrontendContractNodeOutput(input) {
1898
2199
  return { ok: true, contract: analysis.canonical };
1899
2200
  }
1900
2201
  catch (error) {
2202
+ const reason = error instanceof Error ? error.message : String(error);
2203
+ const classified = classifyStructuredContractFailure({
2204
+ reason,
2205
+ kind: error instanceof FrontendContractFailure ? error.kind : undefined,
2206
+ });
1901
2207
  return {
1902
2208
  ok: false,
1903
- reason: error instanceof Error ? error.message : String(error),
2209
+ reason,
2210
+ classification: classified.classification,
2211
+ errors: classified.errors,
1904
2212
  };
1905
2213
  }
1906
2214
  }
1907
2215
  /**
1908
- * Node-output self-check for the plan revision node, which emits an RFC 7386
1909
- * merge-patch delta against the original contract instead of a full contract.
1910
- * Deliberately loose and format-level: the output must contain exactly one
1911
- * ```openspec-citations fenced block and must extract (after the citations
1912
- * blocks are stripped) to exactly one JSON object. The delta carries no
1913
- * schemaVersion/targets, so no full-contract schema/semantic checks run here;
1914
- * content parsing, patch application, and merged-contract validation stay with
1915
- * the prewrite gate, which remains the only authority.
2216
+ * Deterministically assemble the canonical frontend implementation contract
2217
+ * from committed typed facts, never from plan/contract node text.
2218
+ *
2219
+ * Inputs (all read-only, all fail-closed):
2220
+ * - `<planNodeId>/plan-typed-facts.jsonl` (phase=committed) restores the
2221
+ * editable RFC 7386 patch;
2222
+ * - `<contractNodeId>/contract-typed-facts.jsonl` (phase=committed) must carry
2223
+ * a `contract-finalized` terminal fact;
2224
+ * - `<contractNodeId>/frontend-task-contract-vNext.json` must exist and parse.
2225
+ *
2226
+ * The patch is applied onto the runtime skeleton and re-validated through the
2227
+ * same `analyzeFrontendImplementationContract` pipeline used by the legacy
2228
+ * text path, so the downstream artifact semantics are unchanged. The function
2229
+ * is pure: it never writes candidate/audit artifacts and always produces the
2230
+ * same canonical serialization for the same committed facts. It never falls
2231
+ * back to parsing Plan/Contract node text.
1916
2232
  */
1917
- export async function validateFrontendRevisionPatchNodeOutput(input) {
1918
- const nodeId = input.nodeId ?? "frontend-plan-revision-pi";
1919
- const attempt = input.attempt ?? 1;
1920
- const candidateDir = path.posix.join("contracts", "candidates", nodeId);
1921
- const rawPath = await writeFrontendPlanCandidateRaw({
1922
- runDir: input.runDir,
1923
- nodeId,
1924
- attempt,
1925
- text: input.text,
1926
- });
1927
- if (Buffer.byteLength(input.text, "utf8") > FRONTEND_PLAN_RESPONSE_MAX_BYTES) {
2233
+ export async function buildContractFromCommittedFacts(input) {
2234
+ const contractNodeId = input.contractNodeId ?? "frontend-contract-pi";
2235
+ // Skeleton guard: the runtime skeleton must carry the protected identity
2236
+ // fields so the merged contract can be validated against the full schema.
2237
+ const skeletonRecord = asRecord(input.skeleton);
2238
+ const skeletonTargets = asRecord(skeletonRecord?.targets);
2239
+ const skeletonRiskLevel = asString(skeletonRecord?.riskLevel);
2240
+ if (!skeletonRecord ||
2241
+ !["small", "standard", "high-risk"].includes(skeletonRiskLevel) ||
2242
+ !Array.isArray(skeletonTargets?.files) ||
2243
+ skeletonTargets.files.length === 0) {
1928
2244
  return {
1929
2245
  ok: false,
1930
- reason: `frontend revision response exceeds the ${FRONTEND_PLAN_RESPONSE_MAX_BYTES}-byte limit`,
2246
+ reason: "frontend contract skeleton missing required runtime fields (riskLevel/targets.files)",
2247
+ failureCode: "missing-skeleton",
1931
2248
  };
1932
2249
  }
1933
- let citationsBlock;
1934
- let patch;
2250
+ // a. Committed plan ledger restores the editable patch.
2251
+ const planRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.planNodeId, "plan-typed-facts.jsonl"));
2252
+ const patch = restorePlanPatchFromCommittedFacts(planRecords);
2253
+ if (!isPlainObject(patch)) {
2254
+ return {
2255
+ ok: false,
2256
+ reason: "frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority",
2257
+ failureCode: "missing-plan-ledger",
2258
+ };
2259
+ }
2260
+ // b. Committed contract terminal fact (contract-finalized).
2261
+ const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
2262
+ const contractKinds = new Set(contractRecords
2263
+ .filter((record) => record.phase === "committed")
2264
+ .map((record) => record.fact.kind)
2265
+ .filter((kind) => typeof kind === "string"));
2266
+ if (!contractKinds.has("contract-finalized")) {
2267
+ return {
2268
+ ok: false,
2269
+ reason: "frontend contract facts missing: no committed contract-finalized terminal fact",
2270
+ failureCode: "missing-contract-finalize",
2271
+ };
2272
+ }
2273
+ // c. frontend-task-contract-vNext.json must exist and parse (input integrity).
2274
+ let rawVNext;
1935
2275
  try {
1936
- const parsed = extractFrontendPlanPayload(input.text);
1937
- citationsBlock = parsed.citationsBlock;
1938
- patch = parsed.payload;
2276
+ rawVNext = await readFile(path.join(input.runDir, contractNodeId, "frontend-task-contract-vNext.json"), "utf8");
2277
+ }
2278
+ catch {
2279
+ return {
2280
+ ok: false,
2281
+ reason: `frontend task contract vNext missing at ${contractNodeId}/frontend-task-contract-vNext.json`,
2282
+ failureCode: "contract-vnext-missing",
2283
+ };
2284
+ }
2285
+ try {
2286
+ JSON.parse(rawVNext);
1939
2287
  }
1940
2288
  catch (error) {
1941
2289
  return {
1942
2290
  ok: false,
1943
- reason: error instanceof Error ? error.message : String(error),
2291
+ reason: `frontend task contract vNext is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
2292
+ failureCode: "contract-vnext-invalid",
1944
2293
  };
1945
2294
  }
1946
- if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
2295
+ // d. Merge the editable patch onto the protected skeleton and validate.
2296
+ let merged;
2297
+ try {
2298
+ merged = applyFrontendContractMergePatch(input.skeleton, patch);
2299
+ }
2300
+ catch (error) {
1947
2301
  return {
1948
2302
  ok: false,
1949
- reason: "revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)",
2303
+ reason: `frontend contract patch could not be applied to the runtime skeleton: ${error instanceof Error ? error.message : String(error)}`,
2304
+ failureCode: "schema-drift",
2305
+ };
2306
+ }
2307
+ // The design-policy shell is the authoritative materializer of the
2308
+ // canonical contract; keep it on the same unresolved-symbol drop as the
2309
+ // plan validator so the verify trace gate never sees fabricated symbols.
2310
+ await dropUnresolvedVerificationSymbols({
2311
+ runDir: input.runDir,
2312
+ contract: merged,
2313
+ });
2314
+ try {
2315
+ const analysis = await analyzeFrontendImplementationContract({
2316
+ runDir: input.runDir,
2317
+ rawContractText: serializeDeterministicJson(merged),
2318
+ sourceBinding: input.sourceBinding,
2319
+ });
2320
+ return {
2321
+ ok: true,
2322
+ canonical: analysis.canonical,
2323
+ normalizationActions: analysis.normalizationActions,
2324
+ candidateJsonSha256: analysis.candidateJsonSha256,
1950
2325
  };
1951
2326
  }
1952
- const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1953
- if (protectedViolations.length > 0) {
2327
+ catch (error) {
2328
+ const failureKind = error instanceof FrontendContractFailure ? error.kind : undefined;
1954
2329
  return {
1955
2330
  ok: false,
1956
- reason: `frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`,
2331
+ reason: `frontend contract schema drift: ${error instanceof Error ? error.message : String(error)}`,
2332
+ failureCode: "schema-drift",
2333
+ ...(failureKind ? { failureKind } : {}),
1957
2334
  };
1958
2335
  }
1959
- const payloadPath = path.posix.join(candidateDir, `attempt-${attempt}.patch.json`);
1960
- const payloadArtifact = await writeDeterministicJsonArtifact(input.runDir, payloadPath, patch);
1961
- const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
1962
- await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
1963
- return {
1964
- ok: true,
1965
- contract: patch,
1966
- structuredArtifact: {
1967
- path: payloadArtifact.path,
1968
- sha256: payloadArtifact.sha256,
1969
- schemaId: "frontend-implementation-contract-revision-patch-v1",
1970
- },
1971
- frontendPlanOutput: {
1972
- schemaVersion: 1,
1973
- payloadKind: "merge-patch",
1974
- citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
1975
- citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
1976
- rawCandidatePath: path.join(input.runDir, rawPath),
1977
- rawCandidateSha256: sha256Hex(input.text),
1978
- },
1979
- };
1980
2336
  }
1981
2337
  export async function materializeFrontendImplementationContract(input) {
1982
2338
  const analysis = await analyzeFrontendImplementationContract(input);