@selesai/code 0.5.20 → 0.5.21

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 (263) hide show
  1. package/dist/extensions/handoff-new.test.ts +8 -9
  2. package/dist/extensions/handoff-new.ts +6 -8
  3. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  4. package/dist/extensions/pi-subagents/README.md +602 -220
  5. package/dist/extensions/pi-subagents/banner.png +0 -0
  6. package/dist/extensions/pi-subagents/index.ts +1 -0
  7. package/dist/extensions/pi-subagents/install.mjs +3 -3
  8. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  9. package/dist/extensions/pi-subagents/package.json +26 -14
  10. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  11. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  12. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  13. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  14. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  15. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  16. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  18. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  19. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  20. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  21. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  22. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  23. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  24. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  25. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  26. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  27. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  28. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  29. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  30. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  31. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  32. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  33. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  34. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  35. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  36. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  37. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  38. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  39. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  40. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  41. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  42. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  43. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  44. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  45. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  46. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  48. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  49. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  50. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  51. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  53. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  54. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  55. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  56. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  57. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  58. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  59. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  60. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  61. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  63. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  65. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  66. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  67. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  68. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  69. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  70. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  71. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  72. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  73. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  74. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  76. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  78. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  80. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  82. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  83. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  87. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  88. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  89. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  90. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  94. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  96. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  97. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  98. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  99. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  100. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  101. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  102. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  103. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  104. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  105. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  106. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  107. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  108. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  109. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  110. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  111. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  112. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  113. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  114. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  115. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  116. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  117. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  120. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  121. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  122. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  124. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  125. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  126. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  127. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  140. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  141. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  142. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  143. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  144. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  145. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  146. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  147. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  148. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  149. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  150. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  151. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  152. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  153. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  154. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  155. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  156. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  157. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  158. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  159. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  160. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  161. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  162. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  163. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  164. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  165. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  166. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  167. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  168. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  169. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  170. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  171. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  172. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  173. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  174. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  175. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  176. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  177. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  178. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  179. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  180. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  181. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  182. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  183. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  184. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  185. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  186. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  187. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  188. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  189. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  190. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  191. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  192. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  193. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  194. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  195. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  196. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  197. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  198. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  199. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  200. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  201. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  202. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  203. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  204. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  205. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  206. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  207. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  208. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  209. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  210. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  211. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  212. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  213. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  214. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  215. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  216. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  217. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  218. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  222. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  223. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  224. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  225. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  226. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  227. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  228. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  229. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  230. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  232. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  233. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  234. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  235. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  236. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  237. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  238. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  239. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  240. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  250. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  251. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  252. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  253. package/dist/extensions/question/batch.ts +39 -0
  254. package/dist/extensions/question/dialog-adapter.ts +23 -1
  255. package/dist/extensions/question/index.ts +237 -7
  256. package/dist/extensions/question/schemas.ts +28 -15
  257. package/dist/extensions/question/tests/batch.test.ts +26 -0
  258. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  259. package/dist/extensions/question/tui-adapter.ts +2 -3
  260. package/dist/extensions/question/types.ts +37 -0
  261. package/dist/extensions/question/ui-protocol.ts +3 -3
  262. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  263. package/package.json +1 -1
