@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,92 @@
1
+ import { assemblePlanPatchFromCommittedFacts } from "./frontend-committed-facts.js";
2
+ import { createHash } from "node:crypto";
3
+ const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
4
+ const identity = (params) => {
5
+ const p = object(params), entry = object(p.entry ?? p.choice);
6
+ return String(p.id ?? p.scopeId ?? p.requirementId ?? entry.id ?? entry.name ?? entry.purpose ?? entry.state ?? entry.interaction ?? entry.boundary ?? "singleton");
7
+ };
8
+ /** Counts unresolved diagnostic identities, not tool activity. A changed
9
+ * rationale/read/heartbeat cannot resolve the same compiler diagnostic. */
10
+ export function createFrontendPlanProgressGuard() {
11
+ let stop;
12
+ let terminal = false;
13
+ let failures = [];
14
+ const states = [];
15
+ const warned = new Set();
16
+ const listeners = new Set();
17
+ return {
18
+ readStop: () => stop,
19
+ acceptTerminal: () => { if (!stop) {
20
+ terminal = true;
21
+ failures = [];
22
+ } },
23
+ subscribe(listener) { listeners.add(listener); if (stop)
24
+ listener(); return () => { listeners.delete(listener); }; },
25
+ observe(tool, params, details, committed, effectiveDigest) {
26
+ if (stop)
27
+ return stop;
28
+ if (terminal)
29
+ return undefined;
30
+ const key = `${tool}:${identity(params)}`;
31
+ if (details?.ok === true) {
32
+ // Only a successful durable correction of the rejected identity (or
33
+ // a successful compile) resolves it. Unrelated writes do not reset it.
34
+ if (tool.startsWith("finalize_") && committed) {
35
+ terminal = true;
36
+ failures = [];
37
+ }
38
+ else if (committed)
39
+ failures = failures.filter(f => f.key !== key);
40
+ return undefined;
41
+ }
42
+ const stage = typeof details?.validationStage === "string" ? details.validationStage : undefined;
43
+ const completed = Array.isArray(details?.completedValidationStages) ? details.completedValidationStages : [];
44
+ failures = failures.filter(f => !f.stage || !completed.includes(f.stage));
45
+ const error = String(details?.error ?? details?.code ?? "unknown tool failure").replace(/\s+/g, " ").trim();
46
+ const fingerprint = createHash("sha256").update(JSON.stringify([key, details?.code ?? null, error])).digest("hex");
47
+ failures.push({ fingerprint, key, stage });
48
+ const state = `${stage ?? key}:${fingerprint}:${effectiveDigest ?? "unknown"}`;
49
+ states.push(state);
50
+ if (states.length > 8)
51
+ states.shift();
52
+ failures = failures.slice(-12);
53
+ const tail = failures.slice(-6);
54
+ const repeated = failures.filter(f => f.fingerprint === fingerprint).length;
55
+ if (repeated >= 6 || (tail.length === 6 && new Set(tail.map(f => f.fingerprint)).size <= 2) || (states.length === 8 && new Set(states).size <= 2)) {
56
+ stop = { code: "FRONTEND_PLAN_NO_PROGRESS", fingerprint, error: `Unresolved repeated Plan diagnostic: ${error}` };
57
+ for (const listener of listeners)
58
+ listener();
59
+ return stop;
60
+ }
61
+ if (repeated >= 3 && !warned.has(fingerprint)) {
62
+ warned.add(fingerprint);
63
+ return { code: "FRONTEND_PLAN_REPAIR_HINT", fingerprint, error: `The same diagnostic remains unresolved. Correct the named fact before repeating this call: ${error}` };
64
+ }
65
+ return undefined;
66
+ },
67
+ };
68
+ }
69
+ /** Effective facts for the existing node retry ladder. Replacement history and
70
+ * explanation-only edits must not look like a new implementation decision. */
71
+ export function frontendPlanSemanticDigest(records) {
72
+ const current = new Map();
73
+ const strip = (value) => Array.isArray(value) ? value.map(strip) : value && typeof value === "object"
74
+ ? Object.fromEntries(Object.entries(value).filter(([key]) => !["rationale", "replaces"].includes(key)).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, strip(item)])) : value;
75
+ const ordinary = records.some(r => ["plan-requirement", "plan-verification-target", "state-registry", "component-choice", "state-flow", "mock-api"].includes(String(object(r.fact).kind)));
76
+ if (ordinary) {
77
+ try {
78
+ const patch = assemblePlanPatchFromCommittedFacts(records.map(r => ({ ...r, fact: { ...object(r.fact), origin: "plan" } })));
79
+ if (patch)
80
+ return createHash("sha256").update(JSON.stringify(strip(patch))).digest("hex");
81
+ }
82
+ catch { /* An incomplete ledger still needs a stable diagnostic state. */ }
83
+ }
84
+ for (const record of records) {
85
+ if (record.phase !== "committed")
86
+ continue;
87
+ const fact = object(record.fact);
88
+ const key = `${fact.kind}:${identity(fact)}`;
89
+ current.set(key, strip(fact));
90
+ }
91
+ return createHash("sha256").update(JSON.stringify([...current].sort(([a], [b]) => a.localeCompare(b)))).digest("hex");
92
+ }
@@ -61,11 +61,14 @@ function computeFrontendRecoveryPlanForSource(input) {
61
61
  // verification commands / trace. Resetting only verify + its descendants
62
62
  // would re-run the same failing checks against the same implementation, so
63
63
  // reset from implement: the recovery re-implements and re-verifies.
64
- const resetRoot = input.failureSource === "frontend-verify-shell"
65
- ? "frontend-implement-pi"
66
- : input.failureSource === "frontend-review-pi"
67
- ? "frontend-implement-pi"
68
- : input.failureSource;
64
+ const ownerRoots = { contract: "frontend-contract-pi", scout: "frontend-scout-pi", plan: "frontend-plan-pi", implementation: "frontend-implement-pi", "verification-config": "frontend-contract-pi" };
65
+ if (input.failureOwner && !ownerRoots[input.failureOwner])
66
+ throw new Error(`FRONTEND_RECOVERY_OWNER_UNSUPPORTED: ${input.failureOwner}; resolve the owning input before retrying`);
67
+ const resetRoot = input.failureOwner ? ownerRoots[input.failureOwner] :
68
+ input.failureSource === "frontend-verify-shell" || input.failureSource === "frontend-review-pi" ? "frontend-implement-pi" :
69
+ input.failureSource === "frontend-design-review-pi" ? "frontend-plan-pi" : input.failureSource;
70
+ if (!input.spec.tasks.some(task => task.id === resetRoot))
71
+ throw new Error(`FRONTEND_RECOVERY_PRODUCER_UNAVAILABLE: ${resetRoot}; this topology has no authorized producer, reproject the task contract instead of importing the same candidate`);
69
72
  const resetNodeIds = computeResetClosure(input.spec, resetRoot);
70
73
  const resetSet = new Set(resetNodeIds);
71
74
  const importedNodeIds = input.spec.tasks
@@ -77,6 +80,8 @@ function computeFrontendRecoveryPlanForSource(input) {
77
80
  parentRunId: input.parentRunId,
78
81
  requestId: input.requestId,
79
82
  failureSource: input.failureSource,
83
+ ...(input.failureOwner ? { failureOwner: input.failureOwner } : {}),
84
+ resetRootNodeId: resetRoot,
80
85
  resetNodeIds,
81
86
  importedNodeIds,
82
87
  };
@@ -298,6 +298,34 @@ async function casUpdateFrontendRecoveryState(parentRunDir, expectedRevision, de
298
298
  }
299
299
  async function materializeChildStaging(input) {
300
300
  const { cwd, parentRunDir, parentRunId, parentSpec, parentState, recovery, recoveryPlan, requestId, childRunId, stagingDir, createdAt, } = input;
301
+ // Phase 5: writer transient partial-write recovery resets the writer subtree
302
+ // (frontend-implement-pi) instead of the candidate producer; everything
303
+ // upstream is imported as verified parent facts.
304
+ let plan;
305
+ if (recoveryPlan) {
306
+ plan = recoveryPlan;
307
+ }
308
+ else if (recovery.failureSource) {
309
+ plan = computeFrontendRecoveryPlanForSource({
310
+ spec: parentSpec,
311
+ parentRunId,
312
+ requestId,
313
+ failureSource: recovery.failureSource,
314
+ failureOwner: recovery.failureOwner,
315
+ });
316
+ }
317
+ else {
318
+ const admissionRead = await readFrontendWriterAdmissionResult(parentRunDir);
319
+ if (!admissionRead.ok) {
320
+ throw new Error(admissionRead.reason);
321
+ }
322
+ plan = computeFrontendRecoveryPlan({
323
+ spec: parentSpec,
324
+ parentRunId,
325
+ requestId,
326
+ result: admissionRead.result,
327
+ });
328
+ }
301
329
  await prepareRunDir(stagingDir);
302
330
  // Design-review rejection must be visible to the reset planner. Recovery
303
331
  // children otherwise copy the old spec and only rerun design review, which
@@ -305,6 +333,8 @@ async function materializeChildStaging(input) {
305
333
  // findings into the child spec as ordinary rerun feedback.
306
334
  let childSpec = parentSpec;
307
335
  if (recovery.failureSource === "frontend-plan-pi" ||
336
+ recovery.failureSource === "frontend-design-review-pi" ||
337
+ plan.resetRootNodeId === "frontend-contract-pi" ||
308
338
  recovery.failureSource === "frontend-review-pi") {
309
339
  const designRecovery = parentState.terminalReason === "design-request-changes";
310
340
  const reviewRecovery = recovery.failureSource === "frontend-review-pi" ||
@@ -331,12 +361,13 @@ async function materializeChildStaging(input) {
331
361
  }
332
362
  }
333
363
  catch (error) {
334
- if (designRecovery || reviewRecovery) {
335
- throw new Error(`frontend recovery feedback unavailable: ${error instanceof Error ? error.message : String(error)}`);
336
- }
364
+ throw new Error(`frontend recovery feedback unavailable: ${error instanceof Error ? error.message : String(error)}`);
337
365
  }
338
366
  if (feedback) {
339
367
  childSpec = { ...parentSpec, rerunFeedback: feedback };
368
+ if (plan.resetRootNodeId === "frontend-contract-pi") {
369
+ childSpec = { ...childSpec, tasks: childSpec.tasks.map(task => task.id !== plan.resetRootNodeId ? task : { ...task, subtask_prompt: `${task.subtask_prompt}\n\n<contract_recovery_diagnostic>\nParent run: ${parentRunId}\nSource node: ${feedback.sourceNodeId}\nRe-extract only from the unchanged frozen task source. These parent findings are diagnostic hypotheses, not new requirements or authorization. Do not import the parent's Plan as Contract facts. Report unresolved source ambiguity rather than inventing a decision.\n${feedback.feedbackText}\n</contract_recovery_diagnostic>` }) };
370
+ }
340
371
  }
341
372
  else if (designRecovery || reviewRecovery) {
342
373
  // A design/review rejection cannot be safely retried without its typed
@@ -346,33 +377,6 @@ async function materializeChildStaging(input) {
346
377
  }
347
378
  }
348
379
  await writeJsonVerified(stagingDir, "run.json", childSpec);
349
- // Phase 5: writer transient partial-write recovery resets the writer subtree
350
- // (frontend-implement-pi) instead of the candidate producer; everything
351
- // upstream is imported as verified parent facts.
352
- let plan;
353
- if (recoveryPlan) {
354
- plan = recoveryPlan;
355
- }
356
- else if (recovery.failureSource) {
357
- plan = computeFrontendRecoveryPlanForSource({
358
- spec: parentSpec,
359
- parentRunId,
360
- requestId,
361
- failureSource: recovery.failureSource,
362
- });
363
- }
364
- else {
365
- const admissionRead = await readFrontendWriterAdmissionResult(parentRunDir);
366
- if (!admissionRead.ok) {
367
- throw new Error(admissionRead.reason);
368
- }
369
- plan = computeFrontendRecoveryPlan({
370
- spec: parentSpec,
371
- parentRunId,
372
- requestId,
373
- result: admissionRead.result,
374
- });
375
- }
376
380
  const { ranks } = topoSortToRanks(childSpec);
377
381
  const resetSet = new Set(plan.resetNodeIds);
378
382
  const importedSet = new Set(plan.importedNodeIds);
@@ -408,16 +412,16 @@ async function materializeChildStaging(input) {
408
412
  }
409
413
  // Canonical contract + writer-admission identity facts are copied alongside
410
414
  // the per-node facts so reset nodes can re-read them (AC-4).
411
- const canonicalContract = await copyArtifactVerified({
415
+ const canonicalContract = plan.importedNodeIds.some(id => ["frontend-design-policy-shell", "frontend-contract-import-shell"].includes(id)) ? await copyArtifactVerified({
412
416
  parentRunDir,
413
417
  newRunDir: stagingDir,
414
418
  relativePath: resolveCanonicalContractRelPath(childSpec),
415
- });
416
- const writerAdmissionResult = await copyArtifactVerified({
419
+ }) : undefined;
420
+ const writerAdmissionResult = plan.importedNodeIds.includes("frontend-writer-admission-shell") ? await copyArtifactVerified({
417
421
  parentRunDir,
418
422
  newRunDir: stagingDir,
419
423
  relativePath: FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT,
420
- });
424
+ }) : undefined;
421
425
  // Imported nodes keep their parent FINISHED record, so the run-owned files
422
426
  // their bodies wrote outside the node directory travel with them (smoke r31:
423
427
  // a review-triggered recovery reached review-context with no worktree
@@ -470,6 +474,7 @@ async function materializeChildStaging(input) {
470
474
  }
471
475
  }
472
476
  const childState = {
477
+ ...(parentSpec.sourceProviderBudget ? { sourceProviderBudget: structuredClone(parentSpec.sourceProviderBudget) } : {}),
473
478
  ...(parentState.budget ? { budget: structuredClone(parentState.budget) } : {}),
474
479
  ...(parentState.budgetLedger ? { budgetLedger: structuredClone(parentState.budgetLedger) } : {}),
475
480
  version: 1,
@@ -506,6 +511,8 @@ async function materializeChildStaging(input) {
506
511
  ...(recovery.failureSource
507
512
  ? { failureSource: recovery.failureSource }
508
513
  : {}),
514
+ ...(plan.failureOwner ? { failureOwner: plan.failureOwner } : {}),
515
+ ...(plan.resetRootNodeId ? { resetRootNodeId: plan.resetRootNodeId } : {}),
509
516
  revision: 0,
510
517
  },
511
518
  ...(parentState.evaluation
@@ -518,14 +525,8 @@ async function materializeChildStaging(input) {
518
525
  parentRunId,
519
526
  requestId,
520
527
  createdAt,
521
- canonicalContract: {
522
- relativePath: canonicalContract.destinationRelativePath,
523
- sha256: canonicalContract.sha256,
524
- },
525
- writerAdmissionResult: {
526
- relativePath: writerAdmissionResult.destinationRelativePath,
527
- sha256: writerAdmissionResult.sha256,
528
- },
528
+ ...(canonicalContract ? { canonicalContract: { relativePath: canonicalContract.destinationRelativePath, sha256: canonicalContract.sha256 } } : {}),
529
+ ...(writerAdmissionResult ? { writerAdmissionResult: { relativePath: writerAdmissionResult.destinationRelativePath, sha256: writerAdmissionResult.sha256 } } : {}),
529
530
  importedRunArtifacts,
530
531
  importedFacts,
531
532
  };
@@ -590,7 +591,8 @@ export async function stageRecoveryChild(options) {
590
591
  recovery.recoveryRootRunId !== intent.recoveryRootRunId ||
591
592
  recovery.attemptIndex !== intent.attemptIndex ||
592
593
  recovery.continuationCount !== intent.continuationCount ||
593
- recovery.failureSource !== intent.failureSource) {
594
+ recovery.failureSource !== intent.failureSource ||
595
+ recovery.failureOwner !== intent.failureOwner || recovery.resetRootNodeId !== intent.resetRootNodeId) {
594
596
  throw new Error("frontend recovery intent replay binding mismatch");
595
597
  }
596
598
  }
@@ -620,7 +622,8 @@ export async function stageRecoveryChild(options) {
620
622
  if (options.recoveryPlan) {
621
623
  if (options.recoveryPlan.parentRunId !== parentRunId ||
622
624
  options.recoveryPlan.requestId !== requestId ||
623
- options.recoveryPlan.failureSource !== recovery.failureSource) {
625
+ options.recoveryPlan.failureSource !== recovery.failureSource ||
626
+ options.recoveryPlan.failureOwner !== recovery.failureOwner || options.recoveryPlan.resetRootNodeId !== recovery.resetRootNodeId) {
624
627
  throw new Error("frontend recovery plan binding mismatch");
625
628
  }
626
629
  const expectedPlan = computeFrontendRecoveryPlanForSource({
@@ -628,9 +631,11 @@ export async function stageRecoveryChild(options) {
628
631
  parentRunId,
629
632
  requestId,
630
633
  failureSource: recovery.failureSource,
634
+ failureOwner: recovery.failureOwner,
631
635
  });
632
- if (JSON.stringify(expectedPlan.resetNodeIds) !==
633
- JSON.stringify(options.recoveryPlan.resetNodeIds) ||
636
+ if (expectedPlan.resetRootNodeId !== options.recoveryPlan.resetRootNodeId ||
637
+ JSON.stringify(expectedPlan.resetNodeIds) !==
638
+ JSON.stringify(options.recoveryPlan.resetNodeIds) ||
634
639
  JSON.stringify(expectedPlan.importedNodeIds) !==
635
640
  JSON.stringify(options.recoveryPlan.importedNodeIds)) {
636
641
  throw new Error("frontend recovery plan drifted from parent DAG");
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ export const repairAssertionSchema = z.discriminatedUnion("check", [
3
+ z.object({ check: z.literal("file-absent"), token: z.string().min(1).max(512), source: z.string().max(4096) }).strict(),
4
+ z.object({ check: z.literal("state-present"), token: z.string().min(1).max(512), source: z.string().max(4096) }).strict(),
5
+ z.object({ check: z.literal("vt-changed"), token: z.string().min(1).max(512), source: z.string().max(4096), expectedRequirementIds: z.array(z.string().min(1)).min(1).max(128) }).strict(),
6
+ ]);
7
+ /** Invalid optional assertions never erase a blocking finding or its verdict. */
8
+ export function sanitizeRepairAssertions(value) {
9
+ if (value === undefined)
10
+ return { assertions: [], diagnostics: [] };
11
+ if (!Array.isArray(value))
12
+ return { assertions: [], diagnostics: ["repairAssertions must be an array; semantic review remains required"] };
13
+ const assertions = [], diagnostics = [];
14
+ for (const [index, raw] of value.slice(0, 32).entries()) {
15
+ const parsed = repairAssertionSchema.safeParse(raw);
16
+ if (parsed.success)
17
+ assertions.push(parsed.data);
18
+ else
19
+ diagnostics.push(`repairAssertions[${index}] invalid; semantic review remains required`);
20
+ }
21
+ if (value.length > 32)
22
+ diagnostics.push("repairAssertions exceeds 32 items; excess items require semantic review");
23
+ return { assertions, diagnostics };
24
+ }
25
+ /** Review annotations cannot authorize deletion or invent requirements/states. */
26
+ export function authorizeRepairAssertions(assertions, input) {
27
+ const files = new Set(input.parent.targets?.files ?? []);
28
+ const required = new Set([...(input.parent.targets?.requiredDeliverables ?? []), ...(input.requiredDeliverables ?? [])]);
29
+ const states = new Set(input.stateIds);
30
+ const targets = new Set(input.parent.verificationTargets?.map(target => String(target.id)) ?? []);
31
+ return assertions.flatMap(assertion => {
32
+ const permitted = assertion.check === "file-absent" ? files.has(assertion.token) && !required.has(assertion.token) :
33
+ assertion.check === "state-present" ? states.has(assertion.token) : targets.has(assertion.token) && assertion.expectedRequirementIds.every(id => input.requirementIds.includes(id));
34
+ return permitted ? [] : [`${assertion.check}: ${assertion.token} is outside frozen repair authority or contradicts a required delivery`];
35
+ });
36
+ }
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFile } from "node:fs/promises";
2
+ import { readFile, lstat } from "node:fs/promises";
3
+ import { readFileBoundarySafe } from "../../shared/path-safety.js";
3
4
  import path from "node:path";
4
5
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
5
6
  import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "./frontend-worktree-diff.js";
@@ -244,6 +245,31 @@ function assertReviewEvidence(input) {
244
245
  throw new FrontendReviewContextFailure("review-context-verification-unbound", "frontend review context verification trace is not bound to a passing contract");
245
246
  }
246
247
  }
248
+ /** Snapshot only exact behavior-test paths already named by the frozen contract. */
249
+ export async function captureFrontendReviewTestSources(input) {
250
+ const sources = [];
251
+ for (const [index, file] of [...new Set(input.files)].entries()) {
252
+ const targetPath = path.resolve(input.workspaceRoot, file);
253
+ const exists = await lstat(targetPath).catch((error) => { if (error.code === "ENOENT")
254
+ return undefined; throw error; });
255
+ if (!exists) {
256
+ sources.push({ file, status: "unavailable", scopes: [] });
257
+ continue;
258
+ }
259
+ const bytes = await readFileBoundarySafe({ boundaryRoot: input.workspaceRoot, targetPath, label: "frontend review test source" });
260
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
261
+ const text = bytes.toString("utf8");
262
+ const scopes = [];
263
+ // Small text slices bound each model input; all slices are retained.
264
+ for (let offset = 0; offset < Math.max(1, text.length); offset += 2000) {
265
+ const relative = `contracts/test-source-${index}-${offset / 2000}.json`;
266
+ await writeDagRunJsonArtifact(input.runDir, relative, { schemaVersion: 1, file, sourceSha256: sha256, offset, totalCharacters: text.length, text: text.slice(offset, offset + 2000) });
267
+ scopes.push({ path: relative, sha256: createHash("sha256").update(await readFile(path.join(input.runDir, relative))).digest("hex") });
268
+ }
269
+ sources.push({ file, status: "captured", sha256, scopes });
270
+ }
271
+ return sources;
272
+ }
247
273
  export async function runFrontendReviewContextGate(input) {
248
274
  const contractRelativePath = "contracts/frontend-implementation-contract.json";
249
275
  let contractRaw;
@@ -284,6 +310,7 @@ export async function runFrontendReviewContextGate(input) {
284
310
  const verifyFailure = await readOptionalVerifyFailure(input.runDir);
285
311
  const lintAssessment = await readOptionalLintAssessment(input.runDir);
286
312
  const verificationEvidence = await readVerificationEvidence(input.runDir);
313
+ const testSources = await captureFrontendReviewTestSources({ ...input, files: parsedContract.data.verificationTargets.filter(target => target.mode === "behavior").map(target => target.file) });
287
314
  const cumulativeDiff = await captureCumulativeDiffContext({
288
315
  runDir: input.runDir,
289
316
  workspaceRoot: input.workspaceRoot,
@@ -320,6 +347,7 @@ export async function runFrontendReviewContextGate(input) {
320
347
  },
321
348
  verificationTrace,
322
349
  verificationEvidence,
350
+ testSources,
323
351
  // Protocol-legal lint vocabulary; `lintConfigured: false` tells the
324
352
  // reviewer that this task declares no lint surface at all, so
325
353
  // `unavailable` reads as "not applicable", not "missing evidence".
@@ -1,3 +1,5 @@
1
+ import { parseFrontendTestExecutionReport } from "./frontend-test-execution-evidence.js";
2
+ import { readFileBoundarySafe } from "../../shared/path-safety.js";
1
3
  import { createHash, randomUUID } from "node:crypto";
2
4
  import { readFile, realpath, lstat } from "node:fs/promises";
3
5
  import path from "node:path";
@@ -11,9 +13,10 @@ const isArtifactReference = (value) => isRecord(value)
11
13
  && typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256);
12
14
  const hash = (text) => createHash("sha256").update(text).digest("hex");
13
15
  /** The section index is navigation; reconstruct coverage against the canonical source before using it. */
14
- export async function loadFrontendReviewScopes(runDir, phase, targetBytes) {
16
+ export async function loadFrontendReviewScopes(runDir, phase, targetBytes, workspaceRoot) {
15
17
  const root = await realpath(runDir);
16
18
  const files = new Map();
19
+ const sourceChecks = [];
17
20
  async function read(relative, expected) {
18
21
  const absolute = path.resolve(root, relative);
19
22
  if (!absolute.startsWith(root + path.sep) || (await lstat(absolute)).isSymbolicLink() || !(await realpath(absolute)).startsWith(root + path.sep))
@@ -103,11 +106,88 @@ export async function loadFrontendReviewScopes(runDir, phase, targetBytes) {
103
106
  if (diffIndex.patchSha256 !== payload.diff.patchSha256)
104
107
  throw Error("FRONTEND_REVIEW_SCOPE_DRIFT: review diff binding");
105
108
  scopes.push(context);
109
+ const reportCoverage = new Map();
110
+ const reportedTests = [];
111
+ for (const ref of payload.verificationTrace?.testExecutionEvidence ?? []) {
112
+ if (!isArtifactReference(ref) || typeof ref.reportPath !== "string" || !/^[a-f0-9]{64}$/.test(ref.reportSha256))
113
+ throw invalidScope("invalid test execution reference");
114
+ const excerpt = await read(ref.path, ref.sha256);
115
+ const data = JSON.parse(excerpt.text);
116
+ if (data.reportSha256 !== ref.reportSha256)
117
+ throw invalidScope("test execution report binding mismatch");
118
+ const coverage = reportCoverage.get(ref.reportPath) ?? { count: 0, total: data.total };
119
+ if (!Number.isInteger(data.total) || data.total < 1 || data.total !== coverage.total || data.offset !== coverage.count || !Array.isArray(data.tests) || !data.tests.length || data.tests.length > 24)
120
+ throw invalidScope("test report excerpt coverage invalid");
121
+ coverage.count += data.tests.length;
122
+ reportCoverage.set(ref.reportPath, coverage);
123
+ const report = await read(ref.reportPath, ref.reportSha256);
124
+ if (workspaceRoot) {
125
+ if (typeof data.cwd !== "string" || !Number.isInteger(data.offset) || data.offset < 0)
126
+ throw invalidScope("invalid test report excerpt coordinates");
127
+ const rootPath = await realpath(workspaceRoot);
128
+ const cwdPath = await realpath(path.resolve(workspaceRoot, data.cwd));
129
+ if (cwdPath !== rootPath && !cwdPath.startsWith(rootPath + path.sep))
130
+ throw invalidScope("test report cwd escaped workspace");
131
+ const actual = parseFrontendTestExecutionReport(JSON.parse(report.text), cwdPath, rootPath);
132
+ const expected = actual.slice(data.offset, data.offset + 24).map(test => ({ file: test.file, status: test.status, title: test.title.slice(0, 500), ...(test.title.length > 500 ? { titleTruncated: true } : {}) }));
133
+ if (data.total !== actual.length || !expected.length || sha256OfCanonicalJson(expected) !== sha256OfCanonicalJson(data.tests))
134
+ throw invalidScope("test report excerpt differs from actual runner report");
135
+ }
136
+ if (data.tests.some((test) => !isRecord(test) || typeof test.file !== "string" || typeof test.status !== "string"))
137
+ throw invalidScope("invalid test report records");
138
+ reportedTests.push(...data.tests);
139
+ scopes.push(excerpt);
140
+ }
141
+ if ([...reportCoverage.values()].some(coverage => coverage.count !== coverage.total))
142
+ throw invalidScope("test report excerpts incomplete");
143
+ const allowedTests = new Set((canonical.verificationTargets ?? []).filter((target) => target.mode === "behavior").map((target) => target.file));
144
+ // A trace may claim execution even when its entire report reference list
145
+ // was omitted. Require the actual, hash-checked reports for every behavior
146
+ // file before permitting review of the corresponding implementation.
147
+ for (const file of allowedTests) {
148
+ const normalized = path.posix.normalize(String(file).replace(/\\/g, "/"));
149
+ const matching = reportedTests.filter(test => path.posix.normalize(test.file) === normalized);
150
+ if (!workspaceRoot || !matching.length || matching.some(test => test.status !== "passed"))
151
+ throw invalidScope(`behavior test report coverage incomplete or not passed: ${String(file)}`);
152
+ }
153
+ if (allowedTests.size && (!Array.isArray(payload.testSources) || payload.testSources.length !== allowedTests.size || new Set(payload.testSources.map((source) => source.file)).size !== allowedTests.size))
154
+ throw invalidScope("contract test source coverage incomplete");
155
+ for (const source of payload.testSources ?? []) {
156
+ if (!allowedTests.has(source.file))
157
+ throw invalidScope("test source is outside frozen contract");
158
+ if (source.status === "unavailable")
159
+ continue;
160
+ if (source.status !== "captured" || !/^[a-f0-9]{64}$/.test(source.sha256) || !Array.isArray(source.scopes) || !source.scopes.length)
161
+ throw invalidScope("invalid test source reference");
162
+ let content = "";
163
+ for (const ref of source.scopes) {
164
+ if (!isArtifactReference(ref))
165
+ throw invalidScope("invalid test source slice");
166
+ const excerpt = await read(ref.path, ref.sha256);
167
+ const data = JSON.parse(excerpt.text);
168
+ if (data.file !== source.file || data.sourceSha256 !== source.sha256 || data.offset !== content.length || typeof data.text !== "string")
169
+ throw invalidScope("test source slice binding mismatch");
170
+ content += data.text;
171
+ scopes.push(excerpt);
172
+ }
173
+ if (hash(content) !== source.sha256)
174
+ throw invalidScope("test source snapshot incomplete");
175
+ if (workspaceRoot) {
176
+ const check = async () => {
177
+ const bytes = await readFileBoundarySafe({ boundaryRoot: workspaceRoot, targetPath: path.resolve(workspaceRoot, source.file), label: "frontend review test source" });
178
+ if (createHash("sha256").update(bytes).digest("hex") !== source.sha256)
179
+ throw Error(`FRONTEND_REVIEW_SCOPE_DRIFT: ${source.file}`);
180
+ };
181
+ await check();
182
+ sourceChecks.push(check);
183
+ }
184
+ }
106
185
  for (const hunk of diffIndex.hunks)
107
186
  scopes.push(await read(hunk.patchPath, hunk.sha256));
108
187
  }
109
188
  return { scopes, digest: sha256OfCanonicalJson([...files]), validate: async () => { for (const [relative, expected] of files)
110
- await read(relative, expected); } };
189
+ await read(relative, expected); for (const check of sourceChecks)
190
+ await check(); } };
111
191
  }
112
192
  /** Checkpoints attest explicit model completion of already supplied, hash-bound scopes. */
113
193
  export function createFrontendReviewScopeProtocol(input) {
@@ -118,6 +198,16 @@ export function createFrontendReviewScopeProtocol(input) {
118
198
  return {
119
199
  setActiveScope(ids) { active = new Set(ids); },
120
200
  completedScopeIds,
201
+ findingScopeIds(previous) {
202
+ if (!input.inventory)
203
+ return undefined;
204
+ const ids = previous ?? [...active];
205
+ if (!ids.length || ids.some(id => !active.has(id) || !input.inventory.scopes.some(scope => scope.id === id)))
206
+ throw Object.assign(Error("Findings require their original active review scope"), { code: "FRONTEND_INPUT_SCOPE_VIOLATION" });
207
+ if (ids.some(id => completedScopeIds().has(id)))
208
+ throw Object.assign(Error("A completed review scope cannot be changed; restart its owning review"), { code: "FRONTEND_REVIEW_SCOPE_COMPLETED" });
209
+ return [...ids];
210
+ },
121
211
  committedFacts: () => readCommittedEvents(input.getStore(), input.attemptId),
122
212
  assertComplete() { const missing = input.inventory?.scopes.filter(s => !completedScopeIds().has(s.id)) ?? []; if (missing.length)
123
213
  throw Object.assign(Error(`FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${missing.map(s => s.id).join(", ")}`), { code: "FRONTEND_REVIEW_SCOPE_INCOMPLETE" }); },
@@ -55,7 +55,12 @@ function isExplicitSharedExclusion(clause, pattern) {
55
55
  const prefix = clause.slice(0, match.index).trim();
56
56
  const suffix = clause.slice(match.index + match[0].length).trim();
57
57
  const actionInSignal = ["new-route", "new-dependency"].includes(pattern.id);
58
- if (suffix && !(actionInSignal && /^no$/i.test(prefix) && /^is\s+(?:required|needed)$/i.test(suffix)))
58
+ // A coordinated toolchain object remains under the same explicit negation.
59
+ // Match the entire suffix: qualifications ("unless..." / "除非...") and
60
+ // consequences must still retain the signal.
61
+ const excludedToolchainObject = pattern.id === "new-dependency" &&
62
+ /^(?:或|和|及|与)\s*(?:构建|打包)(?:系统|工具|流程)$/.test(suffix);
63
+ if (suffix && !excludedToolchainObject && !(actionInSignal && /^no$/i.test(prefix) && /^is\s+(?:required|needed)$/i.test(suffix)))
59
64
  return false;
60
65
  if (/^(?:不涉及|不(?:修改|变更|调整|改变|新增|添加)|无需(?:修改|变更|调整|改变|新增|添加)|no\s+changes?\s+to|(?:do\s+not|don't)\s+(?:change|modify))$/i.test(prefix))
61
66
  return true;
@@ -169,13 +174,13 @@ export function classifyFrontendRisk(input) {
169
174
  profile.includes("supervised") ||
170
175
  /\bsupervised\b/i.test(blob);
171
176
  for (const pattern of HIGH_RISK_PATTERNS) {
172
- if (pattern.re.test(blob))
177
+ if (hasAffirmativeSharedSignal(blob, pattern))
173
178
  signals.push(pattern.id);
174
179
  }
175
180
  const paths = input.allowedPaths ?? [];
176
181
  const spread = pathSpreadScore(paths);
177
182
  if (paths.some((p) => /package\.json|pnpm-lock|yarn\.lock|package-lock/.test(p))) {
178
- if (DEPENDENCY_INSTALL_LANGUAGE.test(blob)) {
183
+ if (hasAffirmativeSharedSignal(blob, { id: "new-dependency", re: DEPENDENCY_INSTALL_LANGUAGE })) {
179
184
  signals.push("new-dependency");
180
185
  }
181
186
  else {