@leing2021/super-pi 0.22.1 → 0.23.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 (117) hide show
  1. package/README.md +66 -293
  2. package/extensions/ce-core/index.ts +276 -48
  3. package/extensions/ce-core/tools/parallel-subagent.ts +3 -3
  4. package/extensions/ce-core/tools/subagent.ts +2 -2
  5. package/package.json +4 -20
  6. package/rules/common/naming.md +154 -0
  7. package/skills/01-brainstorm/SKILL.md +42 -79
  8. package/skills/01-brainstorm/references/ce-brainstorm-mode.md +47 -0
  9. package/skills/02-plan/SKILL.md +43 -61
  10. package/skills/02-plan/references/solution-search.md +46 -0
  11. package/skills/03-work/SKILL.md +34 -52
  12. package/skills/03-work/references/completion-report.md +51 -0
  13. package/skills/04-review/SKILL.md +44 -51
  14. package/skills/04-review/references/solution-search.md +46 -0
  15. package/skills/06-next/SKILL.md +35 -20
  16. package/skills/06-next/references/recommendation-logic.md +47 -2
  17. package/skills/07-worktree/SKILL.md +21 -20
  18. package/skills/08-help/SKILL.md +46 -0
  19. package/skills/08-help/references/workflow-sequence.md +135 -0
  20. package/skills/references/language-detection.md +44 -0
  21. package/LICENSE +0 -21
  22. package/THIRD-PARTY-NOTICES.md +0 -35
  23. package/extensions/subagent/__tests__/async-job-tracker.test.ts +0 -21
  24. package/extensions/subagent/__tests__/execution-activity.test.ts +0 -15
  25. package/extensions/subagent/__tests__/parallel-render-stress.test.ts +0 -82
  26. package/extensions/subagent/__tests__/render-dedup.test.ts +0 -98
  27. package/extensions/subagent/__tests__/render-widget.test.ts +0 -28
  28. package/extensions/subagent/__tests__/throttle.test.ts +0 -90
  29. package/extensions/subagent/agent-management.ts +0 -596
  30. package/extensions/subagent/agent-manager-chain-detail.ts +0 -163
  31. package/extensions/subagent/agent-manager-detail.ts +0 -232
  32. package/extensions/subagent/agent-manager-edit.ts +0 -391
  33. package/extensions/subagent/agent-manager-list.ts +0 -279
  34. package/extensions/subagent/agent-manager-parallel.ts +0 -305
  35. package/extensions/subagent/agent-manager.ts +0 -706
  36. package/extensions/subagent/agent-scope.ts +0 -9
  37. package/extensions/subagent/agent-selection.ts +0 -26
  38. package/extensions/subagent/agent-serializer.ts +0 -124
  39. package/extensions/subagent/agent-templates.ts +0 -63
  40. package/extensions/subagent/agents/context-builder.md +0 -37
  41. package/extensions/subagent/agents/delegate.md +0 -9
  42. package/extensions/subagent/agents/oracle.md +0 -73
  43. package/extensions/subagent/agents/planner.md +0 -52
  44. package/extensions/subagent/agents/researcher.md +0 -50
  45. package/extensions/subagent/agents/reviewer.md +0 -38
  46. package/extensions/subagent/agents/scout.md +0 -48
  47. package/extensions/subagent/agents/worker.md +0 -52
  48. package/extensions/subagent/agents.ts +0 -762
  49. package/extensions/subagent/artifacts.ts +0 -101
  50. package/extensions/subagent/async-execution.ts +0 -521
  51. package/extensions/subagent/async-job-tracker.ts +0 -240
  52. package/extensions/subagent/async-status.ts +0 -242
  53. package/extensions/subagent/chain-clarify.ts +0 -1365
  54. package/extensions/subagent/chain-execution.ts +0 -854
  55. package/extensions/subagent/chain-serializer.ts +0 -127
  56. package/extensions/subagent/completion-dedupe.ts +0 -66
  57. package/extensions/subagent/doctor.ts +0 -201
  58. package/extensions/subagent/execution.ts +0 -748
  59. package/extensions/subagent/file-coalescer.ts +0 -43
  60. package/extensions/subagent/fork-context.ts +0 -64
  61. package/extensions/subagent/formatters.ts +0 -123
  62. package/extensions/subagent/frontmatter.ts +0 -32
  63. package/extensions/subagent/index.ts +0 -586
  64. package/extensions/subagent/intercom-bridge.ts +0 -241
  65. package/extensions/subagent/jsonl-writer.ts +0 -84
  66. package/extensions/subagent/model-fallback.ts +0 -109
  67. package/extensions/subagent/notify.ts +0 -111
  68. package/extensions/subagent/parallel-utils.ts +0 -109
  69. package/extensions/subagent/pi-args.ts +0 -139
  70. package/extensions/subagent/pi-spawn.ts +0 -101
  71. package/extensions/subagent/post-exit-stdio-guard.ts +0 -88
  72. package/extensions/subagent/prompt-template-bridge.ts +0 -400
  73. package/extensions/subagent/prompts/gather-context-and-clarify.md +0 -13
  74. package/extensions/subagent/prompts/parallel-cleanup.md +0 -42
  75. package/extensions/subagent/prompts/parallel-research.md +0 -50
  76. package/extensions/subagent/prompts/parallel-review.md +0 -40
  77. package/extensions/subagent/render-helpers.ts +0 -83
  78. package/extensions/subagent/render.ts +0 -908
  79. package/extensions/subagent/result-intercom.ts +0 -238
  80. package/extensions/subagent/result-watcher.ts +0 -172
  81. package/extensions/subagent/run-history.ts +0 -58
  82. package/extensions/subagent/run-status.ts +0 -137
  83. package/extensions/subagent/schemas.ts +0 -165
  84. package/extensions/subagent/session-tokens.ts +0 -51
  85. package/extensions/subagent/settings.ts +0 -368
  86. package/extensions/subagent/single-output.ts +0 -98
  87. package/extensions/subagent/skills.ts +0 -627
  88. package/extensions/subagent/slash-bridge.ts +0 -177
  89. package/extensions/subagent/slash-commands.ts +0 -304
  90. package/extensions/subagent/slash-live-state.ts +0 -295
  91. package/extensions/subagent/subagent-control.ts +0 -151
  92. package/extensions/subagent/subagent-executor.ts +0 -1915
  93. package/extensions/subagent/subagent-prompt-runtime.ts +0 -76
  94. package/extensions/subagent/subagent-runner.ts +0 -1471
  95. package/extensions/subagent/subagents-status.ts +0 -473
  96. package/extensions/subagent/text-editor.ts +0 -273
  97. package/extensions/subagent/throttle.ts +0 -77
  98. package/extensions/subagent/top-level-async.ts +0 -16
  99. package/extensions/subagent/types.ts +0 -624
  100. package/extensions/subagent/utils.ts +0 -457
  101. package/extensions/subagent/worktree.ts +0 -580
  102. package/extensions/super-pi-extension/agents/ce-oracle.md +0 -109
  103. package/extensions/super-pi-extension/agents/ce-planner.md +0 -116
  104. package/extensions/super-pi-extension/agents/ce-reviewer.md +0 -99
  105. package/extensions/super-pi-extension/agents/ce-scout.md +0 -80
  106. package/extensions/super-pi-extension/agents/ce-worker.md +0 -78
  107. package/extensions/super-pi-extension/chains/ce-parallel-review.chain.md +0 -34
  108. package/extensions/super-pi-extension/chains/ce-review-only.chain.md +0 -12
  109. package/extensions/super-pi-extension/chains/ce-standard.chain.md +0 -28
  110. package/extensions/super-pi-extension/index.ts +0 -129
  111. package/extensions/super-pi-extension/model-sync.ts +0 -204
  112. package/skills/08-status/SKILL.md +0 -41
  113. package/skills/08-status/references/artifact-locations.md +0 -10
  114. package/skills/09-help/SKILL.md +0 -37
  115. package/skills/09-help/references/workflow-sequence.md +0 -9
  116. package/skills/10-rules/SKILL.md +0 -76
  117. package/skills/pi-subagents/SKILL.md +0 -566
