@selesai/code 0.8.4 → 0.8.6

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 (147) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -1
  3. package/dist/core/slash-commands.js +1 -0
  4. package/dist/defaults/models.json +46 -55
  5. package/dist/defaults/settings.json +7 -8
  6. package/dist/extensions/agent-browser.test.ts +178 -2
  7. package/dist/extensions/agent-browser.ts +2 -0
  8. package/dist/extensions/copy-turn.test.ts +229 -0
  9. package/dist/extensions/copy-turn.ts +11 -0
  10. package/dist/extensions/grep-app/index.test.ts +356 -1
  11. package/dist/extensions/grep-app/index.ts +6 -1
  12. package/dist/extensions/handoff-new.test.ts +351 -161
  13. package/dist/extensions/handoff-new.ts +3 -0
  14. package/dist/extensions/inline-skills.test.ts +93 -0
  15. package/dist/extensions/inline-skills.ts +3 -0
  16. package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
  17. package/dist/extensions/pi-subagents/README.md +2 -0
  18. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  19. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  20. package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
  21. package/dist/extensions/pi-subagents/docs/observability.md +1 -1
  22. package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
  23. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  24. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  25. package/dist/extensions/pi-subagents/package.json +1 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
  27. package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
  28. package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
  29. package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
  30. package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
  31. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
  32. package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
  33. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
  35. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
  36. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
  37. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
  39. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
  40. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
  42. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
  43. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
  44. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
  45. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  46. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
  48. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
  49. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
  50. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
  51. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
  53. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
  54. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  55. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
  56. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
  57. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
  58. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  59. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
  60. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
  61. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
  62. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
  63. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
  64. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  65. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
  66. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
  67. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
  68. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
  69. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
  70. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  71. package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
  72. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
  73. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
  74. package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
  75. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
  76. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  77. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
  78. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
  79. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
  80. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
  81. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
  82. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
  83. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
  84. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
  85. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
  86. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
  87. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
  88. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
  89. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
  90. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
  91. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
  92. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
  93. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
  94. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
  95. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  96. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  97. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
  98. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
  99. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
  100. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
  101. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
  102. package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
  103. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
  104. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
  105. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
  106. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
  107. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
  108. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
  109. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
  110. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
  111. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  112. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
  113. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
  114. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
  115. package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
  116. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  117. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
  118. package/dist/extensions/ponytail/index.js +11 -0
  119. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  120. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  121. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  122. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  123. package/dist/extensions/question/index.ts +18 -0
  124. package/dist/extensions/question/question-list.ts +22 -0
  125. package/dist/extensions/question/tests/batch.test.ts +103 -65
  126. package/dist/extensions/question/tests/helpers.test.ts +68 -35
  127. package/dist/extensions/question/tests/question-list.test.ts +506 -204
  128. package/dist/extensions/question/tests/row-layout.test.ts +180 -114
  129. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  130. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  131. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  132. package/dist/extensions/rtk.test.ts +180 -1
  133. package/dist/extensions/tokenin-onboarding.ts +3 -0
  134. package/dist/extensions/undo.test.ts +720 -0
  135. package/dist/extensions/undo.ts +6 -0
  136. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  137. package/dist/extensions/workflow/extension.ts +3 -3
  138. package/dist/extensions/workflow/modes.ts +41 -19
  139. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  140. package/dist/modes/interactive/interactive-mode.js +48 -1
  141. package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
  142. package/docs/plans/workflow-autoloop-reference.md +1 -2
  143. package/docs/plans/workflow-handoff-carryover.md +108 -0
  144. package/docs/quickstart.md +27 -43
  145. package/docs/settings.md +4 -0
  146. package/docs/usage.md +1 -0
  147. package/package.json +5 -1
