@tea-agent/loop-agent 0.42.0-next.1 → 0.42.0-next.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +321 -1
  3. package/dist/application/dag/generate-task-dag.js +75 -19
  4. package/dist/application/dag/run-dag.js +41 -0
  5. package/dist/application/task-lifecycle/advance.js +24 -5
  6. package/dist/application/task-lifecycle/observe.js +171 -17
  7. package/dist/application/task-lifecycle/plan-transitions.js +42 -7
  8. package/dist/application/task-lifecycle/recommendations.js +13 -2
  9. package/dist/build-stamp.json +3 -3
  10. package/dist/cli/command-definitions.js +7 -0
  11. package/dist/cli/program.js +6 -1
  12. package/dist/commands/client-recovery.js +3 -0
  13. package/dist/commands/dag-follow-up.js +138 -0
  14. package/dist/commands/dag-rerun-task.js +2 -0
  15. package/dist/commands/init.js +27 -1
  16. package/dist/commands/task-advance.js +19 -0
  17. package/dist/executors/dag-pi-executor.js +4010 -78
  18. package/dist/executors/pi-executor.js +15 -4
  19. package/dist/executors/pi-extension-resolver.js +14 -2
  20. package/dist/executors/pi-read-budget-policy.js +239 -0
  21. package/dist/executors/shell-executor.js +978 -188
  22. package/dist/executors/shell-write-guard.js +7 -0
  23. package/dist/infrastructure/console/operation-store.js +226 -6
  24. package/dist/shared/dag-failure-category.js +12 -0
  25. package/dist/shared/openspec-spec.js +70 -4
  26. package/dist/shared/operator/capabilities.js +7 -0
  27. package/dist/task/config-types.js +105 -5
  28. package/dist/task/contract/adopt.js +4 -0
  29. package/dist/task/contract/import-revision.js +4 -0
  30. package/dist/task/contract/project.js +3 -0
  31. package/dist/task/contract/schema.js +2 -1
  32. package/dist/task/frontend-project-capability.js +203 -20
  33. package/dist/task/runtime.js +5 -2
  34. package/dist/task/source-prepare/build-draft.js +3 -3
  35. package/dist/task/source-prepare/fragment-inventory.js +64 -25
  36. package/dist/task/source-prepare/prepare.js +126 -21
  37. package/dist/task/source-prepare/semantic-intake.js +6 -2
  38. package/dist/task/source-references.js +22 -1
  39. package/dist/worker/console/chat/chat-ui-policy.js +4 -2
  40. package/dist/worker/console/chat/model-resolver.js +7 -4
  41. package/dist/worker/console/chat/pi-runtime.js +258 -11
  42. package/dist/worker/console/chat/repo-browser.js +10 -3
  43. package/dist/worker/console/chat/routes.js +165 -71
  44. package/dist/worker/console/chat/sdd-data-alignment.js +222 -0
  45. package/dist/worker/console/chat/session-catalog.js +2 -0
  46. package/dist/worker/console/chat/session-store.js +159 -70
  47. package/dist/worker/console/chat/shortcuts.js +19 -3
  48. package/dist/worker/console/chat/turn-process.js +111 -56
  49. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  50. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  51. package/dist/worker/console/index.js +3 -0
  52. package/dist/worker/console/operator-actions.js +169 -7
  53. package/dist/worker/console/pi-readiness.js +3 -2
  54. package/dist/worker/console/server.js +23 -1
  55. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CcS17TBr.js} +1 -1
  56. package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-COptKq2S.js} +1 -1
  57. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-h4LKMHjP.js} +1 -1
  58. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-COA1MOH0.js} +1 -1
  59. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-DJUf0QPm.js} +1 -1
  60. package/dist/worker/console/static/assets/channel-DdBCaOJ6.js +1 -0
  61. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-mtWfKrUX.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-B8pXsxDQ.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-ipKzByl1.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-DYi3Ald_.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DQtGYoty.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-BU0qS2YV.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-DVH9dIGE.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-Dt60SmgM.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-BoaUGuEY.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-_WHFDTp2.js} +1 -1
  71. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DZFra1GO.js +1 -0
  72. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DZFra1GO.js +1 -0
  73. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-BxkbTIRd.js} +1 -1
  74. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-Bf2UVnoG.js} +1 -1
  75. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-Bo23q1J_.js} +1 -1
  76. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-Dd_UU49i.js} +1 -1
  77. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-ebUa1a9y.js} +1 -1
  78. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-BucthU8r.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-BgqNpAkm.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-DC4q5NGa.js} +1 -1
  81. package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-CBqCaMeb.js} +1 -1
  82. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-CGtnbQ3-.js} +1 -1
  83. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-CG3LUao5.js} +1 -1
  84. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-D2ZVgoFS.js} +1 -1
  85. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DoYFZiKt.js} +1 -1
  86. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-CKoP1s6j.js} +1 -1
  87. package/dist/worker/console/static/assets/index-CAZ2fC_X.css +1 -0
  88. package/dist/worker/console/static/assets/index-vbTcFnFs.js +449 -0
  89. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-Duofv8p2.js} +1 -1
  90. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-D2nlCkA1.js} +1 -1
  91. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-Dd4IHdus.js} +1 -1
  92. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-Bi20AOdt.js} +1 -1
  93. package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-D59YJ9kB.js} +1 -1
  94. package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-Bl13LpNa.js} +5 -5
  95. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-C6DLP5eY.js} +1 -1
  96. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-BjO4cy64.js} +1 -1
  97. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-D7i2qZTG.js} +1 -1
  98. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-Dx8o_h0y.js} +1 -1
  99. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-B55orI_t.js} +1 -1
  100. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-zWaehp6f.js} +1 -1
  101. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-CcA-pjvD.js} +1 -1
  102. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-BOFbzNHI.js} +1 -1
  103. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-BjAejah1.js} +1 -1
  104. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-DXYwxJwZ.js} +1 -1
  105. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Clg3V9t1.js +1 -0
  106. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-OYiOch8n.js} +2 -2
  107. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DX1dxAAW.js +8 -0
  108. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-cZfH3nmU.js} +1 -1
  109. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-BDE7b3E1.js} +1 -1
  110. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-kyyGy9WJ.js} +1 -1
  111. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CJj6VTog.js} +1 -1
  112. package/dist/worker/console/static/index.html +2 -2
  113. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  114. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
  115. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
  116. package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
  117. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
  118. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
  119. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +258 -23
  120. package/dist/worker/console/static-src/operator-chat/useChatThread.js +112 -4
  121. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
  122. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
  123. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
  124. package/dist/worker/console/static-src/shell/console-update-reload.js +36 -9
  125. package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
  126. package/dist/worker/console/workspace-context.js +115 -1
  127. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  128. package/dist/worker/observe/routes.js +4 -0
  129. package/dist/worker/observe/static/constants.js +22 -22
  130. package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
  131. package/dist/worker/observe/static/dag-helpers.js +4 -2
  132. package/dist/worker/observe/static/dag-history-labels.js +1 -0
  133. package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
  134. package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
  135. package/dist/worker/observe/static/dag-node-purpose.js +13 -8
  136. package/dist/worker/observe/static/index.html +4 -4
  137. package/dist/worker/observe/static/inspect-workspace.js +23 -0
  138. package/dist/worker/observe/static/kpi.js +2 -2
  139. package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
  140. package/dist/worker/observe/static/operator-chrome.js +37 -23
  141. package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
  142. package/dist/worker/observe/static/relations.js +7 -7
  143. package/dist/worker/observe/static/router.d.ts +12 -1
  144. package/dist/worker/observe/static/router.js +48 -6
  145. package/dist/worker/observe/static/run-processing.js +4 -2
  146. package/dist/worker/observe/static/shell-chrome.js +2 -2
  147. package/dist/worker/observe/static/state.js +5 -0
  148. package/dist/worker/observe/static/styles.css +231 -0
  149. package/dist/worker/observe/static/views/dag-graph.js +1 -1
  150. package/dist/worker/observe/static/views/dag-inspector.js +336 -181
  151. package/dist/worker/observe/static/views/dag.js +26 -14
  152. package/dist/worker/observe/static/views/dags.js +3 -1
  153. package/dist/worker/observe/static/views/dashboard.js +14 -6
  154. package/dist/worker/observe/static/views/pool.js +7 -2
  155. package/dist/worker/observe/static/views/run.js +12 -3
  156. package/dist/worker/observe/static/views/session-timeline.js +74 -13
  157. package/dist/worker/observe/static/views/task.js +7 -2
  158. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +215 -28
  159. package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
  160. package/dist/workflows/dag/backend-test-plan-protocol.js +210 -0
  161. package/dist/workflows/dag/backend-test-scenario-param.js +172 -25
  162. package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
  163. package/dist/workflows/dag/backend-test-writer-completeness.js +104 -12
  164. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  165. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  166. package/dist/workflows/dag/frontend-closeout.js +221 -0
  167. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  168. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  169. package/dist/workflows/dag/frontend-implementation-contract.js +1237 -192
  170. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  171. package/dist/workflows/dag/frontend-prewrite-gate.js +256 -350
  172. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  173. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  174. package/dist/workflows/dag/frontend-recovery-controller.js +226 -0
  175. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  176. package/dist/workflows/dag/frontend-recovery-plan.js +21 -10
  177. package/dist/workflows/dag/frontend-recovery-run.js +166 -34
  178. package/dist/workflows/dag/frontend-repair.js +1 -432
  179. package/dist/workflows/dag/frontend-review-context.js +261 -15
  180. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  181. package/dist/workflows/dag/frontend-shadow-dual-write.js +941 -0
  182. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  183. package/dist/workflows/dag/frontend-shape-facts.js +419 -0
  184. package/dist/workflows/dag/frontend-shape.js +435 -0
  185. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  186. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  187. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  188. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  189. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  190. package/dist/workflows/dag/frontend-verification-trace.js +252 -25
  191. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  192. package/dist/workflows/dag/frontend-writer-admission.js +319 -0
  193. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  194. package/dist/workflows/dag/init-hybrid.js +1031 -564
  195. package/dist/workflows/dag/interrupt-request.js +7 -0
  196. package/dist/workflows/dag/node-execution.js +854 -4
  197. package/dist/workflows/dag/prompt.js +70 -3
  198. package/dist/workflows/dag/recovery-lease.js +170 -0
  199. package/dist/workflows/dag/report.js +37 -1
  200. package/dist/workflows/dag/rerun-feedback.js +315 -1
  201. package/dist/workflows/dag/rerun-plan.js +17 -6
  202. package/dist/workflows/dag/rerun-run.js +44 -6
  203. package/dist/workflows/dag/rerun-task.js +251 -13
  204. package/dist/workflows/dag/retry-policy.js +230 -104
  205. package/dist/workflows/dag/runner.js +580 -124
  206. package/dist/workflows/dag/scheduler.js +133 -20
  207. package/dist/workflows/dag/types.js +244 -16
  208. package/dist/workflows/dag/validate.js +28 -12
  209. package/docs/examples/README.md +5 -0
  210. package/docs/init-surface.manifest.json +30 -12
  211. package/docs/skills/vetted-skill-registry.md +4 -2
  212. package/docs/templates/README.md +2 -0
  213. package/docs/templates/agent-dag-report.schema.json +8 -2
  214. package/docs/templates/agent-dag.schema.json +1 -1
  215. package/docs/templates/backend-test-dag.json +25 -20
  216. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  217. package/docs/templates/frontend-implementation-dag.json +89 -0
  218. package/docs/templates/spec-registry.schema.json +45 -0
  219. package/harness.json +1 -1
  220. package/package.json +4 -3
  221. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  222. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  223. package/skills/frontend-contract/SKILL.md +23 -0
  224. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  225. package/skills/frontend-design-review/SKILL.md +22 -41
  226. package/skills/frontend-plan/SKILL.md +26 -0
  227. package/skills/frontend-plan/references/decision-contract.md +37 -0
  228. package/skills/frontend-plan/references/design-decisions.md +17 -0
  229. package/skills/frontend-review/SKILL.md +20 -15
  230. package/skills/frontend-review/references/review-findings.md +6 -7
  231. package/skills/frontend-scout/SKILL.md +25 -0
  232. package/skills/frontend-scout/references/design-evidence.md +16 -0
  233. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  234. package/skills/frontend-verification/SKILL.md +1 -1
  235. package/skills/loop-agent/references/command-reference.md +1 -0
  236. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  237. package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
  238. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
  239. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
  240. package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
  241. package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
  242. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
  243. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
  244. package/skills/frontend-implementation/SKILL.md +0 -52
  245. package/skills/frontend-implementation/references/code-standards.md +0 -33
  246. package/skills/frontend-implementation/references/design-spec.md +0 -56
  247. package/skills/frontend-implementation/references/node-contracts.md +0 -31
