@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
@@ -38,6 +38,8 @@ import {
38
38
  } from "../../src/api/delegation.ts";
39
39
  import { CHAIN_RUNS_DIR, DIRS, INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, type AsyncStatus, type SubagentState } from "../../src/shared/types.ts";
40
40
  import { ACTIVE_RUN_INDEX_DIR } from "../../src/runs/background/active-run-index.ts";
41
+ import { resolveMissionStoreLocation } from "../../src/missions/store.ts";
42
+ import { missionStatePath } from "../../src/missions/workflow-state.ts";
41
43
  import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts";
42
44
  import { WAIT_TOOL_ENABLED_ENV } from "../../src/runs/background/wait-config.ts";
43
45
  import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV } from "../../src/runs/shared/tool-budget.ts";
@@ -656,7 +658,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
656
658
  const activityDeadline = Date.now() + 5_000;
657
659
  while (Date.now() < activityDeadline && !fs.existsSync(resultPath)) {
658
660
  const candidate = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
659
- if (candidate.activityState === "active_long_running" && candidate.steps?.[0]?.currentTool === "read") {
661
+ if (candidate.activityState === "needs_attention" && candidate.steps?.[0]?.currentTool === "read") {
660
662
  liveStatus = candidate;
661
663
  break;
662
664
  }
@@ -664,7 +666,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
664
666
  }
665
667
 
666
668
  assert.ok(liveStatus, "expected workflow status to expose live child activity");
667
- assert.equal(liveStatus.activityState, "active_long_running");
669
+ assert.equal(liveStatus.activityState, "needs_attention");
668
670
  assert.equal(typeof liveStatus.lastActivityAt, "number");
669
671
  assert.equal(liveStatus.currentTool, "read");
670
672
  assert.match(liveStatus.currentPath ?? "", /src[/\\]example\.ts$/);
@@ -673,10 +675,10 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
673
675
  assert.equal(liveStatus.steps?.[0]?.agent, "echo");
674
676
  assert.match(liveStatus.steps?.[0]?.sessionFile ?? "", /session\.jsonl$/);
675
677
  assert.equal(fs.existsSync(liveStatus.steps?.[0]?.sessionFile ?? ""), true);
676
- assert.equal(liveStatus.steps?.[0]?.activityState, "active_long_running");
678
+ assert.equal(liveStatus.steps?.[0]?.activityState, "needs_attention");
677
679
  assert.equal(typeof liveStatus.steps?.[0]?.lastActivityAt, "number");
678
680
  assert.equal(liveStatus.steps?.[0]?.toolCount, 1);
679
- assert.equal(asyncJobs.get(workflowRunId)?.activityState, "active_long_running");
681
+ assert.equal(asyncJobs.get(workflowRunId)?.activityState, "needs_attention");
680
682
  assert.equal(asyncJobs.get(workflowRunId)?.steps?.[0]?.currentTool, "read");
681
683
 
682
684
  const completionDeadline = Date.now() + 5_000;
@@ -913,39 +915,58 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
913
915
  fs.rmSync(asyncDir, { recursive: true, force: true });
914
916
  });
915
917
 
