@selesai/code 0.8.5 → 0.8.7

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 (231) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/core/agent-session.js +13 -1
  3. package/dist/core/extensions/types.d.ts +2 -0
  4. package/dist/core/provider-composer.js +17 -4
  5. package/dist/core/settings-manager.d.ts +2 -0
  6. package/dist/core/settings-manager.js +3 -0
  7. package/dist/core/system-prompt.js +2 -8
  8. package/dist/core/system-prompt.test.js +6 -0
  9. package/dist/core/tools/index.d.ts +1 -0
  10. package/dist/core/tools/index.js +1 -0
  11. package/dist/core/tools/schema-prune.d.ts +16 -0
  12. package/dist/core/tools/schema-prune.js +39 -0
  13. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  14. package/dist/core/tools/schema-prune.test.js +111 -0
  15. package/dist/defaults/models.json +5 -0
  16. package/dist/defaults/settings.json +1 -0
  17. package/dist/extensions/agent-browser.test.ts +178 -2
  18. package/dist/extensions/agent-browser.ts +2 -0
  19. package/dist/extensions/copy-turn.test.ts +229 -0
  20. package/dist/extensions/copy-turn.ts +11 -0
  21. package/dist/extensions/grep-app/index.test.ts +356 -1
  22. package/dist/extensions/grep-app/index.ts +6 -1
  23. package/dist/extensions/handoff-new.test.ts +351 -161
  24. package/dist/extensions/handoff-new.ts +3 -0
  25. package/dist/extensions/inline-skills.test.ts +93 -0
  26. package/dist/extensions/inline-skills.ts +3 -0
  27. package/dist/extensions/model-prompt-injector/config.json +10 -0
  28. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  29. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  30. package/dist/extensions/package.json +1 -0
  31. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  32. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  33. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  34. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  35. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  36. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  37. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  38. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  39. package/dist/extensions/pi-subagents/docs/configuration.md +78 -11
  40. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  41. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  42. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  43. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  44. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  45. package/dist/extensions/pi-subagents/docs/workflows.md +4 -4
  46. package/dist/extensions/pi-subagents/install.mjs +2 -2
  47. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  48. package/dist/extensions/pi-subagents/package.json +1 -1
  49. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  50. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  51. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +16 -15
  52. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  53. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  54. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  55. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  56. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  57. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  58. package/dist/extensions/pi-subagents/src/extension/config.ts +59 -0
  59. package/dist/extensions/pi-subagents/src/extension/index.ts +146 -23
  60. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  61. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  62. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  63. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  64. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  65. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  66. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  67. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  68. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  69. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  70. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  71. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  72. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  73. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  74. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  75. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +17 -6
  76. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  77. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  78. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +11 -3
  79. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  80. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  81. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  82. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  83. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  84. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +224 -42
  85. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  86. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  87. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +35 -26
  88. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +108 -9
  89. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  90. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  91. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  92. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  93. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  94. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  95. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  96. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  97. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  98. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  99. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  100. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +368 -50
  101. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  102. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  104. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  105. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  106. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  108. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  109. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  110. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  112. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  113. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  114. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  115. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  116. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  117. package/dist/extensions/pi-subagents/src/shared/types.ts +66 -4
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  120. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  121. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  122. package/dist/extensions/pi-subagents/src/tui/fleet.ts +129 -17
  123. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  124. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  125. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  126. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  127. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  128. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  129. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  133. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  134. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  135. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  136. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  137. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  138. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  139. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  140. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  141. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  142. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  143. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  144. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  145. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  146. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  147. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  148. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  149. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  150. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  152. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  153. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  154. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  155. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  156. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  157. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  158. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  159. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +91 -0
  160. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  161. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  162. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  163. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  164. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  165. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  166. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  167. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  168. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  169. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  170. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  171. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  172. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  173. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  174. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  175. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  176. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  177. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  178. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  179. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  180. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  181. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  182. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  183. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  184. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  185. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  186. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  187. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  188. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  189. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  190. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  191. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  192. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  193. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  194. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  195. package/dist/extensions/ponytail/index.js +11 -0
  196. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  197. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  198. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  199. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  200. package/dist/extensions/question/index.ts +33 -0
  201. package/dist/extensions/question/question-list.ts +28 -0
  202. package/dist/extensions/question/row-layout.ts +3 -0
  203. package/dist/extensions/question/tests/batch.test.ts +103 -65
  204. package/dist/extensions/question/tests/exp.test.ts +70 -0
  205. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  206. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  207. package/dist/extensions/question/tests/helpers.test.ts +75 -35
  208. package/dist/extensions/question/tests/question-list.test.ts +640 -198
  209. package/dist/extensions/question/tests/row-layout.test.ts +193 -114
  210. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  211. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  212. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  213. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  214. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  215. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  216. package/dist/extensions/rtk.test.ts +180 -1
  217. package/dist/extensions/tokenin-onboarding.ts +3 -0
  218. package/dist/extensions/undo.test.ts +720 -0
  219. package/dist/extensions/undo.ts +6 -0
  220. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  221. package/dist/extensions/workflow/extension.ts +3 -3
  222. package/dist/extensions/workflow/modes.ts +41 -19
  223. package/dist/skills/pi-subagents/SKILL.md +1 -1
  224. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  225. package/dist/skills/pi-subagents/references/execution-controls.md +16 -15
  226. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  227. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  228. package/docs/plans/workflow-autoloop-reference.md +1 -2
  229. package/docs/plans/workflow-handoff-carryover.md +108 -0
  230. package/docs/settings.md +6 -0
  231. package/package.json +4 -1
