pi-subagents 0.49.0 → 0.51.0

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 (117) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/gpt-pro.md +17 -0
  3. package/agents/oracle.md +7 -5
  4. package/agents/researcher.md +1 -1
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +1 -1
  7. package/agents/worker.md +1 -1
  8. package/async-retention-discovery-worker.mjs +180 -0
  9. package/docs/agents.md +37 -2
  10. package/docs/configuration.md +76 -14
  11. package/docs/extension-api.md +78 -1
  12. package/docs/missions.md +1 -1
  13. package/docs/observability.md +20 -4
  14. package/docs/tool-reference.md +55 -39
  15. package/docs/workflows.md +171 -5
  16. package/package.json +4 -2
  17. package/skills/pi-subagents/SKILL.md +5 -4
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
  19. package/skills/pi-subagents/references/execution-controls.md +22 -18
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
  22. package/src/agents/agent-management.ts +100 -345
  23. package/src/agents/agent-serializer.ts +2 -0
  24. package/src/agents/agents.ts +135 -25
  25. package/src/api/external-job-provider.ts +185 -0
  26. package/src/api/external-runs.ts +174 -84
  27. package/src/api/preflight.ts +42 -11
  28. package/src/api/shared-types.ts +2 -0
  29. package/src/extension/config.ts +36 -3
  30. package/src/extension/doctor.ts +3 -6
  31. package/src/extension/fanout-child.ts +2 -2
  32. package/src/extension/index.ts +210 -90
  33. package/src/extension/public-execution.ts +31 -2
  34. package/src/extension/rpc.ts +5 -1
  35. package/src/extension/schemas.ts +14 -36
  36. package/src/extension/tool-description.ts +37 -24
  37. package/src/inspectors/herdr/actions.ts +5 -9
  38. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  39. package/src/inspectors/herdr/project-panes.ts +4 -8
  40. package/src/inspectors/herdr/shell-command.ts +16 -0
  41. package/src/intercom/intercom-bridge.ts +2 -3
  42. package/src/intercom/native-supervisor-channel.ts +49 -51
  43. package/src/missions/goal-driver.ts +3 -1
  44. package/src/missions/lifecycle.ts +6 -1
  45. package/src/missions/store.ts +4 -9
  46. package/src/profiles/profiles.ts +3 -1
  47. package/src/runs/background/active-run-index.ts +94 -1
  48. package/src/runs/background/async-execution.ts +98 -38
  49. package/src/runs/background/async-job-tracker.ts +21 -4
  50. package/src/runs/background/async-resume.ts +30 -17
  51. package/src/runs/background/async-retention.ts +888 -0
  52. package/src/runs/background/async-status-snapshot.ts +277 -0
  53. package/src/runs/background/async-status.ts +47 -56
  54. package/src/runs/background/chain-append.ts +3 -33
  55. package/src/runs/background/chain-root-attachment.ts +2 -2
  56. package/src/runs/background/completion-replay.ts +11 -1
  57. package/src/runs/background/control-channel.ts +14 -68
  58. package/src/runs/background/fleet-view.ts +3 -1
  59. package/src/runs/background/index-segment.ts +59 -0
  60. package/src/runs/background/notify.ts +3 -1
  61. package/src/runs/background/result-files.ts +505 -0
  62. package/src/runs/background/result-watcher.ts +250 -51
  63. package/src/runs/background/retained-children.ts +79 -20
  64. package/src/runs/background/run-id-query.ts +7 -0
  65. package/src/runs/background/run-id-resolver.ts +37 -29
  66. package/src/runs/background/run-status.ts +34 -20
  67. package/src/runs/background/scheduled-runs.ts +71 -27
  68. package/src/runs/background/stale-run-reconciler.ts +33 -16
  69. package/src/runs/background/steering.ts +11 -1
  70. package/src/runs/background/subagent-runner.ts +535 -161
  71. package/src/runs/background/subagent-wait.ts +9 -7
  72. package/src/runs/background/terminal-run-index.ts +129 -0
  73. package/src/runs/background/wait-completions.ts +22 -2
  74. package/src/runs/background/wait-subscriptions.ts +80 -1
  75. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  76. package/src/runs/foreground/async-steering-action.ts +21 -14
  77. package/src/runs/foreground/execution.ts +221 -15
  78. package/src/runs/foreground/subagent-executor.ts +529 -1519
  79. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  80. package/src/runs/shared/chain-outputs.ts +1 -3
  81. package/src/runs/shared/completion-guard.ts +96 -6
  82. package/src/runs/shared/external-cli-runner.ts +4 -0
  83. package/src/runs/shared/external-job-bridge.ts +450 -0
  84. package/src/runs/shared/external-job-runner.ts +286 -0
  85. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  86. package/src/runs/shared/model-fallback.ts +34 -3
  87. package/src/runs/shared/nested-events.ts +66 -62
  88. package/src/runs/shared/orca-progress-tabs.ts +437 -0
  89. package/src/runs/shared/parallel-handoff.ts +46 -4
  90. package/src/runs/shared/parallel-utils.ts +6 -15
  91. package/src/runs/shared/permissions.ts +5 -1
  92. package/src/runs/shared/pi-args.ts +8 -1
  93. package/src/runs/shared/subagent-control.ts +41 -4
  94. package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
  95. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  96. package/src/runs/shared/tool-timeout.ts +93 -0
  97. package/src/runs/shared/workflow-graph.ts +1 -23
  98. package/src/runs/shared/worktree.ts +12 -1
  99. package/src/shared/atomic-json.ts +22 -2
  100. package/src/shared/capacity-resilient-json.ts +102 -0
  101. package/src/shared/completion-owner.ts +14 -0
  102. package/src/shared/file-system-retry.ts +49 -1
  103. package/src/shared/fork-context.ts +42 -0
  104. package/src/shared/prompt-resources.ts +0 -40
  105. package/src/shared/settings.ts +3 -27
  106. package/src/shared/types.ts +88 -27
  107. package/src/shared/utils.ts +8 -0
  108. package/src/shared/watch-strategy.ts +10 -0
  109. package/src/slash/slash-commands.ts +45 -28
  110. package/src/slash/slash-live-state.ts +3 -0
  111. package/src/tui/fleet-status.ts +160 -45
  112. package/src/tui/fleet.ts +186 -28
  113. package/src/tui/render.ts +41 -10
  114. package/src/workflows/chat-progress.ts +7 -5
  115. package/src/workflows/scripted-workflow.ts +424 -125
  116. package/src/runs/foreground/chain-clarify.ts +0 -1354
  117. package/src/runs/foreground/chain-execution.ts +0 -1565
