@selesai/code 0.8.6 → 0.8.8

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 (234) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/dist/cli/args.d.ts +1 -0
  3. package/dist/cli/args.js +11 -0
  4. package/dist/cli/args.test.d.ts +1 -0
  5. package/dist/cli/args.test.js +24 -0
  6. package/dist/core/agent-session.d.ts +4 -1
  7. package/dist/core/agent-session.js +18 -7
  8. package/dist/core/experimental.d.ts +4 -0
  9. package/dist/core/experimental.js +4 -0
  10. package/dist/core/extensions/types.d.ts +6 -0
  11. package/dist/core/keybindings.d.ts +24 -0
  12. package/dist/core/model-config.d.ts +5 -0
  13. package/dist/core/model-config.js +1 -0
  14. package/dist/core/project-trust.js +2 -2
  15. package/dist/core/provider-composer.js +17 -4
  16. package/dist/core/sdk.d.ts +5 -3
  17. package/dist/core/sdk.js +2 -1
  18. package/dist/core/session-manager.js +2 -2
  19. package/dist/core/settings-manager.d.ts +8 -0
  20. package/dist/core/settings-manager.js +15 -0
  21. package/dist/core/system-prompt.js +3 -9
  22. package/dist/core/system-prompt.test.js +6 -0
  23. package/dist/core/tools/bash.js +2 -0
  24. package/dist/core/tools/edit.js +2 -0
  25. package/dist/core/tools/find.js +1 -1
  26. package/dist/core/tools/grep.js +1 -1
  27. package/dist/core/tools/index.d.ts +1 -0
  28. package/dist/core/tools/index.js +1 -0
  29. package/dist/core/tools/read.js +2 -0
  30. package/dist/core/tools/schema-prune.d.ts +16 -0
  31. package/dist/core/tools/schema-prune.js +39 -0
  32. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  33. package/dist/core/tools/schema-prune.test.js +111 -0
  34. package/dist/core/tools/write.js +2 -0
  35. package/dist/defaults/settings.json +1 -0
  36. package/dist/extensions/model-prompt-injector/config.json +10 -0
  37. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  38. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  39. package/dist/extensions/package.json +1 -0
  40. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  41. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  42. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  43. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  44. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  45. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  46. package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
  47. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  48. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  49. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  50. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  51. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  52. package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
  53. package/dist/extensions/pi-subagents/install.mjs +2 -2
  54. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  55. package/dist/extensions/pi-subagents/package.json +1 -1
  56. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  57. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  58. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
  59. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  60. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  61. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  62. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  63. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  64. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  65. package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
  66. package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
  67. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  68. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  69. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  70. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  71. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  72. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  73. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  74. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  75. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  76. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  77. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  78. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  79. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  80. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  81. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  82. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
  83. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  84. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  85. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
  86. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  87. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  88. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  89. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  90. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +187 -41
  91. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  92. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  93. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
  94. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
  95. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  96. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  97. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  98. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  99. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  100. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  101. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  102. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  103. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  104. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
  105. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  106. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  108. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  109. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  110. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  112. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  113. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  114. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  115. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  116. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  117. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  118. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  119. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  120. package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
  121. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  122. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  124. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  125. package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
  126. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  127. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  128. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  129. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  130. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  131. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  132. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  133. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  134. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  135. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  136. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  137. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  138. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  139. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  140. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  141. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +395 -98
  142. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  143. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  144. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  145. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  146. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  147. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  148. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  149. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  150. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  151. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  152. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  153. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  154. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  155. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  156. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  157. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  158. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  159. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  160. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
  161. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  162. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  163. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  164. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  165. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  166. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  167. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  168. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  169. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  170. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  171. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  172. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  173. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  174. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  175. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  176. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  177. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  178. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  179. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  180. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  181. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  182. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  183. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  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-chat-progress.test.ts +94 -1
  193. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  194. package/dist/extensions/question/index.ts +21 -6
  195. package/dist/extensions/question/question-list.ts +7 -1
  196. package/dist/extensions/question/row-layout.ts +3 -0
  197. package/dist/extensions/question/tests/exp.test.ts +70 -0
  198. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  199. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  200. package/dist/extensions/question/tests/helpers.test.ts +7 -0
  201. package/dist/extensions/question/tests/question-list.test.ts +140 -0
  202. package/dist/extensions/question/tests/row-layout.test.ts +13 -0
  203. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  204. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  205. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  206. package/dist/extensions/tokenin-onboarding.ts +1 -1
  207. package/dist/index.d.ts +1 -1
  208. package/dist/main.js +4 -0
  209. package/dist/modes/interactive/components/model-selector.js +2 -1
  210. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  211. package/dist/modes/interactive/components/settings-selector.js +10 -0
  212. package/dist/modes/interactive/components/tool-execution.js +10 -1
  213. package/dist/modes/interactive/interactive-mode.d.ts +8 -2
  214. package/dist/modes/interactive/interactive-mode.js +81 -25
  215. package/dist/modes/interactive/model-catalog-refresh.d.ts +6 -0
  216. package/dist/modes/interactive/model-catalog-refresh.js +33 -0
  217. package/dist/modes/interactive/theme/dark.json +2 -0
  218. package/dist/modes/interactive/theme/light.json +2 -0
  219. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  220. package/dist/modes/interactive/theme/theme-controller.js +25 -11
  221. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  222. package/dist/modes/interactive/theme/theme.d.ts +5 -3
  223. package/dist/modes/interactive/theme/theme.js +12 -2
  224. package/dist/skills/pi-subagents/SKILL.md +1 -1
  225. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  226. package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
  227. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  228. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  229. package/dist/utils/abort.d.ts +4 -0
  230. package/dist/utils/abort.js +47 -0
  231. package/dist/utils/tools-manager.d.ts +5 -1
  232. package/dist/utils/tools-manager.js +12 -21
  233. package/docs/settings.md +6 -0
  234. package/package.json +6 -6
