@selesai/code 0.5.19 → 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 (265) 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-intercom/broker/spawn.test.ts +31 -0
  4. package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
  5. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  6. package/dist/extensions/pi-subagents/README.md +602 -220
  7. package/dist/extensions/pi-subagents/banner.png +0 -0
  8. package/dist/extensions/pi-subagents/index.ts +1 -0
  9. package/dist/extensions/pi-subagents/install.mjs +3 -3
  10. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  11. package/dist/extensions/pi-subagents/package.json +26 -14
  12. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  13. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  14. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  15. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  16. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  17. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  18. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  20. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  21. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  22. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  23. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  24. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  25. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  26. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  27. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  28. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  29. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  30. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  31. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  32. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  33. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  34. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  35. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  36. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  37. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  38. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  39. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  40. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  41. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  42. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  43. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  44. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  45. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  46. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  47. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  48. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  49. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  50. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  51. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  52. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  53. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  54. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  55. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  56. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  57. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  58. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  59. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  61. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  63. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  65. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  66. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  67. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  68. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  69. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  70. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  71. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  72. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  73. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  74. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  76. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  78. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  80. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  84. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  86. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  88. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  89. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  90. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  92. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  95. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  96. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  97. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  98. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  99. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  100. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  101. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  102. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  103. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  104. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  105. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  106. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  107. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  108. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  109. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  110. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  111. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  112. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  113. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  114. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  115. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  116. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  117. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  118. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  119. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  120. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  121. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  122. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  124. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  125. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  126. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  127. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  140. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  141. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  142. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  143. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  144. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  145. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  146. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  147. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  148. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  149. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  150. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  151. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  152. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  153. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  154. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  155. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  156. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  157. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  158. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  159. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  160. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  161. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  162. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  163. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  164. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  165. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  166. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  167. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  168. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  169. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  170. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  171. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  172. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  173. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  174. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  175. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  176. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  177. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  178. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  179. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  180. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  181. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  182. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  183. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  184. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  185. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  186. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  187. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  188. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  189. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  190. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  191. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  192. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  193. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  194. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  195. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  196. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  197. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  198. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  199. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  200. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  201. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  202. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  203. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  204. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  205. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  206. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  207. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  208. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  209. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  210. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  211. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  212. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  213. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  214. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  215. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  216. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  217. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  218. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  219. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  220. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  221. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  222. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  223. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  224. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  225. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  226. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  227. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  228. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  229. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  230. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  232. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  233. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  234. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  235. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  236. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  237. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  238. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  239. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  240. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  250. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  251. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  252. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  253. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  254. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  255. package/dist/extensions/question/batch.ts +39 -0
  256. package/dist/extensions/question/dialog-adapter.ts +23 -1
  257. package/dist/extensions/question/index.ts +237 -7
  258. package/dist/extensions/question/schemas.ts +28 -15
  259. package/dist/extensions/question/tests/batch.test.ts +26 -0
  260. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  261. package/dist/extensions/question/tui-adapter.ts +2 -3
  262. package/dist/extensions/question/types.ts +37 -0
  263. package/dist/extensions/question/ui-protocol.ts +3 -3
  264. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  265. package/package.json +2 -1
