@duckmind/dm-darwin-x64 0.13.2 → 0.13.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +49 -2
  2. package/README.md +5 -5
  3. package/dm +0 -0
  4. package/docs/settings.md +1 -0
  5. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  6. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  7. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  8. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  9. package/examples/extensions/with-deps/package-lock.json +2 -2
  10. package/examples/extensions/with-deps/package.json +1 -1
  11. package/extensions/.dm-extensions.json +2 -2
  12. package/extensions/dm-multicodex/README.md +3 -1
  13. package/extensions/dm-multicodex/account-manager.test.ts +3 -1
  14. package/extensions/dm-multicodex/account-manager.ts +27 -1
  15. package/extensions/dm-multicodex/commands.test.ts +1 -0
  16. package/extensions/dm-multicodex/commands.ts +81 -1
  17. package/extensions/dm-multicodex/index.ts +7 -0
  18. package/extensions/dm-multicodex/node_modules/.package-lock.json +28 -28
  19. package/extensions/dm-multicodex/package-lock.json +9633 -9633
  20. package/extensions/dm-multicodex/package.json +56 -56
  21. package/extensions/dm-multicodex/storage.ts +2 -2
  22. package/extensions/dm-multicodex/sync.test.ts +114 -0
  23. package/extensions/dm-multicodex/sync.ts +249 -0
  24. package/extensions/dm-multicodex/tsconfig.json +17 -0
  25. package/extensions/dm-subagents/artifacts.ts +11 -5
  26. package/extensions/dm-subagents/async-execution.ts +6 -1
  27. package/extensions/dm-subagents/execution.ts +1 -1
  28. package/extensions/dm-subagents/index.ts +1 -1
  29. package/extensions/dm-subagents/intercom-bridge.ts +8 -0
  30. package/extensions/dm-subagents/package.json +1 -1
  31. package/extensions/dm-subagents/schemas.ts +1 -1
  32. package/extensions/dm-subagents/settings.ts +6 -4
  33. package/extensions/dm-subagents/skills.ts +117 -25
  34. package/extensions/dm-subagents/subagent-executor.ts +2 -6
  35. package/extensions/dm-subagents/subagent-runner.ts +176 -51
  36. package/extensions/dm-subagents/types.ts +62 -2
  37. package/extensions/dm-subagents/worktree.ts +27 -9
  38. package/extensions/dm-thinking-timer/README.md +1 -1
  39. package/extensions/dm-ultrathink/src/naming.ts +151 -10
  40. package/package.json +1 -1
  41. package/theme/duckmind.json +77 -65
@@ -3,12 +3,10 @@
3
3
  */
4
4
 
5
5
  import * as fs from "node:fs";
6
- import * as os from "node:os";
7
6
  import * as path from "node:path";
8
7
  import type { AgentConfig } from "./agents.ts";
9
8
  import { normalizeSkillInput } from "./skills.ts";
10
-
11
- const CHAIN_RUNS_DIR = path.join(os.tmpdir(), "dm-chain-runs");
9
+ import { CHAIN_RUNS_DIR } from "./types.ts";
12
10
  const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
13
11
 
14
12
  // =============================================================================
@@ -100,7 +98,9 @@ export function createChainDir(runId: string, baseDir?: string): string {
100
98
  export function removeChainDir(chainDir: string): void {
101
99
  try {
102
100
  fs.rmSync(chainDir, { recursive: true });
103
- } catch {}
101
+ } catch {
102
+ // Chain cleanup is best-effort. Runs can already have cleaned their temp dir.
103
+ }
104
104
  }
105
105
 
