@tea-agent/loop-agent 0.44.0-next.1 → 0.44.0-next.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/CHANGELOG.md +379 -416
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
  22. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
  23. package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
  24. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  25. package/dist/executors/dag-pi/plan/record-tools.js +861 -0
  26. package/dist/executors/dag-pi/plan/schema.js +129 -0
  27. package/dist/executors/dag-pi/plan/session.js +1 -0
  28. package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
  29. package/dist/executors/dag-pi/sessions/index.js +6 -0
  30. package/dist/executors/dag-pi/sessions/plan-batches.js +280 -0
  31. package/dist/executors/dag-pi/sessions/plan-prompts.js +367 -0
  32. package/dist/executors/dag-pi/sessions/scout-parallel.js +197 -0
  33. package/dist/executors/dag-pi/sessions/segmented-plan.js +1184 -0
  34. package/dist/executors/dag-pi/sessions/writer-evidence.js +60 -0
  35. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  36. package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
  37. package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
  38. package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
  39. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  40. package/dist/executors/dag-pi-executor.js +733 -5782
  41. package/dist/executors/pi-executor.js +6 -1
  42. package/dist/executors/pi-sdk-executor.js +112 -19
  43. package/dist/executors/shell-executor.js +88 -12
  44. package/dist/executors/shell-presets.js +1 -1
  45. package/dist/executors/shell-write-guard.js +26 -3
  46. package/dist/infrastructure/console/operation-store.js +48 -14
  47. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  48. package/dist/shared/file-identity.js +23 -0
  49. package/dist/shared/git-workspace-status.js +482 -0
  50. package/dist/shared/operator/capabilities.js +10 -5
  51. package/dist/shared/path-safety.js +2 -2
  52. package/dist/shared/wizard-local-telemetry.js +51 -0
  53. package/dist/task/config-types.js +22 -8
  54. package/dist/task/contract/project.js +13 -0
  55. package/dist/task/contract/schema.js +2 -7
  56. package/dist/task/frontend-preflight.js +1 -1
  57. package/dist/task/source-prepare/parse-intent.js +124 -5
  58. package/dist/task/source-prepare/prepare.js +6 -1
  59. package/dist/task/source-prepare/semantic-intake.js +18 -3
  60. package/dist/task/source-prepare/source-execution.js +65 -0
  61. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  62. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  63. package/dist/worker/console/chat/assistant-content.js +5 -34
  64. package/dist/worker/console/chat/chat-event-store.js +62 -10
  65. package/dist/worker/console/chat/context-insights.js +723 -0
  66. package/dist/worker/console/chat/conversation-nodes.js +383 -0
  67. package/dist/worker/console/chat/db-connections.js +854 -0
  68. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  69. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
  70. package/dist/worker/console/chat/pi-runtime.js +445 -36
  71. package/dist/worker/console/chat/provider-error.js +0 -12
  72. package/dist/worker/console/chat/repo-browser.js +52 -4
  73. package/dist/worker/console/chat/routes.js +577 -72
  74. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  75. package/dist/worker/console/chat/session-store.js +147 -19
  76. package/dist/worker/console/chat/shortcuts.js +7 -0
  77. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  78. package/dist/worker/console/chat/tools.js +34 -0
  79. package/dist/worker/console/chat/turn-process.js +237 -66
  80. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  81. package/dist/worker/console/console-update-runtime.js +2 -2
  82. package/dist/worker/console/dag-confirmation.js +3 -0
  83. package/dist/worker/console/dag-execution-receipt.js +3 -0
  84. package/dist/worker/console/operator-actions.js +32 -3
  85. package/dist/worker/console/operator-user-error.js +28 -1
  86. package/dist/worker/console/pi-readiness.js +4 -4
  87. package/dist/worker/console/prd-intake-bridge.js +5 -12
  88. package/dist/worker/console/recovery-cta.js +14 -14
  89. package/dist/worker/console/server.js +5 -1
  90. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  91. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
  92. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  93. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  94. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  95. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  96. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  100. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  101. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  102. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  106. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  107. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  108. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  109. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  110. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  111. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  112. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  113. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  114. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  115. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  116. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  117. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  118. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  119. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  120. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  121. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  122. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  123. package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
  124. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  125. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  126. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  127. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  128. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
  129. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
  130. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  131. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  132. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  133. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  134. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  135. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  136. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  137. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  138. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  139. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  140. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  141. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  142. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  143. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  144. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  145. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  146. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  147. package/dist/worker/console/static/index.html +2 -2
  148. package/dist/worker/console/static-src/app/console-types.js +9 -1
  149. package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
  150. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  151. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
  152. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  153. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  154. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  155. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  156. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  157. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  158. package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
  159. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  160. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  161. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
  162. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  163. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  164. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  165. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
  166. package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
  167. package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
  168. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  169. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  170. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  171. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  172. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  173. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  174. package/dist/worker/console/workspace-context.js +2 -0
  175. package/dist/worker/console/workspace-initialization.js +3 -3
  176. package/dist/worker/observability/read-model.js +23 -2
  177. package/dist/worker/observe/node-context-insights.js +479 -0
  178. package/dist/worker/observe/static/console-theme.css +174 -0
  179. package/dist/worker/observe/static/console-theme.js +36 -0
  180. package/dist/worker/observe/static/operator-chrome.css +85 -0
  181. package/dist/worker/observe/static/operator-chrome.js +51 -0
  182. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  183. package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
  184. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  185. package/dist/workflows/dag/budget-enforcement.js +31 -2
  186. package/dist/workflows/dag/checkpoint.js +686 -0
  187. package/dist/workflows/dag/frontend-closeout.js +3 -1
  188. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  189. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  190. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  191. package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
  192. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  193. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  194. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  195. package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
  196. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  197. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  198. package/dist/workflows/dag/frontend-review-context.js +29 -1
  199. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  200. package/dist/workflows/dag/frontend-risk.js +8 -3
  201. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  202. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  203. package/dist/workflows/dag/frontend-session-context.js +10 -0
  204. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  205. package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
  206. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  207. package/dist/workflows/dag/frontend-writer-admission.js +3 -1
  208. package/dist/workflows/dag/hybrid/sources.js +1812 -0
  209. package/dist/workflows/dag/hybrid/templates/backend-test.js +1807 -0
  210. package/dist/workflows/dag/hybrid/templates/frontend-test.js +702 -0
  211. package/dist/workflows/dag/hybrid/templates/frontend.js +1529 -0
  212. package/dist/workflows/dag/hybrid/templates/index.js +8 -0
  213. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +449 -0
  214. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +495 -0
  215. package/dist/workflows/dag/hybrid/templates/shared.js +101 -0
  216. package/dist/workflows/dag/hybrid/templates/standard.js +265 -0
  217. package/dist/workflows/dag/hybrid/types.js +32 -0
  218. package/dist/workflows/dag/init-hybrid.js +57 -7106
  219. package/dist/workflows/dag/node-execution.js +11 -11
  220. package/dist/workflows/dag/output-protocol.js +1 -0
  221. package/dist/workflows/dag/rerun-feedback.js +35 -4
  222. package/dist/workflows/dag/rerun-task.js +62 -66
  223. package/dist/workflows/dag/runner.js +38 -1159
  224. package/dist/workflows/dag/terminal-status.js +502 -0
  225. package/dist/workflows/dag/types.js +15 -8
  226. package/dist/workflows/loop/benchmark.js +11 -8
  227. package/docs/README.md +1 -1
  228. package/docs/architecture/dag-execution.md +1 -1
  229. package/docs/architecture/runtime-boundaries.md +1 -1
  230. package/docs/governance/harness-methodology-verification.md +11 -0
  231. package/docs/templates/backend-test-dag.json +5 -4
  232. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  233. package/package.json +6 -4
  234. package/skills/frontend-design-review/SKILL.md +8 -11
  235. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  236. package/skills/frontend-review/SKILL.md +5 -1
  237. package/skills/loop-agent/references/command-reference.md +8 -0
  238. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  239. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  240. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  241. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  242. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  243. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  244. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  245. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -0,0 +1,482 @@