@@ -0,0 +1,317 @@
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
+ acquireActiveAsyncCapacity,
8
+ ActiveAsyncCapacityError,
9
+ getActiveAsyncCapacitySnapshot,
10
+ transferActiveAsyncCapacity,
11
+ } from "../../src/runs/background/active-async-capacity.ts";
12
+
13
+ function tempRoot(): string {
14
+ return fs.mkdtempSync(path.join(os.tmpdir(), "active-async-capacity-"));
15
+ }
16
+
17
+ function writeJson(filePath: string, value: unknown): void {
18
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
19
+ fs.writeFileSync(filePath, JSON.stringify(value));
20
+ }
21
+
22
+ function observedProof(runId: string, runnerProcessInstanceId: string) {
23
+ return {
24
+ version: 1,
25
+ state: "observed",
26
+ runId,
27
+ runnerProcessInstanceId,
28
+ observedAt: 300,
29
+ instances: [{
30
+ kind: "runner",
31
+ processInstanceId: runnerProcessInstanceId,
32
+ closeObservedAt: 300,
33
+ exitCode: 0,
34
+ signal: null,
35
+ }],
36
+ };
37
+ }
38
+
39
+ describe("active async capacity", () => {
40
+ it("admits exactly to the limit and rolls back only its own unstarted slot", () => {
41
+ const rootDir = tempRoot();
42
+ try {
43
+ const first = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-a", kind: "runner", asyncDir: path.join(rootDir, "run-a") }, { rootDir, token: () => "token-a" });
44
+ assert.ok(first);
45
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
46
+ assert.throws(
47
+ () => acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-b", kind: "runner", asyncDir: path.join(rootDir, "run-b") }, { rootDir, token: () => "token-b" }),
48
+ (error: unknown) => error instanceof ActiveAsyncCapacityError && error.message === "Active async run capacity exhausted: 1/1 used.",
49
+ );
50
+ assert.equal(first.rollback(), true);
51
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
52
+ } finally {
53
+ fs.rmSync(rootDir, { recursive: true, force: true });
54
+ }
55
+ });
56
+
57
+ it("counts corrupt slot metadata as occupied", () => {
58
+ const rootDir = tempRoot();
59
+ try {
60
+ // Create a valid occupied slot, then corrupt only its owner metadata.
61
+ const handle = acquireActiveAsyncCapacity({ sessionId: "session-corrupt", limit: 1, runId: "run-a", kind: "runner", asyncDir: path.join(rootDir, "run-a") }, { rootDir });
62
+ assert.ok(handle);
63
+ fs.writeFileSync(path.join(rootDir, handle.owner.ownerSessionKey, "slot-0", "owner.json"), "not json");
64
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-corrupt", 1, { rootDir }), { used: 1, limit: 1 });
65
+ assert.throws(() => acquireActiveAsyncCapacity({ sessionId: "session-corrupt", limit: 1, runId: "run-b", kind: "runner", asyncDir: path.join(rootDir, "run-b") }, { rootDir }), ActiveAsyncCapacityError);
66
+ } finally {
67
+ fs.rmSync(rootDir, { recursive: true, force: true });
68
+ }
69
+ });
70
+
71
+ it("uses the injected clock for reservation and start timestamps", () => {
72
+ const rootDir = tempRoot();
73
+ let now = 100;
74
+ try {
75
+ const runner = acquireActiveAsyncCapacity({ sessionId: "session-clock", limit: 2, runId: "runner", kind: "runner", asyncDir: path.join(rootDir, "runner") }, { rootDir, now: () => now });
76
+ assert.ok(runner);
77
+ assert.equal(runner.owner.reservedAt, 100);
78
+ now = 200;
79
+ runner.markStarted("runner-process");
80
+ assert.equal(runner.owner.runnerStartedAt, 200);
81
+
82
+ now = 300;
83
+ const workflow = acquireActiveAsyncCapacity({ sessionId: "session-clock", limit: 2, runId: "workflow", kind: "workflow", asyncDir: path.join(rootDir, "workflow") }, { rootDir, now: () => now });
84
+ assert.ok(workflow);
85
+ workflow.markWorkflowStarted();
86
+ assert.equal(workflow.owner.runnerStartedAt, 300);
87
+ } finally {
88
+ fs.rmSync(rootDir, { recursive: true, force: true });
89
+ }
90
+ });
91
+
92
+ it("does not roll back when runner start was recorded only in memory", () => {
93
+ const rootDir = tempRoot();
94
+ try {
95
+ const handle = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-a", kind: "runner", asyncDir: path.join(rootDir, "runs", "run-a") }, { rootDir });
96
+ assert.ok(handle);
97
+ handle.owner.runnerProcessInstanceId = "runner-a";
98
+ handle.owner.runnerStartedAt = Date.now();
99
+
100
+ assert.equal(handle.rollback(), false);
101
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
102
+ } finally {
103
+ fs.rmSync(rootDir, { recursive: true, force: true });
104
+ }
105
+ });
106
+
107
+ it("retains active and unknown runner proof, then releases matching observed proof", () => {
108
+ const rootDir = tempRoot();
109
+ const asyncDir = path.join(rootDir, "runs", "run-a");
110
+ try {
111
+ const handle = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 2, runId: "run-a", kind: "runner", asyncDir }, { rootDir });
112
+ assert.ok(handle);
113
+ handle.markStarted("runner-a");
114
+ writeJson(path.join(asyncDir, "status.json"), { runId: "run-a", sessionId: "session-a", mode: "single", state: "running", startedAt: 100, processTerminal: { version: 1, state: "pending", runId: "run-a", runnerProcessInstanceId: "runner-a" } });
115
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 2, { rootDir }), { used: 1, limit: 2 });
116
+ writeJson(path.join(asyncDir, "status.json"), { runId: "run-a", sessionId: "session-a", mode: "single", state: "complete", startedAt: 100, processTerminal: { version: 1, state: "unknown", runId: "run-a", runnerProcessInstanceId: "runner-a", reason: "process-tree-unverified" } });
117
+ writeJson(path.join(asyncDir, "process-terminal.json"), { version: 1, state: "unknown", runId: "run-a", runnerProcessInstanceId: "runner-a", reason: "process-tree-unverified" });
118
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 2, { rootDir }), { used: 1, limit: 2 });
119
+ writeJson(path.join(asyncDir, "process-terminal.json"), observedProof("run-a", "runner-a"));
120
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 2, { rootDir }), { used: 0, limit: 2 });
121
+ } finally {
122
+ fs.rmSync(rootDir, { recursive: true, force: true });
123
+ }
124
+ });
125
+
126
+ it("does not delete a slot recreated while a terminal owner is released", () => {
127
+ const rootDir = tempRoot();
128
+ const asyncDir = path.join(rootDir, "runs", "run-a");
129
+ try {
130
+ const handle = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-a", kind: "runner", asyncDir }, { rootDir, token: () => "token-a" });
131
+ assert.ok(handle);
132
+ handle.markStarted("runner-a");
133
+ writeJson(path.join(asyncDir, "status.json"), { runId: "run-a", sessionId: "session-a", mode: "single", state: "complete", startedAt: 100 });
134
+ writeJson(path.join(asyncDir, "process-terminal.json"), observedProof("run-a", "runner-a"));
135
+
136
+ let replacement: ReturnType<typeof acquireActiveAsyncCapacity>;
137
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, {
138
+ rootDir,
139
+ afterSlotRename: () => {
140
+ replacement = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-b", kind: "runner", asyncDir: path.join(rootDir, "runs", "run-b") }, { rootDir, token: () => "token-b" });
141
+ },
142
+ }), { used: 1, limit: 1 });
143
+ assert.equal(replacement!.owner.runId, "run-b");
144
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
145
+ } finally {
146
+ fs.rmSync(rootDir, { recursive: true, force: true });
147
+ }
148
+ });
149
+
150
+ it("does not delete a slot recreated during pre-start rollback", () => {
151
+ const rootDir = tempRoot();
152
+ try {
153
+ const first = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-a", kind: "runner", asyncDir: path.join(rootDir, "runs", "run-a") }, {
154
+ rootDir,
155
+ token: () => "token-a",
156
+ afterSlotRename: () => {
157
+ const replacement = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-b", kind: "runner", asyncDir: path.join(rootDir, "runs", "run-b") }, { rootDir, token: () => "token-b" });
158
+ assert.ok(replacement);
159
+ },
160
+ });
161
+ assert.ok(first);
162
+ assert.equal(first.rollback(), true);
163
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
164
+ assert.equal(first.rollback(), false);
165
+ } finally {
166
+ fs.rmSync(rootDir, { recursive: true, force: true });
167
+ }
168
+ });
169
+
170
+ it("releases a transferred failure that never started", () => {
171
+ const rootDir = tempRoot();
172
+ const sourceDir = path.join(rootDir, "runs", "source");
173
+ const failedDir = path.join(rootDir, "runs", "failed-revival");
174
+ try {
175
+ const source = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "source", kind: "runner", asyncDir: sourceDir }, { rootDir });
176
+ assert.ok(source);
177
+ source.markStarted("source-runner");
178
+ writeJson(path.join(sourceDir, "status.json"), { runId: "source", sessionId: "session-a", mode: "single", state: "paused", startedAt: 100 });
179
+ const transferred = transferActiveAsyncCapacity({ sessionId: "session-a", limit: 1, sourceRunId: "source", runId: "failed-revival", asyncDir: failedDir }, { rootDir });
180
+ assert.ok(transferred);
181
+ transferred.markStarted("failed-runner");
182
+ writeJson(path.join(failedDir, "status.json"), { runId: "failed-revival", sessionId: "session-a", mode: "single", state: "failed", startedAt: 200, error: "Failed before startup proceed", processTerminal: { version: 1, state: "not-started", runId: "failed-revival", runnerProcessInstanceId: "failed-runner" } });
183
+
184
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
185
+ } finally {
186
+ fs.rmSync(rootDir, { recursive: true, force: true });
187
+ }
188
+ });
189
+
190
+ it("does not restore old source proof after a transferred runner starts", () => {
191
+ const rootDir = tempRoot();
192
+ const sourceDir = path.join(rootDir, "runs", "source");
193
+ const revivedDir = path.join(rootDir, "runs", "revived");
194
+ try {
195
+ const source = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "source", kind: "runner", asyncDir: sourceDir }, { rootDir });
196
+ assert.ok(source);
197
+ source.markStarted("source-runner");
198
+ writeJson(path.join(sourceDir, "status.json"), { runId: "source", sessionId: "session-a", mode: "single", state: "complete", startedAt: 100 });
199
+ writeJson(path.join(sourceDir, "process-terminal.json"), observedProof("source", "source-runner"));
200
+ const transferred = transferActiveAsyncCapacity({ sessionId: "session-a", limit: 1, sourceRunId: "source", runId: "revived", asyncDir: revivedDir }, { rootDir });
201
+ assert.ok(transferred);
202
+ writeJson(path.join(revivedDir, "status.json"), { runId: "revived", sessionId: "session-a", mode: "single", state: "failed", startedAt: 200, processTerminal: { version: 1, state: "not-started", runId: "revived", runnerProcessInstanceId: "revived-runner" } });
203
+ transferred.markStarted("revived-runner");
204
+ assert.equal(transferred.rollback(), false);
205
+
206
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
207
+ writeJson(path.join(revivedDir, "process-terminal.json"), observedProof("revived", "revived-runner"));
208
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
209
+ } finally {
210
+ fs.rmSync(rootDir, { recursive: true, force: true });
211
+ }
212
+ });
213
+
214
+ it("transfers a paused reservation without changing usage", () => {
215
+ const rootDir = tempRoot();
216
+ const sourceDir = path.join(rootDir, "runs", "source");
217
+ try {
218
+ const source = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "source", kind: "runner", asyncDir: sourceDir }, { rootDir });
219
+ assert.ok(source);
220
+ source.markStarted("runner-source");
221
+ writeJson(path.join(sourceDir, "status.json"), { runId: "source", sessionId: "session-a", mode: "single", state: "paused", startedAt: 100 });
222
+ const next = transferActiveAsyncCapacity({ sessionId: "session-a", limit: 1, sourceRunId: "source", runId: "next", asyncDir: path.join(rootDir, "runs", "next") }, { rootDir });
223
+ assert.ok(next);
224
+ assert.equal(next.owner.sourceRunId, "source");
225
+ assert.equal(next.owner.generation, 1);
226
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
227
+ } finally {
228
+ fs.rmSync(rootDir, { recursive: true, force: true });
229
+ }
230
+ });
231
+
232
+ it("transfers an existing reservation after the cap is disabled", () => {
233
+ const rootDir = tempRoot();
234
+ const sourceDir = path.join(rootDir, "runs", "source");
235
+ try {
236
+ const source = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "source", kind: "runner", asyncDir: sourceDir }, { rootDir });
237
+ assert.ok(source);
238
+ source.markStarted("runner-source");
239
+ writeJson(path.join(sourceDir, "status.json"), { runId: "source", sessionId: "session-a", mode: "single", state: "paused", startedAt: 100 });
240
+ const next = transferActiveAsyncCapacity({ sessionId: "session-a", limit: undefined, sourceRunId: "source", runId: "next", asyncDir: path.join(rootDir, "runs", "next") }, { rootDir });
241
+ assert.ok(next);
242
+ assert.equal(next.owner.runId, "next");
243
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", undefined, { rootDir }), { used: 1, limit: 0 });
244
+ } finally {
245
+ fs.rmSync(rootDir, { recursive: true, force: true });
246
+ }
247
+ });
248
+
249
+ it("releases terminal workflows only after the controller is gone and async children have observed proof", () => {
250
+ const rootDir = tempRoot();
251
+ const asyncRoot = path.join(rootDir, "runs");
252
+ const workflowDir = path.join(asyncRoot, "workflow");
253
+ const childDir = path.join(asyncRoot, "child");
254
+ try {
255
+ const workflow = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow", kind: "workflow", asyncDir: workflowDir }, { rootDir });
256
+ assert.ok(workflow);
257
+ workflow.markWorkflowStarted();
258
+ writeJson(path.join(workflowDir, "status.json"), { runId: "workflow", sessionId: "session-a", mode: "workflow", state: "complete", startedAt: 100, steps: [{ agent: "worker", workflowKey: "foreground", async: false, status: "completed" }] });
259
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir, liveWorkflowRunIds: new Set(["workflow"]) }), { used: 1, limit: 1 });
260
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
261
+
262
+ const second = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow-2", kind: "workflow", asyncDir: workflowDir }, { rootDir });
263
+ assert.ok(second);
264
+ second.markWorkflowStarted();
265
+ writeJson(path.join(workflowDir, "status.json"), { runId: "workflow-2", sessionId: "session-a", mode: "workflow", state: "complete", startedAt: 100, steps: [{ agent: "worker", workflowKey: "async", runId: "child", async: true, status: "completed" }] });
266
+ writeJson(path.join(childDir, "status.json"), { runId: "child", sessionId: "session-a", mode: "single", state: "complete", startedAt: 100, processTerminal: { version: 1, state: "unknown", runId: "child", runnerProcessInstanceId: "runner-child", reason: "process-tree-unverified" } });
267
+ writeJson(path.join(childDir, "process-terminal.json"), { version: 1, state: "unknown", runId: "child", runnerProcessInstanceId: "runner-child", reason: "process-tree-unverified" });
268
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
269
+ writeJson(path.join(childDir, "process-terminal.json"), observedProof("child", "runner-child"));
270
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
271
+ } finally {
272
+ fs.rmSync(rootDir, { recursive: true, force: true });
273
+ }
274
+ });
275
+
276
+ it("retains terminal workflows while a resumed async child lacks observed proof", () => {
277
+ const rootDir = tempRoot();
278
+ const asyncRoot = path.join(rootDir, "runs");
279
+ const workflowDir = path.join(asyncRoot, "workflow");
280
+ const childDir = path.join(asyncRoot, "revived-child");
281
+ try {
282
+ const workflow = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow", kind: "workflow", asyncDir: workflowDir }, { rootDir });
283
+ assert.ok(workflow);
284
+ workflow.markWorkflowStarted();
285
+ writeJson(path.join(workflowDir, "status.json"), { runId: "workflow", sessionId: "session-a", mode: "workflow", state: "failed", startedAt: 100, steps: [{ agent: "worker", workflowKey: "resume", runId: "revived-child", async: true, status: "failed" }] });
286
+ writeJson(path.join(childDir, "status.json"), { runId: "revived-child", sessionId: "session-a", mode: "single", state: "failed", startedAt: 100, processTerminal: { version: 1, state: "not-started", runId: "revived-child", runnerProcessInstanceId: "runner-child" } });
287
+
288
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
289
+ writeJson(path.join(childDir, "process-terminal.json"), observedProof("revived-child", "runner-child"));
290
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
291
+ } finally {
292
+ fs.rmSync(rootDir, { recursive: true, force: true });
293
+ }
294
+ });
295
+
296
+ it("releases terminal workflows when a child is marked failed before launch identity exists", () => {
297
+ const rootDir = tempRoot();
298
+ const workflowDir = path.join(rootDir, "runs", "workflow");
299
+ try {
300
+ const workflow = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow", kind: "workflow", asyncDir: workflowDir }, { rootDir });
301
+ assert.ok(workflow);
302
+ workflow.markWorkflowStarted();
303
+ writeJson(path.join(workflowDir, "status.json"), {
304
+ runId: "workflow",
305
+ sessionId: "session-a",
306
+ mode: "workflow",
307
+ state: "failed",
308
+ startedAt: 100,
309
+ steps: [{ agent: "worker", workflowKey: "prep-failure", status: "failed", async: false, error: "gate cannot be combined with acceptance" }],
310
+ });
311
+
312
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
313
+ } finally {
314
+ fs.rmSync(rootDir, { recursive: true, force: true });
315
+ }
316
+ });
317
+ });
@@ -80,8 +80,27 @@ describe("builtin agent overrides", () => {
80
80
  assert.equal(scout?.modelSource?.type, "subagents.defaultModel");
81
81
  assert.equal(scout?.modelSource?.scope, "user");
82
82
  assert.equal(builtins.find((agent) => agent.name === "worker")?.model, "deepseek-v4-flash");
83
- assert.equal(builtins.find((agent) => agent.name === "oracle")?.model, "deepseek-v4-pro");
84
- assert.equal(builtins.find((agent) => agent.name === "reviewer")?.model, undefined);
83
+ const oracle = builtins.find((agent) => agent.name === "oracle");
84
+ assert.equal(oracle?.model, "deepseek-v4-pro");
85
+ assert.equal(oracle?.modelSource, undefined);
86
+ const reviewer = builtins.find((agent) => agent.name === "reviewer");
87
+ assert.equal(reviewer?.model, undefined);
88
+ assert.equal(reviewer?.modelSource, undefined);
89
+ });
90
+
91
+ it("clears subagents.defaultModel provenance for same-value agent model overrides", () => {
92
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
93
+ subagents: {
94
+ defaultModel: "deepseek-v4-flash",
95
+ agentOverrides: {
96
+ worker: { model: "deepseek-v4-flash" },
97
+ },
98
+ },
99
+ });
100
+
101
+ const worker = discoverAgentsAll(tempProject).builtin.find((agent) => agent.name === "worker");
102
+ assert.equal(worker?.model, "deepseek-v4-flash");
103
+ assert.equal(worker?.modelSource, undefined);
85
104
  });
