@tea-agent/loop-agent 0.44.0-next.1 → 0.44.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 (245) hide show
  1. package/CHANGELOG.md +379 -416
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
  22. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
  23. package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
  24. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  25. package/dist/executors/dag-pi/plan/record-tools.js +861 -0
  26. package/dist/executors/dag-pi/plan/schema.js +129 -0
  27. package/dist/executors/dag-pi/plan/session.js +1 -0
  28. package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
  29. package/dist/executors/dag-pi/sessions/index.js +6 -0
  30. package/dist/executors/dag-pi/sessions/plan-batches.js +280 -0
  31. package/dist/executors/dag-pi/sessions/plan-prompts.js +367 -0
  32. package/dist/executors/dag-pi/sessions/scout-parallel.js +197 -0
  33. package/dist/executors/dag-pi/sessions/segmented-plan.js +1184 -0
  34. package/dist/executors/dag-pi/sessions/writer-evidence.js +60 -0
  35. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  36. package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
  37. package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
  38. package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
  39. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  40. package/dist/executors/dag-pi-executor.js +733 -5782
  41. package/dist/executors/pi-executor.js +6 -1
  42. package/dist/executors/pi-sdk-executor.js +112 -19
  43. package/dist/executors/shell-executor.js +88 -12
  44. package/dist/executors/shell-presets.js +1 -1
  45. package/dist/executors/shell-write-guard.js +26 -3
  46. package/dist/infrastructure/console/operation-store.js +48 -14
  47. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  48. package/dist/shared/file-identity.js +23 -0
  49. package/dist/shared/git-workspace-status.js +482 -0
  50. package/dist/shared/operator/capabilities.js +10 -5
  51. package/dist/shared/path-safety.js +2 -2
  52. package/dist/shared/wizard-local-telemetry.js +51 -0
  53. package/dist/task/config-types.js +22 -8
  54. package/dist/task/contract/project.js +13 -0
  55. package/dist/task/contract/schema.js +2 -7
  56. package/dist/task/frontend-preflight.js +1 -1
  57. package/dist/task/source-prepare/parse-intent.js +124 -5
  58. package/dist/task/source-prepare/prepare.js +6 -1
  59. package/dist/task/source-prepare/semantic-intake.js +18 -3
  60. package/dist/task/source-prepare/source-execution.js +65 -0
  61. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  62. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  63. package/dist/worker/console/chat/assistant-content.js +5 -34
  64. package/dist/worker/console/chat/chat-event-store.js +62 -10
  65. package/dist/worker/console/chat/context-insights.js +723 -0
  66. package/dist/worker/console/chat/conversation-nodes.js +383 -0
  67. package/dist/worker/console/chat/db-connections.js +854 -0
  68. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  69. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
  70. package/dist/worker/console/chat/pi-runtime.js +445 -36
  71. package/dist/worker/console/chat/provider-error.js +0 -12
  72. package/dist/worker/console/chat/repo-browser.js +52 -4
  73. package/dist/worker/console/chat/routes.js +577 -72
  74. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  75. package/dist/worker/console/chat/session-store.js +147 -19
  76. package/dist/worker/console/chat/shortcuts.js +7 -0
  77. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  78. package/dist/worker/console/chat/tools.js +34 -0
  79. package/dist/worker/console/chat/turn-process.js +237 -66
  80. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  81. package/dist/worker/console/console-update-runtime.js +2 -2
  82. package/dist/worker/console/dag-confirmation.js +3 -0
  83. package/dist/worker/console/dag-execution-receipt.js +3 -0
  84. package/dist/worker/console/operator-actions.js +32 -3
  85. package/dist/worker/console/operator-user-error.js +28 -1
  86. package/dist/worker/console/pi-readiness.js +4 -4
  87. package/dist/worker/console/prd-intake-bridge.js +5 -12
  88. package/dist/worker/console/recovery-cta.js +14 -14
  89. package/dist/worker/console/server.js +5 -1
  90. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  91. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
  92. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  93. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  94. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  95. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  96. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  100. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  101. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  102. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  106. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  107. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  108. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  109. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  110. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  111. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  112. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  113. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  114. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  115. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  116. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  117. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  118. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  119. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  120. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  121. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  122. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  123. package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
  124. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  125. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  126. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  127. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  128. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
  129. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
  130. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  131. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  132. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  133. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  134. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  135. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  136. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  137. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  138. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  139. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  140. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  141. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  142. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  143. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  144. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  145. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  146. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  147. package/dist/worker/console/static/index.html +2 -2
  148. package/dist/worker/console/static-src/app/console-types.js +9 -1
  149. package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
  150. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  151. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
  152. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  153. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  154. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  155. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  156. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  157. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  158. package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
  159. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  160. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  161. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
  162. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  163. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  164. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  165. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
  166. package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
  167. package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
  168. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  169. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  170. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  171. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  172. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  173. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  174. package/dist/worker/console/workspace-context.js +2 -0
  175. package/dist/worker/console/workspace-initialization.js +3 -3
  176. package/dist/worker/observability/read-model.js +23 -2
  177. package/dist/worker/observe/node-context-insights.js +479 -0
  178. package/dist/worker/observe/static/console-theme.css +174 -0
  179. package/dist/worker/observe/static/console-theme.js +36 -0
  180. package/dist/worker/observe/static/operator-chrome.css +85 -0
  181. package/dist/worker/observe/static/operator-chrome.js +51 -0
  182. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  183. package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
  184. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  185. package/dist/workflows/dag/budget-enforcement.js +31 -2
  186. package/dist/workflows/dag/checkpoint.js +686 -0
  187. package/dist/workflows/dag/frontend-closeout.js +3 -1
  188. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  189. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  190. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  191. package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
  192. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  193. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  194. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  195. package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
  196. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  197. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  198. package/dist/workflows/dag/frontend-review-context.js +29 -1
  199. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  200. package/dist/workflows/dag/frontend-risk.js +8 -3
  201. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  202. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  203. package/dist/workflows/dag/frontend-session-context.js +10 -0
  204. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  205. package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
  206. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  207. package/dist/workflows/dag/frontend-writer-admission.js +3 -1
  208. package/dist/workflows/dag/hybrid/sources.js +1812 -0
  209. package/dist/workflows/dag/hybrid/templates/backend-test.js +1807 -0
  210. package/dist/workflows/dag/hybrid/templates/frontend-test.js +702 -0
  211. package/dist/workflows/dag/hybrid/templates/frontend.js +1529 -0
  212. package/dist/workflows/dag/hybrid/templates/index.js +8 -0
  213. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +449 -0
  214. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +495 -0
  215. package/dist/workflows/dag/hybrid/templates/shared.js +101 -0
  216. package/dist/workflows/dag/hybrid/templates/standard.js +265 -0
  217. package/dist/workflows/dag/hybrid/types.js +32 -0
  218. package/dist/workflows/dag/init-hybrid.js +57 -7106
  219. package/dist/workflows/dag/node-execution.js +11 -11
  220. package/dist/workflows/dag/output-protocol.js +1 -0
  221. package/dist/workflows/dag/rerun-feedback.js +35 -4
  222. package/dist/workflows/dag/rerun-task.js +62 -66
  223. package/dist/workflows/dag/runner.js +38 -1159
  224. package/dist/workflows/dag/terminal-status.js +502 -0
  225. package/dist/workflows/dag/types.js +15 -8
  226. package/dist/workflows/loop/benchmark.js +11 -8
  227. package/docs/README.md +1 -1
  228. package/docs/architecture/dag-execution.md +1 -1
  229. package/docs/architecture/runtime-boundaries.md +1 -1
  230. package/docs/governance/harness-methodology-verification.md +11 -0
  231. package/docs/templates/backend-test-dag.json +5 -4
  232. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  233. package/package.json +6 -4
  234. package/skills/frontend-design-review/SKILL.md +8 -11
  235. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  236. package/skills/frontend-review/SKILL.md +5 -1
  237. package/skills/loop-agent/references/command-reference.md +8 -0
  238. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  239. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  240. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  241. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  242. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  243. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  244. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  245. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -790,7 +790,7 @@ export async function executePiStep(options) {
790
790
  reuseRuntimeActive,
791
791
  });