916
- it("routes workflow children through one automatic mission", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
918
+ it("keeps a git worktree clean while routing workflow children through one automatic mission", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
917
919
  mockPi.onCall({ output: "scanned auth" });
918
920
  mockPi.onCall({ output: "reviewed auth" });
919
- const executor = makeExecutor([makeAgent("echo")], { missions: { globalIndex: false } });
920
-
921
- const result = await executor.execute(
922
- "scripted-workflow",
923
- {
924
- async: false,
925
- workflowScript: `
926
- const stateType = typeof state;
927
- const scan = await runs.run("scan", { agent: "echo", task: "Scan auth" });
928
- const review = await runs.run("review", { agent: "echo", task: "Review: " + scan.output });
929
- return { output: review.output, stateType };
930
- `,
931
- },
932
- new AbortController().signal,
933
- undefined,
934
- makeMinimalCtx(tempDir),
935
- );
921
+ const projectDir = path.join(tempDir, "project");
922
+ const agentDir = path.join(tempDir, "agent");
923
+ fs.mkdirSync(projectDir);
924
+ execFileSync("git", ["init"], { cwd: projectDir, stdio: "ignore" });
925
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: projectDir });
926
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd: projectDir });
927
+ fs.writeFileSync(path.join(projectDir, "base.txt"), "base\n", "utf-8");
928
+ execFileSync("git", ["add", "base.txt"], { cwd: projectDir });
929
+ execFileSync("git", ["commit", "-m", "base"], { cwd: projectDir, stdio: "ignore" });
930
+ const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
931
+ process.env.SELESAI_CODING_AGENT_DIR = agentDir;
932
+ try {
933
+ const executor = makeExecutor([makeAgent("echo")], { missions: { globalIndex: false } });
934
+ const result = await executor.execute(
935
+ "scripted-workflow",
936
+ {
937
+ async: false,
938
+ workflowScript: `
939
+ const stateType = typeof state;
940
+ const scan = await runs.run("scan", { agent: "echo", task: "Scan auth" });
941
+ const review = await runs.run("review", { agent: "echo", task: "Review: " + scan.output });
942
+ return { output: review.output, stateType };
943
+ `,
944
+ },
945
+ new AbortController().signal,
946
+ undefined,
947
+ makeMinimalCtx(projectDir),
948
+ );
936
949
 
937
- assert.equal(result.isError, undefined);
938
- assert.match(result.content[0]?.text ?? "", /reviewed auth/);
939
- assert.equal(result.details.mode, "workflow");
940
- assert.equal(result.details.results.length, 2);
941
- assert.equal(result.details.workflow?.value && (result.details.workflow.value as { stateType?: unknown }).stateType, "object");
942
- assert.ok(result.details.missionId);
943
- const missionDir = path.join(tempDir, ".pi-subagents", "missions");
944
- const missionFiles = fs.readdirSync(missionDir).filter((entry) => entry.endsWith(".json"));
945
- assert.equal(missionFiles.length, 1);
946
- const mission = JSON.parse(fs.readFileSync(path.join(missionDir, missionFiles[0]!), "utf-8")) as { objective?: string };
947
- assert.equal(mission.objective, utils.PROMPT_REDACTED);
948
- assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state === "completed").map((entry) => entry.key), ["scan", "review"]);
950
+ assert.equal(result.isError, undefined);
951
+ assert.match(result.content[0]?.text ?? "", /reviewed auth/);
952
+ assert.equal(result.details.mode, "workflow");
953
+ assert.equal(result.details.results.length, 2);
954
+ assert.equal(result.details.workflow?.value && (result.details.workflow.value as { stateType?: unknown }).stateType, "object");
955
+ assert.ok(result.details.missionId);
956
+ const missionFiles = fs.readdirSync(path.join(agentDir, "missions", "projects"), { recursive: true })
957
+ .filter((entry) => typeof entry === "string" && entry.endsWith(".json"));
958
+ assert.equal(missionFiles.length, 1);
959
+ const mission = JSON.parse(fs.readFileSync(path.join(agentDir, "missions", "projects", missionFiles[0]!), "utf-8")) as { objective?: string };
960
+ assert.equal(mission.objective, utils.PROMPT_REDACTED);
961
+ assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state === "completed").map((entry) => entry.key), ["scan", "review"]);
962
+ // Selesai fork: artifactDir defaults to "project" (.pi-subagents), so the
963
+ // tree keeps untracked runtime state; only that dir may appear.
964
+ const porcelain = execFileSync("git", ["status", "--porcelain"], { cwd: projectDir, encoding: "utf-8" });
965
+ assert.match(porcelain, /^\?\? \.pi-subagents\/\n$/);
966
+ } finally {
967
+ if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
968
+ else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
969
+ }
949
970
  });
950
971
 