@@ -0,0 +1,204 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import test from "node:test";
6
+ import {
7
+ finalizeProcessTerminal,
8
+ processTerminalPath,
9
+ readProcessTerminal,
10
+ sanitizeProcessTerminal,
11
+ writeProcessTerminalCandidate,
12
+ } from "../../src/runs/background/process-terminal.ts";
13
+
14
+ test("process-terminal proof requires the matching runner instance and writer close records", () => {
15
+ const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
16
+ try {
17
+ writeProcessTerminalCandidate(asyncDir, {
18
+ version: 1,
19
+ runId: "run-1",
20
+ runnerProcessInstanceId: "runner-1",
21
+ expectedWriters: { "0": 1 },
22
+ writers: {
23
+ "0": [{
24
+ processInstanceId: "writer-1",
25
+ kind: "pi-writer",
26
+ attempt: 0,
27
+ closeObservedAt: 10,
28
+ exitCode: 0,
29
+ signal: null,
30
+ }],
31
+ },
32
+ });
33
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ runId: "run-1", state: "complete", lifecycleArtifactVersion: 3, steps: [{ agent: "worker", status: "complete" }] }));
34
+ fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
35
+
36
+ const mismatch = finalizeProcessTerminal(asyncDir, "run-1", {
37
+ processInstanceId: "runner-2",
38
+ closeObservedAt: 20,
39
+ exitCode: 0,
40
+ signal: null,
41
+ });
42
+ assert.equal(mismatch.state, "unknown");
43
+ assert.equal(mismatch.reason, "runner-instance-mismatch");
44
+
45
+ fs.rmSync(processTerminalPath(asyncDir), { force: true });
46
+ const observed = finalizeProcessTerminal(asyncDir, "run-1", {
47
+ processInstanceId: "runner-1",
48
+ closeObservedAt: 30,
49
+ exitCode: 0,
50
+ signal: null,
51
+ });
52
+ assert.equal(observed.state, "observed");
53
+ assert.equal(observed.instances?.length, 2);
54
+ assert.equal(JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf8")).processTerminal.state, "observed");
55
+ } finally {
56
+ fs.rmSync(asyncDir, { recursive: true, force: true });
57
+ }
58
+ });
59
+
60
+ test("process-terminal rejects missing writer close evidence", () => {
61
+ const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
62
+ try {
63
+ writeProcessTerminalCandidate(asyncDir, {
64
+ version: 1,
65
+ runId: "run-missing-writer",
66
+ runnerProcessInstanceId: "runner-missing-writer",
67
+ expectedWriters: { "0": 1 },
68
+ writers: { "0": [] },
69
+ });
70
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ runId: "run-missing-writer", state: "complete", lifecycleArtifactVersion: 3, steps: [{ agent: "worker", status: "complete" }] }));
71
+ fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
72
+ const proof = finalizeProcessTerminal(asyncDir, "run-missing-writer", {
73
+ processInstanceId: "runner-missing-writer",
74
+ closeObservedAt: 40,
75
+ exitCode: 1,
76
+ signal: null,
77
+ });
78
+ assert.equal(proof.state, "unknown");
79
+ assert.equal(proof.reason, "writer-close-unverified");
80
+ } finally {
81
+ fs.rmSync(asyncDir, { recursive: true, force: true });
82
+ }
83
+ });
84
+
85
+ test("malformed process-terminal sidecars project unknown instead of throwing", () => {
86
+ const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
87
+ try {
88
+ fs.writeFileSync(processTerminalPath(asyncDir), JSON.stringify({ version: 1, state: "bogus" }));
89
+ const proof = readProcessTerminal(asyncDir, { runId: "malformed-run", runnerProcessInstanceId: "malformed-runner" });
90
+ assert.equal(proof?.state, "unknown");
91
+ assert.equal(proof?.reason, "proof-write-failed");
92
+ assert.match(proof?.diagnostic ?? "", /Invalid process-terminal proof/);
93
+ fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
94
+ const finalized = finalizeProcessTerminal(asyncDir, "malformed-run", {
95
+ processInstanceId: "malformed-runner",
96
+ closeObservedAt: 10,
97
+ exitCode: 1,
98
+ signal: null,
99
+ });
100
+ assert.equal(finalized.state, "unknown");
101
+ } finally {
102
+ fs.rmSync(asyncDir, { recursive: true, force: true });
103
+ }
104
+ });
105
+
106
+ test("process-terminal preserves stopped non-resumability and requires lease release acknowledgement", () => {
107
+ const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
108
+ try {
109
+ writeProcessTerminalCandidate(asyncDir, {
110
+ version: 1,
111
+ runId: "stopped-run",
112
+ runnerProcessInstanceId: "stopped-runner",
113
+ expectedWriters: { "0": 1 },
114
+ writers: {
115
+ "0": [{ processInstanceId: "stopped-writer", kind: "pi-writer", attempt: 0, closeObservedAt: 10, exitCode: 0, signal: null }],
116
+ },
117
+ revivalLeaseToken: "lease-token",
118
+ revivalLeaseReleaseAcknowledged: false,
119
+ });
120
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ runId: "stopped-run", state: "stopped", lifecycleArtifactVersion: 3, steps: [{ agent: "worker", status: "stopped" }] }));
121
+ fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
122
+ const unverified = finalizeProcessTerminal(asyncDir, "stopped-run", { processInstanceId: "stopped-runner", closeObservedAt: 20, exitCode: 0, signal: null });
123
+ assert.equal(unverified.state, "unknown");
124
+ assert.equal(unverified.reason, "canonical-session-release-unverified");
125
+
126
+ fs.rmSync(processTerminalPath(asyncDir), { force: true });
127
+ const candidatePath = path.join(asyncDir, "process-terminal-candidate.json");
128
+ const candidate = JSON.parse(fs.readFileSync(candidatePath, "utf8"));
129
+ candidate.revivalLeaseReleaseAcknowledged = true;
130
+ delete candidate.revivalLeaseToken;
131
+ fs.writeFileSync(candidatePath, JSON.stringify(candidate));
132
+ const observed = finalizeProcessTerminal(asyncDir, "stopped-run", { processInstanceId: "stopped-runner", closeObservedAt: 30, exitCode: 0, signal: null });
133
+ assert.equal(observed.state, "observed");
134
+ assert.equal(observed.resumeDisposition, "non-resumable");
135
+ } finally {
136
+ fs.rmSync(asyncDir, { recursive: true, force: true });
137
+ }
138
+ });
139
+
140
+
141
+ test("process-terminal rejects cross-run observed sidecars and inconsistent expected writer maps", () => {
142
+ const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
143
+ try {
144
+ fs.writeFileSync(processTerminalPath(asyncDir), JSON.stringify({
145
+ version: 1,
146
+ state: "observed",
147
+ runId: "wrong-run",
148
+ runnerProcessInstanceId: "runner-1",
149
+ observedAt: 10,
150
+ instances: [{ processInstanceId: "runner-1", kind: "runner", closeObservedAt: 10, exitCode: 0, signal: null }],
151
+ }));
152
+ const crossRun = finalizeProcessTerminal(asyncDir, "actual-run", { processInstanceId: "runner-1", closeObservedAt: 20, exitCode: 0, signal: null });
153
+ assert.equal(crossRun.state, "unknown");
154
+ assert.equal(crossRun.reason, "proof-write-failed");
155
+ assert.match(crossRun.diagnostic ?? "", /wrong-run|expected/);
156
+
157
+ fs.rmSync(processTerminalPath(asyncDir), { force: true });
158
+ writeProcessTerminalCandidate(asyncDir, {
159
+ version: 1,
160
+ runId: "actual-run",
161
+ runnerProcessInstanceId: "runner-1",
162
+ expectedWriters: { "0": 0 },
163
+ writers: {
164
+ "0": [{ processInstanceId: "unexpected-writer", kind: "pi-writer", attempt: 0, closeObservedAt: 10, exitCode: 0, signal: null }],
165
+ },
166
+ });
167
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ runId: "actual-run", state: "complete", lifecycleArtifactVersion: 3, steps: [{ agent: "worker", status: "complete" }] }));
168
+ fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
169
+ const inconsistent = finalizeProcessTerminal(asyncDir, "actual-run", { processInstanceId: "runner-1", closeObservedAt: 30, exitCode: 0, signal: null });
170
+ assert.equal(inconsistent.state, "unknown");
171
+ assert.equal(inconsistent.reason, "writer-close-unverified");
172
+ } finally {
173
+ fs.rmSync(asyncDir, { recursive: true, force: true });
174
+ }
175
+ });
176
+
177
+ test("process-terminal sanitizes malformed status fallback proofs", () => {
178
+ const malformed = readProcessTerminal("/no/such/dir", { runId: "run-fallback", runnerProcessInstanceId: "runner-fallback" });
179
+ assert.equal(malformed, undefined);
180
+ const sanitized = sanitizeProcessTerminal({ version: 1, state: "bogus", runId: "run-fallback", runnerProcessInstanceId: "runner-fallback" }, { runId: "run-fallback", runnerProcessInstanceId: "runner-fallback" }, "status.json");
181
+ assert.equal(sanitized?.state, "unknown");
182
+ assert.equal(sanitized?.reason, "proof-write-failed");
183
+ });
184
+
185
+ test("process-terminal reports unknown when the runner candidate is unavailable", () => {
186
+ const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
187
+ try {
188
+ const proof = finalizeProcessTerminal(asyncDir, "run-2", {
189
+ processInstanceId: "runner-2",
190
+ closeObservedAt: 40,
191
+ exitCode: 1,
192
+ signal: null,
193
+ });
194
+ assert.deepEqual(proof, {
195
+ version: 1,
196
+ state: "unknown",
197
+ runId: "run-2",
198
+ runnerProcessInstanceId: "runner-2",
199
+ reason: "runner-candidate-missing",
200
+ });
201
+ } finally {
202
+ fs.rmSync(asyncDir, { recursive: true, force: true });
203
+ }
204
+ });
@@ -4,7 +4,6 @@ import {
4
4
  checkSubagentDepth,
5
5
  getSubagentDepthEnv,
6
6
  DEFAULT_SUBAGENT_MAX_DEPTH,
7
- DEFAULT_MAX_SUBAGENT_SPAWNS_PER_SESSION,
8
7
  normalizeMaxSubagentDepth,
9
8
  normalizeMaxSubagentSpawnsPerSession,
10
9
  resolveMaxSubagentSpawnsPerSession,
@@ -39,12 +38,6 @@ describe("DEFAULT_SUBAGENT_MAX_DEPTH", () => {
39
38
  });
40
39
  });
