@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
@@ -0,0 +1,261 @@
1
+ import { sourceBudgetContractIdentity } from "../../application/evaluation/budget.js";
2
+ import { verifyFrozenSourceUsage } from "../../task/source-prepare/source-provider-budget.js";
3
+ import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
4
+ import { locateDagRun, readDagRunSpec, readDagRunState } from "./lifecycle.js";
5
+ import { validateCumulativeBudgetSuccessor } from "./budget-enforcement.js";
6
+ import { readFrontendSessionBudgetReceipts, summarizeFrontendPlanCanary, summarizeFrontendSessions } from "./frontend-session-budget.js";
7
+ /** One explicit recovery chain. Missing links never look like cheaper runs. */
8
+ export async function readFrontendRootObservation(workspaceRoot, runId) {
9
+ const diagnostics = [];
10
+ const chain = [];
11
+ const seen = new Set();
12
+ let sourceUsage = null;
13
+ let sourceBudgetHash;
14
+ const load = async (id) => {
15
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id))
16
+ throw Error("invalid run identity");
17
+ const found = await locateDagRun(workspaceRoot, id);
18
+ if (!found)
19
+ throw Error(`missing recovery run: ${id}`);
20
+ const state = await readDagRunState(found.runDir);
21
+ if (state.runId !== id || state.executionMode !== "execute")
22
+ throw Error(`invalid execution identity: ${id}`);
23
+ return { runDir: found.runDir, state };
24
+ };
25
+ const parentOf = (state) => {
26
+ const continuation = state.continuation?.parentRunId;
27
+ const recovery = state.frontendRecoveryState?.parentRunId;
28
+ if (continuation === state.runId)
29
+ throw Error("continuation ancestor cycle");
30
+ const recoveryParent = recovery === state.runId ? undefined : recovery;
31
+ if (continuation && recoveryParent && continuation !== recoveryParent)
32
+ throw Error("conflicting recovery parents");
33
+ return continuation ?? recoveryParent;
34
+ };
35
+ try {
36
+ let current = await load(runId);
37
+ const ancestorChildren = new Map();
38
+ // In a root's pending recovery intent parentRunId can refer to itself.
39
+ while (true) {
40
+ if (seen.has(current.state.runId))
41
+ throw Error("recovery ancestor cycle");
42
+ seen.add(current.state.runId);
43
+ const parent = parentOf(current.state);
44
+ if (!parent)
45
+ break;
46
+ ancestorChildren.set(parent, current.state.runId);
47
+ current = await load(parent);
48
+ }
49
+ const rootId = current.state.runId;
50
+ seen.clear();
51
+ let sourceHash;
52
+ while (true) {
53
+ if (seen.has(current.state.runId))
54
+ throw Error("recovery descendant cycle");
55
+ seen.add(current.state.runId);
56
+ chain.push(current);
57
+ const spec = await readDagRunSpec(current.runDir);
58
+ if (!spec.sourceBinding)
59
+ throw Error("missing frozen source binding");
60
+ const hash = sha256OfCanonicalJson(spec.sourceBinding);
61
+ if (sourceHash && hash !== sourceHash)
62
+ throw Error("recovery source binding drift");
63
+ sourceHash = hash;
64
+ const boundBudgetHash = sha256OfCanonicalJson(spec.sourceProviderBudget ?? null);
65
+ if (sourceBudgetHash !== undefined && sourceBudgetHash !== boundBudgetHash)
66
+ throw Error("recovery source budget evidence drift");
67
+ sourceBudgetHash = boundBudgetHash;
68
+ if (spec.sourceProviderBudget) {
69
+ if (sha256OfCanonicalJson(spec.sourceProviderBudget.contract ?? null) !== sha256OfCanonicalJson(sourceBudgetContractIdentity(spec.taskContractBinding) ?? null) || spec.sourceProviderBudget.sourceBindingSha256 !== hash || sha256OfCanonicalJson(current.state.sourceProviderBudget ?? null) !== boundBudgetHash)
70
+ throw Error("source budget state/spec binding mismatch");
71
+ sourceUsage = await verifyFrozenSourceUsage(workspaceRoot, spec.sourceProviderBudget);
72
+ diagnostics.push(...sourceUsage.diagnostics);
73
+ if (sourceUsage.complete && ((current.state.budgetLedger?.consumed.tokens ?? -1) < sourceUsage.totalTokens || (current.state.budgetLedger?.consumed.providerRequests ?? -1) < spec.sourceProviderBudget.ledger.reservedRequests))
74
+ throw Error("source cost missing from cumulative budget");
75
+ }
76
+ const recovery = current.state.frontendRecoveryState;
77
+ if (recovery && recovery.recoveryRootRunId !== rootId)
78
+ throw Error("recovery root identity drift");
79
+ if (!["finished", "failed", "partial_failed"].includes(current.state.status) || !current.state.finishedAt)
80
+ throw Error(`incomplete run: ${current.state.runId}`);
81
+ const declaredChild = recovery?.childRunId === current.state.runId ? undefined : recovery?.childRunId;
82
+ const selectedChild = ancestorChildren.get(current.state.runId);
83
+ if (declaredChild && selectedChild && declaredChild !== selectedChild)
84
+ throw Error("conflicting recovery children");
85
+ const child = selectedChild ?? declaredChild;
86
+ if (!child)
87
+ break;
88
+ const next = await load(child);
89
+ if (parentOf(next.state) !== current.state.runId)
90
+ throw Error("recovery parent/child mismatch");
91
+ validateCumulativeBudgetSuccessor(current.state, next.state);
92
+ current = next;
93
+ }
94
+ if (!seen.has(runId))
95
+ throw Error("requested run is outside the root chain");
96
+ }
97
+ catch (error) {
98
+ diagnostics.push(error instanceof Error ? error.message : String(error));
99
+ }
100
+ const receipts = [];
101
+ const identities = new Map();
102
+ for (const item of chain) {
103
+ const own = await readFrontendSessionBudgetReceipts(item.runDir);
104
+ const executedPlan = item.state.nodes?.["frontend-plan-pi"];
105
+ if (!own.length && executedPlan && executedPlan.origin?.kind !== "imported")
106
+ diagnostics.push(`missing Plan receipts: ${item.state.runId}`);
107
+ for (const receipt of own) {
108
+ if (!chain.some(entry => entry.state.runId === receipt.identity.runId))
109
+ diagnostics.push("receipt outside recovery lineage");
110
+ const key = `${receipt.identity.runId}:${receipt.identity.sessionId}`;
111
+ const digest = sha256OfCanonicalJson(receipt);
112
+ if (identities.has(key) && identities.get(key) !== digest)
113
+ diagnostics.push(`conflicting receipt: ${key}`);
114
+ identities.set(key, digest);
115
+ receipts.push(receipt);
116
+ }
117
+ }
118
+ if (receipts.some(receipt => receipt.completion !== "complete"))
119
+ diagnostics.push("incomplete session receipts");
120
+ let complete = diagnostics.length === 0 && chain.length > 0;
121
+ const plan = summarizeFrontendPlanCanary(complete ? receipts : []);
122
+ const first = chain[0]?.state, last = chain.at(-1)?.state;
123
+ const wall = first && last ? Date.parse(last.finishedAt) - Date.parse(first.startedAt) : NaN;
124
+ if (complete && (!Number.isFinite(wall) || wall < 0))
125
+ diagnostics.push("invalid root timing");
126
+ complete = diagnostics.length === 0 && complete;
127
+ return { complete, diagnostics, sourceUsage, runIds: chain.map(entry => entry.state.runId), plan,
128
+ sessions: summarizeFrontendSessions(receipts),
129
+ recoveryCount: complete ? chain.length - 1 : null,
130
+ dagWallClockMs: complete && Number.isFinite(wall) && wall >= 0 ? wall : null,
131
+ // The final ledger already includes inherited consumption. Never sum it.
132
+ cumulativeBudget: complete ? last?.budgetLedger ?? null : null,
133
+ outcome: complete ? last?.status ?? null : null };
134
+ }
135
+ /** Explicit identities are required; absent on both sides does not mean equal. */
136
+ export function compareFrontendPairIdentity(before, after) {
137
+ return ["protocol", "source", "model", "budget", "initialTree", "controller", "promptTools", "acceptance"].flatMap(key => typeof before[key] !== "string" || !before[key]?.trim() || typeof after[key] !== "string" || !after[key]?.trim() ? [`missing-pair-identity:${key}`] : before[key] !== after[key] ? [`pair-identity-mismatch:${key}`] : []);
138
+ }
139
+ export const FRONTEND_PLAN_CANARY_SCENARIOS = [
140
+ "missing-idle-loading-empty",
141
+ "loading-not-mounted",
142
+ "test-layer-does-not-prove-wiring",
143
+ "mock-claimed-as-real-integration",
144
+ ];
145
+ export async function readFrontendCanaryEvidence(input) {
146
+ const { createHash } = await import("node:crypto");
147
+ const path = await import("node:path");
148
+ const { z } = await import("zod");
149
+ const { readFileBoundarySafe } = await import("../../shared/path-safety.js");
150
+ const { typedEventRecordSchema } = await import("./frontend-typed-event-store.js");
151
+ const ref = z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
152
+ const identitySchema = z.object({ protocol: ref, source: ref, model: ref, budget: ref, initialTree: ref, controller: ref, promptTools: ref, acceptance: ref }).strict();
153
+ const manifestSchema = z.object({ schemaVersion: z.literal(1), runId: z.string().min(1), run: ref, state: ref, identity: identitySchema, acceptance: ref }).strict();
154
+ const read = async (value, boundaryRoot = input.manifest.directory) => {
155
+ const checked = ref.parse({ path: value.path, sha256: value.sha256 });
156
+ if (path.isAbsolute(checked.path) || checked.path.split(/[\\/]/).includes(".."))
157
+ throw Error("canary-evidence-path");
158
+ const bytes = await readFileBoundarySafe({ boundaryRoot, targetPath: path.join(boundaryRoot, checked.path), label: "canary evidence" });
159
+ if (createHash("sha256").update(bytes).digest("hex") !== checked.sha256)
160
+ throw Error(`canary-evidence-hash:${checked.path}`);
161
+ return bytes;
162
+ };
163
+ const json = async (value) => JSON.parse((await read(value)).toString("utf8"));
164
+ const manifest = manifestSchema.parse(await json(input.manifest));
165
+ const tailId = input.runIds.at(-1);
166
+ const tail = tailId && await locateDagRun(input.workspaceRoot, tailId);
167
+ if (!tail || manifest.runId !== tailId || path.resolve(tail.runDir) !== path.resolve(input.runDir))
168
+ throw Error("canary-evidence-run-mismatch");
169
+ if (manifest.run.path !== "run.json" || manifest.state.path !== "state.json")
170
+ throw Error("canary-evidence-run-path");
171
+ await read(manifest.run, input.runDir);
172
+ await read(manifest.state, input.runDir);
173
+ const spec = await readDagRunSpec(input.runDir);
174
+ const state = await readDagRunState(input.runDir);
175
+ if (state.runId !== tailId || state.status !== "finished" || !state.finishedAt)
176
+ throw Error("canary-evidence-run-not-finished");
177
+ const identity = {};
178
+ for (const key of Object.keys(manifest.identity)) {
179
+ await read(manifest.identity[key]);
180
+ identity[key] = manifest.identity[key].sha256;
181
+ }
182
+ if (sha256OfCanonicalJson(await json(manifest.identity.source)) !== sha256OfCanonicalJson(spec.sourceBinding))
183
+ throw Error("canary-evidence-source-mismatch");
184
+ if (sha256OfCanonicalJson(await json(manifest.identity.budget)) !== sha256OfCanonicalJson(spec.budget ?? null))
185
+ throw Error("canary-evidence-budget-mismatch");
186
+ const modelConfig = z.object({ models: z.array(z.string().min(1)).min(1), parameters: z.record(z.unknown()).optional() }).strict().parse(await json(manifest.identity.model));
187
+ const outcome = z.enum(["pass", "fail"]);
188
+ const expected = z.object({ cases: z.array(z.object({ id: z.string().min(1), expected: outcome, scenario: z.enum(FRONTEND_PLAN_CANARY_SCENARIOS).optional() }).strict()).min(1) }).strict().parse(await json(manifest.identity.acceptance));
189
+ const actual = z.object({ schemaVersion: z.literal(1), runId: z.string(), run: ref, state: ref, results: z.array(z.object({ id: z.string().min(1), actual: outcome, evidence: ref, scenarioResult: z.enum(["blocked", "reported", "not-observed"]).optional() }).strict()).min(1) }).strict().parse(await json(manifest.acceptance));
190
+ if (actual.runId !== tailId || sha256OfCanonicalJson(actual.run) !== sha256OfCanonicalJson(manifest.run) || sha256OfCanonicalJson(actual.state) !== sha256OfCanonicalJson(manifest.state))
191
+ throw Error("canary-acceptance-run-mismatch");
192
+ const expectedIds = new Set(expected.cases.map(row => row.id));
193
+ const actualIds = new Set(actual.results.map(row => row.id));
194
+ if (expectedIds.size !== expected.cases.length || actualIds.size !== actual.results.length || expectedIds.size !== actualIds.size || [...actualIds].some(id => !expectedIds.has(id)))
195
+ throw Error("canary-acceptance-case-mismatch");
196
+ for (const result of actual.results)
197
+ await read(result.evidence);
198
+ const defects = expected.cases.filter(row => actual.results.find(result => result.id === row.id).actual !== row.expected).length;
199
+ const regressions = {};
200
+ for (const scenario of FRONTEND_PLAN_CANARY_SCENARIOS) {
201
+ const cases = expected.cases.filter(row => row.scenario === scenario);
202
+ const verified = cases.length > 0 && cases.every(row => {
203
+ const result = actual.results.find(value => value.id === row.id);
204
+ return result.actual === row.expected && (result.scenarioResult === "blocked" || result.scenarioResult === "reported");
205
+ });
206
+ regressions[scenario] = verified ? "reported" : "not-observed";
207
+ }
208
+ let reviews = 0, rejects = 0;
209
+ const actualModels = new Set();
210
+ for (const id of input.runIds) {
211
+ const found = await locateDagRun(input.workspaceRoot, id);
212
+ if (!found)
213
+ throw Error("canary-design-run-missing");
214
+ for (const receipt of await readFrontendSessionBudgetReceipts(found.runDir)) {
215
+ if (receipt.identity.executionKind !== "model")
216
+ continue;
217
+ const plannedModel = receipt.planned?.capabilities?.model;
218
+ const models = [plannedModel, ...receipt.attempts.map(attempt => attempt.model)];
219
+ if (!receipt.attempts.length || models.some(model => !model || !modelConfig.models.includes(model)))
220
+ throw Error("canary-evidence-model-mismatch");
221
+ for (const attempt of receipt.attempts)
222
+ actualModels.add(attempt.model);
223
+ }
224
+ const runState = await readDagRunState(found.runDir);
225
+ const runSpec = await readDagRunSpec(found.runDir);
226
+ const node = runState.nodes["frontend-design-review-pi"];
227
+ if (!node && runSpec.tasks.some(task => task.id === "frontend-design-review-pi"))
228
+ throw Error("canary-design-node-missing");
229
+ // Small/micro paths have no independent design review; skipped/imported
230
+ // nodes contribute no new verdict. Missing facts on executed nodes are errors.
231
+ if (!node || node.status === "SKIPPED" || node.origin?.kind === "imported")
232
+ continue;
233
+ const bytes = await readFileBoundarySafe({ boundaryRoot: found.runDir, targetPath: path.join(found.runDir, "frontend-design-review-pi/design-typed-facts.jsonl"), label: "canary design facts" });
234
+ const records = bytes.toString("utf8").split("\n").filter(line => line.trim()).map(line => typedEventRecordSchema.parse(JSON.parse(line)));
235
+ const terminals = records.filter(record => record.phase === "committed" && ["approve_design", "request_design_changes"].includes(String(record.fact.kind)));
236
+ const attempts = new Set();
237
+ if (!terminals.length)
238
+ throw Error("canary-design-terminal-missing");
239
+ for (const record of terminals) {
240
+ if (record.payloadSha256 !== sha256OfCanonicalJson(record.fact) || attempts.has(record.attemptId))
241
+ throw Error("canary-design-terminal-invalid");
242
+ attempts.add(record.attemptId);
243
+ reviews++;
244
+ if (record.fact.kind === "request_design_changes")
245
+ rejects++;
246
+ }
247
+ }
248
+ if (actualModels.size !== 1)
249
+ throw Error("canary-evidence-mixed-models");
250
+ identity.model = sha256OfCanonicalJson({ configurationSha256: identity.model, actualModel: [...actualModels][0] });
251
+ // Recheck all observed files after collection, including run state/spec.
252
+ await read(input.manifest);
253
+ await read(manifest.run, input.runDir);
254
+ await read(manifest.state, input.runDir);
255
+ for (const value of Object.values(manifest.identity))
256
+ await read(value);
257
+ await read(manifest.acceptance);
258
+ for (const result of actual.results)
259
+ await read(result.evidence);
260
+ return { identity, regressions, designReviewRejectRate: reviews ? rejects / reviews : 0, finalReviewDefectRate: defects / expected.cases.length, acceptancePassed: defects === 0 };
261
+ }
@@ -1,3 +1,5 @@
1
+ import { currentSourceExecution } from "../../task/source-prepare/source-execution.js";
2
+ import { withFrontendSessionIdentity } from "./frontend-session-context.js";
1
3
  import { createHash, randomUUID } from "node:crypto";
