@tea-agent/loop-agent 0.43.0-next.1 → 0.43.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 (207) hide show
  1. package/CHANGELOG.md +144 -9
  2. package/dist/application/dag/run-dag.js +5 -0
  3. package/dist/application/evaluation/budget.js +19 -1
  4. package/dist/application/task-lifecycle/advance.js +17 -4
  5. package/dist/application/task-lifecycle/observe.js +43 -29
  6. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/executors/dag-pi-executor.js +1237 -586
  9. package/dist/executors/pi-executor.js +22 -1
  10. package/dist/executors/pi-sdk-executor.js +140 -39
  11. package/dist/executors/shell-executor.js +8 -4
  12. package/dist/shared/frontend-execution-policy.js +23 -0
  13. package/dist/task/config-types.js +4 -0
  14. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  15. package/dist/task/source-prepare/ledger-review.js +6 -9
  16. package/dist/task/source-prepare/semantic-intake.js +16 -26
  17. package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
  18. package/dist/task/source-references.js +48 -23
  19. package/dist/worker/console/chat/assistant-content.js +23 -2
  20. package/dist/worker/console/chat/browser-policy.js +113 -0
  21. package/dist/worker/console/chat/browser-routes.js +140 -0
  22. package/dist/worker/console/chat/explore-tools.js +13 -0
  23. package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
  24. package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
  25. package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
  26. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +86 -0
  27. package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
  28. package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
  29. package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
  30. package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
  31. package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
  32. package/dist/worker/console/chat/pi-runtime.js +207 -1178
  33. package/dist/worker/console/chat/routes.js +208 -71
  34. package/dist/worker/console/chat/scm-routes.js +217 -0
  35. package/dist/worker/console/chat/scm-service.js +283 -0
  36. package/dist/worker/console/chat/scm-tools.js +111 -0
  37. package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
  38. package/dist/worker/console/chat/session-catalog.js +32 -0
  39. package/dist/worker/console/chat/session-mode-view.js +49 -0
  40. package/dist/worker/console/chat/session-mode.js +219 -0
  41. package/dist/worker/console/chat/session-store.js +22 -6
  42. package/dist/worker/console/chat/shortcuts.js +6 -0
  43. package/dist/worker/console/chat/terminal-routes.js +216 -0
  44. package/dist/worker/console/chat/terminal-sessions.js +348 -0
  45. package/dist/worker/console/chat/terminal-tools.js +211 -0
  46. package/dist/worker/console/chat/tool-preview.js +11 -0
  47. package/dist/worker/console/chat/tools.js +2 -0
  48. package/dist/worker/console/server.js +2 -28
  49. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-CTSK0xbU.js} +1 -1
  50. package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_2MfctFP.js} +1 -1
  51. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-DsDe2wTg.js} +1 -1
  52. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-Dtz_779U.js} +1 -1
  53. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-LaULi-gw.js} +1 -1
  54. package/dist/worker/console/static/assets/channel-CqS4yBWT.js +1 -0
  55. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-DuHBqqX8.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-BZgZs1dn.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-tzwmlIO_.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CeYrLkSn.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-C92cgilm.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-XOXpGC0F.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CGHbJyCH.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-Bs2-LTzf.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-rN7up7E4.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-B_fWjKpR.js} +1 -1
  65. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVOxNOq0.js +1 -0
  66. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVOxNOq0.js +1 -0
  67. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-DE3SdJvB.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-B-4yuRAX.js} +1 -1
  69. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-TiCPFuO8.js} +1 -1
  70. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-7crzLMzA.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-Cvr-x6Fd.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BzcRHUjH.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-BjkAXilh.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-BkzyATK7.js} +1 -1
  75. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-rZuTarYP.js} +1 -1
  76. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-CAYENaN1.js} +1 -1
  77. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-DCEam0Uq.js} +1 -1
  78. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-e_dH4wVO.js} +1 -1
  79. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-CvZa2zOK.js} +1 -1
  80. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-CbT5TuKp.js} +1 -1
  81. package/dist/worker/console/static/assets/index-CUPeoE8W.js +468 -0
  82. package/dist/worker/console/static/assets/index-SAANIK8g.css +1 -0
  83. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-BWzHQa_Z.js} +1 -1
  84. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-BTjlovtU.js} +1 -1
  85. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DkvQoCc1.js} +1 -1
  86. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-CHSzGi3W.js} +1 -1
  87. package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DZMkTCWH.js} +1 -1
  88. package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-BIMhty7A.js} +5 -5
  89. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-Dx-tyMl3.js} +1 -1
  90. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-C6G34rbY.js} +1 -1
  91. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-CHK9XOfm.js} +1 -1
  92. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-CDARkI0w.js} +1 -1
  93. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-DiuUe0ZQ.js} +1 -1
  94. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-RE61UR56.js} +1 -1
  95. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-KDNLHMEG.js} +1 -1
  96. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-CeVgXnD9.js} +1 -1
  97. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-CXwGyjjG.js} +1 -1
  98. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BjTfozab.js} +1 -1
  99. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BXoYsyr2.js +1 -0
  100. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-BaXOB8GT.js} +2 -2
  101. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DfKliS4e.js +8 -0
  102. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-C3r2hqUU.js} +1 -1
  103. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-MWKgl1S9.js} +1 -1
  104. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-C9iFcZPk.js} +1 -1
  105. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-DqnM24Jp.js} +1 -1
  106. package/dist/worker/console/static/index.html +2 -2
  107. package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +10 -0
  108. package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
  109. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
  110. package/dist/worker/console/static-src/operator-chat/compaction-message.js +7 -0
  111. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
  112. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  113. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  114. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +10 -0
  115. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  116. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +44 -24
  117. package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
  118. package/dist/worker/console/static-src/operator-chat/useChatThread.js +41 -8
  119. package/dist/worker/console/static-src/operator-chat/useComposer.js +26 -11
  120. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  121. package/dist/worker/console/workspace-context.js +15 -1
  122. package/dist/worker/observe/node-transparency.js +81 -72
  123. package/dist/worker/observe/routes.js +20 -1
  124. package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
  125. package/dist/worker/observe/static/dom.js +20 -1
  126. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  127. package/dist/worker/observe/static/format-pool.js +6 -0
  128. package/dist/worker/observe/static/format.js +7 -0
  129. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  130. package/dist/worker/observe/static/inspector-submission.js +32 -0
  131. package/dist/worker/observe/static/kpi.js +1 -0
  132. package/dist/worker/observe/static/relations.js +2 -0
  133. package/dist/worker/observe/static/router.js +13 -0
  134. package/dist/worker/observe/static/run-processing.js +2 -0
  135. package/dist/worker/observe/static/shell-chrome.js +36 -3
  136. package/dist/worker/observe/static/state.js +35 -2
  137. package/dist/worker/observe/static/styles.css +263 -39
  138. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  139. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  140. package/dist/worker/observe/static/task-history.js +12 -0
  141. package/dist/worker/observe/static/views/batch.js +6 -13
  142. package/dist/worker/observe/static/views/dag-graph.js +50 -3
  143. package/dist/worker/observe/static/views/dag-inspector.js +742 -268
  144. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  145. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  146. package/dist/worker/observe/static/views/dag.js +49 -11
  147. package/dist/worker/observe/static/views/dags.js +2 -0
  148. package/dist/worker/observe/static/views/dashboard.js +21 -12
  149. package/dist/worker/observe/static/views/failures.js +21 -11
  150. package/dist/worker/observe/static/views/feature.js +11 -29
  151. package/dist/worker/observe/static/views/pool.js +37 -28
  152. package/dist/worker/observe/static/views/run.js +48 -5
  153. package/dist/worker/observe/static/views/session-timeline.js +189 -240
  154. package/dist/worker/observe/static/views/task.js +81 -62
  155. package/dist/workflows/dag/budget-enforcement.js +53 -3
  156. package/dist/workflows/dag/frontend-capacity.js +9 -0
  157. package/dist/workflows/dag/frontend-closeout.js +1 -1
  158. package/dist/workflows/dag/frontend-design-policy.js +2 -1
  159. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  160. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  161. package/dist/workflows/dag/frontend-implementation-contract.js +13 -0
  162. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  163. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  164. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  165. package/dist/workflows/dag/frontend-plan-render.js +10 -3
  166. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  167. package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
  168. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  169. package/dist/workflows/dag/frontend-recovery-run.js +4 -0
  170. package/dist/workflows/dag/frontend-review-context.js +2 -1
  171. package/dist/workflows/dag/frontend-review-scopes.js +139 -0
  172. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  173. package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
  174. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  175. package/dist/workflows/dag/frontend-shape.js +30 -1
  176. package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
  177. package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
  178. package/dist/workflows/dag/frontend-verification-trace.js +1 -1
  179. package/dist/workflows/dag/init-hybrid.js +65 -167
  180. package/dist/workflows/dag/node-execution.js +38 -155
  181. package/dist/workflows/dag/prompt.js +4 -0
  182. package/dist/workflows/dag/rerun-plan.js +7 -1
  183. package/dist/workflows/dag/runner.js +26 -1
  184. package/dist/workflows/dag/types.js +6 -0
  185. package/docs/operations/README.md +1 -0
  186. package/docs/templates/frontend-design-contract.md +4 -4
  187. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  188. package/docs/templates/frontend-implementation-dag.json +14 -13
  189. package/package.json +6 -3
  190. package/skills/frontend-bounded-implement/SKILL.md +3 -3
  191. package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
  192. package/skills/frontend-contract/SKILL.md +2 -1
  193. package/skills/frontend-contract/references/contract-protocol.md +22 -4
  194. package/skills/frontend-design-review/SKILL.md +22 -16
  195. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  196. package/skills/frontend-plan/SKILL.md +21 -2
  197. package/skills/frontend-plan/references/decision-contract.md +42 -23
  198. package/skills/frontend-review/SKILL.md +22 -29
  199. package/skills/frontend-review/references/review-findings.md +24 -43
  200. package/skills/frontend-scout/references/scout-evidence.md +4 -0
  201. package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
  202. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
  203. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
  204. package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
  205. package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
  206. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
  207. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
