@tea-agent/loop-agent 0.41.1-beta.0 → 0.42.0-next.1

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 (273) hide show
  1. package/CHANGELOG.md +61 -207
  2. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  3. package/dist/adapters/context-transfer/pi-session.js +61 -0
  4. package/dist/application/dag/generate-task-dag.js +19 -75
  5. package/dist/application/task-lifecycle/advance.js +5 -24
  6. package/dist/application/task-lifecycle/observe.js +17 -171
  7. package/dist/application/task-lifecycle/plan-transitions.js +7 -42
  8. package/dist/application/task-lifecycle/recommendations.js +2 -13
  9. package/dist/build-stamp.json +3 -3
  10. package/dist/cli/command-definitions.js +12 -7
  11. package/dist/cli/program.js +23 -6
  12. package/dist/commands/client-recovery.js +0 -3
  13. package/dist/commands/dag-artifact.js +284 -0
  14. package/dist/commands/dag-context.js +184 -0
  15. package/dist/commands/dag-rerun.js +206 -1
  16. package/dist/commands/init.js +1 -27
  17. package/dist/commands/task-advance.js +0 -19
  18. package/dist/executors/dag-pi-executor.js +266 -3792
  19. package/dist/executors/pi-executor.js +4 -15
  20. package/dist/executors/pi-sdk-executor.js +3 -0
  21. package/dist/executors/shell-executor.js +217 -945
  22. package/dist/executors/shell-write-guard.js +0 -7
  23. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  24. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  25. package/dist/infrastructure/console/context-export-store.js +160 -0
  26. package/dist/infrastructure/console/dir-lock.js +132 -0
  27. package/dist/infrastructure/console/operation-store.js +197 -0
  28. package/dist/infrastructure/harness/artifact-store.js +10 -1
  29. package/dist/infrastructure/harness/atomic-write.js +12 -2
  30. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  31. package/dist/shared/context-transfer.js +418 -0
  32. package/dist/shared/dag-failure-category.js +0 -12
  33. package/dist/shared/openspec-spec.js +4 -70
  34. package/dist/shared/operator/capabilities.js +14 -7
  35. package/dist/shared/operator/safe-run-summary.js +1 -0
  36. package/dist/shared/path-safety.js +93 -0
  37. package/dist/shared/preview.js +28 -4
  38. package/dist/task/config-types.js +9 -35
  39. package/dist/task/contract/adopt.js +0 -4
  40. package/dist/task/contract/import-revision.js +0 -4
  41. package/dist/task/contract/project.js +0 -3
  42. package/dist/task/contract/schema.js +1 -2
  43. package/dist/task/frontend-project-capability.js +20 -203
  44. package/dist/task/runtime.js +2 -5
  45. package/dist/task/source-prepare/build-draft.js +3 -3
  46. package/dist/task/source-prepare/fragment-inventory.js +9 -15
  47. package/dist/task/source-prepare/prepare.js +1 -89
  48. package/dist/task/source-prepare/semantic-intake.js +2 -6
  49. package/dist/task/source-references.js +1 -22
  50. package/dist/task/task-demand-routing.js +3 -0
  51. package/dist/worker/console/chat/chat-event-store.js +2 -2
  52. package/dist/worker/console/chat/pi-runtime.js +3 -2
  53. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  54. package/dist/worker/console/chat/routes.js +13 -4
  55. package/dist/worker/console/chat/semantic-activity.js +12 -11
  56. package/dist/worker/console/chat/session-stats.js +96 -0
  57. package/dist/worker/console/chat/session-store.js +1 -1
  58. package/dist/worker/console/chat/turn-process.js +28 -4
  59. package/dist/worker/console/chat/user-questions.js +1 -1
  60. package/dist/worker/console/console-update-runtime.js +1 -1
  61. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  62. package/dist/worker/console/dag-confirmation.js +1 -1
  63. package/dist/worker/console/dag-execution-receipt.js +1 -1
  64. package/dist/worker/console/doctor.js +1 -1
  65. package/dist/worker/console/draft-store.js +1 -1
  66. package/dist/worker/console/human-gate-token.js +1 -1
  67. package/dist/worker/console/index.js +3 -6
  68. package/dist/worker/console/interview/assessment.js +1 -1
  69. package/dist/worker/console/interview/session.js +1 -1
  70. package/dist/worker/console/operation-runner.js +45 -4
  71. package/dist/worker/console/operation-sse.js +1 -1
  72. package/dist/worker/console/operation-wait.js +1 -1
  73. package/dist/worker/console/operator-actions.js +107 -168
  74. package/dist/worker/console/operator-surface-health.js +1 -1
  75. package/dist/worker/console/operator-user-error.js +169 -0
  76. package/dist/worker/console/pi-plugins.js +60 -1
  77. package/dist/worker/console/routes.js +637 -8
  78. package/dist/worker/console/security.js +35 -0
  79. package/dist/worker/console/server.js +6 -6
  80. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DZZ8m3PO.js} +1 -1
  81. package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-D6PvaVd-.js} +1 -1
  82. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-B_OTOiI8.js} +1 -1
  83. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-Bv6rqHBg.js} +1 -1
  84. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-B8eHr0oz.js} +1 -1
  85. package/dist/worker/console/static/assets/channel-BU5gOilw.js +1 -0
  86. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-DYwR0im2.js} +1 -1
  87. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-D2WPGqXt.js} +1 -1
  88. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-CQISJ_I7.js} +1 -1
  89. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-C0o2Du1e.js} +1 -1
  90. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-4f8kr-U7.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-D_OWvXzX.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-CasdPz5X.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-Cj8lRrla.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-CUvD1FSd.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-CqLw_eqB.js} +1 -1
  96. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +1 -0
  97. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +1 -0
  98. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-ku-WqJPx.js} +1 -1
  99. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-Bq_PYDhl.js} +1 -1
  100. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-BiGZV641.js} +1 -1
  101. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-C4JRlglF.js} +1 -1
  102. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-B0dNeWYG.js} +1 -1
  103. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cj35YvYM.js} +1 -1
  104. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-uzQoJ2-8.js} +1 -1
  105. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D1a-Buoz.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-XjRrLRSs.js} +1 -1
  107. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-Da_O24cW.js} +1 -1
  108. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BLJ8jrYU.js} +1 -1
  109. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-B9GrLjM0.js} +1 -1
  110. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-CeJ0TqiK.js} +1 -1
  111. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js} +1 -1
  112. package/dist/worker/console/static/assets/index-H9rFJiGL.css +1 -0
  113. package/dist/worker/console/static/assets/index-xwu9GxEc.js +451 -0
  114. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-DB26i3d8.js} +1 -1
  115. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BOGRyeZT.js} +1 -1
  116. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-CZRoBO_6.js} +1 -1
  117. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-qlWhJyoB.js} +1 -1
  118. package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-CfUiDDB3.js} +1 -1
  119. package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-BQe6fpqj.js} +5 -5
  120. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-zFzWHw64.js} +1 -1
  121. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-DFGUqjZO.js} +1 -1
  122. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-BB5i0l8Z.js} +1 -1
  123. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-obbVZ_X5.js} +1 -1
  124. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-BwdaNzc1.js} +1 -1
  125. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CkTJaugh.js} +1 -1
  126. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-BwJ-hIgq.js} +1 -1
  127. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-BzSgmm7w.js} +1 -1
  128. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-Bn3oj5Q1.js} +1 -1
  129. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-dxhGtL8K.js} +1 -1
  130. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +1 -0
  131. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BLpEInya.js} +2 -2
  132. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +8 -0
  133. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-ChJGYcXy.js} +1 -1
  134. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-DK-qjRer.js} +1 -1
  135. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-3qeaWAg-.js} +1 -1
  136. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-CootlyP9.js} +1 -1
  137. package/dist/worker/console/static/index.html +7 -2
  138. package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
  139. package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
  140. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
  141. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  142. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
  143. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  144. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
  145. package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
  146. package/dist/worker/console/workspace-context.js +28 -112
  147. package/dist/worker/console/workspace-registry.js +2 -2
  148. package/dist/worker/continuation/worker-continuation.js +278 -0
  149. package/dist/worker/observability/read-model.js +4 -0
  150. package/dist/worker/observe/health.js +1 -1
  151. package/dist/worker/observe/node-transparency.js +572 -0
  152. package/dist/worker/observe/routes.js +51 -1
  153. package/dist/worker/observe/static/api.js +69 -5
  154. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  155. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  156. package/dist/worker/observe/static/dag-node-purpose.js +0 -5
  157. package/dist/worker/observe/static/state.js +3 -1
  158. package/dist/worker/observe/static/styles.css +1037 -126
  159. package/dist/worker/observe/static/views/dag-inspector.js +1217 -65
  160. package/dist/worker/observe/static/views/session-timeline.js +121 -25
  161. package/dist/worker/outcomes/adapters.js +19 -0
  162. package/dist/worker/outcomes/types.js +1 -0
  163. package/dist/worker/pool/attempt-lease.js +97 -66
  164. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  165. package/dist/worker/pool/reconcile.js +46 -1
  166. package/dist/worker/pool/run-store.js +2 -0
  167. package/dist/worker/pool/state-projection.js +2 -0
  168. package/dist/worker/task-spec/workflow-routing.js +8 -3
  169. package/dist/workflows/dag/artifact-bindings.js +149 -0
  170. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  171. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  172. package/dist/workflows/dag/budget-enforcement.js +10 -0
  173. package/dist/workflows/dag/context-receipt.js +305 -0
  174. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  175. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  176. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  177. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  178. package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
  179. package/dist/workflows/dag/frontend-plan-render.js +1 -2
  180. package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
  181. package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
  182. package/dist/workflows/dag/frontend-recovery-run.js +20 -33
  183. package/dist/workflows/dag/frontend-repair.js +432 -1
  184. package/dist/workflows/dag/frontend-review-context.js +15 -261
  185. package/dist/workflows/dag/frontend-verification-trace.js +24 -249
  186. package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
  187. package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
  188. package/dist/workflows/dag/init-hybrid.js +640 -685
  189. package/dist/workflows/dag/interrupt-request.js +0 -7
  190. package/dist/workflows/dag/node-execution.js +116 -759
  191. package/dist/workflows/dag/path-safety.js +1 -0
  192. package/dist/workflows/dag/prompt.js +30 -38
  193. package/dist/workflows/dag/report.js +1 -37
  194. package/dist/workflows/dag/rerun-feedback.js +1 -256
  195. package/dist/workflows/dag/rerun-plan.js +557 -11
  196. package/dist/workflows/dag/rerun-run.js +601 -38
  197. package/dist/workflows/dag/rerun-task.js +0 -29
  198. package/dist/workflows/dag/retry-policy.js +104 -214
  199. package/dist/workflows/dag/runner.js +142 -430
  200. package/dist/workflows/dag/scheduler.js +16 -114
  201. package/dist/workflows/dag/skill-snapshot.js +17 -0
  202. package/dist/workflows/dag/types.js +195 -246
  203. package/dist/workflows/dag/validate.js +12 -28
  204. package/docs/architecture/runtime-boundaries.md +6 -6
  205. package/docs/architecture/worker-and-feature.md +1 -1
  206. package/docs/init-surface.manifest.json +12 -30
  207. package/docs/skills/vetted-skill-registry.md +2 -4
  208. package/docs/templates/README.md +0 -2
  209. package/docs/templates/agent-dag-report.schema.json +2 -8
  210. package/docs/templates/agent-dag.schema.json +33 -1
  211. package/docs/templates/frontend-implementation-contract.schema.json +1 -4
  212. package/docs/templates/product-line/task.yaml +1 -1
  213. package/harness.json +3 -3
  214. package/package.json +3 -1
  215. package/skills/frontend-bounded-implement/SKILL.md +14 -15
  216. package/skills/frontend-design-review/SKILL.md +41 -22
  217. package/skills/frontend-implementation/SKILL.md +52 -0
  218. package/skills/frontend-implementation/references/code-standards.md +33 -0
  219. package/skills/frontend-implementation/references/design-spec.md +56 -0
  220. package/skills/frontend-implementation/references/node-contracts.md +31 -0
  221. package/skills/frontend-review/SKILL.md +15 -20
  222. package/skills/frontend-review/references/review-findings.md +7 -6
  223. package/skills/frontend-verification/SKILL.md +1 -1
  224. package/skills/loop-agent/references/command-reference.md +6 -1
  225. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  226. package/dist/commands/dag-follow-up.js +0 -138
  227. package/dist/executors/pi-read-budget-policy.js +0 -239
  228. package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
  229. package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
  230. package/dist/worker/console/operation-store.js +0 -389
  231. package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
  232. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
  233. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
  234. package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
  235. package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
  236. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
  237. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
  238. package/dist/worker/console/static-src/active-run-badge.js +0 -17
  239. package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
  240. package/dist/workflows/dag/dag-retry-schema.js +0 -138
  241. package/dist/workflows/dag/frontend-closeout.js +0 -221
  242. package/dist/workflows/dag/frontend-design-policy.js +0 -400
  243. package/dist/workflows/dag/frontend-human-decision.js +0 -182
  244. package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
  245. package/dist/workflows/dag/frontend-read-budgets.js +0 -12
  246. package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
  247. package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
  248. package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
  249. package/dist/workflows/dag/frontend-review-findings.js +0 -270
  250. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
  251. package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
  252. package/dist/workflows/dag/frontend-shape-facts.js +0 -409
  253. package/dist/workflows/dag/frontend-shape.js +0 -427
  254. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
  255. package/dist/workflows/dag/frontend-split-application-service.js +0 -203
  256. package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
  257. package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
  258. package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
  259. package/dist/workflows/dag/frontend-writer-admission.js +0 -285
  260. package/dist/workflows/dag/frontend-writer-status.js +0 -256
  261. package/dist/workflows/dag/recovery-lease.js +0 -80
  262. package/docs/templates/frontend-implementation-dag.json +0 -89
  263. package/docs/templates/spec-registry.schema.json +0 -45
  264. package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
  265. package/skills/frontend-contract/SKILL.md +0 -23
  266. package/skills/frontend-contract/references/contract-protocol.md +0 -34
  267. package/skills/frontend-plan/SKILL.md +0 -26
  268. package/skills/frontend-plan/references/decision-contract.md +0 -37
  269. package/skills/frontend-plan/references/design-decisions.md +0 -17
  270. package/skills/frontend-scout/SKILL.md +0 -25
  271. package/skills/frontend-scout/references/design-evidence.md +0 -16
  272. package/skills/frontend-scout/references/scout-evidence.md +0 -23
  273. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -2,17 +2,21 @@ import { createHash } from "node:crypto";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
