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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +319 -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 +3976 -76
  18. package/dist/executors/pi-executor.js +15 -4
  19. package/dist/executors/pi-read-budget-policy.js +239 -0
  20. package/dist/executors/shell-executor.js +978 -188
  21. package/dist/executors/shell-write-guard.js +7 -0
  22. package/dist/infrastructure/console/operation-store.js +226 -6
  23. package/dist/shared/dag-failure-category.js +12 -0
  24. package/dist/shared/openspec-spec.js +70 -4
  25. package/dist/shared/operator/capabilities.js +7 -0
  26. package/dist/task/config-types.js +105 -5
  27. package/dist/task/contract/adopt.js +4 -0
  28. package/dist/task/contract/import-revision.js +4 -0
  29. package/dist/task/contract/project.js +3 -0
  30. package/dist/task/contract/schema.js +2 -1
  31. package/dist/task/frontend-project-capability.js +203 -20
  32. package/dist/task/runtime.js +5 -2
  33. package/dist/task/source-prepare/build-draft.js +3 -3
  34. package/dist/task/source-prepare/fragment-inventory.js +64 -25
  35. package/dist/task/source-prepare/prepare.js +126 -21
  36. package/dist/task/source-prepare/semantic-intake.js +6 -2
  37. package/dist/task/source-references.js +22 -1
  38. package/dist/worker/console/chat/chat-ui-policy.js +4 -2
  39. package/dist/worker/console/chat/model-resolver.js +7 -4
  40. package/dist/worker/console/chat/pi-runtime.js +258 -11
  41. package/dist/worker/console/chat/repo-browser.js +10 -3
  42. package/dist/worker/console/chat/routes.js +165 -71
  43. package/dist/worker/console/chat/sdd-data-alignment.js +222 -0
  44. package/dist/worker/console/chat/session-catalog.js +2 -0
  45. package/dist/worker/console/chat/session-store.js +159 -70
  46. package/dist/worker/console/chat/shortcuts.js +19 -3
  47. package/dist/worker/console/chat/turn-process.js +111 -56
  48. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  49. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  50. package/dist/worker/console/index.js +3 -0
  51. package/dist/worker/console/operator-actions.js +169 -7
  52. package/dist/worker/console/pi-readiness.js +3 -2
  53. package/dist/worker/console/server.js +23 -1
  54. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CcS17TBr.js} +1 -1
  55. package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-COptKq2S.js} +1 -1
  56. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-h4LKMHjP.js} +1 -1
  57. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-COA1MOH0.js} +1 -1
  58. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-DJUf0QPm.js} +1 -1
  59. package/dist/worker/console/static/assets/channel-DdBCaOJ6.js +1 -0
  60. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-mtWfKrUX.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-B8pXsxDQ.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-ipKzByl1.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-DYi3Ald_.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DQtGYoty.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-BU0qS2YV.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-DVH9dIGE.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-Dt60SmgM.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-BoaUGuEY.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-_WHFDTp2.js} +1 -1
  70. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DZFra1GO.js +1 -0
  71. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DZFra1GO.js +1 -0
  72. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-BxkbTIRd.js} +1 -1
  73. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-Bf2UVnoG.js} +1 -1
  74. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-Bo23q1J_.js} +1 -1
  75. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-Dd_UU49i.js} +1 -1
  76. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-ebUa1a9y.js} +1 -1
  77. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-BucthU8r.js} +1 -1
  78. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-BgqNpAkm.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-DC4q5NGa.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-CBqCaMeb.js} +1 -1
  81. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-CGtnbQ3-.js} +1 -1
  82. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-CG3LUao5.js} +1 -1
  83. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-D2ZVgoFS.js} +1 -1
  84. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DoYFZiKt.js} +1 -1
  85. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-CKoP1s6j.js} +1 -1
  86. package/dist/worker/console/static/assets/index-CAZ2fC_X.css +1 -0
  87. package/dist/worker/console/static/assets/index-vbTcFnFs.js +449 -0
  88. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-Duofv8p2.js} +1 -1
  89. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-D2nlCkA1.js} +1 -1
  90. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-Dd4IHdus.js} +1 -1
  91. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-Bi20AOdt.js} +1 -1
  92. package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-D59YJ9kB.js} +1 -1
  93. package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-Bl13LpNa.js} +5 -5
  94. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-C6DLP5eY.js} +1 -1
  95. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-BjO4cy64.js} +1 -1
  96. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-D7i2qZTG.js} +1 -1
  97. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-Dx8o_h0y.js} +1 -1
  98. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-B55orI_t.js} +1 -1
  99. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-zWaehp6f.js} +1 -1
  100. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-CcA-pjvD.js} +1 -1
  101. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-BOFbzNHI.js} +1 -1
  102. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-BjAejah1.js} +1 -1
  103. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-DXYwxJwZ.js} +1 -1
  104. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Clg3V9t1.js +1 -0
  105. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-OYiOch8n.js} +2 -2
  106. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DX1dxAAW.js +8 -0
  107. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-cZfH3nmU.js} +1 -1
  108. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-BDE7b3E1.js} +1 -1
  109. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-kyyGy9WJ.js} +1 -1
  110. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CJj6VTog.js} +1 -1
  111. package/dist/worker/console/static/index.html +2 -2
  112. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  113. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
  114. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
  115. package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
  116. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
  117. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
  118. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +258 -23
  119. package/dist/worker/console/static-src/operator-chat/useChatThread.js +112 -4
  120. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
  121. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
  122. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
  123. package/dist/worker/console/static-src/shell/console-update-reload.js +36 -9
  124. package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
  125. package/dist/worker/console/workspace-context.js +115 -1
  126. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  127. package/dist/worker/observe/routes.js +4 -0
  128. package/dist/worker/observe/static/dag-helpers.js +4 -2
  129. package/dist/worker/observe/static/dag-node-purpose.js +5 -0
  130. package/dist/worker/observe/static/inspect-workspace.js +23 -0
  131. package/dist/worker/observe/static/kpi.js +2 -2
  132. package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
  133. package/dist/worker/observe/static/operator-chrome.js +37 -23
  134. package/dist/worker/observe/static/relations.js +2 -2
  135. package/dist/worker/observe/static/router.d.ts +12 -1
  136. package/dist/worker/observe/static/router.js +48 -6
  137. package/dist/worker/observe/static/run-processing.js +4 -2
  138. package/dist/worker/observe/static/shell-chrome.js +2 -2
  139. package/dist/worker/observe/static/state.js +5 -0
  140. package/dist/worker/observe/static/styles.css +95 -0
  141. package/dist/worker/observe/static/views/dag-inspector.js +14 -7
  142. package/dist/worker/observe/static/views/dag.js +10 -2
  143. package/dist/worker/observe/static/views/dags.js +3 -1
  144. package/dist/worker/observe/static/views/dashboard.js +14 -6
  145. package/dist/worker/observe/static/views/pool.js +7 -2
  146. package/dist/worker/observe/static/views/run.js +12 -3
  147. package/dist/worker/observe/static/views/session-timeline.js +69 -10
  148. package/dist/worker/observe/static/views/task.js +7 -2
  149. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +215 -28
  150. package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
  151. package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
  152. package/dist/workflows/dag/backend-test-scenario-param.js +172 -25
  153. package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
  154. package/dist/workflows/dag/backend-test-writer-completeness.js +104 -12
  155. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  156. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  157. package/dist/workflows/dag/frontend-closeout.js +221 -0
  158. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  159. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  160. package/dist/workflows/dag/frontend-implementation-contract.js +1237 -192
  161. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  162. package/dist/workflows/dag/frontend-prewrite-gate.js +256 -350
  163. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  164. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  165. package/dist/workflows/dag/frontend-recovery-controller.js +226 -0
  166. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  167. package/dist/workflows/dag/frontend-recovery-plan.js +21 -10
  168. package/dist/workflows/dag/frontend-recovery-run.js +166 -34
  169. package/dist/workflows/dag/frontend-repair.js +1 -432
  170. package/dist/workflows/dag/frontend-review-context.js +261 -15
  171. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  172. package/dist/workflows/dag/frontend-shadow-dual-write.js +941 -0
  173. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  174. package/dist/workflows/dag/frontend-shape-facts.js +419 -0
  175. package/dist/workflows/dag/frontend-shape.js +435 -0
  176. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  177. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  178. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  179. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  180. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  181. package/dist/workflows/dag/frontend-verification-trace.js +252 -25
  182. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  183. package/dist/workflows/dag/frontend-writer-admission.js +319 -0
  184. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  185. package/dist/workflows/dag/init-hybrid.js +1028 -562
  186. package/dist/workflows/dag/interrupt-request.js +7 -0
  187. package/dist/workflows/dag/node-execution.js +828 -4
  188. package/dist/workflows/dag/prompt.js +70 -3
  189. package/dist/workflows/dag/recovery-lease.js +170 -0
  190. package/dist/workflows/dag/report.js +37 -1
  191. package/dist/workflows/dag/rerun-feedback.js +315 -1
  192. package/dist/workflows/dag/rerun-plan.js +17 -6
  193. package/dist/workflows/dag/rerun-run.js +44 -6
  194. package/dist/workflows/dag/rerun-task.js +251 -13
  195. package/dist/workflows/dag/retry-policy.js +219 -104
  196. package/dist/workflows/dag/runner.js +580 -124
  197. package/dist/workflows/dag/scheduler.js +133 -20
  198. package/dist/workflows/dag/types.js +244 -16
  199. package/dist/workflows/dag/validate.js +28 -12
  200. package/docs/examples/README.md +5 -0
  201. package/docs/init-surface.manifest.json +30 -12
  202. package/docs/skills/vetted-skill-registry.md +4 -2
  203. package/docs/templates/README.md +2 -0
  204. package/docs/templates/agent-dag-report.schema.json +8 -2
  205. package/docs/templates/agent-dag.schema.json +1 -1
  206. package/docs/templates/backend-test-dag.json +23 -20
  207. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  208. package/docs/templates/frontend-implementation-dag.json +89 -0
  209. package/docs/templates/spec-registry.schema.json +45 -0
  210. package/harness.json +1 -1
  211. package/package.json +4 -3
  212. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  213. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  214. package/skills/frontend-contract/SKILL.md +23 -0
  215. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  216. package/skills/frontend-design-review/SKILL.md +22 -41
  217. package/skills/frontend-plan/SKILL.md +26 -0
  218. package/skills/frontend-plan/references/decision-contract.md +37 -0
  219. package/skills/frontend-plan/references/design-decisions.md +17 -0
  220. package/skills/frontend-review/SKILL.md +20 -15
  221. package/skills/frontend-review/references/review-findings.md +6 -7
  222. package/skills/frontend-scout/SKILL.md +25 -0
  223. package/skills/frontend-scout/references/design-evidence.md +16 -0
  224. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  225. package/skills/frontend-verification/SKILL.md +1 -1
  226. package/skills/loop-agent/references/command-reference.md +1 -0
  227. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  228. package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
  229. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
  230. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
  231. package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
  232. package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
  233. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
  234. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
  235. package/skills/frontend-implementation/SKILL.md +0 -52
  236. package/skills/frontend-implementation/references/code-standards.md +0 -33
  237. package/skills/frontend-implementation/references/design-spec.md +0 -56
  238. package/skills/frontend-implementation/references/node-contracts.md +0 -31
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  import { isPauseOnHumanDecisionGate } from "./decision-envelope.js";
4
4
  import { evaluateConditionExpression } from "./dynamic-runtime/condition.js";