951
972
  it("keeps workflow children mission-detached when automatic mission persistence fails", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
@@ -953,7 +974,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
953
974
  mockPi.onCall({ output: "reviewed auth" });
954
975
  const blockedIndex = path.join(tempDir, "blocked-mission-index");
955
976
  fs.writeFileSync(blockedIndex, "not a directory", "utf-8");
956
- const executor = makeExecutor([makeAgent("echo")], { missions: { globalIndexDir: blockedIndex } });
977
+ const executor = makeExecutor([makeAgent("echo")], { missions: { directory: ".pi/subagents/missions", globalIndexDir: blockedIndex } });
957
978
 
958
979
  const result = await executor.execute(
959
980
  "scripted-workflow-mission-warning",
@@ -974,50 +995,62 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
974
995
  assert.equal(result.details.missionId, undefined);
975
996
  assert.match(result.details.missionWarning ?? "", /Mission tracking unavailable/);
976
997
  assert.equal(result.details.results.length, 2);
977
- const missionDir = path.join(tempDir, ".pi-subagents", "missions");
998
+ const missionDir = path.join(tempDir, ".pi/subagents", "missions");
978
999
  const missionFiles = fs.existsSync(missionDir) ? fs.readdirSync(missionDir).filter((entry) => entry.endsWith(".json")) : [];
979
1000
  assert.equal(missionFiles.length, 1);
980
1001
  });
981
1002
 
982
1003
  it("shares durable workflow state across a mission and omits it for mission:false", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
983
- const executor = makeExecutor([makeAgent("echo")], { missions: { globalIndex: false } });
984
- const first = await executor.execute(
985
- "mission-state-first",
986
- {
987
- async: false,
988
- mission: { title: "Stateful workflow" },
989
- workflowScript: `await state.set("review.stage", { count: 1 }); return await state.get("review.stage");`,
990
- },
991
- new AbortController().signal,
992
- undefined,
993
- makeMinimalCtx(tempDir),
994
- );
995
- assert.equal(first.isError, undefined, first.content[0]?.text ?? "first workflow failed");
996
- assert.ok(first.details.missionId);
997
- assert.deepEqual(first.details.workflow?.value, { count: 1 });
998
- const statePath = path.join(tempDir, ".pi-subagents", "missions", first.details.missionId, "state.json");
999
- assert.equal(fs.existsSync(statePath), true);
1000
-
1001
- const second = await executor.execute(
1002
- "mission-state-second",
1003
- { async: false, missionId: first.details.missionId, workflowScript: `return await state.get("review.stage");` },
1004
- new AbortController().signal,
1005
- undefined,
1006
- makeMinimalCtx(tempDir),
1007
- );
1008
- assert.equal(second.isError, undefined, second.content[0]?.text ?? "second workflow failed");
1009
- assert.deepEqual(second.details.workflow?.value, { count: 1 });
1004
+ const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
1005
+ const projectDir = path.join(tempDir, "project");
1006
+ const agentDir = path.join(tempDir, "agent");
1007
+ fs.mkdirSync(projectDir);
1008
+ process.env.SELESAI_CODING_AGENT_DIR = agentDir;
1009
+ try {
1010
+ const executor = makeExecutor([makeAgent("echo")], { missions: { globalIndex: false } });
1011
+ const first = await executor.execute(
1012
+ "mission-state-first",
1013
+ {
1014
+ async: false,
1015
+ mission: { title: "Stateful workflow" },
1016
+ workflowScript: `await state.set("review.stage", { count: 1 }); return await state.get("review.stage");`,
1017
+ },
1018
+ new AbortController().signal,
1019
+ undefined,
1020
+ makeMinimalCtx(projectDir),
1021
+ );
1022
+ assert.equal(first.isError, undefined, first.content[0]?.text ?? "first workflow failed");
1023
+ assert.ok(first.details.missionId);
1024
+ assert.deepEqual(first.details.workflow?.value, { count: 1 });
1025
+ const location = resolveMissionStoreLocation({ projectRoot: projectDir, agentDir });
1026
+ const statePath = missionStatePath(location, first.details.missionId);
1027
+ assert.equal(fs.existsSync(statePath), true);
1028
+ assert.equal(path.relative(projectDir, statePath).startsWith(".."), true);
1029
+
1030
+ const second = await executor.execute(
1031
+ "mission-state-second",
1032
+ { async: false, missionId: first.details.missionId, workflowScript: `return await state.get("review.stage");` },
1033
+ new AbortController().signal,
1034
+ undefined,
1035
+ makeMinimalCtx(projectDir),
1036
+ );
1037
+ assert.equal(second.isError, undefined, second.content[0]?.text ?? "second workflow failed");
1038
+ assert.deepEqual(second.details.workflow?.value, { count: 1 });
1010
1039
 
1011
- const ephemeral = await executor.execute(
1012
- "mission-state-off",
1013
- { async: false, mission: false, workflowScript: `return typeof state;` },
1014
- new AbortController().signal,
1015
- undefined,
1016
- makeMinimalCtx(tempDir),
1017
- );
1018
- assert.equal(ephemeral.isError, undefined, ephemeral.content[0]?.text ?? "ephemeral workflow failed");
1019
- assert.equal(ephemeral.details.workflow?.value, "undefined");
1020
- assert.equal(ephemeral.details.missionId, undefined);
1040
+ const ephemeral = await executor.execute(
1041
+ "mission-state-off",
1042
+ { async: false, mission: false, workflowScript: `return typeof state;` },
1043
+ new AbortController().signal,
1044
+ undefined,
1045
+ makeMinimalCtx(projectDir),
1046
+ );
1047
+ assert.equal(ephemeral.isError, undefined, ephemeral.content[0]?.text ?? "ephemeral workflow failed");
1048
+ assert.equal(ephemeral.details.workflow?.value, "undefined");
1049
+ assert.equal(ephemeral.details.missionId, undefined);
1050
+ } finally {
1051
+ if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
1052
+ else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
1053
+ }
1021
1054
  });
