@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,181 @@
1
+ /**
2
+ * Shared task mutation-intent classifier.
3
+ *
4
+ * Single authority for reading a task's wording, answering two different
5
+ * questions from one prohibition analysis:
6
+ *
7
+ * - `classifyTaskMutationIntent` / `expectsImplementationMutation`: does the
8
+ * task REQUIRE file changes? Consumed by the completion mutation guard,
9
+ * which blocks completion, so its vocabulary is deliberately narrow.
10
+ * - `taskMayMutate`: COULD the task plausibly change files? Consumed by
11
+ * acceptance level inference, which only raises evidence gates, so its
12
+ * vocabulary is deliberately broad (any bare write verb).
13
+ *
14
+ * Explicit read-only wording ("do not modify", "review only") is a task-level
15
+ * intent only when no write imperative survives outside those phrases. A task
16
+ * like "Do not modify tests; implement the fix" is an implementation task with
17
+ * a scoped constraint, not a read-only task.
18
+ */
19
+
20
+ const REVIEW_ONLY_PATTERNS = [
21
+ /\breview only\b/i,
22
+ /\bsuggest fixes only\b/i,
23
+ /\bonly return findings\b/i,
24
+ /\breturn findings only\b/i,
25
+ ];
26
+
27
+ const REVIEWER_REQUIRED_EDIT_PATTERNS = [
28
+ /\bmust\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
29
+ /\brequired\s+to\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
30
+ /(?:^|[.!?\n]\s*)implement\s+(?:the\s+)?(?:approved|requested|specified|file|code|source|fix(?:es)?|changes?)\b/i,
31
+ /\bregardless\s+of\s+findings\b/i,
32
+ /\balways\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
33
+ /\bapply\s+(?:the\s+)?fix(?:es)?\s+directly\b/i,
34
+ /\bmake\s+(?:the\s+)?code\s+changes\b/i,
35
+ ];
36
+
37
+ // The prohibition's object ends at punctuation or at a coordinating word
38
+ // (but/and/then), so a follow-on clause like "but implement the fix" stays in
39
+ // the text for write-intent testing instead of being swallowed as the object.
40
+ const NO_EDIT_PROHIBITION_PATTERN = /\b(?:do not|don't|must not)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b((?:(?!\b(?:but|and|then)\b)[^.;,:!?\n–—-])*)/gi;
41
+
42
+ /** Objects of a no-edit prohibition that mean "the codebase in general" rather than a named scope. */
43
+ const GENERIC_PROHIBITION_OBJECT = /^\s*(?:(?:any|all|the|these|those|your|our|existing|project|source|sources|repo|repository)[\s/,-]*)*(?:files?|code|codebase|sources?|anything|repo(?:sitory)?)?\s*$/i;
44
+
45
+ const SCOPED_NO_EDIT_CONSTRAINT_PATTERNS = [
46
+ /\bdo not edit files?\s+outside\b/i,
47
+ /\bdo not edit\s+outside\b/i,
48
+ /\bdo not edit\s+unrelated files?\b/i,
49
+ /\bdo not change\s+unrelated files?\b/i,
50
+ /\bdo not modify\s+unrelated files?\b/i,
51
+ ];
52
+
53
+ const NO_TOOL_INTENT_PATTERNS = [
54
+ /\bno tools? needed\b/i,
55
+ /\bno tools? required\b/i,
56
+ /\bwithout using tools\b/i,
57
+ /\bdo not use tools\b/i,
58
+ /\bdon't use tools\b/i,
59
+ ];
60
+
61
+ const READ_ONLY_DELIVERABLE_PATTERNS = [
62
+ /\b(?:draft|write|compose|prepare|produce)\s+(?:(?:a|an|the)\s+)?(?:github\s+)?(?:issue|bug report|issue draft|issue body|proposal|plan|report|summary|findings?|analysis|recommendations?)\b/i,
63
+ /\b(?:issue|bug report)\s+(?:draft|body|template)\b/i,
64
+ /\b(?:return|provide|produce)\s+(?:text|markdown|answer|findings?|recommendations?)\s+only\b/i,
65
+ ];
66
+
67
+ const RESEARCH_AGENT_PATTERNS = [
68
+ /\binvestigate\b/i,
69
+ /\bscout\b/i,
70
+ /\bresearch(?:er)?\b/i,
71
+ ];
72
+
73
+ const FIX_OR_PATCH_IMPLEMENTATION_PATTERN = /\b(?:fix|patch)\s+(?:(?:it|this|that|them|each|any|all|these|those)\b|(?:(?:a|an|the|any|all)\s+)?(?:(?:failing|failed|broken|flaky|red|cold|start|current|existing|reported|approved|known|regression|unit|integration|e2e|source|typescript|type-?script|ts|type-?check|compiler)\s+)*(?:bug|defect|issues?|problems?|failures?|regressions?|tests?|errors?|items?|typos?|code|source|implementation|component|function|module|class|method|logic|file|files|readme|docs?|changelog|package\.json|config|manifest|extension|prompt|command|lint(?:ing)?|build|ci|type-?check|type\s+checking)\b)/i;
74
+
75
+ const WORKER_IMPLEMENTATION_PATTERNS = [
76
+ /\b(?:implement|edit|modify|refactor|delete)\b/i,
77
+ FIX_OR_PATCH_IMPLEMENTATION_PATTERN,
78
+ /\b(?:update|add|remove|replace|create)\b(?!\s+(?:(?:a|an|the)\s+)?(?:report|summary|findings?)(?:\b|$))/i,
79
+ /\bapply\s+(?:the\s+)?(?:(?:suggested|proposed|recommended)\s+)?(?:changes?|fix(?:es)?|patch)\b/i,
80
+ /\bmake\s+(?:the\s+)?changes\b/i,
81
+ /\bdo those fixes\b/i,
82
+ ];
83
+
84
+ const GENERAL_IMPLEMENTATION_PATTERNS = [
85
+ /\b(?:implement|edit|modify|refactor)\b/i,
86
+ FIX_OR_PATCH_IMPLEMENTATION_PATTERN,
87
+ /\bapply\s+(?:the\s+)?(?:(?:suggested|proposed|recommended)\s+)?(?:changes?|fix(?:es)?|patch)\b/i,
88
+ /\bmake\s+(?:the\s+)?changes\b/i,
89
+ /\bdo those fixes\b/i,
90
+ /\b(?:update|add|remove|replace|delete|create)\s+(?:the\s+)?(?:file|files|code|source|implementation|test|tests|component|function|module|class|method|logic|import|imports|readme|docs?|changelog|package\.json|config|manifest|extension|prompt|command)\b/i,
91
+ ];
92
+
93
+ export type TaskMutationIntent = { kind: "implementation" } | { kind: "read-only" } | { kind: "unknown" };
94
+
95
+ function stripFrameworkInstructions(task: string): string {
96
+ return task
97
+ .split("\n")
98
+ .filter((line) => !/^\s*\[(?:Write to|Read from):/i.test(line))
99
+ .filter((line) => !/^\s*(?:Create and maintain progress at:|Update progress at:|\*\*Output:\*\*|Write your findings to(?: exactly this path)?:|Return the complete artifact in your final response\.|The runtime will persist it to exactly this path:|Do not call contact_supervisor merely because no write-capable tool is available\.|This path is authoritative for this run\.|Ignore any other output filename or output path mentioned elsewhere)/i.test(line))
100
+ .join("\n");
101
+ }
102
+
103
+ function stripPatterns(task: string, patterns: RegExp[]): string {
104
+ let stripped = task;
105
+ for (const pattern of patterns) {
106
+ const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
107
+ stripped = stripped.replace(new RegExp(pattern.source, flags), " ");
108
+ }
109
+ return stripped;
110
+ }
111
+
112
+ interface NoEditProhibitionAnalysis {
113
+ /** True when any prohibition (or review-only/no-tool wording) is present. */
114
+ present: boolean;
115
+ /** True when a prohibition covers files/code in general; such wording wins over write verbs. */
116
+ blanket: boolean;
117
+ /** Task text with prohibition phrases and their objects removed, for write-intent testing. */
118
+ strippedText: string;
119
+ }
120
+
121
+ function analyzeNoEditProhibitions(taskText: string): NoEditProhibitionAnalysis {
122
+ let present = REVIEW_ONLY_PATTERNS.some((pattern) => pattern.test(taskText))
123
+ || NO_TOOL_INTENT_PATTERNS.some((pattern) => pattern.test(taskText));
124
+ let blanket = present;
125
+ let strippedText = stripPatterns(taskText, [...REVIEW_ONLY_PATTERNS, ...NO_TOOL_INTENT_PATTERNS]);
126
+ strippedText = strippedText.replace(new RegExp(NO_EDIT_PROHIBITION_PATTERN.source, NO_EDIT_PROHIBITION_PATTERN.flags), (_match, object: string) => {
127
+ present = true;
128
+ if (GENERIC_PROHIBITION_OBJECT.test(object)) blanket = true;
129
+ return " ";
130
+ });
131
+ return { present, blanket, strippedText };
132
+ }
133
+
134
+ function taskHasReadOnlyDeliverable(taskText: string): boolean {
135
+ return READ_ONLY_DELIVERABLE_PATTERNS.some((pattern) => pattern.test(taskText));
136
+ }
137
+
138
+ function isReviewerStyleAgent(agent: string): boolean {
139
+ return /\b(?:architect|commentator|explorer|recapper|researcher)\b/i.test(agent);
140
+ }
141
+
142
+ function hasImplementationIntent(agent: string, taskText: string): boolean {
143
+ if (isReviewerStyleAgent(agent)) return REVIEWER_REQUIRED_EDIT_PATTERNS.some((pattern) => pattern.test(taskText));
144
+ if (agent === "builder") return WORKER_IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(taskText));
145
+ return GENERAL_IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(taskText));
146
+ }
147
+
148
+ export function classifyTaskMutationIntent(agent: string, task: string): TaskMutationIntent {
149
+ const taskText = stripFrameworkInstructions(task);
150
+ const taskTextWithoutScopedConstraints = stripPatterns(taskText, SCOPED_NO_EDIT_CONSTRAINT_PATTERNS);
151
+ const prohibitions = analyzeNoEditProhibitions(taskTextWithoutScopedConstraints);
152
+ if (prohibitions.present) {
153
+ if (prohibitions.blanket) return { kind: "read-only" };
154
+ return hasImplementationIntent(agent, prohibitions.strippedText) ? { kind: "implementation" } : { kind: "read-only" };
155
+ }
156
+
157
+ if (RESEARCH_AGENT_PATTERNS.some((pattern) => pattern.test(agent))) return { kind: "read-only" };
158
+ if (hasImplementationIntent(agent, taskText)) return { kind: "implementation" };
159
+ if (isReviewerStyleAgent(agent)) return { kind: "read-only" };
160
+ return taskHasReadOnlyDeliverable(taskTextWithoutScopedConstraints) ? { kind: "read-only" } : { kind: "unknown" };
161
+ }
162
+
163
+ export function expectsImplementationMutation(agent: string, task: string): boolean {
164
+ return classifyTaskMutationIntent(agent, task).kind === "implementation";
165
+ }
166
+
167
+ /** Bare write verbs that make a task write-capable for acceptance inference. */
168
+ const MAY_MUTATE_VERB_PATTERN = /\b(?:fix|implement|update|write|edit|modify|migrate|delete|remove|refactor|commit)\b/i;
169
+
170
+ /**
171
+ * Whether the task could plausibly change files. Blanket prohibitions win;
172
+ * write verbs inside a scoped prohibition's object or a read-only deliverable
173
+ * phrase ("write a summary report") do not count; any other bare write verb
174
+ * does.
175
+ */
176
+ export function taskMayMutate(task: string): boolean {
177
+ const taskText = stripPatterns(stripFrameworkInstructions(task), SCOPED_NO_EDIT_CONSTRAINT_PATTERNS);
178
+ const prohibitions = analyzeNoEditProhibitions(taskText);
179
+ if (prohibitions.blanket) return false;
180
+ return MAY_MUTATE_VERB_PATTERN.test(stripPatterns(prohibitions.strippedText, READ_ONLY_DELIVERABLE_PATTERNS));
181
+ }
@@ -0,0 +1,83 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export const REQUIRED_CHILD_TOOLS_ENV = "SELESAI_SUBAGENT_REQUIRED_TOOLS";
5
+ export const MCP_DIRECT_CHILD_TOOLS_ENV = "SELESAI_SUBAGENT_MCP_DIRECT_TOOLS";
6
+ export const CHILD_TOOL_DIAGNOSTIC_PATH_ENV = "SELESAI_SUBAGENT_TOOL_DIAGNOSTIC_PATH";
7
+
8
+ export interface ChildToolDiagnostic {
9
+ agent?: string;
10
+ required: string[];
11
+ available: string[];
12
+ missing: string[];
13
+ missingMcpDirectTools?: string[];
14
+ }
15
+
16
+ const PI_CORE_CHILD_TOOLS = new Set(["bash", "edit", "find", "grep", "ls", "read", "write"]);
17
+
18
+ export function writeChildToolDiagnostic(
19
+ filePath: string,
20
+ required: string[],
21
+ available: string[],
22
+ agent?: string,
23
+ mcpDirectTools?: string[],
24
+ ): ChildToolDiagnostic | undefined {
25
+ const availableNames = new Set([...available, ...PI_CORE_CHILD_TOOLS]);
26
+ const missing = required.filter((name) => !availableNames.has(name));
27
+ if (missing.length === 0) {
28
+ fs.rmSync(filePath, { force: true });
29
+ return undefined;
30
+ }
31
+
32
+ const missingMcpDirectTools = mcpDirectTools?.length
33
+ ? missing.filter((name) => mcpDirectTools.includes(name))
34
+ : [];
35
+ const diagnostic: ChildToolDiagnostic = {
36
+ agent,
37
+ required,
38
+ available,
39
+ missing,
40
+ ...(missingMcpDirectTools.length > 0 ? { missingMcpDirectTools } : {}),
41
+ };
42
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
+ fs.writeFileSync(filePath, JSON.stringify(diagnostic), { mode: 0o600 });
44
+ return diagnostic;
45
+ }
46
+
47
+ export function readChildToolDiagnostic(filePath: string | undefined): ChildToolDiagnostic | undefined {
48
+ if (!filePath || !fs.existsSync(filePath)) return undefined;
49
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Partial<ChildToolDiagnostic>;
50
+ const stringArray = (value: unknown): value is string[] => Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.length > 0);
51
+ if (!stringArray(parsed.required) || !stringArray(parsed.available) || !stringArray(parsed.missing) || (parsed.agent !== undefined && typeof parsed.agent !== "string") || (parsed.missingMcpDirectTools !== undefined && !stringArray(parsed.missingMcpDirectTools))) {
52
+ throw new Error(`Malformed child tool diagnostic at '${filePath}'.`);
53
+ }
54
+ return {
55
+ ...(parsed.agent ? { agent: parsed.agent } : {}),
56
+ required: parsed.required,
57
+ available: parsed.available,
58
+ missing: parsed.missing,
59
+ ...(parsed.missingMcpDirectTools ? { missingMcpDirectTools: parsed.missingMcpDirectTools } : {}),
60
+ };
61
+ }
62
+
63
+ export function formatChildToolDiagnostic(diagnostic: ChildToolDiagnostic): string {
64
+ const subject = diagnostic.agent ? `Agent '${diagnostic.agent}'` : "Subagent";
65
+ return [
66
+ `${subject} requested unavailable child tools: ${diagnostic.missing.join(", ")}.`,
67
+ "The `tools` field is a strict allowlist; it does not load extension code.",
68
+ ...(diagnostic.missingMcpDirectTools?.length
69
+ ? [`Resolved MCP direct tools missing from the child registry: ${diagnostic.missingMcpDirectTools.join(", ")}. This indicates a host/pi-mcp-adapter registration problem, not a tool-call failure.`]
70
+ : []),
71
+ "For extension tools, add the provider path to `subagentOnlyExtensions` (child-only), `extensions`, or as a path-like entry in `tools`, while keeping each registered tool name in `tools`.",
72
+ "For MCP tools, verify the MCP adapter configuration and selected tool names. For builtin tools, verify the name against the installed Pi version.",
73
+ ].join("\n");
74
+ }
75
+
76
+ export function readChildToolDiagnosticError(filePath: string | undefined): string | undefined {
77
+ try {
78
+ const diagnostic = readChildToolDiagnostic(filePath);
79
+ return diagnostic ? formatChildToolDiagnostic(diagnostic) : undefined;
80
+ } catch (error) {
81
+ return `Failed to read child tool availability diagnostic: ${error instanceof Error ? error.message : String(error)}`;
82
+ }
83
+ }
@@ -2,6 +2,7 @@ import type { ResolvedToolBudget, ToolBudgetConfig, ToolBudgetState } from "../.
2
2
 