41
40
 
42
- describe("DEFAULT_MAX_SUBAGENT_SPAWNS_PER_SESSION", () => {
43
- it("is 40", () => {
44
- assert.equal(DEFAULT_MAX_SUBAGENT_SPAWNS_PER_SESSION, 40);
45
- });
46
- });
47
-
48
41
  describe("normalizeMaxSubagentDepth", () => {
49
42
  it("accepts integers >= 0", () => {
50
43
  assert.equal(normalizeMaxSubagentDepth(0), 0);
@@ -74,20 +67,35 @@ describe("normalizeMaxSubagentSpawnsPerSession", () => {
74
67
  });
75
68
 
76
69
  describe("resolveMaxSubagentSpawnsPerSession", () => {
77
- it("uses env when present", () => {
70
+ it("uses positive env values as opt-in caps", () => {
78
71
  process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION = "5";
79
72
  assert.equal(resolveMaxSubagentSpawnsPerSession(1), 5);
80
73
  });
81
74
 
82
- it("falls back to config when env is absent", () => {
75
+ it("falls back to a positive config cap when env is absent", () => {
83
76
  delete process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION;
84
77
  assert.equal(resolveMaxSubagentSpawnsPerSession(7), 7);
85
78
  });
86
79
 
87
- it("falls back to default when neither env nor config is valid", () => {
80
+ it("ignores invalid env values and falls back to config", () => {
81
+ for (const value of ["garbage", "-1", "1.5"]) {
82
+ process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION = value;
83
+ assert.equal(resolveMaxSubagentSpawnsPerSession(7), 7);
84
+ assert.equal(resolveMaxSubagentSpawnsPerSession(undefined), undefined);
85
+ }
86
+ });
87
+
88
+ it("is unlimited by default", () => {
89
+ delete process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION;
90
+ assert.equal(resolveMaxSubagentSpawnsPerSession(undefined), undefined);
91
+ assert.equal(resolveMaxSubagentSpawnsPerSession(-1), undefined);
92
+ });
93
+
94
+ it("treats zero as an explicit unlimited override", () => {
95
+ process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION = "0";
96
+ assert.equal(resolveMaxSubagentSpawnsPerSession(7), undefined);
88
97
  delete process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION;
89
- assert.equal(resolveMaxSubagentSpawnsPerSession(undefined), 40);
90
- assert.equal(resolveMaxSubagentSpawnsPerSession(-1), 40);
98
+ assert.equal(resolveMaxSubagentSpawnsPerSession(0), undefined);
91
99
  });
92
100
  });
93
101
 
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { describe, it } from "node:test";
6
- import { timeoutRequestPath } from "../../src/runs/background/control-channel.ts";
6
+ import { stopRequestPath } from "../../src/runs/background/control-channel.ts";
7
7
  import {
8
8
  SUBAGENT_RPC_PROTOCOL_VERSION,
9
9
  SUBAGENT_RPC_READY_EVENT,
@@ -83,6 +83,14 @@ describe("subagent extension RPC bridge", () => {
83
83
  assert.equal(reply.success, true);
84
84
  assert.equal(reply.method, "ping");
85
85
  assert.equal((reply as { data: { version?: number } }).data.version, SUBAGENT_RPC_PROTOCOL_VERSION);
86
+ assert.equal(
87
+ (reply as { data: { events?: { asyncComplete?: string } } }).data.events?.asyncComplete,
88
+ "subagent:async-complete",
89
+ );
90
+ assert.equal(
91
+ (reply as { data: { capabilities?: { nonRecoveringSteer?: boolean } } }).data.capabilities?.nonRecoveringSteer,
92
+ true,
93
+ );
86
94
 
87
95
  bridge.dispose();
88
96
  });
@@ -160,6 +168,31 @@ describe("subagent extension RPC bridge", () => {
160
168
  bridge.dispose();
161
169
  });
162
170
 
171
+ it("preserves schema-valid static parallel extension fields", async () => {
172
+ const events = new FakeEvents();
173
+ let executedParams: any;
174
+ const bridge = registerSubagentRpcBridge({
175
+ events,
176
+ getContext: () => ctx(),
177
+ execute: async (_id, params) => {
178
+ executedParams = params;
179
+ return {
180
+ content: [{ type: "text", text: "Async: worker [run-1]" }],
181
+ details: { mode: "chain", results: [], asyncId: "run-1", asyncDir: "/tmp/run-1" },
182
+ } as any;
183
+ },
184
+ });
185
+
186
+ const reply = await request(events, "spawn-chain-extra", "spawn", {
187
+ chain: [{ parallel: [{ agent: "worker", extensionField: true }] }],
188
+ });
189
+
190
+ assert.equal(reply.success, true);
191
+ assert.equal(executedParams.chain[0].parallel[0].extensionField, true);
192
+
193
+ bridge.dispose();
194
+ });
195
+
163
196
  it("rejects foreground or management spawn requests before executor dispatch", async () => {
164
197
  const events = new FakeEvents();
165
198
  let executeCalls = 0;
@@ -185,6 +218,86 @@ describe("subagent extension RPC bridge", () => {
185
218
  bridge.dispose();
186
219
  });
187
220
 
221
+ it("delegates acknowledged steering through the existing async action", async () => {
222
+ const events = new FakeEvents();
223
+ let executedParams: unknown;
224
+ const bridge = registerSubagentRpcBridge({
225
+ events,
226
+ getContext: () => ctx(),
227
+ execute: async (_id, params) => {
228
+ executedParams = params;
229
+ return {
230
+ content: [{ type: "text", text: "Steering delivered." }],
231
+ details: { mode: "management", results: [] },
232
+ } as any;
233
+ },
234
+ });
235
+
236
+ const reply = await request(events, "steer-1", "steer", {
237
+ id: "abc123",
238
+ index: 0,
239
+ message: " Focus on the failing test. ",
240
+ });
241
+
242
+ assert.equal(reply.success, true);
243
+ assert.deepEqual(executedParams, {
244
+ action: "steer",
245
+ id: "abc123",
246
+ index: 0,
247
+ message: "Focus on the failing test.",
248
+ steeringRecovery: false,
249
+ });
250
+ assert.equal((reply as { data: { text?: string } }).data.text, "Steering delivered.");
251
+
252
+ bridge.dispose();
253
+ });
254
+
255
+ it("rejects targetless RPC steering before executor dispatch", async () => {
256
+ const events = new FakeEvents();
257
+ let executeCalls = 0;
258
+ const bridge = registerSubagentRpcBridge({
259
+ events,
260
+ getContext: () => ctx(),
261
+ execute: async () => {
262
+ executeCalls++;
263
+ return { content: [], details: { mode: "management", results: [] } } as any;
264
+ },
265
+ });
266
+
267
+ const reply = await request(events, "steer-no-target", "steer", {
268
+ message: "keep going",
269
+ });
270
+
271
+ assert.equal(reply.success, false);
272
+ assert.equal((reply as { error: { code: string } }).error.code, "invalid_params");
273
+ assert.match((reply as { error: { message: string } }).error.message, /requires id, runId, or dir/);
274
+ assert.equal(executeCalls, 0);
275
+ bridge.dispose();
276
+ });
277
+
278
+ it("rejects empty RPC steering before executor dispatch", async () => {
279
+ const events = new FakeEvents();
280
+ let executeCalls = 0;
281
+ const bridge = registerSubagentRpcBridge({
282
+ events,
283
+ getContext: () => ctx(),
284
+ execute: async () => {
285
+ executeCalls++;
286
+ return { content: [], details: { mode: "management", results: [] } } as any;
287
+ },
288
+ });
289
+
290
+ const reply = await request(events, "steer-empty", "steer", {
291
+ id: "abc123",
292
+ message: " ",
293
+ });
294
+
295
+ assert.equal(reply.success, false);
296
+ assert.equal((reply as { error: { code: string } }).error.code, "invalid_params");
297
+ assert.equal(executeCalls, 0);
298
+ bridge.dispose();
299
+ });
300
+
188
301
  it("delegates interrupt through the existing executor action", async () => {
189
302
  const events = new FakeEvents();
190
303
  let executedParams: unknown;
@@ -205,7 +318,7 @@ describe("subagent extension RPC bridge", () => {
205
318
  bridge.dispose();
206
319
  });
207
320
 
208
- it("uses the existing async timeout control path for stop", async () => {
321
+ it("uses the async stop control path for stop", async () => {
209
322
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-rpc-stop-"));
210
323
  try {
211
324
  const events = new FakeEvents();
@@ -238,7 +351,7 @@ describe("subagent extension RPC bridge", () => {
238
351
  assert.equal(reply.success, true);
239
352
  assert.equal((reply as { data: { runId?: string; state?: string } }).data.runId, "run-stop");
240
353
  assert.equal((reply as { data: { state?: string } }).data.state, "stopping");
241
- assert.equal(fs.existsSync(timeoutRequestPath(asyncDir)), true);
354
+ assert.equal(fs.existsSync(stopRequestPath(asyncDir)), true);
242
355
 
243
356
  bridge.dispose();
244
357
  } finally {
@@ -283,7 +396,7 @@ describe("subagent extension RPC bridge", () => {
283
396
  assert.equal(reply.success, false);
284
397
  assert.equal((reply as { error: { code: string; message: string } }).error.code, "not_found");
285
398
  assert.match((reply as { error: { message: string } }).error.message, /active session/);
286
- assert.equal(fs.existsSync(timeoutRequestPath(asyncDir)), false);
399
+ assert.equal(fs.existsSync(stopRequestPath(asyncDir)), false);
287
400
  assert.equal(killCalls, 0);
288
401
 
289
402
  bridge.dispose();
@@ -308,7 +308,7 @@ describe("async run status inspection", () => {
308
308
 
309
309
  const text = textContent(result);
310
310
  assert.equal(result.isError, undefined);
311
- assert.match(text, /Subagent fleet: 2 active/);
311
+ assert.match(text, /Subagent fleet: 2 tracked/);
312
312
  assert.match(text, /Foreground runs:/);
313
313
  assert.match(text, /fg-run \| running \| scout/);
314
314
  assert.match(text, /Async runs:/);
@@ -456,9 +456,8 @@ describe("async run status inspection", () => {
456
456
  startedAt: 100,
457
457
  lastUpdate: 200,
458
458
  currentStep: 0,
459
- steerCount: 2,
460
- lastSteerAt: 150,
461
- steps: [{ agent: "worker", status: "running", startedAt: 100, steerCount: 2, lastSteerAt: 150 }],
459
+ steering: { requested: 2, scheduled: 1, pending: 1, delivered: 2, failed: 0, recovered: 1, lastRequestedAt: 150, lastDeliveredAt: 150, recent: [{ id: "request", requestedAt: 150, message: "guidance", targets: [{ index: 0, state: "recovered", recoveredAt: 180, lateDeliveredAt: 190 }] }] },
460
+ steps: [{ agent: "worker", status: "running", startedAt: 100, steering: { requested: 2, scheduled: 1, pending: 1, delivered: 2, failed: 0, recovered: 1, lastRequestedAt: 150, lastDeliveredAt: 150, recent: [{ id: "request", requestedAt: 150, message: "guidance", targets: [{ index: 0, state: "recovered", recoveredAt: 180, lateDeliveredAt: 190 }] }] } }],
462
461
  }, null, 2), "utf-8");
463
462
 
464
463
  const exact = inspectSubagentStatus({ id: "run-steered" }, {
@@ -469,8 +468,8 @@ describe("async run status inspection", () => {
469
468
  });
470
469
  const exactText = textContent(exact);
471
470
  assert.equal(exact.isError, undefined);
472
- assert.match(exactText, /Steering: 2 steers, last 1970-01-01T00:00:00\.150Z/);
473
- assert.match(exactText, /Step 1: worker running, steering: 2 steers, last 1970-01-01T00:00:00\.150Z/);
471
+ assert.match(exactText, /Steering: 2 requested, 1 scheduled, 1 pending, 2 delivered, 0 failed, 1 recovered, 1 late acknowledged/);
472
+ assert.match(exactText, /Step 1: worker running/);
474
473
 
475
474
  const list = inspectSubagentStatus({}, {
476
475
  asyncDirRoot: asyncRoot,
@@ -480,7 +479,7 @@ describe("async run status inspection", () => {
480
479
  });
481
480
  const listText = textContent(list);
482
481
  assert.equal(list.isError, undefined);
483
- assert.match(listText, /2 steers \| last steer 1970-01-01T00:00:00\.150Z/);
482
+ assert.match(listText, /steering 1 scheduled, 1 pending, 2 delivered, 0 failed, 1 recovered/);
484
483
  } finally {
485
484
  fs.rmSync(root, { recursive: true, force: true });
486
485
  }
@@ -961,6 +960,40 @@ describe("async run status inspection", () => {
961
960
  }
962
961
  });
963
962
 
963
+ it("shows stopped result-only runs without revive guidance", () => {
964
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-stopped-result-"));
965
+ try {
966
+ const asyncRoot = path.join(root, "runs");
967
+ const resultsDir = path.join(root, "results");
968
+ fs.mkdirSync(path.join(asyncRoot, "run-stopped-result"), { recursive: true });
969
+ fs.mkdirSync(resultsDir, { recursive: true });
970
+ const sessionFile = path.join(root, "session.jsonl");
971
+ fs.writeFileSync(sessionFile, "", "utf-8");
972
+ fs.writeFileSync(path.join(resultsDir, "run-stopped-result.json"), JSON.stringify({
973
+ id: "run-stopped-result",
974
+ agent: "worker",
975
+ success: false,
976
+ state: "stopped",
977
+ stopped: true,
978
+ sessionFile,
979
+ summary: "Subagent stopped by user.",
980
+ }, null, 2), "utf-8");
981
+
982
+ const result = inspectSubagentStatus({ id: "run-stopped-result" }, {
983
+ asyncDirRoot: asyncRoot,
984
+ resultsDir,
985
+ });
986
+
987
+ const text = textContent(result);
988
+ assert.equal(result.isError, undefined);
989
+ assert.match(text, /State: stopped/);
990
+ assert.match(text, /Resume: unavailable; stopped runs are not resumable/);
991
+ assert.doesNotMatch(text, /Revive:/);
992
+ } finally {
993
+ fs.rmSync(root, { recursive: true, force: true });
994
+ }
995
+ });
996
+
964
997
  it("falls back to an existing result when async dir has no status file", () => {
965
998
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-result-fallback-"));
966
999
  try {
@@ -14,6 +14,7 @@ import {
14
14
  scheduledRunsEnabled,
15
15
  } from "../../src/runs/background/scheduled-runs.ts";
16
16
  import type { ExtensionConfig } from "../../src/shared/types.ts";
17
+ import { encodeSubagentCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../../src/api/capability-ceiling.ts";
17
18
  import type { ExtensionContext } from "@selesai/code";
18
19
 
19
20
  type TimerHandle = number;
@@ -78,7 +79,7 @@ async function flushMicrotasks(times = 10): Promise<void> {
78
79
  for (let i = 0; i < times; i++) await Promise.resolve();
79
80
  }
80
81
 
81
- function createHarness(options: { config?: ExtensionConfig; storeRoot?: string; now?: number } = {}): TestHarness {
82
+ function createHarness(options: { config?: ExtensionConfig; storeRoot?: string; now?: number; resolveCapabilityCeiling?: (sessionId: string) => { version: 1; allowedTools?: string[]; denyExtensions: boolean; sources: string[] } | undefined } = {}): TestHarness {
82
83
  const timers = new FakeTimers();
83
84
  const launches: LaunchRecord[] = [];
84
85
  const clock = { now: options.now ?? 1_000_000 };
@@ -89,6 +90,7 @@ function createHarness(options: { config?: ExtensionConfig; storeRoot?: string;
89
90
  now: () => clock.now,
90
91
  randomId: () => "id-" + randomUUID().slice(0, 4),
91
92
  timers,
93
+ resolveCapabilityCeiling: options.resolveCapabilityCeiling,
92
94
  launch: (params, ctx, signal) => {
93
95
  let resolve!: LaunchRecord["resolve"];
94
96
  const promise = new Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown>; isError?: boolean }>((res) => {
@@ -190,6 +192,30 @@ describe("ScheduledRunManager create/list/status/cancel", () => {
190
192
  assert.equal(harness.timers.pendingCount(), 0);
191
193
  });
192
194
 
195
+ it("rejects capability-ceiling-restricted schedules instead of persisting policy-less jobs", async () => {
196
+ const harness = createHarness({ resolveCapabilityCeiling: () => ({ version: 1, allowedTools: ["read"], denyExtensions: true, sources: ["plan"] }) });
197
+ const result = await harness.manager.handleToolCall({ action: "schedule", agent: "worker", task: "review", schedule: "+10m" }, harness.ctx);
198
+ assert.equal(isError(result), true);
199
+ assert.match(result.content[0]!.text, /does not yet persist ceilings/);
200
+ assert.equal(harness.launches.length, 0);
201
+ });
202
+
203
+
204
+ it("rejects schedules restricted only by inherited process policy", async () => {
205
+ const previous = process.env.SELESAI_SUBAGENT_CAPABILITY_CEILING_V1;
206
+ try {
207
+ process.env.SELESAI_SUBAGENT_CAPABILITY_CEILING_V1 = encodeSubagentCapabilityCeiling({ version: 1, allowedTools: ["read"], denyExtensions: true, sources: ["ancestor"] });
208
+ const harness = createHarness({ resolveCapabilityCeiling: resolveCurrentSubagentCapabilityCeiling });
209
+ const result = await harness.manager.handleToolCall({ action: "schedule", agent: "worker", task: "review", schedule: "+10m" }, harness.ctx);
210
+ assert.equal(isError(result), true);
211
+ assert.match(result.content[0]!.text, /does not yet persist ceilings/);
212
+ assert.equal(harness.launches.length, 0);
213
+ } finally {
214
+ if (previous === undefined) delete process.env.SELESAI_SUBAGENT_CAPABILITY_CEILING_V1;
215
+ else process.env.SELESAI_SUBAGENT_CAPABILITY_CEILING_V1 = previous;
216
+ }
217
+ });
218
+
193
219
  it("creates a scheduled job and arms a timer", async () => {
194
220
  const harness = freshHarness();
195
221
  const result = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "review later", schedule: "+10m", scheduleName: "nightly review" }, harness.ctx);
@@ -211,6 +237,27 @@ describe("ScheduledRunManager create/list/status/cancel", () => {
211
237
  assert.match(none.content[0]!.text, /exactly one execution mode/);
212
238
  });
213
239
 
240
+ it("rejects explicit reviewed acceptance at every scheduled nesting level before persisting a job", async () => {
241
+ const harness = freshHarness();
242
+ const cases = [
243
+ { agent: "scout", task: "review later", acceptance: "reviewed" },
244
+ { agent: "scout", task: "review later", acceptance: { level: "reviewed" } },
245
+ { tasks: [{ agent: "scout", task: "review later", acceptance: "reviewed" }] },
246
+ { chain: [{ agent: "scout", task: "review later", acceptance: { level: "reviewed" } }] },
247
+ { chain: [{ parallel: [{ agent: "scout", task: "review later", acceptance: "reviewed" }] }] },
248
+ ];
249
+ for (const params of cases) {
250
+ const result = await harness.manager.handleToolCall({
251
+ action: "schedule",
252
+ schedule: "+10m",
253
+ ...params,
254
+ }, harness.ctx);
255
+ assert.equal(isError(result), true);
256
+ assert.match(result.content[0]!.text, /achieved status.*omit acceptance.*acceptance\.review\.required/i);
257
+ }
258
+ assert.equal(harness.timers.pendingCount(), 0);
259
+ });
260
+
214
261
  it("requires a schedule and rejects fork/async-false/clarify-true", async () => {
215
262
  const harness = freshHarness();
216
263
  const noSchedule = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x" }, harness.ctx);
@@ -180,9 +180,10 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
180
180
  assert.equal(actionSchema.type, "string");
181
181
  assert.equal(actionSchema.enum, undefined);
182
182
  const description = String(actionSchema.description ?? "");
183
- assert.match(description, /Management\/control action only/);
184
- assert.match(description, /Must be omitted for execution mode/);
185
- assert.match(description, /single, parallel, or chain/);
183
+ assert.match(description, /Optional management\/control action/);
184
+ assert.match(description, /Omit this field entirely for execution\/delegation/);
185
+ assert.match(description, /\{agent, task\}, \{tasks\}, or \{chain\}/);
186
+ assert.match(description, /use it only for management\/control actions/);
186
187
  assert.doesNotMatch(description, /orchestration\./);
187
188
  });
188
189
 
@@ -240,6 +241,12 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
240
241
  assert.equal(linesSchema.maximum, 500);
241
242
  assert.match(String(linesSchema.description ?? ""), /transcript/i);
242
243
 
244
+ const additionalSchema = SubagentParams?.properties?.additional;
245
+ assert.ok(additionalSchema, "additional schema should exist");
246
+ assert.equal(additionalSchema.minimum, 1);
247
+ assert.match(String(additionalSchema.description ?? ""), /grant-spawn-budget/);
248
+ assert.match(String(additionalSchema.description ?? ""), /root interactive parent/i);
249
+
243
250
  const controlSchema = SubagentParams?.properties?.control;
244
251
  assert.ok(controlSchema, "control schema should exist");
245
252
  assert.equal(controlSchema.properties?.needsAttentionAfterMs?.minimum, 1);
@@ -416,6 +423,14 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
416
423
  assert.equal(acceptanceSchema.type, undefined);
417
424
  assert.equal(hasAnyOfType(acceptanceSchema, "string"), true);
418
425
  assert.equal(hasAnyOfType(acceptanceSchema, "boolean"), true);
426
+ const acceptanceStringBranches = anyOfBranches(acceptanceSchema).filter((branch) => branch.type === "string");
427
+ const acceptanceLevelBranch = acceptanceStringBranches.find((branch) => Array.isArray(branch.enum) && branch.enum.includes("auto"));
428
+ assert.deepEqual(acceptanceLevelBranch?.enum, ["auto", "attested", "checked", "verified"], "evidence levels end at verified");
429
+ const reviewedRecoveryBranch = acceptanceStringBranches.find((branch) => Array.isArray(branch.enum) && branch.enum.includes("reviewed"));
430
+ assert.deepEqual(reviewedRecoveryBranch?.enum, ["reviewed"]);
431
+ assert.equal(reviewedRecoveryBranch?.deprecated, true);
432
+ assert.match(String(acceptanceSchema.description ?? ""), /reviewer\/read-only calls, omit acceptance/i);
433
+ assert.match(String(acceptanceSchema.description ?? ""), /acceptance\.review\.required/);
419
434
  const acceptanceObjectBranch = anyOfBranches(acceptanceSchema).find((branch) => branch.type === "object");
420
435
  assert.ok(acceptanceObjectBranch, "acceptance should support object config");
421
436
  assert.equal(acceptanceObjectBranch.additionalProperties, true);
@@ -499,6 +514,8 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
499
514
  { tasks: [{ agent: "worker", task: "Fix" }], maxRuntimeMs: 1000 },
500
515
  { chain: [{ agent: "worker", task: "Fix" }], timeoutMs: 1000, maxRuntimeMs: 1000 },
501
516
  { agent: "worker", task: "Fix", acceptance: "checked" },
517
+ { agent: "worker", task: "Fix", acceptance: "reviewed" },
518
+ { agent: "worker", task: "Fix", acceptance: { level: "none", reason: "parent will verify manually" } },
502
519
  { agent: "worker", task: "Fix", acceptance: { level: "checked", review: false } },
503
520
  { tasks: [{ agent: "worker", task: "Fix", acceptance: false }] },
504
521
  { chain: [{ agent: "worker", acceptance: { level: "checked" } }] },
@@ -517,6 +534,7 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
517
534
  ];
518
535
  const invalidValues = [
519
536
  { skill: 123 },
537
+ { agent: "worker", task: "Fix", acceptance: "none" },
520
538
  { skill: [123] },
521
539
  { output: 123 },
522
540
  { timeoutMs: 0 },