@@ -1,3 +1,5 @@
1
+ import { inspectRouteStates } from "../state.js";
2
+ import { mergeTaskHistoryPage } from "../task-history.js";
1
3
  /** Observe UI view module (R5). */
2
4
  import {
3
5
  UI_TEXT,
@@ -16,7 +18,7 @@ import {
16
18
  DAG_EFFECTIVE_STATUS_LABELS,
17
19
  STATUS_LABELS,
18
20
  } from "../constants.js";
19
- import { clearNode, el, metaGrid, buildTable } from "../dom.js";
21
+ import { clearNode, el, metaGrid, buildTable, reconcileKeyed } from "../dom.js";
20
22
  import {
21
23
  badge,
22
24
  badgeClass,
@@ -45,6 +47,7 @@ import {
45
47
  import {
46
48
  fetchJson,
47
49
  fetchJsonResult,
50
+ summarizeFetchError,
48
51
  artifactUrl,
49
52
  parseArtifactPreviewResponse,
50
53
  } from "../api.js";
@@ -66,7 +69,7 @@ import {
66
69
  renderViewState,
67
70
  mountViewState,
68
71
  } from "../state.js";
69
- import { uiState } from "../state.js";
72
+ import { uiState, taskHistoryStates, workspaceScope } from "../state.js";
70
73
  import { sortPoolTasks } from "../format-pool.js";
71
74
  import {
72
75
  dagMetaVisibility,
@@ -75,7 +78,7 @@ import {
75
78
  findDagForRun,
76
79
  } from "../dag-model.js";
77
80
  import { layoutDag } from "../dag-layout.js";
78
- import { showView, setBreadcrumb, updateHeaderRefresh, mountRelationBar } from "../shell-chrome.js";
81
+ import { showView, setBreadcrumb, updateHeaderRefresh, mountRelationBar, bindManualViewRefresh } from "../shell-chrome.js";
79
82
  import { artifactLink } from "../kpi.js";
80
83
  import { findBatchRunIdForTask, taskRunHistoryRow } from "../run-processing.js";
81
84
 
@@ -91,7 +94,7 @@ export async function renderTaskDetail(featureId, taskId) {
91
94
  showView("task");
92
95
  setBreadcrumb([
93
96
  { label: UI_TEXT.dashboard, href: "#/" },
94
- { label: "资源池", href: "/#/inspect/pool" },
97
+ { label: "资源池", href: inspectRouteStates.get("return-list") ?? "/#/inspect/pool" },
95
98
  { label: displayLabel },
96
99
  ]);
97
100
 
@@ -103,13 +106,29 @@ export async function renderTaskDetail(featureId, taskId) {
103
106
  const ledgerEl = document.getElementById("task-ledger");
104
107
  if (!metaEl || !linksEl || !evidenceEl || !runsEl) return false;
105
108
  if (titleEl) titleEl.textContent = displayLabel;
106
- mountViewState(metaEl, "loading", "加载 Task…");
109
+ const identity = `${workspaceScope() ?? ""}:${featureId ?? ""}:${taskId}`;
110
+ const requestGeneration = uiState.pollingGeneration;
111
+ if (metaEl.dataset.taskIdentity !== identity) {
112
+ metaEl.dataset.taskIdentity = identity;
113
+ mountViewState(metaEl, "loading", "加载 Task…");
114
+ clearNode(runsEl);
115
+ for (const element of [linksEl, evidenceEl, ledgerEl]) if (element) clearNode(element);
116
+ }
117
+ if (!taskHistoryStates.has(identity)) taskHistoryStates.set(identity, { runs: [], nextBefore: null, loadedOlder: false, loadingOlder: false });
118
+ const historyState = taskHistoryStates.get(identity);
107
119
 
108
120
  const [taskResult, historyResult, snapshot] = await Promise.all([
109
121
  fetchJsonResult(taskApi),
110
122
  fetchJsonResult(`${taskApi}/runs?limit=20`),
111
123
  fetchJson("/api/snapshot"),
112
124
  ]);
125
+ if (requestGeneration !== uiState.pollingGeneration || metaEl.dataset.taskIdentity !== identity) return false;
126
+ if (!taskResult.ok && ![404, 409].includes(taskResult.status)) {
127
+ let error = metaEl.querySelector(".task-refresh-error");
128
+ if (!error) { error = el("p", "task-refresh-error node-input-error"); metaEl.appendChild(error); }
129
+ error.textContent = `更新失败,保留上次内容。${summarizeFetchError(taskResult)}`;
130
+ return true;
131
+ }
113
132
  const task = taskResult.ok ? taskResult.body : null;
114
133
  const history = historyResult.ok ? historyResult.body : null;
115
134
  if (snapshot) {
@@ -120,7 +139,6 @@ export async function renderTaskDetail(featureId, taskId) {
120
139
  clearNode(metaEl);
121
140
  clearNode(linksEl);
122
141
  clearNode(evidenceEl);
123
- clearNode(runsEl);
124
142
 
125
143
  if (!task) {
126
144
  if (taskResult.status === 409) {
@@ -234,27 +252,9 @@ export async function renderTaskDetail(featureId, taskId) {
234
252
  evidenceEl.appendChild(evidenceLinks);
235
253
  }
236
254
 
237
- runsEl.appendChild(el("h3", "section-heading", "运行历史"));
238
- const runs = history?.runs ?? [];
239
- if (runs.length === 0) {
240
- mountViewState(runsEl, "empty", "暂无 ledger 运行记录。");
241
- } else {
242
- const rows = runs.map(taskRunHistoryRow);
243
- runsEl.appendChild(
244
- buildTable(
245
- ["worker run", "状态", "时间", "batch", "retryOf", "失败分类"],
246
- rows,
247
- ),
248
- );
249
- if (history?.nextBefore) {
250
- const more = el("button", "link-button", "加载更早的运行");
251
- more.type = "button";
252
- more.addEventListener("click", () => {
253
- void loadOlderTaskRuns(featureId, taskId, history.nextBefore, runsEl);
254
- });
255
- runsEl.appendChild(more);
256
- }
257
- }
255
+ if (history) mergeTaskHistoryPage(historyState, history);
256
+ else historyState.error = summarizeFetchError(historyResult);
257
+ renderTaskHistory(runsEl, historyState, featureId, taskId, identity);
258
258
 
259
259
  const active = ["running", "pending", "blocked", "stale"].includes(
260
260
  (task.status ?? "").toLowerCase(),
@@ -262,6 +262,8 @@ export async function renderTaskDetail(featureId, taskId) {
262
262
  if (ledgerEl) {
263
263
  void renderTaskLedger(ledgerEl, taskId);
264
264
  }
265
+ updateHeaderRefresh(snapshot?.generatedAt ?? uiState.lastSnapshot?.generatedAt, { mode: active ? "auto" : "ended" });
266
+ bindManualViewRefresh("task", () => renderTaskDetail(featureId, taskId));
265
267
  return active;
266
268
  }
267
269
 
@@ -315,42 +317,59 @@ async function renderTaskLedger(ledgerEl, taskId) {
315
317
  ledgerEl.appendChild(pre);
316
318
  }
317
319
 
318
- export async function loadOlderTaskRuns(featureId, taskId, before, runsEl) {
319
- if (runsEl === undefined) {
320
- runsEl = before;
321
- before = taskId;
322
- taskId = featureId;
323
- featureId = undefined;
320
+ function renderTaskHistory(runsEl, state, featureId, taskId, identity) {
321
+ if (runsEl.dataset.historyIdentity !== identity) { clearNode(runsEl); runsEl.dataset.historyIdentity = identity; }
322
+ let table = runsEl.querySelector("table");
323
+ if (!table) {
324
+ runsEl.appendChild(el("h3", "section-heading", "运行历史"));
325
+ table = buildTable(["worker run", "状态", "时间", "batch", "retryOf", "失败分类"], []);
326
+ runsEl.appendChild(table);
327
+ const status = el("p", "muted task-history-status");
328
+ status.setAttribute("role", "status");
329
+ runsEl.appendChild(status);
330
+ const more = el("button", "link-button", "加载更早的运行");
331
+ more.type = "button";
332
+ more.addEventListener("click", () => { void loadOlderTaskRuns(featureId, taskId, state.nextBefore, runsEl); });
333
+ runsEl.appendChild(more);
324
334
  }
325
- const taskApi = featureId
326
- ? `/api/features/${encodeURIComponent(featureId)}/tasks/${encodeURIComponent(taskId)}`
327
- : `/api/tasks/${encodeURIComponent(taskId)}`;
328
- const page = await fetchJson(
329
- `${taskApi}/runs?limit=20&before=${encodeURIComponent(before)}`,
330
- );
331
- if (!page?.runs?.length) return;
332
- const table = runsEl.querySelector("table");
333
- if (!table) return;
334
- const tbody = table.querySelector("tbody") || table;
335
- for (const run of page.runs) {
336
- const tr = document.createElement("tr");
337
- for (const cell of taskRunHistoryRow(run).cells) {
338
- const td = document.createElement("td");
339
- if (cell instanceof Node) td.appendChild(cell);
340
- else td.textContent = cell;
341
- tr.appendChild(td);
342
- }
343
- tbody.appendChild(tr);
344
- }
345
- const btn = runsEl.querySelector("button.link-button");
346
- if (btn) {
347
- if (page.nextBefore) {
348
- btn.onclick = () => {
349
- void loadOlderTaskRuns(featureId, taskId, page.nextBefore, runsEl);
350
- };
351
- } else {
352
- btn.remove();
353
- }
335
+ reconcileKeyed(table.querySelector("tbody"), state.runs, {
336
+ getKey: (run) => run.workerRunId,
337
+ create: () => el("tr"),
338
+ update: (row, run) => {
339
+ const signature = JSON.stringify(run);
340
+ if (row.dataset.signature === signature) return;
341
+ clearNode(row);
342
+ for (const cell of taskRunHistoryRow(run).cells) {
343
+ const td = el("td");
344
+ if (cell instanceof Node) td.appendChild(cell); else td.textContent = cell ?? "—";
345
+ row.appendChild(td);
346
+ }
347
+ row.dataset.signature = signature;
348
+ },
349
+ });
350
+ const status = runsEl.querySelector(".task-history-status");
351
+ status.textContent = state.error ? `历史加载失败:${state.error}` : state.runs.length ? `已加载 ${state.runs.length} 次运行` : "暂无 ledger 运行记录。";
352
+ const more = runsEl.querySelector("button.link-button");
353
+ more.hidden = !state.nextBefore && !state.error;
354
+ more.disabled = state.loadingOlder;
355
+ more.textContent = state.loadingOlder ? "正在加载…" : state.error ? "重试加载" : "加载更早的运行";
356
+ }
357
+
358
+ export async function loadOlderTaskRuns(featureId, taskId, before, runsEl) {
359
+ const identity = `${workspaceScope() ?? ""}:${featureId ?? ""}:${taskId}`;
360
+ const state = taskHistoryStates.get(identity);
361
+ if (!state || state.loadingOlder) return;
362
+ state.loadingOlder = true;
363
+ renderTaskHistory(runsEl, state, featureId, taskId, identity);
364
+ const taskApi = featureId ? `/api/features/${encodeURIComponent(featureId)}/tasks/${encodeURIComponent(taskId)}` : `/api/tasks/${encodeURIComponent(taskId)}`;
365
+ try {
366
+ const result = await fetchJsonResult(`${taskApi}/runs?limit=20${state.nextBefore ? `&before=${encodeURIComponent(state.nextBefore)}` : ""}`);
367
+ if (taskHistoryStates.get(identity) !== state) return;
368
+ if (result.ok && result.body) mergeTaskHistoryPage(state, result.body, Boolean(state.nextBefore));
369
+ else state.error = summarizeFetchError(result);
370
+ } finally {
371
+ state.loadingOlder = false;
372
+ if (runsEl.dataset.historyIdentity === identity && taskHistoryStates.get(identity) === state) renderTaskHistory(runsEl, state, featureId, taskId, identity);
354
373
  }
355
374
  }
356
375
 
@@ -1,6 +1,56 @@
1
1
  import { writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { checkBudgetPreNode, createBudgetLedger, formatBudgetReportMarkdown, isHardBudgetBreached, recordNodeBudgetSample, skipPendingNodesForBudgetBreach, } from "../../application/evaluation/budget.js";
3
+ import { checkBudgetPreNode, createBudgetLedger, formatBudgetReportMarkdown, isHardBudgetBreached, recordNodeBudgetSample, reserveProviderBudget, skipPendingNodesForBudgetBreach, } from "../../application/evaluation/budget.js";
4
+ import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
5
+ /** A continuation already contains its ancestors' consumption. Never sum both. */
6
+ export function validateCumulativeBudgetSuccessor(parent, child) {
7
+ const lineage = child.continuation?.budgetLineage ?? child.frontendRecoveryState?.budgetLineage;
8
+ const previous = parent.budgetLedger;
9
+ const next = child.budgetLedger;
10
+ if (!previous || !next || !lineage || (child.continuation?.parentRunId ?? child.frontendRecoveryState?.parentRunId) !== parent.runId || lineage.parentLedgerSha256 !== sha256OfCanonicalJson(previous) || sha256OfCanonicalJson(lineage.inheritedConsumed) !== sha256OfCanonicalJson(previous.consumed) || sha256OfCanonicalJson(parent.budget ?? null) !== sha256OfCanonicalJson(child.budget ?? null))
11
+ throw Error("FRONTEND_PROVIDER_BUDGET_LINEAGE_INVALID: unbound cumulative continuation");
12
+ for (const key of ["executorCalls", "providerRequests", "wallTimeMs", "repairPasses", "peakContextChars"])
13
+ if ((next.consumed[key] ?? 0) < (previous.consumed[key] ?? 0))
14
+ throw Error("FRONTEND_PROVIDER_BUDGET_LINEAGE_INVALID: consumption decreased");
15
+ return next;
16
+ }
17
+ const requestQueues = new WeakMap();
18
+ /** One shared, attempt-fenced, durable admission queue for every request in a run. */
19
+ export async function reserveDagProviderRequest(input) {
20
+ const ledger = input.state.budgetLedger;
21
+ if (!ledger || ledger.limits.maxProviderRequests === undefined)
22
+ throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: frozen request budget unavailable");
23
+ const previous = requestQueues.get(ledger) ?? Promise.resolve();
24
+ const current = previous.catch(() => { }).then(async () => {
25
+ const active = () => input.state.status === "running" && input.state.nodes[input.nodeId]?.status === "RUNNING" && input.state.nodes[input.nodeId]?.currentAttempt === input.attempt;
26
+ if (!active())
27
+ throw Error("FRONTEND_PROVIDER_BUDGET_STALE: request belongs to an inactive attempt");
28
+ if (isHardBudgetBreached(ledger))
29
+ throw Error("FRONTEND_PROVIDER_BUDGET_EXHAUSTED: run budget already breached");
30
+ const breach = reserveProviderBudget(ledger, input.nodeId);
31
+ if (breach)
32
+ applyHardBudgetBreach(input.state, breach);
33
+ try {
34
+ await input.persist();
35
+ }
36
+ catch (error) {
37
+ ledger.status = "breached";
38
+ throw Error(`FRONTEND_PROVIDER_BUDGET_PERSIST_FAILED: ${error instanceof Error ? error.message : String(error)}`);
39
+ }
40
+ if (breach)
41
+ throw Error("FRONTEND_PROVIDER_BUDGET_EXHAUSTED: request quota reached");
42
+ if (!active())
43
+ throw Error("FRONTEND_PROVIDER_BUDGET_STALE: attempt stopped while reserving request");
44
+ });
45
+ requestQueues.set(ledger, current);
46
+ try {
47
+ await current;
48
+ }
49
+ finally {
50
+ if (requestQueues.get(ledger) === current)
51
+ requestQueues.delete(ledger);
52
+ }
53
+ }
4
54
  export function initRunBudgetLedger(state, budget) {
5
55
  if (!budget)
6
56
  return;
@@ -8,7 +58,7 @@ export function initRunBudgetLedger(state, budget) {
8
58
  state.budgetLedger = createBudgetLedger(budget);
9
59
  }
10
60
  function wallTimeMs(state, now = Date.now()) {
11
- const lineage = state.continuation?.budgetLineage;
61
+ const lineage = state.continuation?.budgetLineage ?? state.frontendRecoveryState?.budgetLineage;
12
62
  if (lineage) {
13
63
  return (lineage.inheritedConsumed.wallTimeMs +
14
64
  Math.max(0, now - new Date(lineage.inheritedAt).getTime()));
@@ -16,7 +66,7 @@ function wallTimeMs(state, now = Date.now()) {
16
66
  return Math.max(0, now - new Date(state.startedAt).getTime());
17
67
  }
18
68
  function repairPasses(state) {
19
- const lineage = state.continuation?.budgetLineage;
69
+ const lineage = state.continuation?.budgetLineage ?? state.frontendRecoveryState?.budgetLineage;
20
70
  if (lineage) {
21
71
  return (lineage.inheritedConsumed.repairPasses +
22
72
  Math.max(0, (state.convergence?.currentPass ?? 1) - 1));
@@ -0,0 +1,9 @@
1
+ /** Shared Contract/Plan bound. Permissions and directory globs are not files. */
2
+ export const FRONTEND_MAX_IMPLEMENTATION_FILES = 15;
3
+ export function frontendConcreteDeliveryFiles(files) {
4
+ if (!Array.isArray(files))
5
+ return [];
6
+ return [...new Set(files.filter((file) => typeof file === "string")
7
+ .map(file => file.trim().replace(/\\/g, "/").replace(/^(\.\/)+/, ""))
8
+ .filter(file => file.length > 0 && !/[*?{}]/.test(file) && !file.endsWith("/")))];
9
+ }
@@ -114,7 +114,7 @@ export function buildFrontendTestHandoff(input) {
114
114
  * required verification evidence. Anything else downgrades to pending.
115
115
  */
116
116
  export function deriveFrontendIntegrationFacts(input) {
117
- const requiredEvidenceSatisfied = input.verificationTargets.length > 0 &&
117
+ const requiredEvidenceSatisfied = input.verificationTargets.some((target) => target.mode === "behavior") &&
118
118
  input.verificationTargets.every((target) => target.status === "ok");
119
119
  if (input.realApiEvidence.present &&
120
120
  input.realApiEvidence.passed &&
@@ -1,11 +1,12 @@
1
1
  import { deterministicSha256, } from "./frontend-implementation-contract.js";
2
2
  import { pathMatchesPattern } from "../../shared/git-progress.js";
3
+ import { FRONTEND_MAX_IMPLEMENTATION_FILES } from "./frontend-capacity.js";
3
4
  const SHA256_HEX = /^[a-f0-9]{64}$/;
4
5
  const NPM_PACKAGE_NAME = /^(?:@[a-zA-Z0-9_.-]+\/)?[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
5
6
  /** Max concrete implementation files a single writeSet may carry before the
6
7
  * plan is asked to split. Kept at 15: beyond this, a single implement node's
7
8
  * read/edit loop inflates provider context toward overflow. */
8
- const MAX_WRITE_SET_FILES = 15;
9
+ const MAX_WRITE_SET_FILES = FRONTEND_MAX_IMPLEMENTATION_FILES;
9
10
  function normalizeRelativePath(value) {
10
11
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
11
12
  }
@@ -0,0 +1,193 @@
1
+ import { mkdir, open, rename, rm } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import path from "node:path";
4
+ import { Check, Errors } from "typebox/value";
5
+ import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
6
+ import { assertHarnessWriteAllowed } from "../../infrastructure/harness/completed-facts-guard.js";
7
+ import { readTypedEventStoreFromJsonl, typedEventPayloadSha256, hasCommittedTerminalEvent } from "./frontend-typed-event-store.js";
8
+ const queues = new Map();
9
+ const progressByTool = new WeakMap();
10
+ /** Process-local telemetry only; durable facts remain the authority. */
11
+ export function readFrontendToolProgress(tools) {
12
+ const counters = [...new Set((tools ?? []).flatMap(tool => tool && typeof tool === "object" && progressByTool.has(tool) ? [progressByTool.get(tool)] : []))];
13
+ if (!counters.length)
14
+ return undefined;
15
+ return counters.reduce((sum, item) => ({ failedRecords: sum.failedRecords + item.failedRecords, duplicateRecords: sum.duplicateRecords + item.duplicateRecords, durableSubmissions: sum.durableSubmissions + item.durableSubmissions, durableReplacements: sum.durableReplacements + item.durableReplacements }), { failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 });
16
+ }
17
+ async function exclusive(key, work) {
18
+ const previous = queues.get(key) ?? Promise.resolve();
19
+ const current = previous.catch(() => { }).then(work);
20
+ queues.set(key, current);
21
+ try {
22
+ return await current;
23
+ }
24
+ finally {
25
+ if (queues.get(key) === current)
26
+ queues.delete(key);
27
+ }
28
+ }
29
+ const hash = (value) => sha256OfCanonicalJson(JSON.parse(JSON.stringify(value, (_key, v) => v instanceof Map ? [...v.entries()].sort(([a], [b]) => String(a).localeCompare(String(b))) : v)));
30
+ const ledgerError = (code, message) => Object.assign(new Error(message), { code });
31
+ const failure = (code, error) => ({ content: [{ type: "text", text: JSON.stringify({ ok: false, code, error }) }], details: { ok: false, code, error } });
32
+ /** A failed rename must never fall back to copying over a live ledger. */
33
+ async function persist(file, records) {
34
+ assertHarnessWriteAllowed(file);
35
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
36
+ const temporary = `${file}.${randomUUID()}.tmp`;
37
+ try {
38
+ const handle = await open(temporary, "wx", 0o600);
39
+ try {
40
+ await handle.writeFile(records.map(r => JSON.stringify(r)).join("\n") + (records.length ? "\n" : ""));
41
+ await handle.sync();
42
+ }
43
+ finally {
44
+ await handle.close();
45
+ }
46
+ await rename(temporary, file);
47
+ }
48
+ finally {
49
+ await rm(temporary, { force: true }).catch(() => { });
50
+ }
51
+ }
52
+ /** One queue covers schema checks, semantic checks, all facts in a tool call,
53
+ * atomic persistence, and acknowledgement. The model never sees a draft as saved. */
54
+ export async function createDurableFrontendTools(input) {
55
+ const file = path.resolve(input.file);
56
+ const tools = input.tools.map(tool => {
57
+ const schema = tool.parameters;
58
+ return { ...tool, parameters: { ...schema, properties: { ...schema.properties, replace: { type: "boolean", description: "Explicitly revise the current fact with the same identity. Omit for new facts or identical replay." } } } };
59
+ });
60
+ const bindingSha256 = hash({ version: 1, attemptId: input.attemptId, binding: input.binding, schemas: tools.map(t => ({ name: t.name, parameters: t.parameters })) });
61
+ const publish = (store) => { input.store.records = store.records; input.store.revision = store.revision; input.setWorkingStore(input.store); };
62
+ const validate = async (records) => {
63
+ let revision = 0;
64
+ let terminal = false;
65
+ const ids = new Set();
66
+ const requests = new Set();
67
+ for (const r of records) {
68
+ if (r.attemptId !== input.attemptId || r.submission?.bindingSha256 !== bindingSha256)
69
+ throw ledgerError("FRONTEND_LEDGER_BINDING_MISMATCH", "frontend ledger binding/scope mismatch; preserve evidence and restart its owning phase");
70
+ if (r.phase !== "committed" || r.payloadSha256 !== typedEventPayloadSha256(r.fact) || r.revision <= revision || ids.has(r.eventId) || requests.has(r.requestId) || terminal)
71
+ throw ledgerError("FRONTEND_LEDGER_INTEGRITY_INVALID", "frontend ledger integrity/revision mismatch");
72
+ ids.add(r.eventId);
73
+ requests.add(r.requestId);
74
+ revision = r.revision;
75
+ terminal = hasCommittedTerminalEvent({ records: [r], revision }, input.attemptId);
76
+ }
77
+ await input.validateRestored?.(records);
78
+ };
79
+ const restore = async () => {
80
+ const records = await readTypedEventStoreFromJsonl(file);
81
+ if (records.length) {
82
+ await validate(records);
83
+ publish({ records, revision: records.at(-1).revision });
84
+ }
85
+ else if (input.store.records.some(r => r.submission))
86
+ throw ledgerError("FRONTEND_LEDGER_MISSING", "frontend durable ledger disappeared or became empty; refusing an unproven acknowledgement");
87
+ };
88
+ await exclusive(file, restore);
89
+ const progress = { failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 };
90
+ const replay = (receipt) => { progress.duplicateRecords++; return receipt; };
91
+ const wrapped = tools.map(tool => ({ ...tool, execute: (...args) => exclusive(file, async () => {
92
+ const [callId, params] = args;
93
+ const valid = Check(tool.parameters, params);
94
+ if (!valid) {
95
+ const errors = [...Errors(tool.parameters, params)];
96
+ return failure("TOOL_SCHEMA_INVALID", `${tool.name}: ${errors.slice(0, 8).map(e => `${e.instancePath || "/"}: ${e.message}`).join("; ")}${errors.length > 8 ? `; ${errors.length - 8} more fields` : ""}`);
97
+ }
98
+ try {
99
+ await restore();
100
+ const paramsSha256 = hash(params);
101
+ const singleton = new Set(["record_route_selection", "record_state_registry", "record_mock_api", "record_data_flow", "record_dependency", "record_design_deviation", "record_required_deliverables", "record_target_surface", "finalize_contract", "finalize_plan"]);
102
+ const naturalId = params?.id ?? params?.scopeId ?? params?.entry?.id ?? params?.requirementId ?? params?.choice?.purpose ?? (tool.name === "record_openspec_selection" ? params?.path : undefined) ?? (params?.endpoint ? `${params.endpoint.method}:${params.endpoint.path}` : undefined) ?? (singleton.has(tool.name) ? "singleton" : undefined);
103
+ const identity = `${tool.name}:${typeof naturalId === "string" ? naturalId : "incremental"}`;
104
+ const submissions = input.store.records.flatMap(r => r.submission?.receipt ? [r.submission] : []);
105
+ const previousCall = submissions.find(s => s.callId === callId);
106
+ if (previousCall)
107
+ return previousCall.tool === tool.name && previousCall.paramsSha256 === paramsSha256 ? replay(previousCall.receipt) : failure("REQUEST_ID_REUSE_CONFLICT", `${tool.name}: call id already acknowledged with different parameters`);
108
+ const previousFact = submissions.filter(s => s.identity === identity).at(-1);
109
+ if (tool.name !== "record_plan_group_coverage" && previousFact?.paramsSha256 === paramsSha256)
110
+ return replay(previousFact.receipt);
111
+ if (previousFact && naturalId !== undefined && params?.replace !== true && !["record_evidence_expectation", "record_required_deliverables", "record_data_flow"].includes(tool.name))
112
+ return failure("FACT_IDENTITY_CONFLICT", `${identity}: changing a saved fact requires explicit replacement (replace:true)`);
113
+ if (input.store.records.some(r => ["contract-finalized", "finalize_plan", "approve_review", "request_review_changes", "approve_design", "request_design_changes"].includes(String(r.fact.kind))))
114
+ return failure("TERMINAL_CONFLICT", "A successful terminal is already durable; only identical acknowledged submissions may be replayed");
115
+ const draft = structuredClone(input.store);
116
+ input.setWorkingStore(draft);
117
+ let result;
118
+ try {
119
+ // Protocol controls must not be spread into domain facts by permissive callers.
120
+ const original = input.tools.find(t => t.name === tool.name);
121
+ const domainParams = { ...params };
122
+ if (!original.parameters.properties?.replace)
123
+ delete domainParams.replace;
124
+ result = await tool.execute(args[0], domainParams, ...args.slice(2));
125
+ }
126
+ finally {
127
+ input.setWorkingStore(input.store);
128
+ }
129
+ if (result.details?.ok !== true)
130
+ return result;
131
+ const newRecords = draft.records.filter(r => r.phase === "committed" && r.revision > input.store.revision);
132
+ if (!newRecords.length)
133
+ return replay(result);
134
+ for (const record of draft.records) {
135
+ record.submission ??= { version: 1, bindingSha256, tool: "bootstrap", callId: record.requestId, identity: record.eventId, paramsSha256: record.payloadSha256 };
136
+ }
137
+ for (const record of newRecords)
138
+ record.submission = { version: 1, bindingSha256, tool: tool.name, callId, identity, paramsSha256 };
139
+ newRecords.at(-1).submission.receipt = JSON.parse(JSON.stringify(result));
140
+ const committed = draft.records.filter(r => r.phase === "committed");
141
+ await validate(committed);
142
+ await persist(file, committed);
143
+ publish(draft);
144
+ progress.durableSubmissions++;
145
+ if (previousFact)
146
+ progress.durableReplacements++;
147
+ return result;
148
+ }
149
+ catch (error) {
150
+ input.setWorkingStore(input.store);
151
+ return failure(typeof error?.code === "string" ? error.code : "FRONTEND_COMMIT_FAILED", error instanceof Error ? error.message : String(error));
152
+ }
153
+ }).then(result => { if (result.details?.ok !== true)
154
+ progress.failedRecords++; return result; }) }));
155
+ for (const tool of wrapped)
156
+ progressByTool.set(tool, progress);
157
+ return {
158
+ customTools: wrapped,
159
+ commitExternal: (work) => exclusive(file, async () => {
160
+ await restore();
161
+ const draft = structuredClone(input.store);
162
+ input.setWorkingStore(draft);
163
+ try {
164
+ await work();
165
+ const newRecords = draft.records.filter(r => r.phase === "committed" && r.revision > input.store.revision);
166
+ for (const record of draft.records) {
167
+ record.submission ??= {
168
+ version: 1,
169
+ bindingSha256,
170
+ tool: "external-adoption",
171
+ callId: record.requestId,
172
+ identity: record.eventId,
173
+ paramsSha256: record.payloadSha256,
174
+ };
175
+ }
176
+ const committed = draft.records.filter(r => r.phase === "committed");
177
+ await validate(committed);
178
+ await persist(file, committed);
179
+ publish(draft);
180
+ progress.durableSubmissions += newRecords.length;
181
+ }
182
+ finally {
183
+ input.setWorkingStore(input.store);
184
+ }
185
+ }),
186
+ flush: () => exclusive(file, async () => {
187
+ await restore();
188
+ const committed = input.store.records.filter(r => r.phase === "committed");
189
+ await validate(committed);
190
+ await persist(file, committed);
191
+ }),
192
+ };
193
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ /** Model-authored execution ownership; never replaces a source obligation. */
3
+ export const frontendExecutionSchema = z.object({
4
+ groupId: z.string().trim().min(1),
5
+ kind: z.enum(["behavior", "constraint", "exclusion"]),
6
+ summary: z.string().trim().min(1),
7
+ }).strict();
8
+ /** Latest canonical member wins; joining a group requires consistent metadata.
9
+ * No lexical/semantic similarity heuristic can erase a source ID or condition. */
10
+ export function collectFrontendExecutionGroups(units) {
11
+ const groups = new Map();
12
+ for (const unit of new Map(units.map(unit => [unit.id, unit])).values()) {
13
+ const execution = unit.execution === undefined ? undefined : frontendExecutionSchema.parse(unit.execution);
14
+ const key = execution ? `group:${execution.groupId}` : `requirement:${unit.id}`;
15
+ const previous = groups.get(key);
16
+ if (previous && execution && (previous.kind !== execution.kind || previous.summary !== execution.summary))
17
+ throw Error(`EXECUTION_GROUP_CONFLICT: ${execution.groupId}; preserve differing permissions, thresholds and error paths as separate obligations/groups`);
18
+ if (previous)
19
+ previous.requirementIds.push(unit.id);
20
+ else
21
+ groups.set(key, { id: execution?.groupId ?? unit.id, kind: execution?.kind ?? "unclassified", summary: execution?.summary ?? `Independent requirement ${unit.id}`, requirementIds: [unit.id] });
22
+ }
23
+ return [...groups.values()];
24
+ }
@@ -1,3 +1,4 @@
1
+ import { frontendExecutionSchema, collectFrontendExecutionGroups } from "./frontend-execution-groups.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { readFileSync } from "node:fs";
3
4
  import { readFile, stat } from "node:fs/promises";
@@ -711,6 +712,7 @@ export const frontendImplementationContractSchema = z
711
712
  .object({
712
713
  id,
713
714
  expectedOutcome: z.string().min(1),
715
+ execution: frontendExecutionSchema.optional(),
714
716
  implementationTargets: z.array(safePath),
715
717
  verificationTargetIds: z.array(z.string().min(1)),
716
718
  evidenceGap: gap.optional(),
@@ -806,6 +808,12 @@ export const frontendImplementationContractSchema = z
806
808
  })
807
809
  .strict()
808
810
  .superRefine((value, ctx) => {
811
+ try {
812
+ collectFrontendExecutionGroups(value.requirements);
813
+ }
814
+ catch (error) {
815
+ ctx.addIssue({ code: "custom", path: ["requirements"], message: String(error) });
816
+ }
809
817
  const stableIdentities = [
810
818
  {
811
819
  path: "requirements",
@@ -2063,6 +2071,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
2063
2071
  ? { sourceFragmentIds: req.sourceFragmentIds }
2064
2072
  : {}),
2065
2073
  ...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
2074
+ ...(req.execution !== undefined ? { execution: req.execution } : {}),
2066
2075
  };
2067
2076
  // Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
2068
2077
  if (gapText) {
@@ -2500,6 +2509,10 @@ function splitFrontendContractReviewGroup(canonical, fields) {
2500
2509
  if (!Object.hasOwn(canonical, field))
2501
2510
  continue;
2502
2511
  const fieldValue = canonical[field];
2512
+ // JSON omits undefined object properties; the index must describe the
2513
+ // persisted contract, not phantom keys retained by normalization.
2514
+ if (fieldValue === undefined)
2515
+ continue;
2503
2516
  if (!Array.isArray(fieldValue)) {
2504
2517
  const candidate = { ...data, [field]: fieldValue };
2505
2518
  if (Object.keys(data).length > 0 &&