5
5
  import { sha256Hex } from "./frontend-implementation-contract.js";
6
- import { frontendPrewriteResultV1Schema, FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME, } from "./frontend-prewrite-gate.js";
6
+ import { boundaryForTargetNode, computeFrontendShapeNodeDigest, evaluateFrontendShapeBoundary, } from "./frontend-shape-facts.js";
7
+ import { FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, frontendWriterAdmissionResultV1Schema, normalizeConcreteWriteSetPath, } from "./frontend-writer-admission.js";
7
8
  import { FRONTEND_RECOVERY_IMPORT_MANIFEST_REL_PATH, FRONTEND_RECOVERY_INTENT_REL_DIR, } from "./frontend-recovery-plan.js";
8
9
  export function isConditionSkippedReason(reason) {
9
10
  return Boolean(reason?.startsWith("condition "));
@@ -111,16 +112,25 @@ async function mapConcurrent(items, limit, fn) {
111
112
  }
112
113
  export const FRONTEND_WRITER_NODE_IDS = [
113
114
  "frontend-implement-pi",
114
- "frontend-repair-pi",
115
115
  ];
116
- /** Run-relative artifact location written by the prewrite gate generator. */
117
- export const FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT = path.posix.join("contracts", FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME);
118
116
  /**
119
- * Read and validate the frontend-prewrite-result-v1 artifact. Fail-closed:
120
- * a missing file or invalid payload returns ok:false and never throws.
117
+ * Run-relative artifact location read by the frontend writer admission gate.
118
+ * M8 re-pointed the legacy prewrite-result source at the new
119
+ * frontend-writer-admission-result.json artifact (written by the
120
+ * frontend-writer-admission-shell); the exported name is retained because
121
+ * runner.ts / node-execution.ts reference it as the admission evidence source.
122
+ */
123
+ export const FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT = FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT;
124
+ export { FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT };
125
+ /**
126
+ * Read and validate the writer admission result artifact.
127
+ * Fail-closed: a missing file or invalid payload returns ok:false and never
128
+ * throws. This is the M8 write authorization source (replaces the legacy
129
+ * frontend-prewrite-result-v1 for dispatch; the legacy artifact remains
130
+ * produced only by the shadow `runFrontendPrewriteGate` library export).
121
131
  */
122
132
  export async function readFrontendPrewriteResult(runDir) {
123
- const artifactPath = path.join(runDir, "contracts", FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME);
133
+ const artifactPath = path.join(runDir, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT);
124
134
  let raw;
125
135
  try {
126
136
  raw = await readFile(artifactPath, "utf8");
@@ -129,7 +139,7 @@ export async function readFrontendPrewriteResult(runDir) {
129
139
  if (error.code === "ENOENT") {
130
140
  return {
131
141
  ok: false,
132
- reason: `frontend prewrite result artifact missing: ${artifactPath}`,
142
+ reason: `frontend writer admission artifact missing: ${artifactPath}`,
133
143
  };
134
144
  }
135
145
  throw error;
@@ -142,24 +152,29 @@ export async function readFrontendPrewriteResult(runDir) {
142
152
  catch (error) {
143
153
  return {
144
154
  ok: false,
145
- reason: `frontend prewrite result artifact is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
155
+ reason: `frontend writer admission artifact is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
146
156
  };
147
157
  }
148
- const result = frontendPrewriteResultV1Schema.safeParse(parsed);
158
+ const result = frontendWriterAdmissionResultV1Schema.safeParse(parsed);
149
159
  if (!result.success) {
150
160
  return {
151
161
  ok: false,
152
- reason: "frontend prewrite result artifact failed schema validation",
162
+ reason: "frontend writer admission artifact failed schema validation",
153
163
  };
154
164
  }
155
165
  return { ok: true, result: result.data, artifactHash };
156
166
  }
157
- /** Authorize only accepted / accepted-normalized classifications. */
167
+ /**
168
+ * M8 alias: read the writer admission result artifact under the explicit name.
169
+ * Same fail-closed semantics as `readFrontendPrewriteResult`.
170
+ */
171
+ export async function readFrontendWriterAdmissionResult(runDir) {
172
+ return readFrontendPrewriteResult(runDir);
173
+ }
174
+ /** Authorize only the `accepted` classification (four-state admission).
175
+ * `requires-human-approval`, `blocked`, and `stale` are all denied. */
158
176
  export function isFrontendWriterAuthorized(result) {
159
- return result.classification === "accepted" ||
160
- result.classification === "accepted-normalized"
161
- ? "authorized"
162
- : "denied";
177
+ return result.classification === "accepted" ? "authorized" : "denied";
163
178
  }
164
179
  /** Fail-closed: leave no PENDING nodes that look "still scheduled" after abort. */
165
180
  export function markPendingNodesControllerInterrupted(state, reason = "run aborted by controller (abortSignal)") {
@@ -399,6 +414,12 @@ export async function executeDagRanksOnce(input) {
399
414
  continue;
400
415
  }
401
416
  const decision = isFrontendWriterAuthorized(admission.result);
417
+ const normalizedWriteSet = admission.result.writeSet
418
+ .map(normalizeConcreteWriteSetPath)
419
+ .filter((entry) => entry !== undefined);
420
+ const concreteWriteSetValid = (admission.result.writeSet.length > 0 &&
421
+ normalizedWriteSet.length === admission.result.writeSet.length &&
422
+ new Set(normalizedWriteSet).size === normalizedWriteSet.length);
402
423
  node.frontendWriterAdmission = {
403
424
  schemaVersion: 1,
404
425
  writerNodeId: id,
@@ -410,18 +431,110 @@ export async function executeDagRanksOnce(input) {
410
431
  ? `classification: ${admission.result.classification}`
411
432
  : null,
412
433
  };
413
- if (decision === "denied") {
434
+ if (decision === "denied" || !concreteWriteSetValid) {
435
+ node.frontendWriterAdmission.reason = decision === "denied"
436
+ ? `classification: ${admission.result.classification}`
437
+ : "admission writeSet is not a concrete unique path set";
414
438
  node.status = "SKIPPED";
415
439
  node.skippedReason = "frontend-prewrite-not-authorized";
416
440
  node.finishedAt = checkedAt;
417
441
  frontendAdmissionSettled.push(id);
418
442
  }
443
+ else {
444
+ node.runtimeWriteAuthorization = {
445
+ schemaVersion: 1,
446
+ status: "validated",
447
+ approvalSourceNodeId: FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT,
448
+ approvalDigest: admission.result.admissionDigest,
449
+ effectiveWriteSet: normalizedWriteSet,
450
+ };
451
+ }
419
452
  }
420
453
  if (frontendAdmissionSettled.length > 0)
421
454
  await input.persistState();
422
455
  }
423
456
  const frontendAdmissionSkippedSet = new Set(frontendAdmissionSettled);
424
457
  const runnableAfterAdmission = actuallyRunnable.filter((id) => !frontendAdmissionSkippedSet.has(id));
458
+ // Shape boundaries are rank gates: evaluate every protected candidate
459
+ // before creating an executor or starting any sibling in the rank.
460
+ const shapeBlocked = new Set();
461
+ const checkedBoundaries = new Set();
462
+ let shapeTerminalCommitted = false;
463
+ for (const id of runnableAfterAdmission) {
464
+ const mapping = boundaryForTargetNode(id);
465
+ if (!mapping || checkedBoundaries.has(mapping.boundary))
466
+ continue;
467
+ checkedBoundaries.add(mapping.boundary);
468
+ const sourceNode = input.state.nodes[mapping.sourceNodeId];
469
+ const fact = input.state.frontendShapeState?.committedFacts[mapping.boundary];
470
+ const outcome = evaluateFrontendShapeBoundary({
471
+ boundary: mapping.boundary,
472
+ sourceNode: sourceNode
473
+ ? { status: sourceNode.status, digest: computeFrontendShapeNodeDigest(sourceNode) }
474
+ : undefined,
475
+ fact,
476
+ });
477
+ if (outcome.kind === "continue") {
478
+ if (input.state.frontendShapeState) {
479
+ input.state.frontendShapeState.lastBoundary = mapping.boundary;
480
+ input.state.frontendShapeState.lastOutcome = outcome.kind;
481
+ await input.persistState();
482
+ }
483
+ continue;
484
+ }
485
+ if (outcome.kind === "blocked") {
486
+ if (input.state.frontendShapeState) {
487
+ input.state.frontendShapeState.lastBoundary = mapping.boundary;
488
+ input.state.frontendShapeState.lastOutcome = outcome.kind;
489
+ }
490
+ const finishedAt = new Date().toISOString();
491
+ for (const targetId of mapping.targetNodeIds) {
492
+ if (!runnableAfterAdmission.includes(targetId))
493
+ continue;
494
+ const node = input.state.nodes[targetId];
495
+ if (!node || node.status !== "PENDING")
496
+ continue;
497
+ node.status = "SKIPPED";
498
+ node.skippedReason = outcome.reason;
499
+ node.finishedAt = finishedAt;
500
+ shapeBlocked.add(targetId);
501
+ }
502
+ await input.persistState();
503
+ continue;
504
+ }
505
+ if (!fact || !input.commitFrontendShapeTransition) {
506
+ const finishedAt = new Date().toISOString();
507
+ for (const targetId of mapping.targetNodeIds) {
508
+ const node = input.state.nodes[targetId];
509
+ if (!node || node.status !== "PENDING")
510
+ continue;
511
+ node.status = "SKIPPED";
512
+ node.skippedReason = "frontend-shape-transition-cas-unavailable";
513
+ node.finishedAt = finishedAt;
514
+ shapeBlocked.add(targetId);
515
+ }
516
+ await input.persistState();
517
+ continue;
518
+ }
519
+ const committed = await input.commitFrontendShapeTransition({
520
+ boundary: mapping.boundary,
521
+ outcome: outcome.kind,
522
+ reason: outcome.reason,
523
+ expectedRevision: input.state.frontendShapeState?.revision ?? 0,
524
+ fact,
525
+ });
526
+ if (committed.ok) {
527
+ shapeTerminalCommitted = true;
528
+ break;
529
+ }
530
+ // A failed transition CAS must remain zero-write. Stop the controller
531
+ // before any executor starts; never compensate with a capsule-less
532
+ // terminal state or mutate pending descendants.
533
+ throw new Error(`frontend shape transition CAS failed at ${mapping.boundary}: ${committed.reason}`);
534
+ }
535
+ if (shapeTerminalCommitted)
536
+ break;
537
+ const runnableAfterShape = runnableAfterAdmission.filter((id) => !shapeBlocked.has(id));
425
538
  const blocked = pending.filter((id) => {
426
539
  const task = input.tasksById.get(id);
427
540
  return (dependencyReadiness(task, input.state.nodes, input.tasksById) === "skip");
@@ -436,12 +549,12 @@ export async function executeDagRanksOnce(input) {
436
549
  if (blocked.length > 0) {
437
550
  await input.persistState();
438
551
  }
439
- const pauseGateRunnable = runnableAfterAdmission.filter((id) => {
552
+ const pauseGateRunnable = runnableAfterShape.filter((id) => {
440
553
  const task = input.tasksById.get(id);
441
554
  return isPauseOnHumanDecisionGate(task);
442
555
  });
443
- const regularRunnable = runnableAfterAdmission.filter((id) => !pauseGateRunnable.includes(id));
444
- const rankWriterNodeIds = runnableAfterAdmission.filter((id) => {
556
+ const regularRunnable = runnableAfterShape.filter((id) => !pauseGateRunnable.includes(id));
557
+ const rankWriterNodeIds = runnableAfterShape.filter((id) => {
445
558
  const task = input.tasksById.get(id);
446
559
  return (task?.executor === "pi" &&
447
560
  task.toolProfile === "write" &&
@@ -1,8 +1,13 @@
1
1
  import { z } from "zod";
2
- import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
2
+ import { DEFAULT_FRONTEND_SPEC_ROOTS, isFrontendSpecFilePath, isValidFrontendSpecRoot, OPENSPEC_SPEC_EXT_RE, } from "../../shared/openspec-spec.js";
3
3
  import { campaignBudgetSchema, } from "../../application/evaluation/budget.js";
4
4
  import { assertDagPromptSourceRule } from "./prompt-source.js";
5
- import { dagRetryPolicySchema, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, } from "./retry-policy.js";
5
+ // Leaf-module import: dagRetryPolicySchema must be defined by a module that
6
+ // never transitively reaches types.ts during init (retry-policy does, via
7
+ // frontend-recovery-capsule → frontend-recovery-lineage →
8
+ // frontend-implementation-contract), or a run whose entry module initializes
9
+ // retry-policy first reads an undefined schema here.
10
+ import { dagRetryPolicySchema, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, } from "./dag-retry-schema.js";
6
11
  import { dagLivenessPolicySchema, } from "./liveness-policy.js";
7
12
  import { dagOutputProtocolSchema } from "./output-protocol.js";
8
13
  export const dagComplexitySchema = z.enum(["HIGH", "MED", "LOW"]);
@@ -82,7 +87,7 @@ export const dagShellVerifyEvidenceSchema = z.object({
82
87
  preflight: z
83
88
  .array(z.object({
84
89
  label: z.string(),
85
- status: z.enum(["ok", "future-delivery-script"]),
90
+ status: z.enum(["ok", "future-delivery-script", "future-test-target"]),
86
91
  }))
87
92
  .default([]),
88
93
  /** Effective timeout applied independently to each shell command. */
@@ -188,7 +193,16 @@ const dagFrontendNodeIdSchema = z
188
193
  .regex(/^[a-z][a-z0-9-]*$/, "frontend node id must be kebab-case");
189
194
  const openspecCandidatePathSchema = z
190
195
  .string()
191
- .refine((candidate) => !candidate.includes("\\") && isOpenspecSpecFilePath(candidate), "openspec candidate must be a repo-relative supported file under openspec/schemas/, openspec/project-specs/, or ai_workspace/");
196
+ .refine((candidate) => !candidate.includes("\\") &&
197
+ !candidate.startsWith("/") &&
198
+ !candidate.split("/").includes("..") &&
199
+ OPENSPEC_SPEC_EXT_RE.test(candidate), "openspec candidate must be a repo-relative supported specification file");
200
+ const openspecCandidateSnapshotSchema = z
201
+ .object({
202
+ path: openspecCandidatePathSchema,
203
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
204
+ })
205
+ .strict();
192
206
  export const dagFrontendPrewriteGateSchema = z.object({
193
207
  schemaVersion: z.literal(1),
194
208
  planFromNodeId: dagFrontendNodeIdSchema,
@@ -207,7 +221,24 @@ export const dagFrontendPrewriteGateSchema = z.object({
207
221
  outputDir: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/),
208
222
  requireSourceFreshness: z.literal(true),
209
223
  implementationWriteSet: z.array(z.string().min(1)).min(1).optional(),
224
+ /**
225
+ * Generation-frozen roots that bound custom normative directories. Omitted
226
+ * legacy DAGs retain the canonical OpenSpec + ai_workspace roots.
227
+ */
228
+ openspecSpecRoots: z
229
+ .array(z.string().refine((root) => isValidFrontendSpecRoot(root), "openspec spec root must be a normalized repo-relative directory"))
230
+ .optional(),
210
231
  openspecCandidatePaths: z.array(openspecCandidatePathSchema).default([]),
232
+ /** Read-only selector output; omitted for scan-strict and legacy DAGs. */
233
+ openspecSelectionNodeId: dagFrontendNodeIdSchema.optional(),
234
+ /** Contract-declared and source-explicit paths that cannot be downgraded. */
235
+ openspecMandatoryPaths: z.array(openspecCandidatePathSchema).optional(),
236
+ /**
237
+ * Content fingerprints frozen with the generation-time OpenSpec scope. New
238
+ * DAGs use these to reject a plan whose cited specification changed after it
239
+ * was read; legacy DAGs without the field retain existence-only drift checks.
240
+ */
241
+ openspecCandidateSnapshots: z.array(openspecCandidateSnapshotSchema).optional(),
211
242
  /**
212
243
  * Generation-frozen component/theme specification bucket. Derived from
213
244
  * `designEvidence.classified.component ∪ theme ∪ rule.components` at DAG
@@ -252,14 +283,28 @@ export const dagFrontendPrewriteGateSchema = z.object({
252
283
  scanStrict: z.array(openspecCandidatePathSchema).default([]),
253
284
  })
254
285
  .optional(),
286
+ }).superRefine((gate, context) => {
287
+ const roots = gate.openspecSpecRoots ?? DEFAULT_FRONTEND_SPEC_ROOTS;
288
+ const candidateGroups = [
289
+ ["openspecCandidatePaths", gate.openspecCandidatePaths],
290
+ ["openspecMandatoryPaths", gate.openspecMandatoryPaths],
291
+ ["componentSpecCandidatePaths", gate.componentSpecCandidatePaths],
292
+ ["openspecCandidateSources.declared", gate.openspecCandidateSources?.declared],
293
+ ["openspecCandidateSources.taskSourceCited", gate.openspecCandidateSources?.taskSourceCited],
294
+ ["openspecCandidateSources.scanStrict", gate.openspecCandidateSources?.scanStrict],
295
+ ];
296
+ for (const [field, paths] of candidateGroups) {
297
+ for (const [index, candidate] of (paths ?? []).entries()) {
298
+ if (!isFrontendSpecFilePath(candidate, roots)) {
299
+ context.addIssue({
300
+ code: z.ZodIssueCode.custom,
301
+ path: [...field.split("."), index],
302
+ message: "openspec candidate must be a supported file under the generation-frozen openspec spec roots",
303
+ });
304
+ }
305
+ }
306
+ }
255
307
  });
256
- export const dagFrontendLintBaselineSchema = z
257
- .object({
258
- schemaVersion: z.literal(1),
259
- lintCommands: z.array(z.string().min(1)).min(1),
260
- lintEvidence: dagShellVerifyEvidenceSchema,
261
- })
262
- .strict();
263
308
  export const dagFrontendVerificationBundleSchema = z
264
309
  .object({
265
310
  schemaVersion: z.literal(1),
@@ -273,6 +318,10 @@ export const dagFrontendVerificationBundleSchema = z
273
318
  behaviorEvidence: dagShellVerifyEvidenceSchema,
274
319
  lintBaselineNodeId: dagFrontendNodeIdSchema.optional(),
275
320
  writerNodeIds: z.array(dagFrontendNodeIdSchema).optional(),
321
+ /** "initial" = first-pass assess bundle; "repair" = the convergence
322
+ * loop's post-repair reverify, which re-runs the same frozen commands
323
+ * and fails on any failure (still no same-run repair branch inside the
324
+ * node itself). */
276
325
  mode: z.enum(["initial", "repair"]),
277
326
  })
278
327
  .superRefine((bundle, context) => {
@@ -348,6 +397,135 @@ export const dagFrontendReviewContextSchema = z.object({
348
397
  schemaVersion: z.literal(1),
349
398
  requireBaseline: z.literal(true),
350
399
  });
400
+ /**
401
+ * Deterministic frontend contract-import shell (micro topology). Re-validates
402
+ * the generation-frozen managed Contract's source/binding/schema freshness and
403
+ * emits a digest. It never invokes a model and never rewrites requirements —
404
+ * micro requires a pre-validated managed Contract, not natural-language intake.
405
+ */
406
+ export const dagFrontendContractImportSchema = z.object({
407
+ schemaVersion: z.literal(1),
408
+ artifactName: z.string().regex(/^[a-z0-9][a-z0-9._-]*\.json$/),
409
+ outputDir: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/),
410
+ requireSourceFreshness: z.literal(true),
411
+ });
412
+ /**
413
+ * Deterministic frontend design-policy shell (M8). Materializes the canonical
414
+ * implementation contract from the plan node and evaluates the design policy
415
+ * (Mock strategy, source freshness, component spec candidates, writeSet
416
+ * patterns, allowed dependencies). Runs BEFORE frontend-design-review-pi, so it
417
+ * never consumes a design verdict; the verdict is enforced by the writer
418
+ * admission shell instead. Replaces the prewrite-gate authorization path.
419
+ */
420
+ export const dagFrontendDesignPolicySchema = z
421
+ .object({
422
+ schemaVersion: z.literal(1),
423
+ planFromNodeId: dagFrontendNodeIdSchema,
424
+ requiredRequirementIds: z
425
+ .array(z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/))
426
+ .default([]),
427
+ allowedMockStrategies: z
428
+ .array(z.enum(["native", "browser-intercept", "request-adapter", "not-needed"]))
429
+ .min(1),
430
+ /** Frozen Mock verification command labels the DAG will actually execute. */
431
+ mockCommandLabels: z.array(z.string()).default([]),
432
+ artifactName: z.string().regex(/^[a-z0-9][a-z0-9._-]*\.json$/),
433
+ outputDir: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/),
434
+ requireSourceFreshness: z.literal(true),
435
+ implementationWriteSet: z.array(z.string().min(1)).min(1).optional(),
436
+ /** Generation-frozen roots that bound custom normative directories. */
437
+ openspecSpecRoots: z
438
+ .array(z.string().refine((root) => isValidFrontendSpecRoot(root), "openspec spec root must be a normalized repo-relative directory"))
439
+ .optional(),
440
+ openspecCandidatePaths: z.array(openspecCandidatePathSchema).default([]),
441
+ /** Read-only selector output; omitted for scan-strict and legacy DAGs. */
442
+ openspecSelectionNodeId: dagFrontendNodeIdSchema.optional(),
443
+ /** Contract-declared and source-explicit paths that cannot be downgraded. */
444
+ openspecMandatoryPaths: z.array(openspecCandidatePathSchema).optional(),
445
+ openspecCandidateSnapshots: z
446
+ .array(openspecCandidateSnapshotSchema)
447
+ .optional(),
448
+ /**
449
+ * Generation-frozen component/theme specification bucket. Derived from
450
+ * `designEvidence.classified.component ∪ theme ∪ rule.components` at DAG
451
+ * generation. When non-empty and the canonical contract has UI-visible
452
+ * work, the design policy requires a non-empty `uiComponentChoices` and
453
+ * cross-checks each `specReference.path` against this set + the effective
454
+ * plan openspec-citations block.
455
+ */
456
+ componentSpecCandidatePaths: z.array(openspecCandidatePathSchema).optional(),
457
+ /** OpenSpec gate policy (cited | scan-strict); generation always writes it. */
458
+ openspecPolicy: z.enum(["cited", "scan-strict"]).optional(),
459
+ /** Generation-frozen candidate source metadata (diagnostic). */
460
+ openspecCandidateSources: z
461
+ .object({
462
+ declared: z.array(openspecCandidatePathSchema).default([]),
463
+ taskSourceCited: z.array(openspecCandidatePathSchema).default([]),
464
+ scanStrict: z.array(openspecCandidatePathSchema).default([]),
465
+ })
466
+ .optional(),
467
+ /** Dependency identifiers authorized at generation time (empty = none). */
468
+ allowedDependencies: z.array(z.string()).default([]),
469
+ })
470
+ .superRefine((gate, context) => {
471
+ const roots = gate.openspecSpecRoots ?? DEFAULT_FRONTEND_SPEC_ROOTS;
472
+ const candidateGroups = [
473
+ ["openspecCandidatePaths", gate.openspecCandidatePaths],
474
+ ["openspecMandatoryPaths", gate.openspecMandatoryPaths],
475
+ ["componentSpecCandidatePaths", gate.componentSpecCandidatePaths],
476
+ ["openspecCandidateSources.declared", gate.openspecCandidateSources?.declared],
477
+ ["openspecCandidateSources.taskSourceCited", gate.openspecCandidateSources?.taskSourceCited],
478
+ ["openspecCandidateSources.scanStrict", gate.openspecCandidateSources?.scanStrict],
479
+ ];
480
+ for (const [field, paths] of candidateGroups) {
481
+ for (const [index, candidate] of (paths ?? []).entries()) {
482
+ if (!isFrontendSpecFilePath(candidate, roots)) {
483
+ context.addIssue({
484
+ code: z.ZodIssueCode.custom,
485
+ path: [...field.split("."), index],
486
+ message: "openspec candidate must be a supported file under the generation-frozen openspec spec roots",
487
+ });
488
+ }
489
+ }
490
+ }
491
+ });
492
+ /**
493
+ * Deterministic frontend writer-admission shell (M8). Runs after
494
+ * frontend-design-review-pi: freezes the writer baseline, derives the concrete
495
+ * writeSet, folds the design-policy result + design verdict into admission,
496
+ * and writes contracts/frontend-writer-admission-result.json
497
+ * (schemaId frontend-writer-admission-shell-v1).
498
+ */
499
+ export const dagFrontendWriterAdmissionShellSchema = z
500
+ .object({
501
+ schemaVersion: z.literal(1),
502
+ /** Design verdict source (frontend-design-review-pi); pass is enforced.
503
+ * Omitted in small topology, which has no design review node. */
504
+ designReviewFromNodeId: dagFrontendNodeIdSchema.optional(),
505
+ /** Frozen command labels the verification bundle will execute. */
506
+ frozenCommandLabels: z.array(z.string()).default([]),
507
+ allowedMockStrategies: z
508
+ .array(z.enum(["native", "browser-intercept", "request-adapter", "not-needed"]))
509
+ .min(1),
510
+ /** Writer-preceding lint commands; baseline is materialized here (R5). */
511
+ lintCommands: z.array(z.string().min(1)).optional(),
512
+ lintEvidence: dagShellVerifyEvidenceSchema.optional(),
513
+ })
514
+ .superRefine((admission, context) => {
515
+ if ((admission.lintCommands?.length ?? 0) > 0 && !admission.lintEvidence) {
516
+ context.addIssue({
517
+ code: z.ZodIssueCode.custom,
518
+ path: ["lintEvidence"],
519
+ message: "lintEvidence is required when lintCommands are present",
520
+ });
521
+ }
522
+ });
523
+ /** Deterministic frontend closeout shell (M8). Renders the closeout from
524
+ * committed facts only (review context + typed review terminal + trace). */
525
+ export const dagFrontendCloseoutSchema = z.object({
526
+ schemaVersion: z.literal(1),
527
+ reviewFromNodeId: dagFrontendNodeIdSchema,
528
+ });
351
529
  export const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
352
530
  export const dagVersionSchema = z
353
531
  .union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)])
@@ -395,7 +573,14 @@ export const dagWriteGuardPolicySchema = z.enum(["full", "tools-only", "filesyst
395
573
  */
396
574
  export const dagWriterOutcomePolicySchema = z
397
575
  .object({
398
- type: z.literal("implementation-outcome-v1"),
576
+ /**
577
+ * Protocol face: `implementation-outcome-v1` keeps the legacy
578
+ * IMPLEMENTATION_OUTCOME first-line protocol (backend/README/module
579
+ * writers + frontend-repair-pi); `frontend-facts-v1` derives the status
580
+ * from mechanical facts (write-tool events / run delta / write guard /
581
+ * requirement coverage / focused-check) and never reads the first line.
582
+ */
583
+ type: z.enum(["implementation-outcome-v1", "frontend-facts-v1"]),
399
584
  /** Generation writers may forbid already-satisfied/no-diff completion. */
400
585
  requireChangedFiles: z.boolean().optional(),
401
586
  })
@@ -528,10 +713,13 @@ export const dagShellConfigSchema = z.object({
528
713
  .optional(),
529
714
  requirementCoverageGate: dagRequirementCoverageGateSchema.optional(),
530
715
  jsonArtifactGate: dagJsonArtifactGateSchema.optional(),
716
+ frontendDesignPolicy: dagFrontendDesignPolicySchema.optional(),
717
+ frontendWriterAdmission: dagFrontendWriterAdmissionShellSchema.optional(),
531
718
  frontendPrewriteGate: dagFrontendPrewriteGateSchema.optional(),
532
- frontendLintBaseline: dagFrontendLintBaselineSchema.optional(),
533
719
  frontendVerificationBundle: dagFrontendVerificationBundleSchema.optional(),
534
720
  frontendReviewContext: dagFrontendReviewContextSchema.optional(),
721
+ frontendContractImport: dagFrontendContractImportSchema.optional(),
722
+ frontendCloseout: dagFrontendCloseoutSchema.optional(),
535
723
  frontendTestCaseChecklist: dagFrontendTestCaseChecklistSchema.optional(),
536
724
  frontendBrowserToolPreflight: dagFrontendBrowserToolPreflightSchema.optional(),
537
725
  frontendTestStandardScenarios: dagFrontendTestStandardScenariosSchema.optional(),
@@ -821,6 +1009,19 @@ export const dagTaskSchema = z.object({
821
1009
  role: dagRoleSchema.optional(),
822
1010
  skills: z.array(z.string()).optional(),
823
1011
  toolProfile: dagToolProfileSchema.optional(),
1012
+ /** Bounded read-only discovery budget for nodes that inspect a repository. */
1013
+ readBudget: z
1014
+ .object({
1015
+ maxFiles: z.number().int().positive(),
1016
+ maxBytes: z.number().int().positive(),
1017
+ maxMs: z.number().int().positive().optional(),
1018
+ /** Maximum read/grep/find/ls calls. */
1019
+ maxOperations: z.number().int().positive().optional(),
1020
+ /** On any read budget exhaustion, return concise completion guidance instead of failing the node. */
1021
+ onExhaustion: z.enum(["return-guidance"]).optional(),
1022
+ })
1023
+ .strict()
1024
+ .optional(),
824
1025
  /**
825
1026
  * Explicit Pi extension allowlist (backend implementation DAGs only):
826
1027
  * frozen short ids resolved at run time against the Pi settings package
@@ -1189,6 +1390,22 @@ export const dagSpecSchema = z
1189
1390
  taskContractBinding: dagTaskContractBindingSchema.optional(),
1190
1391
  /** Optional unresolved parent-run feedback for a full task rerun. */
1191
1392
  rerunFeedback: dagRerunFeedbackSchema.optional(),
1393
+ /** Frontend recovery continuation quota (frozen at generation time from
1394
+ * task.json frontendRecovery.maxContinuations). Bounds how many times a
1395
+ * failed frontend run may auto-recover via M6 recovery operations before
1396
+ * the run stays failed and needs human intervention. */
1397
+ frontendRecovery: z
1398
+ .object({
1399
+ maxContinuations: z
1400
+ .number()
1401
+ .int()
1402
+ .min(1)
1403
+ .max(5)
1404
+ .optional()
1405
+ .default(1),
1406
+ })
1407
+ .strict()
1408
+ .optional(),
1192
1409
  /** Backend-test-only workspace mutation control; absent means Git. */
1193
1410
  backendTestWorkspaceControl: z.enum(["git", "filesystem-only"]).optional(),
1194
1411
  /** Frozen backend-test artifact layout (plan A); absent = historical default testcase/ layout. */
@@ -1315,6 +1532,9 @@ export const FRONTEND_RECOVERY_RESULT_SCHEMA_VERSION = 1;
1315
1532
  * Phase 3a parents converge first, so `rollback-pending` is not produced and
1316
1533
  * was removed by AC-1; the phase starts at `child-staging` and ends at
1317
1534
  * `settled`.
1535
+ *
1536
+ * @deprecated Read-only compatibility only; the M6 operation controller no
1537
+ * longer uses this phase as the authoritative recovery entry point.
1318
1538
  */
1319
1539
  export const frontendRecoveryPhaseSchema = z.enum([
1320
1540
  "child-staging",
@@ -1328,12 +1548,16 @@ export const frontendRecoveryPhaseSchema = z.enum([
1328
1548
  * the child carries a frozen lineage snapshot so the activation gate can prove
1329
1549
  * it is the reserved child of a child-running parent.
1330
1550
  *
1331
- * Invariants: `attemptIndex ∈ {0,1}`; `continuationCount ∈ {0,1}`; a child's
1551
+ * Invariants: `attemptIndex` and `continuationCount` are bounded by the frozen
1552
+ * frontend recovery quota (currently 1-5); a child's
1332
1553
  * `recoveryRootRunId` always equals the root runId; `revision` is the CAS
1333
1554
  * pre-comparison counter. `revision` monotonicity is enforced at runtime by the
1334
1555
  * runner's `(existing?.revision ?? 0) + 1` CAS write (the schema only bounds it
1335
1556
  * non-negative). `childRunId` may be absent through `child-activating` and must
1336
1557
  * be non-empty from `child-running` onward.
1558
+ *
1559
+ * @deprecated Read-only compatibility only; the runner no longer writes this
1560
+ * intent (the M6 operation controller supervises recovery instead).
1337
1561
  */
1338
1562
  export const frontendRecoveryStateSchema = z
1339
1563
  .object({
@@ -1344,8 +1568,8 @@ export const frontendRecoveryStateSchema = z
1344
1568
  parentRunId: z.string().min(1),
1345
1569
  childRunId: z.string().min(1).optional(),
1346
1570
  attemptId: z.string().min(1),
1347
- attemptIndex: z.number().int().min(0).max(1),
1348
- continuationCount: z.number().int().min(0).max(1),
1571
+ attemptIndex: z.number().int().min(0).max(5),
1572
+ continuationCount: z.number().int().min(0).max(5),
1349
1573
  /** Node id of the reset-closure root: frontend-plan-pi /
1350
1574
  * frontend-plan-revision-pi / frontend-prewrite-gate-shell /
1351
1575
  * frontend-implement-pi (writer partial write). */
@@ -1405,6 +1629,10 @@ export const frontendRecoveryEvidenceRefSchema = z
1405
1629
  * stays permissive (failureClass optional, evidenceRefs may be empty) so it
1406
1630
  * accepts the runner's actual phase-3a products, which do not always carry a
1407
1631
  * failure class or evidence reference.
1632
+ *
1633
+ * @deprecated Read-only compatibility only; the M6 operation controller writes
1634
+ * `recovered`/`auto-recovery-blocked` results directly and never uses this
1635
+ * schema as the authoritative recovery settlement.
1408
1636
  */
1409
1637
  export const frontendRecoveryResultSchema = z
1410
1638
  .object({