@@ -1,1471 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Nico Bailon
2
- // SPDX-License-Identifier: MIT
3
- // Source: https://github.com/nicobailon/pi-subagents
4
- import { spawn, spawnSync } from "node:child_process";
5
- import * as fs from "node:fs";
6
- import { createRequire } from "node:module";
7
- import * as path from "node:path";
8
- import { pathToFileURL } from "node:url";
9
- import type { Message } from "@mariozechner/pi-ai";
10
- import { appendJsonl, getArtifactPaths } from "./artifacts.ts";
11
- import { getPiSpawnCommand } from "./pi-spawn.ts";
12
- import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
13
- import {
14
- type ActivityState,
15
- type ArtifactConfig,
16
- type ArtifactPaths,
17
- type ModelAttempt,
18
- type ResolvedControlConfig,
19
- type Usage,
20
- DEFAULT_MAX_OUTPUT,
21
- type MaxOutputConfig,
22
- truncateOutput,
23
- getSubagentDepthEnv,
24
- } from "./types.ts";
25
- import {
26
- DEFAULT_CONTROL_CONFIG,
27
- buildControlEvent,
28
- deriveActivityState,
29
- claimControlNotification,
30
- formatControlIntercomMessage,
31
- formatControlNoticeMessage,
32
- shouldEmitControlEvent,
33
- } from "./subagent-control.ts";
34
- import {
35
- type RunnerSubagentStep as SubagentStep,
36
- type RunnerStep,
37
- isParallelGroup,
38
- flattenSteps,
39
- mapConcurrent,
40
- aggregateParallelOutputs,
41
- MAX_PARALLEL_CONCURRENCY,
42
- } from "./parallel-utils.ts";
43
- import { buildPiArgs, cleanupTempDir } from "./pi-args.ts";
44
- import { formatModelAttemptNote, isRetryableModelFailure } from "./model-fallback.ts";
45
- import { attachPostExitStdioGuard, trySignalChild } from "./post-exit-stdio-guard.ts";
46
- import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "./utils.ts";
47
- import { parseSessionTokens, type TokenUsage } from "./session-tokens.ts";
48
- import {
49
- cleanupWorktrees,
50
- createWorktrees,
51
- diffWorktrees,
52
- findWorktreeTaskCwdConflict,
53
- formatWorktreeDiffSummary,
54
- formatWorktreeTaskCwdConflict,
55
- type WorktreeSetup,
56
- } from "./worktree.ts";
57
- import { writeInitialProgressFile } from "./settings.ts";
58
-
59
- interface SubagentRunConfig {
60
- id: string;
61
- steps: RunnerStep[];
62
- resultPath: string;
63
- cwd: string;
64
- placeholder: string;
65
- taskIndex?: number;
66
- totalTasks?: number;
67
- maxOutput?: MaxOutputConfig;
68
- artifactsDir?: string;
69
- artifactConfig?: Partial<ArtifactConfig>;
70
- share?: boolean;
71
- sessionDir?: string;
72
- asyncDir: string;
73
- sessionId?: string | null;
74
- piPackageRoot?: string;
75
- piArgv1?: string;
76
- worktreeSetupHook?: string;
77
- worktreeSetupHookTimeoutMs?: number;
78
- controlConfig?: ResolvedControlConfig;
79
- controlIntercomTarget?: string;
80
- childIntercomTargets?: Array<string | undefined>;
81
- resultMode?: "single" | "parallel" | "chain";
82
- }
83
-
84
- interface StepResult {
85
- agent: string;
86
- output: string;
87
- success: boolean;
88
- skipped?: boolean;
89
- intercomTarget?: string;
90
- model?: string;
91
- attemptedModels?: string[];
92
- modelAttempts?: ModelAttempt[];
93
- artifactPaths?: ArtifactPaths;
94
- truncated?: boolean;
95
- }
96
-
97
- const require = createRequire(import.meta.url);
98
- const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
99
-
100
- function findLatestSessionFile(sessionDir: string): string | null {
101
- try {
102
- const files = fs
103
- .readdirSync(sessionDir)
104
- .filter((f) => f.endsWith(".jsonl"))
105
- .map((f) => path.join(sessionDir, f));
106
- if (files.length === 0) return null;
107
- files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
108
- return files[0] ?? null;
109
- } catch {
110
- // Session lookup is optional metadata.
111
- return null;
112
- }
113
- }
114
-
115
- function emptyUsage(): Usage {
116
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
117
- }
118
-
119
- function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsage | null {
120
- if (!attempts || attempts.length === 0) return null;
121
- let input = 0;
122
- let output = 0;
123
- for (const attempt of attempts) {
124
- input += attempt.usage?.input ?? 0;
125
- output += attempt.usage?.output ?? 0;
126
- }
127
- const total = input + output;
128
- return total > 0 ? { input, output, total } : null;
129
- }
130
-
131
- interface ChildEventContext {
132
- eventsPath: string;
133
- runId: string;
134
- stepIndex: number;
135
- agent: string;
136
- }
137
-
138
- interface ChildUsage {
139
- input?: number;
140
- inputTokens?: number;
141
- output?: number;
142
- outputTokens?: number;
143
- cacheRead?: number;
144
- cacheWrite?: number;
145
- cost?: { total?: number };
146
- }
147
-
148
- type ChildMessage = Message & {
149
- model?: string;
150
- errorMessage?: string;
151
- usage?: ChildUsage;
152
- };
153
-
154
- interface ChildEvent {
155
- type?: string;
156
- message?: ChildMessage;
157
- toolName?: string;
158
- args?: Record<string, unknown>;
159
- }
160
-
161
- interface RunPiStreamingResult {
162
- stderr: string;
163
- exitCode: number | null;
164
- messages: Message[];
165
- usage: Usage;
166
- model?: string;
167
- error?: string;
168
- finalOutput: string;
169
- interrupted?: boolean;
170
- }
171
-
172
- function runPiStreaming(
173
- args: string[],
174
- cwd: string,
175
- outputFile: string,
176
- env?: Record<string, string | undefined>,
177
- piPackageRoot?: string,
178
- piArgv1?: string,
179
- maxSubagentDepth?: number,
180
- childEventContext?: ChildEventContext,
181
- registerInterrupt?: (interrupt: (() => void) | undefined) => void,
182
- ): Promise<RunPiStreamingResult> {
183
- return new Promise((resolve) => {
184
- const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
185
- const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
186
- const spawnSpec = getPiSpawnCommand(args, {
187
- ...(piPackageRoot ? { piPackageRoot } : {}),
188
- ...(piArgv1 ? { argv1: piArgv1 } : {}),
189
- });
190
- const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
191
- let stderr = "";
192
- let stdoutBuf = "";
193
- let stderrBuf = "";
194
- const messages: Message[] = [];
195
- const usage = emptyUsage();
196
- let model: string | undefined;
197
- let error: string | undefined;
198
- let interrupted = false;
199
- const rawStdoutLines: string[] = [];
200
-
201
- const writeOutputLine = (line: string) => {
202
- if (!line.trim()) return;
203
- outputStream.write(`${line}\n`);
204
- };
205
-
206
- const writeOutputText = (text: string) => {
207
- for (const line of text.split("\n")) {
208
- writeOutputLine(line);
209
- }
210
- };
211
-
212
- const appendChildEvent = (event: Record<string, unknown>) => {
213
- if (!childEventContext) return;
214
- appendJsonl(childEventContext.eventsPath, JSON.stringify({
215
- ...event,
216
- subagentSource: "child",
217
- subagentRunId: childEventContext.runId,
218
- subagentStepIndex: childEventContext.stepIndex,
219
- subagentAgent: childEventContext.agent,
220
- observedAt: Date.now(),
221
- }));
222
- };
223
-
224
- const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
225
- appendChildEvent({ type, line });
226
- };
227
-
228
- const processStdoutLine = (line: string) => {
229
- if (!line.trim()) return;
230
- let event: ChildEvent;
231
- try {
232
- event = JSON.parse(line) as ChildEvent;
233
- } catch {
234
- rawStdoutLines.push(line);
235
- writeOutputLine(line);
236
- appendChildLine("subagent.child.stdout", line);
237
- return;
238
- }
239
-
240
- appendChildEvent(event);
241
-
242
- if (event.type === "tool_execution_start" && event.toolName) {
243
- const toolArgs = extractToolArgsPreview(event.args ?? {});
244
- writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
245
- return;
246
- }
247
-
248
- if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
249
- messages.push(event.message);
250
- const text = extractTextFromContent(event.message.content);
251
- if (text) writeOutputText(text);
252
-
253
- if (event.type !== "message_end" || event.message.role !== "assistant") return;
254
- if (event.message.model) model = event.message.model;
255
- if (event.message.errorMessage) error = event.message.errorMessage;
256
- const eventUsage = event.message.usage;
257
- if (eventUsage) {
258
- usage.turns++;
259
- usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
260
- usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
261
- usage.cacheRead += eventUsage.cacheRead ?? 0;
262
- usage.cacheWrite += eventUsage.cacheWrite ?? 0;
263
- usage.cost += eventUsage.cost?.total ?? 0;
264
- }
265
- const stopReason = (event.message as { stopReason?: string }).stopReason;
266
- const hasToolCall = Array.isArray(event.message.content)
267
- && event.message.content.some((part) => (part as { type?: string }).type === "toolCall");
268
- if (stopReason === "stop" && !hasToolCall) startFinalDrain();
269
- }
270
- };
271
-
272
- const processStderrText = (text: string) => {
273
- stderr += text;
274
- stderrBuf += text;
275
- outputStream.write(text);
276
- if (!childEventContext) return;
277
- const lines = stderrBuf.split("\n");
278
- stderrBuf = lines.pop() || "";
279
- for (const line of lines) {
280
- if (!line.trim()) continue;
281
- appendChildLine("subagent.child.stderr", line);
282
- }
283
- };
284
-
285
- // Guard both cases that can leave the parent waiting on `close` forever:
286
- // a lingering stdio holder after `exit`, or a child that never exits.
287
- const FINAL_DRAIN_MS = 5000;
288
- const HARD_KILL_MS = 3000;
289
- let childExited = false;
290
- let forcedTerminationSignal = false;
291
- let finalDrainTimer: NodeJS.Timeout | undefined;
292
- let finalHardKillTimer: NodeJS.Timeout | undefined;
293
- let settled = false;
294
- const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
295
- child.stdout.on("data", (chunk: Buffer) => {
296
- const text = chunk.toString();
297
- stdoutBuf += text;
298
- const lines = stdoutBuf.split("\n");
299
- stdoutBuf = lines.pop() || "";
300
- for (const line of lines) processStdoutLine(line);
301
- });
302
-
303
- child.stderr.on("data", (chunk: Buffer) => {
304
- processStderrText(chunk.toString());
305
- });
306
- registerInterrupt?.(() => {
307
- if (settled) return;
308
- interrupted = true;
309
- if (!error) error = "Interrupted. Waiting for explicit next action.";
310
- trySignalChild(child, "SIGINT");
311
- setTimeout(() => {
312
- if (!settled) trySignalChild(child, "SIGTERM");
313
- }, 1000).unref?.();
314
- });
315
- const clearDrainTimers = () => {
316
- if (finalDrainTimer) {
317
- clearTimeout(finalDrainTimer);
318
- finalDrainTimer = undefined;
319
- }
320
- if (finalHardKillTimer) {
321
- clearTimeout(finalHardKillTimer);
322
- finalHardKillTimer = undefined;
323
- }
324
- };
325
- function startFinalDrain(): void {
326
- if (childExited || finalDrainTimer || settled) return;
327
- finalDrainTimer = setTimeout(() => {
328
- if (settled) return;
329
- const termSent = trySignalChild(child, "SIGTERM");
330
- if (!termSent) return;
331
- forcedTerminationSignal = true;
332
- if (!error) {
333
- error = `Subagent process did not exit within ${FINAL_DRAIN_MS}ms after its final message. Forcing termination.`;
334
- }
335
- finalHardKillTimer = setTimeout(() => {
336
- if (settled) return;
337
- forcedTerminationSignal = trySignalChild(child, "SIGKILL") || forcedTerminationSignal;
338
- }, HARD_KILL_MS);
339
- finalHardKillTimer.unref?.();
340
- }, FINAL_DRAIN_MS);
341
- finalDrainTimer.unref?.();
342
- }
343
- child.on("exit", () => {
344
- childExited = true;
345
- clearDrainTimers();
346
- });
347
- child.on("close", (exitCode, signal) => {
348
- settled = true;
349
- registerInterrupt?.(undefined);
350
- clearDrainTimers();
351
- clearStdioGuard();
352
- if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
353
- if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
354
- outputStream.end();
355
- const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
356
- resolve({
357
- stderr,
358
- exitCode: interrupted ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
359
- messages,
360
- usage,
361
- model,
362
- error: interrupted ? undefined : error,
363
- finalOutput,
364
- interrupted,
365
- });
366
- });
367
-
368
- child.on("error", (spawnError) => {
369
- settled = true;
370
- registerInterrupt?.(undefined);
371
- clearDrainTimers();
372
- clearStdioGuard();
373
- outputStream.end();
374
- const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
375
- const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
376
- resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? spawnErrorMessage, finalOutput });
377
- });
378
- });
379
- }
380
-
381
- function resolvePiPackageRootFallback(): string {
382
- // Try to resolve the main entry point and walk up to find the package root
383
- const entryPoint = require.resolve("@mariozechner/pi-coding-agent");
384
- // Entry point is typically /path/to/dist/index.js, so go up to find package root
385
- let dir = path.dirname(entryPoint);
386
- while (dir !== path.dirname(dir)) {
387
- const pkgJsonPath = path.join(dir, "package.json");
388
- try {
389
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
390
- if (pkg.name === "@mariozechner/pi-coding-agent") return dir;
391
- } catch {
392
- // Keep walking up until a readable package.json is found.
393
- }
394
- dir = path.dirname(dir);
395
- }
396
- throw new Error("Could not resolve @mariozechner/pi-coding-agent package root");
397
- }
398
-
399
- async function exportSessionHtml(sessionFile: string, outputDir: string, piPackageRoot?: string): Promise<string> {
400
- const pkgRoot = piPackageRoot ?? resolvePiPackageRootFallback();
401
- const exportModulePath = path.join(pkgRoot, "dist", "core", "export-html", "index.js");
402
- const moduleUrl = pathToFileURL(exportModulePath).href;
403
- const mod = await import(moduleUrl);
404
- const exportFromFile = (mod as { exportFromFile?: (inputPath: string, options?: { outputPath?: string }) => string })
405
- .exportFromFile;
406
- if (typeof exportFromFile !== "function") {
407
- throw new Error("exportFromFile not available");
408
- }
409
- const outputPath = path.join(outputDir, `${path.basename(sessionFile, ".jsonl")}.html`);
410
- return exportFromFile(sessionFile, { outputPath });
411
- }
412
-
413
- function createShareLink(htmlPath: string): { shareUrl: string; gistUrl: string } | { error: string } {
414
- try {
415
- const auth = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
416
- if (auth.status !== 0) {
417
- return { error: "GitHub CLI is not logged in. Run 'gh auth login' first." };
418
- }
419
- } catch {
420
- return { error: "GitHub CLI (gh) is not installed." };
421
- }
422
-
423
- try {
424
- const result = spawnSync("gh", ["gist", "create", htmlPath], { encoding: "utf-8" });
425
- if (result.status !== 0) {
426
- const err = (result.stderr || "").trim() || "Failed to create gist.";
427
- return { error: err };
428
- }
429
- const gistUrl = (result.stdout || "").trim();
430
- const gistId = gistUrl.split("/").pop();
431
- if (!gistId) return { error: "Failed to parse gist ID." };
432
- const shareUrl = `https://shittycodingagent.ai/session/?${gistId}`;
433
- return { shareUrl, gistUrl };
434
- } catch (err) {
435
- return { error: String(err) };
436
- }
437
- }
438
-
439
- function writeJson(filePath: string, payload: object): void {
440
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
441
- const tempPath = path.join(
442
- path.dirname(filePath),
443
- `.${path.basename(filePath)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`,
444
- );
445
- try {
446
- fs.writeFileSync(tempPath, JSON.stringify(payload, null, 2), "utf-8");
447
- fs.renameSync(tempPath, filePath);
448
- } finally {
449
- if (fs.existsSync(tempPath)) {
450
- try {
451
- fs.unlinkSync(tempPath);
452
- } catch {}
453
- }
454
- }
455
- }
456
-
457
- function formatDuration(ms: number): string {
458
- if (ms < 1000) return `${ms}ms`;
459
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
460
- const minutes = Math.floor(ms / 60000);
461
- const seconds = Math.floor((ms % 60000) / 1000);
462
- return `${minutes}m${seconds}s`;
463
- }
464
-
465
- function writeRunLog(
466
- logPath: string,
467
- input: {
468
- id: string;
469
- mode: "single" | "chain";
470
- cwd: string;
471
- startedAt: number;
472
- endedAt: number;
473
- steps: Array<{
474
- agent: string;
475
- status: string;
476
- durationMs?: number;
477
- }>;
478
- summary: string;
479
- truncated: boolean;
480
- artifactsDir?: string;
481
- sessionFile?: string;
482
- shareUrl?: string;
483
- shareError?: string;
484
- },
485
- ): void {
486
- const lines: string[] = [];
487
- lines.push(`# Subagent run ${input.id}`);
488
- lines.push("");
489
- lines.push(`- **Mode:** ${input.mode}`);
490
- lines.push(`- **CWD:** ${input.cwd}`);
491
- lines.push(`- **Started:** ${new Date(input.startedAt).toISOString()}`);
492
- lines.push(`- **Ended:** ${new Date(input.endedAt).toISOString()}`);
493
- lines.push(`- **Duration:** ${formatDuration(input.endedAt - input.startedAt)}`);
494
- if (input.sessionFile) lines.push(`- **Session:** ${input.sessionFile}`);
495
- if (input.shareUrl) lines.push(`- **Share:** ${input.shareUrl}`);
496
- if (input.shareError) lines.push(`- **Share error:** ${input.shareError}`);
497
- if (input.artifactsDir) lines.push(`- **Artifacts:** ${input.artifactsDir}`);
498
- lines.push("");
499
- lines.push("## Steps");
500
- lines.push("| Step | Agent | Status | Duration |");
501
- lines.push("| --- | --- | --- | --- |");
502
- input.steps.forEach((step, i) => {
503
- const duration = step.durationMs !== undefined ? formatDuration(step.durationMs) : "-";
504
- lines.push(`| ${i + 1} | ${step.agent} | ${step.status} | ${duration} |`);
505
- });
506
- lines.push("");
507
- lines.push("## Summary");
508
- if (input.truncated) {
509
- lines.push("_Output truncated_");
510
- lines.push("");
511
- }
512
- lines.push(input.summary.trim() || "(no output)");
513
- lines.push("");
514
- fs.writeFileSync(logPath, lines.join("\n"), "utf-8");
515
- }
516
-
517
- /** Context for running a single step */
518
- interface SingleStepContext {
519
- previousOutput: string;
520
- placeholder: string;
521
- cwd: string;
522
- sessionEnabled: boolean;
523
- sessionDir?: string;
524
- artifactsDir?: string;
525
- artifactConfig?: Partial<ArtifactConfig>;
526
- id: string;
527
- flatIndex: number;
528
- flatStepCount: number;
529
- outputFile: string;
530
- piPackageRoot?: string;
531
- piArgv1?: string;
532
- registerInterrupt?: (interrupt: (() => void) | undefined) => void;
533
- childIntercomTarget?: string;
534
- }
535
-
536
- /** Run a single pi agent step, returning output and metadata */
537
- async function runSingleStep(
538
- step: SubagentStep,
539
- ctx: SingleStepContext,
540
- ): Promise<{
541
- agent: string;
542
- output: string;
543
- exitCode: number | null;
544
- error?: string;
545
- model?: string;
546
- attemptedModels?: string[];
547
- modelAttempts?: ModelAttempt[];
548
- artifactPaths?: ArtifactPaths;
549
- interrupted?: boolean;
550
- intercomTarget?: string;
551
- }> {
552
- const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
553
- const task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
554
- const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
555
- const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
556
- const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
557
-
558
- let artifactPaths: ArtifactPaths | undefined;
559
- if (ctx.artifactsDir && ctx.artifactConfig?.enabled !== false) {
560
- const index = ctx.flatStepCount > 1 ? ctx.flatIndex : undefined;
561
- artifactPaths = getArtifactPaths(ctx.artifactsDir, ctx.id, step.agent, index);
562
- fs.mkdirSync(ctx.artifactsDir, { recursive: true });
563
- if (ctx.artifactConfig?.includeInput !== false) {
564
- fs.writeFileSync(artifactPaths.inputPath, `# Task for ${step.agent}\n\n${task}`, "utf-8");
565
- }
566
- }
567
-
568
- const candidates = step.modelCandidates && step.modelCandidates.length > 0
569
- ? step.modelCandidates
570
- : step.model
571
- ? [step.model]
572
- : [undefined];
573
- const attemptedModels: string[] = [];
574
- const modelAttempts: ModelAttempt[] = [];
575
- const attemptNotes: string[] = [];
576
- const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
577
- let finalResult: RunPiStreamingResult | undefined;
578
-
579
- for (let index = 0; index < candidates.length; index++) {
580
- const candidate = candidates[index];
581
- const { args, env, tempDir } = buildPiArgs({
582
- baseArgs: ["--mode", "json", "-p"],
583
- task,
584
- sessionEnabled,
585
- sessionDir,
586
- sessionFile: step.sessionFile,
587
- model: candidate,
588
- inheritProjectContext: step.inheritProjectContext,
589
- inheritSkills: step.inheritSkills,
590
- tools: step.tools,
591
- extensions: step.extensions,
592
- systemPrompt: step.systemPrompt,
593
- systemPromptMode: step.systemPromptMode,
594
- mcpDirectTools: step.mcpDirectTools,
595
- promptFileStem: step.agent,
596
- intercomSessionName: ctx.childIntercomTarget,
597
- });
598
- const run = await runPiStreaming(
599
- args,
600
- step.cwd ?? ctx.cwd,
601
- ctx.outputFile,
602
- env,
603
- ctx.piPackageRoot,
604
- ctx.piArgv1,
605
- step.maxSubagentDepth,
606
- { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
607
- ctx.registerInterrupt,
608
- );
609
- cleanupTempDir(tempDir);
610
-
611
- const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
612
- const effectiveExitCode = hiddenError?.hasError ? (hiddenError.exitCode ?? 1) : run.exitCode;
613
- const error = hiddenError?.hasError
614
- ? hiddenError.details
615
- ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
616
- : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
617
- : run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
618
- const attempt: ModelAttempt = {
619
- model: candidate ?? run.model ?? step.model ?? "default",
620
- success: effectiveExitCode === 0 && !error,
621
- exitCode: effectiveExitCode,
622
- error,
623
- usage: run.usage,
624
- };
625
- modelAttempts.push(attempt);
626
- if (candidate) attemptedModels.push(candidate);
627
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
628
- if (attempt.success) break;
629
- if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
630
- attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
631
- }
632
-
633
- const rawOutput = finalResult?.finalOutput ?? "";
634
- const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
635
- ? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
636
- : { fullOutput: rawOutput };
637
- const output = resolvedOutput.fullOutput;
638
- let outputForSummary = output;
639
- if (attemptNotes.length > 0) {
640
- outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
641
- }
642
- if (resolvedOutput.savedPath) {
643
- outputForSummary = outputForSummary
644
- ? `${outputForSummary}\n\nOutput saved to: ${resolvedOutput.savedPath}`
645
- : `Output saved to: ${resolvedOutput.savedPath}`;
646
- } else if (resolvedOutput.saveError && step.outputPath && finalResult?.exitCode === 0) {
647
- outputForSummary = outputForSummary
648
- ? `${outputForSummary}\n\nFailed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`
649
- : `Failed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`;
650
- }
651
-
652
- if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
653
- if (ctx.artifactConfig?.includeOutput !== false) {
654
- fs.writeFileSync(artifactPaths.outputPath, output, "utf-8");
655
- }
656
- if (ctx.artifactConfig?.includeMetadata !== false) {
657
- fs.writeFileSync(
658
- artifactPaths.metadataPath,
659
- JSON.stringify({
660
- runId: ctx.id,
661
- agent: step.agent,
662
- task,
663
- exitCode: finalResult?.exitCode,
664
- model: finalResult?.model,
665
- attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
666
- modelAttempts,
667
- skills: step.skills,
668
- timestamp: Date.now(),
669
- }, null, 2),
670
- "utf-8",
671
- );
672
- }
673
- }
674
-
675
- return {
676
- agent: step.agent,
677
- output: outputForSummary,
678
- exitCode: finalResult?.exitCode ?? 1,
679
- error: finalResult?.error,
680
- intercomTarget: ctx.childIntercomTarget,
681
- model: finalResult?.model,
682
- attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
683
- modelAttempts,
684
- artifactPaths,
685
- interrupted: finalResult?.interrupted,
686
- };
687
- }
688
-
689
- type RunnerStatusPayload = {
690
- runId: string;
691
- mode: "single" | "chain";
692
- state: "queued" | "running" | "complete" | "failed" | "paused";
693
- activityState?: ActivityState;
694
- lastActivityAt?: number;
695
- currentTool?: string;
696
- currentToolStartedAt?: number;
697
- startedAt: number;
698
- endedAt?: number;
699
- lastUpdate: number;
700
- pid: number;
701
- cwd: string;
702
- currentStep: number;
703
- steps: Array<{
704
- agent: string;
705
- status: "pending" | "running" | "complete" | "failed";
706
- activityState?: ActivityState;
707
- lastActivityAt?: number;
708
- currentTool?: string;
709
- currentToolStartedAt?: number;
710
- startedAt?: number;
711
- endedAt?: number;
712
- durationMs?: number;
713
- exitCode?: number | null;
714
- tokens?: TokenUsage;
715
- skills?: string[];
716
- model?: string;
717
- attemptedModels?: string[];
718
- modelAttempts?: ModelAttempt[];
719
- error?: string;
720
- }>;
721
- artifactsDir?: string;
722
- sessionDir?: string;
723
- outputFile?: string;
724
- totalTokens?: TokenUsage;
725
- sessionFile?: string;
726
- shareUrl?: string;
727
- gistUrl?: string;
728
- shareError?: string;
729
- error?: string;
730
- };
731
-
732
- function markParallelGroupSetupFailure(input: {
733
- statusPayload: RunnerStatusPayload;
734
- results: StepResult[];
735
- group: Extract<RunnerStep, { parallel: SubagentStep[] }>;
736
- groupStartFlatIndex: number;
737
- setupError: string;
738
- failedAt: number;
739
- statusPath: string;
740
- eventsPath: string;
741
- asyncDir: string;
742
- runId: string;
743
- stepIndex: number;
744
- }): void {
745
- for (let taskIndex = 0; taskIndex < input.group.parallel.length; taskIndex++) {
746
- const flatTaskIndex = input.groupStartFlatIndex + taskIndex;
747
- input.statusPayload.steps[flatTaskIndex].status = "failed";
748
- input.statusPayload.steps[flatTaskIndex].startedAt = input.failedAt;
749
- input.statusPayload.steps[flatTaskIndex].endedAt = input.failedAt;
750
- input.statusPayload.steps[flatTaskIndex].durationMs = 0;
751
- input.statusPayload.steps[flatTaskIndex].exitCode = 1;
752
- input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false });
753
- }
754
- input.statusPayload.currentStep = input.groupStartFlatIndex;
755
- input.statusPayload.lastUpdate = input.failedAt;
756
- input.statusPayload.outputFile = path.join(input.asyncDir, `output-${input.groupStartFlatIndex}.log`);
757
- writeJson(input.statusPath, input.statusPayload);
758
- appendJsonl(input.eventsPath, JSON.stringify({
759
- type: "subagent.parallel.completed",
760
- ts: input.failedAt,
761
- runId: input.runId,
762
- stepIndex: input.stepIndex,
763
- success: false,
764
- }));
765
- }
766
-
767
- function markParallelGroupRunning(input: {
768
- statusPayload: RunnerStatusPayload;
769
- group: Extract<RunnerStep, { parallel: SubagentStep[] }>;
770
- groupStartFlatIndex: number;
771
- groupStartTime: number;
772
- statusPath: string;
773
- eventsPath: string;
774
- asyncDir: string;
775
- runId: string;
776
- stepIndex: number;
777
- }): void {
778
- for (let taskIndex = 0; taskIndex < input.group.parallel.length; taskIndex++) {
779
- const flatTaskIndex = input.groupStartFlatIndex + taskIndex;
780
- input.statusPayload.steps[flatTaskIndex].status = "running";
781
- input.statusPayload.steps[flatTaskIndex].startedAt = input.groupStartTime;
782
- input.statusPayload.steps[flatTaskIndex].lastActivityAt = input.groupStartTime;
783
- }
784
- input.statusPayload.currentStep = input.groupStartFlatIndex;
785
- input.statusPayload.activityState = undefined;
786
- input.statusPayload.lastActivityAt = input.groupStartTime;
787
- input.statusPayload.lastUpdate = input.groupStartTime;
788
- input.statusPayload.outputFile = path.join(input.asyncDir, `output-${input.groupStartFlatIndex}.log`);
789
- writeJson(input.statusPath, input.statusPayload);
790
- appendJsonl(input.eventsPath, JSON.stringify({
791
- type: "subagent.parallel.started",
792
- ts: input.groupStartTime,
793
- runId: input.runId,
794
- stepIndex: input.stepIndex,
795
- agents: input.group.parallel.map((task) => task.agent),
796
- count: input.group.parallel.length,
797
- }));
798
- }
799
-
800
- function prepareParallelTaskRun(
801
- task: SubagentStep,
802
- cwd: string,
803
- worktreeSetup: WorktreeSetup | undefined,
804
- taskIndex: number,
805
- ): { taskForRun: SubagentStep; taskCwd: string } {
806
- if (!worktreeSetup) return { taskForRun: task, taskCwd: cwd };
807
- return {
808
- taskForRun: { ...task, cwd: undefined },
809
- taskCwd: worktreeSetup.worktrees[taskIndex]!.agentCwd,
810
- };
811
- }
812
-
813
- function appendParallelWorktreeSummary(
814
- previousOutput: string,
815
- worktreeSetup: WorktreeSetup | undefined,
816
- asyncDir: string,
817
- stepIndex: number,
818
- group: Extract<RunnerStep, { parallel: SubagentStep[] }>,
819
- ): string {
820
- if (!worktreeSetup) return previousOutput;
821
- const diffsDir = path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`);
822
- const diffs = diffWorktrees(worktreeSetup, group.parallel.map((task) => task.agent), diffsDir);
823
- const diffSummary = formatWorktreeDiffSummary(diffs);
824
- if (!diffSummary) return previousOutput;
825
- return `${previousOutput}\n\n${diffSummary}`;
826
- }
827
-
828
- function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { parallel: SubagentStep[] }>): void {
829
- const progressPath = path.join(cwd, "progress.md");
830
- if (!group.parallel.some((task) => task.task.includes(`Update progress at: ${progressPath}`))) return;
831
- writeInitialProgressFile(cwd);
832
- }
833
-
834
- async function runSubagent(config: SubagentRunConfig): Promise<void> {
835
- const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
836
- config;
837
- let previousOutput = "";
838
- const results: StepResult[] = [];
839
- const overallStartTime = Date.now();
840
- const shareEnabled = config.share === true;
841
- const asyncDir = config.asyncDir;
842
- const statusPath = path.join(asyncDir, "status.json");
843
- const eventsPath = path.join(asyncDir, "events.jsonl");
844
- const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
845
- const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
846
- let activeChildInterrupt: (() => void) | undefined;
847
- let interrupted = false;
848
- let currentActivityState: ActivityState | undefined;
849
- let activityTimer: NodeJS.Timeout | undefined;
850
- let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
851
- let latestSessionFile: string | undefined;
852
-
853
- const flatSteps = flattenSteps(steps);
854
- const sessionEnabled = Boolean(config.sessionDir)
855
- || shareEnabled
856
- || flatSteps.some((step) => Boolean(step.sessionFile));
857
- const statusPayload: RunnerStatusPayload = {
858
- runId: id,
859
- mode: flatSteps.length > 1 ? "chain" : "single",
860
- state: "running",
861
- lastActivityAt: overallStartTime,
862
- startedAt: overallStartTime,
863
- lastUpdate: overallStartTime,
864
- pid: process.pid,
865
- cwd,
866
- currentStep: 0,
867
- steps: flatSteps.map((step) => ({
868
- agent: step.agent,
869
- status: "pending",
870
- skills: step.skills,
871
- model: step.model,
872
- attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
873
- })),
874
- artifactsDir,
875
- sessionDir: config.sessionDir,
876
- outputFile: path.join(asyncDir, "output-0.log"),
877
- };
878
-
879
- fs.mkdirSync(asyncDir, { recursive: true });
880
- writeJson(statusPath, statusPayload);
881
-
882
- const currentStepAgent = () => statusPayload.steps[statusPayload.currentStep]?.agent ?? flatSteps[statusPayload.currentStep]?.agent ?? "subagent";
883
- const currentOutputActivityAt = (): number => {
884
- const runningIndexes = statusPayload.steps
885
- .map((step, index) => step.status === "running" ? index : -1)
886
- .filter((index) => index >= 0);
887
- let lastActivityAt = statusPayload.steps[statusPayload.currentStep]?.startedAt ?? overallStartTime;
888
- for (const index of runningIndexes.length > 0 ? runningIndexes : [statusPayload.currentStep]) {
889
- try {
890
- lastActivityAt = Math.max(lastActivityAt, fs.statSync(path.join(asyncDir, `output-${index}.log`)).mtimeMs);
891
- } catch {
892
- // Missing output files are normal before a child writes its first line.
893
- }
894
- }
895
- return lastActivityAt;
896
- };
897
- const emittedControlEventKeys = new Set<string>();
898
- const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
899
- const childIntercomTarget = config.childIntercomTargets?.[statusPayload.currentStep];
900
- if (controlConfig.notifyChannels.length === 0 || !claimControlNotification(controlConfig, event, emittedControlEventKeys, childIntercomTarget)) return;
901
- appendJsonl(eventsPath, JSON.stringify({
902
- type: "subagent.control",
903
- event,
904
- channels: controlConfig.notifyChannels,
905
- childIntercomTarget,
906
- noticeText: formatControlNoticeMessage(event, childIntercomTarget),
907
- ...(config.controlIntercomTarget && controlConfig.notifyChannels.includes("intercom") ? {
908
- intercom: {
909
- to: config.controlIntercomTarget,
910
- message: formatControlIntercomMessage(event, childIntercomTarget),
911
- },
912
- } : {}),
913
- }));
914
- };
915
- const updateRunnerActivityState = (now: number): boolean => {
916
- const lastActivityAt = currentOutputActivityAt();
917
- const next = deriveActivityState({
918
- config: controlConfig,
919
- startedAt: overallStartTime,
920
- lastActivityAt,
921
- now,
922
- });
923
- if (next === currentActivityState && statusPayload.lastActivityAt === lastActivityAt) return false;
924
- const previous = currentActivityState;
925
- currentActivityState = next;
926
- statusPayload.activityState = next;
927
- statusPayload.lastActivityAt = lastActivityAt;
928
- for (const step of statusPayload.steps) {
929
- if (step.status === "running") {
930
- step.activityState = next;
931
- step.lastActivityAt = lastActivityAt;
932
- }
933
- }
934
- statusPayload.lastUpdate = now;
935
- if (shouldEmitControlEvent(controlConfig, previous, next)) {
936
- const event = buildControlEvent({
937
- from: previous,
938
- to: next,
939
- runId: id,
940
- agent: currentStepAgent(),
941
- index: statusPayload.currentStep,
942
- ts: now,
943
- lastActivityAt,
944
- });
945
- appendControlEvent(event);
946
- }
947
- writeJson(statusPath, statusPayload);
948
- return true;
949
- };
950
- if (controlConfig.enabled) {
951
- activityTimer = setInterval(() => {
952
- if (statusPayload.state !== "running") return;
953
- const now = Date.now();
954
- updateRunnerActivityState(now);
955
- }, 1000);
956
- activityTimer.unref?.();
957
- }
958
-
959
- const interruptRunner = () => {
960
- if (interrupted || statusPayload.state !== "running") return;
961
- interrupted = true;
962
- const now = Date.now();
963
- statusPayload.state = "paused";
964
- currentActivityState = undefined;
965
- statusPayload.activityState = undefined;
966
- statusPayload.lastUpdate = now;
967
- const current = statusPayload.steps[statusPayload.currentStep];
968
- if (current?.status === "running") {
969
- current.activityState = undefined;
970
- current.endedAt = now;
971
- current.durationMs = current.startedAt ? now - current.startedAt : undefined;
972
- }
973
- writeJson(statusPath, statusPayload);
974
- appendJsonl(eventsPath, JSON.stringify({
975
- type: "subagent.run.paused",
976
- ts: now,
977
- runId: id,
978
- }));
979
- activeChildInterrupt?.();
980
- };
981
- process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
982
- appendJsonl(
983
- eventsPath,
984
- JSON.stringify({
985
- type: "subagent.run.started",
986
- ts: overallStartTime,
987
- runId: id,
988
- mode: statusPayload.mode,
989
- cwd,
990
- pid: process.pid,
991
- }),
992
- );
993
-
994
- let flatIndex = 0;
995
-
996
- for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
997
- if (interrupted) break;
998
- const step = steps[stepIndex];
999
-
1000
- if (isParallelGroup(step)) {
1001
- const group = step;
1002
- const concurrency = group.concurrency ?? MAX_PARALLEL_CONCURRENCY;
1003
- const failFast = group.failFast ?? false;
1004
- const groupStartFlatIndex = flatIndex;
1005
- let aborted = false;
1006
- let worktreeSetup: WorktreeSetup | undefined;
1007
- if (group.worktree) {
1008
- const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(group.parallel, cwd);
1009
- if (worktreeTaskCwdConflict) {
1010
- const failedAt = Date.now();
1011
- markParallelGroupSetupFailure({
1012
- statusPayload,
1013
- results,
1014
- group,
1015
- groupStartFlatIndex,
1016
- setupError: formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, cwd),
1017
- failedAt,
1018
- statusPath,
1019
- eventsPath,
1020
- asyncDir,
1021
- runId: id,
1022
- stepIndex,
1023
- });
1024
- flatIndex += group.parallel.length;
1025
- break;
1026
- }
1027
- try {
1028
- worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, {
1029
- agents: group.parallel.map((task) => task.agent),
1030
- setupHook: config.worktreeSetupHook
1031
- ? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
1032
- : undefined,
1033
- });
1034
- } catch (error) {
1035
- const setupError = error instanceof Error ? error.message : String(error);
1036
- const failedAt = Date.now();
1037
- markParallelGroupSetupFailure({
1038
- statusPayload,
1039
- results,
1040
- group,
1041
- groupStartFlatIndex,
1042
- setupError,
1043
- failedAt,
1044
- statusPath,
1045
- eventsPath,
1046
- asyncDir,
1047
- runId: id,
1048
- stepIndex,
1049
- });
1050
- flatIndex += group.parallel.length;
1051
- break;
1052
- }
1053
- }
1054
-
1055
- try {
1056
- if (group.worktree) ensureParallelProgressFile(cwd, group);
1057
- const groupStartTime = Date.now();
1058
- markParallelGroupRunning({
1059
- statusPayload,
1060
- group,
1061
- groupStartFlatIndex,
1062
- groupStartTime,
1063
- statusPath,
1064
- eventsPath,
1065
- asyncDir,
1066
- runId: id,
1067
- stepIndex,
1068
- });
1069
- const parallelResults = await mapConcurrent(
1070
- group.parallel,
1071
- concurrency,
1072
- async (task, taskIdx) => {
1073
- if (aborted && failFast) {
1074
- return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
1075
- }
1076
-
1077
- const fi = groupStartFlatIndex + taskIdx;
1078
- const taskStartTime = Date.now();
1079
-
1080
- appendJsonl(eventsPath, JSON.stringify({
1081
- type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent,
1082
- }));
1083
-
1084
- const taskSessionDir = config.sessionDir
1085
- ? path.join(config.sessionDir, `parallel-${taskIdx}`)
1086
- : undefined;
1087
- const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx);
1088
-
1089
- const singleResult = await runSingleStep(taskForRun, {
1090
- previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
1091
- sessionDir: taskSessionDir,
1092
- artifactsDir, artifactConfig, id,
1093
- flatIndex: fi, flatStepCount: flatSteps.length,
1094
- outputFile: path.join(asyncDir, `output-${fi}.log`),
1095
- piPackageRoot: config.piPackageRoot,
1096
- piArgv1: config.piArgv1,
1097
- childIntercomTarget: config.childIntercomTargets?.[fi],
1098
- registerInterrupt: (interrupt) => {
1099
- activeChildInterrupt = interrupt;
1100
- },
1101
- });
1102
- if (task.sessionFile) {
1103
- latestSessionFile = task.sessionFile;
1104
- }
1105
-
1106
- const taskEndTime = Date.now();
1107
- const taskDuration = taskEndTime - taskStartTime;
1108
-
1109
- statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
1110
- statusPayload.steps[fi].endedAt = taskEndTime;
1111
- statusPayload.steps[fi].durationMs = taskDuration;
1112
- statusPayload.steps[fi].exitCode = singleResult.exitCode;
1113
- statusPayload.steps[fi].model = singleResult.model;
1114
- statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
1115
- statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
1116
- statusPayload.steps[fi].error = singleResult.error;
1117
- statusPayload.lastUpdate = taskEndTime;
1118
- writeJson(statusPath, statusPayload);
1119
-
1120
- appendJsonl(eventsPath, JSON.stringify({
1121
- type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
1122
- ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
1123
- exitCode: singleResult.exitCode, durationMs: taskDuration,
1124
- }));
1125
-
1126
- if (singleResult.exitCode !== 0 && failFast) aborted = true;
1127
- return { ...singleResult, skipped: false };
1128
- },
1129
- );
1130
-
1131
- flatIndex += group.parallel.length;
1132
-
1133
- for (let t = 0; t < group.parallel.length; t++) {
1134
- const fi = groupStartFlatIndex + t;
1135
- const sessionTokens = config.sessionDir
1136
- ? parseSessionTokens(path.join(config.sessionDir, `parallel-${t}`))
1137
- : null;
1138
- const taskTokens = sessionTokens ?? tokenUsageFromAttempts(parallelResults[t]?.modelAttempts);
1139
- if (!taskTokens) continue;
1140
- statusPayload.steps[fi].tokens = taskTokens;
1141
- previousCumulativeTokens = {
1142
- input: previousCumulativeTokens.input + taskTokens.input,
1143
- output: previousCumulativeTokens.output + taskTokens.output,
1144
- total: previousCumulativeTokens.total + taskTokens.total,
1145
- };
1146
- }
1147
- statusPayload.totalTokens = { ...previousCumulativeTokens };
1148
- statusPayload.lastUpdate = Date.now();
1149
- writeJson(statusPath, statusPayload);
1150
-
1151
- for (const pr of parallelResults) {
1152
- results.push({
1153
- agent: pr.agent,
1154
- output: pr.output,
1155
- success: pr.exitCode === 0,
1156
- skipped: pr.skipped,
1157
- intercomTarget: pr.intercomTarget,
1158
- model: pr.model,
1159
- attemptedModels: pr.attemptedModels,
1160
- modelAttempts: pr.modelAttempts,
1161
- artifactPaths: pr.artifactPaths,
1162
- });
1163
- }
1164
-
1165
- previousOutput = aggregateParallelOutputs(
1166
- parallelResults.map((r) => ({
1167
- agent: r.agent,
1168
- output: r.output,
1169
- exitCode: r.exitCode,
1170
- error: r.error,
1171
- model: r.model,
1172
- attemptedModels: r.attemptedModels,
1173
- })),
1174
- );
1175
- previousOutput = appendParallelWorktreeSummary(previousOutput, worktreeSetup, asyncDir, stepIndex, group);
1176
-
1177
- appendJsonl(eventsPath, JSON.stringify({
1178
- type: "subagent.parallel.completed",
1179
- ts: Date.now(),
1180
- runId: id,
1181
- stepIndex,
1182
- success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1),
1183
- }));
1184
-
1185
- if (parallelResults.some((r) => r.exitCode !== 0 && r.exitCode !== -1)) {
1186
- break;
1187
- }
1188
- } finally {
1189
- if (worktreeSetup) cleanupWorktrees(worktreeSetup);
1190
- }
1191
- } else {
1192
- const seqStep = step as SubagentStep;
1193
- const stepStartTime = Date.now();
1194
- statusPayload.currentStep = flatIndex;
1195
- statusPayload.steps[flatIndex].status = "running";
1196
- statusPayload.steps[flatIndex].activityState = undefined;
1197
- statusPayload.activityState = undefined;
1198
- statusPayload.steps[flatIndex].skills = seqStep.skills;
1199
- statusPayload.steps[flatIndex].startedAt = stepStartTime;
1200
- statusPayload.steps[flatIndex].lastActivityAt = stepStartTime;
1201
- statusPayload.lastActivityAt = stepStartTime;
1202
- statusPayload.lastUpdate = stepStartTime;
1203
- statusPayload.outputFile = path.join(asyncDir, `output-${flatIndex}.log`);
1204
- writeJson(statusPath, statusPayload);
1205
-
1206
- appendJsonl(eventsPath, JSON.stringify({
1207
- type: "subagent.step.started",
1208
- ts: stepStartTime,
1209
- runId: id,
1210
- stepIndex: flatIndex,
1211
- agent: seqStep.agent,
1212
- }));
1213
-
1214
- const singleResult = await runSingleStep(seqStep, {
1215
- previousOutput, placeholder, cwd, sessionEnabled,
1216
- sessionDir: config.sessionDir,
1217
- artifactsDir, artifactConfig, id,
1218
- flatIndex, flatStepCount: flatSteps.length,
1219
- outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
1220
- piPackageRoot: config.piPackageRoot,
1221
- piArgv1: config.piArgv1,
1222
- childIntercomTarget: config.childIntercomTargets?.[flatIndex],
1223
- registerInterrupt: (interrupt) => {
1224
- activeChildInterrupt = interrupt;
1225
- },
1226
- });
1227
- if (seqStep.sessionFile) {
1228
- latestSessionFile = seqStep.sessionFile;
1229
- }
1230
-
1231
- previousOutput = singleResult.output;
1232
- results.push({
1233
- agent: singleResult.agent,
1234
- output: singleResult.output,
1235
- success: singleResult.exitCode === 0,
1236
- intercomTarget: singleResult.intercomTarget,
1237
- model: singleResult.model,
1238
- attemptedModels: singleResult.attemptedModels,
1239
- modelAttempts: singleResult.modelAttempts,
1240
- artifactPaths: singleResult.artifactPaths,
1241
- });
1242
-
1243
- const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
1244
- let stepTokens: TokenUsage | null = cumulativeTokens
1245
- ? {
1246
- input: cumulativeTokens.input - previousCumulativeTokens.input,
1247
- output: cumulativeTokens.output - previousCumulativeTokens.output,
1248
- total: cumulativeTokens.total - previousCumulativeTokens.total,
1249
- }
1250
- : null;
1251
- if (cumulativeTokens) {
1252
- previousCumulativeTokens = cumulativeTokens;
1253
- } else {
1254
- stepTokens = tokenUsageFromAttempts(singleResult.modelAttempts);
1255
- if (stepTokens) {
1256
- previousCumulativeTokens = {
1257
- input: previousCumulativeTokens.input + stepTokens.input,
1258
- output: previousCumulativeTokens.output + stepTokens.output,
1259
- total: previousCumulativeTokens.total + stepTokens.total,
1260
- };
1261
- }
1262
- }
1263
-
1264
- const stepEndTime = Date.now();
1265
- statusPayload.steps[flatIndex].status = singleResult.exitCode === 0 ? "complete" : "failed";
1266
- statusPayload.steps[flatIndex].endedAt = stepEndTime;
1267
- statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
1268
- statusPayload.steps[flatIndex].exitCode = singleResult.exitCode;
1269
- statusPayload.steps[flatIndex].model = singleResult.model;
1270
- statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
1271
- statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
1272
- statusPayload.steps[flatIndex].error = singleResult.error;
1273
- if (stepTokens) {
1274
- statusPayload.steps[flatIndex].tokens = stepTokens;
1275
- statusPayload.totalTokens = { ...previousCumulativeTokens };
1276
- }
1277
- statusPayload.lastUpdate = stepEndTime;
1278
- writeJson(statusPath, statusPayload);
1279
-
1280
- appendJsonl(eventsPath, JSON.stringify({
1281
- type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
1282
- ts: stepEndTime,
1283
- runId: id,
1284
- stepIndex: flatIndex,
1285
- agent: seqStep.agent,
1286
- exitCode: singleResult.exitCode,
1287
- durationMs: stepEndTime - stepStartTime,
1288
- tokens: stepTokens,
1289
- }));
1290
-
1291
- flatIndex++;
1292
- if (singleResult.exitCode !== 0) {
1293
- break;
1294
- }
1295
- }
1296
- }
1297
-
1298
- let summary = results.map((r) => `${r.agent}:\n${r.output}`).join("\n\n");
1299
- let truncated = false;
1300
-
1301
- if (maxOutput) {
1302
- const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
1303
- const lastArtifactPath = results[results.length - 1]?.artifactPaths?.outputPath;
1304
- const truncResult = truncateOutput(summary, config, lastArtifactPath);
1305
- if (truncResult.truncated) {
1306
- summary = truncResult.text;
1307
- truncated = true;
1308
- }
1309
- }
1310
-
1311
- const agentName = flatSteps.length === 1
1312
- ? flatSteps[0].agent
1313
- : `chain:${flatSteps.map((s) => s.agent).join("->")}`;
1314
- let sessionFile: string | undefined;
1315
- let shareUrl: string | undefined;
1316
- let gistUrl: string | undefined;
1317
- let shareError: string | undefined;
1318
-
1319
- if (shareEnabled) {
1320
- sessionFile = config.sessionDir
1321
- ? (findLatestSessionFile(config.sessionDir) ?? undefined)
1322
- : undefined;
1323
- if (!sessionFile && latestSessionFile) {
1324
- sessionFile = latestSessionFile;
1325
- }
1326
- if (sessionFile) {
1327
- try {
1328
- const exportDir = config.sessionDir ?? path.dirname(sessionFile);
1329
- const htmlPath = await exportSessionHtml(sessionFile, exportDir, config.piPackageRoot);
1330
- const share = createShareLink(htmlPath);
1331
- if ("error" in share) shareError = share.error;
1332
- else {
1333
- shareUrl = share.shareUrl;
1334
- gistUrl = share.gistUrl;
1335
- }
1336
- } catch (err) {
1337
- shareError = String(err);
1338
- }
1339
- } else {
1340
- shareError = "Session file not found.";
1341
- }
1342
- }
1343
-
1344
- if (activityTimer) {
1345
- clearInterval(activityTimer);
1346
- activityTimer = undefined;
1347
- }
1348
- const effectiveSessionFile = sessionFile ?? latestSessionFile;
1349
- const runEndedAt = Date.now();
1350
- statusPayload.state = interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
1351
- statusPayload.activityState = undefined;
1352
- statusPayload.endedAt = runEndedAt;
1353
- statusPayload.lastUpdate = runEndedAt;
1354
- statusPayload.sessionFile = effectiveSessionFile;
1355
- statusPayload.shareUrl = shareUrl;
1356
- statusPayload.gistUrl = gistUrl;
1357
- statusPayload.shareError = shareError;
1358
- if (statusPayload.state === "failed") {
1359
- const failedStep = statusPayload.steps.find((s) => s.status === "failed");
1360
- if (failedStep?.agent) {
1361
- statusPayload.error = `Step failed: ${failedStep.agent}`;
1362
- }
1363
- }
1364
- writeJson(statusPath, statusPayload);
1365
- appendJsonl(
1366
- eventsPath,
1367
- JSON.stringify({
1368
- type: "subagent.run.completed",
1369
- ts: runEndedAt,
1370
- runId: id,
1371
- status: statusPayload.state,
1372
- durationMs: runEndedAt - overallStartTime,
1373
- }),
1374
- );
1375
- writeRunLog(logPath, {
1376
- id,
1377
- mode: statusPayload.mode,
1378
- cwd,
1379
- startedAt: overallStartTime,
1380
- endedAt: runEndedAt,
1381
- steps: statusPayload.steps.map((step) => ({
1382
- agent: step.agent,
1383
- status: step.status,
1384
- durationMs: step.durationMs,
1385
- })),
1386
- summary,
1387
- truncated,
1388
- artifactsDir,
1389
- sessionFile: effectiveSessionFile,
1390
- shareUrl,
1391
- shareError,
1392
- });
1393
-
1394
- try {
1395
- writeJson(resultPath, {
1396
- id,
1397
- agent: agentName,
1398
- mode: config.resultMode ?? statusPayload.mode,
1399
- success: !interrupted && results.every((r) => r.success),
1400
- state: interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
1401
- summary: interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
1402
- results: results.map((r) => ({
1403
- agent: r.agent,
1404
- output: r.output,
1405
- success: r.success,
1406
- skipped: r.skipped || undefined,
1407
- intercomTarget: r.intercomTarget,
1408
- model: r.model,
1409
- attemptedModels: r.attemptedModels,
1410
- modelAttempts: r.modelAttempts,
1411
- artifactPaths: r.artifactPaths,
1412
- truncated: r.truncated,
1413
- })),
1414
- exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
1415
- timestamp: runEndedAt,
1416
- durationMs: runEndedAt - overallStartTime,
1417
- truncated,
1418
- artifactsDir,
1419
- cwd,
1420
- asyncDir,
1421
- sessionId: config.sessionId,
1422
- sessionFile: effectiveSessionFile,
1423
- intercomTarget: config.controlIntercomTarget,
1424
- shareUrl,
1425
- gistUrl,
1426
- shareError,
1427
- ...(taskIndex !== undefined && { taskIndex }),
1428
- ...(totalTasks !== undefined && { totalTasks }),
1429
- });
1430
- } catch (err) {
1431
- console.error(`Failed to write result file ${resultPath}:`, err);
1432
- }
1433
- }
1434
-
1435
- const configArg = process.argv[2];
1436
- if (configArg) {
1437
- try {
1438
- const configJson = fs.readFileSync(configArg, "utf-8");
1439
- const config = JSON.parse(configJson) as SubagentRunConfig;
1440
- try {
1441
- fs.unlinkSync(configArg);
1442
- } catch {
1443
- // Temp config cleanup is best effort.
1444
- }
1445
- runSubagent(config).catch((runErr) => {
1446
- console.error("Subagent runner error:", runErr);
1447
- process.exit(1);
1448
- });
1449
- } catch (err) {
1450
- console.error("Subagent runner error:", err);
1451
- process.exit(1);
1452
- }
1453
- } else {
1454
- let input = "";
1455
- process.stdin.setEncoding("utf-8");
1456
- process.stdin.on("data", (chunk) => {
1457
- input += chunk;
1458
- });
1459
- process.stdin.on("end", () => {
1460
- try {
1461
- const config = JSON.parse(input) as SubagentRunConfig;
1462
- runSubagent(config).catch((runErr) => {
1463
- console.error("Subagent runner error:", runErr);
1464
- process.exit(1);
1465
- });
1466
- } catch (err) {
1467
- console.error("Subagent runner error:", err);
1468
- process.exit(1);
1469
- }
1470
- });
1471
- }