@tea-agent/loop-agent 0.43.0 → 0.44.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +408 -367
  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/tools/contract-tools.js +515 -0
  30. package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
  31. package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
  32. package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
  33. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  34. package/dist/executors/dag-pi-executor.js +398 -3384
  35. package/dist/executors/pi-executor.js +6 -1
  36. package/dist/executors/pi-sdk-executor.js +112 -19
  37. package/dist/executors/shell-executor.js +88 -12
  38. package/dist/executors/shell-presets.js +1 -1
  39. package/dist/executors/shell-write-guard.js +26 -3
  40. package/dist/infrastructure/console/operation-store.js +48 -14
  41. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  42. package/dist/shared/file-identity.js +23 -0
  43. package/dist/shared/git-workspace-status.js +482 -0
  44. package/dist/shared/operator/capabilities.js +10 -5
  45. package/dist/shared/path-safety.js +2 -2
  46. package/dist/shared/wizard-local-telemetry.js +51 -0
  47. package/dist/task/config-types.js +22 -8
  48. package/dist/task/contract/project.js +13 -0
  49. package/dist/task/contract/schema.js +2 -7
  50. package/dist/task/frontend-preflight.js +1 -1
  51. package/dist/task/source-prepare/parse-intent.js +124 -5
  52. package/dist/task/source-prepare/prepare.js +6 -1
  53. package/dist/task/source-prepare/semantic-intake.js +18 -3
  54. package/dist/task/source-prepare/source-execution.js +65 -0
  55. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  56. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  57. package/dist/worker/console/chat/assistant-content.js +5 -34
  58. package/dist/worker/console/chat/chat-event-store.js +62 -10
  59. package/dist/worker/console/chat/context-insights.js +723 -0
  60. package/dist/worker/console/chat/conversation-nodes.js +383 -0
  61. package/dist/worker/console/chat/db-connections.js +854 -0
  62. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  63. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
  64. package/dist/worker/console/chat/pi-runtime.js +445 -36
  65. package/dist/worker/console/chat/provider-error.js +0 -12
  66. package/dist/worker/console/chat/repo-browser.js +52 -4
  67. package/dist/worker/console/chat/routes.js +577 -72
  68. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  69. package/dist/worker/console/chat/session-store.js +147 -19
  70. package/dist/worker/console/chat/shortcuts.js +7 -0
  71. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  72. package/dist/worker/console/chat/tools.js +34 -0
  73. package/dist/worker/console/chat/turn-process.js +237 -66
  74. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  75. package/dist/worker/console/console-update-runtime.js +2 -2
  76. package/dist/worker/console/dag-confirmation.js +3 -0
  77. package/dist/worker/console/dag-execution-receipt.js +3 -0
  78. package/dist/worker/console/operator-actions.js +32 -3
  79. package/dist/worker/console/operator-user-error.js +28 -1
  80. package/dist/worker/console/pi-readiness.js +4 -4
  81. package/dist/worker/console/prd-intake-bridge.js +5 -12
  82. package/dist/worker/console/recovery-cta.js +14 -14
  83. package/dist/worker/console/server.js +5 -1
  84. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  85. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
  86. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  87. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  88. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  89. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  90. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  100. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  101. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  102. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  103. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  104. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  105. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  111. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  112. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  113. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  114. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  115. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  116. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  117. package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
  118. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  119. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  120. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  121. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  122. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
  123. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
  124. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  125. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  126. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  127. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  128. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  129. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  130. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  131. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  132. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  133. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  134. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  135. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  136. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  137. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  138. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  139. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  140. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  141. package/dist/worker/console/static/index.html +2 -2
  142. package/dist/worker/console/static-src/app/console-types.js +9 -1
  143. package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
  144. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  145. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
  146. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  147. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  148. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  149. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  150. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  151. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  152. package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
  153. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  154. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  155. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
  156. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  157. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  158. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  159. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
  160. package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
  161. package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
  162. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  163. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  164. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  165. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  166. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  167. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  168. package/dist/worker/console/workspace-context.js +2 -0
  169. package/dist/worker/console/workspace-initialization.js +3 -3
  170. package/dist/worker/observability/read-model.js +23 -2
  171. package/dist/worker/observe/node-context-insights.js +479 -0
  172. package/dist/worker/observe/static/console-theme.css +174 -0
  173. package/dist/worker/observe/static/console-theme.js +36 -0
  174. package/dist/worker/observe/static/operator-chrome.css +85 -0
  175. package/dist/worker/observe/static/operator-chrome.js +51 -0
  176. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  177. package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
  178. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  179. package/dist/workflows/dag/budget-enforcement.js +31 -2
  180. package/dist/workflows/dag/frontend-closeout.js +3 -1
  181. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  182. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  183. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  184. package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
  185. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  186. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  187. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  188. package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
  189. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  190. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  191. package/dist/workflows/dag/frontend-review-context.js +29 -1
  192. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  193. package/dist/workflows/dag/frontend-risk.js +8 -3
  194. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  195. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  196. package/dist/workflows/dag/frontend-session-context.js +10 -0
  197. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  198. package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
  199. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  200. package/dist/workflows/dag/frontend-writer-admission.js +3 -1
  201. package/dist/workflows/dag/init-hybrid.js +108 -37
  202. package/dist/workflows/dag/node-execution.js +11 -11
  203. package/dist/workflows/dag/output-protocol.js +1 -0
  204. package/dist/workflows/dag/rerun-feedback.js +35 -4
  205. package/dist/workflows/dag/rerun-task.js +62 -66
  206. package/dist/workflows/dag/runner.js +58 -12
  207. package/dist/workflows/dag/types.js +15 -8
  208. package/dist/workflows/loop/benchmark.js +11 -8
  209. package/docs/README.md +1 -1
  210. package/docs/architecture/runtime-boundaries.md +1 -1
  211. package/docs/governance/harness-methodology-verification.md +11 -0
  212. package/docs/templates/backend-test-dag.json +5 -4
  213. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  214. package/package.json +6 -4
  215. package/skills/frontend-design-review/SKILL.md +8 -11
  216. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  217. package/skills/frontend-review/SKILL.md +5 -1
  218. package/skills/loop-agent/references/command-reference.md +8 -0
  219. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  220. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  221. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  222. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  223. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  224. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  225. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  226. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -198,6 +198,10 @@ export async function listExplicitlyAssociatedRuns(repoRoot, taskId) {
198
198
  const recorded = new Map((record?.runAssociations ?? []).map((entry) => [entry.runId, entry.source]));
199
199
  const entries = await listAllDagRunEntries(repoRoot);
200
200
  const related = [];
201
+ // Recency key per run. `runId` is NOT chronological (a later recovery run can
202
+ // sort before an earlier one lexicographically, e.g. `dag-…` vs `2026…`), so
203
+ // ordering by runId alone reports a superseded run as the latest.
204
+ const startedAtByRunId = new Map();
201
205
  for (const entry of entries) {
202
206
  let source = recorded.get(entry.runId) ?? null;
203
207
  let stateStatus;
@@ -207,6 +211,9 @@ export async function listExplicitlyAssociatedRuns(repoRoot, taskId) {
207
211
  try {
208
212
  const state = await readDagRunState(entry.runDir);
209
213
  stateStatus = state.status;
214
+ if (typeof state.startedAt === "string") {
215
+ startedAtByRunId.set(entry.runId, state.startedAt);
216
+ }
210
217
  const liveness = assessDagRunLiveness({ state });
211
218
  const activeNode = Object.values(state.nodes ?? {}).find((node) => node.status === "RUNNING");
212
219
  lastMeaningfulProgressAt =
@@ -266,7 +273,25 @@ export async function listExplicitlyAssociatedRuns(repoRoot, taskId) {
266
273
  contractBinding,
267
274
  });
268
275
  }
269
- return related.sort((a, b) => a.runId.localeCompare(b.runId));
276
+ // Chronological order by run start time; runs without a readable start time
277
+ // sort first so a run with real timing is never displaced by an unknown.
278
+ // runId breaks ties deterministically.
279
+ return related.sort((a, b) => {
280
+ const aStarted = startedAtByRunId.get(a.runId);
281
+ const bStarted = startedAtByRunId.get(b.runId);
282
+ if (aStarted !== bStarted) {
283
+ if (aStarted === undefined)
284
+ return -1;
285
+ if (bStarted === undefined)
286
+ return 1;
287
+ const aTime = Date.parse(aStarted);
288
+ const bTime = Date.parse(bStarted);
289
+ if (Number.isFinite(aTime) && Number.isFinite(bTime) && aTime !== bTime) {
290
+ return aTime - bTime;
291
+ }
292
+ }
293
+ return a.runId.localeCompare(b.runId);
294
+ });
270
295
  }
271
296
  function deriveLifecycleState(input) {
272
297
  if (!input.exists)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.43.0",
4
- "gitSha": "0765d0a560eacc5719265999838fc71826f233a0",
5
- "builtAt": "2026-09-14T01:41:52.345Z"
3
+ "version": "0.44.0-next.10",
4
+ "gitSha": "9ab06d59ee69d2e55b671bba52ee83e0362ec56f",
5
+ "builtAt": "2026-09-17T03:17:20.149Z"
6
6
  }
@@ -1,4 +1,5 @@
1
1
  import { runDoctor } from "../commands/doctor.js";
2
+ import { runPromoteRun } from "../commands/promote-run.js";
2
3
  import { runDocsArchive } from "../commands/docs-archive.js";
3
4
  import { runDocsAudit } from "../commands/docs-audit.js";
4
5
  import { runEval } from "../commands/eval.js";
@@ -197,6 +198,16 @@ export const COMMAND_DEFINITIONS = [
197
198
  await runDoctor(adapter, repoRoot, rest.filter((arg) => Boolean(arg)));
198
199
  },
199
200
  },