2
4
  import { mkdir, readdir, rename, rm, readFile, writeFile } from "node:fs/promises";
3
5
  import path from "node:path";
@@ -157,13 +159,13 @@ export async function observeFrontendSession(input, execute) {
157
159
  const before = safeObservation(() => input.committedCount?.(), undefined);
158
160
  const toolsBefore = readFrontendToolProgress(input.customTools);
159
161
  const receipt = {
160
- schemaVersion: 1, policyVersion: "frontend-session-observation-v1",
161
- identity: { sessionId: randomUUID(), phase: input.phase, scopeIds: input.scopeIds ?? [], taskId: input.taskId ?? null, runId: input.runId ?? null, nodeId: input.nodeId ?? null, attempt: input.attempt ?? null, sourceDigest: input.sourceDigest ?? null, contractDigest: input.contractDigest ?? null },
162
+ schemaVersion: 2, policyVersion: "frontend-session-observation-v2",
163
+ identity: { ...(currentSourceExecution(input.taskId) ? { sourceExecutionId: currentSourceExecution(input.taskId) } : {}), sessionId: randomUUID(), phase: input.phase, executionKind: input.executionKind ?? "model", dispatchReason: input.dispatchReason ?? "initial", scopeIds: input.scopeIds ?? [], taskId: input.taskId ?? null, runId: input.runId ?? null, nodeId: input.nodeId ?? null, attempt: input.attempt ?? null, sourceDigest: input.sourceDigest ?? null, contractDigest: input.contractDigest ?? null },
162
164
  startedAt: new Date().toISOString(), finishedAt: null, completion: "incomplete",
163
165
  planned: { prompt: measure(input.prompt), userMessage: measure(input.userMessage ?? ""), toolSchemas: input.customTools ? safeObservation(() => measure(JSON.stringify(input.customTools.map(t => { const tool = record(t); return { name: tool.name, description: tool.description, parameters: tool.parameters }; }))), null) : null,
164
166
  estimatorVersion: "utf8-bytes-div3-v1-estimate", capabilities: { ...frontendModelCapabilities(undefined), model: input.model ?? null }, unmeasuredComponents: ["sdk-system-overhead", "built-in-tools", "history", "tool-results", "attachments"] },
165
167
  actual: collectFrontendUsage([]), attempts: [],
166
- progress: { memoryCommittedDelta: null, durableCommittedDelta: null, failedRecords: null, duplicateRecords: null, durableSubmissions: null, durableReplacements: null, successfulReadBytes: null, duplicateReadBytes: null }, terminal: null,
168
+ progress: { finalizeRejections: null, memoryCommittedDelta: null, durableCommittedDelta: null, failedRecords: null, duplicateRecords: null, durableSubmissions: null, durableReplacements: null, successfulReadBytes: null, duplicateReadBytes: null }, terminal: null,
167
169
  };
168
170
  let writes = Promise.resolve();
169
171
  let abandoned = false;
@@ -203,14 +205,14 @@ export async function observeFrontendSession(input, execute) {
203
205
  }
204
206
  };