5
- import { OPENSPEC_SPEC_EXT_RE, isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
5
+ import { isDagSourceBindingV2 } from "./types.js";
6
+ import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
6
7
  import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
7
8
  import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
8
- import { checkFrontendSourceFidelityLedger, } from "./frontend-source-fidelity-ledger.js";
9
- import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
9
+ import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
10
+ import { parseLedgerJson, validateRequirementLedger, } from "../../task/source-prepare/ledger.js";
11
+ import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
10
12
  import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
11
13
  import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
12
14
  import { pathMatchesPattern } from "../../shared/git-progress.js";
13
15
  import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
14
16
  export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
15
17
  export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
18
+ /** Canonical contract serialization must stay at or below this byte size. */
19
+ export const FRONTEND_CONTRACT_MAX_BYTES = 65536;
16
20
  /**
17
21
  * Machine-readable failure classification for blocked/retryable-invalid
18
22
  * prewrite outcomes. Kept alongside the human-readable failureReason so
@@ -33,12 +37,13 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
33
37
  "verification-target-outside-write-set",
34
38
  "openspec-not-read",
35
39
  "candidate-missing-drift",
36
- "candidate-content-drift",
37
- "openspec-selection-invalid",
40
+ "openspec-citation-block-unparseable",
41
+ "openspec-not-cited",
42
+ "openspec-citation-not-read",
38
43
  "component-spec-reference-invalid",
39
44
  "component-spec-not-cited",
40
- "component-spec-choice-unbound",
41
45
  "component-choices-missing",
46
+ "contract-too-large",
42
47
  "revision-patch-unmergeable",
43
48
  // Source fidelity ledger 类目(SOURCE_FIDELITY_*/source-* 稳定错误码)。