106
106
  export function cleanupOldChainDirs(): void {
@@ -110,6 +110,8 @@ export function cleanupOldChainDirs(): void {
110
110
  try {
111
111
  dirs = fs.readdirSync(CHAIN_RUNS_DIR);
112
112
  } catch {
113
+ // Startup cleanup is best-effort. If the scoped temp root is unreadable,
114
+ // skip cleanup instead of failing extension startup.
113
115
  return;
114
116
  }
115
117
 
@@ -6,7 +6,6 @@ import { execSync } from "node:child_process";
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
- import { loadSkills, type Skill } from "@mariozechner/pi-coding-agent";
10
9
 
11
10
  export type SkillSource =
12
11
  | "project"
@@ -86,6 +85,7 @@ function getPackageSkillPaths(packageRoot: string): string[] {
86
85
  .filter((s: unknown) => typeof s === "string")
87
86
  .map((s: string) => path.resolve(packageRoot, s));
88
87
  } catch {
88
+ // Package scanning is opportunistic; ignore malformed/missing package metadata.
89
89
  return [];
90
90
  }
91
91
  }
@@ -98,6 +98,7 @@ function getGlobalNpmRoot(): string | null {
98
98
  cachedGlobalNpmRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
99
99
  return cachedGlobalNpmRoot;
100
100
  } catch {
101
+ // Global npm root is optional in constrained environments.
101
102
  cachedGlobalNpmRoot = ""; // Empty string means "tried but failed"
102
103
  return null;
103
104
  }
@@ -123,6 +124,7 @@ function collectPackageSkillPaths(cwd: string): string[] {
123
124
  try {
124
125
  entries = fs.readdirSync(dir, { withFileTypes: true });
125
126
  } catch {
127
+ // Ignore unreadable package roots and continue scanning other roots.
126
128
  continue;
127
129
  }
128
130
 
@@ -136,6 +138,7 @@ function collectPackageSkillPaths(cwd: string): string[] {
136
138
  try {
137
139
  scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
138
140
  } catch {
141
+ // Ignore unreadable scoped package directories and continue.
139
142
  continue;
140
143
  }
141
144
  for (const scopeEntry of scopeEntries) {
@@ -178,7 +181,9 @@ function collectSettingsSkillPaths(cwd: string): string[] {
178
181
  }
179
182
  results.push(resolved);
180
183
  }
181
- } catch {}
184
+ } catch {
185
+ // Settings-provided skills are optional; ignore malformed or missing settings files.
186
+ }
182
187
  }
183
188
 
184
189
  return results;
@@ -196,21 +201,22 @@ function buildSkillPaths(cwd: string): string[] {
196
201
  return [...new Set([...defaultSkillPaths, ...packagePaths, ...settingsPaths])];
197
202
  }
198
203
 