205
207
  await waitForWrite(save());
206
- const result = await execute(event => {
208
+ const result = await withFrontendSessionIdentity({ ...receipt.identity, scopeIds: [...receipt.identity.scopeIds] }, () => execute(event => {
207
209
  const index = receipt.attempts.findIndex(a => a.id === event.id);
208
210
  if (index < 0)
209
211
  receipt.attempts.push(event);
210
212
  else
211
213
  receipt.attempts[index] = event;
212
214
  void save();
213
- });
215
+ }));
214
216
  receipt.finishedAt = new Date().toISOString();
215
217
  receipt.completion = "complete";
216
218
  receipt.actual = receipt.attempts.length ? aggregateAttempts(receipt.attempts) : result.usageObservation ?? collectFrontendUsage([]);
@@ -220,8 +222,10 @@ export async function observeFrontendSession(input, execute) {
220
222
  receipt.progress.durableCommittedDelta = durableBefore !== undefined && durableAfter !== undefined ? Math.max(0, durableAfter - durableBefore) : null;
221
223
  const toolsAfter = readFrontendToolProgress(input.customTools);
222
224
  if (toolsBefore && toolsAfter)
223
- for (const key of ["failedRecords", "duplicateRecords", "durableSubmissions", "durableReplacements"])
225
+ for (const key of ["finalizeRejections", "failedRecords", "duplicateRecords", "durableSubmissions", "durableReplacements"])
224
226
  receipt.progress[key] = Math.max(0, toolsAfter[key] - toolsBefore[key]);
227
+ if (input.executionKind === "runtime")
228
+ receipt.progress.finalizeRejections = result.ok ? 0 : 1;
225
229
  const reads = receipt.attempts.length ? receipt.attempts.map(a => a.reads) : [result.readObservation];
226
230
  for (const key of ["successfulReadBytes", "duplicateReadBytes"])
227
231
  receipt.progress[key] = sumKnown(reads.map(r => r?.[key] ?? null));
@@ -229,16 +233,30 @@ export async function observeFrontendSession(input, execute) {
229
233
  await waitForWrite(save());
230
234
  return result;
231
235
  }
236
+ /** Observe deterministic compilation without inflating model/session counts. */
237
+ export async function observeFrontendFinalization(input, execute) {
238
+ let value;
239
+ await observeFrontendSession({ ...input, executionKind: "runtime", phase: "plan/runtime-finalize", prompt: "" }, async () => {
240
+ value = await execute();
241
+ const details = record(record(value).details ?? value);
242
+ return { ok: details.ok === true, assistantText: "", command: [], durationMs: 0, exitCode: null,
243
+ failureCategory: details.ok === true ? "success" : "invalid-output", modelDisplay: "runtime", parsedEvents: 0,
244
+ stderr: details.ok === true ? "" : String(details.error ?? details.code ?? "finalize rejected"), stdout: "", timedOut: false,
245
+ attemptedModels: [], fallbackUsed: false, tokensUsed: 0 };
246
+ });
247
+ return value;
248
+ }
232
249
  function uniqueFrontendSessions(receipts) {
233
250
  const byId = new Map();
234
251
  for (const receipt of receipts) {
235
- const previous = byId.get(receipt.identity.sessionId);
252
+ const key = `${receipt.identity.runId ?? "unknown"}:${receipt.identity.sessionId}`;
253
+ const previous = byId.get(key);
236
254
  const rank = (r) => [r.completion === "complete" ? 1 : 0, Date.parse(r.finishedAt ?? r.startedAt), r.attempts.filter(a => a.completion === "complete").length, r.attempts.length];
237
255
  const candidateRank = rank(receipt);
238
256
  const previousRank = previous ? rank(previous) : [];
239
257
  const difference = candidateRank.map((v, i) => v - (previousRank[i] ?? 0)).find(v => v !== 0) ?? 0;
240
258
  if (!previous || difference > 0)
241
- byId.set(receipt.identity.sessionId, receipt);
259
+ byId.set(key, receipt);
242
260
  }
243
261
  return [...byId.values()];
244
262
  }
@@ -288,13 +306,13 @@ export async function readFrontendSessionBudgetReceipts(runDir, nodeId = "fronte
288
306
  // Keep a visible incomplete receipt so canary aggregation cannot turn a
289
307
  // truncated artifact into an apparent zero-cost improvement.
290
308
  receipts.push({
291
- schemaVersion: 1,
292
- policyVersion: "frontend-session-observation-v1",
293
- identity: { sessionId: `invalid:${file}`, phase: "unknown", scopeIds: [], taskId: null, runId: null, nodeId, attempt: null, sourceDigest: null, contractDigest: null },
309
+ schemaVersion: 2,
310
+ policyVersion: "frontend-session-observation-v2",
311
+ identity: { sessionId: `invalid:${file}`, phase: "unknown", executionKind: "model", dispatchReason: "initial", scopeIds: [], taskId: null, runId: null, nodeId, attempt: null, sourceDigest: null, contractDigest: null },
294
312
  startedAt: new Date(0).toISOString(), finishedAt: null, completion: "incomplete",
295
313
  planned: { prompt: { sha256: "", chars: 0, bytes: 0, estimatedTokens: 0 }, userMessage: { sha256: "", chars: 0, bytes: 0, estimatedTokens: 0 }, toolSchemas: null, estimatorVersion: "unknown", capabilities: { ...frontendModelCapabilities(undefined), model: null }, unmeasuredComponents: [] },
296
314
  actual: { ...collectFrontendUsage([]) }, attempts: [],
297
- progress: { memoryCommittedDelta: null, durableCommittedDelta: null, failedRecords: null, duplicateRecords: null, durableSubmissions: null, durableReplacements: null, successfulReadBytes: null, duplicateReadBytes: null }, terminal: null,
315
+ progress: { finalizeRejections: null, memoryCommittedDelta: null, durableCommittedDelta: null, failedRecords: null, duplicateRecords: null, durableSubmissions: null, durableReplacements: null, successfulReadBytes: null, duplicateReadBytes: null }, terminal: null,
298
316
  });
299
317
  }
300
318
  }
