@selesai/code 0.3.0 → 0.3.1

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 (230) hide show
  1. package/dist/core/diagnostics.d.ts +1 -0
  2. package/dist/core/diagnostics.d.ts.map +1 -1
  3. package/dist/core/diagnostics.js.map +1 -1
  4. package/dist/core/package-manager.d.ts +10 -0
  5. package/dist/core/package-manager.d.ts.map +1 -1
  6. package/dist/core/package-manager.js +93 -8
  7. package/dist/core/package-manager.js.map +1 -1
  8. package/dist/core/package-manager.test.d.ts +2 -0
  9. package/dist/core/package-manager.test.d.ts.map +1 -0
  10. package/dist/core/package-manager.test.js +20 -0
  11. package/dist/core/package-manager.test.js.map +1 -0
  12. package/dist/core/resource-loader.d.ts +3 -0
  13. package/dist/core/resource-loader.d.ts.map +1 -1
  14. package/dist/core/resource-loader.js +10 -0
  15. package/dist/core/resource-loader.js.map +1 -1
  16. package/dist/core/settings-manager.d.ts +1 -0
  17. package/dist/core/settings-manager.d.ts.map +1 -1
  18. package/dist/core/settings-manager.js.map +1 -1
  19. package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
  20. package/dist/extensions/caveman/index.js +116 -0
  21. package/dist/extensions/caveman/package.json +8 -0
  22. package/dist/extensions/caveman/test/extension.test.js +185 -0
  23. package/dist/extensions/caveman/test/helpers.test.js +67 -0
  24. package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
  25. package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
  26. package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
  27. package/dist/extensions/pi-intercom/config.ts +2 -2
  28. package/dist/extensions/pi-intercom/index.ts +2 -2
  29. package/dist/extensions/pi-intercom/package.json +2 -2
  30. package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
  31. package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
  32. package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
  33. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
  34. package/dist/extensions/pi-powerline-footer/index.ts +9 -16
  35. package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
  36. package/dist/extensions/pi-powerline-footer/working-vibes.ts +2 -9
  37. package/dist/extensions/pi-rewind-hook/index.ts +1 -1
  38. package/dist/extensions/pi-rewind-hook/package.json +1 -1
  39. package/dist/extensions/pi-subagents/README.md +224 -52
  40. package/dist/extensions/pi-subagents/install.mjs +93 -0
  41. package/dist/extensions/pi-subagents/package.json +4 -5
  42. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  43. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  44. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
  45. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
  46. package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
  47. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
  48. package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
  49. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
  50. package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
  51. package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
  52. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
  53. package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
  54. package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
  55. package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
  56. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
  57. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
  58. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
  59. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
  61. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
  62. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
  63. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
  64. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
  66. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
  67. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
  68. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
  69. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
  70. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
  71. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
  72. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
  73. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
  74. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
  75. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
  76. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
  77. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
  78. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
  79. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
  80. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
  81. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
  87. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
  88. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
  89. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
  90. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
  92. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
  93. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
  94. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
  95. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
  96. package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
  97. package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
  98. package/dist/extensions/pi-subagents/src/shared/utils.ts +100 -15
  99. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
  100. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
  101. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
  102. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
  103. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
  104. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
  105. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
  106. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
  107. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
  108. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
  109. package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
  110. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
  111. package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
  112. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
  113. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
  114. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
  115. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
  116. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
  117. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
  118. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
  119. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
  120. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
  121. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
  122. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
  123. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
  124. package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
  125. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
  126. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
  127. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
  128. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
  129. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
  130. package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
  131. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
  132. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
  133. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
  134. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
  135. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
  136. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
  137. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
  138. package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
  139. package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
  140. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
  141. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
  142. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
  143. package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
  144. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
  145. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
  146. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
  147. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
  148. package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
  149. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
  150. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
  152. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
  153. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
  154. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
  155. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
  156. package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
  157. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
  158. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
  159. package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
  160. package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
  161. package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
  162. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
  163. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
  164. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
  165. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
  166. package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
  167. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
  168. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
  169. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
  170. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
  171. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
  172. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
  173. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
  174. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
  175. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
  176. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
  177. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
  178. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
  179. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
  180. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
  181. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
  182. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
  183. package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
  184. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
  185. package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
  186. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
  187. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
  188. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
  189. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
  190. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
  191. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
  192. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
  193. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
  194. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
  195. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
  196. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
  197. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
  198. package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
  199. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
  200. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
  201. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
  202. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
  203. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
  204. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
  205. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
  206. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
  207. package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
  208. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
  209. package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
  210. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
  211. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
  212. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
  213. package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
  214. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
  215. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
  216. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
  217. package/dist/main.d.ts.map +1 -1
  218. package/dist/main.js +4 -0
  219. package/dist/main.js.map +1 -1
  220. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  221. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  222. package/dist/modes/interactive/components/first-time-setup.js +19 -5
  223. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  224. package/dist/skills/caveman/SKILL.md +49 -0
  225. package/dist/skills/handoff-text/SKILL.md +13 -0
  226. package/docs/docs.json +4 -0
  227. package/docs/extensions.md +2 -0
  228. package/docs/settings.md +15 -0
  229. package/docs/shared-host-extensions.md +109 -0
  230. package/package.json +1 -1