@@ -8,11 +8,12 @@ const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
8
8
 
9
9
  export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
10
10
  • Use { action: "list" } before execution and only run executable/non-disabled agents.
11
- • Keep execution and management separate: omit action for workflowScript execution; use action only for management/control.
12
- • Async/background runs are the default. Use async:false only when a blocking foreground result is needed. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
11
+ • Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
12
+ • Async/background runs are the default. Use async:false only when a blocking foreground result is needed. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
13
13
  • Ordinary child subagents are not orchestrators. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
14
+ • Oracle/advisor consultations should use supervisor dialogue for material unknowns when available; request one-shot only when desired.
14
15
  • Keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers for independent review, then have the parent synthesize and apply fixes.
15
- • Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, and status via { action: "status", id }. Include output paths and residual risks when reporting results.`;
16
+ • Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, status via { action: "status", id }, and lifecycle diagnostics via { action: "debug.run", id }. Include output paths and residual risks when reporting results.`;
16
17
 
17
18
  export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Delegate one child with { agent, task }, compose steps with { chain }, fan out with { tasks }, or orchestrate with { workflowScript }; omit action. Use action only for management/control actions.
18
19
 
@@ -21,7 +22,7 @@ EXECUTION (use exactly one mode):
21
22
  • SINGLE: { agent, task? } launches one child. Omit task for a self-contained agent.
22
23
  • CHAIN: { chain: [{ agent: "explorer", task: "..." }, { agent: "builder", task: "Implement from {previous}" }] } runs sequential steps. Steps support template variables {task}, {previous}, {chain_dir}, and {outputs.name}; use as: "name" on a step to bind {outputs.name} for later steps, phase/label for status readability, outputSchema for structured results, and { checkpoint: "name", message?: "..." } for human gates paused until approve-checkpoint / reject-checkpoint. Set gateOn: "acceptance" to advance on acceptance instead of execution. agentContract: { version: 1 } gives generic result projections.
23
24
  • PARALLEL: { tasks: [{ agent, task }, ...], concurrency? } runs top-level parallel children (count: N repeats one template). Parallel groups also work inside chain steps with { parallel: [...] }, plus dynamic fanout via { expand: { from: { output, path }, item?, maxItems, parallel: {...}, collect: { as, outputSchema? } } }.
24
- • SCRIPTED WORKFLOW: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, terminal, milestones, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable plain task text, then pass the result explicitly as task. Use {action:"children.list"} to list up to 10 completed retained children from this parent session, then continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); resume and agent are mutually exclusive, resume keeps the stored agent/model/tool contract, workflow resumes wait for completed output, and loops must continue from each latest returned runId. For repository mutation lanes, set worktree:true on a direct single child, chain, workflow, or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.status, runs.ref/refs, prompts.render, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
25
+ • SCRIPTED WORKFLOW: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\n")\`. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, terminal, milestones, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable plain task text, then pass the result explicitly as task. Use {action:"children.list"} to list recent retained children with resumable/not-resumable states. Resume only rows reported resumable; {action:"resume", id:"run-id", message:"..."} keeps the stored agent/model/tool contract, or use runs.run(key, {resume:"run-id", task:"follow-up"}) inside workflowScript. Workflow resumes wait for completed output, and loops must continue from each latest returned runId. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. For repository mutation lanes, set worktree:true on a direct single child, chain, workflow, or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.status, runs.ref/refs, prompts.render, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
25
26
  • Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
26
27
  • Parallel example: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
27
28
  • Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async runs; foreground defaults to 30 minutes absent call/agent. Omit acceptance for read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review. For one host-run verification command, gate: "npm test" on a runs.run/runs.all item is shorthand; it cannot be combined with acceptance.