@@ -124,6 +124,13 @@ export function isEphemeralToolCachePath(filePath) {
124
124
  if (normalized === ".coverage" || normalized.startsWith(".coverage.")) {
125
125
  return true;
126
126
  }
127
+ // vite/vitest runtime caches live under node_modules/.vite (e.g.
128
+ // node_modules/.vite/vitest/results.json). node_modules is the gitignored
129
+ // dependency tree — tool cache writes there are never a writer's product
130
+ // change and must not trip the write guard.
131
+ if (normalized.startsWith("node_modules/.vite/")) {
132
+ return true;
133
+ }
127
134
  if (/(?:^|\/)__pycache__(?:\/|$)/.test(normalized))
128
135
  return true;
129
136
  if (/\.(?:pyc|pyo)$/.test(normalized))
@@ -1,5 +1,9 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import path from "node:path";
3
+ import { assertFrontendSplitProjectionSuccessor, frontendSplitProjectionSchema, } from "../../workflows/dag/frontend-split-application-service.js";
4
+ import { buildFrontendHumanDecisionProjection, consumeFrontendHumanDecision, materializeFrontendHumanDecision, parseFrontendHumanDecisionProjection, parseFrontendHumanDecisionReceipt, } from "../../workflows/dag/frontend-human-decision.js";
5
+ import { buildFrontendSplitAggregateCloseout, computeFrontendSplitDigest, finalizeFrontendSplitAggregate, frontendCommittedFactRefSchema, frontendSplitChildCloseoutFactsSchema, initializeFrontendSplitParent, } from "../../workflows/dag/frontend-split-orchestrator.js";
6
+ import { frontendCloseoutFactsSchema, frontendTestHandoffSchema, } from "../../workflows/dag/frontend-closeout.js";
3
7
  import { listJsonFiles, readJsonIfExists, sha256Json, writeSecureJson, } from "./app-data.js";
4
8
  export const TERMINAL_OPERATION_STATES = new Set(["succeeded", "failed", "timed-out", "restarting"]);
5
9
  function operationFile(appData, operationId) {
@@ -15,16 +19,21 @@ export function computeActionPayloadHash(action, actionParams) {
15
19
  export function newOperationId() {
16
20
  return `op_${randomBytes(12).toString("hex")}`;
17
21
  }
22
+ const sharedWriteChains = new Map();
18
23
  export class OperationStore {
19
24
  appData;
20
- /**
21
- * Per-key write lock. Concurrent update/save/create on the same operation
22
- * or clientRequestId must not race rename() into the same JSON path
23
- * (Windows EPERM) or last-write-wins lost patches.
24
- */
25
- writeChains = new Map();
25
+ /** Shared per-store-root, per-key write lock. Multiple OperationStore instances
26
+ * in one worker process must observe the same CAS critical section. */
27
+ writeChains;
26
28
  constructor(appData) {
27
29
  this.appData = appData;
30
+ const root = path.resolve(appData.operations);
31
+ let chains = sharedWriteChains.get(root);
32
+ if (!chains) {
33
+ chains = new Map();
34
+ sharedWriteChains.set(root, chains);
35
+ }
36
+ this.writeChains = chains;
28
37
  }
29
38
  serialize(key, op) {
30
39
  const prev = this.writeChains.get(key) ?? Promise.resolve();
@@ -66,6 +75,7 @@ export class OperationStore {
66
75
  payloadHash,
67
76
  state: "queued",
68
77
  createdAt: now,
78
+ revision: 1,
69
79
  taskId: input.taskId,
70
80
  cliArgs: input.cliArgs,
71
81
  ...(input.externalCommand
@@ -77,11 +87,61 @@ export class OperationStore {
77
87
  return { kind: "created", operation };
78
88
  });
79
89
  }
90
+ /**
91
+ * Idempotently materialize a split child/integration operation with a
92
+ * caller-specified deterministic operationId (from the split manifest).
93
+ * Unlike create() (random op_<hex> id) and save/update (no create), this is
94
+ * the durable cross-restart idempotency boundary for the production
95
+ * FrontendSplitTaskMaterializer backend: an existing operation file with the
96
+ * same taskId + payloadHash is returned as {created:false}; any drift fails
97
+ * closed instead of overwriting or silently diverging.
98
+ */
99
+ async ensureSplitMaterializedOperation(input) {
100
+ return this.serialize(input.operationId, async () => {
101
+ const existing = await this.get(input.operationId);
102
+ if (existing) {
103
+ if (existing.taskId !== input.taskId ||
104
+ existing.payloadHash !== input.payloadHash) {
105
+ throw new Error("frontend split operation materialization payload drift");
106
+ }
107
+ return { operation: existing, created: false };
108
+ }
109
+ const now = new Date().toISOString();
110
+ const operation = {
111
+ schemaVersion: 1,
112
+ operationId: input.operationId,
113
+ clientRequestId: `split:${input.operationId}`,
114
+ action: input.action,
115
+ actionParams: input.actionParams,
116
+ payloadHash: input.payloadHash,
117
+ state: "queued",
118
+ createdAt: now,
119
+ revision: 1,
120
+ taskId: input.taskId,
121
+ ...(input.cliArgs ? { cliArgs: input.cliArgs } : {}),
122
+ };
123
+ await writeSecureJson(operationFile(this.appData, input.operationId), operation);
124
+ return { operation, created: true };
125
+ });
126
+ }
80
127
  async get(operationId) {
81
128
  return readJsonIfExists(operationFile(this.appData, operationId));
82
129
  }
83
130
  async save(operation) {
84
131
  return this.serialize(operation.operationId, async () => {
132
+ const current = await this.get(operation.operationId);
133
+ if (current?.revision !== undefined && operation.revision !== current.revision)
134
+ throw new Error("operation revision may not be changed by save");
135
+ if (operation.frontendHumanDecision !== undefined || current?.frontendHumanDecision !== undefined) {
136
+ if (!current || JSON.stringify(operation) !== JSON.stringify(current))
137
+ throw new Error("human-decision projection may only be mutated by canonical CAS");
138
+ return current;
139
+ }
140
+ if (operation.frontendSplit !== undefined || current?.frontendSplit !== undefined) {
141
+ if (!current || JSON.stringify(operation) !== JSON.stringify(current))
142
+ throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
143
+ return current;
144
+ }
85
145
  await writeSecureJson(operationFile(this.appData, operation.operationId), operation);
86
146
  return operation;
87
147
  });
@@ -92,6 +152,10 @@ export class OperationStore {
92
152
  if (!current) {
93
153
  throw new Error(`operation not found: ${operationId}`);
94
154
  }
155
+ if ("frontendHumanDecision" in patch || (current.frontendHumanDecision !== undefined && ("state" in patch || "frontendExecution" in patch)))
156
+ throw new Error("human-decision projection may only be mutated by canonical CAS");
157
+ if ("frontendSplit" in patch || "revision" in patch || (current.frontendSplit !== undefined && ("state" in patch || "frontendExecution" in patch)))
158
+ throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
95
159
  const next = {
96
160
  ...current,
97
161
  ...patch,
@@ -106,6 +170,162 @@ export class OperationStore {
106
170
  return next;
107
171
  });
108
172
  }
173
+ async materializeFrontendHumanDecision(input) {
174
+ return this.serialize(input.operationId, async () => {
175
+ const current = await this.get(input.operationId);
176
+ if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision) {
177
+ const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.envelope.runId, revision: Math.max(1, input.expectedRecordRevision) });
178
+ return { outcome: "rejected", reason: "record revision conflict or legacy record", projection: fallback };
179
+ }
180
+ const projection = current.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: current.operationId, runId: input.envelope.runId, revision: current.revision });
181
+ const result = materializeFrontendHumanDecision({ projection, envelope: input.envelope, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest });
182
+ if (result.outcome !== "applied")
183
+ return result;
184
+ const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection, state: "running" };
185
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
186
+ return result;
187
+ });
188
+ }
189
+ async consumeFrontendHumanDecision(input) {
190
+ return this.serialize(input.operationId, async () => {
191
+ const current = await this.get(input.operationId);
192
+ if (!current || current.revision === undefined || !current.frontendHumanDecision) {
193
+ const fallback = buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
194
+ return { outcome: "rejected", reason: "record has no mutable human-decision projection", projection: fallback };
195
+ }
196
+ const projection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
197
+ if (current.revision !== input.expectedRecordRevision)
198
+ return { outcome: "rejected", reason: "record revision conflict", projection };
199
+ const result = consumeFrontendHumanDecision({ projection, receipt: input.receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
200
+ if (result.outcome !== "applied")
201
+ return result;
202
+ const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection };
203
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
204
+ return result;
205
+ });
206
+ }
207
+ async initializeFrontendHumanApprovedSplit(input) {
208
+ return this.serialize(input.operationId, async () => {
209
+ const current = await this.get(input.operationId);
210
+ const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
211
+ if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision || !current.frontendHumanDecision || !current.frontendExecution || current.frontendSplit)
212
+ return { outcome: "rejected", reason: "record is not an authoritative pending single-task split", projection: fallback };
213
+ const humanProjection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
214
+ if (humanProjection.status !== "blocked-human" || humanProjection.envelope.kind !== "split-materialization-approval" || humanProjection.digest !== input.expectedProjectionDigest)
215
+ return { outcome: "rejected", reason: "pending split envelope conflict", projection: humanProjection };
216
+ const receipt = parseFrontendHumanDecisionReceipt(input.receipt);
217
+ if (receipt.kind !== "split-materialization-approval")
218
+ return { outcome: "rejected", reason: "receipt is not a split approval", projection: humanProjection };
219
+ if (JSON.stringify(current.frontendExecution) !== JSON.stringify(input.execution) || input.execution.kind !== "single-task")
220
+ return { outcome: "rejected", reason: "single-task execution conflict", projection: humanProjection };
221
+ const consumed = consumeFrontendHumanDecision({ projection: humanProjection, receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
222
+ if (consumed.outcome !== "applied")
223
+ return consumed;
224
+ const initialized = initializeFrontendSplitParent({
225
+ execution: input.execution,
226
+ manifest: input.manifest,
227
+ expectedOperationRevision: input.execution.operationRevision,
228
+ successorOperationRevision: consumed.projection.revision,
229
+ envelopeTargetDigest: humanProjection.envelope.targetDigest,
230
+ activeWriterOperationId: input.activeWriterOperationId,
231
+ activeProviderOperationId: input.activeProviderOperationId,
232
+ authorization: { mode: "human", envelope: humanProjection.envelope, receipt },
233
+ });
234
+ if (initialized.outcome !== "applied")
235
+ return { outcome: "rejected", reason: initialized.reason, projection: humanProjection };
236
+ const splitProjection = frontendSplitProjectionSchema.parse({ revision: consumed.projection.revision, execution: initialized.execution, manifest: input.manifest, parent: initialized.parent });
237
+ const { frontendExecution: _singleTask, ...withoutSingleTask } = current;
238
+ const record = { ...withoutSingleTask, revision: consumed.projection.revision, frontendHumanDecision: consumed.projection, frontendSplit: splitProjection, state: "running" };
239
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
240
+ return { outcome: "applied", projection: splitProjection, humanDecision: consumed.projection };
241
+ });
242
+ }
243
+ async compareAndSwapFrontendSplit(input) {
244
+ return this.serialize(input.operationId, async () => {
245
+ const current = await this.get(input.operationId);
246
+ if (!current || current.revision === undefined || current.revision !== input.expectedRevision)
247
+ return { outcome: "conflict" };
248
+ const currentProjection = current.frontendSplit;
249
+ if (currentProjection) {
250
+ if (currentProjection.execution.digest !== input.expectedExecutionDigest || currentProjection.parent.digest !== input.expectedParentDigest)
251
+ return { outcome: "conflict" };
252
+ }
253
+ else if (input.expectedExecutionDigest || input.expectedParentDigest)
254
+ return { outcome: "conflict" };
255
+ const next = currentProjection
256
+ ? assertFrontendSplitProjectionSuccessor(currentProjection, input.next)
257
+ : frontendSplitProjectionSchema.parse(input.next);
258
+ if (next.revision !== current.revision + 1 || next.parent.operationId !== input.operationId)
259
+ throw new Error("split CAS next revision or operation binding mismatch");
260
+ const state = next.parent.operationState === "success" ? "succeeded" : next.parent.operationTerminal !== undefined ? "failed" : current.state;
261
+ const record = { ...current, revision: next.revision, frontendSplit: next, state };
262
+ await writeSecureJson(operationFile(this.appData, input.operationId), record);
263
+ return { outcome: "applied", projection: next };
264
+ });
265
+ }
266
+ async loadChildCloseoutFacts(input) {
267
+ const factsDir = input.closeoutRef.ref;
268
+ const facts = await readJsonIfExists(path.join(factsDir, "frontend-closeout-facts.json"));
269
+ if (facts === undefined)
270
+ return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: [], frontendTestHandoffRef: undefined });
271
+ const parsedFacts = frontendCloseoutFactsSchema.parse(facts);
272
+ if (computeFrontendSplitDigest(parsedFacts) !== input.closeoutRef.digest)
273
+ throw new Error(`child closeout facts digest mismatch: ${input.childId}`);
274
+ const handoff = await readJsonIfExists(path.join(factsDir, "frontend-test-handoff.json"));
275
+ const frontendTestHandoffRef = handoff === undefined ? undefined : computeFrontendSplitDigest(frontendTestHandoffSchema.parse(handoff));
276
+ return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: parsedFacts.residualRisks ?? [], frontendTestHandoffRef });
277
+ }
278
+ async settleFrontendSplitAggregate(input) {
279
+ return this.serialize(input.operationId, async () => {
280
+ const record = await this.get(input.operationId);
281
+ if (!record || record.revision === undefined || record.revision !== input.expectedRevision || !record.frontendSplit)
282
+ return { outcome: "conflict" };
283
+ const authoritative = frontendSplitProjectionSchema.parse(record.frontendSplit);
284
+ if (authoritative.parent.operationId !== input.operationId
285
+ || authoritative.revision !== input.expectedRevision
286
+ || authoritative.execution.digest !== input.expectedExecutionDigest
287
+ || authoritative.parent.digest !== input.expectedParentDigest
288
+ || authoritative.manifest.digest !== input.expectedManifestDigest
289
+ || authoritative.candidateBinding?.digest !== input.expectedCandidateBindingDigest
290
+ || authoritative.integrationBinding?.digest !== input.expectedIntegrationBindingDigest)
291
+ return { outcome: "conflict" };
292
+ if (!authoritative.candidateBinding)
293
+ return { outcome: "conflict" };
294
+ const expectedAggregate = buildFrontendSplitAggregateCloseout(authoritative.parent, authoritative.manifest, authoritative.candidateBinding, authoritative.integrationBinding, input.childCloseoutFacts);
295
+ if (JSON.stringify(expectedAggregate) !== JSON.stringify(input.aggregate))
296
+ return { outcome: "conflict" };
297
+ const transaction = await input.prepareAggregate({
298
+ operationId: authoritative.parent.operationId,
299
+ aggregate: expectedAggregate,
300
+ });
301
+ try {
302
+ const committedRef = frontendCommittedFactRefSchema.parse(transaction.committedRef);
303
+ const finalized = finalizeFrontendSplitAggregate(authoritative.parent, expectedAggregate, committedRef);
304
+ const next = assertFrontendSplitProjectionSuccessor(authoritative, { ...authoritative, revision: authoritative.revision + 1, parent: finalized.parent, execution: finalized.execution, aggregateCloseout: { value: expectedAggregate, committedRef } });
305
+ const nextRecord = { ...record, revision: next.revision, frontendSplit: next, state: "succeeded" };
306
+ await transaction.commit({
307
+ parentRecordPath: operationFile(this.appData, record.operationId),
308
+ parentRecord: nextRecord,
309
+ });
310
+ return { outcome: "applied", projection: next };
311
+ }
312
+ catch (error) {
313
+ await transaction.rollback();
314
+ throw error;
315
+ }
316
+ });
317
+ }
318
+ async initializeFrontendSplit(expectedRevision, projectionValue) {
319
+ const projection = frontendSplitProjectionSchema.parse(projectionValue);
320
+ return this.serialize(projection.parent.operationId, async () => {
321
+ const current = await this.get(projection.parent.operationId);
322
+ if (!current || current.revision === undefined || current.revision !== expectedRevision || current.frontendSplit !== undefined || projection.revision !== expectedRevision + 1)
323
+ return { outcome: "conflict" };
324
+ const record = { ...current, revision: projection.revision, frontendSplit: projection };
325
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
326
+ return { outcome: "applied", projection };
327
+ });
328
+ }
109
329
  async list() {
110
330
  const files = await listJsonFiles(this.appData.operations);
111
331
  const ops = [];
@@ -47,10 +47,22 @@ const RAW_TO_NORMALIZED = {
47
47
  "verify-failure": "validation",
48
48
  "test-failure": "validation",
49
49
  "verification-plan-stale": "validation",
50
+ "read-burst": "validation",
51
+ // Writer persisted real file changes but the final provider call hit a
52
+ // context overflow; verification never ran. Distinct from empty-output so
53
+ // the report shows partial success instead of a spec issue.
54
+ "partial-success-with-context-overflow": "validation",
55
+ // Writer burned an excessive token budget (unconverged read-edit-test loop)
56
+ // and still failed; distinct from empty-output for accurate reporting.
57
+ "writer-budget-exhausted": "validation",
50
58
  network: "executor",
51
59
  quota: "executor",
52
60
  "rate-limit": "executor",
53
61
  unavailable: "executor",
62
+ // Context window overflow (400 request-too-large): the session needs
63
+ // compaction / fresh context before retrying — an executor-side
64
+ // environment condition, not a plan-quality defect.
65
+ "context-overflow": "executor",
54
66
  // Deterministic capability/policy refusal: missing verified launcher JS
55
67
  // entry, capability allowlist rejection, or missing bounded write tool
56
68
  // profile. These are environment/config defects the operator repairs
@@ -12,6 +12,19 @@ export const OPENSPEC_SPEC_DIRS = [
12
12
  "openspec/project-specs",
13
13
  "ai_workspace",
14
14
  ];
15
+ /** Default roots used when a frontend task does not declare specRoots. */
16
+ export const DEFAULT_FRONTEND_SPEC_ROOTS = OPENSPEC_SPEC_DIRS;
17
+ /** A task-owned spec root must be a normalized, repo-relative POSIX directory. */
18
+ export function isValidFrontendSpecRoot(value) {
19
+ return (Boolean(value) &&
20
+ !value.includes("\\") &&
21
+ !value.startsWith("/") &&
22
+ !value.includes("//") &&
23
+ !value.split("/").some((segment) => !segment ||
24
+ segment === "." ||
25
+ segment === ".." ||
26
+ /\s/.test(segment)));
27
+ }
15
28
  /** Allowed spec file extensions (case-insensitive). */
16
29
  export const OPENSPEC_SPEC_EXT_RE = /\.(md|mdx|json|yaml|yml)$/i;
17
30
  /**
@@ -43,14 +56,17 @@ export function isGovernanceSubtreePath(filePath, governanceRoot) {
43
56
  * directories. `filePath` must be a repo-relative POSIX path (backslashes are
44
57
  * normalised internally). Directory names are case-sensitive.
45
58
  */
46
- export function isOpenspecSpecPath(filePath) {
59
+ export function isFrontendSpecPath(filePath, specRoots = DEFAULT_FRONTEND_SPEC_ROOTS) {
47
60
  const normalized = filePath.replaceAll("\\", "/");
48
- for (const dir of OPENSPEC_SPEC_DIRS) {
61
+ for (const dir of specRoots) {
49
62
  if (normalized === dir || normalized.startsWith(dir + "/"))
50
63
  return true;
51
64
  }
52
65
  return false;
53
66
  }
67
+ export function isOpenspecSpecPath(filePath) {
68
+ return isFrontendSpecPath(filePath);
69
+ }
54
70
  /**
55
71
  * Returns `true` when `filename` (the basename only, not a full path) has an
56
72
  * allowed frontend specification extension.
@@ -59,10 +75,13 @@ export function isValidOpenspecExtension(filename) {
59
75
  return OPENSPEC_SPEC_EXT_RE.test(filename);
60
76
  }
61
77
  /** Returns `true` for a supported file inside a canonical openspec spec dir. */
62
- export function isOpenspecSpecFilePath(filePath) {
63
- return (isOpenspecSpecPath(filePath) &&
78
+ export function isFrontendSpecFilePath(filePath, specRoots = DEFAULT_FRONTEND_SPEC_ROOTS) {
79
+ return (isFrontendSpecPath(filePath, specRoots) &&
64
80
  isValidOpenspecExtension(filePath.replaceAll("\\", "/")));
65
81
  }
82
+ export function isOpenspecSpecFilePath(filePath) {
83
+ return isFrontendSpecFilePath(filePath);
84
+ }
66
85
  /**
67
86
  * Returns `true` when free-form search input references a canonical frontend
68
87
  * specification directory with exact lowercase directory names.
@@ -123,3 +142,50 @@ export function extractTaskSourceOpenspecPaths(markdown, governanceRoot = DEFAUL
123
142
  !isGovernanceSubtreePath(token, governanceRoot))
124
143
  .sort();
125
144
  }
145
+ /**
146
+ * Extract explicit specification-file citations for a task's frozen spec roots.
147
+ * This is intentionally path-only: source preparation decides which roots are
148
+ * normative, while DAG execution only consumes that frozen configuration.
149
+ */
150
+ export function extractTaskSourceFrontendSpecPaths(markdown, specRoots, governanceRoot = DEFAULT_OPENSPEC_GOVERNANCE_ROOT) {
151
+ if (!markdown)
152
+ return [];
153
+ const raw = new Set();
154
+ const collect = (token) => {
155
+ const normalized = normalizeCitationToken(token);
156
+ if (normalized)
157
+ raw.add(normalized);
158
+ };
159
+ for (const match of markdown.matchAll(/`([^`\n]+)`/g))
160
+ collect(match[1] ?? "");
161
+ for (const match of markdown.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g))
162
+ collect(match[1] ?? "");
163
+ for (const root of specRoots) {
164
+ const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
165
+ for (const match of markdown.matchAll(new RegExp(`(?:^|[\\s\"'([>])(${escaped}/[A-Za-z0-9._/-]+)`, "g"))) {
166
+ collect(match[1] ?? "");
167
+ }
168
+ }
169
+ return [...raw]
170
+ .filter((token) => isFrontendSpecFilePath(token, specRoots) && !isGovernanceSubtreePath(token, governanceRoot))
171
+ .sort();
172
+ }
173
+ /**
174
+ * Source-preparation helper for explicitly named normative directories. A root
175
+ * is inferred only on a line that declares a spec/规范/design-system intent;
176
+ * arbitrary Markdown links never silently broaden task configuration.
177
+ */
178
+ export function extractDeclaredFrontendSpecRoots(markdown) {
179
+ const roots = new Set();
180
+ for (const line of markdown.split(/\r?\n/)) {
181
+ if (!/(?:规范|规约|标准|设计系统|\bspec(?:ification)?\b|\bdesign[ -]?system\b|\bguidelines?\b|\brules?\b)/i.test(line))
182
+ continue;
183
+ for (const match of line.matchAll(/`([^`]+)`|\[[^\]]+\]\(([^)\s]+)\)/g)) {
184
+ const raw = (match[1] ?? match[2] ?? "").replaceAll("\\", "/").replace(/\/+$/, "");
185
+ if (!raw || !isValidFrontendSpecRoot(raw))
186
+ continue;
187
+ roots.add(isValidOpenspecExtension(raw) ? raw.split("/").slice(0, -1).join("/") : raw);
188
+ }
189
+ }
190
+ return [...roots].filter(isValidFrontendSpecRoot).sort();
191
+ }
@@ -2240,6 +2240,13 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
2240
2240
  source: "loop-agent",
2241
2241
  note: "server-side read-only run-facts check (ineligible plan / explicit rerun-task recommendation) replaces the browser Human Gate (2026-08-11).",
2242
2242
  },
2243
+ {
2244
+ command: "loop-agent dag follow-up",
2245
+ coverage: "excluded",
2246
+ action: null,
2247
+ source: "loop-agent",
2248
+ exclusionReason: "Frontend review recovery helper is a direct advanced CLI composition (create/import task and optional advance), not an Operator action endpoint.",
2249
+ },
2243
2250
  {
2244
2251
  command: "loop-agent dag reconcile-tasks",
2245
2252
  coverage: "human-gated-required",
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { isOpenspecSpecFilePath } from "../shared/openspec-spec.js";
2
+ import { DEFAULT_FRONTEND_SPEC_ROOTS, isFrontendSpecFilePath, isValidFrontendSpecRoot, } from "../shared/openspec-spec.js";
3
3
  export const taskFlowSchema = z.enum([
4
4
  "auto",
5
5
  "micro",
@@ -77,13 +77,27 @@ export const frontendOpenspecPolicySchema = z.enum(["cited", "scan-strict"]);
77
77
  */
78
78
  export const frontendOpenspecConfigSchema = z.object({
79
79
  policy: frontendOpenspecPolicySchema.optional().default("cited"),
80
+ /**
81
+ * Repo-relative directories containing frontend normative files. These are
82
+ * frozen in task.json during contract preparation, before DAG generation.
83
+ */
84
+ specRoots: z
85
+ .array(z.string().refine((candidate) => isValidFrontendSpecRoot(candidate), "frontendOpenspec.specRoots entries must be normalized repo-relative directories"))
86
+ .optional(),
80
87
  requiredReadPaths: z
81
- .array(z
82
- .string()
83
- .refine((candidate) => !candidate.includes("\\") &&
84
- isOpenspecSpecFilePath(candidate), "frontendOpenspec.requiredReadPaths entries must be repo-relative supported files under openspec/schemas/, openspec/project-specs/, or ai_workspace/"))
88
+ .array(z.string())
85
89
  .optional()
86
90
  .default([]),
91
+ }).superRefine((value, ctx) => {
92
+ const roots = value.specRoots ?? DEFAULT_FRONTEND_SPEC_ROOTS;
93
+ if (new Set(roots).size !== roots.length) {
94
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "frontendOpenspec.specRoots must not contain duplicates", path: ["specRoots"] });
95
+ }
96
+ for (const required of value.requiredReadPaths) {
97
+ if (!isFrontendSpecFilePath(required, roots)) {
98
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "frontendOpenspec.requiredReadPaths entries must be repo-relative supported files under frontendOpenspec.specRoots", path: ["requiredReadPaths"] });
99
+ }
100
+ }
87
101
  });
88
102
  /** Batch limits and gate profile for the browser-driven frontend test DAG.
89
103
  * Token fields are post-case stop thresholds, not model-provider hard caps. */
@@ -131,6 +145,75 @@ export const frontendTestConfigSchema = z.object({
131
145
  .strict()
132
146
  .optional(),
133
147
  });
148
+ const backendTestBudgetProofSchema = z
149
+ .object({
150
+ groupId: z.string().min(1),
151
+ estimatedCaseCount: z.number().int().positive(),
152
+ estimatedTestPointCount: z.number().int().positive(),
153
+ estimatedOutputChars: z.number().int().positive(),
154
+ maxOutputCharsPerWriter: z.number().int().positive(),
155
+ estimatorVersion: z.literal("backend-test-output-estimator-v1"),
156
+ inputSha256: z.string().regex(/^[a-f0-9]{64}$/),
157
+ })
158
+ .strict();
159
+ const backendTestModuleLayoutEntrySchema = z
160
+ .object({
161
+ stem: z.string().regex(/^[a-z][a-z0-9_]*$/),
162
+ businessResource: z.string().min(1),
163
+ markdownPath: z.string().min(1),
164
+ pytestPath: z.string().min(1),
165
+ ownedOperations: z.array(z.string().regex(/^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) \/\S+$/)).min(1),
166
+ splitReason: z.enum([
167
+ "explicit-user-layout",
168
+ "primary-business-resource",
169
+ "independent-business-resource",
170
+ "output-budget",
171
+ ]),
172
+ budgetProof: backendTestBudgetProofSchema.optional(),
173
+ })
174
+ .strict()
175
+ .superRefine((entry, ctx) => {
176
+ const safe = (value) => !value.startsWith("/") &&
177
+ !value.startsWith("\\") &&
178
+ !value.includes("..") &&
179
+ !value.includes("\\");
180
+ if (!safe(entry.markdownPath) || !entry.markdownPath.endsWith(".md")) {
181
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["markdownPath"], message: "must be a safe repository-relative .md path" });
182
+ }
183
+ if (!safe(entry.pytestPath) || !entry.pytestPath.endsWith(".py")) {
184
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["pytestPath"], message: "must be a safe repository-relative .py path" });
185
+ }
186
+ if (entry.splitReason === "output-budget" && !entry.budgetProof) {
187
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["budgetProof"], message: "is required for output-budget split" });
188
+ }
189
+ if (entry.splitReason !== "output-budget" && entry.budgetProof) {
190
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["budgetProof"], message: "is only valid for output-budget split" });
191
+ }
192
+ });
193
+ const backendTestModuleLayoutSchema = z
194
+ .object({
195
+ schemaId: z.literal("backend-test-module-layout-v1"),
196
+ mode: z.enum(["explicit-file-layout", "business-resource-layout"]),
197
+ modules: z.array(backendTestModuleLayoutEntrySchema).min(1).max(8),
198
+ })
199
+ .strict()
200
+ .superRefine((layout, ctx) => {
201
+ for (const key of ["stem", "markdownPath", "pytestPath"]) {
202
+ const seen = new Set();
203
+ for (let index = 0; index < layout.modules.length; index += 1) {
204
+ const value = layout.modules[index][key];
205
+ if (seen.has(value))
206
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["modules", index, key], message: `duplicate ${key}` });
207
+ seen.add(value);
208
+ }
209
+ }
210
+ if (layout.mode === "explicit-file-layout") {
211
+ layout.modules.forEach((entry, index) => {
212
+ if (entry.splitReason !== "explicit-user-layout")
213
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["modules", index, "splitReason"], message: "explicit-file-layout requires explicit-user-layout" });
214
+ });
215
+ }
216
+ });
134
217
  /** Backend-test artifact layout and pipeline mode configuration (plan A/B/D). */
135
218
  export const backendTestConfigSchema = z
136
219
  .object({
@@ -152,6 +235,8 @@ export const backendTestConfigSchema = z
152
235
  workspaceControl: z.enum(["git", "filesystem-only"]).optional(),
153
236
  /** Per-command timeout for the readiness-authorized pytest/HTML execution node. */
154
237
  executeTimeoutMs: z.number().int().min(60_000).max(7_200_000).optional(),
238
+ /** Optional strict physical/business module layout; authoritative over model-derived layout. */
239
+ moduleLayout: backendTestModuleLayoutSchema.optional(),
155
240
  })
156
241
  .strict()
157
242
  .optional();
@@ -210,6 +295,21 @@ const taskConfigObjectSchema = z.object({
210
295
  maxFixLoops: z.number().int().min(0).optional().default(2),
211
296
  /** Supervised DAG convergence is opt-in until runtime smoke evidence is stronger. */
212
297
  convergence: convergenceConfigSchema.optional().default({ enabled: false }),
298
+ /** Frontend recovery continuation quota. Bounds how many times a failed
299
+ * frontend run may auto-recover via M6 recovery operations (1 = current
300
+ * default; raise to 2-3 for more tolerance of transient verify failures). */
301
+ frontendRecovery: z
302
+ .object({
303
+ maxContinuations: z
304
+ .number()
305
+ .int()
306
+ .min(1)
307
+ .max(5)
308
+ .optional()
309
+ .default(1),
310
+ })
311
+ .strict()
312
+ .optional(),
213
313
  /** Outer loop auto mode never writes by default; DAG execute requires this policy plus path/writeSet gates. */
214
314
  loopAutoExecutionPolicy: loopAutoExecutionPolicySchema
215
315
  .optional()
@@ -90,6 +90,10 @@ export async function adoptTaskContract(input) {
90
90
  requirementBytes: bytes.requirementBytes,
91
91
  constraintsBytes: bytes.constraintsBytes,
92
92
  referenceManifestBytes: bytes.referenceManifestBytes,
93
+ ...(state.ref?.sourceHashes.requirementLedgerSha256 &&
94
+ bytes.requirementLedgerBytes !== null
95
+ ? { requirementLedgerBytes: bytes.requirementLedgerBytes }
96
+ : {}),
93
97
  });
94
98
  const taskConfigSha256 = computeTaskConfigSha256(fullConfig);
95
99
  const canonicalHash = computeCanonicalHash({
@@ -45,6 +45,10 @@ export async function refreshManagedContractAfterImport(input) {
45
45
  requirementBytes: bytes.requirementBytes,
46
46
  constraintsBytes: bytes.constraintsBytes,
47
47
  referenceManifestBytes: bytes.referenceManifestBytes,
48
+ ...(state2.ref.sourceHashes.requirementLedgerSha256 &&
49
+ bytes.requirementLedgerBytes !== null
50
+ ? { requirementLedgerBytes: bytes.requirementLedgerBytes }
51
+ : {}),
48
52
  });
49
53
  const taskConfigSha256 = computeTaskConfigSha256(fullConfig);
50
54
  const canonicalHash = computeCanonicalHash({
@@ -184,6 +184,9 @@ export function mergeManagedTaskConfigFields(existing, draft) {
184
184
  command: cmd.command,
185
185
  ...(cmd.timeoutMs !== undefined ? { timeoutMs: cmd.timeoutMs } : {}),
186
186
  })),
187
+ ...(draft.frontendOpenspec !== undefined
188
+ ? { frontendOpenspec: draft.frontendOpenspec }
189
+ : {}),
187
190
  };
188
191
  if (draft.featureId !== undefined) {
189
192
  next.featureId = draft.featureId;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { backendTestConfigSchema, taskKindSchema, taskVerifyCommandSchema, } from "../config-types.js";
2
+ import { backendTestConfigSchema, frontendOpenspecConfigSchema, taskKindSchema, taskVerifyCommandSchema, } from "../config-types.js";
3
3
  import { TASK_CONFIG_SCHEMA_VERSION, TASK_CONTRACT_CANONICALIZER_VERSION, TASK_CONTRACT_DRAFT_SCHEMA_VERSION, TASK_CONTRACT_PROJECTION_VERSION, TASK_CONTRACT_REF_SCHEMA_VERSION, } from "./constants.js";
4
4
  const sha256Schema = z
5
5
  .string()
@@ -74,6 +74,7 @@ export const taskContractDraftSchema = z
74
74
  openQuestions: z.array(z.string()).optional(),
75
75
  assumptions: z.array(z.string()).optional(),
76
76
  hardConstraints: z.array(z.string()).optional(),
77
+ frontendOpenspec: frontendOpenspecConfigSchema.optional(),
77
78
  interviewAssessmentRef: z
78
79
  .object({
79
80
  draftSha256: z.string().min(1),