@@ -0,0 +1,299 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import * as fs from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { createAtomicJsonWriter } from "../../shared/atomic-json.ts";
7
+ import { TEMP_ROOT_DIR } from "../../shared/types.ts";
8
+
9
+ export const SESSION_LEASES_DIR = path.join(TEMP_ROOT_DIR, "session-leases");
10
+
11
+ export interface SessionLeaseRequest {
12
+ sessionFile: string;
13
+ runId: string;
14
+ sourceRunId: string;
15
+ parentSessionId?: string;
16
+ }
17
+
18
+ export interface SessionLeaseOwner {
19
+ version: 1;
20
+ token: string;
21
+ canonicalSessionFile: string;
22
+ runId: string;
23
+ sourceRunId: string;
24
+ parentSessionId?: string;
25
+ pid: number;
26
+ hostname: string;
27
+ processStartIdentity?: string;
28
+ writerState: "none" | "spawning" | "running";
29
+ writerPid?: number;
30
+ writerProcessStartIdentity?: string;
31
+ acquiredAt: string;
32
+ acquiredAtMs: number;
33
+ updatedAtMs: number;
34
+ }
35
+
36
+ export interface SessionLeaseHandle {
37
+ leaseDir: string;
38
+ owner: SessionLeaseOwner;
39
+ updateWriter(writer: { state: "none" | "spawning" } | { state: "running"; pid: number }): void;
40
+ release(): boolean;
41
+ }
42
+
43
+ export type SessionLeaseState =
44
+ | { state: "free"; canonicalSessionFile: string; canonicalSessionId: string }
45
+ | { state: "owned"; canonicalSessionFile: string; canonicalSessionId: string; owner: SessionLeaseOwner }
46
+ | { state: "unreadable"; canonicalSessionFile: string; canonicalSessionId: string };
47
+
48
+ interface SessionLeaseOptions {
49
+ rootDir?: string;
50
+ now?: () => number;
51
+ token?: () => string;
52
+ pid?: number;
53
+ hostname?: string;
54
+ processStartIdentity?: string;
55
+ isProcessAlive?: (pid: number) => boolean | undefined;
56
+ getProcessStartIdentity?: (pid: number) => string | undefined;
57
+ }
58
+
59
+ export class SessionLeaseConflictError extends Error {
60
+ readonly owner?: SessionLeaseOwner;
61
+
62
+ constructor(message: string, owner?: SessionLeaseOwner) {
63
+ super(message);
64
+ this.name = "SessionLeaseConflictError";
65
+ this.owner = owner;
66
+ }
67
+ }
68
+
69
+ function getProcessStartIdentity(pid: number): string | undefined {
70
+ if (process.platform === "linux") {
71
+ try {
72
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf-8");
73
+ const commandEnd = stat.lastIndexOf(")");
74
+ if (commandEnd === -1) return undefined;
75
+ const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
76
+ const startTicks = fields[19];
77
+ return startTicks ? `linux:${startTicks}` : undefined;
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ if (process.platform === "darwin" || process.platform === "freebsd") {
83
+ const result = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf-8" });
84
+ const started = result.status === 0 ? result.stdout.trim() : "";
85
+ return started ? `${process.platform}:${started}` : undefined;
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ function processIsAlive(pid: number): boolean | undefined {
91
+ try {
92
+ process.kill(pid, 0);
93
+ return true;
94
+ } catch (error) {
95
+ const code = (error as NodeJS.ErrnoException).code;
96
+ if (code === "ESRCH") return false;
97
+ if (code === "EPERM") return true;
98
+ return undefined;
99
+ }
100
+ }
101
+
102
+ export function canonicalSessionFilePath(sessionFile: string): string {
103
+ return fs.realpathSync.native(path.resolve(sessionFile));
104
+ }
105
+
106
+ export function canonicalSessionId(sessionFile: string): string {
107
+ const canonical = canonicalSessionFilePath(sessionFile);
108
+ const key = process.platform === "win32" ? canonical.toLowerCase() : canonical;
109
+ return createHash("sha256").update(key).digest("hex");
110
+ }
111
+
112
+ export function sessionLeaseDir(sessionFile: string, rootDir = SESSION_LEASES_DIR): string {
113
+ return path.join(rootDir, canonicalSessionId(sessionFile));
114
+ }
115
+
116
+ export function inspectSessionLease(sessionFile: string, rootDir = SESSION_LEASES_DIR): SessionLeaseState {
117
+ const canonicalSessionFile = canonicalSessionFilePath(sessionFile);
118
+ const canonicalSessionIdValue = canonicalSessionId(canonicalSessionFile);
119
+ const leaseDir = path.join(rootDir, canonicalSessionIdValue);
120
+ if (!fs.existsSync(leaseDir)) return { state: "free", canonicalSessionFile, canonicalSessionId: canonicalSessionIdValue };
121
+ const owner = readLeaseOwner(leaseDir);
122
+ return owner
123
+ ? { state: "owned", canonicalSessionFile, canonicalSessionId: canonicalSessionIdValue, owner }
124
+ : { state: "unreadable", canonicalSessionFile, canonicalSessionId: canonicalSessionIdValue };
125
+ }
126
+
127
+ function parseOwner(value: unknown): SessionLeaseOwner | undefined {
128
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
129
+ const owner = value as Partial<SessionLeaseOwner>;
130
+ if (owner.version !== 1
131
+ || typeof owner.token !== "string"
132
+ || typeof owner.canonicalSessionFile !== "string"
133
+ || typeof owner.runId !== "string"
134
+ || typeof owner.sourceRunId !== "string"
135
+ || typeof owner.pid !== "number"
136
+ || !Number.isInteger(owner.pid)
137
+ || owner.pid <= 0
138
+ || typeof owner.hostname !== "string"
139
+ || (owner.writerState !== "none" && owner.writerState !== "spawning" && owner.writerState !== "running")
140
+ || typeof owner.acquiredAt !== "string"
141
+ || typeof owner.acquiredAtMs !== "number"
142
+ || typeof owner.updatedAtMs !== "number") return undefined;
143
+ if (owner.parentSessionId !== undefined && typeof owner.parentSessionId !== "string") return undefined;
144
+ if (owner.processStartIdentity !== undefined && typeof owner.processStartIdentity !== "string") return undefined;
145
+ if (owner.writerPid !== undefined && (typeof owner.writerPid !== "number" || !Number.isInteger(owner.writerPid) || owner.writerPid <= 0)) return undefined;
146
+ if (owner.writerProcessStartIdentity !== undefined && typeof owner.writerProcessStartIdentity !== "string") return undefined;
147
+ if (owner.writerState === "running" && owner.writerPid === undefined) return undefined;
148
+ if (owner.writerState !== "running" && (owner.writerPid !== undefined || owner.writerProcessStartIdentity !== undefined)) return undefined;
149
+ return owner as SessionLeaseOwner;
150
+ }
151
+
152
+ function readLeaseOwner(leaseDir: string): SessionLeaseOwner | undefined {
153
+ try {
154
+ return parseOwner(JSON.parse(fs.readFileSync(path.join(leaseDir, "owner.json"), "utf-8")));
155
+ } catch {
156
+ return undefined;
157
+ }
158
+ }
159
+
160
+ function conflictMessage(canonicalSessionFile: string, owner: SessionLeaseOwner | undefined): string {
161
+ if (!owner) {
162
+ return `Direct revival of session '${canonicalSessionFile}' is blocked by an existing lease with unreadable owner metadata. Refusing to reclaim it without proof that the owner is stale.`;
163
+ }
164
+ const parent = owner.parentSessionId ? `, parent session '${owner.parentSessionId}'` : "";
165
+ return `Direct revival of session '${canonicalSessionFile}' is already owned by run '${owner.runId}' (source run '${owner.sourceRunId}'${parent}, pid ${owner.pid} on ${owner.hostname}). Wait for that revival to finish or start a separate continuation without reusing this session file.`;
166
+ }
167
+
168
+ function processDemonstrablyGone(
169
+ pid: number,
170
+ startIdentity: string | undefined,
171
+ options: Required<Pick<SessionLeaseOptions, "isProcessAlive" | "getProcessStartIdentity">>,
172
+ ): boolean {
173
+ const alive = options.isProcessAlive(pid);
174
+ if (alive === false) return true;
175
+ if (alive !== true || !startIdentity) return false;
176
+ const currentIdentity = options.getProcessStartIdentity(pid);
177
+ return currentIdentity !== undefined && currentIdentity !== startIdentity;
178
+ }
179
+
180
+ function demonstrablyStale(owner: SessionLeaseOwner, options: Required<Pick<SessionLeaseOptions, "hostname" | "isProcessAlive" | "getProcessStartIdentity">>): boolean {
181
+ if (owner.hostname !== options.hostname) return false;
182
+ if (!processDemonstrablyGone(owner.pid, owner.processStartIdentity, options)) return false;
183
+ if (owner.writerState === "spawning") return false;
184
+ if (owner.writerState === "none") return true;
185
+ return owner.writerPid !== undefined
186
+ && processDemonstrablyGone(owner.writerPid, owner.writerProcessStartIdentity, options);
187
+ }
188
+
189
+ function createLeaseDirectory(leaseDir: string, owner: SessionLeaseOwner): boolean {
190
+ const tempDir = `${leaseDir}.candidate-${owner.token}`;
191
+ fs.mkdirSync(path.dirname(leaseDir), { recursive: true, mode: 0o700 });
192
+ fs.rmSync(tempDir, { recursive: true, force: true });
193
+ fs.mkdirSync(tempDir, { mode: 0o700 });
194
+ try {
195
+ fs.writeFileSync(path.join(tempDir, "owner.json"), JSON.stringify(owner, null, 2), { encoding: "utf-8", mode: 0o600 });
196
+ try {
197
+ fs.renameSync(tempDir, leaseDir);
198
+ return true;
199
+ } catch (error) {
200
+ if (fs.existsSync(leaseDir)) return false;
201
+ throw error;
202
+ }
203
+ } finally {
204
+ fs.rmSync(tempDir, { recursive: true, force: true });
205
+ }
206
+ }
207
+
208
+ export function acquireSessionLease(request: SessionLeaseRequest, options: SessionLeaseOptions = {}): SessionLeaseHandle {
209
+ const canonicalSessionFile = canonicalSessionFilePath(request.sessionFile);
210
+ const rootDir = options.rootDir ?? SESSION_LEASES_DIR;
211
+ const leaseDir = sessionLeaseDir(canonicalSessionFile, rootDir);
212
+ const now = options.now ?? Date.now;
213
+ const pid = options.pid ?? process.pid;
214
+ const hostname = options.hostname ?? os.hostname();
215
+ const getIdentity = options.getProcessStartIdentity ?? getProcessStartIdentity;
216
+ const processStartIdentity = options.processStartIdentity
217
+ ?? getIdentity(pid)
218
+ ?? (pid === process.pid ? `runtime:${Math.round(Date.now() - process.uptime() * 1000)}` : undefined);
219
+ const acquiredAtMs = now();
220
+ const owner: SessionLeaseOwner = {
221
+ version: 1,
222
+ token: options.token?.() ?? randomUUID(),
223
+ canonicalSessionFile,
224
+ runId: request.runId,
225
+ sourceRunId: request.sourceRunId,
226
+ ...(request.parentSessionId ? { parentSessionId: request.parentSessionId } : {}),
227
+ pid,
228
+ hostname,
229
+ ...(processStartIdentity ? { processStartIdentity } : {}),
230
+ writerState: "none",
231
+ acquiredAt: new Date(acquiredAtMs).toISOString(),
232
+ acquiredAtMs,
233
+ updatedAtMs: acquiredAtMs,
234
+ };
235
+ const staleOptions = {
236
+ hostname,
237
+ isProcessAlive: options.isProcessAlive ?? processIsAlive,
238
+ getProcessStartIdentity: getIdentity,
239
+ };
240
+
241
+ for (let attempt = 0; attempt < 4; attempt++) {
242
+ if (createLeaseDirectory(leaseDir, owner)) {
243
+ const writeOwner = createAtomicJsonWriter();
244
+ return {
245
+ leaseDir,
246
+ owner,
247
+ updateWriter(writer) {
248
+ const currentOwner = readLeaseOwner(leaseDir);
249
+ if (!currentOwner || currentOwner.token !== owner.token) {
250
+ throw new Error(`Session revival lease ownership changed for run '${owner.runId}'.`);
251
+ }
252
+ const writerProcessStartIdentity = writer.state === "running" ? getIdentity(writer.pid) : undefined;
253
+ const nextOwner: SessionLeaseOwner = {
254
+ ...owner,
255
+ writerState: writer.state,
256
+ ...(writer.state === "running" ? { writerPid: writer.pid } : {}),
257
+ ...(writerProcessStartIdentity ? { writerProcessStartIdentity } : {}),
258
+ updatedAtMs: now(),
259
+ };
260
+ delete nextOwner.writerPid;
261
+ delete nextOwner.writerProcessStartIdentity;
262
+ if (writer.state === "running") {
263
+ nextOwner.writerPid = writer.pid;
264
+ if (writerProcessStartIdentity) nextOwner.writerProcessStartIdentity = writerProcessStartIdentity;
265
+ }
266
+ writeOwner(path.join(leaseDir, "owner.json"), nextOwner);
267
+ delete owner.writerPid;
268
+ delete owner.writerProcessStartIdentity;
269
+ Object.assign(owner, nextOwner);
270
+ },
271
+ release() {
272
+ const currentOwner = readLeaseOwner(leaseDir);
273
+ if (!currentOwner || currentOwner.token !== owner.token) return false;
274
+ fs.rmSync(leaseDir, { recursive: true, force: true });
275
+ return !fs.existsSync(leaseDir);
276
+ },
277
+ };
278
+ }
279
+
280
+ const existingOwner = readLeaseOwner(leaseDir);
281
+ if (!existingOwner || !demonstrablyStale(existingOwner, staleOptions)) {
282
+ throw new SessionLeaseConflictError(conflictMessage(canonicalSessionFile, existingOwner), existingOwner);
283
+ }
284
+ // The per-owner tombstone is retained. Every contender that observed this
285
+ // stale token targets the same occupied destination, so only the first can
286
+ // rename it and later contenders cannot move a successor lease by mistake.
287
+ const tombstone = `${leaseDir}.stale-${existingOwner.token.replace(/[^A-Za-z0-9._-]/g, "-")}`;
288
+ try {
289
+ fs.renameSync(leaseDir, tombstone);
290
+ } catch (error) {
291
+ const code = (error as NodeJS.ErrnoException).code;
292
+ if (code === "ENOENT" || fs.existsSync(tombstone)) continue;
293
+ throw error;
294
+ }
295
+ }
296
+
297
+ const existingOwner = readLeaseOwner(leaseDir);
298
+ throw new SessionLeaseConflictError(conflictMessage(canonicalSessionFile, existingOwner), existingOwner);
299
+ }
@@ -1,6 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import type { Message } from "@earendil-works/pi-ai";
3
4
  import type { OutputMode, SavedOutputReference } from "../../shared/types.ts";
5
+ import { hasMutationToolCapability } from "./completion-guard.ts";
4
6
 
5
7
  export interface SingleOutputSnapshot {
6
8
  exists: boolean;
@@ -8,6 +10,47 @@ export interface SingleOutputSnapshot {
8
10
  size?: number;
9
11
  }
10
12
 
13
+ /**
14
+ * Content the child itself sent to the configured output path, taken from its
15
+ * last `write` tool call whose tool result reports success. Unlike reading the
16
+ * path from disk, this cannot be polluted by a sibling run writing the same
17
+ * path (#420); requiring the successful tool result keeps failed, cancelled,
18
+ * or unanswered write calls from counting as authored output. Returns
19
+ * undefined when no such write exists (e.g. bash or edit-based construction),
20
+ * in which case callers must not assume file authorship.
21
+ */
22
+ export function extractChildWrittenOutput(
23
+ messages: Message[] | undefined,
24
+ outputPath: string | undefined,
25
+ cwd?: string,
26
+ ): string | undefined {
27
+ if (!messages?.length || !outputPath) return undefined;
28
+ const resolvedTarget = path.resolve(cwd ?? ".", outputPath);
29
+ const comparableTarget = process.platform === "win32" ? resolvedTarget.toLowerCase() : resolvedTarget;
30
+ const successfulCallIds = new Set<string>();
31
+ for (const message of messages) {
32
+ if (message.role === "toolResult" && message.isError === false && typeof message.toolCallId === "string") {
33
+ successfulCallIds.add(message.toolCallId);
34
+ }
35
+ }
36
+ let content: string | undefined;
37
+ for (const message of messages) {
38
+ if (message.role !== "assistant") continue;
39
+ for (const part of message.content) {
40
+ if (part.type !== "toolCall" || part.name !== "write" || !successfulCallIds.has(part.id)) continue;
41
+ const args = typeof part.arguments === "object" && part.arguments !== null && !Array.isArray(part.arguments)
42
+ ? part.arguments as Record<string, unknown>
43
+ : {};
44
+ if (typeof args.path !== "string" || typeof args.content !== "string") continue;
45
+ const resolvedWritePath = path.resolve(cwd ?? ".", args.path);
46
+ const comparableWritePath = process.platform === "win32" ? resolvedWritePath.toLowerCase() : resolvedWritePath;
47
+ if (comparableWritePath !== comparableTarget) continue;
48
+ content = args.content;
49
+ }
50
+ }
51
+ return content;
52
+ }
53
+
11
54
  export function normalizeSingleOutputOverride(
12
55
  output: string | boolean | undefined,
13
56
  defaultOutput: string | undefined,
@@ -33,22 +76,34 @@ export function resolveSingleOutputPath(
33
76
  return path.resolve(baseCwd, output);
34
77
  }
35
78
 
36
- function formatOutputPathInstruction(outputPath: string): string {
79
+ interface OutputInstructionCapabilities {
80
+ tools?: string[];
81
+ mcpDirectTools?: string[];
82
+ }
83
+
84
+ function formatOutputPathInstruction(outputPath: string, capabilities?: OutputInstructionCapabilities): string {
85
+ const delivery = !capabilities || hasMutationToolCapability(capabilities.tools, capabilities.mcpDirectTools)
86
+ ? `Write your findings to exactly this path: ${outputPath}`
87
+ : [
88
+ "Return the complete artifact in your final response.",
89
+ `The runtime will persist it to exactly this path: ${outputPath}`,
90
+ "Do not call contact_supervisor merely because no write-capable tool is available.",
91
+ ].join("\n");
37
92
  return [
38
- `Write your findings to exactly this path: ${outputPath}`,
93
+ delivery,
39
94
  "This path is authoritative for this run.",
40
95
  "Ignore any other output filename or output path mentioned elsewhere, including output destinations in the base agent prompt, system prompt, or task instructions.",
41
96
  ].join("\n");
42
97
  }
43
98
 
44
- export function injectSingleOutputInstruction(task: string, outputPath: string | undefined): string {
99
+ export function injectSingleOutputInstruction(task: string, outputPath: string | undefined, capabilities?: OutputInstructionCapabilities): string {
45
100
  if (!outputPath) return task;
46
- return `${task}\n\n---\n**Output:**\n${formatOutputPathInstruction(outputPath)}`;
101
+ return `${task}\n\n---\n**Output:**\n${formatOutputPathInstruction(outputPath, capabilities)}`;
47
102
  }
48
103
 
49
- export function injectOutputPathSystemPrompt(systemPrompt: string, outputPath: string | undefined): string {
104
+ export function injectOutputPathSystemPrompt(systemPrompt: string, outputPath: string | undefined, capabilities?: OutputInstructionCapabilities): string {
50
105
  if (!outputPath) return systemPrompt;
51
- const instruction = `Runtime output path override:\n${formatOutputPathInstruction(outputPath)}`;
106
+ const instruction = `Runtime output path override:\n${formatOutputPathInstruction(outputPath, capabilities)}`;
52
107
  return systemPrompt ? `${systemPrompt}\n\n${instruction}` : instruction;
53
108
  }
54
109
 
@@ -0,0 +1,128 @@
1
+ import {
2
+ resolveMaxSubagentSpawnsPerSession,
3
+ type ExtensionConfig,
4
+ type SpawnBudgetSnapshot,
5
+ type SubagentState,
6
+ } from "../../shared/types.ts";
7
+
8
+ const MAX_GRANT_HISTORY = 20;
9
+
10
+ function sessionState(state: SubagentState, config: ExtensionConfig, sessionId: string | null) {
11
+ let counters = state.subagentSpawns;
12
+ if (!counters || counters.sessionId !== sessionId) {
13
+ counters = {
14
+ sessionId,
15
+ count: 0,
16
+ configuredLimit: resolveMaxSubagentSpawnsPerSession(config.maxSubagentSpawnsPerSession) ?? null,
17
+ granted: 0,
18
+ grantHistory: [],
19
+ };
20
+ state.subagentSpawns = counters;
21
+ }
22
+ if (counters.configuredLimit === undefined) {
23
+ counters.configuredLimit = resolveMaxSubagentSpawnsPerSession(config.maxSubagentSpawnsPerSession) ?? null;
24
+ }
25
+ counters.granted ??= 0;
26
+ counters.grantHistory ??= [];
27
+ return counters;
28
+ }
29
+
30
+ export function getSpawnBudgetSnapshot(
31
+ state: SubagentState,
32
+ config: ExtensionConfig,
33
+ sessionId: string | null = state.currentSessionId,
34
+ ): SpawnBudgetSnapshot {
35
+ const counters = sessionState(state, config, sessionId);
36
+ const configuredLimit = counters.configuredLimit ?? null;
37
+ const granted = configuredLimit === null ? 0 : counters.granted ?? 0;
38
+ const limit = configuredLimit === null ? null : configuredLimit + granted;
39
+ return {
40
+ used: counters.count,
41
+ configuredLimit,
42
+ granted,
43
+ limit,
44
+ remaining: limit === null ? null : Math.max(0, limit - counters.count),
45
+ grantRemaining: configuredLimit === null ? null : Math.max(0, configuredLimit - granted),
46
+ grantHistory: [...(counters.grantHistory ?? [])],
47
+ };
48
+ }
49
+
50
+ export function formatSpawnBudgetSummary(snapshot: SpawnBudgetSnapshot): string {
51
+ if (snapshot.limit === null) return "unlimited";
52
+ return `${snapshot.used}/${snapshot.limit} used, ${snapshot.remaining} remaining (configured ${snapshot.configuredLimit}; granted ${snapshot.granted}; grant allowance ${snapshot.grantRemaining})`;
53
+ }
54
+
55
+ export function formatSpawnBudget(snapshot: SpawnBudgetSnapshot): string {
56
+ return `Spawn budget: ${formatSpawnBudgetSummary(snapshot)}`;
57
+ }
58
+
59
+ export function preflightSpawnBudget(
60
+ state: SubagentState,
61
+ config: ExtensionConfig,
62
+ sessionId: string | null,
63
+ requested: number,
64
+ ): { snapshot: SpawnBudgetSnapshot; error?: string } {
65
+ const snapshot = getSpawnBudgetSnapshot(state, config, sessionId);
66
+ if (requested <= 0 || snapshot.limit === null || requested <= (snapshot.remaining ?? 0)) return { snapshot };
67
+ return {
68
+ snapshot,
69
+ error: `Subagent spawn limit reached for this session (${snapshot.used}/${snapshot.limit} used, ${requested} requested). ${snapshot.remaining} remaining; the declared run cannot fit, so no children were started. Grant budget explicitly from the root interactive session or start a new session.`,
70
+ };
71
+ }
72
+
73
+ export function reserveSpawnBudget(
74
+ state: SubagentState,
75
+ config: ExtensionConfig,
76
+ sessionId: string | null,
77
+ requested: number,
78
+ ): { snapshot: SpawnBudgetSnapshot; error?: string } {
79
+ const checked = preflightSpawnBudget(state, config, sessionId, requested);
80
+ if (checked.error || requested <= 0 || checked.snapshot.limit === null) return checked;
81
+ state.subagentSpawns!.count += requested;
82
+ return { snapshot: getSpawnBudgetSnapshot(state, config, sessionId) };
83
+ }
84
+
85
+ export function preflightSpawnBudgetGrant(
86
+ state: SubagentState,
87
+ config: ExtensionConfig,
88
+ sessionId: string,
89
+ additional: number,
90
+ ): { snapshot: SpawnBudgetSnapshot; error?: string } {
91
+ const snapshot = getSpawnBudgetSnapshot(state, config, sessionId);
92
+ if (!Number.isInteger(additional) || additional <= 0) {
93
+ return { snapshot, error: "action='grant-spawn-budget' requires additional to be a positive integer." };
94
+ }
95
+ if (snapshot.configuredLimit === null || snapshot.limit === null) {
96
+ return { snapshot, error: "The current session has no configured spawn cap, so it does not need a budget grant." };
97
+ }
98
+ if (additional > (snapshot.grantRemaining ?? 0)) {
99
+ return {
100
+ snapshot,
101
+ error: `Spawn budget grant rejected: ${additional} requested but only ${snapshot.grantRemaining} of the original configured limit remains grantable.`,
102
+ };
103
+ }
104
+ return { snapshot };
105
+ }
106
+
107
+ export function grantSpawnBudget(
108
+ state: SubagentState,
109
+ config: ExtensionConfig,
110
+ sessionId: string,
111
+ additional: number,
112
+ now = Date.now(),
113
+ ): { snapshot: SpawnBudgetSnapshot; error?: string } {
114
+ const checked = preflightSpawnBudgetGrant(state, config, sessionId, additional);
115
+ if (checked.error) return checked;
116
+ const before = checked.snapshot;
117
+ if (before.limit === null) {
118
+ return { snapshot: before, error: "The current session has no configured spawn cap, so it does not need a budget grant." };
119
+ }
120
+ const counters = state.subagentSpawns!;
121
+ counters.granted = (counters.granted ?? 0) + additional;
122
+ const limit = before.limit + additional;
123
+ counters.grantHistory = [
124
+ ...(counters.grantHistory ?? []),
125
+ { sessionId, amount: additional, grantedAt: now, previousLimit: before.limit, limit },
126
+ ].slice(-MAX_GRANT_HISTORY);
127
+ return { snapshot: getSpawnBudgetSnapshot(state, config, sessionId) };
128
+ }