@@ -41,7 +42,7 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Delegate one child with { agen
41
42
  EXECUTE:
42
43
  • Call { action:"list" } first and use only executable/non-disabled agents.
43
44
  • SINGLE {agent, task?}; CHAIN {chain:[{agent,task,...}]} with {previous}/{outputs.name} templates and { checkpoint } human gates; PARALLEL {tasks:[...], concurrency?} with optional count and in-chain { parallel: [...] } / dynamic { expand }+{ collect } fanout.
44
- • SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable task text and pass it explicitly to runs.run. Use {action:"children.list"} for the last 10 retained children in this parent session, then runs.run(key,{resume:"run-id",task:"follow-up"}) to continue one with its stored contract; workflow resumes wait for completion and loops continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on a direct single child, chain, or runs.run/runs.all item for managed isolation. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off/terminal/milestones.
45
+ • SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable task text and pass it explicitly to runs.run. Use {action:"children.list"} for recent retained children and resume only rows reported resumable, with {action:"resume", id, message} or runs.run(key,{resume:"run-id",task:"follow-up"}); workflow resumes wait for completion and loops continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on a direct single child, chain, or runs.run/runs.all item for managed isolation. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off/terminal/milestones.
45
46
  • Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
46
47
  • context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async runs; foreground defaults to 30 minutes absent call/agent. Omit acceptance for read-only calls.
47
48
 
@@ -72,10 +73,10 @@ export interface ToolDescriptionOptions {
72
73
 
73
74
  export function resolveToolDescriptionMode(config: Pick<ExtensionConfig, "toolDescriptionMode">, options?: ToolDescriptionOptions): ToolDescriptionMode {
74
75
  const mode = config.toolDescriptionMode;
75
- if (mode === undefined) return "full";
76
+ if (mode === undefined) return "compact";
76
77
  if (isToolDescriptionMode(mode)) return mode;
77
78
  warn(options, `Ignoring invalid toolDescriptionMode ${JSON.stringify(mode)}; expected "full", "compact", or "custom".`);
78
- return "full";
79
+ return "compact";
79
80
  }
80
81
 
81
82
  function customDescriptionPaths(options?: ToolDescriptionOptions): string[] {
@@ -178,8 +179,8 @@ export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "tool
178
179
  const custom = loadCustomToolDescription(options);
179
180
  if (custom) description = withMandatorySafetyGuidance(custom);
180
181
  else {
181
- warn(options, `${CUSTOM_TOOL_DESCRIPTION_FILE} was not found or valid for toolDescriptionMode "custom"; using full description.`);
182
- description = FULL_SUBAGENT_TOOL_DESCRIPTION;
182
+ warn(options, `${CUSTOM_TOOL_DESCRIPTION_FILE} was not found or valid for toolDescriptionMode "custom"; using compact description.`);
183
+ description = COMPACT_SUBAGENT_TOOL_DESCRIPTION;
183
184
  }
184
185
  } else description = FULL_SUBAGENT_TOOL_DESCRIPTION;
185
186
  return config.legacyChainControls === false ? withoutLegacyChainControlGuidance(description) : description;
@@ -10,7 +10,9 @@ import { writeAtomicJson } from "../../shared/atomic-json.ts";
10
10
  import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
11
11
  import { readStatus } from "../../shared/utils.ts";
12
12
  import { resolveSubagentRunId } from "../../runs/background/run-id-resolver.ts";
13
+ import { resolveNodeExecutable } from "../../shared/node-executable.ts";
13
14
  import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode, type HerdrResult } from "./client.ts";
15
+ import { formatShellCommand } from "./shell-command.ts";
14
16
 
15
17
  export const HERDR_INSPECTOR_ACTIONS = ["inspector.open", "inspector.status", "inspector.close"] as const;
16
18
  export type HerdrInspectorAction = typeof HERDR_INSPECTOR_ACTIONS[number];
@@ -45,6 +47,7 @@ interface InspectorDeps {
45
47
  client?: HerdrClient;
46
48
  missions?: MissionStoreConfig;
47
49
  authorityPolicy?: AuthorityPolicyConfig;
50
+ sessionRoots?: string[];
48
51
  cwd: string;
49
52
  signal?: AbortSignal;
50
53
  now?: () => Date;
@@ -84,16 +87,11 @@ function extractPaneId(value: unknown): string | undefined {
84
87
  return undefined;
85
88
  }
86
89
 
87
- function shellQuote(value: string): string {
88
- if (process.platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
89
- return `'${value.replaceAll("'", "'\\''")}'`;
90
- }
91
-
92
- function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean }): string {
93
- const args = [process.execPath, input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop)];
90
+ function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean; sessionRoots: string[] }): string {
91
+ const args = [input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop), "--session-roots", JSON.stringify(input.sessionRoots)];
94
92
  if (input.index !== undefined) args.push("--index", String(input.index));
95
93
  if (input.missionPath) args.push("--mission-path", input.missionPath);
96
- return `${process.platform === "win32" ? "& " : ""}${args.map(shellQuote).join(" ")}`;
94
+ return formatShellCommand(resolveNodeExecutable(), args);
97
95
  }
98
96
 
99
97
  function missionForRun(asyncDir: string, cwd: string, config: MissionStoreConfig | undefined, runId: string): { id: string; path: string } | undefined {
@@ -114,6 +112,12 @@ function pathWithin(base: string, candidate: string): boolean {
114
112
  return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
115
113
  }
116
114
 
115
+ function herdrSessionRoots(target: { runId: string }, deps: InspectorDeps): string[] {
116
+ const roots = deps.sessionRoots ?? deps.state?.trustedSessionRoots ?? [];
117
+ const job = deps.state?.asyncJobs.get(target.runId) ?? deps.state?.fleetJobs?.get(target.runId);
118
+ return [...new Set([...roots, ...(job?.sessionRoot ? [job.sessionRoot] : [])])];
119
+ }
120
+
117
121
  function isTrustedAsyncDir(asyncDir: string, deps: InspectorDeps): boolean {
118
122
  try {
119
123
  if (fs.lstatSync(asyncDir).isSymbolicLink() || !fs.statSync(asyncDir).isDirectory()) return false;
@@ -204,6 +208,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
204
208
  missionPath: mission?.path,
205
209
  allowSteer: resolveAuthorityDecision({ action: "steerRun", policy: deps.authorityPolicy }) === "auto",
206
210
  allowStop: resolveAuthorityDecision({ action: "stopRun", policy: deps.authorityPolicy }) === "auto",
211
+ sessionRoots: herdrSessionRoots(target, deps),
207
212
  });
208
213
  const started = await client.run(["pane", "run", paneId, command], { timeoutMs: 15_000, signal: deps.signal });
209
214
  if (started.ok === false) {
@@ -17,6 +17,7 @@ export interface RunnerOptions {
17
17
  refreshMs: number;
18
18
  allowSteer?: boolean;
19
19
  allowStop?: boolean;
20
+ sessionRoots: string[];
20
21
  }
21
22
 
22
23
  function readMission(filePath: string | undefined): MissionRecord | undefined {
@@ -24,7 +25,7 @@ function readMission(filePath: string | undefined): MissionRecord | undefined {
24
25
  try { return parseMissionRecord(JSON.parse(fs.readFileSync(filePath, "utf-8")), filePath); } catch { return undefined; }
25
26
  }
26
27
 
27
- export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir: string; index?: number; mission?: MissionRecord; allowSteer?: boolean; allowStop?: boolean }): string {
28
+ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir: string; index?: number; mission?: MissionRecord; allowSteer?: boolean; allowStop?: boolean; sessionRoots?: string[] }): string {
28
29
  const { status, asyncDir, mission } = input;
29
30
  const lines = [
30
31
  `pi-subagents inspector for ${status.runId}`,
@@ -37,7 +38,7 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
37
38
  if (open.length) lines.push(`Open decisions: ${open.map((decision) => `${decision.id}: ${decision.title}`).join(" | ")}`);
38
39
  lines.push("");
39
40
  }
40
- lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60 }));
41
+ lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60, sessionRoots: input.sessionRoots }));
41
42
  const controls = [input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
42
43
  lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom).");