@@ -302,7 +320,8 @@ export async function readFrontendSessionBudgetReceipts(runDir, nodeId = "fronte
302
320
  }
303
321
  export function summarizeFrontendPlanCanary(receipts) {
304
322
  const unique = uniqueFrontendSessions(receipts);
305
- const complete = unique.length > 0 && unique.every(receipt => isCanaryReceipt(receipt) && receipt.completion === "complete");
323
+ const model = unique.filter(r => r.identity.executionKind === "model");
324
+ const complete = model.length > 0 && unique.every(receipt => isCanaryReceipt(receipt) && receipt.completion === "complete");
306
325
  const known = (values, sum) => {
307
326
  if (!complete || values.length === 0)
308
327
  return null;
@@ -310,14 +329,17 @@ export function summarizeFrontendPlanCanary(receipts) {
310
329
  return present.length === values.length ? sum(present) : null;
311
330
  };
312
331
  return {
313
- planModelRequests: known(unique.map((receipt) => receipt.actual.requestCount), (items) => items.reduce((total, value) => total + value, 0)),
314
- planSessions: complete ? unique.length : null,
332
+ planModelRequests: known(model.map((receipt) => receipt.actual.requestCount), (items) => items.reduce((total, value) => total + value, 0)),
333
+ planSessions: complete ? model.length : null,
315
334
  // Receipt envelope includes gaps and overlapping sessions only once. The
316
335
  // run reader below replaces this with the authoritative node interval.
317
336
  planWallClockMs: complete ? Math.max(...unique.map(r => Date.parse(r.finishedAt))) - Math.min(...unique.map(r => Date.parse(r.startedAt))) : null,
318
- planTokens: known(unique.map((receipt) => receipt.actual.totalTokens), (items) => items.reduce((total, value) => total + value, 0)),
319
- toolRejections: known(unique.map((receipt) => receipt.progress.failedRecords), (items) => items.reduce((total, value) => total + value, 0)),
320
- retries: complete ? unique.reduce((total, receipt) => total + Math.max(0, receipt.attempts.length - 1), 0) : null,
337
+ planTokens: known(model.map((receipt) => receipt.actual.totalTokens), (items) => items.reduce((total, value) => total + value, 0)),
338
+ toolRejections: known(model.map((receipt) => receipt.progress.failedRecords), (items) => items.reduce((total, value) => total + value, 0)),
339
+ factReplacements: known(model.map(r => r.progress.durableReplacements), items => items.reduce((a, b) => a + b, 0)),
340
+ finalizeRejections: known(unique.map(r => r.progress.finalizeRejections), items => items.reduce((a, b) => a + b, 0)),
341
+ correctionSessions: complete ? model.filter(r => r.identity.dispatchReason === "correction").length : null,
342
+ transportRetries: complete ? model.reduce((total, receipt) => total + Math.max(0, receipt.attempts.length - 1), 0) : null,
321
343
  };
322
344
  }
323
345
  /** Validate the fields consumed by aggregation before accessing nested values.
@@ -325,11 +347,15 @@ export function summarizeFrontendPlanCanary(receipts) {
325
347
  function isCanaryReceipt(value) {
326
348
  const r = record(value), identity = record(r.identity), actual = record(r.actual), progress = record(r.progress);
327
349
  const nullableCount = (v) => v === null || (typeof v === "number" && Number.isFinite(v) && Number.isInteger(v) && v >= 0);
328
- return r.schemaVersion === 1 && r.policyVersion === "frontend-session-observation-v1"
350
+ return r.schemaVersion === 2 && r.policyVersion === "frontend-session-observation-v2"
351
+ && ["model", "runtime"].includes(String(identity.executionKind))
352
+ && ["initial", "correction"].includes(String(identity.dispatchReason))
353
+ && typeof identity.phase === "string" && Array.isArray(identity.scopeIds)
329
354
  && typeof identity.sessionId === "string" && identity.sessionId.length > 0
330
355
  && typeof r.startedAt === "string" && Number.isFinite(Date.parse(r.startedAt))
331
356
  && (r.completion === "incomplete" || (r.completion === "complete" && typeof r.finishedAt === "string" && Date.parse(r.finishedAt) >= Date.parse(r.startedAt)))
332
357
  && Array.isArray(r.attempts) && r.attempts.every(a => a !== null && typeof a === "object")
358
+ && nullableCount(progress.durableReplacements) && nullableCount(progress.finalizeRejections)
333
359
  && nullableCount(actual.requestCount) && nullableCount(actual.totalTokens) && nullableCount(progress.failedRecords);
334
360
  }
335
361
  /** Read one frozen canary run. The optional artifact may add only run-level
@@ -337,27 +363,14 @@ function isCanaryReceipt(value) {
337
363
  * request usage and tool rejections always come from session receipts. */
338
364
  export async function readFrontendPlanCanaryRunMetrics(runDir, nodeId = "frontend-plan-pi") {
339
365
  const sessionMetrics = summarizeFrontendPlanCanary(await readFrontendSessionBudgetReceipts(runDir, nodeId));
340
- let supplemental = {};
341
- try {
342
- const parsed = JSON.parse(await readFile(path.join(runDir, "frontend-plan-canary-metrics.json"), "utf8"));
343
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
344
- supplemental = parsed;
345
- }
346
- }
347
- catch {
348
- // Missing or malformed supplemental evidence remains unavailable.
349
- }
350
- const optionalNumber = (key) => {
351
- const value = supplemental[key];
352
- return typeof value === "number" && Number.isFinite(value) && value >= 0
353
- ? value
354
- : null;
355
- };
366
+ // A freely editable metrics JSON is not execution or adjudication evidence.
367
+ // Root evaluation must supply validated lineage and independent acceptance;
368
+ // until then the Plan projection can pass only its own local gate.
356
369
  return {
357
370
  ...sessionMetrics,
358
- dagWallClockMs: optionalNumber("dagWallClockMs"),
359
- designReviewRejectRate: optionalNumber("designReviewRejectRate"),
360
- finalReviewDefectRate: optionalNumber("finalReviewDefectRate"),
361
- recoveryCount: optionalNumber("recoveryCount"),
371
+ dagWallClockMs: null,
372
+ designReviewRejectRate: null,
373
+ finalReviewDefectRate: null,
374
+ recoveryCount: null,
362
375
  };
363
376
  }
@@ -0,0 +1,10 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const context = new AsyncLocalStorage();
3
+ /** Async-local identity only; no mutable budget or usage is stored here. */
4
+ export function withFrontendSessionIdentity(identity, execute) {
5
+ return context.run(structuredClone(identity), execute);
6
+ }
7
+ export function currentFrontendSessionIdentity() {
8
+ const identity = context.getStore();
9
+ return identity && structuredClone(identity);
10
+ }