199
- function inferSkillSource(sourceInfo: { source: string; scope: string }, filePath: string, cwd: string): SkillSource {
200
- const { scope, source } = sourceInfo;
201
-
202
- if (scope === "project" && source === "local") return "project";
203
- if (scope === "user" && source === "local") return "user";
204
+ function inferSkillSource(filePath: string, cwd: string): SkillSource {
205
+ const projectConfigRoot = path.resolve(cwd, CONFIG_DIR);
206
+ const projectSkillsRoot = path.resolve(cwd, CONFIG_DIR, "skills");
207
+ const projectPackagesRoot = path.resolve(cwd, CONFIG_DIR, "npm", "node_modules");
208
+ const projectAgentsRoot = path.resolve(cwd, ".agents");
209
+ const userSkillsRoot = path.resolve(AGENT_DIR, "skills");
210
+ const userPackagesRoot = path.resolve(AGENT_DIR, "npm", "node_modules");
211
+ const userAgentsRoot = path.resolve(os.homedir(), ".agents");
204
212
 
205
- // Fallback: infer from file path when sourceInfo isn't specific enough
206
- // (e.g. scope === "temporary" for skills loaded via explicit skillPaths)
207
- const projectRoot = path.resolve(cwd, CONFIG_DIR);
208
- const altProjectRoot = path.resolve(cwd, ".agents");
209
- const isProjectScoped = isWithinPath(filePath, projectRoot) || isWithinPath(filePath, altProjectRoot);
210
- if (isProjectScoped) return "project";
213
+ if (isWithinPath(filePath, projectPackagesRoot)) return "project-package";
214
+ if (isWithinPath(filePath, projectSkillsRoot) || isWithinPath(filePath, projectAgentsRoot)) return "project";
215
+ if (isWithinPath(filePath, projectConfigRoot)) return "project-settings";
211
216
 
212
- const isUserScoped = isWithinPath(filePath, AGENT_DIR) || isWithinPath(filePath, path.join(os.homedir(), ".agents"));
213
- if (isUserScoped) return "user";
217
+ if (isWithinPath(filePath, userPackagesRoot)) return "user-package";
218
+ if (isWithinPath(filePath, userSkillsRoot) || isWithinPath(filePath, userAgentsRoot)) return "user";
219
+ if (isWithinPath(filePath, AGENT_DIR)) return "user-settings";
214
220
 
215
221
  const globalRoot = getGlobalNpmRoot();
216
222
  if (globalRoot && isWithinPath(filePath, globalRoot)) return "user-package";
@@ -227,6 +233,99 @@ function chooseHigherPrioritySkill(existing: CachedSkillEntry | undefined, candi
227
233
  return candidate.order < existing.order ? candidate : existing;
228
234
  }
229
235
 
236
+ function maybeReadSkillDescription(filePath: string): string | undefined {
237
+ try {
238
+ const content = fs.readFileSync(filePath, "utf-8");
239
+ const normalized = content.replace(/\r\n/g, "\n");
240
+ if (!normalized.startsWith("---")) return undefined;
241
+
242
+ const endIndex = normalized.indexOf("\n---", 3);
243
+ if (endIndex === -1) return undefined;
244
+
245
+ const frontmatter = normalized.slice(3, endIndex).trim();
246
+ const match = frontmatter.match(/^description:\s*(.+)$/m);
247
+ if (!match) return undefined;
248
+ return match[1]?.trim().replace(/^['\"]|['\"]$/g, "");
249
+ } catch {
250
+ // Description parsing is best-effort metadata extraction.
251
+ return undefined;
252
+ }
253
+ }
254
+
255
+ function collectFilesystemSkills(cwd: string, skillPaths: string[]): CachedSkillEntry[] {
256
+ const entries: CachedSkillEntry[] = [];
257
+ const seen = new Set<string>();
258
+ let order = 0;
259
+
260
+ const pushEntry = (name: string, filePath: string) => {
261
+ const resolvedFile = path.resolve(filePath);
262
+ if (seen.has(resolvedFile)) return;
263
+ if (!fs.existsSync(resolvedFile)) return;
264
+ seen.add(resolvedFile);
265
+ entries.push({
266
+ name,
267
+ filePath: resolvedFile,
268
+ source: inferSkillSource(resolvedFile, cwd),
269
+ description: maybeReadSkillDescription(resolvedFile),
270
+ order: order++,
271
+ });
272
+ };
273
+
274
+ for (const skillPath of skillPaths) {
275
+ if (!fs.existsSync(skillPath)) continue;
276
+
277
+ let stat: fs.Stats;
278
+ try {
279
+ stat = fs.statSync(skillPath);
280
+ } catch {
281
+ // Ignore paths that disappear or become unreadable during discovery.
282
+ continue;
283
+ }
284
+
285
+ if (stat.isFile()) {
286
+ const fileName = path.basename(skillPath);
287
+ if (!fileName.toLowerCase().endsWith(".md")) continue;
288
+ const skillName = fileName.toLowerCase() === "skill.md"
289
+ ? path.basename(path.dirname(skillPath))
290
+ : path.basename(fileName, path.extname(fileName));
291
+ pushEntry(skillName, skillPath);
292
+ continue;
293
+ }
294
+
295
+ if (!stat.isDirectory()) continue;
296
+
297
+ const rootSkillFile = path.join(skillPath, "SKILL.md");
298
+ if (fs.existsSync(rootSkillFile)) {
299
+ pushEntry(path.basename(skillPath), rootSkillFile);
300
+ }
301
+
302
+ let childEntries: fs.Dirent[];
303
+ try {
304
+ childEntries = fs.readdirSync(skillPath, { withFileTypes: true });
305
+ } catch {
306
+ // Ignore unreadable skill directories and continue scanning.
307
+ continue;
308
+ }
309
+
310
+ for (const child of childEntries) {
311
+ if (child.name.startsWith(".")) continue;
312
+ const childPath = path.join(skillPath, child.name);
313
+ if (child.isDirectory() || child.isSymbolicLink()) {
314
+ const nestedSkillPath = path.join(childPath, "SKILL.md");
315
+ if (fs.existsSync(nestedSkillPath)) {
316
+ pushEntry(child.name, nestedSkillPath);
317
+ }
318
+ continue;
319
+ }
320
+ if (child.isFile() && child.name.toLowerCase().endsWith(".md")) {
321
+ pushEntry(path.basename(child.name, path.extname(child.name)), childPath);
322
+ }
323
+ }
324
+ }
325
+
326
+ return entries;
327
+ }
328
+
230
329
  function getCachedSkills(cwd: string): CachedSkillEntry[] {
231
330
  const now = Date.now();
232
331
  if (loadSkillsCache && loadSkillsCache.cwd === cwd && now - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS) {
@@ -234,18 +333,10 @@ function getCachedSkills(cwd: string): CachedSkillEntry[] {
234
333
  }
235
334
 
236
335
  const skillPaths = buildSkillPaths(cwd);
237
- const loaded = loadSkills({ cwd, skillPaths, includeDefaults: false });
336
+ const loaded = collectFilesystemSkills(cwd, skillPaths);
238
337
  const dedupedByName = new Map<string, CachedSkillEntry>();
239
338
 
240
- for (let i = 0; i < loaded.skills.length; i++) {
241
- const skill = loaded.skills[i] as Skill;
242
- const entry: CachedSkillEntry = {
243
- name: skill.name,
244
- filePath: skill.filePath,
245
- source: inferSkillSource(skill.sourceInfo, skill.filePath, cwd),
246
- description: skill.description,
247
- order: i,
248
- };
339
+ for (const entry of loaded) {
249
340
  const current = dedupedByName.get(entry.name);
250
341
  dedupedByName.set(entry.name, chooseHigherPrioritySkill(current, entry));
251
342
  }
@@ -294,6 +385,7 @@ export function readSkill(
294
385
 
295
386
  return skill;
296
387
  } catch {
388
+ // Treat unreadable skill files as unresolved so callers can surface as missing.
297
389
  return undefined;
298
390
  }
299
391
  }
@@ -22,7 +22,7 @@ import {
22
22
  import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
23
23
  import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.ts";
24
24
  import { createForkContextResolver } from "./fork-context.ts";
25
- import { applyIntercomBridgeToAgent, resolveIntercomBridge } from "./intercom-bridge.ts";
25
+ import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget } from "./intercom-bridge.ts";
26
26
  import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
27
27
  import { getSingleResultOutput, mapConcurrent } from "./utils.ts";
28
28
  import {
@@ -1117,11 +1117,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1117
1117
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
1118
1118
  deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1119
1119
  const discoveredAgents = deps.discoverAgents(ctx.cwd, scope).agents;
1120
- let sessionName = deps.pi.getSessionName()?.trim();
1121
- if (!sessionName) {
1122
- sessionName = `session-${ctx.sessionManager.getSessionId().slice(0, 8)}`;
1123
- deps.pi.setSessionName(sessionName);
1124
- }
1120
+ const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
1125
1121
  const intercomBridge = resolveIntercomBridge({
1126
1122
  config: deps.config.intercomBridge,
1127
1123
  context: normalizedParams.context,
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import * as path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ import type { Message } from "@mariozechner/pi-ai";
6
7
  import { appendJsonl, getArtifactPaths } from "./artifacts.ts";
7
8
  import { getPiSpawnCommand } from "./pi-spawn.ts";
8
9
  import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
@@ -27,6 +28,7 @@ import {
27
28
  } from "./parallel-utils.ts";
28
29
  import { buildPiArgs, cleanupTempDir } from "./pi-args.ts";
29
30
  import { formatModelAttemptNote, isRetryableModelFailure } from "./model-fallback.ts";
31
+ import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "./utils.ts";
30
32
  import {
31
33
  cleanupWorktrees,
32
34
  createWorktrees,
@@ -53,6 +55,7 @@ interface SubagentRunConfig {
53
55
  asyncDir: string;
54
56
  sessionId?: string | null;
55
57
  piPackageRoot?: string;
58
+ piArgv1?: string;
56
59
  worktreeSetupHook?: string;
57
60
  worktreeSetupHookTimeoutMs?: number;
58
61
  }
@@ -122,32 +125,44 @@ function emptyUsage(): Usage {
122
125
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
123
126
  }
124
127
 
125
- function parseRunOutput(output: string): { usage: Usage; model?: string; error?: string } {
126
- const usage = emptyUsage();
127
- let model: string | undefined;
128
- let error: string | undefined;
129
- for (const line of output.split("\n")) {
130
- if (!line.trim()) continue;
131
- try {
132
- const evt = JSON.parse(line) as { type?: string; message?: { role?: string; model?: string; errorMessage?: string; usage?: any } };
133
- if (evt.type !== "message_end" || evt.message?.role !== "assistant") continue;
134
- const msg = evt.message;
135
- if (msg.model) model = msg.model;
136
- if (msg.errorMessage) error = msg.errorMessage;
137
- const u = msg.usage;
138
- if (u) {
139
- usage.turns++;
140
- usage.input += u.input ?? u.inputTokens ?? 0;
141
- usage.output += u.output ?? u.outputTokens ?? 0;
142
- usage.cacheRead += u.cacheRead ?? 0;
143
- usage.cacheWrite += u.cacheWrite ?? 0;
144
- usage.cost += u.cost?.total ?? 0;
145
- }
146
- } catch {
147
- // Ignore malformed stdout lines.
148
- }
149
- }
150
- return { usage, model, error };
128
+ interface ChildEventContext {
129
+ eventsPath: string;
130
+ runId: string;
131
+ stepIndex: number;
132
+ agent: string;
133
+ }
134
+
135
+ interface ChildUsage {
136
+ input?: number;
137
+ inputTokens?: number;
138
+ output?: number;
139
+ outputTokens?: number;
140
+ cacheRead?: number;
141
+ cacheWrite?: number;
142
+ cost?: { total?: number };
143
+ }
144
+
145
+ type ChildMessage = Message & {
146
+ model?: string;
147
+ errorMessage?: string;
148
+ usage?: ChildUsage;
149
+ };
150
+
151
+ interface ChildEvent {
152
+ type?: string;
153
+ message?: ChildMessage;
154
+ toolName?: string;
155
+ args?: Record<string, unknown>;
156
+ }
157
+
158
+ interface RunPiStreamingResult {
159
+ stderr: string;
160
+ exitCode: number | null;
161
+ messages: Message[];
162
+ usage: Usage;
163
+ model?: string;
164
+ error?: string;
165
+ finalOutput: string;
151
166
  }
152
167
 
153
168
  function runPiStreaming(
@@ -156,36 +171,131 @@ function runPiStreaming(
156
171
  outputFile: string,
157
172
  env?: Record<string, string | undefined>,
158
173
  piPackageRoot?: string,
174
+ piArgv1?: string,
159
175
  maxSubagentDepth?: number,
160
- ): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
176
+ childEventContext?: ChildEventContext,
177
+ ): Promise<RunPiStreamingResult> {
161
178
  return new Promise((resolve) => {
162
179
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
163
180
  const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
164
- const spawnSpec = getPiSpawnCommand(args, piPackageRoot ? { piPackageRoot } : undefined);
181
+ const spawnSpec = getPiSpawnCommand(args, {
182
+ ...(piPackageRoot ? { piPackageRoot } : {}),
183
+ ...(piArgv1 ? { argv1: piArgv1 } : {}),
184
+ });
165
185
  const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
166
- let stdout = "";
167
186
  let stderr = "";
187
+ let stdoutBuf = "";
188
+ let stderrBuf = "";
189
+ const messages: Message[] = [];
190
+ const usage = emptyUsage();
191
+ let model: string | undefined;
192
+ let error: string | undefined;
193
+ const rawStdoutLines: string[] = [];
194
+
195
+ const writeOutputLine = (line: string) => {
196
+ if (!line.trim()) return;
197
+ outputStream.write(`${line}\n`);
198
+ };
199
+
200
+ const writeOutputText = (text: string) => {
201
+ for (const line of text.split("\n")) {
202
+ writeOutputLine(line);
203
+ }
204
+ };
205
+
206
+ const appendChildEvent = (event: Record<string, unknown>) => {
207
+ if (!childEventContext) return;
208
+ appendJsonl(childEventContext.eventsPath, JSON.stringify({
209
+ ...event,
210
+ subagentSource: "child",
211
+ subagentRunId: childEventContext.runId,
212
+ subagentStepIndex: childEventContext.stepIndex,
213
+ subagentAgent: childEventContext.agent,
214
+ observedAt: Date.now(),
215
+ }));
216
+ };
217
+
218
+ const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
219
+ appendChildEvent({ type, line });
220
+ };
221
+
222
+ const processStdoutLine = (line: string) => {
223
+ if (!line.trim()) return;
224
+ let event: ChildEvent;
225
+ try {
226
+ event = JSON.parse(line) as ChildEvent;
227
+ } catch {
228
+ rawStdoutLines.push(line);
229
+ writeOutputLine(line);
230
+ appendChildLine("subagent.child.stdout", line);
231
+ return;
232
+ }
233
+
234
+ appendChildEvent(event);
235
+
236
+ if (event.type === "tool_execution_start" && event.toolName) {
237
+ const toolArgs = extractToolArgsPreview(event.args ?? {});
238
+ writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
239
+ return;
240
+ }
241
+
242
+ if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
243
+ messages.push(event.message);
244
+ const text = extractTextFromContent(event.message.content);
245
+ if (text) writeOutputText(text);
246
+
247
+ if (event.type !== "message_end" || event.message.role !== "assistant") return;
248
+ if (event.message.model) model = event.message.model;
249
+ if (event.message.errorMessage) error = event.message.errorMessage;
250
+ const eventUsage = event.message.usage;
251
+ if (!eventUsage) return;
252
+ usage.turns++;
253
+ usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
254
+ usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
255
+ usage.cacheRead += eventUsage.cacheRead ?? 0;
256
+ usage.cacheWrite += eventUsage.cacheWrite ?? 0;
257
+ usage.cost += eventUsage.cost?.total ?? 0;
258
+ }
259
+ };
260
+
261
+ const processStderrText = (text: string) => {
262
+ stderr += text;
263
+ stderrBuf += text;
264
+ outputStream.write(text);
265
+ if (!childEventContext) return;
266
+ const lines = stderrBuf.split("\n");
267
+ stderrBuf = lines.pop() || "";
268
+ for (const line of lines) {
269
+ if (!line.trim()) continue;
270
+ appendChildLine("subagent.child.stderr", line);
271
+ }
272
+ };
168
273
 
169
274
  child.stdout.on("data", (chunk: Buffer) => {
170
275
  const text = chunk.toString();
171
- stdout += text;
172
- outputStream.write(text);
276
+ stdoutBuf += text;
277
+ const lines = stdoutBuf.split("\n");
278
+ stdoutBuf = lines.pop() || "";
279
+ for (const line of lines) processStdoutLine(line);
173
280
  });
174
281
 
175
282
  child.stderr.on("data", (chunk: Buffer) => {
176
- const text = chunk.toString();
177
- stderr += text;
178
- outputStream.write(text);
283
+ processStderrText(chunk.toString());
179
284
  });
180
285
 
181
286
  child.on("close", (exitCode) => {
287
+ if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
288
+ if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
182
289
  outputStream.end();
183
- resolve({ stdout, stderr, exitCode });
290
+ const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
291
+ resolve({ stderr, exitCode, messages, usage, model, error, finalOutput });
184
292
  });
185
293
 
186
- child.on("error", () => {
294
+ child.on("error", (spawnError) => {
187
295
  outputStream.end();
188
- resolve({ stdout, stderr, exitCode: 1 });
296
+ const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
297
+ const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
298
+ resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? spawnErrorMessage, finalOutput });
189
299
  });
190
300
  });
191
301
  }
@@ -349,6 +459,7 @@ interface SingleStepContext {
349
459
  flatStepCount: number;
350
460
  outputFile: string;
351
461
  piPackageRoot?: string;
462
+ piArgv1?: string;
352
463
  }
353
464
 
354
465
  /** Run a single pi agent step, returning output and metadata */
@@ -389,14 +500,13 @@ async function runSingleStep(
389
500
  const attemptedModels: string[] = [];
390
501
  const modelAttempts: ModelAttempt[] = [];
391
502
  const attemptNotes: string[] = [];
392
- let finalResult:
393
- | { stdout: string; stderr: string; exitCode: number | null; usage: Usage; model?: string; error?: string }
394
- | undefined;
503
+ const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
504
+ let finalResult: RunPiStreamingResult | undefined;
395
505
 
396
506
  for (let index = 0; index < candidates.length; index++) {
397
507
  const candidate = candidates[index];
398
508
  const { args, env, tempDir } = buildPiArgs({
399
- baseArgs: ["-p"],
509
+ baseArgs: ["--mode", "json", "-p"],
400
510
  task,
401
511
  sessionEnabled,
402
512
  sessionDir,
@@ -409,28 +519,41 @@ async function runSingleStep(
409
519
  mcpDirectTools: step.mcpDirectTools,
410
520
  promptFileStem: step.agent,
411
521
  });
412
- const outputFile = index === 0 ? ctx.outputFile : `${ctx.outputFile}.attempt-${index + 1}`;
413
- const run = await runPiStreaming(args, step.cwd ?? ctx.cwd, outputFile, env, ctx.piPackageRoot, step.maxSubagentDepth);
522
+ const run = await runPiStreaming(
523
+ args,
524
+ step.cwd ?? ctx.cwd,
525
+ ctx.outputFile,
526
+ env,
527
+ ctx.piPackageRoot,
528
+ ctx.piArgv1,
529
+ step.maxSubagentDepth,
530
+ { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
531
+ );
414
532
  cleanupTempDir(tempDir);
415
533
 
416
- const parsed = parseRunOutput(run.stdout);
417
- const error = parsed.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
534
+ const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
535
+ const effectiveExitCode = hiddenError?.hasError ? (hiddenError.exitCode ?? 1) : run.exitCode;
536
+ const error = hiddenError?.hasError
537
+ ? hiddenError.details
538
+ ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
539
+ : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
540
+ : run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
418
541
  const attempt: ModelAttempt = {
419
- model: candidate ?? parsed.model ?? step.model ?? "default",
420
- success: run.exitCode === 0 && !error,
421
- exitCode: run.exitCode,
542
+ model: candidate ?? run.model ?? step.model ?? "default",
543
+ success: effectiveExitCode === 0 && !error,
544
+ exitCode: effectiveExitCode,
422
545
  error,
423
- usage: parsed.usage,
546
+ usage: run.usage,
424
547
  };
425
548
  modelAttempts.push(attempt);
426
549
  if (candidate) attemptedModels.push(candidate);
427
- finalResult = { ...run, usage: parsed.usage, model: candidate ?? parsed.model, error };
550
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
428
551
  if (attempt.success) break;
429
552
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
430
553
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
431
554
  }
432
555
 
433
- const rawOutput = (finalResult?.stdout || "").trim();
556
+ const rawOutput = finalResult?.finalOutput ?? "";
434
557
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
435
558
  ? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
436
559
  : { fullOutput: rawOutput };
@@ -768,6 +891,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
768
891
  flatIndex: fi, flatStepCount: flatSteps.length,
769
892
  outputFile: path.join(asyncDir, `output-${fi}.log`),
770
893
  piPackageRoot: config.piPackageRoot,
894
+ piArgv1: config.piArgv1,
771
895
  });
772
896
  if (task.sessionFile) {
773
897
  latestSessionFile = task.sessionFile;
@@ -888,6 +1012,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
888
1012
  flatIndex, flatStepCount: flatSteps.length,
889
1013
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
890
1014
  piPackageRoot: config.piPackageRoot,
1015
+ piArgv1: config.piArgv1,
891
1016
  });
892
1017
  if (seqStep.sessionFile) {
893
1018
  latestSessionFile = seqStep.sessionFile;
@@ -309,10 +309,66 @@ export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
309
309
  cleanupDays: 7,
310
310
  };
311
311
 
312
+ function sanitizeTempScopeSegment(value: string): string {
313
+ const sanitized = value
314
+ .trim()
315
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
316
+ .replace(/^-+|-+$/g, "");
317
+ return sanitized || "unknown";
318
+ }
319
+
320
+ export function resolveTempScopeId(options?: {
321
+ env?: NodeJS.ProcessEnv;
322
+ getuid?: (() => number) | undefined;
323
+ userInfo?: (() => { username?: string | null }) | undefined;
324
+ homedir?: (() => string) | undefined;
325
+ }): string {
326
+ const env = options?.env ?? process.env;
327
+ const getuid = options && Object.hasOwn(options, "getuid")
328
+ ? options.getuid
329
+ : process.getuid?.bind(process);
330
+ if (typeof getuid === "function") {
331
+ return `uid-${getuid()}`;
332
+ }
333
+
334
+ for (const key of ["USERNAME", "USER", "LOGNAME"] as const) {
335
+ const value = env[key];
336
+ if (value) return `user-${sanitizeTempScopeSegment(value)}`;
337
+ }
338
+
339
+ const userInfo = options && Object.hasOwn(options, "userInfo")
340
+ ? options.userInfo
341
+ : os.userInfo;
342
+ try {
343
+ const username = userInfo?.().username;
344
+ if (username) return `user-${sanitizeTempScopeSegment(username)}`;
345
+ } catch {
346
+ // Fall through to home-directory-based scoping.
347
+ }
348
+
349
+ const homedir = env.USERPROFILE ?? env.HOME;
350
+ if (homedir) return `home-${sanitizeTempScopeSegment(homedir)}`;
351
+
352
+ const resolveHomedir = options && Object.hasOwn(options, "homedir")
353
+ ? options.homedir
354
+ : os.homedir;
355
+ try {
356
+ const fallbackHomedir = resolveHomedir?.();
357
+ if (fallbackHomedir) return `home-${sanitizeTempScopeSegment(fallbackHomedir)}`;
358
+ } catch {
359
+ // Fall through to the last-resort shared scope.
360
+ }
361
+
362
+ return "shared";
363
+ }
364
+
312
365
  export const MAX_PARALLEL = 8;
313
366
  export const MAX_CONCURRENCY = 4;
314
- export const RESULTS_DIR = path.join(os.tmpdir(), "dm-async-subagent-results");
315
- export const ASYNC_DIR = path.join(os.tmpdir(), "dm-async-subagent-runs");
367
+ export const TEMP_ROOT_DIR = path.join(os.tmpdir(), `pi-subagents-${resolveTempScopeId()}`);
368
+ export const RESULTS_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-results");
369
+ export const ASYNC_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-runs");
370
+ export const CHAIN_RUNS_DIR = path.join(TEMP_ROOT_DIR, "chain-runs");
371
+ export const TEMP_ARTIFACTS_DIR = path.join(TEMP_ROOT_DIR, "artifacts");
316
372
  export const WIDGET_KEY = "subagent-async";
317
373
  export const SLASH_RESULT_TYPE = "subagent-slash-result";
318
374
  export const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
@@ -329,6 +385,10 @@ export const DEFAULT_FORK_PREAMBLE =
329
385
  "Your sole job is to execute the task below. Do not continue or respond to the prior conversation " +
330
386
  "— focus exclusively on completing this task using your tools.";
331
387
 
388
+ export function getAsyncConfigPath(suffix: string): string {
389
+ return path.join(TEMP_ROOT_DIR, `async-cfg-${suffix}.json`);
390
+ }
391
+
332
392
  export function wrapForkTask(task: string, preamble?: string | false): string {
333
393
  if (preamble === false) return task;
334
394
  const effectivePreamble = preamble ?? DEFAULT_FORK_PREAMBLE;