43
44
  return lines.join("\n");
@@ -57,6 +58,17 @@ function parseArgs(argv: string[]): RunnerOptions {
57
58
  const indexRaw = values.get("--index");
58
59
  const childIndex = indexRaw === undefined ? undefined : Number(indexRaw);
59
60
  if (childIndex !== undefined && (!Number.isInteger(childIndex) || childIndex < 0)) throw new Error("--index must be a non-negative integer.");
61
+ const sessionRootsRaw = values.get("--session-roots");
62
+ let sessionRoots: string[] = [];
63
+ if (sessionRootsRaw !== undefined) {
64
+ try {
65
+ const parsed = JSON.parse(sessionRootsRaw) as unknown;
66
+ if (!Array.isArray(parsed) || parsed.some((root) => typeof root !== "string")) throw new Error();
67
+ sessionRoots = parsed;
68
+ } catch {
69
+ throw new Error("--session-roots must be a JSON array of strings.");
70
+ }
71
+ }
60
72
  const refreshRaw = values.get("--refresh-ms");
61
73
  const refreshMs = refreshRaw === undefined ? 1_500 : Number(refreshRaw);
62
74
  if (!Number.isInteger(refreshMs) || refreshMs < 250) throw new Error("--refresh-ms must be an integer >= 250.");
@@ -65,6 +77,7 @@ function parseArgs(argv: string[]): RunnerOptions {
65
77
  runId,
66
78
  ...(childIndex !== undefined ? { index: childIndex } : {}),
67
79
  ...(values.get("--mission-path") ? { missionPath: path.resolve(values.get("--mission-path")!) } : {}),
80
+ sessionRoots,
68
81
  refreshMs,
69
82
  allowSteer: values.get("--allow-steer") !== "false",
70
83
  allowStop: values.get("--allow-stop") !== "false",
@@ -115,7 +128,7 @@ export function runInspector(argv = process.argv.slice(2)): void {
115
128
  process.stdout.write(`\x1b[2J\x1b[Hpi-subagents inspector\n\nLifecycle status for ${options.runId} is unavailable.\n`);
116
129
  return;
117
130
  }
118
- process.stdout.write(`\x1b[2J\x1b[H${formatInspectorDashboard({ status, asyncDir: options.asyncDir, index: options.index, mission: readMission(options.missionPath), allowSteer: options.allowSteer, allowStop: options.allowStop })}${notice ? `\n\n${notice}` : ""}\n> `);
131
+ process.stdout.write(`\x1b[2J\x1b[H${formatInspectorDashboard({ status, asyncDir: options.asyncDir, index: options.index, mission: readMission(options.missionPath), allowSteer: options.allowSteer, allowStop: options.allowStop, sessionRoots: options.sessionRoots })}${notice ? `\n\n${notice}` : ""}\n> `);
119
132
  if (isTerminal(status) && timer) {
120
133
  clearInterval(timer);
121
134
  timer = undefined;
@@ -6,6 +6,7 @@ import { getProjectSubagentsDir } from "../../shared/artifacts.ts";
6
6
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
7
  import type { Details } from "../../shared/types.ts";
8
8
  import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode } from "./client.ts";
9
+ import { formatShellCommand } from "./shell-command.ts";
9
10
 
10
11
  export const HERDR_PROJECT_PANE_ACTIONS = ["project.open", "project.status", "project.close"] as const;
11
12
  export type HerdrProjectPaneAction = typeof HERDR_PROJECT_PANE_ACTIONS[number];
@@ -257,11 +258,6 @@ function projectPaneRuntime(value: unknown): ProjectPaneRuntime | undefined {
257
258
  };
258
259
  }
259
260
 
260
- function shellQuote(value: string): string {
261
- if (process.platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
262
- return `'${value.replaceAll("'", "'\\''")}'`;
263
- }
264
-
265
261
  function resolveProjectRoot(requested: string): ProjectPaneResult<string> {
266
262
  const resolved = path.resolve(requested);
267
263
  try {
@@ -284,7 +280,7 @@ async function inspectPane(client: HerdrClient, paneId: string, signal?: AbortSi
284
280
  function projectPaneCommand(message: string | undefined): string {
285
281
  const args = message?.trim() ? [message.trim()] : [];
286
282
  const command = getPiSpawnCommand(args);
287
- return `${process.platform === "win32" ? "& " : ""}${[command.command, ...command.args].map(shellQuote).join(" ")}`;
283
+ return formatShellCommand(command.command, command.args);
288
284
  }
289
285
 
290
286
  function canonicalRuntimePath(value: string | undefined): string | undefined {
@@ -0,0 +1,16 @@
1
+ function shellQuote(value: string, platform: NodeJS.Platform): string {
2
+ if (platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
3
+ return `'${value.replaceAll("'", "'\\''")}'`;
4
+ }
5
+
6
+ function isBareExecutable(value: string): boolean {
7
+ return /^[\w./@:-]+$/.test(value);
8
+ }
9
+
10
+ export function formatShellCommand(exe: string, args: readonly string[], platform: NodeJS.Platform = process.platform): string {
11
+ const quotedArgs = args.map((arg) => shellQuote(arg, platform));
12
+ if (platform === "win32") return `& ${[shellQuote(exe, platform), ...quotedArgs].join(" ")}`;
13
+ // Nushell treats a leading quoted token as a string, so use a bare invoker for paths that need quoting.
14
+ const invocation = isBareExecutable(exe) ? exe : `sh -c 'exec "$0" "$@"' ${shellQuote(exe, platform)}`;
15
+ return [invocation, ...quotedArgs].join(" ");
16
+ }
@@ -26,9 +26,8 @@ Use contact_supervisor first. It resolves the supervisor session "{orchestratorT
26
26
  - After contact_supervisor with reason "need_decision" or "interview_request", stay alive and continue only after the reply arrives. Do not finish your final response with a choose-one question.
27
27
  - Do not ask for clarification when the only conflict is review-only/no-edit versus progress-writing or artifact-writing instructions. If an output path is configured but no write-capable tool is available, return the complete artifact in your final response; the runtime will persist it. Do not contact the supervisor merely because you cannot write that output path directly.
28
28
  - Meaningful progress or unexpected discoveries that change the plan: contact_supervisor({ reason: "progress_update", message: "UPDATE: <summary>" })
29
- - Generic intercom is lower-level plumbing/fallback only: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
30
29
 
31
- Do not use contact_supervisor or intercom for routine completion handoffs. If no coordination is needed, return a focused task result.`;
30
+ Do not use contact_supervisor for routine completion handoffs. If no coordination is needed, return a focused task result.`;
32
31
 
33
32
  export interface IntercomBridgeState {
34
33
  active: boolean;
@@ -51,6 +50,8 @@ export interface IntercomBridgeDiagnostic {
51
50
 
52
51
  interface ResolveIntercomBridgeInput {
53
52
  config: ExtensionConfig["intercomBridge"];
53
+ /** Per-run config replaces the global config when supplied. */
54
+ override?: IntercomBridgeConfig;
54
55
  context: "fresh" | "fork" | undefined;
55
56
  orchestratorTarget?: string;
56
57
  cwd?: string;
@@ -143,7 +144,7 @@ export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): Inter
143
144
  }
144
145
 
145
146
  export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
146
- const config = resolveIntercomBridgeConfig(input.config);
147
+ const config = resolveIntercomBridgeConfig(input.override !== undefined ? input.override : input.config);
147
148
  const mode = config.mode;
148
149
  const orchestratorTarget = input.orchestratorTarget?.trim();
149
150
  const agentDir = path.resolve(input.agentDir ?? defaultAgentDir());
@@ -169,7 +170,7 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
169
170
  export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomBridgeState): AgentConfig {
170
171
  if (!bridge.active || !bridge.orchestratorTarget) return agent;
171
172
 
172
- const bridgeTools = ["intercom", "contact_supervisor"];
173
+ const bridgeTools = ["contact_supervisor"];
173
174
  const tools = agent.tools && agent.tools.length > 0
174
175
  ? [...agent.tools, ...bridgeTools.filter((tool) => !agent.tools?.includes(tool))]
175
176
  : agent.tools;
@@ -295,38 +295,18 @@ function hasTool(pi: ExtensionAPI, name: string): boolean {
295
295
  }
296
296
  }
297
297
 
298
- export function registerNativeSupervisorClient(pi: ExtensionAPI, options: { includeIntercomFallback?: boolean } = {}): void {
299
- if (!readChildMetadata()) return;
300
- const includeIntercomFallback = options.includeIntercomFallback !== false;
301
- if (!hasTool(pi, "contact_supervisor")) {
302
- const tool: ToolDefinition<typeof ContactSupervisorParamsSchema, Record<string, unknown>> = {
303
- name: "contact_supervisor",
304
- label: "Contact Supervisor",
305
- description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
306
- parameters: ContactSupervisorParamsSchema,
307
- execute(_id, params, signal) {
308
- return sendSupervisorRequest(params as ContactSupervisorParams, signal);
309
- },
310
- };
311
- pi.registerTool(tool);
312
- }
313
- if (includeIntercomFallback && !hasTool(pi, "intercom")) {
314
- const tool: ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> = {
315
- name: "intercom",
316
- label: "Intercom",
317
- description: "Native supervisor-channel intercom fallback for subagents. Prefer contact_supervisor when available.",
318
- parameters: IntercomParamsSchema,
319
- async execute(_id, params, signal) {
320
- const action = (params as IntercomParams).action;
321
- if (action === "status") return { content: [{ type: "text", text: "Native supervisor channel is active." }], details: { active: true } };
322
- if (action === "list") return { content: [{ type: "text", text: "Supervisor session available through contact_supervisor." }], details: { sessions: [] } };
323
- if (action === "send") return sendSupervisorRequest({ reason: "progress_update", message: (params as IntercomParams).message ?? "" }, signal);
324
- if (action === "ask") return sendSupervisorRequest({ reason: "need_decision", message: (params as IntercomParams).message ?? "" }, signal);
325
- throw new Error("Native child intercom supports status, list, send, and ask. Use parent intercom reply from the supervisor session.");
326
- },
327
- };
328
- pi.registerTool(tool);
329
- }
298
+ export function registerNativeSupervisorClient(pi: ExtensionAPI): void {
299
+ if (!readChildMetadata() || hasTool(pi, "contact_supervisor")) return;
300
+ const tool: ToolDefinition<typeof ContactSupervisorParamsSchema, Record<string, unknown>> = {
301
+ name: "contact_supervisor",
302
+ label: "Contact Supervisor",
303
+ description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
304
+ parameters: ContactSupervisorParamsSchema,
305
+ execute(_id, params, signal) {
306
+ return sendSupervisorRequest(params as ContactSupervisorParams, signal);
307
+ },
308
+ };
309
+ pi.registerTool(tool);
330
310
  }
331
311
 
332
312
  function parseRequestFile(file: string, channelDir: string): PendingSupervisorRequest | undefined {
@@ -601,13 +581,11 @@ function publicPendingRequests(pending: Map<string, PendingSupervisorRequest>):
601
581
  }));
602
582
  }
603
583
 
604
- function buildParentIntercomTool(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, name = "intercom"): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
584
+ function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest>, state: SubagentState): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
605
585
  return {
606
- name,
607
- label: name === "intercom" ? "Intercom" : "Subagent Supervisor",
608
- description: name === "intercom"
609
- ? "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests."
610
- : "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
586
+ name: NATIVE_SUPERVISOR_TOOL_NAME,
587
+ label: "Subagent Supervisor",
588
+ description: "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
611
589
  parameters: IntercomParamsSchema,
612
590
  async execute(_id, params) {
613
591
  refreshPendingRequests(pending, state, state.lastUiContext ?? undefined);
@@ -627,9 +605,9 @@ function buildParentIntercomTool(pending: Map<string, PendingSupervisorRequest>,
627
605
  return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };
628
606
  }
629
607
  if (input.action === "send" || input.action === "ask") {
630
- throw new Error("Native pi-subagents intercom currently handles supervisor replies. Child agents initiate asks with contact_supervisor.");
608
+ throw new Error("The native subagent supervisor handles replies only. Child agents initiate asks with contact_supervisor.");
631
609
  }
632
- throw new Error(`Unsupported intercom action: ${input.action}`);
610
+ throw new Error(`Unsupported supervisor action: ${input.action}`);
633
611
  },
634
612
  };
635
613
  }
@@ -647,8 +625,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
647
625
  let lastStaleCleanupAt = 0;
648
626
 
649
627
  const registerParentTools = (): void => {
650
- if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentIntercomTool(pending, state, NATIVE_SUPERVISOR_TOOL_NAME));
651
- if (!hasTool(pi, "intercom")) pi.registerTool(buildParentIntercomTool(pending, state));
628
+ if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pending, state));
652
629
  };