1
+ import { access, lstat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { runResilientGitCommand } from "./resilient-git.js";
4
+ const MAX_SUBMODULE_DEPTH = 4;
5
+ export const DEFAULT_WORKSPACE_STATUS_DEPENDENCIES = {
6
+ runGit: (cwd, args) => runResilientGitCommand({ cwd, args, readOnly: true }).then((result) => ({
7
+ code: result.code,
8
+ stdout: result.stdout,
9
+ stderr: result.stderr,
10
+ })),
11
+ readSubmoduleStatus: async (cwd) => {
12
+ const result = await runResilientGitCommand({
13
+ cwd,
14
+ args: ["status", "--porcelain=v1", "--untracked-files=all"],
15
+ readOnly: true,
16
+ });
17
+ if (result.code !== 0) {
18
+ throw new Error(result.stderr.trim() ||
19
+ result.stdout.trim() ||
20
+ `git status exited ${result.code}`);
21
+ }
22
+ return result.stdout;
23
+ },
24
+ };
25
+ /** Parse porcelain v1 output into normalized path -> "XY" entries. */
26
+ export function parsePorcelainEntries(porcelain) {
27
+ const entries = new Map();
28
+ for (const line of porcelain.split("\n")) {
29
+ const trimmed = line.trimEnd();
30
+ if (!trimmed)
31
+ continue;
32
+ const x = trimmed[0] ?? " ";
33
+ const y = trimmed[1] ?? " ";
34
+ const rawPath = trimmed.slice(3);
35
+ if (!rawPath)
36
+ continue;
37
+ const normalized = normalizeWorkspacePath(unquoteGitPath(renameTargetPath(rawPath)));
38
+ if (!normalized)
39
+ continue;
40
+ entries.set(normalized, `${x}${y}`);
41
+ }
42
+ return entries;
43
+ }
44
+ /**
45
+ * Return the destination path of a porcelain path field. Rename entries
46
+ * render as `ORIG -> DEST` where either side may independently be C-quoted;
47
+ * splitting on a quoted ` -> ` (or unquoting before splitting) corrupts the
48
+ * key with residual quotes or half-decoded octal. Scan for the last
49
+ * outside-quote separator instead, then return the still-quoted DEST for
50
+ * the caller to unquote.
51
+ */
52
+ function renameTargetPath(rawPath) {
53
+ let inQuotes = false;
54
+ let separatorIndex = -1;
55
+ for (let i = 0; i < rawPath.length - 3; i += 1) {
56
+ const char = rawPath[i];
57
+ if (char === "\\") {
58
+ i += 1; // skip the escaped character; it cannot open/close quotes
59
+ continue;
60
+ }
61
+ if (char === '"')
62
+ inQuotes = !inQuotes;
63
+ if (!inQuotes && rawPath.startsWith(" -> ", i))
64
+ separatorIndex = i;
65
+ }
66
+ if (separatorIndex === -1)
67
+ return rawPath;
68
+ return rawPath.slice(separatorIndex + 4);
69
+ }
70
+ function normalizeWorkspacePath(value) {
71
+ const target = value.includes(" -> ")
72
+ ? (value.split(" -> ").at(-1) ?? value)
73
+ : value;
74
+ // Embedded-repo untracked entries render as `dir/`; strip the trailing
75
+ // slash so the key matches the registered submodule path form.
76
+ return target.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
77
+ }
78
+ /**
79
+ * Unquote a git C-quoted path (`"..."` with C-style escapes; non-ASCII bytes
80
+ * arrive as octal like \346\225\260). Returns the input unchanged when not
81
+ * quoted. This keeps Chinese/space paths (e.g. `source/需求.md`) comparable
82
+ * against writeSet patterns.
83
+ */
84
+ export function unquoteGitPath(value) {
85
+ if (value.length < 2 || !value.startsWith('"') || !value.endsWith('"')) {
86
+ return value;
87
+ }
88
+ const inner = value.slice(1, -1);
89
+ let out = "";
90
+ for (let i = 0; i < inner.length; i += 1) {
91
+ const char = inner[i];
92
+ if (char !== "\\") {
93
+ out += char;
94
+ continue;
95
+ }
96
+ const next = inner[i + 1];
97
+ if (next === undefined)
98
+ break;
99
+ if (next === "n")
100
+ out += "\n";
101
+ else if (next === "t")
102
+ out += "\t";
103
+ else if (next === "r")
104
+ out += "\r";
105
+ else if (next === "\\")
106
+ out += "\\";
107
+ else if (next === '"')
108
+ out += '"';
109
+ else if (/[0-7]{3}/.test(inner.slice(i + 1, i + 4))) {
110
+ // Octal byte escape: collect consecutive escapes into one string
111
+ // and decode as UTF-8. `continue` skips the shared `i += 1` below
112
+ // (i already points past the consumed run).
113
+ const bytes = [];
114
+ let j = i;
115
+ while (j < inner.length && inner[j] === "\\" && /[0-7]{3}/.test(inner.slice(j + 1, j + 4))) {
116
+ bytes.push(Number.parseInt(inner.slice(j + 1, j + 4), 8));
117
+ j += 4;
118
+ }
119
+ out += Buffer.from(bytes).toString("utf8");
120
+ i = j - 1;
121
+ continue;
122
+ }
123
+ else {
124
+ out += next;
125
+ }
126
+ i += 1;
127
+ }
128
+ return out;
129
+ }
130
+ /**
131
+ * Read registered submodule directories from `.gitmodules` via
132
+ * `git config -f .gitmodules --get-regexp` (structured output; no
133
+ * shell-quoting hazards). Returns [] when the file is absent or empty.
134
+ */
135
+ export async function readSubmoduleRegistrations(rootCwd, dependencies = DEFAULT_WORKSPACE_STATUS_DEPENDENCIES) {
136
+ if (!(await hasGitmodulesFile(rootCwd)))
137
+ return [];
138
+ const result = await dependencies.runGit(rootCwd, [
139
+ "config",
140
+ "-f",
141
+ ".gitmodules",
142
+ "--get-regexp",
143
+ "^submodule\\..*\\.path$",
144
+ ]);
145
+ // Exit 1 with no stderr = the file exists but has no matching keys.
146
+ // Any other non-zero exit (unreadable file, parse error) must fail closed:
147
+ // returning [] would silently drop every submodule from observation.
148
+ if (result.code === 1 && !result.stderr.trim())
149
+ return [];
150
+ if (result.code !== 0) {
151
+ throw new Error(`git config -f .gitmodules failed (exit ${result.code}): ${result.stderr.trim() || result.stdout.trim() || "no output"}`);
152
+ }
153
+ if (!result.stdout.trim())
154
+ return [];
155
+ const paths = [];
156
+ for (const line of result.stdout.split("\n")) {
157
+ const trimmed = line.trim();
158
+ if (!trimmed)
159
+ continue;
160
+ // Line format: `submodule.<name>.path <value>`. The value may contain
161
+ // spaces; split on the first space after the key only.
162
+ const separator = trimmed.indexOf(" ");
163
+ if (separator <= 0)
164
+ continue;
165
+ const submodulePath = trimmed.slice(separator + 1).trim();
166
+ if (!submodulePath)
167
+ continue;
168
+ paths.push(normalizeWorkspacePath(submodulePath));
169
+ }
170
+ return paths;
171
+ }
172
+ /**
173
+ * Probe the gitlink state of one submodule against its parent repository.
174
+ * Porcelain codes cannot distinguish "dirty content inside the submodule"
175
+ * (covered by the child's file-level entries) from "the child HEAD actually
176
+ * moved" (a git mutation the child's own clean status cannot express), so
177
+ * the relevant SHAs are compared directly.
178
+ *
179
+ * Three facts are captured so staged gitlinks and repeated moves cannot
180
+ * hide a mutation:
181
+ * - `indexSha`: what the parent index records for the gitlink now.
182
+ * - `headTreeSha`: what the parent HEAD tree records (detects `git add sub`
183
+ * staged after a commit — the working gitlink moved but index == HEAD is
184
+ * not the full story; see gitlinkMutated).
185
+ * - `headSha`: the child's checked-out HEAD.
186
+ *
187
+ * `readFailed` maps to the caller's fail-closed incomplete path.
188
+ */
189
+ async function readGitlinkState(parentRoot, submodulePath, dependencies) {
190
+ const ls = await dependencies.runGit(parentRoot, [
191
+ "ls-files",
192
+ "-s",
193
+ "--",
194
+ submodulePath,
195
+ ]);
196
+ const line = ls.code !== 0
197
+ ? undefined
198
+ : ls.stdout.split("\n").find((entry) => entry.trim());
199
+ // `160000 <sha> 0\t<path>` for a gitlink entry.
200
+ const indexSha = line?.split(/\s+/)[1];
201
+ const headTree = await dependencies.runGit(parentRoot, [
202
+ "rev-parse",
203
+ `HEAD:${submodulePath}`,
204
+ ]);
205
+ const headTreeSha = headTree.code === 0 ? headTree.stdout.trim() : undefined;
206
+ const rev = await dependencies.runGit(joinFsPath(parentRoot, submodulePath), ["rev-parse", "HEAD"]);
207
+ const headSha = rev.code === 0 ? rev.stdout.trim() : undefined;
208
+ if (ls.code !== 0 || rev.code !== 0) {
209
+ return {
210
+ indexSha,
211
+ headTreeSha,
212
+ headSha,
213
+ readFailed: true,
214
+ errorDetail: `ls-files exit ${ls.code}; rev-parse exit ${rev.code}: ${rev.stderr.trim() || ls.stderr.trim() || "no output"}`,
215
+ };
216
+ }
217
+ return { indexSha, headTreeSha, headSha, readFailed: false };
218
+ }
219
+ /**
220
+ * A gitlink mutation is any state where the working gitlink (child HEAD)
221
+ * disagrees with what the parent's committed or staged history expects, or
222
+ * where the parent index gitlink is itself dirty relative to HEAD:
223
+ * - child HEAD != index: writer moved the submodule HEAD (committed inside
224
+ * the submodule, or checked out another commit).
225
+ * - index != HEAD tree: the gitlink change was staged (`git add sub`) but
226
+ * not committed — still an unauthorized git mutation during a run.
227
+ * - child HEAD != HEAD tree (index clean): a moved HEAD not yet staged;
228
+ * the classic in-submodule commit.
229
+ * When the parent has never committed the submodule (fresh `submodule add`,
230
+ * no `HEAD:<path>`), only the index-vs-child comparison applies.
231
+ */
232
+ function gitlinkMutated(state) {
233
+ const { indexSha, headTreeSha, headSha } = state;
234
+ if (indexSha !== undefined && headSha !== undefined && indexSha !== headSha) {
235
+ return true;
236
+ }
237
+ if (headTreeSha !== undefined &&
238
+ indexSha !== undefined &&
239
+ headTreeSha !== indexSha) {
240
+ return true;
241
+ }
242
+ if (headTreeSha !== undefined &&
243
+ headSha !== undefined &&
244
+ headTreeSha !== headSha) {
245
+ return true;
246
+ }
247
+ return false;
248
+ }
249
+ /**
250
+ * Sentinel key for a mutated gitlink. Embeds the observed SHA triple so the
251
+ * key changes when the gitlink moves again (a constant key would make a
252
+ * second move on a dirty baseline indistinguishable from no move).
253
+ */
254
+ function gitlinkSentinelKey(workspacePath, state) {
255
+ const fingerprint = [state.indexSha, state.headTreeSha, state.headSha]
256
+ .map((sha) => sha ?? "-")
257
+ .join("+");
258
+ return `${GITLINK_SENTINEL_PREFIX}${workspacePath}@${fingerprint}`;
259
+ }
260
+ async function submoduleWorkingTreeInitialized(rootCwd, submodulePath) {
261
+ try {
262
+ const info = await lstat(path.resolve(rootCwd, ...submodulePath.split("/"), ".git"));
263
+ return info.isFile() || info.isDirectory();
264
+ }
265
+ catch {
266
+ return false;
267
+ }
268
+ }
269
+ async function collectSubmoduleEntries(input) {
270
+ // undefined = collection failed for this submodule (recorded, fail-closed).
271
+ const repoRoot = joinFsPath(input.rootCwd, input.workspacePath);
272
+ try {
273
+ const porcelain = await input.dependencies.readSubmoduleStatus(repoRoot);
274
+ input.repos.push({
275
+ repoRelativePath: input.repoWorkspacePath,
276
+ repoId: input.repoWorkspacePath,
277
+ state: "clean-status-read",
278
+ porcelain,
279
+ });
280
+ const childEntries = parsePorcelainEntries(porcelain);
281
+ const expanded = new Map();
282
+ for (const [childPath, code] of childEntries) {
283
+ expanded.set(`${input.repoWorkspacePath}/${childPath}`, code);
284
+ }
285
+ if (input.depth < MAX_SUBMODULE_DEPTH) {
286
+ const nestedPaths = await readSubmoduleRegistrations(repoRoot, input.dependencies);
287
+ for (const nestedPath of nestedPaths) {
288
+ const nestedWorkspacePath = `${input.repoWorkspacePath}/${nestedPath}`;
289
+ const initialized = await submoduleWorkingTreeInitialized(repoRoot, nestedPath);
290
+ if (!initialized) {
291
+ input.repos.push({
292
+ repoRelativePath: nestedWorkspacePath,
293
+ repoId: nestedWorkspacePath,
294
+ state: "uninitialized",
295
+ porcelain: "",
296
+ });
297
+ continue;
298
+ }
299
+ // Gitlink probe at this nesting level: the parent of `nested` is the
300
+ // current repository (repoRoot). Same three-SHA semantics as the
301
+ // top level; a moved nested HEAD surfaces as a SHA-tagged sentinel.
302
+ const nestedGitlink = await readGitlinkState(repoRoot, nestedPath, input.dependencies);
303
+ if (nestedGitlink.readFailed) {
304
+ input.repos.push({
305
+ repoRelativePath: nestedWorkspacePath,
306
+ repoId: nestedWorkspacePath,
307
+ state: "status-read-failed",
308
+ porcelain: "",
309
+ errorDetail: `gitlink probe failed: ${nestedGitlink.errorDetail ?? "unknown"}`,
310
+ });
311
+ continue;
312
+ }
313
+ const nestedEntries = await collectSubmoduleEntries({
314
+ rootCwd: repoRoot,
315
+ workspacePath: nestedPath,
316
+ repoWorkspacePath: nestedWorkspacePath,
317
+ depth: input.depth + 1,
318
+ repos: input.repos,
319
+ dependencies: input.dependencies,
320
+ });
321
+ if (nestedEntries === undefined) {
322
+ // Nested collection failures are recorded in the shared
323
+ // repos array with state status-read-failed; completeness
324
+ // is derived from those states after the top-level loop.
325
+ continue;
326
+ }
327
+ for (const [nestedFile, code] of nestedEntries) {
328
+ // nestedEntries keys are already full workspace paths
329
+ // (prefixed with nestedWorkspacePath).
330
+ expanded.set(nestedFile, code);
331
+ }
332
+ if (gitlinkMutated(nestedGitlink)) {
333
+ expanded.set(gitlinkSentinelKey(nestedWorkspacePath, nestedGitlink), "M ");
334
+ }
335
+ // Drop the child's own directory-level entry for the nested
336
+ // submodule (gitlink ` M nested` or embedded `?? nested/`);
337
+ // its file facts now come from the nested repository.
338
+ expanded.delete(`${input.repoWorkspacePath}/${nestedPath}`);
339
+ }
340
+ }
341
+ return expanded;
342
+ }
343
+ catch (error) {
344
+ input.repos.push({
345
+ repoRelativePath: input.repoWorkspacePath,
346
+ repoId: input.repoWorkspacePath,
347
+ state: "status-read-failed",
348
+ porcelain: "",
349
+ errorDetail: error instanceof Error ? error.message : String(error),
350
+ });
351
+ return undefined;
352
+ }
353
+ }
354
+ function joinFsPath(root, workspacePath) {
355
+ return path.resolve(root, ...workspacePath.split("/"));
356
+ }
357
+ /**
358
+ * Expand a parent porcelain snapshot into workspace file-level facts.
359
+ * Callers pass the already-captured parent porcelain so the parent bytes are
360
+ * captured once and reused.
361
+ */
362
+ export async function expandWorkspaceStatus(input) {
363
+ const dependencies = input.dependencies ?? DEFAULT_WORKSPACE_STATUS_DEPENDENCIES;
364
+ const parentEntries = parsePorcelainEntries(input.parentPorcelain);
365
+ const repos = [
366
+ {
367
+ repoRelativePath: "",
368
+ repoId: "parent",
369
+ state: "clean-status-read",
370
+ porcelain: input.parentPorcelain,
371
+ },
372
+ ];
373
+ const entries = new Map();
374
+ let complete = true;
375
+ const registrations = input.hasGitmodulesFile
376
+ ? await readSubmoduleRegistrations(input.rootCwd, dependencies)
377
+ : [];
378
+ for (const registration of registrations) {
379
+ const initialized = await submoduleWorkingTreeInitialized(input.rootCwd, registration);
380
+ if (!initialized) {
381
+ repos.push({
382
+ repoRelativePath: registration,
383
+ repoId: registration,
384
+ state: "uninitialized",
385
+ porcelain: "",
386
+ });
387
+ continue;
388
+ }
389
+ // Gitlink probe: SHA-compare the parent index gitlink against the
390
+ // child's checked-out HEAD. Only a real HEAD move (e.g. the writer
391
+ // committed inside the submodule) becomes a sentinel; dirty child
392
+ // content stays covered by the child's file-level entries.
393
+ const gitlink = await readGitlinkState(input.rootCwd, registration, dependencies);
394
+ if (gitlink.readFailed) {
395
+ repos.push({
396
+ repoRelativePath: registration,
397
+ repoId: registration,
398
+ state: "status-read-failed",
399
+ porcelain: "",
400
+ errorDetail: `gitlink probe failed: ${gitlink.errorDetail ?? "unknown"}`,
401
+ });
402
+ complete = false;
403
+ continue;
404
+ }
405
+ const submoduleEntries = await collectSubmoduleEntries({
406
+ rootCwd: input.rootCwd,
407
+ workspacePath: registration,
408
+ repoWorkspacePath: registration,
409
+ depth: 1,
410
+ repos,
411
+ dependencies,
412
+ });
413
+ if (submoduleEntries === undefined) {
414
+ complete = false;
415
+ continue;
416
+ }
417
+ for (const [workspacePath, code] of submoduleEntries) {
418
+ entries.set(workspacePath, code);
419
+ }
420
+ if (gitlinkMutated(gitlink)) {
421
+ entries.set(gitlinkSentinelKey(registration, gitlink), "M ");
422
+ }
423
+ }
424
+ // Completeness is derived from the recorded repo states: any
425
+ // status-read-failed repository at any depth (top-level or nested)
426
+ // makes the observation incomplete, so nested failures bubble up.
427
+ // (.gitmodules read failures throw and fail the whole observation.)
428
+ const completeNow = complete &&
429
+ !repos.some((repo) => repo.state === "status-read-failed");
430
+ // Parent entries: keep everything except directory-level gitlink entries
431
+ // of submodules whose own status was read successfully AND whose gitlink
432
+ // is unchanged. When the parent still reports the submodule as modified
433
+ // (gitlink differs from the index — the writer committed inside the
434
+ // submodule, or checked out another commit), keep a sentinel entry so the
435
+ // change stays visible in the expanded delta instead of silently
436
+ // vanishing behind the child's clean status. The child's clean file-level
437
+ // status cannot distinguish "gitlink untouched" from "gitlink moved";
438
+ // only the parent entry can, so it is preserved whenever non-clean.
439
+ const expandedRegistrations = new Set(repos
440
+ .filter((repo) => repo.repoRelativePath !== "" &&
441
+ repo.state === "clean-status-read")
442
+ .map((repo) => repo.repoRelativePath));
443
+ for (const [parentPath, code] of parentEntries) {
444
+ if (expandedRegistrations.has(parentPath)) {
445
+ // Directory-level submodule entries are dropped: file-level facts
446
+ // come from the child repos, and a moved gitlink is surfaced by the
447
+ // SHA probe as a sentinel during the registration loop. Porcelain
448
+ // codes cannot distinguish dirty content from a moved HEAD, so no
449
+ // code-based sentinel may be derived here.
450
+ continue;
451
+ }
452
+ if (entries.has(parentPath))
453
+ continue; // child facts take precedence
454
+ entries.set(parentPath, code);
455
+ }
456
+ return {
457
+ parentPorcelain: input.parentPorcelain,
458
+ entries,
459
+ repos,
460
+ complete: completeNow,
461
+ };
462
+ }
463
+ /** Prefix marking a preserved parent gitlink fact (mutated submodule HEAD). */
464
+ export const GITLINK_SENTINEL_PREFIX = "\u0000gitlink\u0000/";
465
+ /** Serialize expanded entries back into sorted porcelain-v1-shaped lines. */
466
+ export function synthesizeExpandedPorcelain(entries) {
467
+ const lines = [];
468
+ for (const [filePath, code] of [...entries.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
469
+ lines.push(`${code} ${filePath}`);
470
+ }
471
+ return lines.join("\n") + (lines.length > 0 ? "\n" : "");
472
+ }
473
+ /** Check for a `.gitmodules` file at the repository root. */
474
+ export async function hasGitmodulesFile(rootCwd) {
475
+ try {
476
+ await access(path.resolve(rootCwd, ".gitmodules"));
477
+ return true;
478
+ }
479
+ catch {
480
+ return false;
481
+ }
482
+ }
@@ -863,9 +863,9 @@ export function buildOperatorCapabilitiesDocument() {
863
863
  },
864
864
  {
865
865
  name: "capabilityBoundary",
866
- type: "array",
866
+ type: "object",
867
867
  required: false,
868
- description: "optional frozen test capability boundary {allowed?, forbidden?} composed into dependencyPolicy by the runtime",
868
+ description: "optional frozen test capability boundary {allowed?, forbidden?, requiredClauses?} composed into dependencyPolicy by the runtime",
869
869
  },
870
870
  {
871
871
  name: "interactionIds",
@@ -1237,10 +1237,9 @@ export function buildOperatorCapabilitiesDocument() {
1237
1237
  },
1238
1238
  {
1239
1239
  name: "capabilityBoundary",
1240
- type: "array",
1241
- itemsType: "string",
1240
+ type: "object",
1242
1241
  required: false,
1243
- description: "frozen test capability boundary {allowed?, forbidden?} composed into dependencyPolicy by the runtime",
1242
+ description: "frozen test capability boundary {allowed?, forbidden?, requiredClauses?} composed into dependencyPolicy by the runtime",
1244
1243
  },
1245
1244
  {
1246
1245
  name: "interactionIds",
@@ -1950,6 +1949,12 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
1950
1949
  action: "doctor",
1951
1950
  source: "loop-agent",
1952
1951
  },
1952
+ {
1953
+ command: "loop-agent promote-run",
1954
+ coverage: "excluded",
1955
+ action: null,
1956
+ source: "loop-agent",
1957
+ },
1953
1958
  {
1954
1959
  command: "loop-agent task advance",
1955
1960
  coverage: "model-callable",
@@ -1,6 +1,7 @@
1
1
  import { constants as fsConstants } from "node:fs";
2
2
  import { lstat, open, realpath } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { sameFileIdentity } from "./file-identity.js";
4
5
  function assertContained(root, target, label) {
5
6
  if (target === root || !target.startsWith(`${root}${path.sep}`)) {
6
7
  throw new Error(`${label}: path escapes boundary: ${target}`);
@@ -81,8 +82,7 @@ export async function readFileBoundarySafe(input) {
81
82
  if (current.isSymbolicLink() ||
82
83
  !current.isFile() ||
83
84
  current.nlink !== 1 ||
84
- current.dev !== opened.dev ||
85
- current.ino !== opened.ino) {
85
+ !sameFileIdentity(opened, current)) {
86
86
  throw new Error(`${input.label}: path changed while opening`);
87
87
  }
88
88
  return await handle.readFile();
@@ -0,0 +1,51 @@
1
+ export const DAG_TELEMETRY_SESSION_ID_ENV = "LOOP_AGENT_DAG_TELEMETRY_SESSION_ID";
2
+ export const WIZARD_LOCAL_DEFAULT_USER_AGENT = "Builder/OpenCode ai-sdk/provider-utils/4.0.21 runtime/bun/1.3.11";
3
+ export function isTelemetrySessionId(value) {
4
+ return (typeof value === "string" &&
5
+ /^ses_[0-9A-Za-z]{10,128}$/.test(value));
6
+ }
7
+ function deleteHeaderCaseInsensitive(headers, name) {
8
+ for (const key of Object.keys(headers)) {
9
+ if (key.toLowerCase() === name.toLowerCase())
10
+ delete headers[key];
11
+ }
12
+ }
13
+ function configuredHeader(headers, name) {
14
+ const key = Object.keys(headers ?? {}).find((entry) => entry.toLowerCase() === name.toLowerCase());
15
+ const value = key ? headers?.[key] : undefined;
16
+ return typeof value === "string" ? value : undefined;
17
+ }
18
+ function setStaticHeader(headers, configured, name, defaultValue) {
19
+ deleteHeaderCaseInsensitive(headers, name);
20
+ headers[name] = configuredHeader(configured, name) ?? defaultValue;
21
+ }
22
+ /** Apply the shared reporting identity without changing Pi's internal session. */
23
+ export async function injectWizardLocalTelemetryHeaders(options) {
24
+ const model = options.context.model;
25
+ if (model?.provider !== "wizard-local" ||
26
+ !model.id ||
27
+ !isTelemetrySessionId(options.telemetrySessionId)) {
28
+ return;
29
+ }
30
+ const modelRegistry = options.context.modelRegistry;
31
+ if (!modelRegistry?.getApiKeyAndHeaders)
32
+ return;
33
+ // Keep the call attached because ModelRegistry reads private runtime state via `this`.
34
+ const auth = await modelRegistry.getApiKeyAndHeaders(model);
35
+ if (!auth.ok || typeof auth.apiKey !== "string" || !auth.apiKey)
36
+ return;
37
+ setStaticHeader(options.headers, auth.headers, "client-code", "OpenCode");
38
+ setStaticHeader(options.headers, auth.headers, "User-Agent", WIZARD_LOCAL_DEFAULT_USER_AGENT);
39
+ for (const name of [
40
+ "x-api-key",
41
+ "X-Session-Id",
42
+ "X-Model-Id",
43
+ "ws-auth",
44
+ "ws-sign",
45
+ ]) {
46
+ deleteHeaderCaseInsensitive(options.headers, name);
47
+ }
48
+ options.headers["x-api-key"] = auth.apiKey;
49
+ options.headers["X-Session-Id"] = options.telemetrySessionId;
50
+ options.headers["X-Model-Id"] = model.id;
51
+ }
@@ -1,6 +1,20 @@
1
1
  import { z } from "zod";
2
2
  import { frontendExecutionPolicySchema } from "../shared/frontend-execution-policy.js";
3
3
  import { DEFAULT_FRONTEND_SPEC_ROOTS, isFrontendSpecFilePath, isValidFrontendSpecRoot, } from "../shared/openspec-spec.js";
4
+ /** Frozen user-authored clauses. Preserve wording/line breaks; normalize only
5
+ * line endings and surrounding whitespace, with explicit bounded payloads. */
6
+ const capabilityClauses = z.array(z.string().transform(value => value.replace(/\r\n?/g, "\n").trim()).pipe(z.string().min(1).max(4096))).max(64).transform(values => [...new Set(values)]);
7
+ export const capabilityBoundarySchema = z.object({
8
+ allowed: capabilityClauses.optional(),
9
+ forbidden: capabilityClauses.optional(),
10
+ requiredClauses: capabilityClauses.optional(),
11
+ }).strict().superRefine((boundary, ctx) => {
12
+ const clauses = [...(boundary.allowed ?? []), ...(boundary.forbidden ?? []), ...(boundary.requiredClauses ?? [])];
13
+ if (!clauses.length)
14
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "capabilityBoundary needs allowed, forbidden or requiredClauses entries" });
15
+ if (clauses.reduce((total, clause) => total + clause.length, 0) > 32768)
16
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "capabilityBoundary exceeds 32768 characters" });
17
+ });
4
18
  export const taskFlowSchema = z.enum([
5
19
  "auto",
6
20
  "micro",
@@ -39,6 +53,13 @@ export const taskVerifyCommandSchema = z.object({
39
53
  label: z.string().min(1),
40
54
  command: z.string().min(1),
41
55
  timeoutMs: z.number().int().positive().optional(),
56
+ /** Explicit verification lane for this command. Omit to keep automatic
57
+ * classification. Declaring `static` is the escape hatch for commands that
58
+ * provably cannot emit a test report (governance/lint scripts): the behavior
59
+ * lane requires test-observation capability at writer admission, so a
60
+ * non-test command misclassified into it blocks admission deterministically.
61
+ * Static commands still execute and their exit code is still checked. */
62
+ mode: z.enum(["static", "behavior"]).optional(),
42
63
  });
43
64
  export const dagVerifyStrategySchema = z
44
65
  .object({
@@ -304,14 +325,7 @@ const taskConfigObjectSchema = z.object({
304
325
  .optional(),
305
326
  /** Generation-frozen test capability boundary composed into the canonical
306
327
  * dependencyPolicy by the runtime (planner never maintains it). */
307
- capabilityBoundary: z
308
- .object({
309
- allowed: z.array(z.string().min(1)).optional(),
310
- forbidden: z.array(z.string().min(1)).optional(),
311
- })
312
- .strict()
313
- .refine((entry) => (entry.allowed?.length ?? 0) > 0 || (entry.forbidden?.length ?? 0) > 0, { message: "capabilityBoundary needs allowed or forbidden entries" })
314
- .optional(),
328
+ capabilityBoundary: capabilityBoundarySchema.optional(),
315
329
  /** Generation-frozen interaction id vocabulary. Contract interaction names
316
330
  * must come from this set (plan finalize deterministic check). */
317
331
  interactionIds: z.array(z.string().min(1)).optional(),
@@ -34,6 +34,19 @@ export function projectRequirementMarkdown(draft, ledger) {
34
34
  }
35
35
  lines.push("", "## Acceptance Criteria", "");
36
36
  for (const ac of draft.requirement.acceptanceCriteria) {
37
+ const body = ac.body?.trim();
38
+ if (body) {
39
+ // Emit the heading form when a body exists. Only `#### AC-*` headings
40
+ // round-trip their prose: a bullet's text is a single line, and an
41
+ // indented continuation line is dropped on the next read. The
42
+ // derived view is re-parsed on every advance, so the projection has
43
+ // to be a fixed point for the requirement body — otherwise the
44
+ // substance is lost again and the frontend shape classifier keeps
45
+ // seeing a bodyless criterion, freezing a reduced topology that
46
+ // fails post-Contract with `reshape-required`.
47
+ lines.push("", `#### ${ac.id} ${ac.text}`, "", body, "");
48
+ continue;
49
+ }
37
50
  lines.push(`- ${ac.id}: ${ac.text}`);
38
51
  }
39
52
  if (draft.assumptions && draft.assumptions.length > 0) {
@@ -1,3 +1,4 @@
1
+ import { capabilityBoundarySchema } from "../config-types.js";
1
2
  import { z } from "zod";
2
3
  import { backendTestConfigSchema, frontendOpenspecConfigSchema, taskKindSchema, taskVerifyCommandSchema, } from "../config-types.js";
3
4
  import { TASK_CONFIG_SCHEMA_VERSION, TASK_CONTRACT_CANONICALIZER_VERSION, TASK_CONTRACT_DRAFT_SCHEMA_VERSION, TASK_CONTRACT_PROJECTION_VERSION, TASK_CONTRACT_REF_SCHEMA_VERSION, } from "./constants.js";
@@ -84,13 +85,7 @@ export const taskContractDraftSchema = z
84
85
  })
85
86
  .strict())
86
87
  .optional(),
87
- capabilityBoundary: z
88
- .object({
89
- allowed: z.array(z.string().min(1)).optional(),
90
- forbidden: z.array(z.string().min(1)).optional(),
91
- })
92
- .strict()
93
- .optional(),
88
+ capabilityBoundary: capabilityBoundarySchema.optional(),
94
89
  interactionIds: z.array(z.string().min(1)).optional(),
95
90
  interviewAssessmentRef: z
96
91
  .object({