792
792
  }
793
- if (options.reserveProviderRequest || !shouldFallbackSdkToCli(sdkResult, validationIssues, options)) {
793
+ if (options.frontendPlanControl || options.reserveProviderRequest || !shouldFallbackSdkToCli(sdkResult, validationIssues, options)) {
794
794
  const failureCategory = validationIssues.length > 0
795
795
  ? "invalid-output"
796
796
  : sdkResult.failureCategory;
@@ -1104,6 +1104,11 @@ export function classifyPiFailure(input) {
1104
1104
  // Provider/transport verdicts read error-shaped channels only; the raw stdout
1105
1105
  // stream embeds assistant text, tool arguments and findings verbatim.
1106
1106
  const combined = `${input.stderr}\n${extractProviderFailureText(input.stdout)}`.toLowerCase();
1107
+ // An explicit spend-budget stop is terminal even if a transport wrapper also
1108
+ // reports a closed connection. Read only the error channels above, never
1109
+ // assistant/tool prose; generic quota and context limits keep their categories.
1110
+ if (/(?<![\w-])(?:frontend_provider_budget_exhausted|budget_exhausted|budget_breach|budget exhausted)(?![\w-])/.test(combined))
1111
+ return "budget_breach";
1107
1112
  // Context overflow (400 request-too-large / context window exceeded).
1108
1113
  // Must be detected BEFORE the generic rate-limit/quota phrases: an overflow
1109
1114
  // error often contains "too many tokens" / "maximum context" which would
@@ -9,33 +9,65 @@ import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DE
9
9
  import { serializeSessionEvent } from "./pi-event-serializer.js";
10
10
  import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
11
11
  import { createCompactionController, createContextPressureExtension, createSiftBridgeState, inMemoryCompactionSettings, isDagSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, SIFT_RETRIEVE_TOOL_NAME, } from "../shared/pi-context-pressure/index.js";
12
+ import { DAG_TELEMETRY_SESSION_ID_ENV, injectWizardLocalTelemetryHeaders, isTelemetrySessionId, } from "../shared/wizard-local-telemetry.js";
12
13
  /** Maximum number of same-session continuations after provider output truncation. */
13
14
  export const PI_SDK_OUTPUT_LIMIT_CONTINUE_MAX = 3;
14
15
  export const PI_SDK_OUTPUT_LIMIT_CONTINUE_PROMPT = "继续。上一轮因输出长度上限(stopReason=length)截断;当前会话、工具与已完成工作仍保留。只完成尚未完成的部分,不重做已成功的写入或事实提交。";
16
+ export function createDagWizardLocalTelemetryHeadersExtension(telemetrySessionId) {
17
+ if (!isTelemetrySessionId(telemetrySessionId))
18
+ return undefined;
19
+ return {
20
+ name: "dag-wizard-local-telemetry-headers",
21
+ hidden: true,
22
+ factory: (api) => {
23
+ api.on("before_provider_headers", async (rawEvent, context) => {
24
+ if (!context ||
25
+ !rawEvent.headers ||
26
+ typeof rawEvent.headers !== "object") {
27
+ return;
28
+ }
29
+ await injectWizardLocalTelemetryHeaders({
30
+ headers: rawEvent.headers,
31
+ context,
32
+ telemetrySessionId,
33
+ });
34
+ });
35
+ },
36
+ };
37
+ }
15
38
  /** Wrap the actual stream seam: tool turns, retries and compaction all await it. */
16
- export async function installPiProviderRequestBudget(session, reserve, onFailure, capacity) {
39
+ export async function installPiProviderRequestBudget(session, reserve, onFailure, capacity, plan) {
17
40
  const agent = session.agent;
18
41
  const original = agent?.streamFunction;
19
42
  if (!agent || typeof original !== "function")
20
- throw Error("FRONTEND_PROVIDER_BUDGET_UNSUPPORTED: SDK actual request stream unavailable");
43
+ throw new Error("FRONTEND_PROVIDER_BUDGET_UNSUPPORTED: SDK actual request stream unavailable");
21
44
  const { createAssistantMessageEventStream } = await import("@earendil-works/pi-ai");
22
45
  agent.streamFunction = async (...args) => {
23
46
  try {
24
47
  if (args[2]?.signal?.aborted)
25
- throw Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
48
+ throw new Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
49
+ const protocolStop = plan?.readStop();
50
+ if (protocolStop)
51
+ throw Object.assign(new Error(`${protocolStop.code}: ${protocolStop.error}`), { frontendPlanStop: true });
26
52
  if (capacity) {
27
53
  const measurement = measureFrontendRequestEnvelope(args[1], args[0], capacity.policy);
28
54
  capacity.observe(measurement);
29
55
  if (measurement.exceedsEstimate)
30
- throw Error(`FRONTEND_CONTEXT_ESTIMATE_EXCEEDED: estimated=${measurement.estimatedTokens} available=${measurement.availableInputTokens}; estimator=${measurement.estimatorVersion}; registry capability is not a measured gateway limit`);
56
+ throw new Error(`FRONTEND_CONTEXT_ESTIMATE_EXCEEDED: estimated=${measurement.estimatedTokens} available=${measurement.availableInputTokens}; estimator=${measurement.estimatorVersion}; registry capability is not a measured gateway limit`);
31
57
  }
32
58
  await reserve();
33
59
  if (args[2]?.signal?.aborted)
34
- throw Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
60
+ throw new Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
61
+ const stoppedDuringReservation = plan?.readStop();
62
+ if (stoppedDuringReservation)
63
+ throw Object.assign(new Error(`${stoppedDuringReservation.code}: ${stoppedDuringReservation.error}`), { frontendPlanStop: true });
35
64
  }
36
65
  catch (error) {
37
66
  const message = error instanceof Error ? error.message : String(error);
38
- onFailure(message);
67
+ if (error.frontendPlanStop)
68
+ plan?.onStop();
69
+ else
70
+ onFailure(message);
39
71
  const stream = createAssistantMessageEventStream();
40
72
  const model = args[0] ?? {};
41
73
  stream.push({ type: "error", reason: "error", error: { role: "assistant", content: [], api: model.api ?? "unknown", provider: model.provider ?? "unknown", model: model.id ?? "unknown", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, stopReason: "error", errorMessage: message, timestamp: Date.now() } });
@@ -101,7 +133,7 @@ export function setPiSdkImportOverrideForTests(fn) {
101
133
  export function setPiSdkModuleOverrideForTests(fn) {
102
134
  sdkModuleOverrideForTests = fn;
103
135
  }
104
- /** Check whether the Pi SDK runtime dependency satisfies the 0.83.0 runtime contract. */
136
+ /** Check whether the Pi SDK runtime dependency satisfies the 0.84.4 runtime contract. */
105
137
  export async function checkPiSdkAvailability(_repoRoot) {
106
138
  if (sdkSessionFactoryOverride) {
107
139
  return { ok: true, detail: "pi SDK session factory override active" };
@@ -117,10 +149,10 @@ export async function checkPiSdkAvailability(_repoRoot) {
117
149
  typeof ModelRuntime?.create !== "function") {
118
150
  return {
119
151
  ok: false,
120
- detail: "pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.83.0 contract)",
152
+ detail: "pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.84.4 contract)",
121
153
  };
122
154
  }
123
- return { ok: true, detail: "pi SDK 0.83.0 contract available" };
155
+ return { ok: true, detail: "pi SDK 0.84.4 contract available" };
124
156
  }
125
157
  catch (error) {
126
158
  const message = error instanceof Error ? error.message : String(error);
@@ -290,6 +322,8 @@ async function createSdkSession(sdk, input, shared) {
290
322
  controller,
291
323
  sift,
292
324
  });
325
+ const telemetryExtension = createDagWizardLocalTelemetryHeadersExtension(input.telemetrySessionId);
326
+ const extensionFactories = [pressureExtension, telemetryExtension].filter((extension) => Boolean(extension));
293
327
  const loader = new DefaultResourceLoader({
294
328
  cwd: input.cwd,
295
329
  agentDir,
@@ -299,7 +333,7 @@ async function createSdkSession(sdk, input, shared) {
299
333
  ...(input.additionalExtensionPaths && input.additionalExtensionPaths.length > 0
300
334
  ? { additionalExtensionPaths: input.additionalExtensionPaths }
301
335
  : {}),
302
- ...(pressureExtension ? { extensionFactories: [pressureExtension] } : {}),
336
+ ...(extensionFactories.length > 0 ? { extensionFactories } : {}),
303
337
  ...(settingsManager ? { settingsManager } : {}),
304
338
  appendSystemPromptOverride: (base) => [
305
339
  ...base,
@@ -625,6 +659,34 @@ async function executeSingleSdkAttemptInternal(options) {
625
659
  let terminationConfirmed = true;
626
660
  let stderr = "";
627
661
  let providerBudgetFailure;
662
+ let providerBudgetAbort = false;
663
+ let providerBudgetStopRequested = false;
664
+ let resolveProviderBudgetStop;
665
+ const providerBudgetStopPromise = new Promise(resolve => { resolveProviderBudgetStop = resolve; });
666
+ const frontendBudgetEnabled = Boolean(options.reserveProviderRequest || options.frontendExecutionPolicy || options.frontendPlanControl);
667
+ const observeProviderBudgetError = (message) => {
668
+ if (!frontendBudgetEnabled || typeof message !== "string" || providerBudgetStopRequested)
669
+ return;
670
+ if (classifyPiFailure({ assistantText: "", exitCode: 1, outputTooLarge: false, stderr: message, stdout: "", timedOut: false }) !== "budget_breach")
671
+ return;
672
+ providerBudgetFailure = message;
673
+ providerBudgetStopRequested = true;
674
+ if (!stderr.includes(message))
675
+ appendStderr(message);
676
+ resolveProviderBudgetStop("provider-budget");
677
+ };
678
+ let protocolStop;
679
+ let protocolAbort = false;
680
+ let unsubscribePlan;
681
+ let resolveProtocolStop;
682
+ const protocolStopPromise = new Promise(resolve => { resolveProtocolStop = resolve; });
683
+ const requestProtocolStop = () => {
684
+ const stop = options.frontendPlanControl?.readStop();
685
+ if (stop && stopReason !== "aborted" && !providerBudgetFailure) {
686
+ protocolStop = stop;
687
+ resolveProtocolStop("protocol-stop");
688
+ }
689
+ };
628
690
  let capabilitiesObservation = frontendModelCapabilities(undefined);
629
691
  const requestEnvelopeObservation = { peakBytes: 0, peakEstimatedTokens: 0, estimatorVersion: "utf8-bytes-div3-v1-estimate" };
630
692
  const stdoutPreview = new BoundedTextPreview("stdout");
@@ -718,6 +780,9 @@ async function executeSingleSdkAttemptInternal(options) {
718
780
  provider: modelConfig.provider,
719
781
  model: modelConfig.model,
720
782
  thinking: modelConfig.thinking,
783
+ telemetrySessionId: isTelemetrySessionId(process.env[DAG_TELEMETRY_SESSION_ID_ENV])
784
+ ? process.env[DAG_TELEMETRY_SESSION_ID_ENV]
785
+ : undefined,
721
786
  ...(Array.isArray(writerCustomTools) && writerCustomTools.length > 0
722
787
  ? { customTools: writerCustomTools }
723
788
  : {}),
@@ -730,12 +795,14 @@ async function executeSingleSdkAttemptInternal(options) {
730
795
  : {}),
731
796
  });
732
797
  capabilitiesObservation = frontendModelCapabilities(session.agent?.state?.model);
733
- if (options.reserveProviderRequest || options.frontendExecutionPolicy)
734
- await installPiProviderRequestBudget(session, options.reserveProviderRequest ?? (async () => { }), message => { providerBudgetFailure = message; appendStderr(message); }, options.frontendExecutionPolicy ? { policy: options.frontendExecutionPolicy, observe: measurement => {
798
+ if (options.frontendPlanControl || options.reserveProviderRequest || options.frontendExecutionPolicy)
799
+ await installPiProviderRequestBudget(session, options.reserveProviderRequest ?? (async () => { }), message => { if (protocolAbort && message.includes("REQUEST_ABORTED"))
800
+ return; providerBudgetFailure = message; appendStderr(message); observeProviderBudgetError(message); }, options.frontendExecutionPolicy ? { policy: options.frontendExecutionPolicy, observe: measurement => {
735
801
  capabilitiesObservation = measurement.capabilities;
736
802
  requestEnvelopeObservation.peakBytes = Math.max(requestEnvelopeObservation.peakBytes, measurement.bytes);
737
803
  requestEnvelopeObservation.peakEstimatedTokens = Math.max(requestEnvelopeObservation.peakEstimatedTokens, measurement.estimatedTokens);
738
- } } : undefined);
804
+ } } : undefined, options.frontendPlanControl ? { readStop: options.frontendPlanControl.readStop, onStop: requestProtocolStop } : undefined);
805
+ unsubscribePlan = options.frontendPlanControl?.subscribe(requestProtocolStop);
739
806
  let resolveStall;
740
807
  let resolveSettlement;
741
808
  let resolveContextBudget;
@@ -816,13 +883,23 @@ async function executeSingleSdkAttemptInternal(options) {
816
883
  }
817
884
  }
818
885
  const observationMessage = isRecord(event.message) ? event.message : undefined;
819
- if (typeof observationMessage?.errorMessage === "string")
886
+ observeProviderBudgetError(observationMessage?.errorMessage ?? (event.type === "auto_retry_start" ? event.errorMessage : undefined));
887
+ if (providerBudgetStopRequested && event.type === "auto_retry_start") {
888
+ // Pi emits the notification before installing its retry controller.
889
+ // Cancel after that synchronous setup, including when abort() already
890
+ // ran on message_end and is still waiting for the session to idle.
891
+ queueMicrotask(() => { try {
892
+ session?.abortRetry?.();
893
+ }
894
+ catch { /* abort/dispose confirmation remains authoritative */ } });
895
+ }
896
+ if (typeof observationMessage?.errorMessage === "string" && !(protocolStop && observationMessage.errorMessage === `${protocolStop.code}: ${protocolStop.error}`))
820
897
  providerErrorMessage = observationMessage.errorMessage;
821
898
  try {
822
899
  readObservation.observe(event);
823
900
  }
824
901
  catch { /* observation must not change execution */ }
825
- if (!(typeof observationMessage?.errorMessage === "string" && observationMessage.errorMessage.startsWith("FRONTEND_PROVIDER_")) && (event.usage || observationMessage?.usage || event.tokenUsage))
902
+ if (!(typeof observationMessage?.errorMessage === "string" && /^(FRONTEND_PROVIDER_|FRONTEND_PLAN_NO_PROGRESS|FRONTEND_CONTEXT_ESTIMATE_EXCEEDED)/.test(observationMessage.errorMessage)) && (event.usage || observationMessage?.usage || event.tokenUsage))
826
903
  observationEvents.push({ responseId: event.responseId, messageId: event.messageId, usage: event.usage, tokenUsage: event.tokenUsage, message: observationMessage ? { role: observationMessage.role, id: observationMessage.id, responseId: observationMessage.responseId, usage: observationMessage.usage } : undefined });
827
904
  const usageSample = extractSdkUsageSample(event);
828
905
  if (usageSample)
@@ -870,8 +947,10 @@ async function executeSingleSdkAttemptInternal(options) {
870
947
  promptPromise.then(() => "done"),
871
948
  settlementPromise,
872
949
  ...(timeoutPromise ? [timeoutPromise] : []),
950
+ ...(frontendBudgetEnabled ? [providerBudgetStopPromise] : []),
873
951
  ...(stallPromise ? [stallPromise] : []),
874
952
  ...(contextBudgetPromise ? [contextBudgetPromise] : []),
953
+ ...(options.frontendPlanControl ? [protocolStopPromise] : []),
875
954
  ]);
876
955
  };
877
956
  const disposeSession = async () => {
@@ -900,7 +979,18 @@ async function executeSingleSdkAttemptInternal(options) {
900
979
  let continuesSent = 0;
901
980
  let raced = await runTurn(promptMessage);
902
981
  while (true) {
903
- if (raced === "context-budget") {
982
+ if (providerBudgetStopRequested) {
983
+ providerBudgetAbort = true;
984
+ abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
985
+ await disposeSession();
986
+ }
987
+ else if ((raced === "protocol-stop" || ((raced === "done" || raced === "settled") && protocolStop)) && !providerBudgetFailure && !contextBudgetExhausted && stopReason !== "aborted") {
988
+ protocolAbort = true;
989
+ appendStderr(`${protocolStop.code}: ${protocolStop.error}; fingerprint=${protocolStop.fingerprint}`);
990
+ abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
991
+ await disposeSession();
992
+ }
993
+ else if (raced === "context-budget" || contextBudgetExhausted) {
904
994
  appendStderr(contextBudgetDetail);
905
995
  abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
906
996
  await disposeSession();
@@ -922,6 +1012,8 @@ async function executeSingleSdkAttemptInternal(options) {
922
1012
  maxContinues: PI_SDK_STALL_CONTINUE_MAX,
923
1013
  });
924
1014
  raced = await runTurn(PI_SDK_STALL_CONTINUE_PROMPT);
1015
+ if (raced === "protocol-stop" || raced === "context-budget" || raced === "provider-budget")
1016
+ continue;
925
1017
  if (raced !== "done" && raced !== "settled") {
926
1018
  await failClosedAfterWait(raced === "stall" ? "stall" : "absolute-timeout", raced === "stall"
927
1019
  ? "pi SDK stall continue did not recover; treating as timeout"
@@ -962,6 +1054,7 @@ async function executeSingleSdkAttemptInternal(options) {
962
1054
  if (stallHandle)
963
1055
  clearTimeout(stallHandle);
964
1056
  unsubscribe?.();
1057
+ unsubscribePlan?.();
965
1058
  if (session && !disposeAttempted) {
966
1059
  disposeAttempted = true;
967
1060
  const disposeConfirmed = await runSessionActionWithGrace("dispose", () => session.dispose());
@@ -976,7 +1069,7 @@ async function executeSingleSdkAttemptInternal(options) {
976
1069
  }
977
1070
  }
978
1071
  }
979
- if (providerErrorMessage && stopReason === "error" && !stderr)
1072
+ if (providerErrorMessage && stopReason === "error" && !stderr.includes(providerErrorMessage))
980
1073
  appendStderr(providerErrorMessage);
981
1074
  if (options.outputLimitRecovery && stopReason === "length" && !committedTerminalObserved && outputContinuesSent >= PI_SDK_OUTPUT_LIMIT_CONTINUE_MAX) {
982
1075
  const note = serializeSessionEvent({ type: "loop-agent-output-limit-exhausted", maxContinues: PI_SDK_OUTPUT_LIMIT_CONTINUE_MAX });
@@ -996,9 +1089,9 @@ async function executeSingleSdkAttemptInternal(options) {
996
1089
  ? collected.parsedEvents
997
1090
  : (fallbackParsed?.parsedEvents ?? 0);
998
1091
  const tokensUsed = aggregateSdkTokenUsage(usageSamples);
999
- const cancelled = stopReason === "aborted";
1092
+ const cancelled = stopReason === "aborted" && !protocolAbort && !providerBudgetAbort;
1000
1093
  const outputLimitExhausted = Boolean(options.outputLimitRecovery && stopReason === "length" && !committedTerminalObserved && !timedOut && !stderr);
1001
- const failureCategory = !terminationConfirmed ? "termination-unconfirmed" : stopReason === "aborted" ? "cancelled" : outputLimitExhausted ? "output-limit" : providerBudgetFailure ? (providerBudgetFailure.includes("REQUEST_ABORTED") ? "interrupted" : providerBudgetFailure.startsWith("FRONTEND_CONTEXT_ESTIMATE_EXCEEDED") ? "context-budget-exhausted" : "budget_breach") : terminationConfirmed
1094
+ const failureCategory = !terminationConfirmed ? "termination-unconfirmed" : cancelled ? "cancelled" : outputLimitExhausted ? "output-limit" : providerBudgetFailure ? (providerBudgetFailure.includes("REQUEST_ABORTED") ? "interrupted" : providerBudgetFailure.startsWith("FRONTEND_CONTEXT_ESTIMATE_EXCEEDED") ? "context-budget-exhausted" : "budget_breach") : contextBudgetExhausted ? "context-budget-exhausted" : protocolStop && !providerErrorMessage && !timedOut ? "invalid-output" : terminationConfirmed
1002
1095
  ? classifyPiFailure({
1003
1096
  assistantText,
1004
1097
  exitCode: timedOut ? 1 : stderr ? 1 : 0,
@@ -1,6 +1,7 @@
1
+ import { collectExistingFrontendVerificationPaths } from "../workflows/dag/frontend-implementation-contract.js";
1
2
  import { spawn } from "node:child_process";
2
3
  import { createHash, randomUUID } from "node:crypto";
3
- import { withFrontendTestReporter, parseFrontendTestExecutionReport } from "../workflows/dag/frontend-test-execution-evidence.js";
4
+ import { inspectFrontendTestCommand, compareFrontendTestObservation, parseFrontendTestExecutionReport } from "../workflows/dag/frontend-test-execution-evidence.js";
4
5
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
5
6
  import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
6
7
  import path from "node:path";
@@ -493,6 +494,25 @@ export function buildFrontendVerificationEvidence(input) {
493
494
  lintCommandTexts: input.bundle.lintEvidence?.commandTexts ?? [],
494
495
  });
495
496
  const entryByCommand = new Map(directory.map((entry) => [entry.command.trim(), entry]));
497
+ const entryForExecutedCommand = (command) => {
498
+ const normalized = command.trim();
499
+ const exact = entryByCommand.get(normalized);
500
+ if (exact)
501
+ return exact;
502
+ // Behavior commands with Vitest JSON capability are executed with the
503
+ // runtime-owned reporter/outputFile suffix. Preserve their frozen command
504
+ // identity in review evidence, but accept *only* that exact suffix so an
505
+ // arbitrary command sharing a textual prefix is never attributed to a
506
+ // verification-directory entry.
507
+ return directory.find((entry) => {
508
+ if (entry.lane !== "behavior")
509
+ return false;
510
+ if (!normalized.startsWith(entry.command.trim()))
511
+ return false;
512
+ const suffix = normalized.slice(entry.command.trim().length);
513
+ return /^(?:\s+--)?\s+--reporter=json\s+--outputFile=(?:'[^']*'|"[^"]*"|\S+)$/u.test(suffix);
514
+ });
515
+ };
496
516
  return {
497
517
  schemaVersion: 1,
498
518
  schemaId: "frontend-verification-evidence-v1",
@@ -503,7 +523,7 @@ export function buildFrontendVerificationEvidence(input) {
503
523
  lintStatus: input.lintStatus,
504
524
  lintConfigured: input.lintConfigured,
505
525
  commands: input.results.map((result, index) => {
506
- const entry = entryByCommand.get(result.command.trim());
526
+ const entry = entryForExecutedCommand(result.command);
507
527
  return {
508
528
  index: index + 1,
509
529
  commandId: entry?.commandId ?? null,
@@ -1993,12 +2013,29 @@ async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandI
1993
2013
  return [];
1994
2014
  }
1995
2015
  }
2016
+ async function inspectFrontendBundleTests(bundle, cwd, workspaceRoot, phase, writeSet = []) {
2017
+ const inspected = new Map();
2018
+ const failures = [];
2019
+ for (const [index, command] of bundle.behaviorCommands.entries()) {
2020
+ const label = bundle.behaviorEvidence.commandLabels[index];
2021
+ const frozen = bundle.behaviorEvidence.preflight.find(entry => entry.label === label)?.testObservation;
2022
+ const observed = await inspectFrontendTestCommand({ command, cwd, workspaceRoot, allowedPaths: writeSet });
2023
+ const reason = frozen ? compareFrontendTestObservation(frozen, observed.observation, phase, writeSet) : "missing frozen test observation preflight; regenerate the DAG";
2024
+ if (reason)
2025
+ failures.push({ code: "FRONTEND_TEST_OBSERVATION_INVALID", commandId: frozen?.commandId, command, reason });
2026
+ else
2027
+ inspected.set(command, observed);
2028
+ }
2029
+ return { inspected, failures };
2030
+ }
1996
2031
  async function executeFrontendVerificationBundle(input, meta) {
1997
2032
  const started = Date.now();
1998
2033
  const shell = input.task.shell;
1999
2034
  const bundle = shell.frontendVerificationBundle;
2000
2035
  const cwd = resolveShellCwd(input.cwd, shell.cwd);
2001
2036
  const results = [];
2037
+ const testPreflight = await inspectFrontendBundleTests(bundle, cwd, input.cwd, "execution");
2038
+ let testObservationFailure = testPreflight.failures[0];
2002
2039
  const commandResultsByKey = new Map();
2003
2040
  const commandKey = (command) => `${cwd}\u0000${command.trim()}`;
2004
2041
  const reuseCommandResult = (result, command) => {
@@ -2027,8 +2064,9 @@ async function executeFrontendVerificationBundle(input, meta) {
2027
2064
  const successfulLabels = new Map();
2028
2065
  const successfulCommandTexts = new Map();
2029
2066
  const executedTests = new Map();
2067
+ const testExecutionEvidence = [];
2030
2068
  const commandExecutionEvidence = new Map();
2031
- for (const command of bundle.lintCommands ?? []) {
2069
+ for (const command of testObservationFailure ? [] : bundle.lintCommands ?? []) {
2032
2070
  const key = commandKey(command);
2033
2071
  const cached = commandResultsByKey.get(key);
2034
2072
  let result = cached ? reuseCommandResult(cached, command) : undefined;
@@ -2085,17 +2123,26 @@ async function executeFrontendVerificationBundle(input, meta) {
2085
2123
  ];
2086
2124
  const failedGroups = new Set();
2087
2125
  const groupFailures = [];
2088
- for (const group of lintBlocked ? [] : groups) {
2126
+ for (const group of lintBlocked || testObservationFailure ? [] : groups) {
2089
2127
  for (const [index, command] of group.commands.entries()) {
2090
- if (failedGroups.has(group.name))
2128
+ if (failedGroups.has(group.name) || testObservationFailure)
2091
2129
  break;
2130
+ if (testPreflight.inspected.has(command)) {
2131
+ const fresh = await inspectFrontendBundleTests(bundle, cwd, input.cwd, "execution");
2132
+ testObservationFailure = fresh.failures[0];
2133
+ if (testObservationFailure)
2134
+ break;
2135
+ for (const [text, observed] of fresh.inspected)
2136
+ testPreflight.inspected.set(text, observed);
2137
+ }
2092
2138
  const key = commandKey(command);
2093
2139
  const cached = commandResultsByKey.get(key);
2094
2140
  let result = cached ? reuseCommandResult(cached, command) : undefined;
2095
2141
  if (!result) {
2096
2142
  const commandNumber = results.length + 1;
2097
2143
  const reportPath = path.join(meta.runDir, input.task.id, "commands", `${commandNumber}-${randomUUID()}.vitest.json`);
2098
- const observedCommand = await withFrontendTestReporter({ command, cwd, reportPath });
2144
+ const observedTest = testPreflight.inspected.get(command);
2145
+ const observedCommand = observedTest?.withReporter?.(reportPath);
2099
2146
  if (observedCommand)
2100
2147
  await mkdir(path.dirname(reportPath), { recursive: true });
2101
2148
  result = await executeShellCommand({
@@ -2116,7 +2163,17 @@ async function executeFrontendVerificationBundle(input, meta) {
2116
2163
  // Vitest reports physical paths (e.g. /private/var on macOS).
2117
2164
  // Match those in workspace coordinates even when shell.cwd is
2118
2165
  // a subdirectory or the caller used a directory alias.
2119
- commandExecutionEvidence.set(key, parseFrontendTestExecutionReport(JSON.parse(await readFile(reportPath, "utf8")), await realpath(cwd), await realpath(input.cwd)));
2166
+ const rawReport = await readFile(reportPath, "utf8");
2167
+ const reportSha256 = createHash("sha256").update(rawReport).digest("hex");
2168
+ commandExecutionEvidence.set(key, parseFrontendTestExecutionReport(JSON.parse(rawReport), await realpath(observedTest?.effectiveCwd ?? cwd), await realpath(input.cwd)));
2169
+ const actual = commandExecutionEvidence.get(key);
2170
+ for (let offset = 0; offset < actual.length; offset += 24) {
2171
+ const relative = `${input.task.id}/commands/${commandNumber}-execution-${offset / 24}.json`;
2172
+ const excerpt = { schemaVersion: 1, commandSha256: observedTest.observation.commandSha256, cwd: observedTest.observation.cwd, reportSha256, offset, total: actual.length, behaviorCoverage: "unconfirmed", tests: actual.slice(offset, offset + 24).map(test => ({ file: test.file, status: test.status, title: test.title.slice(0, 500), ...(test.title.length > 500 ? { titleTruncated: true } : {}) })) };
2173
+ await writeDagRunJsonArtifact(meta.runDir, relative, excerpt);
2174
+ const bytes = await readFile(path.join(meta.runDir, relative));
2175
+ testExecutionEvidence.push({ path: relative, sha256: createHash("sha256").update(bytes).digest("hex"), reportPath: path.relative(meta.runDir, reportPath).replace(/\\/g, "/"), reportSha256 });
2176
+ }
2120
2177
  }
2121
2178
  catch (error) {
2122
2179
  reportError = `frontend test execution report unavailable: ${error instanceof Error ? error.message : String(error)}`;
@@ -2212,6 +2269,8 @@ async function executeFrontendVerificationBundle(input, meta) {
2212
2269
  : undefined;
2213
2270
  let traceError;
2214
2271
  try {
2272
+ if (testObservationFailure)
2273
+ throw new Error(`${testObservationFailure.code}: ${testObservationFailure.reason}`);
2215
2274
  await runFrontendVerificationTraceGate({
2216
2275
  runDir: meta.runDir,
2217
2276
  workspaceRoot: input.cwd,
@@ -2229,6 +2288,7 @@ async function executeFrontendVerificationBundle(input, meta) {
2229
2288
  nodeId: input.task.id,
2230
2289
  commandLabels: successfulLabels.get("behavior") ?? [],
2231
2290
  executedTests,
2291
+ testExecutionEvidence,
2232
2292
  },
2233
2293
  },
2234
2294
  });
@@ -2261,9 +2321,10 @@ async function executeFrontendVerificationBundle(input, meta) {
2261
2321
  nodeId: failedNodeId,
2262
2322
  rawFailureCategory: failureCategory,
2263
2323
  });
2264
- const failureOwner = classifiedOwner === "unknown" &&
2265
- ["typecheck", "build", "lint", "component-test", "unit-test", "trace"].includes(failureClass)
2266
- ? "implementation" : classifiedOwner;
2324
+ const failureOwner = testObservationFailure ? "verification-config" :
2325
+ classifiedOwner === "unknown" &&
2326
+ ["typecheck", "build", "lint", "component-test", "unit-test", "trace"].includes(failureClass)
2327
+ ? "implementation" : classifiedOwner;
2267
2328
  const restartPhase = classifyRestartPhase(failureOwner);
2268
2329
  // A+B (AC-008): verification-config routing. Produce structured
2269
2330
  // unresolved entrypoint refs from committed plan facts and the frozen
@@ -2325,6 +2386,7 @@ async function executeFrontendVerificationBundle(input, meta) {
2325
2386
  ...(verificationConfigRoute
2326
2387
  ? { verificationConfigRoute }
2327
2388
  : {}),
2389
+ ...(testObservationFailure ? { testObservationFailure, commandsExecuted: results.some(result => !result.reused) } : {}),
2328
2390
  commandResults,
2329
2391
  failedAt: new Date().toISOString(),
2330
2392
  };
@@ -2689,8 +2751,9 @@ async function executeFrontendDesignPolicy(input, meta) {
2689
2751
  durationMs: Date.now() - started,
2690
2752
  };
2691
2753
  }
2754
+ const existingVerificationPaths = await collectExistingFrontendVerificationPaths(input.cwd, contract);
2692
2755
  const uncoveredVTs = contract.verificationTargets
2693
- .filter((vt) => vt.mode !== "static")
2756
+ .filter((vt) => vt.mode !== "static" && !existingVerificationPaths.includes(vt.file))
2694
2757
  .filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
2695
2758
  if (uncoveredVTs.length > 0) {
2696
2759
  return {
@@ -2757,6 +2820,7 @@ async function executeFrontendDesignPolicy(input, meta) {
2757
2820
  [],
2758
2821
  allowedDependencies: config.allowedDependencies,
2759
2822
  writeSetPatterns: config.implementationWriteSet ?? [],
2823
+ existingVerificationPaths: await collectExistingFrontendVerificationPaths(input.cwd, contract),
2760
2824
  // Fresh-existence evidence at gate time (Scout pathEvidence.fresh
2761
2825
  // semantics): a reuse-existing evidencePath must name a file that
2762
2826
  // exists NOW; greenfield paths are decision=new only.
@@ -2894,12 +2958,21 @@ async function executeFrontendWriterAdmission(input, meta) {
2894
2958
  const workspaceFiles = await collectWorkspaceFiles(input.cwd);
2895
2959
  const admission = deriveFrontendWriterAdmissionShell({
2896
2960
  contract,
2961
+ existingVerificationPaths: await collectExistingFrontendVerificationPaths(input.cwd, contract),
2897
2962
  designPolicyResult,
2898
2963
  baseline,
2899
2964
  frozenCommandLabels: config.frozenCommandLabels,
2900
2965
  allowedMockStrategies: config.allowedMockStrategies,
2901
2966
  workspaceFiles,
2902
2967
  });
2968
+ const testObservationFindings = [];
2969
+ for (const node of meta.spec.tasks) {
2970
+ const verification = node.shell?.frontendVerificationBundle;
2971
+ if (!verification)
2972
+ continue;
2973
+ const checked = await inspectFrontendBundleTests(verification, resolveShellCwd(input.cwd, node.shell?.cwd), input.cwd, "admission", admission.writeSet);
2974
+ testObservationFindings.push(...checked.failures.map(failure => ({ code: failure.code, message: failure.reason })));
2975
+ }
2903
2976
  // 6. Design verdict enforcement: request_design_changes is a blocked
2904
2977
  // admission. This keeps the writer fail-closed until the recovery plan
2905
2978
  // has incorporated the reviewer's concrete findings.
@@ -2907,9 +2980,10 @@ async function executeFrontendWriterAdmission(input, meta) {
2907
2980
  ? "frontend-design-policy-shell"
2908
2981
  : "frontend-writer-admission-shell";
2909
2982
  const findings = verdictPass
2910
- ? admission.findings
2983
+ ? [...admission.findings, ...testObservationFindings]
2911
2984
  : [
2912
2985
  ...admission.findings,
2986
+ ...testObservationFindings,
2913
2987
  {
2914
2988
  code: designRequested
2915
2989
  ? "design-review-requested"
@@ -3101,6 +3175,8 @@ async function executeFrontendCloseout(input, meta) {
3101
3175
  status: target.status === "ok" ? "passed" : "failed",
3102
3176
  evidence: trace?.targets?.[index]?.evidence ?? "unavailable",
3103
3177
  behaviorCoverage: "unconfirmed",
3178
+ plannedEvidenceLevel: contract.verificationTargets.find(entry => entry.id === trace?.targets?.[index]?.id)?.evidenceLevel ?? "unknown",
3179
+ boundary: contract.verificationTargets.find(entry => entry.id === trace?.targets?.[index]?.id)?.boundary,
3104
3180
  })),
3105
3181
  lintStatus: "unavailable",
3106
3182
  integrationFacts,
@@ -3,7 +3,7 @@ export const DAG_SHELL_PRESETS = {
3
3
  "loop-agent-standard-verify": [
4
4
  "npm run lint",
5
5
  "npm run typecheck",
6
- "npx vitest run test/dag-shell-executor.test.ts test/dag-validate.test.ts test/dag-runner.test.ts --reporter=dot",
6
+ "npx vitest run test/dag-shell-executor.test.ts test/dag-validate.test.ts test/dag-runner.test.ts test/dag-runner-convergence.test.ts test/dag-runner-artifacts.test.ts test/dag-runner-decisions.test.ts --reporter=dot",
7
7
  ],
8
8
  };
9
9
  export function expandShellPreset(preset) {
@@ -4,6 +4,7 @@ import { constants, createReadStream } from "node:fs";
4
4
  import { access, lstat, readlink, unlink } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { pathMatchesPattern } from "../shared/git-progress.js";
7
+ import { expandWorkspaceStatus, hasGitmodulesFile, synthesizeExpandedPorcelain, } from "../shared/git-workspace-status.js";
7
8
  import { ResilientGitCommandError, runResilientGitCommand, } from "../shared/resilient-git.js";
8
9
  async function sha256File(filePath) {
9
10
  const hash = createHash("sha256");
@@ -411,6 +412,7 @@ async function resolveGitExecutableCandidates(platform, env) {
411
412
  return ["git"];
412
413
  }
413
414
  export async function readGitStatusPorcelain(cwd, options = {}) {
415
+ let parentPorcelain;
414
416
  try {
415
417
  const result = await runResilientGitCommand({
416
418
  cwd,
@@ -419,9 +421,12 @@ export async function readGitStatusPorcelain(cwd, options = {}) {
419
421
  attempts: options.attempts,
420
422
  retryDelayMs: options.retryDelayMs,
421
423
  });
422
- if (result.code === 0)
423
- return result.stdout;
424
- throw new Error(result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`);
424
+ if (result.code === 0) {
425
+ parentPorcelain = result.stdout;
426
+ }
427
+ else {
428
+ throw new Error(result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`);
429
+ }
425
430
  }
426
431
  catch (error) {
427
432
  if (error instanceof ResilientGitCommandError) {
@@ -436,6 +441,24 @@ export async function readGitStatusPorcelain(cwd, options = {}) {
436
441
  }
437
442
  throw error;
438
443
  }
444
+ // Submodule workspaces: a parent-only porcelain hides every file inside a
445
+ // registered submodule behind a single directory-level gitlink entry,
446
+ // which historically produced false empty-diff and false violations
447
+ // (plan 2026-09-15-development-workflow-git-submodule). Repositories
448
+ // without .gitmodules keep the parent bytes verbatim — byte-identical
449
+ // behavior for every non-submodule workspace.
450
+ if (!(await hasGitmodulesFile(cwd)))
451
+ return parentPorcelain;
452
+ const observation = await expandWorkspaceStatus({
453
+ rootCwd: cwd,
454
+ parentPorcelain,
455
+ hasGitmodulesFile: true,
456
+ });
457
+ if (!observation.complete) {
458
+ const failed = observation.repos.find((repo) => repo.state === "status-read-failed");
459
+ throw new Error(`workspace status expansion incomplete: submodule ${failed?.repoRelativePath ?? "unknown"} status read failed: ${failed?.errorDetail ?? "unknown error"}`);
460
+ }
461
+ return synthesizeExpandedPorcelain(observation.entries);
439
462
  }
440
463
  export async function readGitStatusPorcelainWithDependencies(cwd, options, dependencies) {
441
464
  const candidates = await dependencies.resolveExecutableCandidates();