653
630
 
654
631
  const cleanupStaleChannelsIfDue = (): void => {
@@ -99,7 +99,9 @@ function missionStateAction(location: MissionStoreLocation, record: MissionRecor
99
99
  function retainedResumeTarget(record: MissionRecord, retainedChildren: RetainedChild[]): RetainedChild | undefined {
100
100
  const latestRun = record.runs.at(-1);
101
101
  if (!latestRun) return undefined;
102
- return retainedChildren.find((child) => child.runId === latestRun.runId || child.parentRunId === latestRun.runId);
102
+ return retainedChildren.find((child) =>
103
+ child.resumability.state === "resumable" && (child.runId === latestRun.runId || child.parentRunId === latestRun.runId)
104
+ );
103
105
  }
104
106
 
105
107
  function nextReadyAction(location: MissionStoreLocation, record: MissionRecord, retainedChildren: RetainedChild[]): string {
@@ -2,7 +2,6 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
- import { getProjectSubagentsDir } from "../shared/artifacts.ts";
6
5
  import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
7
6
  import { getAgentDir } from "../shared/utils.ts";
8
7
  import {
@@ -259,6 +258,11 @@ function expandConfiguredPath(value: string, projectRoot: string): string {
259
258
  return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(projectRoot, expanded);
260
259
  }
261
260
 
261
+ function projectMissionDirectory(agentDir: string, projectRoot: string): string {
262
+ const projectKey = createHash("sha256").update(projectRoot).digest("hex");
263
+ return path.join(agentDir, "missions", "projects", projectKey);
264
+ }
265
+
262
266
  export function validateMissionStoreConfig(value: unknown, label = "config.missions"): MissionStoreConfig | undefined {
263
267
  if (value === undefined) return undefined;
264
268
  const input = asObject(value, label);
@@ -289,12 +293,13 @@ export function resolveMissionStoreLocation(input: {
289
293
  agentDir?: string;
290
294
  }): MissionStoreLocation {
291
295
  const projectRoot = path.resolve(input.projectRoot);
296
+ const agentDir = input.agentDir ?? getAgentDir();
292
297
  const missionDir = input.config?.directory
293
298
  ? expandConfiguredPath(input.config.directory, projectRoot)
294
- : path.join(getProjectSubagentsDir(projectRoot), "missions");
299
+ : projectMissionDirectory(agentDir, projectRoot);
295
300
  const globalIndexDir = input.config?.globalIndexDir
296
301
  ? expandConfiguredPath(input.config.globalIndexDir, projectRoot)
297
- : path.join(input.agentDir ?? getAgentDir(), "missions", "index");
302
+ : path.join(agentDir, "missions", "index");
298
303
  return {
299
304
  projectRoot,
300
305
  missionDir,
@@ -39,6 +39,18 @@ interface CapacityOptions {
39
39
  afterSlotRename?: (releasedDir: string) => void;
40
40
  }
41
41
 
42
+ export type ActiveAsyncCapacityReleaseVerdict =
43
+ | { state: "releasable"; reason: string }
44
+ | { state: "retained"; reason: string }
45
+ | { state: "not-owned"; reason: string };
46
+
47
+ export interface ActiveAsyncCapacityInspection {
48
+ owner?: ActiveAsyncCapacityOwnerV1;
49
+ relation: "current" | "source" | "none";
50
+ slotDir?: string;
51
+ release: ActiveAsyncCapacityReleaseVerdict;
52
+ }
53
+
42
54
  export class ActiveAsyncCapacityError extends Error {
43
55
  readonly snapshot: ActiveAsyncCapacitySnapshot;
44
56
 
@@ -160,55 +172,100 @@ function terminalState(state: AsyncStatus["state"]): boolean {
160
172
  return state !== "queued" && state !== "running" && state !== "paused";
161
173
  }
162
174
 
163
- function runnerCanRelease(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus): boolean {
164
- if (!owner.runnerProcessInstanceId
165
- || status.sessionId !== owner.ownerSessionId
166
- || status.runId !== owner.runId
167
- || !terminalState(status.state)) return false;
175
+ function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus | null): ActiveAsyncCapacityReleaseVerdict {
176
+ if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
177
+ if (!owner.runnerProcessInstanceId) return { state: "retained", reason: "runner process identity has not been recorded" };
178
+ if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
179
+ if (status.runId !== owner.runId) return { state: "retained", reason: `status run ${status.runId} does not match owner run ${owner.runId}` };
180
+ if (!terminalState(status.state)) return { state: "retained", reason: `run is still ${status.state}` };
168
181
  if (status.processTerminal?.state === "not-started"
169
182
  && status.processTerminal.runId === owner.runId
170
183
  && status.processTerminal.runnerProcessInstanceId === owner.runnerProcessInstanceId
171
184
  && typeof status.error === "string"
172
- && status.error) return true;
185
+ && status.error) return { state: "releasable", reason: "run failed before child startup completed" };
173
186
  const proof = readProcessTerminal(owner.asyncDir, {
174
187
  runId: owner.runId,
175
188
  runnerProcessInstanceId: owner.runnerProcessInstanceId,
176
189
  });
177
190
  return proof?.state === "observed"
178
191
  && proof.runId === owner.runId
179
- && proof.runnerProcessInstanceId === owner.runnerProcessInstanceId;
192
+ && proof.runnerProcessInstanceId === owner.runnerProcessInstanceId
193
+ ? { state: "releasable", reason: "matching observed process-terminal proof is present" }
194
+ : { state: "retained", reason: `process-terminal proof is ${proof?.state ?? "missing"}` };
180
195
  }
181
196
 
182
- function workflowCanRelease(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus, liveWorkflowRunIds: ReadonlySet<string>): boolean {
183
- if (status.sessionId !== owner.ownerSessionId
184
- || status.runId !== owner.runId
185
- || status.mode !== "workflow"
186
- || !terminalState(status.state)
187
- || liveWorkflowRunIds.has(owner.runId)) return false;
197
+ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus | null, liveWorkflowRunIds: ReadonlySet<string>): ActiveAsyncCapacityReleaseVerdict {
198
+ if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
199
+ if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
200
+ if (status.runId !== owner.runId) return { state: "retained", reason: `status run ${status.runId} does not match owner run ${owner.runId}` };
201
+ if (status.mode !== "workflow") return { state: "retained", reason: `status mode is ${status.mode}, not workflow` };
202
+ if (!terminalState(status.state)) return { state: "retained", reason: `workflow is still ${status.state}` };
203
+ if (liveWorkflowRunIds.has(owner.runId)) return { state: "retained", reason: "workflow controller is still live" };
188
204
  for (const step of status.steps ?? []) {
189
- if (step.status === "pending" || step.status === "running" || step.status === "paused") return false;
190
- if (typeof step.async !== "boolean") return false;
205
+ const label = step.workflowKey ?? step.agent;
206
+ if (step.status === "pending" || step.status === "running" || step.status === "paused") return { state: "retained", reason: `workflow child ${label} is still ${step.status}` };
207
+ if (typeof step.async !== "boolean") return { state: "retained", reason: `workflow child ${label} is missing async classification` };
191
208
  if (!step.async) continue;
192
- if (!step.runId) return false;
209
+ if (!step.runId) return { state: "retained", reason: `async workflow child ${label} is missing run id` };
193
210
  const childDir = path.join(path.dirname(owner.asyncDir), step.runId);
194
- if (!fs.existsSync(childDir)) return false;
211
+ if (!fs.existsSync(childDir)) return { state: "retained", reason: `async workflow child ${label} directory is missing` };
195
212
  const childStatus = readStatus(childDir);
196
- if (!childStatus || !terminalState(childStatus.state) || !childStatus.processTerminal?.runnerProcessInstanceId) return false;
213
+ if (!childStatus) return { state: "retained", reason: `async workflow child ${label} status is missing or unreadable` };
214
+ if (!terminalState(childStatus.state)) return { state: "retained", reason: `async workflow child ${label} is still ${childStatus.state}` };
215
+ if (!childStatus.processTerminal?.runnerProcessInstanceId) return { state: "retained", reason: `async workflow child ${label} has no runner process identity` };
197
216
  const proof = readProcessTerminal(childDir, {
198
217
  runId: step.runId,
199
218
  runnerProcessInstanceId: childStatus.processTerminal.runnerProcessInstanceId,
200
219
  });
201
- if (proof?.state !== "observed" || proof.runId !== step.runId) return false;
220
+ if (proof?.state !== "observed" || proof.runId !== step.runId) return { state: "retained", reason: `async workflow child ${label} process-terminal proof is ${proof?.state ?? "missing"}` };
202
221
  }
203
- return true;
222
+ return { state: "releasable", reason: "workflow is terminal, controller is gone, and async children have observed proof" };
204
223
  }
205
224
 
206
- function ownerCanRelease(owner: ActiveAsyncCapacityOwnerV1, liveWorkflowRunIds: ReadonlySet<string>): boolean {
225
+ function ownerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, liveWorkflowRunIds: ReadonlySet<string>): ActiveAsyncCapacityReleaseVerdict {
207
226
  const status = readStatus(owner.asyncDir);
208
- if (!status) return false;
209
227
  return owner.kind === "runner"
210
- ? runnerCanRelease(owner, status)
211
- : workflowCanRelease(owner, status, liveWorkflowRunIds);
228
+ ? runnerReleaseVerdict(owner, status)
229
+ : workflowReleaseVerdict(owner, status, liveWorkflowRunIds);
230
+ }
231
+
232
+ function capacitySessionDirs(rootDir: string, sessionId?: string): string[] {
233
+ if (sessionId) return [sessionDir(sessionId, rootDir)];
234
+ try {
235
+ return fs.readdirSync(rootDir, { withFileTypes: true })
236
+ .filter((entry) => entry.isDirectory())
237
+ .map((entry) => path.join(rootDir, entry.name));
238
+ } catch (error) {
239
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
240
+ throw error;
241
+ }
242
+ }
243
+
244
+ export function inspectActiveAsyncCapacityOwner(
245
+ input: { runId: string; sessionId?: string; asyncDir?: string },
246
+ options: CapacityOptions & { liveWorkflowRunIds?: ReadonlySet<string> } = {},
247
+ ): ActiveAsyncCapacityInspection {
248
+ const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
249
+ const liveWorkflowRunIds = options.liveWorkflowRunIds ?? new Set<string>();
250
+ for (const poolDir of capacitySessionDirs(rootDir, input.sessionId)) {
251
+ for (const dir of occupiedSlots(poolDir)) {
252
+ const owner = readOwner(dir);
253
+ if (!owner) continue;
254
+ const sameRun = owner.runId === input.runId || (input.asyncDir !== undefined && path.resolve(owner.asyncDir) === path.resolve(input.asyncDir));
255
+ const sourceRun = owner.sourceRunId === input.runId;
256
+ if (!sameRun && !sourceRun) continue;
257
+ if (sourceRun && !sameRun) {
258
+ return {
259
+ owner,
260
+ relation: "source",
261
+ slotDir: dir,
262
+ release: { state: "not-owned", reason: `slot was transferred to ${owner.runId}` },
263
+ };
264
+ }
265
+ return { owner, relation: "current", slotDir: dir, release: ownerReleaseVerdict(owner, liveWorkflowRunIds) };
266
+ }
267
+ }
268
+ return { relation: "none", release: { state: "not-owned", reason: "no active-capacity slot records this run" } };
212
269
  }
213
270
 
214
271
  export function reconcileActiveAsyncCapacity(
@@ -225,7 +282,7 @@ export function reconcileActiveAsyncCapacity(
225
282
  || owner.ownerSessionId !== sessionId
226
283
  || owner.ownerSessionKey !== activeAsyncCapacitySessionKey(sessionId)
227
284
  || path.basename(dir) !== `slot-${owner.slot}`
228
- || !ownerCanRelease(owner, liveWorkflowRunIds)) continue;
285
+ || ownerReleaseVerdict(owner, liveWorkflowRunIds).state !== "releasable") continue;
229
286
  removeOwnedSlot(dir, owner, options);
230
287
  }
231
288
  return snapshotFor(sessionId, limit, rootDir);