@@ -0,0 +1,327 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import {
7
+ consumeInterruptRequest,
8
+ consumeSteerRequests,
9
+ deliverInterruptRequest,
10
+ enqueueStepSteer,
11
+ interruptRequestPath,
12
+ requestAsyncInterrupt,
13
+ requestAsyncSteer,
14
+ steerRequestsDir,
15
+ stepSteerInboxDir,
16
+ watchAsyncControlInbox,
17
+ } from "../../src/runs/background/control-channel.ts";
18
+
19
+ function tmpAsyncDir(label: string): string {
20
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), label));
21
+ return path.join(root, "run");
22
+ }
23
+
24
+ function cleanup(asyncDir: string): void {
25
+ fs.rmSync(path.dirname(asyncDir), { recursive: true, force: true });
26
+ }
27
+
28
+ describe("control channel: request file", () => {
29
+ it("writes a parseable interrupt request, creating the inbox dir", () => {
30
+ const asyncDir = tmpAsyncDir("pi-control-write-");
31
+ try {
32
+ const requestPath = requestAsyncInterrupt(asyncDir, { source: "test" }, { now: () => 999 });
33
+ assert.equal(requestPath, interruptRequestPath(asyncDir));
34
+ const data = JSON.parse(fs.readFileSync(requestPath, "utf-8"));
35
+ assert.equal(data.type, "interrupt");
36
+ assert.equal(data.ts, 999);
37
+ assert.equal(data.source, "test");
38
+ } finally {
39
+ cleanup(asyncDir);
40
+ }
41
+ });
42
+
43
+ it("keeps the request type authoritative even for untyped callers", () => {
44
+ const asyncDir = tmpAsyncDir("pi-control-write-type-");
45
+ try {
46
+ const requestPath = requestAsyncInterrupt(asyncDir, { type: "not-interrupt", source: "test" } as any, { now: () => 999 });
47
+ const data = JSON.parse(fs.readFileSync(requestPath, "utf-8"));
48
+ assert.equal(data.type, "interrupt");
49
+ assert.equal(data.source, "test");
50
+ } finally {
51
+ cleanup(asyncDir);
52
+ }
53
+ });
54
+
55
+ it("consumes a pending request exactly once and removes the file", () => {
56
+ const asyncDir = tmpAsyncDir("pi-control-consume-");
57
+ try {
58
+ requestAsyncInterrupt(asyncDir);
59
+ assert.equal(consumeInterruptRequest(asyncDir), true);
60
+ assert.equal(fs.existsSync(interruptRequestPath(asyncDir)), false);
61
+ assert.equal(consumeInterruptRequest(asyncDir), false);
62
+ } finally {
63
+ cleanup(asyncDir);
64
+ }
65
+ });
66
+
67
+ it("removes a malformed request directory instead of firing forever", () => {
68
+ const asyncDir = tmpAsyncDir("pi-control-consume-dir-");
69
+ try {
70
+ fs.mkdirSync(interruptRequestPath(asyncDir), { recursive: true });
71
+ assert.equal(consumeInterruptRequest(asyncDir), true);
72
+ assert.equal(fs.existsSync(interruptRequestPath(asyncDir)), false);
73
+ assert.equal(consumeInterruptRequest(asyncDir), false);
74
+ } finally {
75
+ cleanup(asyncDir);
76
+ }
77
+ });
78
+
79
+ it("writes and consumes ordered steer requests", () => {
80
+ const asyncDir = tmpAsyncDir("pi-control-steer-");
81
+ try {
82
+ requestAsyncSteer(asyncDir, { message: " later guidance ", targetIndex: 1, id: "b", ts: 200, source: "test" });
83
+ requestAsyncSteer(asyncDir, { message: "first guidance", id: "a", ts: 100 });
84
+ assert.equal(fs.readdirSync(steerRequestsDir(asyncDir)).length, 2);
85
+
86
+ assert.deepEqual(consumeSteerRequests(asyncDir), [
87
+ { type: "steer", id: "a", ts: 100, message: "first guidance" },
88
+ { type: "steer", id: "b", ts: 200, message: "later guidance", targetIndex: 1, source: "test" },
89
+ ]);
90
+ assert.deepEqual(consumeSteerRequests(asyncDir), []);
91
+ } finally {
92
+ cleanup(asyncDir);
93
+ }
94
+ });
95
+
96
+ it("keeps steer request ids out of filesystem paths", () => {
97
+ const asyncDir = tmpAsyncDir("pi-control-steer-safe-name-");
98
+ try {
99
+ const requestPath = requestAsyncSteer(asyncDir, { message: "safe", id: "../outside\\bad:thing", ts: 1 });
100
+ assert.equal(path.dirname(requestPath), steerRequestsDir(asyncDir));
101
+ assert.equal(path.basename(requestPath), `0000000000001-${Buffer.from("../outside\\bad:thing").toString("base64url")}.json`);
102
+ assert.deepEqual(consumeSteerRequests(asyncDir), [
103
+ { type: "steer", id: "../outside\\bad:thing", ts: 1, message: "safe" },
104
+ ]);
105
+ } finally {
106
+ cleanup(asyncDir);
107
+ }
108
+ });
109
+
110
+ it("does not deliver a steer request if another consumer removed it first", () => {
111
+ const asyncDir = tmpAsyncDir("pi-control-steer-concurrent-");
112
+ try {
113
+ requestAsyncSteer(asyncDir, { message: "already taken", id: "s", ts: 1 });
114
+ const fsImpl = {
115
+ existsSync: fs.existsSync,
116
+ readdirSync: fs.readdirSync,
117
+ readFileSync: fs.readFileSync,
118
+ rmSync: (target: fs.PathLike, options?: fs.RmOptions) => {
119
+ fs.rmSync(target, options);
120
+ const error = new Error("already removed") as NodeJS.ErrnoException;
121
+ error.code = "ENOENT";
122
+ throw error;
123
+ },
124
+ };
125
+ assert.deepEqual(consumeSteerRequests(asyncDir, fsImpl), []);
126
+ assert.deepEqual(consumeSteerRequests(asyncDir), []);
127
+ } finally {
128
+ cleanup(asyncDir);
129
+ }
130
+ });
131
+
132
+ it("enqueues a steer request for a specific child inbox", () => {
133
+ const asyncDir = tmpAsyncDir("pi-control-step-steer-");
134
+ try {
135
+ enqueueStepSteer(asyncDir, 2, { type: "steer", id: "s1", ts: 300, message: "focus", targetIndex: 0 });
136
+ const request = JSON.parse(fs.readFileSync(path.join(stepSteerInboxDir(asyncDir, 2), fs.readdirSync(stepSteerInboxDir(asyncDir, 2))[0]!), "utf-8"));
137
+ assert.equal(request.targetIndex, 2);
138
+ assert.equal(request.message, "focus");
139
+ } finally {
140
+ cleanup(asyncDir);
141
+ }
142
+ });
143
+
144
+ it("rejects empty steer messages and invalid target indexes", () => {
145
+ const asyncDir = tmpAsyncDir("pi-control-steer-invalid-");
146
+ try {
147
+ assert.throws(() => requestAsyncSteer(asyncDir, { message: " " }), /steer message must not be empty/);
148
+ assert.throws(() => requestAsyncSteer(asyncDir, { message: "ok", targetIndex: -1 }), /targetIndex/);
149
+ } finally {
150
+ cleanup(asyncDir);
151
+ }
152
+ });
153
+ });
154
+
155
+ describe("control channel: deliverInterruptRequest", () => {
156
+ it("writes the portable request and signals best-effort when kill succeeds", () => {
157
+ const asyncDir = tmpAsyncDir("pi-control-deliver-ok-");
158
+ try {
159
+ const kills: Array<{ pid: number; signal?: NodeJS.Signals | 0 }> = [];
160
+ deliverInterruptRequest({
161
+ asyncDir,
162
+ pid: 4242,
163
+ signal: "SIGUSR2",
164
+ kill: (pid, signal) => {
165
+ kills.push({ pid, signal });
166
+ return true;
167
+ },
168
+ });
169
+ assert.equal(fs.existsSync(interruptRequestPath(asyncDir)), true);
170
+ assert.deepEqual(kills, [{ pid: 4242, signal: "SIGUSR2" }]);
171
+ } finally {
172
+ cleanup(asyncDir);
173
+ }
174
+ });
175
+
176
+ it("still writes the request when the OS signal throws ENOSYS (Windows)", () => {
177
+ const asyncDir = tmpAsyncDir("pi-control-deliver-enosys-");
178
+ try {
179
+ assert.doesNotThrow(() =>
180
+ deliverInterruptRequest({
181
+ asyncDir,
182
+ pid: 4242,
183
+ kill: () => {
184
+ const error = new Error("kill ENOSYS") as NodeJS.ErrnoException;
185
+ error.code = "ENOSYS";
186
+ throw error;
187
+ },
188
+ }),
189
+ );
190
+ assert.equal(fs.existsSync(interruptRequestPath(asyncDir)), true);
191
+ } finally {
192
+ cleanup(asyncDir);
193
+ }
194
+ });
195
+
196
+ it("surfaces non-portability signal failures and removes the stale request", () => {
197
+ const asyncDir = tmpAsyncDir("pi-control-deliver-esrch-");
198
+ try {
199
+ assert.throws(
200
+ () =>
201
+ deliverInterruptRequest({
202
+ asyncDir,
203
+ pid: 4242,
204
+ kill: () => {
205
+ const error = new Error("missing process") as NodeJS.ErrnoException;
206
+ error.code = "ESRCH";
207
+ throw error;
208
+ },
209
+ }),
210
+ /missing process/,
211
+ );
212
+ assert.equal(fs.existsSync(interruptRequestPath(asyncDir)), false);
213
+ } finally {
214
+ cleanup(asyncDir);
215
+ }
216
+ });
217
+
218
+ it("skips signalling when no live pid is provided", () => {
219
+ const asyncDir = tmpAsyncDir("pi-control-deliver-nopid-");
220
+ try {
221
+ let killed = false;
222
+ deliverInterruptRequest({ asyncDir, kill: () => { killed = true; return true; } });
223
+ assert.equal(killed, false);
224
+ assert.equal(fs.existsSync(interruptRequestPath(asyncDir)), true);
225
+ } finally {
226
+ cleanup(asyncDir);
227
+ }
228
+ });
229
+ });
230
+
231
+ describe("control channel: watchAsyncControlInbox", () => {
232
+ type WatchHarness = {
233
+ fsImpl: import("../../src/runs/background/control-channel.ts").ControlChannelFs;
234
+ timers: import("../../src/runs/background/control-channel.ts").ControlChannelTimers;
235
+ trigger: () => void;
236
+ closed: () => boolean;
237
+ };
238
+
239
+ function harness(): WatchHarness {
240
+ let listener: (() => void) | undefined;
241
+ let closed = false;
242
+ const fsImpl = {
243
+ mkdirSync: fs.mkdirSync,
244
+ existsSync: fs.existsSync,
245
+ rmSync: fs.rmSync,
246
+ readdirSync: fs.readdirSync,
247
+ readFileSync: fs.readFileSync,
248
+ watch: ((_dir: string, cb: () => void) => {
249
+ listener = cb;
250
+ return { close: () => { closed = true; }, on: () => {} };
251
+ }),
252
+ } as unknown as WatchHarness["fsImpl"];
253
+ const timers = {
254
+ setInterval: (() => ({ unref() {} })) as unknown as typeof setInterval,
255
+ clearInterval: (() => {}) as unknown as typeof clearInterval,
256
+ };
257
+ return { fsImpl, timers, trigger: () => listener?.(), closed: () => closed };
258
+ }
259
+
260
+ it("fires on a request that arrived before the watcher started", () => {
261
+ const asyncDir = tmpAsyncDir("pi-control-watch-early-");
262
+ try {
263
+ requestAsyncInterrupt(asyncDir);
264
+ let fired = 0;
265
+ const h = harness();
266
+ const dispose = watchAsyncControlInbox(asyncDir, { onInterrupt: () => fired++, fs: h.fsImpl, timers: h.timers });
267
+ assert.equal(fired, 1);
268
+ assert.equal(fs.existsSync(interruptRequestPath(asyncDir)), false);
269
+ dispose();
270
+ } finally {
271
+ cleanup(asyncDir);
272
+ }
273
+ });
274
+
275
+ it("fires once per request via the watch event and stops after dispose", () => {
276
+ const asyncDir = tmpAsyncDir("pi-control-watch-event-");
277
+ try {
278
+ let fired = 0;
279
+ const h = harness();
280
+ const dispose = watchAsyncControlInbox(asyncDir, { onInterrupt: () => fired++, fs: h.fsImpl, timers: h.timers });
281
+ assert.equal(fired, 0);
282
+
283
+ requestAsyncInterrupt(asyncDir);
284
+ h.trigger();
285
+ assert.equal(fired, 1);
286
+ assert.equal(fs.existsSync(interruptRequestPath(asyncDir)), false);
287
+
288
+ // No pending request → spurious event is a no-op.
289
+ h.trigger();
290
+ assert.equal(fired, 1);
291
+
292
+ dispose();
293
+ assert.equal(h.closed(), true);
294
+
295
+ // After dispose, even a fresh request is ignored.
296
+ requestAsyncInterrupt(asyncDir);
297
+ h.trigger();
298
+ assert.equal(fired, 1);
299
+ } finally {
300
+ cleanup(asyncDir);
301
+ }
302
+ });
303
+
304
+ it("delivers steer requests without firing interrupt", () => {
305
+ const asyncDir = tmpAsyncDir("pi-control-watch-steer-");
306
+ try {
307
+ let interrupted = 0;
308
+ const steers: Array<{ message: string; targetIndex?: number }> = [];
309
+ const h = harness();
310
+ const dispose = watchAsyncControlInbox(asyncDir, {
311
+ onInterrupt: () => interrupted++,
312
+ onSteer: (request) => steers.push({ message: request.message, targetIndex: request.targetIndex }),
313
+ fs: h.fsImpl,
314
+ timers: h.timers,
315
+ });
316
+
317
+ requestAsyncSteer(asyncDir, { message: "go narrower", targetIndex: 0, id: "s", ts: 1 });
318
+ h.trigger();
319
+
320
+ assert.equal(interrupted, 0);
321
+ assert.deepEqual(steers, [{ message: "go narrower", targetIndex: 0 }]);
322
+ dispose();
323
+ } finally {
324
+ cleanup(asyncDir);
325
+ }
326
+ });
327
+ });
@@ -0,0 +1,161 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ clearPendingForegroundControlNotices,
5
+ handleSubagentControlNotice,
6
+ } from "../../src/extension/control-notices.ts";
7
+ import type { ControlEvent, SubagentState } from "../../src/shared/types.ts";
8
+
9
+ function makeState(): SubagentState {
10
+ return {
11
+ baseCwd: "/tmp/project",
12
+ currentSessionId: null,
13
+ asyncJobs: new Map(),
14
+ foregroundControls: new Map(),
15
+ lastForegroundControlId: null,
16
+ pendingForegroundControlNotices: new Map(),
17
+ cleanupTimers: new Map(),
18
+ lastUiContext: null,
19
+ poller: null,
20
+ completionSeen: new Map(),
21
+ watcher: null,
22
+ watcherRestartTimer: null,
23
+ resultFileCoalescer: { schedule: () => false, clear: () => {} },
24
+ };
25
+ }
26
+
27
+ function needsAttentionEvent(overrides: Partial<ControlEvent> = {}): ControlEvent {
28
+ return {
29
+ type: "needs_attention",
30
+ to: "needs_attention",
31
+ ts: 1,
32
+ runId: "run-1",
33
+ agent: "worker",
34
+ index: 0,
35
+ message: "worker needs attention",
36
+ reason: "idle",
37
+ ...overrides,
38
+ };
39
+ }
40
+
41
+ function makeRecorder() {
42
+ const sent: Array<{ message: unknown; options: unknown }> = [];
43
+ return {
44
+ sent,
45
+ pi: {
46
+ sendMessage(message: unknown, options: unknown) {
47
+ sent.push({ message, options });
48
+ },
49
+ },
50
+ };
51
+ }
52
+
53
+ function wait(ms: number): Promise<void> {
54
+ return new Promise((resolve) => setTimeout(resolve, ms));
55
+ }
56
+
57
+ describe("subagent control notice delivery", () => {
58
+ it("delivers async needs-attention notices immediately", () => {
59
+ const state = makeState();
60
+ const recorder = makeRecorder();
61
+
62
+ handleSubagentControlNotice({
63
+ pi: recorder.pi,
64
+ state,
65
+ visibleControlNotices: new Set(),
66
+ details: { source: "async", event: needsAttentionEvent() },
67
+ foregroundDelayMs: 20,
68
+ });
69
+
70
+ assert.equal(recorder.sent.length, 1);
71
+ assert.deepEqual(recorder.sent[0]?.options, { triggerTurn: true });
72
+ });
73
+
74
+ it("queues foreground needs-attention notices until the same step is still actionable", async () => {
75
+ const state = makeState();
76
+ state.foregroundControls.set("run-1", {
77
+ runId: "run-1",
78
+ mode: "chain",
79
+ startedAt: 0,
80
+ updatedAt: 0,
81
+ currentAgent: "worker",
82
+ currentIndex: 0,
83
+ currentActivityState: "needs_attention",
84
+ });
85
+ const recorder = makeRecorder();
86
+
87
+ handleSubagentControlNotice({
88
+ pi: recorder.pi,
89
+ state,
90
+ visibleControlNotices: new Set(),
91
+ details: { source: "foreground", event: needsAttentionEvent() },
92
+ foregroundDelayMs: 10,
93
+ });
94
+
95
+ assert.equal(recorder.sent.length, 0);
96
+ await wait(25);
97
+ assert.equal(recorder.sent.length, 1);
98
+ assert.deepEqual(recorder.sent[0]?.options, { triggerTurn: false });
99
+ });
100
+
101
+ it("drops queued foreground notices when the run finishes before delivery", async () => {
102
+ const state = makeState();
103
+ state.foregroundControls.set("run-1", {
104
+ runId: "run-1",
105
+ mode: "chain",
106
+ startedAt: 0,
107
+ updatedAt: 0,
108
+ currentAgent: "worker",
109
+ currentIndex: 0,
110
+ currentActivityState: "needs_attention",
111
+ });
112
+ const recorder = makeRecorder();
113
+
114
+ handleSubagentControlNotice({
115
+ pi: recorder.pi,
116
+ state,
117
+ visibleControlNotices: new Set(),
118
+ details: { source: "foreground", event: needsAttentionEvent() },
119
+ foregroundDelayMs: 20,
120
+ });
121
+ clearPendingForegroundControlNotices(state, "run-1");
122
+ state.foregroundControls.delete("run-1");
123
+
124
+ await wait(35);
125
+ assert.equal(recorder.sent.length, 0);
126
+ });
127
+
128
+ it("drops queued foreground notices after the chain advances to another step", async () => {
129
+ const state = makeState();
130
+ state.foregroundControls.set("run-1", {
131
+ runId: "run-1",
132
+ mode: "chain",
133
+ startedAt: 0,
134
+ updatedAt: 0,
135
+ currentAgent: "worker",
136
+ currentIndex: 0,
137
+ currentActivityState: "needs_attention",
138
+ });
139
+ const recorder = makeRecorder();
140
+
141
+ handleSubagentControlNotice({
142
+ pi: recorder.pi,
143
+ state,
144
+ visibleControlNotices: new Set(),
145
+ details: { source: "foreground", event: needsAttentionEvent() },
146
+ foregroundDelayMs: 10,
147
+ });
148
+ state.foregroundControls.set("run-1", {
149
+ runId: "run-1",
150
+ mode: "chain",
151
+ startedAt: 0,
152
+ updatedAt: 0,
153
+ currentAgent: "writer",
154
+ currentIndex: 1,
155
+ currentActivityState: undefined,
156
+ });
157
+
158
+ await wait(25);
159
+ assert.equal(recorder.sent.length, 0);
160
+ });
161
+ });
@@ -0,0 +1,159 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import { buildDoctorReport } from "../../src/extension/doctor.ts";
7
+ import type { AgentConfig, ChainConfig } from "../../src/agents/agents.ts";
8
+ import type { SubagentState } from "../../src/shared/types.ts";
9
+
10
+ function makeState(cwd: string): SubagentState {
11
+ return {
12
+ baseCwd: cwd,
13
+ currentSessionId: "session-current",
14
+ asyncJobs: new Map(),
15
+ foregroundControls: new Map(),
16
+ lastForegroundControlId: null,
17
+ cleanupTimers: new Map(),
18
+ lastUiContext: null,
19
+ poller: null,
20
+ completionSeen: new Map(),
21
+ watcher: null,
22
+ watcherRestartTimer: null,
23
+ resultFileCoalescer: { schedule: () => false, clear: () => {} },
24
+ };
25
+ }
26
+
27
+ function makeAgent(name: string, source: AgentConfig["source"]): AgentConfig {
28
+ return {
29
+ name,
30
+ description: `${name} agent`,
31
+ systemPrompt: "Prompt",
32
+ systemPromptMode: "replace",
33
+ inheritProjectContext: false,
34
+ inheritSkills: false,
35
+ source,
36
+ filePath: `/tmp/${name}.md`,
37
+ };
38
+ }
39
+
40
+ function makeChain(name: string, source: ChainConfig["source"]): ChainConfig {
41
+ return {
42
+ name,
43
+ description: `${name} chain`,
44
+ source,
45
+ filePath: `/tmp/${name}.chain.md`,
46
+ steps: [{ agent: "worker", task: "Work" }],
47
+ };
48
+ }
49
+
50
+ describe("buildDoctorReport", () => {
51
+ it("formats a bounded successful environment summary", () => {
52
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-doctor-success-"));
53
+ try {
54
+ const paths = {
55
+ tempRootDir: path.join(root, "temp-root"),
56
+ asyncDir: path.join(root, "async"),
57
+ resultsDir: path.join(root, "results"),
58
+ chainRunsDir: path.join(root, "chains"),
59
+ };
60
+ for (const dir of Object.values(paths)) fs.mkdirSync(dir, { recursive: true });
61
+
62
+ const report = buildDoctorReport({
63
+ cwd: root,
64
+ config: { defaultSessionDir: "~/subagent-sessions", intercomBridge: { mode: "always" } },
65
+ state: makeState(root),
66
+ currentSessionFile: path.join(root, "sessions", "parent.jsonl"),
67
+ currentSessionId: "session-abc123",
68
+ orchestratorTarget: "subagent-chat-abc123",
69
+ expandTilde: (value) => value.replace(/^~\//, `${root}/home/`),
70
+ paths,
71
+ deps: {
72
+ isAsyncAvailable: () => true,
73
+ discoverAgentsAll: () => ({
74
+ builtin: [makeAgent("builtin-a", "builtin")],
75
+ user: [makeAgent("user-a", "user")],
76
+ project: [makeAgent("project-a", "project"), makeAgent("project-b", "project")],
77
+ chains: [makeChain("user-flow", "user"), makeChain("project-flow", "project")],
78
+ userDir: path.join(root, "home", ".agents"),
79
+ projectDir: path.join(root, ".selesai", "agents"),
80
+ userChainDir: path.join(root, "home", ".selesai", "agent", "chains"),
81
+ projectChainDir: path.join(root, ".selesai", "chains"),
82
+ userSettingsPath: path.join(root, "home", ".selesai", "agent", "settings.json"),
83
+ projectSettingsPath: path.join(root, ".selesai", "settings.json"),
84
+ }),
85
+ discoverAvailableSkills: () => [
86
+ { name: "project-skill", source: "project" },
87
+ { name: "package-skill", source: "user-package" },
88
+ ],
89
+ diagnoseIntercomBridge: () => ({
90
+ active: true,
91
+ mode: "always",
92
+ wantsIntercom: true,
93
+ supervisorChannelAvailable: true,
94
+ extensionDir: "native:pi-subagents-supervisor-channel",
95
+ orchestratorTarget: "subagent-chat-abc123",
96
+ }),
97
+ },
98
+ });
99
+
100
+ assert.match(report, /^Subagents doctor report/);
101
+ assert.ok(report.includes(`- cwd: ${root}`));
102
+ assert.match(report, /- async support: available/);
103
+ assert.match(report, /- configured session dir: .*subagent-sessions/);
104
+ assert.match(report, /- current session file: .*parent\.jsonl/);
105
+ assert.match(report, /- temp root: ok /);
106
+ assert.match(report, /- agents: total 4 \(builtin 1, package 0, user 1, project 2\)/);
107
+ assert.match(report, /- chains: total 2 \(builtin 0, package 0, user 1, project 1\)/);
108
+ assert.match(report, /- skills: total 2 \(project 1, user-package 1\)/);
109
+ assert.match(report, /- bridge: active/);
110
+ assert.match(report, /- supervisor channel: available \(native:pi-subagents-supervisor-channel\)/);
111
+ assert.doesNotMatch(report, /Companion packages/);
112
+ } finally {
113
+ fs.rmSync(root, { recursive: true, force: true });
114
+ }
115
+ });
116
+
117
+ it("keeps reporting when a directory or discovery check fails", () => {
118
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-doctor-failure-"));
119
+ try {
120
+ const asyncPath = path.join(root, "async-file");
121
+ fs.writeFileSync(asyncPath, "not a directory");
122
+ const report = buildDoctorReport({
123
+ cwd: root,
124
+ config: {},
125
+ state: makeState(root),
126
+ paths: {
127
+ tempRootDir: root,
128
+ asyncDir: asyncPath,
129
+ resultsDir: path.join(root, "missing-results"),
130
+ chainRunsDir: path.join(root, "missing-chains"),
131
+ },
132
+ deps: {
133
+ isAsyncAvailable: () => false,
134
+ discoverAgentsAll: () => {
135
+ throw new Error("discovery exploded");
136
+ },
137
+ discoverAvailableSkills: () => [],
138
+ diagnoseIntercomBridge: () => ({
139
+ active: false,
140
+ mode: "fork-only",
141
+ wantsIntercom: false,
142
+ supervisorChannelAvailable: true,
143
+ extensionDir: "native:pi-subagents-supervisor-channel",
144
+ reason: "bridge mode is fork-only and context is not fork",
145
+ }),
146
+ },
147
+ });
148
+
149
+ assert.match(report, /- async support: unavailable/);
150
+ assert.match(report, /- async runs: failed .*Error: not a directory:/);
151
+ assert.match(report, /- results: missing /);
152
+ assert.match(report, /- agents\/chains: failed — Error: discovery exploded/);
153
+ assert.match(report, /- skills: total 0 \(none\)/);
154
+ assert.match(report, /- bridge: inactive \(bridge mode is fork-only and context is not fork\)/);
155
+ } finally {
156
+ fs.rmSync(root, { recursive: true, force: true });
157
+ }
158
+ });
159
+ });