3
3
  export const DEFAULT_TOOL_BUDGET_BLOCK = ["read", "grep", "find", "ls"] as const;
4
4
  export const TOOL_BUDGET_ENV = "SELESAI_SUBAGENT_TOOL_BUDGET";
5
+ export const TOOL_BUDGET_ZERO_AUTH_ENV = "SELESAI_SUBAGENT_TOOL_BUDGET_ZERO_AUTH";
5
6
 
6
7
  export function normalizeToolBudgetBlock(block: ToolBudgetConfig["block"] | undefined): "*" | string[] {
7
8
  if (block === "*") return "*";
@@ -9,12 +10,17 @@ export function normalizeToolBudgetBlock(block: ToolBudgetConfig["block"] | unde
9
10
  return [...new Set(block.map((tool) => tool.trim()).filter(Boolean))];
10
11
  }
11
12
 
12
- export function validateToolBudgetConfig(raw: unknown, label = "toolBudget"): { budget?: ResolvedToolBudget; error?: string } {
13
+ export function validateToolBudgetConfig(
14
+ raw: unknown,
15
+ label = "toolBudget",
16
+ options: { minimumHard?: 0 | 1 } = {},
17
+ ): { budget?: ResolvedToolBudget; error?: string } {
13
18
  if (raw === undefined) return {};
14
19
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { error: `${label} must be an object with hard and optional soft/block.` };
15
20
  const value = raw as ToolBudgetConfig;
16
- if (typeof value.hard !== "number" || !Number.isInteger(value.hard) || value.hard < 1) {
17
- return { error: `${label}.hard must be an integer >= 1.` };
21
+ const minimumHard = options.minimumHard ?? 1;
22
+ if (typeof value.hard !== "number" || !Number.isInteger(value.hard) || value.hard < minimumHard) {
23
+ return { error: `${label}.hard must be an integer >= ${minimumHard}.` };
18
24
  }
19
25
  if (value.soft !== undefined && (typeof value.soft !== "number" || !Number.isInteger(value.soft) || value.soft < 1)) {
20
26
  return { error: `${label}.soft must be an integer >= 1 when provided.` };
@@ -65,10 +71,10 @@ export function encodeToolBudgetEnv(budget: ResolvedToolBudget | undefined): str
65
71
  return budget ? JSON.stringify(budget) : undefined;
66
72
  }
67
73
 
68
- export function decodeToolBudgetEnv(value: string | undefined): ResolvedToolBudget | undefined {
74
+ export function decodeToolBudgetEnv(value: string | undefined, options: { allowZero?: boolean } = {}): ResolvedToolBudget | undefined {
69
75
  if (!value?.trim()) return undefined;
70
76
  const parsed = JSON.parse(value) as unknown;
71
- const normalized = validateToolBudgetConfig(parsed, TOOL_BUDGET_ENV);
77
+ const normalized = validateToolBudgetConfig(parsed, TOOL_BUDGET_ENV, options.allowZero ? { minimumHard: 0 } : undefined);
72
78
  if (normalized.error) throw new Error(normalized.error);
73
79
  return normalized.budget;
74
80
  }
@@ -2,6 +2,27 @@ import type { ResolvedTurnBudget, TurnBudgetState } from "../../shared/types.ts"
2
2
 
3
3
  export const DEFAULT_TURN_BUDGET_GRACE_TURNS = 1;
4
4
 
5
+ export function resolveTurnBudgetConfig(
6
+ raw: unknown,
7
+ label = "turnBudget",
8
+ ): { turnBudget?: ResolvedTurnBudget; error?: string } {
9
+ if (raw === undefined) return {};
10
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
11
+ return { error: `${label} must be an object with maxTurns and optional graceTurns.` };
12
+ }
13
+ const unknownField = Object.keys(raw).find((key) => key !== "maxTurns" && key !== "graceTurns");
14
+ if (unknownField) return { error: `${label}.${unknownField} is not supported.` };
15
+ const budget = raw as { maxTurns?: unknown; graceTurns?: unknown };
16
+ if (typeof budget.maxTurns !== "number" || !Number.isInteger(budget.maxTurns) || budget.maxTurns < 1) {
17
+ return { error: `${label}.maxTurns must be an integer >= 1.` };
18
+ }
19
+ const graceTurns = budget.graceTurns ?? DEFAULT_TURN_BUDGET_GRACE_TURNS;
20
+ if (typeof graceTurns !== "number" || !Number.isInteger(graceTurns) || graceTurns < 0) {
21
+ return { error: `${label}.graceTurns must be an integer >= 0.` };
22
+ }
23
+ return { turnBudget: { maxTurns: budget.maxTurns, graceTurns } };
24
+ }
25
+
5
26
  export function appendTurnBudgetSystemPrompt(systemPrompt: string, budget: ResolvedTurnBudget | undefined): string {
6
27
  if (!budget) return systemPrompt;
7
28
  const grace = budget.graceTurns === 1 ? "1 additional assistant turn" : `${budget.graceTurns} additional assistant turns`;
@@ -24,6 +45,10 @@ export function turnBudgetExceededMessage(budget: ResolvedTurnBudget, turnCount:
24
45
  return `Subagent exceeded turn budget after ${turnCount} assistant turn${turnCount === 1 ? "" : "s"} (soft limit ${budget.maxTurns} + grace ${budget.graceTurns}).`;
25
46
  }
26
47
 
48
+ export function turnBudgetDeferredNote(budget: ResolvedTurnBudget, turnCount: number): string {
49
+ return `Turn-budget termination was deferred at ${turnCount} assistant turn${turnCount === 1 ? "" : "s"} (soft limit ${budget.maxTurns} + grace ${budget.graceTurns}) because the assistant started tool work. The run ended before another safe assistant boundary; output may be partial.`;
50
+ }
51
+
27
52
  export function formatTurnBudgetOutput(message: string, output: string): string {
28
53
  return output.trim()
29
54
  ? `${message}\n\nPartial output before turn-budget abort:\n${output}`
@@ -44,9 +69,30 @@ export function turnBudgetState(budget: ResolvedTurnBudget, turnCount: number, e
44
69
  };
45
70
  }
46
71
 
47
- export function shouldAbortForTurnBudget(budget: ResolvedTurnBudget, turnCount: number, terminalAssistantStop: boolean): boolean {
72
+ export function turnBudgetDeferredState(
73
+ budget: ResolvedTurnBudget,
74
+ turnCount: number,
75
+ terminationDeferredAtTurn = turnCount,
76
+ ): TurnBudgetState {
77
+ return {
78
+ ...budget,
79
+ turnCount,
80
+ outcome: "termination-deferred",
81
+ wrapUpRequestedAtTurn: budget.maxTurns,
82
+ terminationDeferredAtTurn,
83
+ };
84
+ }
85
+
86
+ export function turnBudgetDecision(
87
+ budget: ResolvedTurnBudget,
88
+ turnCount: number,
89
+ terminalAssistantStop: boolean,
90
+ toolWorkActiveOrStarting: boolean,
91
+ enforceHardLimit = false,
92
+ ): "continue" | "defer" | "abort" {
48
93
  const hardLimit = budget.maxTurns + budget.graceTurns;
49
- if (turnCount < hardLimit) return false;
50
- if (turnCount > hardLimit) return true;
51
- return !terminalAssistantStop;
94
+ if (turnCount < hardLimit) return "continue";
95
+ if (toolWorkActiveOrStarting && !enforceHardLimit) return "defer";
96
+ if (turnCount === hardLimit && terminalAssistantStop) return "continue";
97
+ return "abort";
52
98
  }
@@ -18,7 +18,7 @@ interface WorktreeInfo {
18
18
  syntheticPaths: string[];
19
19
  }
20
20
 
21
- interface WorktreeDiff {
21
+ export interface WorktreeDiff {
22
22
  index: number;
23
23
  agent: string;
24
24
  branch: string;
@@ -27,6 +27,23 @@ interface WorktreeDiff {
27
27
  insertions: number;
28
28
  deletions: number;
29
29
  patchPath: string;
30
+ error?: string;
31
+ }
32
+
33
+ export interface WorktreeCleanupTask {
34
+ index: number;
35
+ path: string;
36
+ branch: string;
37
+ worktreeRemoved: boolean;
38
+ branchRemoved: boolean;
39
+ errors?: string[];
40
+ }
41
+
42
+ export interface WorktreeCleanupReport {
43
+ state: "complete" | "partial";
44
+ tasks: WorktreeCleanupTask[];
45
+ pruned: boolean;
46
+ errors?: string[];
30
47
  }
31
48
 
32
49
  interface WorktreeTaskCwdConflict {
@@ -426,7 +443,7 @@ function removeSyntheticPathsBeforeDiff(worktree: WorktreeInfo): void {
426
443
  }
427
444
  }
428
445
 
429
- function emptyDiff(index: number, agent: string, branch: string, patchPath: string): WorktreeDiff {
446
+ function emptyDiff(index: number, agent: string, branch: string, patchPath: string, error?: string): WorktreeDiff {
430
447
  return {
431
448
  index,
432
449
  agent,
@@ -436,6 +453,7 @@ function emptyDiff(index: number, agent: string, branch: string, patchPath: stri
436
453
  insertions: 0,
437
454
  deletions: 0,
438
455
  patchPath,
456
+ ...(error ? { error } : {}),
439
457
  };
440
458
  }
441
459
 
@@ -497,13 +515,30 @@ function writeEmptyPatch(patchPath: string): void {
497
515
  }
498
516
  }
499
517
 
500
- function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo): void {
501
- try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch {
502
- // Cleanup is best-effort to avoid masking caller errors.
518
+ function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo): WorktreeCleanupTask {
519
+ const errors: string[] = [];
520
+ let worktreeRemoved = false;
521
+ let branchRemoved = false;
522
+ try {
523
+ runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]);
524
+ worktreeRemoved = true;
525
+ } catch (error) {
526
+ errors.push(`worktree removal failed: ${error instanceof Error ? error.message : String(error)}`);
503
527
  }
504
- try { runGitChecked(repoCwd, ["branch", "-D", worktree.branch]); } catch {
505
- // Cleanup is best-effort to avoid masking caller errors.
528
+ try {
529
+ runGitChecked(repoCwd, ["branch", "-D", worktree.branch]);
530
+ branchRemoved = true;
531
+ } catch (error) {
532
+ errors.push(`branch removal failed: ${error instanceof Error ? error.message : String(error)}`);
506
533
  }
534
+ return {
535
+ index: worktree.index,
536
+ path: worktree.path,
537
+ branch: worktree.branch,
538
+ worktreeRemoved,
539
+ branchRemoved,
540
+ ...(errors.length ? { errors } : {}),
541
+ };
507
542
  }
508
543
 
509
544
  function hasWorktreeChanges(diff: WorktreeDiff): boolean {
@@ -560,23 +595,37 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
560
595
  const patchPath = path.join(diffsDir, `task-${index}-${safePatchAgentName(agent)}.patch`);
561
596
  try {
562
597
  diffs.push(captureWorktreeDiff(setup, worktree, agent, patchPath));
563
- } catch {
564
- // Preserve execution flow; failed diff capture maps to an empty per-task patch.
598
+ } catch (error) {
599
+ // Preserve execution flow while retaining the failed capture as handoff evidence.
565
600
  writeEmptyPatch(patchPath);
566
- diffs.push(emptyDiff(index, agent, worktree.branch, patchPath));
601
+ diffs.push(emptyDiff(index, agent, worktree.branch, patchPath, error instanceof Error ? error.message : String(error)));
567
602
  }
568
603
  }
569
604
 
570
605
  return diffs;
571
606
  }
572
607
 
573
- export function cleanupWorktrees(setup: WorktreeSetup): void {
608
+ export function cleanupWorktrees(setup: WorktreeSetup): WorktreeCleanupReport {
609
+ const tasks: WorktreeCleanupTask[] = [];
574
610
  for (let index = setup.worktrees.length - 1; index >= 0; index--) {
575
- cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!);
611
+ tasks.push(cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!));
576
612
  }
577
- try { runGitChecked(setup.cwd, ["worktree", "prune"]); } catch {
578
- // Pruning is best-effort cleanup.
613
+ tasks.sort((left, right) => left.index - right.index);
614
+ const errors: string[] = [];
615
+ let pruned = false;
616
+ try {
617
+ runGitChecked(setup.cwd, ["worktree", "prune"]);
618
+ pruned = true;
619
+ } catch (error) {
620
+ errors.push(`worktree prune failed: ${error instanceof Error ? error.message : String(error)}`);
579
621
  }
622
+ const state = tasks.every((task) => task.worktreeRemoved && task.branchRemoved) && pruned ? "complete" : "partial";
623
+ return {
624
+ state,
625
+ tasks,
626
+ pruned,
627
+ ...(errors.length ? { errors } : {}),
628
+ };
580
629
  }
581
630
 
582
631
  export function formatWorktreeDiffSummary(diffs: WorktreeDiff[]): string {
@@ -0,0 +1,25 @@
1
+ import * as fs from "node:fs";
2
+ import { DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS, runFileSystemOperationWithRetry, waitForFileSystemRetry } from "./file-system-retry.ts";
3
+
4
+ type AccessibleDirFs = Pick<typeof fs, "accessSync" | "mkdirSync">;
5
+
6
+ type AccessibleDirOptions = {
7
+ fs?: AccessibleDirFs;
8
+ retryDirectoryErrors?: boolean;
9
+ retryDelaysMs?: readonly number[];
10
+ wait?: (delayMs: number) => void;
11
+ };
12
+
13
+ export function ensureAccessibleDir(dirPath: string, options: AccessibleDirOptions = {}): void {
14
+ const fsImpl = options.fs ?? fs;
15
+ const retryDirectoryErrors = options.retryDirectoryErrors ?? process.platform === "win32";
16
+ const retryDelaysMs = retryDirectoryErrors ? options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS : [];
17
+ const wait = options.wait ?? waitForFileSystemRetry;
18
+
19
+ runFileSystemOperationWithRetry(() => {
20
+ fsImpl.mkdirSync(dirPath, { recursive: true });
21
+ }, { retryDelaysMs, wait });
22
+ runFileSystemOperationWithRetry(() => {
23
+ fsImpl.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
24
+ }, { retryDelaysMs, wait });
25
+ }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { TEMP_ARTIFACTS_DIR, type ArtifactPaths } from "./types.ts";
3
+ import { TEMP_ARTIFACTS_DIR, type ArtifactPaths, type ArtifactDirPreference } from "./types.ts";
4
4
  import { getAgentDir } from "./utils.ts";
5
5
  const CLEANUP_MARKER_FILE = ".last-cleanup";
6
6
  const PROJECT_ARTIFACT_ROOT = ".pi-subagents";
@@ -17,13 +17,30 @@ export function getProjectChainRunsDir(cwd: string): string {
17
17
  return path.join(getProjectSubagentsDir(cwd), "chain-runs");
18
18
  }
19
19
 
20
- export function getArtifactsDir(sessionFile: string | null, projectCwd?: string): string {
21
- if (projectCwd) return getProjectArtifactsDir(projectCwd);
22
- if (sessionFile) {
23
- const sessionDir = path.dirname(sessionFile);
24
- return path.join(sessionDir, "subagent-artifacts");
20
+ export function getArtifactsDir(
21
+ sessionFile: string | null,
22
+ projectCwd?: string,
23
+ dirPreference: ArtifactDirPreference = "project",
24
+ ): string {
25
+ switch (dirPreference) {
26
+ case "session":
27
+ if (sessionFile) {
28
+ const sessionDir = path.dirname(sessionFile);
29
+ return path.join(sessionDir, "subagent-artifacts");
30
+ }
31
+ return TEMP_ARTIFACTS_DIR;
32
+ case "temp":
33
+ return TEMP_ARTIFACTS_DIR;
34
+ case "project":
35
+ if (projectCwd) return getProjectArtifactsDir(projectCwd);
36
+ if (sessionFile) {
37
+ const sessionDir = path.dirname(sessionFile);
38
+ return path.join(sessionDir, "subagent-artifacts");
39
+ }
40
+ return TEMP_ARTIFACTS_DIR;
41
+ default:
42
+ throw new Error(`Unsupported artifactDir ${JSON.stringify(dirPreference)}; expected "project", "session", or "temp".`);
25
43
  }
26
- return TEMP_ARTIFACTS_DIR;
27
44
  }
28
45
 
29
46
  export function getArtifactPaths(artifactsDir: string, runId: string, agent: string, index?: number): ArtifactPaths {
@@ -47,6 +64,19 @@ export function writeArtifact(filePath: string, content: string): void {
47
64
  fs.writeFileSync(filePath, content, "utf-8");
48
65
  }
49
66
 
67
+ export function formatOutputArtifactContent(input: {
68
+ output: string;
69
+ error?: string;
70
+ transcriptPath?: string;
71
+ metadataPath?: string;
72
+ }): string {
73
+ if (input.output.trim() || !input.error) return input.output;
74
+ const lines = ["Subagent run failed before producing output.", "", "Error:", input.error];
75
+ if (input.transcriptPath) lines.push("", `Transcript: ${input.transcriptPath}`);
76
+ if (input.metadataPath) lines.push(`Metadata: ${input.metadataPath}`);
77
+ return lines.join("\n");
78
+ }
79
+
50
80
  export function writeMetadata(filePath: string, metadata: object): void {
51
81
  fs.writeFileSync(filePath, JSON.stringify(metadata, null, 2), "utf-8");
52
82
  }