1022
1055
 
1023
1056
  it("runs a direct single child in a managed worktree", { skip: !createSubagentExecutor || process.platform === "win32" ? "executor unavailable or worktree paths differ on Windows" : undefined }, async () => {
@@ -1356,7 +1389,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1356
1389
 
1357
1390
  assert.equal(detachAccepted, true);
1358
1391
  assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
1359
- assert.match(result.content[0]?.text ?? "", /run detaches: failed/);
1392
+ assert.match(result.content[0]?.text ?? "", /run detaches: detached/);
1360
1393
  const workflowValue = result.details.workflow?.value as Array<{ ok: boolean; artifactPaths: string[] }>;
1361
1394
  assert.equal(workflowValue[0]?.ok, false);
1362
1395
  const handoffPath = workflowValue[0]?.artifactPaths.find((candidate) => candidate.endsWith(".json"));
@@ -2668,7 +2701,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2668
2701
 
2669
2702
  const result = await runSync(tempDir, agents, "echo", "Investigate behavior", {
2670
2703
  runId: "run-active",
2671
- controlConfig: { enabled: true, activeNoticeAfterTurns: 2, activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 999_999, notifyOn: ["active_long_running", "needs_attention"] },
2704
+ controlConfig: { enabled: true, activeNoticeAfterTurns: 2, activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 999_999, notifyOn: ["active_long_running", "active_long_running"] },
2672
2705
  onControlEvent: (event: NonNullable<RunSyncResult["controlEvents"]>[number]) => controlEvents.push(event),
2673
2706
  });
2674
2707
 
@@ -2725,7 +2758,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2725
2758
 
2726
2759
  const result = await runSync(tempDir, agents, "echo", "Investigate behavior", {
2727
2760
  runId: "run-control-disabled",
2728
- controlConfig: { enabled: false, activeNoticeAfterTurns: 1, activeNoticeAfterMs: 1, activeNoticeAfterTokens: 1, notifyOn: ["active_long_running", "needs_attention"] },
2761
+ controlConfig: { enabled: false, activeNoticeAfterTurns: 1, activeNoticeAfterMs: 1, activeNoticeAfterTokens: 1, notifyOn: ["active_long_running", "active_long_running"] },
2729
2762
  onControlEvent: (event: NonNullable<RunSyncResult["controlEvents"]>[number]) => controlEvents.push(event),
2730
2763
  });
2731
2764
 
@@ -3451,14 +3484,14 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3451
3484
 
3452
3485
  const result = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
3453
3486
  runId: "delayed-tool-attention",
3454
- controlConfig: { enabled: true, needsAttentionAfterMs: 200, activeNoticeAfterMs: 999_999, notifyOn: ["needs_attention"] },
3487
+ controlConfig: { enabled: true, needsAttentionAfterMs: 200, activeNoticeAfterMs: 999_999, notifyOn: ["active_long_running"] },
3455
3488
  onUpdate: (update: { details?: { progress?: ProgressSummary[] } }) => updates.push(update),
3456
3489
  onControlEvent: (event: NonNullable<RunSyncResult["controlEvents"]>[number]) => controlEvents.push(event),
3457
3490
  });