86
105
 
87
106
  it("prefers project subagents.defaultModel over user defaultModel", () => {
@@ -2,8 +2,21 @@ import assert from "node:assert/strict";
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
- import { describe, it } from "node:test";
5
+ import { afterEach, describe, it } from "node:test";
6
6
  import { readAsyncRecoveryDescriptor } from "../../src/runs/background/async-resume.ts";
7
+ import { createRunFanoutBudget } from "../../src/runs/shared/run-fanout-budget.ts";
8
+
9
+ const budgetDirectories: string[] = [];
10
+
11
+ function runFanoutBudget(runId: string) {
12
+ const descriptor = createRunFanoutBudget(runId, 64);
13
+ budgetDirectories.push(descriptor.directory);
14
+ return descriptor;
15
+ }
16
+
17
+ afterEach(() => {
18
+ for (const directory of budgetDirectories.splice(0)) fs.rmSync(directory, { recursive: true, force: true });
19
+ });
7
20
 
8
21
  describe("async recovery descriptor", () => {
9
22
  it("accepts launchContractDigest written by async execution", () => {
@@ -13,6 +26,7 @@ describe("async recovery descriptor", () => {
13
26
  fs.writeFileSync(path.join(root, "recovery-descriptor.json"), JSON.stringify({
14
27
  version: 1,
15
28
  launchContractDigest: digest,
29
+ runFanoutBudget: runFanoutBudget("run-digest"),
16
30
  sourceRunId: "run-digest",
17
31
  agent: "worker",
18
32
  cwd: root,
@@ -38,6 +52,7 @@ describe("async recovery descriptor", () => {
38
52
  fs.writeFileSync(path.join(root, "recovery-descriptor.json"), JSON.stringify({
39
53
  version: 1,
40
54
  launchContractDigest: {},
55
+ runFanoutBudget: runFanoutBudget("run-bad-digest"),
41
56
  sourceRunId: "run-digest",
42
57
  agent: "worker",
43
58
  cwd: root,
@@ -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 { buildRevivedAsyncTask, resolveAsyncResumeTarget } from "../../src/runs/background/async-resume.ts";
7
+ import { createRunFanoutBudget } from "../../src/runs/shared/run-fanout-budget.ts";
7
8
 
8
9
  function writeJson(filePath: string, value: object): void {
9
10
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
@@ -97,7 +98,7 @@ describe("async resume lookup", () => {
97
98
  steps: [{ agent: "worker", status: "paused", sessionFile }],
98
99
  });
99
100
  const descriptor = {
100
- version: 1, sourceRunId: "run-descriptor", agent: "worker", cwd: root, systemPromptMode: "replace",
101
+ version: 1, runFanoutBudget: createRunFanoutBudget("run-descriptor", 64), sourceRunId: "run-descriptor", agent: "worker", cwd: root, systemPromptMode: "replace",
101
102
  inheritProjectContext: false, inheritSkills: false, outputMode: "inline", maxSubagentDepth: 2, share: false,
102
103
  };
103
104
  writeJson(path.join(asyncDir, "recovery-descriptor.json"), { ...descriptor, token: "must-not-be-accepted" });
@@ -141,6 +142,7 @@ describe("async resume lookup", () => {
141
142
  });
142
143
  const descriptor = {
143
144
  version: 1,
145
+ runFanoutBudget: createRunFanoutBudget("run-turn-budget", 64),
144
146
  sourceRunId: "run-turn-budget",
145
147
  agent: "worker",
146
148
  cwd: root,
@@ -195,6 +197,7 @@ describe("async resume lookup", () => {
195
197
  });
196
198
  writeJson(path.join(asyncDir, "recovery-descriptor.json"), {
197
199
  version: 1,
200
+ runFanoutBudget: createRunFanoutBudget("run-acceptance", 64),
198
201
  sourceRunId: "run-acceptance",
199
202
  agent: "worker",
200
203
  cwd: root,
@@ -244,6 +247,7 @@ describe("async resume lookup", () => {
244
247
  });
245
248
  writeJson(path.join(asyncDir, "recovery-descriptor.json"), {
246
249
  version: 1,
250
+ runFanoutBudget: createRunFanoutBudget("run-reviewed-acceptance", 64),
247
251
  sourceRunId: "run-reviewed-acceptance",
248
252
  agent: "worker",
249
253
  cwd: root,
@@ -288,6 +292,7 @@ describe("async resume lookup", () => {
288
292
  });
289
293
  writeJson(path.join(asyncDir, "recovery-descriptor.json"), {
290
294
  version: 1,
295
+ runFanoutBudget: createRunFanoutBudget("run-inferred-acceptance", 64),
291
296
  sourceRunId: "run-inferred-acceptance",
292
297
  agent: "worker",
293
298
  cwd: root,
@@ -12,6 +12,8 @@ import {
12
12
  } from "../../src/runs/background/chain-append.ts";
13
13
  import type { AsyncStatus } from "../../src/shared/types.ts";
14
14
  import type { RunnerStep } from "../../src/runs/shared/parallel-utils.ts";
15
+ import { claimRunFanoutBatchWithCommit, createRunFanoutBudget, getRunFanoutBudgetSnapshot } from "../../src/runs/shared/run-fanout-budget.ts";
16
+ import { PROMPT_REDACTED } from "../../src/shared/utils.ts";
15
17
  import { createTempDir, removeTempDir } from "../support/helpers.ts";
16
18
 
17
19
  function writeStatus(asyncDir: string, status: Partial<AsyncStatus> & Pick<AsyncStatus, "runId" | "mode" | "state" | "startedAt">): void {
@@ -71,6 +73,75 @@ describe("chain append requests", () => {
71
73
  }
72
74
  });
73
75
 
76
+ it("persists the request inside the admission callback", () => {
77
+ const root = createTempDir("pi-chain-append-admission-");
78
+ try {
79
+ const asyncDir = path.join(root, "run-admission");
80
+ writeStatus(asyncDir, {
81
+ runId: "run-admission",
82
+ mode: "chain",
83
+ state: "running",
84
+ startedAt: 100,
85
+ steps: [{ agent: "scout", status: "running" }],
86
+ });
87
+ let admitted = false;
88
+ const result = enqueueChainAppendRequest({
89
+ asyncDir,
90
+ runId: "run-admission",
91
+ steps: [runnerStep("worker")],
92
+ now: 200,
93
+ admit: (persist) => {
94
+ assert.equal(countPendingChainAppendRequests(asyncDir), 0);
95
+ persist();
96
+ admitted = true;
97
+ },
98
+ });
99
+
100
+ assert.equal(admitted, true);
101
+ assert.equal(result.pendingCount, 1);
102
+ } finally {
103
+ removeTempDir(root);
104
+ }
105
+ });
106
+
107
+ it("reports accepted appends and keeps admission claims when bookkeeping fails after persistence", () => {
108
+ const root = createTempDir("pi-chain-append-bookkeeping-");
109
+ const budget = createRunFanoutBudget("append-bookkeeping", 1);
110
+ try {
111
+ const asyncDir = path.join(root, "run-bookkeeping");
112
+ writeStatus(asyncDir, {
113
+ runId: "run-bookkeeping",
114
+ mode: "chain",
115
+ state: "running",
116
+ startedAt: 100,
117
+ steps: [{ agent: "scout", status: "running" }],
118
+ });
119
+ const statusPath = path.join(asyncDir, "status.json");
120
+ fs.mkdirSync(path.join(asyncDir, "events.jsonl"));
121
+
122
+ const result = enqueueChainAppendRequest({
123
+ asyncDir,
124
+ runId: "run-bookkeeping",
125
+ steps: [runnerStep("worker")],
126
+ now: 200,
127
+ admit: (persist) => claimRunFanoutBatchWithCommit(budget, ["chain[1]"], () => {
128
+ persist();
129
+ fs.rmSync(statusPath, { force: true });
130
+ fs.mkdirSync(statusPath);
131
+ }),
132
+ });
133
+
134
+ assert.equal(result.pendingCount, 1);
135
+ assert.match(result.bookkeepingError ?? "", /status update failed/);
136
+ assert.match(result.bookkeepingError ?? "", /event append failed/);
137
+ assert.equal(readPendingChainAppendRequests(asyncDir).length, 1);
138
+ assert.deepEqual(getRunFanoutBudgetSnapshot(budget), { used: 1, limit: 1, remaining: 0 });
139
+ } finally {
140
+ fs.rmSync(budget.directory, { recursive: true, force: true });
141
+ removeTempDir(root);
142
+ }
143
+ });
144
+
74
145
  it("reads pending append requests without consuming them and reports reserved output names", () => {
75
146
  const root = createTempDir("pi-chain-append-pending-");
76
147
  try {
@@ -198,11 +269,11 @@ describe("chain append requests", () => {
198
269
  "reviewer:pending",
199
270
  "auditor:pending",
200
271
  ]);
201
- // Appended steps must carry their own bounded task description for fleet display.
272
+ // Appended steps must not carry raw task text into durable status.
202
273
  assert.deepEqual(status.steps?.slice(1).map((step) => step.description), [
203
- "Use {previous}",
204
- "Use {previous}",
205
- "Use {previous}",
274
+ PROMPT_REDACTED,
275
+ PROMPT_REDACTED,
276
+ PROMPT_REDACTED,
206
277
  ]);
207
278
  assert.deepEqual(status.parallelGroups, [{ start: 2, count: 2, stepIndex: 2 }]);
208
279
  assert.equal(status.workflowGraph?.nodes[1]?.id, "step-1");
@@ -53,6 +53,24 @@ test("declared read-only builtin tools suppress implementation-word false positi
53
53
  });
54
54
  });
55
55
 
56
+ test("hyphenated fix adjectives in review tasks do not trigger the completion guard", () => {
57
+ const result = evaluateCompletionMutationGuard({
58
+ agent: "worker",
59
+ task: "Return a review with the top 2-3 must-fix items.",
60
+ messages: [assistantText("Review: findings with severity labels.")],
61
+ });
62
+
63
+ assert.deepEqual(result, {
64
+ expectedMutation: false,
65
+ attemptedMutation: false,
66
+ triggered: false,
67
+ });
68
+ assert.equal(
69
+ expectsImplementationMutation("worker", "Return a review with the top 2-3 must-fix items."),
70
+ false,
71
+ );
72
+ });
73
+
56
74
  test("read-only issue drafting tasks do not trigger on suggested fix wording", () => {
57
75
  const task = "Draft GitHub issue for pi-subagents bug from current conversation. Include title, environment/context, reproduction steps, actual/expected, logs excerpt, suspected cause, suggested fix. Terse but complete. No tools needed.";
58
76
  const result = evaluateCompletionMutationGuard({
@@ -69,7 +69,7 @@ describe("buildDoctorReport", () => {
69
69
 
70
70
  const report = buildDoctorReport({
71
71
  cwd: root,
72
- config: { defaultSessionDir: "~/subagent-sessions", intercomBridge: { mode: "always" }, maxSubagentSpawnsPerSession: 4 },
72
+ config: { defaultSessionDir: "~/subagent-sessions", intercomBridge: { mode: "always" }, maxSubagentSpawnsPerSession: 4, maxActiveAsyncRunsPerSession: 2 },
73
73
  state,
74
74
  currentSessionFile: path.join(root, "sessions", "parent.jsonl"),
75
75
  currentSessionId: "session-abc123",
@@ -116,6 +116,10 @@ describe("buildDoctorReport", () => {
116
116
  assert.match(report, /Spawn budget\n- usage: 3\/5 used, 2 remaining \(configured 4; granted 1; grant allowance 3\)/);
117
117
  assert.match(report, /- recent grants: \+1 at 1970-01-01T00:00:00\.000Z \(4 → 5\)/);
118
118
  assert.match(report, /new parent session resets usage and grants; compaction does not/);
119
+ assert.match(report, /Run fan-out budget\n- configured limit: 64 \(default\)/);
120
+ assert.match(report, /cumulative claims are never released; a new top-level run creates a new budget/);
121
+ assert.match(report, /Active async capacity\n- usage: 0\/2 used/);
122
+ assert.match(report, /missing or unknown cleanup proof retains capacity/);
119
123
  assert.match(report, /- skills: total 2 \(project 1, user-package 1\)/);
120
124
  assert.match(report, /- bridge: active/);
121
125
  assert.match(report, /- supervisor channel: available \(native:pi-subagents-supervisor-channel\)/);
@@ -125,6 +129,30 @@ describe("buildDoctorReport", () => {
125
129
  }
126
130
  });
127
131
 
132
+ it("reports the effective source when the run fan-out environment value is invalid", () => {
133
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-doctor-fanout-source-"));
134
+ const previous = process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_RUN;
135
+ try {
136
+ process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_RUN = "invalid";
137
+ const report = buildDoctorReport({
138
+ cwd: root,
139
+ config: { maxSubagentSpawnsPerRun: 12 },
140
+ state: makeState(root),
141
+ deps: {
142
+ isAsyncAvailable: () => true,
143
+ discoverAgentsAll: () => ({ builtin: [], user: [], project: [], chains: [], userDir: root, projectDir: root, userChainDir: root, projectChainDir: root, userSettingsPath: path.join(root, "user.json"), projectSettingsPath: path.join(root, "project.json") }),
144
+ discoverAvailableSkills: () => [],
145
+ diagnoseIntercomBridge: () => ({ active: false, mode: "off", wantsIntercom: false, supervisorChannelAvailable: false, extensionDir: "none" }),
146
+ },
147
+ });
148
+ assert.match(report, /Run fan-out budget\n- configured limit: 12 \(config\)/);
149
+ } finally {
150
+ if (previous === undefined) delete process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_RUN;
151
+ else process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_RUN = previous;
152
+ fs.rmSync(root, { recursive: true, force: true });
153
+ }
154
+ });
155
+
128
156
  it("keeps reporting when a directory or discovery check fails", () => {
129
157
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-doctor-failure-"));
130
158
  try {