201
+ {
202
+ name: "promote-run",
203
+ adapter: "required",
204
+ tier: "operator",
205
+ intent: "Promote deterministic evidence from a completed DAG or one-shot run into task artifacts (修改记录.md, 验证结果.md).",
206
+ usage: "promote-run <task-id> --run-id <run-id>",
207
+ handler: async ({ adapter, repoRoot, rest }) => {
208
+ await runPromoteRun(repoRoot, rest.filter((arg) => Boolean(arg)));
209
+ },
210
+ },
200
211
  {
201
212
  name: "docs",
202
213
  adapter: "required",
@@ -29,6 +29,7 @@ import { runDocsArchive } from "../commands/docs-archive.js";
29
29
  import { runDocsAudit } from "../commands/docs-audit.js";
30
30
  import { runEval } from "../commands/eval.js";
31
31
  import { runDoctor } from "../commands/doctor.js";
32
+ import { runPromoteRun } from "../commands/promote-run.js";
32
33
  import { runExamples } from "../commands/examples.js";
33
34
  import { runGoal } from "../commands/goal.js";
34
35
  import { runHandoffCheck } from "../commands/handoff-check.js";
@@ -149,6 +150,9 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
149
150
  case "doctor":
150
151
  await runDoctor(ctx.adapter, ctx.repoRoot, compactArgs([subcommand, ...rest]));
151
152
  return;
153
+ case "promote-run":
154
+ await runPromoteRun(ctx.repoRoot, compactArgs([subcommand, ...rest]));
155
+ return;
152
156
  case "docs":
153
157
  await runDocsCommand(ctx.repoRoot, subcommand, rest);
154
158
  return;
@@ -1,3 +1,4 @@
1
+ import { capabilityBoundarySchema } from "../task/config-types.js";
1
2
  import { advanceTaskLifecycle, } from "../application/task-lifecycle/index.js";
2
3
  import { backendTestConfigSchema, } from "../task/config-types.js";
3
4
  import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
@@ -145,13 +146,10 @@ export function parseTaskAdvanceArgs(args) {
145
146
  if (token === "--capability-boundary") {
146
147
  try {
147
148
  const parsed = JSON.parse(next());
148
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
149
- throw new Error("not an object");
150
- }
151
- options.capabilityBoundary = parsed;
149
+ options.capabilityBoundary = capabilityBoundarySchema.parse(parsed);
152
150
  }
153
151
  catch (error) {
154
- throw new Error(`--capability-boundary must be a JSON object {allowed?, forbidden?}: ${error.message}`);
152
+ throw new Error(`--capability-boundary must be a JSON object {allowed?, forbidden?, requiredClauses?}: ${error.message}`);
155
153
  }
156
154
  continue;
157
155
  }
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { stdin as stdinStream } from "node:process";
4
- import { parseAcceptanceFlag, parseVerifyFlag, parseVerifyTimeoutFlag, prepareTaskSource, PREPARE_MAX_FILE_BYTES, } from "../task/source-prepare/index.js";
4
+ import { parseAcceptanceFlag, parseVerifyFlag, parseVerifyLaneFlag, parseVerifyTimeoutFlag, prepareTaskSource, PREPARE_MAX_FILE_BYTES, } from "../task/source-prepare/index.js";
5
5
  import { parseTaskContractDraft } from "../task/contract/schema.js";
6
6
  import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
7
7
  const COMMAND = "task source prepare";
@@ -28,6 +28,7 @@ const USAGE = `usage:
28
28
  [--open-question <text>]...
29
29
  [--verify <label:command>]...
30
30
  [--verify-timeout <label:timeoutMs>]...
31
+ [--verify-lane <label:static|behavior>]...
31
32
  [--request-id <id>]
32
33
  [--dry-run | --apply]
33
34
  [--force-overwrite-source]
@@ -157,6 +158,10 @@ export function parseTaskSourcePrepareArgs(args) {
157
158
  options.verifyTimeout = pushList(options.verifyTimeout, next());
158
159
  continue;
159
160
  }
161
+ if (token === "--verify-lane") {
162
+ options.verifyLane = pushList(options.verifyLane, next());
163
+ continue;
164
+ }
160
165
  if (token === "--request-id") {
161
166
  options.requestId = next();
162
167
  continue;
@@ -287,13 +292,20 @@ function buildFlags(options) {
287
292
  const parsed = parseVerifyTimeoutFlag(raw);
288
293
  timeouts.set(parsed.label, parsed.timeoutMs);
289
294
  }
295
+ const lanes = new Map();
296
+ for (const raw of options.verifyLane ?? []) {
297
+ const parsed = parseVerifyLaneFlag(raw);
298
+ lanes.set(parsed.label, parsed.mode);
299
+ }
290
300
  const commands = (options.verify ?? []).map((raw) => {
291
301
  const parsed = parseVerifyFlag(raw);
292
302
  const timeoutMs = timeouts.get(parsed.label);
303
+ const mode = lanes.get(parsed.label);
293
304
  return {
294
305
  label: parsed.label,
295
306
  command: parsed.command,
296
307
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
308
+ ...(mode !== undefined ? { mode } : {}),
297
309
  };
298
310
  });
299
311
  // If only timeouts provided without verify, treat as empty explicit list? No — only set when --verify appears.
@@ -0,0 +1,4 @@
1
+ /** DAG Pi 共享类型守卫:纯叶子模块,零依赖,禁止反向依赖执行器入口。 */
2
+ export function isRecordObject(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
@@ -0,0 +1,30 @@
1
+ /** DAG Pi 模型引用解析:纯配置叶子模块,零依赖,禁止反向依赖执行器入口。 */
2
+ export const DEFAULT_DAG_PI_PROVIDER = "wizard-local";
3
+ export const DAG_PI_MODEL_PROVIDERS = {
4
+ "gpt-5.3-codex-spark": "wizard-local",
5
+ "gpt-5.5": "wizard-local",
6
+ "glm-5.2": "wizard-local",
7
+ "deepseek-v4-flash": "deepseek",
8
+ "deepseek-v4-pro": "deepseek",
9
+ };
10
+ export function resolveDagPiModelConfig(modelReference, options) {
11
+ const separatorIndex = modelReference.indexOf("/");
12
+ const qualified = separatorIndex >= 0;
13
+ const provider = qualified
14
+ ? modelReference.slice(0, separatorIndex)
15
+ : (DAG_PI_MODEL_PROVIDERS[modelReference] ?? DEFAULT_DAG_PI_PROVIDER);
16
+ const model = qualified
17
+ ? modelReference.slice(separatorIndex + 1)
18
+ : modelReference;
19
+ if (!provider || !model) {
20
+ throw new Error(`invalid DAG Pi model reference "${modelReference}": expected non-empty provider/model`);
21
+ }
22
+ const explicitThinking = options?.thinking?.trim();
23
+ const thinking = explicitThinking ??
24
+ (provider === "wizard-local" && model === "gpt-5.5" ? "low" : undefined);
25
+ return {
26
+ provider,
27
+ model,
28
+ ...(thinking ? { thinking } : {}),
29
+ };
30
+ }
@@ -0,0 +1,55 @@
1
+ import { routeFrontendProviderCapability } from "../../../workflows/dag/frontend-provider-capability-matrix.js";
2
+ /**
3
+ * 构建 plan ledger 的 fact adopter。`deps.session` 必须传会话对象本身:adopter 会在**调用时**
4
+ * 读取 `session.store`(createDurableFrontendTools 每次域工具调用前后会换入 structuredClone 草稿,
5
+ * 构建时快照会写到过期 store)。
6
+ */
7
+ export function createPlanFactAdopter(deps) {
8
+ return async function adoptPlanFact(kind, requestId, fact) {
9
+ // A+B (AC-005): a provider-capability fact kind reaching the plan ledger
10
+ // is out of route — it belongs to the provider capability channel,
11
+ // not the plan decision ledger. Route it through the frozen seven-kind
12
+ // matrix and fail closed to `unsupported-provider-capability` instead of
13
+ // silently widening the plan catalog.
14
+ const capabilityRoute = routeFrontendProviderCapability({ factKind: kind });
15
+ if (capabilityRoute.ok) {
16
+ return {
17
+ ok: false,
18
+ kind,
19
+ code: "unsupported-provider-capability",
20
+ error: `plan ledger cannot adopt provider capability fact kind: ${kind}`,
21
+ };
22
+ }
23
+ try {
24
+ const staged = deps.stageTypedEventFact({
25
+ store: deps.session.store,
26
+ requestId,
27
+ attemptId: deps.session.attemptId,
28
+ fact: fact,
29
+ });
30
+ const committed = await deps.adoptTypedEventFact({
31
+ store: deps.session.store,
32
+ requestId,
33
+ attemptId: deps.session.attemptId,
34
+ fact: fact,
35
+ eventId: staged.eventId,
36
+ expectedRevision: deps.session.store.revision,
37
+ });
38
+ return {
39
+ ok: true,
40
+ kind,
41
+ eventId: committed.eventId,
42
+ revision: committed.revision,
43
+ error: "",
44
+ };
45
+ }
46
+ catch (error) {
47
+ return {
48
+ ok: false,
49
+ kind,
50
+ code: error?.code,
51
+ error: error instanceof Error ? error.message : String(error),
52
+ };
53
+ }
54
+ };
55
+ }
@@ -1,4 +1,5 @@
1
- import { collectCanonicalStateFlowNames } from "./frontend-contract-facts.js";
1
+ /** DAG Pi plan ledger 完整性事实计算:纯类型与纯函数,禁止反向依赖执行器入口。 */
2
+ import { collectCanonicalStateFlowNames } from "../../../workflows/dag/frontend-contract-facts.js";
2
3
  export function committedFactFromPlanRecord(value) {
3
4
  if (!value || typeof value !== "object" || Array.isArray(value))
4
5
  return undefined;
@@ -66,9 +67,16 @@ export function collectFrontendPlanMissingFacts(input) {
66
67
  });
67
68
  continue;
68
69
  }
69
- const targetIds = Array.isArray(entry.verificationTargetIds)
70
+ // Verification targets are the single authoritative direction. The
71
+ // legacy requirement-side list is accepted only as a fallback while
72
+ // resuming older ledgers; new plans derive it from target.requirementIds.
73
+ const derivedTargetIds = [...verificationTargetRequirements.entries()]
74
+ .filter(([, requirementIds]) => requirementIds.has(id))
75
+ .map(([targetId]) => targetId);
76
+ const legacyTargetIds = Array.isArray(entry.verificationTargetIds)
70
77
  ? entry.verificationTargetIds.filter((value) => typeof value === "string" && value.trim().length > 0)
71
78
  : [];
79
+ const targetIds = derivedTargetIds.length > 0 ? derivedTargetIds : legacyTargetIds;
72
80
  const gap = entry.evidenceGap && typeof entry.evidenceGap === "object"
73
81
  ? entry.evidenceGap
74
82
  : undefined;
@@ -113,44 +121,31 @@ export function collectFrontendPlanPhaseMissingFacts(input) {
113
121
  ];
114
122
  }
115
123
  if (input.phase === "ux-local") {
116
- // One snapshot per check: no cross-revision cache to invalidate.
117
- const required = new Set(input.behaviorRequiredRequirementIds ?? []);
118
- const requested = new Set(input.requirementIds.filter(id => required.has(id)));
119
- if (!requested.size)
120
- return [];
121
- const byRequirement = new Map();
122
- for (const fact of facts) {
123
- for (const id of new Set(planFactStringList(fact.scopeRequirementIds))) {
124
- if (!requested.has(id))
125
- continue;
126
- const entries = byRequirement.get(id) ?? [];
127
- entries.push(fact);
128
- byRequirement.set(id, entries);
129
- }
130
- }
131
- const liveStateFlow = collectCanonicalStateFlowNames(facts);
132
124
  const missing = [];
133
- for (const id of input.requirementIds) {
134
- if (!required.has(id))
125
+ // Evaluate each behaviour requirement independently. A fact scoped to AC-1
126
+ // must not accidentally satisfy AC-2 merely because both ids share one
127
+ // UX session; shared facts remain valid when they explicitly list both ids.
128
+ for (const requirementId of input.requirementIds) {
129
+ if (!input.behaviorRequiredRequirementIds?.includes(requirementId))
135
130
  continue;
136
- const scopedFacts = byRequirement.get(id) ?? [];
131
+ const scopedFacts = facts.filter((fact) => planFactScopeIntersects(fact, new Set([requirementId])));
137
132
  const hasChoice = scopedFacts.some((fact) => fact.kind === "component-choice" &&
138
133
  Array.isArray(fact.uiComponentChoices) &&
139
134
  fact.uiComponentChoices.length > 0);
140
135
  const canonicalStateFlow = collectCanonicalStateFlowNames(scopedFacts);
141
- const hasStateFlow = [...canonicalStateFlow.uiStateNames].some(name => liveStateFlow.uiStateNames.has(name)) ||
142
- [...canonicalStateFlow.interactionNames].some(name => liveStateFlow.interactionNames.has(name));
136
+ const hasStateFlow = canonicalStateFlow.uiStateNames.size > 0 ||
137
+ canonicalStateFlow.interactionNames.size > 0;
143
138
  if (!hasChoice) {
144
139
  missing.push({
145
140
  kind: "component-choice",
146
- requirementIds: [id],
141
+ requirementIds: [requirementId],
147
142
  reason: "behaviour-required UX slice has no committed component-choice fact",
148
143
  });
149
144
  }
150
145
  if (!hasStateFlow) {
151
146
  missing.push({
152
147
  kind: "state-flow",
153
- requirementIds: [id],
148
+ requirementIds: [requirementId],
154
149
  reason: "behaviour-required UX slice has no committed state-flow fact",
155
150
  });
156
151
  }
@@ -158,9 +153,8 @@ export function collectFrontendPlanPhaseMissingFacts(input) {
158
153
  return missing;
159
154
  }
160
155
  const hasMockApi = facts.some((fact) => fact.kind === "mock-api");
161
- const allInteractions = collectCanonicalStateFlowNames(facts).interactionNames;
162
- const requested = new Set(input.requirementIds);
163
- const liveInteractions = new Set([...collectCanonicalStateFlowNames(facts.filter(f => !planFactStringList(f.scopeRequirementIds).length || planFactScopeIntersects(f, requested))).interactionNames].filter(name => allInteractions.has(name)));
156
+ const allInteractions = collectCanonicalStateFlowNames(input.committedFacts).interactionNames;
157
+ const liveInteractions = new Set([...collectCanonicalStateFlowNames(facts.filter(f => !planFactStringList(f.scopeRequirementIds).length || planFactScopeIntersects(f, new Set(input.requirementIds)))).interactionNames].filter(name => allInteractions.has(name)));
164
158
  const coveredInteractions = new Set(facts
165
159
  .filter((fact) => fact.kind === "data-flow")
166
160
  .flatMap((fact) => planFactStringList(fact.interactions)));
@@ -0,0 +1,259 @@
1
+ import { loadContractRequirementInheritance } from "./ledger-contract.js";
2
+ import { collectFrontendPlanPhaseMissingFacts } from "./facts.js";
3
+ import { planToolReceipt, stringList } from "./tool-helpers.js";
4
+ import { collectCanonicalStateFlowNames } from "../../../workflows/dag/frontend-contract-facts.js";
5
+ import { isRecordObject } from "../guards.js";
6
+ import { randomUUID } from "node:crypto";
7
+ export function buildPlanFinalizeTools(ctx) {
8
+ const { Type, defineTool, session, schemas, input, readCommittedEvents, assemblePlanPatchFromCommittedFacts, contractInheritance, adoptStagedFact, adoptPlanFact } = ctx;
9
+ const { optionalString, optionalStringArray } = ctx.schemas;
10
+ const finalizePlanTool = defineTool({
11
+ name: "finalize_plan",
12
+ label: "finalize_plan",
13
+ description: "Commit the finalize_plan terminal. Requirements, verification targets, and evidence gaps (optional) were already committed incrementally through record_plan_requirement / record_plan_verification_target / record_plan_evidence_gap; finalize_plan assembles them from the ledger together with these optional remaining fields, publishes the canonical editable patch on a target-surface fact, and commits the terminal. A successful terminal commit occurs exactly once. If validation fails, correct only the reported facts and retry finalize.",
14
+ promptSnippet: "Commit the finalize_plan terminal (ledger fields + optional residualRisks / realIntegrationGap).",
15
+ parameters: Type.Object({
16
+ residualRisks: optionalStringArray,
17
+ realIntegrationGap: optionalString,
18
+ }, { additionalProperties: false }),
19
+ async execute(_toolCallId, params) {
20
+ try {
21
+ const committed = readCommittedEvents(session.store, session.attemptId);
22
+ // Source fidelity ledger (AC-005/AC-006): inherit the contract
23
+ // node's declared requirement→fragment provenance so the compiled
24
+ // canonical contract carries sourceFragmentIds/sourceRefs even
25
+ // when the plan did not re-declare them. The contract node is the
26
+ // sole synthesis point; the plan inherits by requirement id.
27
+ const contractInheritance = await loadContractRequirementInheritance(input.runDir);
28
+ const missingData = collectFrontendPlanPhaseMissingFacts({ phase: "global-mock-data", requirementIds: input.requirementIds ?? [...contractInheritance.keys()], committedFacts: committed }).filter(f => f.kind === "data-flow");
29
+ if (missingData.length)
30
+ return planToolReceipt({ ok: false, kind: "finalize_plan", code: "PLAN_DATA_FLOW_INCOMPLETE", error: missingData.map(f => f.reason).join("; ") });
31
+ const fragment = assemblePlanPatchFromCommittedFacts(committed, contractInheritance) ?? {};
32
+ const patch = {
33
+ ...fragment,
34
+ ...(params?.residualRisks
35
+ ? { residualRisks: params.residualRisks }
36
+ : {}),
37
+ ...(params?.realIntegrationGap
38
+ ? { realIntegrationGap: params.realIntegrationGap }
39
+ : {}),
40
+ };
41
+ const latestRegistry = [...committed]
42
+ .reverse()
43
+ .map((record) => record.fact)
44
+ .find((fact) => isRecordObject(fact) && fact.kind === "state-registry");
45
+ if (latestRegistry) {
46
+ const registryUiStates = new Set(stringList(latestRegistry.uiStateNames));
47
+ const registryInteractions = new Set(stringList(latestRegistry.interactionNames));
48
+ const live = collectCanonicalStateFlowNames(committed);
49
+ const missingUiStates = [...registryUiStates].filter((name) => !live.uiStateNames.has(name));
50
+ const missingInteractions = [...registryInteractions].filter((name) => !live.interactionNames.has(name));
51
+ const undeclaredUiStates = [...live.uiStateNames].filter((name) => !registryUiStates.has(name));
52
+ const undeclaredInteractions = [...live.interactionNames].filter((name) => !registryInteractions.has(name));
53
+ if (missingUiStates.length > 0 ||
54
+ missingInteractions.length > 0 ||
55
+ undeclaredUiStates.length > 0 ||
56
+ undeclaredInteractions.length > 0) {
57
+ return planToolReceipt({
58
+ ok: false,
59
+ kind: "finalize_plan",
60
+ error: `finalize_plan UX registry mismatch: every registered name must have one live state-flow entry and every live entry must be registered (missing uiStates: ${missingUiStates.join(", ") || "none"}; missing interactions: ${missingInteractions.join(", ") || "none"}; undeclared uiStates: ${undeclaredUiStates.join(", ") || "none"}; undeclared interactions: ${undeclaredInteractions.join(", ") || "none"})`,
61
+ });
62
+ }
63
+ }
64
+ // Front-load the node's compile + policy gates into the finalize
65
+ // receipt (same pipeline the design-policy shell and the node
66
+ // self-check run: merge the patch onto the runtime skeleton,
67
+ // then the full analyze). A failing gate used to burn an entire
68
+ // attempt per finding (r8/r9: ui-design-coverage, verification
69
+ // targets, UI-state shape, one attempt each); surfaced here the
70
+ // model fixes the facts and re-calls finalize_plan in-node.
71
+ if (input.skeleton && input.sourceBinding) {
72
+ // Front-load the exact pipeline the design-policy shell and
73
+ // the node self-check run (patch ⊕ skeleton -> analyze ->
74
+ // policy pre-checks) into the finalize receipt. Findings
75
+ // come back as fixable receipt errors instead of burning
76
+ // an attempt per gate (r8/r9: coverage, verification
77
+ // targets, UI-state shape each cost a full attempt).
78
+ const { analyzeFrontendPlanPatchCandidate, applyFrontendContractMergePatch, FrontendContractFailure, PlanPolicyPrecheckFailure, serializeDeterministicJson, } = await import("../../../workflows/dag/frontend-implementation-contract.js");
79
+ try {
80
+ const merged = applyFrontendContractMergePatch(input.skeleton, patch);
81
+ await analyzeFrontendPlanPatchCandidate({
82
+ runDir: input.runDir,
83
+ rawContractText: serializeDeterministicJson(merged),
84
+ sourceBinding: input.sourceBinding,
85
+ });
86
+ }
87
+ catch (error) {
88
+ if (error instanceof PlanPolicyPrecheckFailure) {
89
+ // Template the fix: every uncovered interaction / state
90
+ // maps to a record_component_choice skeleton. Reuse is
91
+ // never implied: the operator/model must fill an existing
92
+ // evidencePath or switch the choice to decision=new with
93
+ // the required frozen source citation.
94
+ const suggestions = error.findings
95
+ .filter((finding) => finding.code === "ui-design-coverage-missing" &&
96
+ finding.path)
97
+ .map((finding) => ({
98
+ tool: "record_component_choice",
99
+ args: {
100
+ choice: {
101
+ purpose: finding.path,
102
+ component: "<name the existing or new component>",
103
+ decision: "reuse-existing",
104
+ evidencePath: "<existing repo file that proves this reuse>",
105
+ },
106
+ },
107
+ }));
108
+ const suggestionBlock = suggestions.length > 0
109
+ ? ` Suggested record_* calls (copy, fill component, submit): ${JSON.stringify(suggestions)}`
110
+ : "";
111
+ return planToolReceipt({
112
+ ok: false,
113
+ kind: "finalize_plan",
114
+ error: `finalize_plan pre-validation failed (fix the listed plan facts with record_* tools, then call finalize_plan again): ${error.message}${suggestionBlock}`,
115
+ });
116
+ }
117
+ if (!(error instanceof FrontendContractFailure))
118
+ throw error;
119
+ return planToolReceipt({
120
+ ok: false,
121
+ kind: "finalize_plan",
122
+ error: `finalize_plan pre-validation failed (fix the listed plan facts with record_* tools, then call finalize_plan again): ${error.message}`,
123
+ });
124
+ }
125
+ }
126
+ const patchResult = await adoptPlanFact("target-surface", `${session.attemptId}:finalize_plan:patch:${randomUUID()}`, { kind: "target-surface", origin: "plan", patch });
127
+ if (!patchResult.ok) {
128
+ return planToolReceipt({
129
+ ok: false,
130
+ kind: "finalize_plan",
131
+ error: patchResult.error,
132
+ code: patchResult.code,
133
+ });
134
+ }
135
+ const terminal = await adoptPlanFact("finalize_plan", `${session.attemptId}:finalize_plan:terminal:${randomUUID()}`, { kind: "finalize_plan", origin: "plan", patch });
136
+ return planToolReceipt(terminal);
137
+ }
138
+ catch (error) {
139
+ return planToolReceipt({
140
+ ok: false,
141
+ kind: "finalize_plan",
142
+ error: error instanceof Error ? error.message : String(error),
143
+ });
144
+ }
145
+ },
146
+ });
147
+ const adoptStagedFactTool = defineTool({
148
+ name: "adopt_staged_fact",
149
+ label: "adopt_staged_fact",
150
+ description: "Explicitly adopt a quarantined fact from a prior attempt into the committed ledger (idempotent by requestId).",
151
+ promptSnippet: "Adopt a quarantined fact into the committed ledger.",
152
+ parameters: Type.Object({
153
+ requestId: Type.String({}),
154
+ eventId: Type.String({}),
155
+ expectedRevision: Type.Number({}),
156
+ }, { additionalProperties: false }),
157
+ async execute(_toolCallId, params) {
158
+ try {
159
+ const adopted = await adoptStagedFact({
160
+ store: session.store,
161
+ requestId: typeof params?.requestId === "string" ? params.requestId : "",
162
+ attemptId: session.attemptId,
163
+ eventId: typeof params?.eventId === "string" ? params.eventId : "",
164
+ expectedRevision: typeof params?.expectedRevision === "number"
165
+ ? params.expectedRevision
166
+ : session.store.revision,
167
+ });
168
+ return planToolReceipt({
169
+ ok: true,
170
+ kind: "adopt_staged_fact",
171
+ eventId: adopted.eventId,
172
+ revision: adopted.revision,
173
+ error: "",
174
+ });
175
+ }
176
+ catch (error) {
177
+ return planToolReceipt({
178
+ ok: false,
179
+ kind: "adopt_staged_fact",
180
+ error: error instanceof Error ? error.message : String(error),
181
+ code: error?.code,
182
+ });
183
+ }
184
+ },
185
+ });
186
+ const readPlanFactsTool = defineTool({
187
+ name: "read_plan_facts",
188
+ label: "read_plan_facts",
189
+ description: "Read the LIVE committed plan ledger without repository access. Filter by kind and optional entryId or eventId. Use field (dot path, e.g. entry.requirementIds or uiStates) to page large arrays/strings. offset/limit paginate results; follow nextOffset until null before replacing any full-array fact. expectedRevision pins all pages to one ledger revision; restart if it changes. No writes.",
190
+ parameters: Type.Object({
191
+ kind: Type.String(), entryId: Type.Optional(Type.String()), eventId: Type.Optional(Type.String()),
192
+ field: Type.Optional(Type.String()), offset: Type.Optional(Type.Integer({ minimum: 0 })),
193
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
194
+ stringOffset: Type.Optional(Type.Integer({ minimum: 0 })),
195
+ expectedRevision: Type.Optional(Type.Integer({ minimum: 0 })),
196
+ }, { additionalProperties: false }),
197
+ async execute(_id, params) {
198
+ const reply = (details) => ({ content: [{ type: "text", text: JSON.stringify(details) }], details });
199
+ if (params.expectedRevision !== undefined && params.expectedRevision !== session.store.revision) {
200
+ return reply({ ok: false, revision: session.store.revision, error: "ledger revision changed; restart pagination" });
201
+ }
202
+ let records = readCommittedEvents(session.store, session.attemptId).filter(record => {
203
+ const fact = record.fact;
204
+ return fact.kind === params.kind && (!params.eventId || record.eventId === params.eventId) &&
205
+ (!params.entryId || fact.entry?.id === params.entryId);
206
+ });
207
+ // Full-entry corrections and registries are last-wins. Other kinds
208
+ // remain an ordered event log (state removals must remain visible).
209
+ if (params.entryId || params.kind === "state-registry")
210
+ records = records.slice(-1);
211
+ let values = records.map(record => ({ eventId: record.eventId, ...record.fact }));
212
+ if (params.field) {
213
+ values = records.flatMap(record => {
214
+ let value = record.fact;
215
+ for (const key of params.field.split(".")) {
216
+ if (!value || typeof value !== "object" || !Object.hasOwn(value, key))
217
+ return [];
218
+ value = value[key];
219
+ }
220
+ return Array.isArray(value) ? value : [value];
221
+ });
222
+ }
223
+ const offset = params.offset ?? 0;
224
+ if (params.stringOffset !== undefined) {
225
+ const value = values[offset];
226
+ if (typeof value !== "string")
227
+ return reply({ ok: false, error: "stringOffset requires a string field item" });
228
+ // Unicode code points prevent a page boundary splitting a surrogate pair.
229
+ const characters = Array.from(value);
230
+ const chunk = characters.slice(params.stringOffset, params.stringOffset + 1000).join("");
231
+ const next = params.stringOffset + Array.from(chunk).length;
232
+ return reply({ ok: true, revision: session.store.revision, offset, stringOffset: params.stringOffset,
233
+ chunk, totalCharacters: characters.length, nextStringOffset: next < characters.length ? next : null });
234
+ }
235
+ const items = [];
236
+ let bytes = 0;
237
+ for (const value of values.slice(offset, offset + (params.limit ?? 10))) {
238
+ const size = Buffer.byteLength(JSON.stringify(value));
239
+ if (bytes + size > 12_000)
240
+ break;
241
+ bytes += size;
242
+ items.push(value);
243
+ }
244
+ if (!items.length && offset < values.length)
245
+ return reply({
246
+ ok: false, revision: session.store.revision, total: values.length, offset,
247
+ error: "item exceeds the page budget; select an eventId/entryId and a narrower field path; for a string item set stringOffset: 0 and follow nextStringOffset",
248
+ fields: typeof values[offset] === "object" && values[offset] !== null ? Object.keys(values[offset]) : [],
249
+ });
250
+ return reply({ ok: true, revision: session.store.revision, total: values.length, offset, items,
251
+ nextOffset: offset + items.length < values.length ? offset + items.length : null });
252
+ },
253
+ });
254
+ return {
255
+ finalizePlanTool,
256
+ adoptStagedFactTool,
257
+ readPlanFactsTool,
258
+ };
259
+ }