3458
3491
 
3459
3492
  assert.equal(result.exitCode, 0);
3460
3493
  assert.equal(result.progress.activityState, undefined);
3461
- assert.equal(controlEvents.some((event) => event.type === "needs_attention"), false);
3494
+ assert.equal(controlEvents.some((event) => event.type === "active_long_running"), false);
3462
3495
  assert.equal(updates.some((update) => update.details?.progress?.some((progress) => progress.currentTool === "bash")), true);
3463
3496
  });
3464
3497
 
@@ -46,6 +46,7 @@ interface RegisterSlashCommandsModule {
46
46
  watcherRestartTimer: ReturnType<typeof setTimeout> | null;
47
47
  resultFileCoalescer: { schedule(file: string, delayMs?: number): boolean; clear(): void };
48
48
  },
49
+ options?: { foregroundDetachShortcut?: string },
49
50
  ) => void;
50
51
  }
51
52
 
@@ -480,6 +481,52 @@ describe("slash command custom message delivery", { skip: !available ? "slash-co
480
481
  clearSlashSnapshots?.();
481
482
  });
482
483
 
484
+ it("registers a configured foreground detach shortcut", async () => {
485
+ const shortcuts = new Map<string, { handler(ctx: unknown): Promise<void> }>();
486
+ const sent: unknown[] = [];
487
+ const state = createState(process.cwd());
488
+ let detachCalls = 0;
489
+ state.foregroundControls.set("run-123", {
490
+ runId: "run-123",
491
+ mode: "single",
492
+ updatedAt: Date.now(),
493
+ detach: () => {
494
+ detachCalls += 1;
495
+ return true;
496
+ },
497
+ });
498
+ state.lastForegroundControlId = "run-123";
499
+
500
+ registerSlashCommands!({
501
+ events: createEventBus(),
502
+ on() {},
503
+ on() {},
504
+ registerCommand() {},
505
+ registerShortcut(key: string, spec: { handler(ctx: unknown): Promise<void> }) {
506
+ shortcuts.set(key, spec);
507
+ },
508
+ sendMessage(message: unknown) { sent.push(message); },
509
+ }, state, { foregroundDetachShortcut: "ctrl+b" });
510
+
511
+ assert.ok(shortcuts.has("ctrl+b"));
512
+ await shortcuts.get("ctrl+b")!.handler(createCommandContext());
513
+ assert.equal(detachCalls, 1);
514
+ assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /Detached foreground run run-123/);
515
+ });
516
+
517
+ it("does not reserve a foreground detach shortcut by default", () => {
518
+ const shortcuts = new Map<string, unknown>();
519
+ registerSlashCommands!({
520
+ events: createEventBus(),
521
+ on() {},
522
+ on() {},
523
+ registerCommand() {},
524
+ registerShortcut(key: string, spec: unknown) { shortcuts.set(key, spec); },
525
+ sendMessage() {},
526
+ }, createState(process.cwd()));
527
+ assert.equal(shortcuts.has("ctrl+b"), false);
528
+ });
529
+
483
530
  it("/subagents-stop keeps the selector within its allocated width", async () => {
484
531
  await withTempProject("pi-stop-selector-width-", async (root) => {
485
532
  const id = "scheduled-width-check";
@@ -589,7 +636,7 @@ describe("slash command custom message delivery", { skip: !available ? "slash-co
589
636
  assert.equal(requestedCtx, ctx);
590
637
  assert.equal(sent.length, 2);
591
638
  assert.equal((sent[0] as { display?: boolean }).display, true);
592
- assert.equal((sent[0] as { content?: string }).content, "Running subagent...");
639
+ assert.equal((sent[0] as { content?: string }).content, "Workflow running.");
593
640
  assert.equal((sent[1] as { display?: boolean }).display, true);
594
641
  assert.match((sent[1] as { content?: string }).content ?? "", /Commit finished/);
595
642
  assert.equal(sessionManager.rewrites, 2);
@@ -636,6 +683,7 @@ Inspect
636
683
  const pi = {
637
684
  events,
638
685
  on() {},
686
+ on() {},
639
687
  registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
640
688
  commands.set(name, spec);
641
689
  },
@@ -63,6 +63,18 @@ describe("slash live state", { skip: !available ? "slash-live-state.ts not impor
63
63
  assert.equal(snapshot.version > 0, true);
64
64
  });
65
65
 
66
+ it("marks async initial slash snapshots as background", () => {
67
+ clearSlashSnapshots!();
68
+ const details = buildSlashInitialResult!("req-bg", {
69
+ workflowScript: "return runs.run(\"run\", { agent: \"scout\", task: \"inspect\" })",
70
+ async: true,
71
+ });
72
+
73
+ assert.equal(details.result.details.mode, "workflow");
74
+ assert.equal(details.result.details.background, true);
75
+ assert.equal(details.result.details.asyncId, undefined);
76
+ });
77
+
66
78
  it("does not assign a parallel child update to another chain placeholder", () => {
67
79
  clearSlashSnapshots!();
68
80
  const details = buildSlashInitialResult!("req-parallel", {
@@ -0,0 +1,15 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export function writeNodeCommand(directory: string, name: string, source: string): string {
5
+ if (process.platform === "win32") {
6
+ const scriptPath = path.join(directory, `${name}.cjs`);
7
+ const commandPath = path.join(directory, `${name}.cmd`);
8
+ fs.writeFileSync(scriptPath, source, "utf-8");
9
+ fs.writeFileSync(commandPath, `@echo off\r\n"${process.execPath}" "${scriptPath}" %*\r\n`, "utf-8");
10
+ return commandPath;
11
+ }
12
+ const commandPath = path.join(directory, name);
13
+ fs.writeFileSync(commandPath, `#!/usr/bin/env node\n${source}\n`, { encoding: "utf-8", mode: 0o755 });
14
+ return commandPath;
15
+ }
@@ -7,6 +7,7 @@ import {
7
7
  acquireActiveAsyncCapacity,
8
8
  ActiveAsyncCapacityError,
9
9
  getActiveAsyncCapacitySnapshot,
10
+ inspectActiveAsyncCapacityOwner,
10
11
  transferActiveAsyncCapacity,
11
12
  } from "../../src/runs/background/active-async-capacity.ts";
12
13
 
@@ -104,6 +105,32 @@ describe("active async capacity", () => {
104
105
  }
105
106
  });
106
107
 
108
+ it("explains runner release verdicts without mutating slots", () => {
109
+ const rootDir = tempRoot();
110
+ const asyncDir = path.join(rootDir, "runs", "run-a");
111
+ try {
112
+ const handle = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 2, runId: "run-a", kind: "runner", asyncDir }, { rootDir });
113
+ assert.ok(handle);
114
+ handle.markStarted("runner-a");
115
+ writeJson(path.join(asyncDir, "status.json"), { runId: "run-a", sessionId: "session-a", mode: "single", state: "complete", startedAt: 100, processTerminal: { version: 1, state: "unknown", runId: "run-a", runnerProcessInstanceId: "runner-a", reason: "process-tree-unverified" } });
116
+
117
+ const retained = inspectActiveAsyncCapacityOwner({ runId: "run-a", sessionId: "session-a", asyncDir }, { rootDir });
118
+
119
+ assert.equal(retained.relation, "current");
120
+ assert.equal(retained.release.state, "retained");
121
+ assert.match(retained.release.reason, /process-terminal proof is missing/);
122
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 2, { rootDir }), { used: 1, limit: 2 });
123
+
124
+ writeJson(path.join(asyncDir, "process-terminal.json"), observedProof("run-a", "runner-a"));
125
+ const releasable = inspectActiveAsyncCapacityOwner({ runId: "run-a", sessionId: "session-a", asyncDir }, { rootDir });
126
+ assert.equal(releasable.release.state, "releasable");
127
+ assert.match(releasable.release.reason, /observed process-terminal proof/);
128
+ assert.equal(releasable.slotDir ? fs.existsSync(releasable.slotDir) : false, true);
129
+ } finally {
130
+ fs.rmSync(rootDir, { recursive: true, force: true });
131
+ }
132
+ });
133
+
107
134
  it("retains active and unknown runner proof, then releases matching observed proof", () => {
108
135
  const rootDir = tempRoot();
109
136
  const asyncDir = path.join(rootDir, "runs", "run-a");
@@ -211,6 +238,29 @@ describe("active async capacity", () => {
211
238
  }
212
239
  });
213
240
 
241
+ it("reports transferred source runs as not owned", () => {
242
+ const rootDir = tempRoot();
243
+ const sourceDir = path.join(rootDir, "runs", "source");
244
+ const nextDir = path.join(rootDir, "runs", "next");
245
+ try {
246
+ const source = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "source", kind: "runner", asyncDir: sourceDir }, { rootDir });
247
+ assert.ok(source);
248
+ source.markStarted("runner-source");
249
+ writeJson(path.join(sourceDir, "status.json"), { runId: "source", sessionId: "session-a", mode: "single", state: "paused", startedAt: 100 });
250
+ const next = transferActiveAsyncCapacity({ sessionId: "session-a", limit: 1, sourceRunId: "source", runId: "next", asyncDir: nextDir }, { rootDir });
251
+ assert.ok(next);
252
+
253
+ const inspection = inspectActiveAsyncCapacityOwner({ runId: "source", sessionId: "session-a", asyncDir: sourceDir }, { rootDir });
254
+
255
+ assert.equal(inspection.relation, "source");
256
+ assert.equal(inspection.owner?.runId, "next");
257
+ assert.equal(inspection.release.state, "not-owned");
258
+ assert.match(inspection.release.reason, /transferred to next/);
259
+ } finally {
260
+ fs.rmSync(rootDir, { recursive: true, force: true });
261
+ }
262
+ });
263
+
214
264
  it("transfers a paused reservation without changing usage", () => {
215
265
  const rootDir = tempRoot();
216
266
  const sourceDir = path.join(rootDir, "runs", "source");
@@ -472,6 +472,13 @@ Do work
472
472
  }
473
473
  const oracle = agents.find((candidate) => candidate.name === "oracle");
474
474
  assert.deepEqual(oracle?.aliases, ["advisor"]);
475
+ assert.doesNotMatch(oracle?.tools?.join(",") ?? "", /contact_supervisor/);
476
+ for (const name of ["scout", "researcher", "oracle", "reviewer"]) {
477
+ assert.equal(agents.find((candidate) => candidate.name === name)?.tools?.includes("intercom"), false, `${name} should not require generic intercom`);
478
+ }
479
+ assert.match(oracle?.systemPrompt ?? "", /asking or consulting the oracle/);
480
+ assert.match(oracle?.systemPrompt ?? "", /When runtime bridge instructions provide `contact_supervisor`/);
481
+ assert.match(oracle?.systemPrompt ?? "", /If no supervisor channel is available/);
475
482
  assert.equal(agents.some((candidate) => candidate.name === "planner"), false);
476
483
  assert.equal(agents.some((candidate) => candidate.name === "context-builder"), false);
477
484
  });
@@ -493,6 +500,7 @@ describe("agent frontmatter launch defaults", () => {
493
500
  filePath,
494
501
  defaultAsync: false,
495
502
  defaultTimeoutMs: 90_000,
503
+ defaultToolTimeoutMs: 600_000,
496
504
  defaultTurnBudget: { maxTurns: 12, graceTurns: 2 },
497
505
  defaultAcceptance: { level: "none", reason: "lightweight lookup" },
498
506
  };
@@ -500,6 +508,7 @@ describe("agent frontmatter launch defaults", () => {
500
508
  const serialized = serializeAgent(agent);
501
509
  assert.match(serialized, /^async: false$/m);
502
510
  assert.match(serialized, /^timeoutMs: 90000$/m);
511
+ assert.match(serialized, /^toolTimeoutMs: 600000$/m);
503
512
  assert.match(serialized, /^turnBudget: \{"maxTurns":12,"graceTurns":2\}$/m);
504
513
  assert.match(serialized, /^acceptance: \{"level":"none","reason":"lightweight lookup"\}$/m);
505
514
  writeAgent(filePath, serialized);
@@ -507,6 +516,7 @@ describe("agent frontmatter launch defaults", () => {
507
516
  const worker = discoverAgents(dir, "project").agents.find((candidate) => candidate.name === "worker");
508
517
  assert.equal(worker?.defaultAsync, false);
509
518
  assert.equal(worker?.defaultTimeoutMs, 90_000);
519
+ assert.equal(worker?.defaultToolTimeoutMs, 600_000);
510
520
  assert.deepEqual(worker?.defaultTurnBudget, { maxTurns: 12, graceTurns: 2 });
511
521
  assert.deepEqual(worker?.defaultAcceptance, { level: "none", reason: "lightweight lookup" });
512
522
  });
@@ -601,6 +611,24 @@ Do work
601
611
  /Agent 'worker' has invalid async frontmatter; expected true or false/,
602
612
  );
603
613
  });
614
+
615
+ it("rejects oversized toolTimeoutMs frontmatter at discovery", () => {
616
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-oversized-tool-timeout-"));
617
+ tempDirs.push(dir);
618
+ writeAgent(path.join(dir, ".selesai", "agents", "worker.md"), `---
619
+ name: worker
620
+ description: Worker
621
+ toolTimeoutMs: 2147483648
622
+ ---
623
+
624
+ Do work
625
+ `);
626
+
627
+ assert.throws(
628
+ () => discoverAgents(dir, "project"),
629
+ /Agent 'worker' has invalid toolTimeoutMs frontmatter; expected a positive integer no larger than 2147483647/,
630
+ );
631
+ });
604
632
  });
605
633
 
606
634
  describe("chain discovery", () => {
@@ -88,6 +88,67 @@ describe("builtin agent overrides", () => {
88
88
  assert.equal(reviewer?.modelSource, undefined);
89
89
  });
90
90
 
91
+ it("lets a builtin agent inherit Pi's normal tools from an override", () => {
92
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
93
+ subagents: {
94
+ agentOverrides: {
95
+ researcher: { tools: "inherit" },
96
+ },
97
+ },
98
+ });
99
+
100
+ const researcher = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "researcher");
101
+ assert.equal(researcher?.tools, undefined);
102
+ assert.equal(researcher?.mcpDirectTools, undefined);
103
+ });
104
+
105
+ it("keeps strict builtin tools unless a role opts into inheritance", () => {
106
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
107
+ subagents: {
108
+ agentOverrides: {
109
+ researcher: { tools: "inherit" },
110
+ },
111
+ },
112
+ });
113
+
114
+ const builtins = discoverAgentsAll(tempProject).builtin;
115
+ assert.equal(builtins.find((agent) => agent.name === "researcher")?.tools, undefined);
116
+ assert.deepEqual(builtins.find((agent) => agent.name === "reviewer")?.tools, ["read", "grep", "find", "ls"]);
117
+ });
118
+
119
+ it("keeps explicit empty builtin tool allowlists distinct from inherited tools", () => {
120
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
121
+ subagents: {
122
+ agentOverrides: {
123
+ researcher: { tools: [] },
124
+ },
125
+ },
126
+ });
127
+
128
+ const researcher = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "researcher");
129
+ assert.deepEqual(researcher?.tools, []);
130
+ assert.equal(researcher?.mcpDirectTools, undefined);
131
+ });
132
+
133
+ it("surfaces invalid string tool override settings", () => {
134
+ const settingsPath = path.join(tempHome, ".selesai", "agent", "settings.json");
135
+ writeJson(settingsPath, {
136
+ subagents: {
137
+ agentOverrides: {
138
+ researcher: { tools: "read" },
139
+ },
140
+ },
141
+ });
142
+
143
+ assert.throws(
144
+ () => discoverAgents(tempProject, "both"),
145
+ (error: unknown) => error instanceof Error
146
+ && error.message.includes(settingsPath)
147
+ && error.message.includes("researcher")
148
+ && error.message.includes("tools"),
149
+ );
150
+ });
151
+
91
152
  it("clears subagents.defaultModel provenance for same-value agent model overrides", () => {
92
153
  writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
93
154
  subagents: {
@@ -0,0 +1,14 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type { StreamFn } from "@earendil-works/pi-agent-core";
4
+ import { agentStreamOptions } from "../../src/shared/agent-stream-options.ts";
5
+
6
+ describe("agent stream options", () => {
7
+ it("supports the Pi 0.81 and 0.84 constructor option names", () => {
8
+ const streamFn: StreamFn = () => { throw new Error("not called"); };
9
+ const options = agentStreamOptions(streamFn);
10
+
11
+ assert.equal(options.streamFunction, streamFn);
12
+ assert.equal(options.streamFn, streamFn);
13
+ });
14
+ });