44
49
  "source-fidelity-ledger-missing",
@@ -135,7 +140,7 @@ export const frontendPrewriteResultV1Schema = z
135
140
  export const frontendWriterAdmissionSchema = z
136
141
  .object({
137
142
  schemaVersion: z.literal(1),
138
- writerNodeId: z.enum(["frontend-implement-pi"]),
143
+ writerNodeId: z.enum(["frontend-implement-pi", "frontend-repair-pi"]),
139
144
  decision: z.enum(["authorized", "denied"]),
140
145
  sourceArtifact: z.string().min(1),
141
146
  artifactHash: z.string().regex(/^[a-f0-9]{64}$/),
@@ -269,7 +274,6 @@ function isFailedToolResult(event) {
269
274
  }
270
275
  async function collectOpenspecReadPaths(input) {
271
276
  const { runDir, planNodeId, reviewNodeId, repoRoot } = input;
272
- const candidateSet = input.candidatePaths ? new Set(input.candidatePaths) : undefined;
273
277
  const matched = new Set();
274
278
  for (const nodeId of [planNodeId, reviewNodeId]) {
275
279
  const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
@@ -304,7 +308,7 @@ async function collectOpenspecReadPaths(input) {
304
308
  : null;
305
309
  if (relativePath &&
306
310
  !isFailedToolResult(event) &&
307
- (candidateSet ? candidateSet.has(relativePath) : OPENSPEC_SPEC_EXT_RE.test(relativePath))) {
311
+ isOpenspecSpecFilePath(relativePath)) {
308
312
  matched.add(relativePath);
309
313
  }
310
314
  startedReads.delete(event.toolCallId);
@@ -319,6 +323,21 @@ async function collectOpenspecReadPaths(input) {
319
323
  }
320
324
  return [...matched];
321
325
  }
326
+ async function checkOpenspecReadEvidence(input) {
327
+ const { runDir, candidatePaths, planNodeId, reviewNodeId, repoRoot } = input;
328
+ if (candidatePaths.length === 0)
329
+ return [];
330
+ const allReadPaths = await collectOpenspecReadPaths({
331
+ runDir,
332
+ planNodeId,
333
+ reviewNodeId,
334
+ repoRoot,
335
+ });
336
+ const normalizedCandidates = new Set(candidatePaths
337
+ .map((candidate) => toRepoRelativePath(candidate, repoRoot))
338
+ .filter((candidate) => Boolean(candidate && isOpenspecSpecFilePath(candidate))));
339
+ return allReadPaths.filter((readPath) => normalizedCandidates.has(readPath));
340
+ }
322
341
  function normalizeCitationPath(rawPath, repoRoot) {
323
342
  let candidate = rawPath.trim();
324
343
  const hashIndex = candidate.indexOf("#");
@@ -333,132 +352,57 @@ function normalizeCitationPath(rawPath, repoRoot) {
333
352
  }
334
353
  return candidate;
335
354
  }
336
- async function resolveRequiredOpenspecPaths(input) {
337
- if (input.policy === "scan-strict")
338
- return { ok: true, paths: input.candidatePaths };
339
- // Legacy cited DAGs had no selector: preserve their full candidate must-read
340
- // semantics instead of silently weakening an existing run.
341
- if (!input.selectionNodeId)
342
- return { ok: true, paths: input.candidatePaths };
343
- // OpenSpec classifications arrive as individual typed `openspec-selection`
344
- // facts (one path per record_openspec_selection call) so a large candidate
345
- // set never exceeds a single model output budget. Only positive
346
- // classifications (required | relevant) are legal facts; candidates that
347
- // were never mentioned default to irrelevant. Explicit task declarations /
348
- // source citations (`mandatoryPaths`) are never downgraded: they are
349
- // always required and always must-read, whether or not the model declared
350
- // them. Fall back to the legacy single fenced JSON for older runs (which
351
- // may still carry explicit irrelevant rows).
352
- let selections;
353
- let allowIrrelevant = false;
354
- const selectionNodeId = input.selectionNodeId; // guarded above: no selector -> full candidate semantics
355
- try {
356
- const facts = await readCommittedContractFacts(input.runDir, selectionNodeId);
357
- selections = facts
358
- .filter((fact) => fact.kind === "openspec-selection")
359
- .map((fact) => ({
360
- path: String(fact.path ?? ""),
361
- disposition: String(fact.disposition ?? ""),
362
- rationale: String(fact.rationale ?? ""),
363
- }));
364
- }
365
- catch {
366
- selections = [];
367
- }
368
- if (selections.length === 0) {
369
- const legacy = await readLegacyOpenspecSelection({
370
- runDir: input.runDir,
371
- selectionNodeId,
372
- candidatePaths: input.candidatePaths,
373
- mandatoryPaths: input.mandatoryPaths,
374
- });
375
- if (!legacy.ok)
376
- return legacy;
377
- selections = legacy.selections;
378
- allowIrrelevant = true;
379
- }
380
- try {
381
- const byPath = new Map();
382
- for (const row of selections) {
383
- const legalDispositions = allowIrrelevant
384
- ? ["required", "relevant", "irrelevant"]
385
- : ["required", "relevant"];
386
- if (typeof row.path !== "string" ||
387
- !legalDispositions.includes(row.disposition) ||
388
- typeof row.rationale !== "string" ||
389
- !row.rationale.trim()) {
390
- throw new Error(`invalid selection row (path+${legalDispositions.join("|")}+non-empty rationale expected): ${JSON.stringify(row)}`);
355
+ /**
356
+ * Parses the deterministic openspec-citations fence from the effective plan
357
+ * output. Each non-empty line must be one JSON object {"path","section","line"}.
358
+ * Rows whose normalized path is not a supported openspec spec file are ignored
359
+ * (out of scope); rows that are invalid JSON or lack a string path mark the
360
+ * block unparseable (fail-closed when candidates are non-empty).
361
+ */
362
+ export function parseOpenspecCitations(planText, repoRoot) {
363
+ const citations = [];
364
+ let blockPresent = false;
365
+ let unparseable = false;
366
+ const fenceRe = /```openspec-citations[ \t]*\r?\n([\s\S]*?)\r?\n```/g;
367
+ let match;
368
+ while ((match = fenceRe.exec(planText)) !== null) {
369
+ blockPresent = true;
370
+ const body = match[1] ?? "";
371
+ for (const rawLine of body.split(/\r?\n/)) {
372
+ const line = rawLine.trim();
373
+ if (!line)
374
+ continue;
375
+ let parsed;
376
+ try {
377
+ parsed = JSON.parse(line);
391
378
  }
392
- if (!input.candidatePaths.includes(row.path)) {
393
- throw new Error(`invalid selection ${row.path}: not a candidate path`);
379
+ catch {
380
+ unparseable = true;
381
+ continue;
394
382
  }
395
- if (byPath.has(row.path))
396
- throw new Error(`duplicate selection ${row.path}`);
397
- byPath.set(row.path, row.disposition);
398
- }
399
- // Only explicitly required/relevant candidates must be read; everything
400
- // unmentioned defaults to irrelevant. mandatoryPaths are intrinsic:
401
- // they must be read even if the model omitted them, and they can never
402
- // be downgraded by a declaration.
403
- const mustRead = [
404
- ...input.mandatoryPaths,
405
- ...input.candidatePaths.filter((candidate) => byPath.get(candidate) === "required"),
406
- ];
407
- return { ok: true, paths: [...new Set(mustRead)].sort() };
408
- }
409
- catch (error) {
410
- return {
411
- ok: false,
412
- reason: `openspec selector invalid: ${error instanceof Error ? error.message : String(error)}`,
413
- };
414
- }
415
- }
416
- async function readCommittedContractFacts(runDir, nodeId) {
417
- const { readTypedEventStoreFromJsonl } = await import("./frontend-typed-event-store.js");
418
- const records = await readTypedEventStoreFromJsonl(path.join(runDir, nodeId, "contract-typed-facts.jsonl"));
419
- return records
420
- .filter((record) => record.phase === "committed")
421
- .map((record) => record.fact)
422
- .filter((fact) => Boolean(fact));
423
- }
424
- async function readLegacyOpenspecSelection(input) {
425
- let text;
426
- try {
427
- text = await readNodeText(input.runDir, input.selectionNodeId);
428
- }
429
- catch (error) {
430
- return {
431
- ok: false,
432
- reason: `openspec selector output unreadable: ${error instanceof Error ? error.message : String(error)}`,
433
- };
434
- }
435
- const fenced = /```json\s*\n([\s\S]*?)\n```/.exec(text);
436
- if (!fenced) {
437
- return {
438
- ok: false,
439
- reason: "openspec selector emitted neither typed openspec-selection facts nor a legacy fenced JSON selection object",
440
- };
441
- }
442
- try {
443
- const parsed = JSON.parse(fenced[1] ?? "");
444
- if (parsed.schemaVersion !== 1 ||
445
- parsed.schemaId !== "frontend-openspec-selection-v1" ||
446
- !Array.isArray(parsed.selections)) {
447
- throw new Error("schemaVersion/schemaId/selections invalid");
383
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
384
+ unparseable = true;
385
+ continue;
386
+ }
387
+ const rawPath = parsed.path;
388
+ if (typeof rawPath !== "string" || !rawPath.trim()) {
389
+ unparseable = true;
390
+ continue;
391
+ }
392
+ const normalized = normalizeCitationPath(rawPath, repoRoot);
393
+ if (!normalized || !isOpenspecSpecFilePath(normalized))
394
+ continue;
395
+ const section = typeof parsed.section === "string"
396
+ ? parsed.section
397
+ : "";
398
+ const lineValue = parsed.line;
399
+ const citationLine = typeof lineValue === "number" && Number.isInteger(lineValue)
400
+ ? lineValue
401
+ : null;
402
+ citations.push({ path: normalized, section, line: citationLine });
448
403
  }
449
- const rows = parsed.selections.map((row) => ({
450
- path: String(row.path ?? ""),
451
- disposition: String(row.disposition ?? ""),
452
- rationale: String(row.rationale ?? ""),
453
- }));
454
- return { ok: true, selections: rows };
455
- }
456
- catch (error) {
457
- return {
458
- ok: false,
459
- reason: `openspec selector invalid: ${error instanceof Error ? error.message : String(error)}`,
460
- };
461
404
  }
405
+ return { blockPresent, unparseable, citations };
462
406
  }
463
407
  async function finalizePrewrite(input, pending) {
464
408
  const sourceBindingSha256 = input.sourceBinding
@@ -509,7 +453,162 @@ async function finalizePrewrite(input, pending) {
509
453
  openspecPolicy: pending.openspecPolicy,
510
454
  };
511
455
  }
512
- export { checkFrontendSourceFidelityLedger } from "./frontend-source-fidelity-ledger.js";
456
+ /**
457
+ * Source fidelity ledger 门禁检查(AC-FIDELITY-002/003/006):绑定携带 ledger 指针时,
458
+ * 依次核验 ledger 存在、绑定 hash 一致、inputDigest 新鲜、无 unresolved/conflict/
459
+ * 高风险未覆盖、canonical requirement 全覆盖、来源引用有效且非伪造。任一失败 fail closed。
460
+ */
461
+ export async function checkFrontendSourceFidelityLedger(input) {
462
+ const { binding } = input;
463
+ // v1 绑定不携带 ledger 指针:非 frontend task / 无可解析引用 → 无 v2 校验路径。
464
+ if (!isDagSourceBindingV2(binding)) {
465
+ return { ok: true, failureCode: null, failureReason: null, ledger: null };
466
+ }
467
+ const absolute = resolveDagTaskSourcePath({
468
+ workspaceRoot: input.workspaceRoot,
469
+ taskId: binding.taskId,
470
+ sourcePath: binding.ledgerPath,
471
+ });
472
+ let raw;
473
+ try {
474
+ raw = await readFile(absolute, "utf8");
475
+ }
476
+ catch (error) {
477
+ return {
478
+ ok: false,
479
+ failureCode: "source-fidelity-ledger-missing",
480
+ failureReason: `frontend source fidelity ledger missing: ${binding.ledgerPath} (${error instanceof Error ? error.message : String(error)})`,
481
+ ledger: null,
482
+ };
483
+ }
484
+ const actualSha = createHash("sha256").update(raw).digest("hex");
485
+ if (actualSha !== binding.ledgerSha256) {
486
+ return {
487
+ ok: false,
488
+ failureCode: "source-fidelity-ledger-stale",
489
+ failureReason: `frontend source fidelity ledger stale or tampered: ${binding.ledgerPath} (bound ${binding.ledgerSha256}, observed ${actualSha})`,
490
+ ledger: null,
491
+ };
492
+ }
493
+ let ledger;
494
+ try {
495
+ ledger = parseLedgerJson(raw);
496
+ }
497
+ catch (error) {
498
+ return {
499
+ ok: false,
500
+ failureCode: "source-fidelity-ledger-stale",
501
+ failureReason: `frontend source fidelity ledger is not a valid v1 ledger: ${error instanceof Error ? error.message : String(error)}`,
502
+ ledger: null,
503
+ };
504
+ }
505
+ if (ledger.inputDigest !== binding.inputDigest) {
506
+ return {
507
+ ok: false,
508
+ failureCode: "source-fidelity-ledger-stale",
509
+ failureReason: `frontend source fidelity ledger inputDigest mismatch: bound ${binding.inputDigest}, ledger ${ledger.inputDigest}`,
510
+ ledger,
511
+ };
512
+ }
513
+ const validation = validateRequirementLedger(ledger);
514
+ const firstError = (code) => validation.find((error) => error.code === code)?.message ?? null;
515
+ if (ledger.stats.unresolvedCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_UNRESOLVED")) {
516
+ return {
517
+ ok: false,
518
+ failureCode: "source-fidelity-unresolved",
519
+ failureReason: firstError("SOURCE_FIDELITY_UNRESOLVED") ??
520
+ `frontend source fidelity ledger has ${ledger.stats.unresolvedCount} unresolved fragment(s)`,
521
+ ledger,
522
+ };
523
+ }
524
+ if (ledger.stats.conflictCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_CONFLICT")) {
525
+ return {
526
+ ok: false,
527
+ failureCode: "source-fidelity-conflict",
528
+ failureReason: firstError("SOURCE_FIDELITY_CONFLICT") ??
529
+ `frontend source fidelity ledger has ${ledger.stats.conflictCount} conflict(s)`,
530
+ ledger,
531
+ };
532
+ }
533
+ if (ledger.stats.highRiskUncoveredCount > 0 ||
534
+ validation.some((e) => e.code === "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED")) {
535
+ return {
536
+ ok: false,
537
+ failureCode: "source-fidelity-high-risk-uncovered",
538
+ failureReason: firstError("SOURCE_FIDELITY_HIGH_RISK_UNCOVERED") ??
539
+ `frontend source fidelity ledger has ${ledger.stats.highRiskUncoveredCount} high-risk uncovered fragment(s)`,
540
+ ledger,
541
+ };
542
+ }
543
+ // AC-BLOCK-001:canonical requirement 缺少 fragment/sourceRef 证据(ledger 侧
544
+ // REQUIREMENT_NOT_COVERED)与来源引用/片段无效(SOURCE_REF_INVALID /
545
+ // FRAGMENT_INVALID)均为阻断性失败,结构化失败结果不得产生可继续执行的成功。
546
+ const notCoveredError = validation.find((e) => e.code === "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED");
547
+ if (notCoveredError) {
548
+ return {
549
+ ok: false,
550
+ failureCode: "source-fidelity-requirement-not-covered",
551
+ failureReason: notCoveredError.message,
552
+ ledger,
553
+ };
554
+ }
555
+ const refInvalidError = validation.find((e) => e.code === "SOURCE_FIDELITY_SOURCE_REF_INVALID" ||
556
+ e.code === "SOURCE_FIDELITY_FRAGMENT_INVALID");
557
+ if (refInvalidError) {
558
+ return {
559
+ ok: false,
560
+ failureCode: "source-fidelity-source-ref-invalid",
561
+ failureReason: refInvalidError.message,
562
+ ledger,
563
+ };
564
+ }
565
+ const ledgerIds = new Set(ledger.canonicalRequirements.map((requirement) => requirement.id));
566
+ const contractIds = new Set(input.contractRequirements.map((r) => r.id));
567
+ const uncoveredRequirements = [...ledgerIds].filter((id) => !contractIds.has(id));
568
+ if (uncoveredRequirements.length > 0) {
569
+ return {
570
+ ok: false,
571
+ failureCode: "source-fidelity-requirement-not-covered",
572
+ failureReason: `frontend contract requirements do not cover ledger canonical requirement ids: ${uncoveredRequirements.join(", ")}`,
573
+ ledger,
574
+ };
575
+ }
576
+ const fragmentById = new Map(ledger.fragments.map((fragment) => [fragment.id, fragment]));
577
+ for (const requirement of input.contractRequirements) {
578
+ if (!ledgerIds.has(requirement.id))
579
+ continue;
580
+ const declared = requirement.sourceFragmentIds ?? [];
581
+ if (declared.length === 0) {
582
+ return {
583
+ ok: false,
584
+ failureCode: "source-fidelity-source-ref-invalid",
585
+ failureReason: `frontend contract requirement ${requirement.id} lacks sourceFragmentIds for the bound ledger`,
586
+ ledger,
587
+ };
588
+ }
589
+ const boundFragments = new Set(ledger.canonicalRequirements.find((r) => r.id === requirement.id)
590
+ ?.sourceFragmentIds ?? []);
591
+ for (const fragmentId of declared) {
592
+ if (!fragmentById.has(fragmentId)) {
593
+ return {
594
+ ok: false,
595
+ failureCode: "source-fidelity-source-ref-invalid",
596
+ failureReason: `frontend contract requirement ${requirement.id} references unknown fragment ${fragmentId}`,
597
+ ledger,
598
+ };
599
+ }
600
+ if (!boundFragments.has(fragmentId)) {
601
+ return {
602
+ ok: false,
603
+ failureCode: "source-fidelity-source-ref-invalid",
604
+ failureReason: `frontend contract requirement ${requirement.id} declares fabricated fragment reference ${fragmentId} (not bound in ledger)`,
605
+ ledger,
606
+ };
607
+ }
608
+ }
609
+ }
610
+ return { ok: true, failureCode: null, failureReason: null, ledger };
611
+ }
513
612
  export async function runFrontendPrewriteGate(input) {
514
613
  const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
515
614
  // selectNode throws for a scheduling defect (no FINISHED plan/review). That
@@ -558,6 +657,9 @@ export async function runFrontendPrewriteGate(input) {
558
657
  openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
559
658
  openspecPolicy,
560
659
  };
660
+ const planText = [planArtifact.contractText, planArtifact.citationsText]
661
+ .filter(Boolean)
662
+ .join("\n");
561
663
  let reviewText;
562
664
  try {
563
665
  reviewText = await readNodeText(input.runDir, reviewNodeId);
@@ -681,6 +783,21 @@ export async function runFrontendPrewriteGate(input) {
681
783
  });
682
784
  }
683
785
  }
786
+ // Size guard: a canonical contract above 64KB is a retryable-invalid defect
787
+ // (compress the contract) rather than a writer authorization failure.
788
+ const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
789
+ if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
790
+ return finalizePrewrite(input, {
791
+ ...basePending,
792
+ verdict,
793
+ candidateJsonSha256: analysis.candidateJsonSha256,
794
+ normalizationActions: [],
795
+ mockStrategy: analysis.canonical.mockApi.strategy,
796
+ classification: "retryable-invalid",
797
+ failureReason: `frontend prewrite gate: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit. 压缩契约(移除冗余 prose、重复目标或过长描述)后重试。`,
798
+ failureCode: "contract-too-large",
799
+ });
800
+ }
684
801
  const mockStrategy = analysis.canonical.mockApi.strategy;
685
802
  if (!input.config.allowedMockStrategies.includes(mockStrategy)) {
686
803
  const quotedAllowed = input.config.allowedMockStrategies
@@ -752,31 +869,14 @@ export async function runFrontendPrewriteGate(input) {
752
869
  // Effective plan/review must actually read the candidate openspec paths when
753
870
  // any exist. This fail-closes BEFORE the canonical contract is materialized
754
871
  // so a writer is never authorized on top of unread normative evidence.
755
- const frozenCandidatePaths = input.config.openspecCandidatePaths ?? [];
756
- const selection = await resolveRequiredOpenspecPaths({
757
- runDir: input.runDir,
758
- selectionNodeId: input.config.openspecSelectionNodeId,
759
- candidatePaths: frozenCandidatePaths,
760
- mandatoryPaths: input.config.openspecMandatoryPaths ?? [],
761
- policy: openspecPolicy,
762
- });
763
- if (!selection.ok) {
764
- return finalizePrewrite(input, {
765
- ...basePending, verdict, candidateJsonSha256: analysis.candidateJsonSha256,
766
- normalizationActions: [], mockStrategy, classification: "retryable-invalid",
767
- failureReason: `frontend prewrite gate: ${selection.reason}`,
768
- failureCode: "openspec-selection-invalid",
769
- openspecCandidatePaths: frozenCandidatePaths,
770
- });
771
- }
772
- const candidatePaths = selection.paths;
872
+ const candidatePaths = input.config.openspecCandidatePaths ?? [];
773
873
  const candidateRoot = workspaceRoot ?? process.cwd();
774
874
  // Drift precheck: candidates are frozen at generation time. A candidate that
775
875
  // no longer exists on disk was deleted/renamed after generation and is a
776
876
  // generation-contract defect, not an "unread" failure.
777
877
  const missingCandidates = [];
778
- if (frozenCandidatePaths.length > 0) {
779
- for (const candidate of frozenCandidatePaths) {
878
+ if (candidatePaths.length > 0) {
879
+ for (const candidate of candidatePaths) {
780
880
  try {
781
881
  await stat(path.join(candidateRoot, candidate));
782
882
  }
@@ -799,78 +899,99 @@ export async function runFrontendPrewriteGate(input) {
799
899
  openspecCandidatePaths: candidatePaths,
800
900
  });
801
901
  }
802
- const frozenSnapshots = input.config.openspecCandidateSnapshots;
803
- if (frozenSnapshots) {
804
- const snapshotByPath = new Map(frozenSnapshots.map((snapshot) => [snapshot.path, snapshot.sha256]));
805
- const incompleteSnapshots = frozenCandidatePaths.filter((candidate) => !snapshotByPath.has(candidate));
806
- const unexpectedSnapshots = frozenSnapshots
807
- .map((snapshot) => snapshot.path)
808
- .filter((candidate) => !frozenCandidatePaths.includes(candidate));
809
- const changedCandidates = [];
810
- for (const candidate of frozenCandidatePaths) {
811
- const expected = snapshotByPath.get(candidate);
812
- if (!expected)
813
- continue;
814
- const actual = createHash("sha256")
815
- .update(await readFile(path.join(candidateRoot, candidate)))
816
- .digest("hex");
817
- if (actual !== expected)
818
- changedCandidates.push(candidate);
819
- }
820
- if (incompleteSnapshots.length > 0 ||
821
- unexpectedSnapshots.length > 0 ||
822
- changedCandidates.length > 0) {
823
- const details = [
824
- ...(incompleteSnapshots.length > 0
825
- ? [`missing snapshots: ${incompleteSnapshots.join(", ")}`]
826
- : []),
827
- ...(unexpectedSnapshots.length > 0
828
- ? [`unexpected snapshots: ${unexpectedSnapshots.join(", ")}`]
829
- : []),
830
- ...(changedCandidates.length > 0
831
- ? [`content changed: ${changedCandidates.join(", ")}`]
832
- : []),
833
- ].join("; ");
834
- return finalizePrewrite(input, {
835
- ...basePending,
836
- verdict,
837
- candidateJsonSha256: analysis.candidateJsonSha256,
838
- normalizationActions: [],
839
- mockStrategy,
840
- classification: "blocked",
841
- failureReason: `frontend prewrite gate blocked: the generation-frozen OpenSpec content snapshot no longer matches the workspace (${details}). Re-read the changed specification and regenerate the DAG.`,
842
- failureCode: "candidate-content-drift",
843
- openspecReadPaths: [],
844
- openspecCandidatePaths: candidatePaths,
845
- });
902
+ let openspecReadPaths;
903
+ if (openspecPolicy === "cited") {
904
+ openspecReadPaths = await collectOpenspecReadPaths({
905
+ runDir: input.runDir,
906
+ planNodeId,
907
+ reviewNodeId,
908
+ repoRoot: candidateRoot,
909
+ });
910
+ if (candidatePaths.length > 0) {
911
+ const parsed = parseOpenspecCitations(planText, candidateRoot);
912
+ if (!parsed.blockPresent || parsed.unparseable) {
913
+ return finalizePrewrite(input, {
914
+ ...basePending,
915
+ verdict,
916
+ candidateJsonSha256: analysis.candidateJsonSha256,
917
+ normalizationActions: [],
918
+ mockStrategy,
919
+ classification: "retryable-invalid",
920
+ failureReason: `frontend prewrite gate: the effective plan output must end with a parseable \`\`\`openspec-citations fenced block (one JSON {"path","section","line"} per line). ${parsed.blockPresent
921
+ ? "The block is present but contains unparseable lines."
922
+ : "No openspec-citations block was found."}`,
923
+ failureCode: "openspec-citation-block-unparseable",
924
+ openspecReadPaths,
925
+ openspecCandidatePaths: candidatePaths,
926
+ });
927
+ }
928
+ const citedPaths = new Set(parsed.citations.map((citation) => citation.path));
929
+ const notCited = candidatePaths.filter((candidate) => !citedPaths.has(candidate));
930
+ if (notCited.length > 0) {
931
+ return finalizePrewrite(input, {
932
+ ...basePending,
933
+ verdict,
934
+ candidateJsonSha256: analysis.candidateJsonSha256,
935
+ normalizationActions: [],
936
+ mockStrategy,
937
+ classification: "retryable-invalid",
938
+ failureReason: `frontend prewrite gate: these openspec candidate paths were not cited in the plan openspec-citations block: ${notCited.join(", ")}`,
939
+ failureCode: "openspec-not-cited",
940
+ openspecReadPaths,
941
+ openspecCandidatePaths: candidatePaths,
942
+ });
943
+ }
944
+ const readSet = new Set(openspecReadPaths);
945
+ const uncorroborated = [
946
+ ...new Set(parsed.citations
947
+ .filter((citation) => !readSet.has(citation.path))
948
+ .map((citation) => citation.path)),
949
+ ];
950
+ if (uncorroborated.length > 0) {
951
+ return finalizePrewrite(input, {
952
+ ...basePending,
953
+ verdict,
954
+ candidateJsonSha256: analysis.candidateJsonSha256,
955
+ normalizationActions: [],
956
+ mockStrategy,
957
+ classification: "retryable-invalid",
958
+ failureReason: `frontend prewrite gate: these openspec paths were cited in the plan but have no successful read event in the effective plan/review: ${uncorroborated.join(", ")}`,
959
+ failureCode: "openspec-citation-not-read",
960
+ openspecReadPaths,
961
+ openspecCandidatePaths: candidatePaths,
962
+ });
963
+ }
846
964
  }
965
+ // cited with empty candidates: no read enforcement. The generation step
966
+ // emits an advisory instead of failing the gate.
847
967
  }
848
- let openspecReadPaths;
849
- openspecReadPaths = await collectOpenspecReadPaths({
850
- runDir: input.runDir,
851
- planNodeId,
852
- reviewNodeId,
853
- repoRoot: candidateRoot,
854
- ...(candidatePaths.length > 0 ? { candidatePaths } : {}),
855
- });
856
- if (candidatePaths.length > 0) {
857
- const readSet = new Set(openspecReadPaths);
858
- const unread = candidatePaths.filter((candidate) => !readSet.has(candidate));
859
- if (unread.length > 0) {
860
- const governanceRoot = await resolveOpenspecGovernanceRoot(candidateRoot);
861
- const excludedGovernanceCount = await countExcludedGovernanceSpecFiles(candidateRoot, governanceRoot);
862
- return finalizePrewrite(input, {
863
- ...basePending,
864
- verdict,
865
- candidateJsonSha256: analysis.candidateJsonSha256,
866
- normalizationActions: [],
867
- mockStrategy,
868
- classification: "retryable-invalid",
869
- failureReason: `frontend prewrite gate: effective plan/review did not read these openspec candidate paths: ${unread.join(", ")}. Read each candidate and report applicable rules plus hit path/section/line number, conflicts, and missing specifications, then rerun. 实际成功读取: ${openspecReadPaths.length > 0 ? openspecReadPaths.join(", ") : "(none)"}; 候选总数: ${candidatePaths.length}; 排除治理目录 ${governanceRoot} 下 ${excludedGovernanceCount} 个文件(运行期诊断口径); 候选集在生成期冻结,若候选与本任务无关属于生成期契约配置问题,应修正后重新生成 DAG。`,
870
- failureCode: "openspec-not-read",
871
- openspecReadPaths,
872
- openspecCandidatePaths: candidatePaths,
873
- });
968
+ else {
969
+ openspecReadPaths = await checkOpenspecReadEvidence({
970
+ runDir: input.runDir,
971
+ candidatePaths,
972
+ planNodeId,
973
+ reviewNodeId,
974
+ repoRoot: candidateRoot,
975
+ });
976
+ if (candidatePaths.length > 0) {
977
+ const readSet = new Set(openspecReadPaths);
978
+ const unread = candidatePaths.filter((candidate) => !readSet.has(candidate));
979
+ if (unread.length > 0) {
980
+ const governanceRoot = await resolveOpenspecGovernanceRoot(candidateRoot);
981
+ const excludedGovernanceCount = await countExcludedGovernanceSpecFiles(candidateRoot, governanceRoot);
982
+ return finalizePrewrite(input, {
983
+ ...basePending,
984
+ verdict,
985
+ candidateJsonSha256: analysis.candidateJsonSha256,
986
+ normalizationActions: [],
987
+ mockStrategy,
988
+ classification: "retryable-invalid",
989
+ failureReason: `frontend prewrite gate: effective plan/review did not read these openspec candidate paths: ${unread.join(", ")}. Read each candidate and report applicable rules plus hit path/section/line number, conflicts, and missing specifications, then rerun. 实际成功读取: ${openspecReadPaths.length > 0 ? openspecReadPaths.join(", ") : "(none)"}; 候选总数: ${candidatePaths.length}; 排除治理目录 ${governanceRoot} 下 ${excludedGovernanceCount} 个文件(运行期诊断口径); 候选集在生成期冻结,若候选与本任务无关属于生成期契约配置问题,应修正后重新生成 DAG。`,
990
+ failureCode: "openspec-not-read",
991
+ openspecReadPaths,
992
+ openspecCandidatePaths: candidatePaths,
993
+ });
994
+ }
874
995
  }
875
996
  }
876
997
  // Component selection spec conformance (ADR 0016). Deterministic cross-check
@@ -892,17 +1013,13 @@ export async function runFrontendPrewriteGate(input) {
892
1013
  normalizationActions: [],
893
1014
  mockStrategy,
894
1015
  classification: "retryable-invalid",
895
- failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new);specified 必须给 OpenSpec specReference.path/section/line,new 必须给任务源 PRD specReference.path/section/line。",
1016
+ failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new),specified 必须给 specReference.path/section/line,new 必须给 rationale 声明偏差。",
896
1017
  failureCode: "component-choices-missing",
897
1018
  openspecReadPaths,
898
1019
  openspecCandidatePaths: candidatePaths,
899
1020
  });
900
1021
  }
901
- // Only `specified` choices cite the frozen OpenSpec candidate bucket. A
902
- // `new` choice instead cites its task-source/PRD declaration and must not be
903
- // rejected merely because that source is outside the OpenSpec roots.
904
1022
  const componentSpecReferences = componentChoices
905
- .filter((choice) => choice.decision === "specified")
906
1023
  .map((choice) => ({
907
1024
  raw: choice.specReference?.path ?? "",
908
1025
  normalized: choice.specReference?.path
@@ -910,37 +1027,11 @@ export async function runFrontendPrewriteGate(input) {
910
1027
  : null,
911
1028
  }))
912
1029
  .filter((ref) => ref.raw.length > 0);
913
- if (componentSpecCandidates.length > 0 && hasUiVisibleWork) {
914
- const componentCandidateSet = new Set(componentSpecCandidates);
915
- const hasBoundSpecifiedChoice = componentChoices.some((choice) => {
916
- if (choice.decision !== "specified" || !choice.specReference?.path) {
917
- return false;
918
- }
919
- const normalized = normalizeCitationPath(choice.specReference.path, candidateRoot);
920
- return Boolean(normalized && componentCandidateSet.has(normalized));
921
- });
922
- if (!hasBoundSpecifiedChoice) {
923
- return finalizePrewrite(input, {
924
- ...basePending,
925
- verdict,
926
- candidateJsonSha256: analysis.candidateJsonSha256,
927
- normalizationActions: [],
928
- mockStrategy,
929
- classification: "retryable-invalid",
930
- failureReason: "frontend prewrite gate: UI-visible work has a generation-frozen component/theme specification bucket, but no uiComponentChoices decision=specified is bound to a path in that bucket. At least one declared UI purpose must use and cite the applicable component/theme specification; reuse-existing/new alone cannot bypass the bucket.",
931
- failureCode: "component-spec-choice-unbound",
932
- openspecReadPaths,
933
- openspecCandidatePaths: candidatePaths,
934
- });
935
- }
936
- }
937
1030
  if (componentSpecReferences.length > 0) {
938
1031
  const invalidRefs = [
939
1032
  ...new Set(componentSpecReferences
940
1033
  .filter((ref) => !ref.normalized ||
941
- !(candidatePaths.length > 0
942
- ? candidatePaths.includes(ref.normalized)
943
- : isOpenspecSpecFilePath(ref.normalized)))
1034
+ !isOpenspecSpecFilePath(ref.normalized))
944
1035
  .map((ref) => ref.raw)),
945
1036
  ];
946
1037
  if (invalidRefs.length > 0) {
@@ -958,10 +1049,13 @@ export async function runFrontendPrewriteGate(input) {
958
1049
  });
959
1050
  }
960
1051
  if (openspecPolicy === "cited") {
1052
+ const parsedCitations = parseOpenspecCitations(planText, candidateRoot);
1053
+ const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
961
1054
  const readSet = new Set(openspecReadPaths);
962
1055
  const uncorroborated = [
963
1056
  ...new Set(componentSpecReferences
964
1057
  .filter((ref) => !ref.normalized ||
1058
+ !citedPaths.has(ref.normalized) ||
965
1059
  !readSet.has(ref.normalized))
966
1060
  .map((ref) => ref.raw)),
967
1061
  ];
@@ -973,7 +1067,7 @@ export async function runFrontendPrewriteGate(input) {
973
1067
  normalizationActions: [],
974
1068
  mockStrategy,
975
1069
  classification: "retryable-invalid",
976
- failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths are missing a ledger specReference or lack a successful read event: ${uncorroborated.join(", ")}`,
1070
+ failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths were not cited in the plan openspec-citations block or lack a successful read event: ${uncorroborated.join(", ")}`,
977
1071
  failureCode: "component-spec-not-cited",
978
1072
  openspecReadPaths,
979
1073
  openspecCandidatePaths: candidatePaths,