@@ -0,0 +1,437 @@
1
+ import { spawn } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import type { Message } from "@earendil-works/pi-ai";
6
+ import { resolveNodeExecutable } from "../../shared/node-executable.ts";
7
+ import { TEMP_ROOT_DIR, type OrcaProgressTabsConfig } from "../../shared/types.ts";
8
+ import { extractTextFromContent, extractToolArgsPreview, getAgentDir } from "../../shared/utils.ts";
9
+
10
+ const ORCA_CREATE_TIMEOUT_MS = 20_000;
11
+ const ORCA_KILL_GRACE_MS = 2_000;
12
+ const CLEANUP_DELAY_MS = 5 * 60_000;
13
+ const STALE_PROGRESS_MAX_AGE_MS = 24 * 60 * 60_000;
14
+ const VIEWER_POLL_MS = 150;
15
+ const MAX_MIRROR_BYTES = 1024 * 1024;
16
+ const MIRROR_FOOTER_RESERVE_BYTES = 16 * 1024;
17
+ const COUNTER_LOCK_STALE_MS = 30_000;
18
+ const COUNTER_LOCK_RETRIES = 200;
19
+ const COUNTER_LOCK_RETRY_MS = 10;
20
+ const ORCA_CREATE_WAIT_TIMEOUT_MS = ORCA_CREATE_TIMEOUT_MS + ORCA_KILL_GRACE_MS + 3_000;
21
+
22
+ const ORCA_CREATE_WATCHDOG_SCRIPT = [
23
+ "const {spawn}=require('node:child_process');",
24
+ "const fs=require('node:fs');",
25
+ "const timeout=Number(process.argv[1]),grace=Number(process.argv[2]),waitTimeout=Number(process.argv[3]);",
26
+ "const previous=process.argv[4],done=process.argv[5],command=process.argv[6],args=process.argv.slice(7);",
27
+ "function mark(){try{fs.writeFileSync(done.replace(/\\.pending$/,'.ready'),'')}catch{}}",
28
+ "function exists(file){try{return fs.existsSync(file)}catch{return false}}",
29
+ "function keepQueued(){try{const now=new Date();fs.utimesSync(done,now,now)}catch{}}",
30
+ "function predecessorReady(){if(previous==='-')return true;if(exists(previous.replace(/\\.pending$/,'.ready')))return true;try{return Date.now()-fs.statSync(previous).mtimeMs>=waitTimeout}catch{return true}}",
31
+ "function start(){",
32
+ " try{",
33
+ " const child=spawn(command,args,{stdio:'ignore',windowsHide:true});",
34
+ " let hardKill;",
35
+ " const timer=setTimeout(()=>{child.kill('SIGTERM');hardKill=setTimeout(()=>child.kill('SIGKILL'),grace)},timeout);",
36
+ " const clear=()=>{clearTimeout(timer);if(hardKill)clearTimeout(hardKill);mark()};",
37
+ " child.once('error',()=>{clear();process.exitCode=1});",
38
+ " child.once('close',code=>{clear();process.exitCode=code===0?0:1});",
39
+ " }catch{mark();process.exitCode=1}",
40
+ "}",
41
+ "if(predecessorReady())start();",
42
+ "else{(function waitPrev(){keepQueued();if(predecessorReady())return start();setTimeout(waitPrev,20)})();}",
43
+ ].join("");
44
+
45
+ const ORCA_CLEANUP_WATCHDOG_SCRIPT = [
46
+ "const fs=require('node:fs');",
47
+ "const deadline=Date.now()+Number(process.argv[1]),files=process.argv.slice(2);",
48
+ "function check(){if(!files.some(file=>fs.existsSync(file)))process.exit(0);if(Date.now()<deadline)return;for(const file of files){try{fs.rmSync(file,{force:true})}catch{}}process.exit(0)}",
49
+ "setInterval(check,1000);check();",
50
+ ].join("");
51
+
52
+ export interface OrcaProgressTab {
53
+ append(text: string): void;
54
+ event(event: { type?: string; message?: Message; toolName?: string; args?: unknown }): void;
55
+ finish(status: "completed" | "failed" | "stopped", sessionFile?: string): void;
56
+ }
57
+
58
+ function executableFile(candidate: string): boolean {
59
+ try {
60
+ const stats = fs.statSync(candidate);
61
+ if (!stats.isFile()) return false;
62
+ if (process.platform === "win32") return true;
63
+ fs.accessSync(candidate, fs.constants.X_OK);
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ function executableNames(command: string, env: NodeJS.ProcessEnv): string[] {
71
+ if (process.platform !== "win32") return [command];
72
+ const extensions = (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean);
73
+ return [command, ...extensions.map((extension) => `${command}${extension.toLowerCase()}`), ...extensions.map((extension) => `${command}${extension.toUpperCase()}`)];
74
+ }
75
+
76
+ export function resolveOrcaCommand(env: NodeJS.ProcessEnv = process.env): string | undefined {
77
+ const override = env.PI_SUBAGENT_ORCA_BINARY?.trim();
78
+ if (override) return executableFile(override) ? override : undefined;
79
+ for (const directory of (env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
80
+ for (const name of executableNames("orca", env)) {
81
+ const candidate = path.join(directory, name);
82
+ if (executableFile(candidate)) return candidate;
83
+ }
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ function shellQuote(value: string): string {
89
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
90
+ }
91
+
92
+ const VIEWER_SCRIPT = [
93
+ "const fs=require('fs'),{StringDecoder}=require('string_decoder');",
94
+ "const log=process.argv[1],done=process.argv[2],decoder=new StringDecoder('utf8');",
95
+ "let offset=0,finishing=false,state='text';",
96
+ "function sanitize(input){let output='';for(const char of input){const code=char.charCodeAt(0);",
97
+ "if(state==='text'){if(code===27){state='escape';continue}if(code===155){state='csi';continue}if(code===157){state='osc';continue}if(code===144||code===152||code===158||code===159){state='string';continue}if(code===10||(code>=32&&code!==127&&(code<128||code>159)))output+=char;continue}",
98
+ "if(state==='escape'){if(char==='[')state='csi';else if(char===']')state='osc';else if(char==='P'||char==='X'||char==='^'||char==='_')state='string';else if(code<32||code>47)state='text';continue}",
99
+ "if(state==='csi'){if(code>=64&&code<=126)state='text';continue}",
100
+ "if(state==='osc'){if(code===7)state='text';else if(code===27)state='osc-escape';continue}",
101
+ "if(state==='osc-escape'){state=char==='\\\\'?'text':code===27?'osc-escape':'osc';continue}",
102
+ "if(state==='string'){if(code===27)state='string-escape';continue}",
103
+ "if(state==='string-escape')state=char==='\\\\'?'text':code===27?'string-escape':'string';",
104
+ "}return output}",
105
+ "function write(buffer){const output=sanitize(decoder.write(buffer));if(output)process.stdout.write(output)}",
106
+ "function pump(){",
107
+ " try{const size=fs.statSync(log).size;if(size<offset)offset=0;if(size>offset){const fd=fs.openSync(log,'r');const b=Buffer.alloc(size-offset);fs.readSync(fd,b,0,b.length,offset);fs.closeSync(fd);offset=size;write(b);}}catch{}",
108
+ " if(!finishing&&fs.existsSync(done)){finishing=true;pump();setTimeout(()=>{const output=sanitize(decoder.end());if(output)process.stdout.write(output);try{fs.unlinkSync(done)}catch{}try{fs.unlinkSync(log)}catch{}process.exit(0)},250);}",
109
+ "}",
110
+ `const timer=setInterval(pump,${VIEWER_POLL_MS});`,
111
+ "pump();",
112
+ "process.on('exit',()=>clearInterval(timer));",
113
+ ].join("");
114
+
115
+ function viewerCommand(nodeExecutable: string, logPath: string, donePath: string): string {
116
+ const invocation = [nodeExecutable, "-e", VIEWER_SCRIPT, logPath, donePath].map(shellQuote).join(" ");
117
+ // Do not replace or exit Orca's interactive shell. The short-lived viewer may
118
+ // finish and delete its mirror files, while the completed tab remains open at
119
+ // the shell prompt until the user closes it.
120
+ return invocation;
121
+ }
122
+
123
+ function progressRoot(): string {
124
+ return path.join(TEMP_ROOT_DIR, "orca-progress");
125
+ }
126
+
127
+ function pruneStaleProgressFiles(root: string, now = Date.now()): void {
128
+ try {
129
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
130
+ if (!entry.isFile() || (!entry.name.endsWith(".log") && !entry.name.endsWith(".done") && !entry.name.endsWith(".ready") && !entry.name.endsWith(".pending"))) continue;
131
+ const file = path.join(root, entry.name);
132
+ try {
133
+ if (now - fs.statSync(file).mtimeMs > STALE_PROGRESS_MAX_AGE_MS) fs.rmSync(file, { force: true });
134
+ } catch { /* best-effort stale cleanup */ }
135
+ }
136
+ } catch {
137
+ // The observer remains optional when temp cleanup is unavailable.
138
+ }
139
+ }
140
+
141
+ function safeSegment(value: unknown, fallback = "subagent"): string {
142
+ if (typeof value !== "string") return fallback;
143
+ return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || fallback;
144
+ }
145
+
146
+ function sleepSync(ms: number): void {
147
+ const signal = new Int32Array(new SharedArrayBuffer(4));
148
+ Atomics.wait(signal, 0, 0, ms);
149
+ }
150
+
151
+ function resolveSequenceScope(cwd: string): string {
152
+ let current = path.resolve(cwd);
153
+ try { current = fs.realpathSync(current); } catch { /* use the lexical cwd */ }
154
+ while (true) {
155
+ try {
156
+ if (fs.existsSync(path.join(current, ".git"))) return current;
157
+ } catch { /* keep walking */ }
158
+ const parent = path.dirname(current);
159
+ if (parent === current) return path.resolve(cwd);
160
+ current = parent;
161
+ }
162
+ }
163
+
164
+ function sequenceKey(cwd: string): string {
165
+ return createHash("sha256").update(resolveSequenceScope(cwd)).digest("hex").slice(0, 20);
166
+ }
167
+
168
+ function withSequenceLock<T>(root: string, key: string, fn: () => T): T | undefined {
169
+ const lockPath = path.join(root, `counter-${key}.lock`);
170
+ let locked = false;
171
+ for (let attempt = 0; attempt < COUNTER_LOCK_RETRIES; attempt++) {
172
+ try {
173
+ fs.mkdirSync(lockPath, { mode: 0o700 });
174
+ locked = true;
175
+ break;
176
+ } catch (error) {
177
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") break;
178
+ try {
179
+ if (Date.now() - fs.statSync(lockPath).mtimeMs > COUNTER_LOCK_STALE_MS) {
180
+ fs.rmSync(lockPath, { recursive: true, force: true });
181
+ continue;
182
+ }
183
+ } catch { /* another process released the lock */ }
184
+ sleepSync(COUNTER_LOCK_RETRY_MS);
185
+ }
186
+ }
187
+ if (!locked) return undefined;
188
+ try {
189
+ return fn();
190
+ } finally {
191
+ try { fs.rmSync(lockPath, { recursive: true, force: true }); } catch { /* stale lock cleanup handles crashes */ }
192
+ }
193
+ }
194
+
195
+ function predecessorMarker(pathValue: string | undefined): string | undefined {
196
+ if (!pathValue) return undefined;
197
+ const ready = pathValue.endsWith(".pending") ? pathValue.replace(/\.pending$/, ".ready") : pathValue;
198
+ const pending = ready.replace(/\.ready$/, ".pending");
199
+ if (fs.existsSync(ready) || fs.existsSync(pending)) return pending;
200
+ return undefined;
201
+ }
202
+
203
+ function reserveTabSequence(root: string, cwd: string): { sequence: number; previousCreateDone?: string; createDone: string } | undefined {
204
+ const key = sequenceKey(cwd);
205
+ const counterPath = path.join(root, `counter-${key}`);
206
+ const createDone = path.join(root, `create-${key}-${randomUUID()}.pending`);
207
+ return withSequenceLock(root, key, () => {
208
+ let current = 0;
209
+ let previousCreateDone: string | undefined;
210
+ try {
211
+ const raw = fs.readFileSync(counterPath, "utf-8");
212
+ const [countLine, previousLine] = raw.split("\n");
213
+ const parsed = Number.parseInt(countLine ?? "", 10);
214
+ if (Number.isSafeInteger(parsed) && parsed >= 0) current = parsed;
215
+ previousCreateDone = predecessorMarker(previousLine?.trim());
216
+ } catch { /* first tab for this worktree */ }
217
+ const next = current + 1;
218
+ try {
219
+ fs.writeFileSync(createDone, "", { encoding: "utf-8", mode: 0o600 });
220
+ fs.writeFileSync(counterPath, `${next}\n${createDone}\n`, { encoding: "utf-8", mode: 0o600 });
221
+ return { sequence: next, previousCreateDone, createDone };
222
+ } catch {
223
+ try { fs.rmSync(createDone, { force: true }); } catch { /* best effort */ }
224
+ return undefined;
225
+ }
226
+ });
227
+ }
228
+
229
+ export function resolvePiSessionId(sessionFile: string | undefined): string | undefined {
230
+ if (!sessionFile) return undefined;
231
+ try {
232
+ const fd = fs.openSync(sessionFile, "r");
233
+ try {
234
+ const buffer = Buffer.alloc(16 * 1024);
235
+ const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
236
+ const line = buffer.toString("utf-8", 0, bytes).split("\n", 1)[0];
237
+ if (!line) return undefined;
238
+ const header = JSON.parse(line) as { type?: unknown; id?: unknown };
239
+ if (header.type === "session" && typeof header.id === "string" && /^[A-Za-z0-9-]{8,}$/.test(header.id)) return header.id;
240
+ return undefined;
241
+ } finally {
242
+ fs.closeSync(fd);
243
+ }
244
+ } catch {
245
+ return undefined;
246
+ }
247
+ }
248
+
249
+ function scheduleCleanup(nodeExecutable: string, paths: string[]): void {
250
+ try {
251
+ const watchdog = spawn(nodeExecutable, ["-e", ORCA_CLEANUP_WATCHDOG_SCRIPT, String(CLEANUP_DELAY_MS), ...paths], {
252
+ detached: true,
253
+ stdio: "ignore",
254
+ windowsHide: true,
255
+ });
256
+ watchdog.once("error", () => {});
257
+ watchdog.unref();
258
+ } catch {
259
+ // Cleanup remains best effort for this optional observer.
260
+ }
261
+ }
262
+
263
+ function loadOrcaProgressTabsConfig(): OrcaProgressTabsConfig | undefined {
264
+ try {
265
+ const configPath = path.join(getAgentDir(), "extensions", "subagent", "config.json");
266
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
267
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
268
+ const value = (parsed as Record<string, unknown>).orcaProgressTabs;
269
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
270
+ const config = value as Record<string, unknown>;
271
+ if (Object.keys(config).some((key) => key !== "enabled") || typeof config.enabled !== "boolean") return undefined;
272
+ return { enabled: config.enabled };
273
+ } catch {
274
+ return undefined;
275
+ }
276
+ }
277
+
278
+ export function createOrcaProgressTab(input: {
279
+ cwd: string;
280
+ runId: string;
281
+ agent: string;
282
+ index: number;
283
+ config?: OrcaProgressTabsConfig;
284
+ env?: NodeJS.ProcessEnv;
285
+ command?: string;
286
+ }): OrcaProgressTab | undefined {
287
+ const config = input.config ?? loadOrcaProgressTabsConfig();
288
+ if (config?.enabled !== true || process.platform === "win32") return undefined;
289
+ const command = input.command ?? resolveOrcaCommand(input.env);
290
+ if (!command) return undefined;
291
+ const nodeExecutable = resolveNodeExecutable();
292
+
293
+ const root = progressRoot();
294
+ try {
295
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
296
+ } catch {
297
+ return undefined;
298
+ }
299
+ pruneStaleProgressFiles(root);
300
+ const runId = safeSegment(input.runId, "run");
301
+ const agent = safeSegment(input.agent, "subagent");
302
+ const index = typeof input.index === "number" && Number.isInteger(input.index) && input.index >= 0 ? input.index : 0;
303
+ const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : process.cwd();
304
+ const reservation = reserveTabSequence(root, cwd);
305
+ if (reservation === undefined) return undefined;
306
+ const tabSequence = reservation.sequence;
307
+ const stem = `${runId}-${index}-${randomUUID()}`;
308
+ const logPath = path.join(root, `${stem}.log`);
309
+ const donePath = path.join(root, `${stem}.done`);
310
+ const title = `subagent · ${agent} · ${tabSequence}`;
311
+ const markCreateReady = () => {
312
+ try { fs.writeFileSync(reservation.createDone.replace(/\.pending$/, ".ready"), ""); } catch { /* unblock later tabs */ }
313
+ };
314
+ try {
315
+ fs.writeFileSync(logPath, `pi-subagents / ${agent}\nrun ${runId} · child ${index + 1}\n${"─".repeat(48)}\n`, { encoding: "utf-8", mode: 0o600 });
316
+ fs.rmSync(donePath, { force: true });
317
+ } catch {
318
+ markCreateReady();
319
+ return undefined;
320
+ }
321
+
322
+ let available = true;
323
+ let truncated = false;
324
+ let scheduledBytes = fs.statSync(logPath).size;
325
+ const progressByteLimit = MAX_MIRROR_BYTES - MIRROR_FOOTER_RESERVE_BYTES;
326
+ const logStream = fs.createWriteStream(logPath, { flags: "a", mode: 0o600 });
327
+ const failObserver = () => {
328
+ if (!available) return;
329
+ available = false;
330
+ logStream.destroy();
331
+ try { fs.rmSync(logPath, { force: true }); } catch { /* best effort */ }
332
+ try { fs.rmSync(donePath, { force: true }); } catch { /* best effort */ }
333
+ };
334
+ logStream.once("error", failObserver);
335
+ const writeProgress = (text: string) => {
336
+ if (!available || !text || truncated) return;
337
+ const bytes = Buffer.byteLength(text);
338
+ if (scheduledBytes + bytes > progressByteLimit) {
339
+ truncated = true;
340
+ return;
341
+ }
342
+ scheduledBytes += bytes;
343
+ try {
344
+ logStream.write(text);
345
+ } catch {
346
+ failObserver();
347
+ }
348
+ };
349
+ let createSettled = false;
350
+ let cleanupPaths: string[] | undefined;
351
+ const scheduleDeferredCleanup = () => {
352
+ if (!createSettled || cleanupPaths === undefined) return;
353
+ scheduleCleanup(nodeExecutable, cleanupPaths);
354
+ cleanupPaths = undefined;
355
+ };
356
+ try {
357
+ const watchdog = spawn(nodeExecutable, [
358
+ "-e", ORCA_CREATE_WATCHDOG_SCRIPT,
359
+ String(ORCA_CREATE_TIMEOUT_MS),
360
+ String(ORCA_KILL_GRACE_MS),
361
+ String(ORCA_CREATE_WAIT_TIMEOUT_MS),
362
+ reservation.previousCreateDone ?? "-",
363
+ reservation.createDone,
364
+ command,
365
+ "terminal", "create",
366
+ "--worktree", `path:${path.resolve(cwd)}`,
367
+ "--title", title,
368
+ "--command", viewerCommand(nodeExecutable, logPath, donePath),
369
+ "--json",
370
+ ], {
371
+ cwd,
372
+ detached: true,
373
+ stdio: "ignore",
374
+ windowsHide: true,
375
+ env: input.env ?? process.env,
376
+ });
377
+ watchdog.once("close", (code) => {
378
+ createSettled = true;
379
+ if (code !== 0) failObserver();
380
+ scheduleDeferredCleanup();
381
+ });
382
+ watchdog.once("error", () => {
383
+ markCreateReady();
384
+ createSettled = true;
385
+ failObserver();
386
+ scheduleDeferredCleanup();
387
+ });
388
+ watchdog.unref();
389
+ } catch {
390
+ markCreateReady();
391
+ failObserver();
392
+ return undefined;
393
+ }
394
+
395
+ let finished = false;
396
+ return {
397
+ append(text) {
398
+ if (finished) return;
399
+ writeProgress(text);
400
+ },
401
+ event(event) {
402
+ if (!available || finished) return;
403
+ if (event.type === "tool_execution_start" && event.toolName) {
404
+ const args = event.args && typeof event.args === "object" && !Array.isArray(event.args)
405
+ ? extractToolArgsPreview(event.args as Record<string, unknown>)
406
+ : "";
407
+ writeProgress(`\n› ${event.toolName}${args ? `: ${args}` : ""}\n`);
408
+ return;
409
+ }
410
+ if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
411
+ const text = extractTextFromContent(event.message.content);
412
+ if (text.trim()) writeProgress(`${text}${text.endsWith("\n") ? "" : "\n"}`);
413
+ }
414
+ },
415
+ finish(status, sessionFile) {
416
+ if (!available || finished) return;
417
+ finished = true;
418
+ const sessionId = status === "completed" ? resolvePiSessionId(sessionFile) : undefined;
419
+ let verifiedSessionFile: string | undefined;
420
+ if (sessionId && sessionFile) {
421
+ try { verifiedSessionFile = fs.realpathSync(sessionFile); } catch { /* the session is no longer available */ }
422
+ }
423
+ const terminalMessage = verifiedSessionFile
424
+ ? `completed. To remove the Pi session of this subagent, run rm -- ${shellQuote(verifiedSessionFile)}`
425
+ : status;
426
+ const truncation = truncated ? `\n[progress mirror truncated at ${MAX_MIRROR_BYTES} bytes]\n` : "";
427
+ let footer = `${truncation}\n${"─".repeat(48)}\n${terminalMessage}\n`;
428
+ if (scheduledBytes + Buffer.byteLength(footer) > MAX_MIRROR_BYTES) footer = `\n${status}\n`;
429
+ logStream.end(footer, () => {
430
+ if (!available) return;
431
+ try { fs.writeFileSync(donePath, `${status}\n`, { encoding: "utf-8", mode: 0o600 }); } catch { /* best effort */ }
432
+ cleanupPaths = [logPath, donePath];
433
+ scheduleDeferredCleanup();
434
+ });
435
+ },
436
+ };
437
+ }
@@ -34,6 +34,48 @@ function readManifest(manifestPath: string): ParallelHandoffManifest | undefined
34
34
  return parsed;
35
35
  }
36
36
 
37
+ function resolveExistingPath(candidate: string): string {
38
+ try {
39
+ return fs.realpathSync(candidate);
40
+ } catch {
41
+ return path.resolve(candidate);
42
+ }
43
+ }
44
+
45
+ function pathInside(root: string, candidate: string): boolean {
46
+ const relative = path.relative(root, candidate);
47
+ return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
48
+ }
49
+
50
+ export function resolveRetainedWorktreeCwd(manifestPath: string, runId: string, childIndex: number): string | undefined {
51
+ const manifest = readManifest(manifestPath);
52
+ if (!manifest) return undefined;
53
+ if (manifest.runId !== runId) throw new Error(`Managed worktree handoff belongs to run '${manifest.runId}', not '${runId}'.`);
54
+ const match = manifest.groups
55
+ .flatMap((group) => group.children.map((child) => ({ group, child })))
56
+ .find(({ child }) => child.index === childIndex);
57
+ if (!match) return undefined;
58
+ const cleanup = match.group.cleanup.tasks.find((task) => task.index === match.child.taskIndex);
59
+ if (!cleanup) throw new Error(`Async run '${runId}' child ${childIndex} has no managed worktree cleanup record.`);
60
+ const relativeCwd = path.relative(resolveExistingPath(match.group.repoRoot), resolveExistingPath(manifest.cwd));
61
+ if (path.isAbsolute(relativeCwd) || relativeCwd === ".." || relativeCwd.startsWith(`..${path.sep}`)) throw new Error(`Async run '${runId}' has an invalid managed worktree cwd.`);
62
+ const requiredCwd = path.join(cleanup.path, relativeCwd);
63
+ if (cleanup.worktreeRemoved || cleanup.branchRemoved) throw new Error(`Async run '${runId}' required managed worktree was removed: ${requiredCwd}`);
64
+ let worktreeRoot = "";
65
+ let resolvedRequiredCwd = "";
66
+ try {
67
+ const rootStat = fs.lstatSync(cleanup.path);
68
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error("worktree root is not a directory");
69
+ worktreeRoot = fs.realpathSync(cleanup.path);
70
+ if (!fs.statSync(requiredCwd).isDirectory()) throw new Error("path is not a directory");
71
+ resolvedRequiredCwd = fs.realpathSync(requiredCwd);
72
+ } catch (error) {
73
+ throw new Error(`Async run '${runId}' required managed worktree cwd is missing: ${requiredCwd}`, { cause: error instanceof Error ? error : undefined });
74
+ }
75
+ if (!pathInside(worktreeRoot, resolvedRequiredCwd)) throw new Error(`Async run '${runId}' has an invalid managed worktree cwd.`);
76
+ return requiredCwd;
77
+ }
78
+
37
79
  function referenceFor(manifestPath: string, manifest: ParallelHandoffManifest): ParallelHandoffReference {
38
80
  const children = manifest.groups.flatMap((group) => group.children);
39
81
  return {
@@ -74,7 +116,7 @@ function missingDiff(input: { manifestPath: string; stepIndex: number; taskIndex
74
116
  export function writeParallelHandoffGroup(input: {
75
117
  manifestPath: string;
76
118
  runId: string;
77
- mode: "parallel" | "chain";
119
+ mode: "single" | "parallel" | "chain";
78
120
  source: "foreground" | "async";
79
121
  cwd: string;
80
122
  stepIndex: number;
@@ -162,7 +204,7 @@ export function parallelHandoffPath(baseDir: string, runId?: string): string {
162
204
  export function writePendingParallelHandoff(input: {
163
205
  manifestPath: string;
164
206
  runId: string;
165
- mode: "parallel" | "chain";
207
+ mode: "single" | "parallel" | "chain";
166
208
  source: "foreground" | "async";
167
209
  cwd: string;
168
210
  stepIndex: number;
@@ -173,11 +215,11 @@ export function writePendingParallelHandoff(input: {
173
215
  }
174
216
 
175
217
  export function formatParallelHandoffReference(reference: ParallelHandoffReference): string {
176
- return `Parallel handoff: ${reference.path} (${reference.childCount} children, ${reference.changedPatches} changed patches, cleanup ${reference.cleanupState})`;
218
+ return `Worktree handoff: ${reference.path} (${reference.childCount} children, ${reference.changedPatches} changed patches, cleanup ${reference.cleanupState})`;
177
219
  }
178
220
 
179
221
  export function formatParallelHandoffError(error: unknown): string {
180
- return `Parallel handoff unavailable: ${error instanceof Error ? error.message : String(error)}`;
222
+ return `Worktree handoff unavailable: ${error instanceof Error ? error.message : String(error)}`;
181
223
  }
182
224
 
183
225
  export function discardPreservedWorktrees(
@@ -41,6 +41,8 @@ export interface RunnerSubagentStep {
41
41
  sessionFile?: string;
42
42
  maxSubagentDepth?: number;
43
43
  timeoutMs?: number;
44
+ /** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
45
+ toolTimeoutMs?: number;
44
46
  waitToolEnabled?: boolean;
45
47
  structuredOutput?: {
46
48
  schema: import("../../shared/types.ts").JsonSchemaObject;
@@ -63,13 +65,8 @@ export interface RunnerSubagentStep {
63
65
  capabilityAudit?: import("./capability-ceiling.ts").SubagentCapabilityAudit;
64
66
  /** Private stable logical-child path for inherited run fan-out accounting. */
65
67
  runFanoutPath?: string;
66
- }
67
-
68
- export interface RunnerCheckpointStep {
69
- checkpoint: string;
70
- message?: string;
71
- phase?: string;
72
- label?: string;
68
+ /** Run this single child in one managed worktree. */
69
+ worktree?: boolean;
73
70
  }
74
71
 
75
72
  export interface ParallelStepGroup {
@@ -98,11 +95,7 @@ export interface DynamicRunnerGroup {
98
95
  capabilityAudit?: import("./capability-ceiling.ts").SubagentCapabilityAudit;
99
96
  }
100
97
 
101
- export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup | RunnerCheckpointStep;
102
-
103
- export function isCheckpointRunnerStep(step: RunnerStep): step is RunnerCheckpointStep {
104
- return "checkpoint" in step;
105
- }
98
+ export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup;
106
99
 
107
100
  export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
108
101
  return "parallel" in step && Array.isArray(step.parallel);
@@ -115,9 +108,7 @@ export function isDynamicRunnerGroup(step: RunnerStep): step is DynamicRunnerGro
115
108
  export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
116
109
  const flat: RunnerSubagentStep[] = [];
117
110
  for (const step of steps) {
118
- if (isCheckpointRunnerStep(step)) {
119
- continue;
120
- } else if (isParallelGroup(step)) {
111
+ if (isParallelGroup(step)) {
121
112
  for (const task of step.parallel) flat.push(task);
122
113
  } else if (isDynamicRunnerGroup(step)) {
123
114
  continue;
@@ -14,6 +14,10 @@ const MAX_PREVIEW_BYTES = 2048;
14
14
  const SECRET_KEY = /(?:authorization|cookie|credential|password|secret|token|api[-_]?key)/i;
15
15
  const SECRET_VALUE = /\b(?:Bearer\s+\S+|(?:sk|ghp|github_pat|xox[baprs])[-_A-Za-z0-9]{8,})\b/gi;
16
16
 
17
+ export function redactSecretValues(value: string): string {
18
+ return value.replace(SECRET_VALUE, "[redacted]");
19
+ }
20
+
17
21
  export function validatePermissionRules(value: unknown, label: string): PermissionRules | undefined {
18
22
  if (value === undefined) return undefined;
19
23
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object mapping tool names to allow, ask, or deny.`);
@@ -66,7 +70,7 @@ function redact(value: unknown, key = "", depth = 0): unknown {
66
70
  if (Array.isArray(value)) return value.slice(0, 10).map((item) => redact(item, "", depth + 1));
67
71
  if (value && typeof value === "object") return Object.fromEntries(Object.entries(value as Record<string, unknown>).slice(0, 20).map(([entryKey, entryValue]) => [entryKey, redact(entryValue, entryKey, depth + 1)]));
68
72
  if (typeof value === "string") {
69
- const redacted = value.replace(SECRET_VALUE, "[redacted]");
73
+ const redacted = redactSecretValues(value);
70
74
  return redacted.length > 500 ? `${redacted.slice(0, 500)}…` : redacted;
71
75
  }
72
76
  return value;
@@ -433,13 +433,20 @@ export function resolvePiLaunchToolPlan(
433
433
  ...internalTools,
434
434
  ]),
435
435
  ];
436
+ // Supervisor-coordination names stay in the --tools allowlist but are never
437
+ // strict requirements: children register contact_supervisor at runtime through
438
+ // the native supervisor channel (or pi-intercom). The pre-0.50 bridge always
439
+ // appended intercom alongside contact_supervisor, so that exact pairing is
440
+ // legacy plumbing, not a user demand for an external intercom provider;
441
+ // a lone intercom entry stays strictly required (#1207).
442
+ const legacySupervisorPairing = declaredBuiltinTools.includes("contact_supervisor");
436
443
  const requiredChildTools = explicitToolAllowlist
437
444
  ? [
438
445
  ...new Set([
439
446
  ...(input.tools !== undefined ? declaredBuiltinTools : []),
440
447
  ...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
441
448
  ...internalTools,
442
- ]),
449
+ ].filter((tool) => tool !== "contact_supervisor" && (!legacySupervisorPairing || tool !== "intercom"))),
443
450
  ]
444
451
  : [];
445
452
  const permSystemExt = capabilityCeiling?.denyExtensions