@@ -8,6 +8,7 @@ import {
8
8
  formatControlIntercomMessage,
9
9
  formatControlNoticeMessage,
10
10
  resolveControlConfig,
11
+ shouldEmitOpenToolAttention,
11
12
  shouldNotifyControlEvent,
12
13
  } from "../../src/runs/shared/subagent-control.ts";
13
14
  import { nextLongRunningTrigger } from "../../src/runs/shared/long-running-guard.ts";
@@ -99,6 +100,15 @@ describe("subagent control attention state", () => {
99
100
  }), "time_threshold");
100
101
  });
101
102
 
103
+ it("marks non-exempt open tools for attention at the active threshold", () => {
104
+ const defaults = resolveControlConfig();
105
+
106
+ assert.equal(shouldEmitOpenToolAttention({ config: defaults, currentTool: "bash", currentToolStartedAt: 0, now: 239_999 }), false);
107
+ assert.equal(shouldEmitOpenToolAttention({ config: defaults, currentTool: "bash", currentToolStartedAt: 0, now: 240_000 }), true);
108
+ assert.equal(shouldEmitOpenToolAttention({ config: defaults, currentTool: "contact_supervisor", currentToolStartedAt: 0, now: 999_999 }), false);
109
+ assert.equal(shouldEmitOpenToolAttention({ config: { ...defaults, enabled: false }, currentTool: "bash", currentToolStartedAt: 0, now: 999_999 }), false);
110
+ });
111
+
102
112
  it("supports opt-in turn and token long-running thresholds", () => {
103
113
  const tokenBudget = resolveControlConfig(undefined, { activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 500_000 });
104
114
  const turnBudget = resolveControlConfig(undefined, { activeNoticeAfterMs: 999_999, activeNoticeAfterTurns: 5 });
@@ -172,6 +182,24 @@ describe("subagent control attention state", () => {
172
182
  assert.doesNotMatch(message, /Wait:/);
173
183
  });
174
184
 
185
+ it("formats open-tool attention notices with tool facts", () => {
186
+ const event = buildControlEvent({
187
+ to: "needs_attention",
188
+ runId: "78f659a3",
189
+ agent: "worker",
190
+ reason: "tool_open_threshold",
191
+ message: "worker has had tool 'bash' open for 240s",
192
+ currentTool: "bash",
193
+ currentToolDurationMs: 240_000,
194
+ currentPath: "scripts/run-tests.sh",
195
+ });
196
+
197
+ const message = formatControlNoticeMessage(event, "subagent-worker-78f659a3");
198
+
199
+ assert.match(message, /worker has had tool 'bash' open for 240s/);
200
+ assert.match(message, /Facts: tool bash 240s \| path scripts\/run-tests\.sh/);
201
+ });
202
+
175
203
  it("formats supervisor-request notices with pending-channel guidance", () => {
176
204
  const event = buildControlEvent({
177
205
  to: "needs_attention",
@@ -595,9 +595,13 @@ describe("subagent prompt runtime", () => {
595
595
 
596
596
  it("registers child watchdog lifecycle handlers only when enabled by env", () => {
597
597
  delete process.env[CHILD_WATCHDOG_CONFIG_ENV];
598
- // Clear the ack capture env explicitly: when this test suite itself runs inside a
599
- // pi-subagents child, the runner sets it and an extra agent_end handler registers.
598
+ // Clear the ack capture and steer env vars explicitly: when this test suite itself
599
+ // runs inside a pi-subagents child, the runner sets them and extra agent_end
600
+ // handlers register (steering inbox is gated on SUBAGENT_STEER_INBOX_ENV).
600
601
  delete process.env[RUNTIME_EXTENSION_ACK_PATH_ENV];
602
+ delete process.env[SUBAGENT_STEER_INBOX_ENV];
603
+ delete process.env[SUBAGENT_STEER_CAPABILITY_ENV];
604
+ delete process.env[SUBAGENT_STEER_ACK_DIR_ENV];
601
605
  const handlersWithout = new Map<string, unknown[]>();
602
606
  registerSubagentPromptRuntime({
603
607
  on(event: string, handler: unknown) {
@@ -922,7 +926,7 @@ describe("subagent prompt runtime", () => {
922
926
  assert.deepEqual(registered, ["subagent_wait"]);
923
927
  });
924
928
 
925
- it("registers native intercom before the final strict allowlist check", () => {
929
+ it("does not satisfy strict allowlists with native generic intercom", () => {
926
930
  setSupervisorEnv();
927
931
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagent-intercom-diagnostic-"));
928
932
  try {
@@ -944,9 +948,14 @@ describe("subagent prompt runtime", () => {
944
948
  } as { on(event: string, handler: (payload?: unknown) => unknown): void; getAllTools(): Array<{ name: string }>; registerTool(tool: { name: string }): void });
945
949
 
946
950
  handlers.get("session_start")?.({});
947
- assert.deepEqual(registered, ["subagent_wait", "contact_supervisor", "intercom"]);
951
+ assert.deepEqual(registered, ["subagent_wait", "contact_supervisor"]);
948
952
  handlers.get("agent_start")?.({});
949
- assert.equal(fs.existsSync(diagnosticPath), false);
953
+ assert.deepEqual(readChildToolDiagnostic(diagnosticPath), {
954
+ agent: "scout",
955
+ required: ["read", "grep", "find", "ls", "bash", "edit", "write", "intercom"],
956
+ available: ["subagent_wait", "contact_supervisor"],
957
+ missing: ["intercom"],
958
+ });
950
959
  } finally {
951
960
  fs.rmSync(dir, { recursive: true, force: true });
952
961
  }
@@ -973,7 +982,7 @@ describe("subagent prompt runtime", () => {
973
982
  assert.deepEqual(registered, ["subagent_wait", "contact_supervisor"]);
974
983
  });
975
984
 
976
- it("registers native supervisor tools at runtime when pi-intercom is absent", async () => {
985
+ it("registers only native supervisor tools at runtime when pi-intercom is absent", async () => {
977
986
  setSupervisorEnv();
978
987
  const previousRequiredTools = process.env[REQUIRED_CHILD_TOOLS_ENV];
979
988
  delete process.env[REQUIRED_CHILD_TOOLS_ENV];
@@ -995,7 +1004,7 @@ describe("subagent prompt runtime", () => {
995
1004
  assert.deepEqual(registered, ["subagent_wait", "contact_supervisor"]);
996
1005
 
997
1006
  await handlers.get("before_agent_start")?.({ systemPrompt: BASE_PROMPT });
998
- assert.deepEqual(registered, ["subagent_wait", "contact_supervisor", "intercom"]);
1007
+ assert.deepEqual(registered, ["subagent_wait", "contact_supervisor"]);
999
1008
  } finally {
1000
1009
  if (previousRequiredTools === undefined) delete process.env[REQUIRED_CHILD_TOOLS_ENV];
1001
1010
  else process.env[REQUIRED_CHILD_TOOLS_ENV] = previousRequiredTools;
@@ -4,6 +4,7 @@ import type { Usage } from "../../src/shared/types.ts";
4
4
  import {
5
5
  MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS,
6
6
  SUBAGENT_STARTUP_RETRY_DELAYS_MS,
7
+ formatSubagentExtensionConflictError,
7
8
  formatSubagentStartupRetryExhaustedError,
8
9
  formatSubagentStartupRetryNote,
9
10
  isRetryableSubagentStartupFailure,
@@ -69,6 +70,20 @@ describe("subagent startup retry", () => {
69
70
  assert.equal(isRetryableSubagentStartupFailure(startupFailure({ turnBudgetExceeded: true })), false);
70
71
  });
71
72
 
73
+ it("adds actionable guidance for ambient extension registration conflicts", () => {
74
+ const conflict = 'Error: Failed to load extension "/tmp/pi-mcp-adapter-clone/index.ts": Tool "mcpScript" conflicts with /tmp/pi-mcp-adapter/index.ts\nError: Failed to load extension "/tmp/pi-mcp-adapter-clone/index.ts": Flag "--mcp-config" conflicts with /tmp/pi-mcp-adapter/index.ts';
75
+ const formatted = formatSubagentExtensionConflictError(conflict, {
76
+ agent: "reviewer",
77
+ ambientExtensionsEnabled: true,
78
+ });
79
+
80
+ assert.match(formatted ?? "", /loaded conflicting ambient Pi extensions/);
81
+ assert.match(formatted ?? "", /\.pi\/settings\.json/);
82
+ assert.match(formatted ?? "", /\{"subagents":\{"agentOverrides":\{"reviewer":\{"extensions":\[\]\}\}\}\}/);
83
+ assert.equal(formatSubagentExtensionConflictError(conflict, { agent: "reviewer", ambientExtensionsEnabled: false }), conflict);
84
+ assert.equal(formatSubagentExtensionConflictError("authentication failed", { agent: "reviewer", ambientExtensionsEnabled: true }), "authentication failed");
85
+ });
86
+
72
87
  it("formats retry and exhaustion diagnostics without task content", () => {
73
88
  assert.equal(
74
89
  formatSubagentStartupRetryNote({ model: "openai/test", attempt: 1, maxAttempts: 4, delayMs: 250 }),
@@ -4,6 +4,7 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { describe, it } from "node:test";
6
6
  import { updateActiveRunIndex } from "../../src/runs/background/active-run-index.ts";
7
+ import { writeAsyncResultFile } from "../../src/runs/background/result-files.ts";
7
8
  import { WAIT_TOOL_ENABLED_ENV, resolveWaitToolConfig, waitForSubagents, type SubagentWaitDeps } from "../../src/runs/background/subagent-wait.ts";
8
9
  import { recordWaitCompletion } from "../../src/runs/background/wait-completions.ts";
9
10
  import type { AsyncStatus, SubagentState } from "../../src/shared/types.ts";
@@ -218,6 +219,32 @@ describe("subagent_wait tool", () => {
218
219
  }
219
220
  });
220
221
 
222
+ it("tells blocking callers to reply and wait for intercom-detached failed runs", async () => {
223
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-intercom-detach-"));
224
+ try {
225
+ const asyncRoot = path.join(root, "runs");
226
+ const sessionFile = path.join(root, "worker-session.jsonl");
227
+ fs.writeFileSync(sessionFile, "{}\n", "utf-8");
228
+ const state = makeState("sess-1");
229
+ writeStatus(asyncRoot, "run-detached", "running", { sessionId: "sess-1", pid: 999999 });
230
+ const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, {
231
+ sleep: async () => writeStatus(asyncRoot, "run-detached", "failed", {
232
+ sessionId: "sess-1",
233
+ error: "Step failed: worker",
234
+ steps: [{ agent: "worker", status: "failed", sessionFile, error: "Detached for intercom coordination before task completion." }],
235
+ }),
236
+ }));
237
+
238
+ const text = textOf(result);
239
+ assert.match(text, /Reply to the supervisor request first/);
240
+ assert.match(text, /wait with subagent_wait/);
241
+ assert.match(text, /do not resume or launch a replacement/);
242
+ assert.doesNotMatch(text, /Resume-first/);
243
+ } finally {
244
+ fs.rmSync(root, { recursive: true, force: true });
245
+ }
246
+ });
247
+
221
248
  it("surfaces terminal completion payloads from the result file in details.completions", async () => {
222
249
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-completions-file-"));
223
250
  try {
@@ -270,6 +297,45 @@ describe("subagent_wait tool", () => {
270
297
  }
271
298
  });
272
299
 
300
+ it("surfaces terminal completion payloads from an indexed pending result file", async () => {
301
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-completions-pending-"));
302
+ const originalError = console.error;
303
+ try {
304
+ console.error = () => {};
305
+ const asyncRoot = path.join(root, "runs");
306
+ const resultsDir = path.join(root, "results");
307
+ const publicResultPath = path.join(resultsDir, "run-pending.json");
308
+ fs.mkdirSync(publicResultPath, { recursive: true });
309
+ const state = makeState("sess-1");
310
+ writeStatus(asyncRoot, "run-pending", "running", { sessionId: "sess-1", pid: 999999 });
311
+
312
+ const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, {
313
+ sleep: async () => {
314
+ writeStatus(asyncRoot, "run-pending", "complete", { sessionId: "sess-1" });
315
+ assert.deepEqual(writeAsyncResultFile(publicResultPath, {
316
+ id: "run-pending",
317
+ runId: "run-pending",
318
+ sessionId: "sess-1",
319
+ agent: "worker",
320
+ mode: "single",
321
+ state: "complete",
322
+ success: true,
323
+ results: [{ agent: "worker", success: true, outputState: "present" }],
324
+ }), { state: "pending" });
325
+ },
326
+ }));
327
+
328
+ assert.equal(result.isError, undefined);
329
+ assert.equal(result.details.completions?.[0]?.runId, "run-pending");
330
+ assert.equal(result.details.completions?.[0]?.success, true);
331
+ assert.equal(result.details.completions?.[0]?.results?.[0]?.agent, "worker");
332
+ assert.equal(fs.statSync(publicResultPath).isDirectory(), true);
333
+ } finally {
334
+ console.error = originalError;
335
+ fs.rmSync(root, { recursive: true, force: true });
336
+ }
337
+ });
338
+
273
339
  it("reports malformed terminal result files as actionable errors", async () => {
274
340
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-completions-malformed-"));
275
341
  try {
@@ -949,33 +1015,31 @@ describe("subagent_wait tool", () => {
949
1015
  },
950
1016
  };
951
1017
 
952
- // A real timer-based sleep with a LONG poll interval; if wait waited for
953
- // the poll it would take ~10s. The event should wake it in ~10ms.
954
1018
  let sleepCalls = 0;
955
- const realSleep = (ms: number, signal?: AbortSignal) => new Promise<void>((resolve) => {
1019
+ let sleepArmed!: () => void;
1020
+ const armed = new Promise<void>((resolve) => { sleepArmed = resolve; });
1021
+ const sleep = (_ms: number, signal?: AbortSignal) => new Promise<void>((resolve) => {
956
1022
  sleepCalls += 1;
957
- const t = setTimeout(resolve, ms);
958
- signal?.addEventListener("abort", () => { clearTimeout(t); resolve(); }, { once: true });
1023
+ signal?.addEventListener("abort", resolve, { once: true });
1024
+ sleepArmed();
959
1025
  });
960
1026
 
961
- const startedAt = Date.now();
962
1027
  const p = waitForSubagents({ all: true }, undefined, baseDeps(root, state, {
963
1028
  events,
964
1029
  pollIntervalMs: 10_000,
965
- sleep: realSleep,
1030
+ sleep,
966
1031
  }));
967
1032
 
968
- // After a short delay, flip the run terminal and emit a completion event.
969
- setTimeout(() => {
970
- writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
971
- events.emit("subagent:async-complete", { id: "run-a" });
972
- }, 15);
1033
+ await armed;
1034
+ writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
1035
+ events.emit("subagent:async-complete", { id: "run-a" });
973
1036
 
974
- const result = await p;
975
- const elapsed = Date.now() - startedAt;
1037
+ const result = await Promise.race([
1038
+ p,
1039
+ new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error("event wake did not resolve subagent_wait")), 1_000)),
1040
+ ]);
976
1041
  assert.equal(result.isError, undefined);
977
1042
  assert.match(textOf(result), /done/i);
978
- assert.ok(elapsed < 5_000, `should wake via event, not the 10s poll; took ${elapsed}ms`);
979
1043
  assert.ok(sleepCalls >= 1, "poll-interval sleep still armed as fallback");
980
1044
  } finally {
981
1045
  fs.rmSync(root, { recursive: true, force: true });
@@ -29,7 +29,7 @@ function parentToolEnv(agentDir?: string): NodeJS.ProcessEnv {
29
29
 
30
30
  describe("registered subagent tool description", () => {
31
31
  it("describes the four first-class execution modes", () => {
32
- const description = buildSubagentToolDescription();
32
+ const description = buildSubagentToolDescription({ toolDescriptionMode: "full" });
33
33
  assert.match(description, /^Delegate one child with \{ agent, task \}/i);
34
34
  assert.match(description, /SINGLE: \{ agent, task\? \}/i);
35
35
  assert.match(description, /CHAIN: \{ chain: \[\{ agent: "explorer"/i);
@@ -49,7 +49,7 @@ describe("registered subagent tool description", () => {
49
49
  });
50
50
 
51
51
  it("includes legacy chain-control guidance when enabled", () => {
52
- const description = buildSubagentToolDescription({ legacyChainControls: true });
52
+ const description = buildSubagentToolDescription({ toolDescriptionMode: "full", legacyChainControls: true });
53
53
  assert.match(description, /append-step.*step:/i);
54
54
  assert.match(description, /approve-checkpoint|reject-checkpoint/);
55
55
  });
@@ -147,7 +147,7 @@ describe("registered subagent tool description", () => {
147
147
  assert.doesNotMatch(description, /decide a paused durable legacy chain checkpoint/);
148
148
  });
149
149
 
150
- it("falls back to full mode when custom mode has no valid file", () => {
150
+ it("falls back to compact mode when custom mode has no valid file", () => {
151
151
  const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-missing-"));
152
152
  const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-agent-"));
153
153
  const warnings: string[] = [];
@@ -157,11 +157,11 @@ describe("registered subagent tool description", () => {
157
157
  { cwd, agentDir, warn: (message) => warnings.push(message) },
158
158
  );
159
159
 
160
- assert.equal(description, FULL_SUBAGENT_TOOL_DESCRIPTION);
161
- assert.ok(warnings.some((message) => message.includes("using full description")));
160
+ assert.equal(description, COMPACT_SUBAGENT_TOOL_DESCRIPTION);
161
+ assert.ok(warnings.some((message) => message.includes("using compact description")));
162
162
  });
163
163
 
164
- it("falls back to full mode when toolDescriptionMode is invalid", () => {
164
+ it("falls back to compact mode when toolDescriptionMode is invalid", () => {
165
165
  const warnings: string[] = [];
166
166
 
167
167
  const description = buildSubagentToolDescription(
@@ -169,7 +169,7 @@ describe("registered subagent tool description", () => {
169
169
  { warn: (message) => warnings.push(message) },
170
170
  );
171
171
 
172
- assert.equal(description, FULL_SUBAGENT_TOOL_DESCRIPTION);
172
+ assert.equal(description, COMPACT_SUBAGENT_TOOL_DESCRIPTION);
173
173
  assert.ok(warnings.some((message) => message.includes("Ignoring invalid toolDescriptionMode")));
174
174
  });
175
175
 
@@ -219,7 +219,7 @@ describe("registered subagent tool description", () => {
219
219
 
220
220
  it("registers full, compact, custom, and fallback descriptions from extension config", () => {
221
221
  const defaultAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-default-"));
222
- writeExtensionConfig(defaultAgentDir, { legacyChainControls: true });
222
+ writeExtensionConfig(defaultAgentDir, { toolDescriptionMode: "full", legacyChainControls: true });
223
223
  assert.equal(readRegisteredTool(defaultAgentDir).description, FULL_SUBAGENT_TOOL_DESCRIPTION);
224
224
 
225
225
  const compactAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-compact-"));
@@ -235,20 +235,20 @@ describe("registered subagent tool description", () => {
235
235
 
236
236
  const missingCustomAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-missing-"));
237
237
  writeExtensionConfig(missingCustomAgentDir, { toolDescriptionMode: "custom", legacyChainControls: true });
238
- assert.equal(readRegisteredTool(missingCustomAgentDir).description, FULL_SUBAGENT_TOOL_DESCRIPTION);
238
+ assert.equal(readRegisteredTool(missingCustomAgentDir).description, COMPACT_SUBAGENT_TOOL_DESCRIPTION);
239
239
 
240
240
  const invalidAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-invalid-"));
241
241
  writeExtensionConfig(invalidAgentDir, { toolDescriptionMode: "tiny", legacyChainControls: true });
242
- assert.equal(readRegisteredTool(invalidAgentDir).description, FULL_SUBAGENT_TOOL_DESCRIPTION);
242
+ assert.equal(readRegisteredTool(invalidAgentDir).description, COMPACT_SUBAGENT_TOOL_DESCRIPTION);
243
243
  });
244
244
 
245
- it("registers the full schema and first-class chain description by default", () => {
245
+ it("registers the full schema and chain description by default", () => {
246
246
  const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-trimmed-"));
247
247
  const tool = readRegisteredTool(agentDir);
248
248
  assert.equal(tool.properties.includes("step"), true);
249
249
  assert.equal(tool.properties.includes("chain"), true);
250
250
  assert.equal(tool.properties.includes("tasks"), true);
251
- assert.match(tool.description, /CHAIN: \{ chain:/i);
251
+ assert.match(tool.description, /CHAIN \{chain:/i);
252
252
  });
253
253
 
254
254
  it("registers the trimmed schema and description when legacyChainControls is false", () => {
@@ -0,0 +1,109 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ DEFAULT_FAST_TOOL_TIMEOUT_MS,
5
+ DEFAULT_FAST_TOOL_TIMEOUT_TOOLS,
6
+ TOOL_TIMEOUT_ALLOWLIST,
7
+ TOOL_TIMEOUT_ENV,
8
+ defaultToolTimeoutMs,
9
+ effectiveToolTimeoutMs,
10
+ formatToolTimeoutMessage,
11
+ isToolTimeoutExempt,
12
+ resolveToolTimeoutMs,
13
+ toolTimeoutCallKey,
14
+ toolTimeoutFromEnv,
15
+ } from "../../src/runs/shared/tool-timeout.ts";
16
+
17
+ describe("resolveToolTimeoutMs", () => {
18
+ it("has no configured hard timeout when nothing is configured anywhere", () => {
19
+ assert.deepEqual(resolveToolTimeoutMs({}), {});
20
+ });
21
+
22
+ it("honors precedence: per-call > agent frontmatter > config > env", () => {
23
+ const input = {
24
+ callValue: 1_000,
25
+ agentValue: 2_000,
26
+ configValue: 3_000,
27
+ envValue: "4000",
28
+ };
29
+ assert.deepEqual(resolveToolTimeoutMs(input), { toolTimeoutMs: 1_000 });
30
+ assert.deepEqual(resolveToolTimeoutMs({ ...input, callValue: undefined }), { toolTimeoutMs: 2_000 });
31
+ assert.deepEqual(resolveToolTimeoutMs({ ...input, callValue: undefined, agentValue: undefined }), {
32
+ toolTimeoutMs: 3_000,
33
+ });
34
+ assert.deepEqual(
35
+ resolveToolTimeoutMs({ ...input, callValue: undefined, agentValue: undefined, configValue: undefined }),
36
+ { toolTimeoutMs: 4_000 },
37
+ );
38
+ });
39
+
40
+ it("accepts the env value as a plain integer string", () => {
41
+ assert.deepEqual(
42
+ resolveToolTimeoutMs({ envValue: "60000", callValue: undefined, agentValue: undefined, configValue: undefined }),
43
+ { toolTimeoutMs: 60_000 },
44
+ );
45
+ });
46
+
47
+ it("rejects non-positive, non-integer, and oversized values with an error", () => {
48
+ for (const bad of [0, -1, 1.5, "abc", Number.NaN]) {
49
+ const r = resolveToolTimeoutMs({ callValue: bad });
50
+ assert.ok(r.error, `expected error for ${String(bad)}`);
51
+ assert.match(r.error!, /positive integer/);
52
+ }
53
+ const huge = resolveToolTimeoutMs({ callValue: 2_147_483_648 });
54
+ assert.ok(huge.error);
55
+ assert.match(huge.error!, /no larger than/);
56
+ });
57
+
58
+ it("ignores an empty env string (treated as unset)", () => {
59
+ assert.deepEqual(resolveToolTimeoutMs({ envValue: " " }), {});
60
+ });
61
+ });
62
+
63
+ describe("effectiveToolTimeoutMs", () => {
64
+ it("uses a default hard timeout only for known-fast tools", () => {
65
+ for (const tool of ["read", "grep", "find", "ls", "edit", "write", "structured_output"]) {
66
+ assert.equal(defaultToolTimeoutMs(tool), DEFAULT_FAST_TOOL_TIMEOUT_MS, tool);
67
+ assert.equal(effectiveToolTimeoutMs(tool, undefined), DEFAULT_FAST_TOOL_TIMEOUT_MS, tool);
68
+ }
69
+ assert.equal(defaultToolTimeoutMs("bash"), undefined);
70
+ assert.equal(effectiveToolTimeoutMs("bash", undefined), undefined);
71
+ assert.deepEqual([...DEFAULT_FAST_TOOL_TIMEOUT_TOOLS].sort(), ["edit", "find", "grep", "ls", "read", "structured_output", "write"]);
72
+ });
73
+
74
+ it("lets an explicit configured timeout win for non-exempt tools", () => {
75
+ assert.equal(effectiveToolTimeoutMs("bash", 1234), 1234);
76
+ assert.equal(effectiveToolTimeoutMs("read", 1234), 1234);
77
+ });
78
+
79
+ it("never applies a tool timeout to blocking coordination tools", () => {
80
+ for (const tool of ["contact_supervisor", "intercom", "subagent_wait"]) {
81
+ assert.equal(isToolTimeoutExempt(tool), true, `${tool} must be exempt`);
82
+ assert.equal(effectiveToolTimeoutMs(tool, 1234), undefined);
83
+ assert.equal(effectiveToolTimeoutMs(tool, undefined), undefined);
84
+ }
85
+ assert.equal(isToolTimeoutExempt("bash"), false);
86
+ assert.equal(isToolTimeoutExempt(undefined), false);
87
+ assert.deepEqual([...TOOL_TIMEOUT_ALLOWLIST].sort(), ["contact_supervisor", "intercom", "subagent_wait"]);
88
+ });
89
+ });
90
+
91
+ describe("tool timeout formatting and keys", () => {
92
+ it("formats timeout errors consistently", () => {
93
+ assert.equal(formatToolTimeoutMessage("bash", 1000), "Tool 'bash' exceeded its timeout of 1000ms.");
94
+ });
95
+
96
+ it("uses toolCallId when present and falls back to an anonymous sequence", () => {
97
+ assert.equal(toolTimeoutCallKey({ toolCallId: "call-1", toolName: "read" }, 9), "id:call-1");
98
+ assert.equal(toolTimeoutCallKey({ toolName: "read" }, 9), "anon:read:9");
99
+ });
100
+ });
101
+
102
+ describe("toolTimeoutFromEnv", () => {
103
+ it("reads SELESAI_SUBAGENT_TOOL_TIMEOUT_MS from the process env, with PI_ fallback", () => {
104
+ assert.equal(TOOL_TIMEOUT_ENV, "SELESAI_SUBAGENT_TOOL_TIMEOUT_MS");
105
+ assert.equal(toolTimeoutFromEnv({ SELESAI_SUBAGENT_TOOL_TIMEOUT_MS: "123" }), "123");
106
+ assert.equal(toolTimeoutFromEnv({ PI_SUBAGENT_TOOL_TIMEOUT_MS: "456" }), "456");
107
+ assert.equal(toolTimeoutFromEnv({}), undefined);
108
+ });
109
+ });
@@ -235,6 +235,40 @@ describe("non-blocking wait subscriptions", () => {
235
235
  }
236
236
  });
237
237
 
238
+ it("tells the parent to reply and wait for an intercom-detached failed async run", () => {
239
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-subscribe-intercom-detach-"));
240
+ const asyncRoot = path.join(root, "runs");
241
+ const subscriptionsDir = path.join(root, "subscriptions");
242
+ const sessionFile = path.join(root, "worker-session.jsonl");
243
+ const sent: string[] = [];
244
+ const state = makeState();
245
+ const manager = createWaitSubscriptionManager({
246
+ events: new TestBus(),
247
+ sendMessage(message: { content?: unknown }) { sent.push(String(message.content)); },
248
+ } as never, state, { asyncDirRoot: asyncRoot, subscriptionsDir, pollIntervalMs: 60_000, kill: () => true });
249
+ try {
250
+ fs.writeFileSync(sessionFile, "{}\n", "utf-8");
251
+ writeStatus(asyncRoot, "run-detached", "running", { sessionId: "session-a", pid: 999_999 });
252
+ manager.arm({ targetKind: "async", runId: "run-detached", requestedId: "run-detached", timeoutMs: 30_000 });
253
+
254
+ writeStatus(asyncRoot, "run-detached", "failed", {
255
+ sessionId: "session-a",
256
+ error: "Step failed: worker",
257
+ steps: [{ agent: "worker", status: "failed", sessionFile, error: "Detached for intercom coordination before task completion." }],
258
+ });
259
+ manager.reconcile();
260
+
261
+ const message = sent[0] ?? "";
262
+ assert.match(message, /Reply to the supervisor request first/);
263
+ assert.match(message, /wait with subagent_wait/);
264
+ assert.match(message, /do not resume or launch a replacement/);
265
+ assert.doesNotMatch(message, /Resume-first/);
266
+ } finally {
267
+ manager.dispose();
268
+ fs.rmSync(root, { recursive: true, force: true });
269
+ }
270
+ });
271
+
238
272
  it("waits for foreground run restoration before reconciling a restored subscription", () => {
239
273
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-subscribe-foreground-restore-"));
240
274
  const subscriptionsDir = path.join(root, "subscriptions");
@@ -4,7 +4,7 @@ import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { describe, it } from "node:test";
7
- import { isSameGitRepository, resolveWorkflowChatProgress } from "../../src/workflows/chat-progress.ts";
7
+ import { buildWorkflowChatProgressRows, isSameGitRepository, resolveWorkflowChatProgress } from "../../src/workflows/chat-progress.ts";
8
8
  import { renderSubagentResult } from "../../src/tui/render.ts";
9
9
  import { bindMissionWorkflowChildAsyncLaunch, createSubagentExecutor, foregroundResultIntercomStatus, missionWorkflowChildStatus, runMissionWorkflowChild, shouldSuppressRoutineResultIntercom } from "../../src/runs/foreground/subagent-executor.ts";
10
10
  import { readMissionBinding } from "../../src/missions/lifecycle.ts";
@@ -202,6 +202,38 @@ describe("workflow chat progress rendering", () => {
202
202
  }
203
203
  });
204
204
 
205
+ it("warns once when a workflow child outlives its mission record", async () => {
206
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-workflow-missing-mission-"));
207
+ const originalWarn = console.warn;
208
+ const warnings: string[] = [];
209
+ try {
210
+ const location = {
211
+ projectRoot: root,
212
+ missionDir: path.join(root, ".pi/subagents", "missions"),
213
+ globalIndexDir: path.join(root, ".pi/subagents", "mission-index"),
214
+ writeGlobalIndex: false,
215
+ };
216
+ const mission = createMission(location, { title: "Workflow", objective: "Track child" });
217
+ fs.rmSync(path.join(location.missionDir, `${mission.id}.json`));
218
+ console.warn = (...args: unknown[]) => warnings.push(args.map(String).join(" "));
219
+
220
+ for (const key of ["first", "second"]) {
221
+ await assert.rejects(
222
+ () => runMissionWorkflowChild({ missionId: mission.id, location, autoCreated: false }, "wf-missing-mission", key, "Prep", async () => {
223
+ throw new Error("child failed");
224
+ }),
225
+ /child failed/,
226
+ );
227
+ }
228
+
229
+ assert.equal(warnings.length, 1);
230
+ assert.match(warnings[0] ?? "", new RegExp(`Mission '${mission.id}' is no longer in the mission store`));
231
+ } finally {
232
+ console.warn = originalWarn;
233
+ fs.rmSync(root, { recursive: true, force: true });
234
+ }
235
+ });
236
+
205
237
  it("renders stable rows keyed by workflow trace entries", () => {
206
238
  const text = componentText(renderSubagentResult({
207
239
  content: [{ type: "text", text: "Workflow running." }],
@@ -215,6 +247,7 @@ describe("workflow chat progress rendering", () => {
215
247
  { operation: "run", key: "scout", state: "completed", runId: "run-scout", phase: "Validation", label: "Found renderer seam", durationMs: 12 },
216
248
  { operation: "run", key: "tests", state: "started", phase: "Validation", label: "focused integration suite" },
217
249
  { operation: "run", key: "review", state: "failed", phase: "Validation", label: "fresh-context UX review", error: "needs fixes" },
250
+ { operation: "run", key: "stale", state: "stopped", phase: "Validation", label: "superseded exact-head review", error: "Workflow stopped by user." },
218
251
  ],
219
252
  emits: [],
220
253
  console: [],
@@ -228,6 +261,66 @@ describe("workflow chat progress rendering", () => {
228
261
  assert.match(text, /complete\s+scout Found renderer seam/);
229
262
  assert.match(text, /running\s+tests focused integration suite/);
230
263
  assert.match(text, /failed\s+review fresh-context UX review .* needs fixes/);
264
+ assert.match(text, /stopped\s+stale superseded exact-head review .* Workflow stopped by user/);
265
+ });
266
+
267
+ it("renders detached workflow trace rows as paused attention", () => {
268
+ const trace: NonNullable<Details["workflow"]>["trace"] = [{
269
+ operation: "run",
270
+ key: "detaches",
271
+ state: "detached",
272
+ phase: "Decision",
273
+ label: "supervisor handoff",
274
+ error: "Detached for intercom coordination. Reply to the supervisor request first.",
275
+ }];
276
+ const rows = buildWorkflowChatProgressRows(trace);
277
+ assert.equal(rows[0]?.state, "detached");
278
+
279
+ const text = componentText(renderSubagentResult({
280
+ content: [{ type: "text", text: "Workflow paused." }],
281
+ details: {
282
+ mode: "workflow",
283
+ runId: "wf_detached",
284
+ results: [],
285
+ chatProgress: { mode: "live-card", repoRelation: "same", repoLabel: "pi-subagents" },
286
+ workflow: { trace, emits: [], console: [] },
287
+ },
288
+ }, { expanded: false }, theme as any));
289
+
290
+ assert.match(text, /workflow wf_detached .* same repo .* paused/);
291
+ assert.match(text, /Phase Decision/);
292
+ assert.match(text, /detached\s+detaches supervisor handoff .* Detached for intercom coordination/);
293
+ assert.doesNotMatch(text, /running\s+detaches/);
294
+ assert.doesNotMatch(text, /failed\s+detaches/);
295
+ });
296
+
297
+ it("applies main-window density settings to collapsed workflow live cards", () => {
298
+ const trace = Array.from({ length: 10 }, (_, index) => ({
299
+ operation: "run" as const,
300
+ key: `step-${index}`,
301
+ state: "started" as const,
302
+ label: `review ${index}`,
303
+ }));
304
+ const result = {
305
+ content: [{ type: "text" as const, text: "Workflow running." }],
306
+ details: {
307
+ mode: "workflow" as const,
308
+ runId: "wf_density",
309
+ results: [],
310
+ chatProgress: { mode: "live-card" as const, repoRelation: "same" as const, repoLabel: "pi-subagents" },
311
+ workflow: { trace, emits: [], console: [] },
312
+ },
313
+ };
314
+
315
+ const compact = renderSubagentResult(result, { expanded: false }, theme as any, undefined, { horizontalSpacing: 0, compactResultMaxLines: 3 }).render(120);
316
+ assert.equal(compact.length, 3);
317
+ assert.match(compact[1]!, /^Repo pi-subagents\s*$/);
318
+ assert.match(compact[2]!, /rows hidden/);
319
+
320
+ const expanded = renderSubagentResult(result, { expanded: true }, theme as any, undefined, { horizontalSpacing: 0, compactResultMaxLines: 3 }).render(120);
321
+ assert.ok(expanded.length > 3);
322
+ assert.match(expanded[1]!, /^ Repo pi-subagents\s*$/);
323
+ assert.doesNotMatch(expanded.join("\n"), /rows hidden · .* expands/);
231
324
  });
232
325
 
233
326
  it("bounds workflow live-card rows and keeps old failed children visible", () => {