pi-subagents 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/skills.ts CHANGED
@@ -210,9 +210,70 @@ function collectSettingsSkillPaths(cwd: string): SkillSearchPath[] {
210
210
  return results;
211
211
  }
212
212
 
213
+ function isSafePackagePath(value: string): boolean {
214
+ return value.length > 0
215
+ && !path.isAbsolute(value)
216
+ && value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
217
+ }
218
+
219
+ function parseNpmPackageName(source: string): string | undefined {
220
+ const spec = source.slice(4).trim();
221
+ if (!spec) return undefined;
222
+ const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/);
223
+ const packageName = match?.[1] ?? spec;
224
+ return isSafePackagePath(packageName) ? packageName : undefined;
225
+ }
226
+
227
+ function stripGitRef(repoPath: string): string {
228
+ const atIndex = repoPath.indexOf("@");
229
+ const hashIndex = repoPath.indexOf("#");
230
+ const refIndex = [atIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0];
231
+ return refIndex === undefined ? repoPath : repoPath.slice(0, refIndex);
232
+ }
233
+
234
+ function parseGitPackagePath(source: string): { host: string; repoPath: string } | undefined {
235
+ const spec = source.slice(4).trim();
236
+ if (!spec) return undefined;
237
+
238
+ let host = "";
239
+ let repoPath = "";
240
+ const scpLike = spec.match(/^git@([^:]+):(.+)$/);
241
+ if (scpLike) {
242
+ host = scpLike[1] ?? "";
243
+ repoPath = scpLike[2] ?? "";
244
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(spec)) {
245
+ try {
246
+ const url = new URL(spec);
247
+ host = url.hostname;
248
+ repoPath = url.pathname.replace(/^\/+/, "");
249
+ } catch {
250
+ return undefined;
251
+ }
252
+ } else {
253
+ const slashIndex = spec.indexOf("/");
254
+ if (slashIndex < 0) return undefined;
255
+ host = spec.slice(0, slashIndex);
256
+ repoPath = spec.slice(slashIndex + 1);
257
+ }
258
+
259
+ const normalizedPath = stripGitRef(repoPath).replace(/\.git$/, "").replace(/^\/+/, "");
260
+ if (!host || !isSafePackagePath(host) || !isSafePackagePath(normalizedPath) || normalizedPath.split(/[\\/]/).length < 2) {
261
+ return undefined;
262
+ }
263
+ return { host, repoPath: normalizedPath };
264
+ }
265
+
213
266
  function resolveSettingsPackageRoot(source: string, baseDir: string): string | undefined {
214
267
  const trimmed = source.trim();
215
268
  if (!trimmed) return undefined;
269
+ if (trimmed.startsWith("git:")) {
270
+ const parsed = parseGitPackagePath(trimmed);
271
+ return parsed ? path.join(baseDir, "git", parsed.host, parsed.repoPath) : undefined;
272
+ }
273
+ if (trimmed.startsWith("npm:")) {
274
+ const packageName = parseNpmPackageName(trimmed);
275
+ return packageName ? path.join(baseDir, "npm", "node_modules", packageName) : undefined;
276
+ }
216
277
  const normalized = trimmed.startsWith("file:") ? trimmed.slice(5) : trimmed;
217
278
  if (normalized === "~") return os.homedir();
218
279
  if (normalized.startsWith("~/")) return path.join(os.homedir(), normalized.slice(2));
package/slash-commands.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
2
4
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
3
5
  import { Key, matchesKey } from "@mariozechner/pi-tui";
4
6
  import { discoverAgents, discoverAgentsAll } from "./agents.ts";
@@ -6,6 +8,7 @@ import { AgentManagerComponent, type ManagerResult } from "./agent-manager.ts";
6
8
  import { SubagentsStatusComponent } from "./subagents-status.ts";
7
9
  import { discoverAvailableSkills } from "./skills.ts";
8
10
  import type { SubagentParamsLike } from "./subagent-executor.ts";
11
+ import { isParallelStep, type ChainStep } from "./settings.ts";
9
12
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
10
13
  import {
11
14
  applySlashUpdate,
@@ -20,6 +23,7 @@ import {
20
23
  SLASH_SUBAGENT_RESPONSE_EVENT,
21
24
  SLASH_SUBAGENT_STARTED_EVENT,
22
25
  SLASH_SUBAGENT_UPDATE_EVENT,
26
+ type SingleResult,
23
27
  type SubagentState,
24
28
  } from "./types.ts";
25
29
 
@@ -194,6 +198,46 @@ function extractSlashMessageText(content: string | Array<{ type?: string; text?:
194
198
  .join("\n");
195
199
  }
196
200
 
201
+ function formatExportPathList(paths: string[]): string {
202
+ return paths.map((file) => `- \`${file}\``).join("\n");
203
+ }
204
+
205
+ function collectResultPaths(results: SingleResult[], getPath: (result: SingleResult) => string | undefined): string[] {
206
+ return results
207
+ .map(getPath)
208
+ .filter((file): file is string => typeof file === "string" && file.length > 0);
209
+ }
210
+
211
+ function buildSlashExportText(response: SlashSubagentResponse): string {
212
+ const output = extractSlashMessageText(response.result.content) || response.errorText || "(no output)";
213
+ const results = response.result.details?.results ?? [];
214
+ const sessionFiles = collectResultPaths(results, (result) => result.sessionFile);
215
+ const savedOutputs = collectResultPaths(results, (result) => result.savedOutputPath);
216
+ const artifactOutputs = collectResultPaths(results, (result) => result.artifactPaths?.outputPath);
217
+ const sections = ["## Subagent result", output];
218
+ if (sessionFiles.length > 0) sections.push("## Child session exports", formatExportPathList(sessionFiles));
219
+ if (savedOutputs.length > 0) sections.push("## Saved outputs", formatExportPathList(savedOutputs));
220
+ if (artifactOutputs.length > 0) sections.push("## Artifact outputs", formatExportPathList(artifactOutputs));
221
+ return sections.join("\n\n");
222
+ }
223
+
224
+ function persistSlashSessionSnapshot(ctx: ExtensionContext): void {
225
+ try {
226
+ if (!ctx.sessionManager) return;
227
+ const sessionManager = ctx.sessionManager as typeof ctx.sessionManager & {
228
+ _rewriteFile?: () => void;
229
+ flushed?: boolean;
230
+ };
231
+ const sessionFile = sessionManager.getSessionFile();
232
+ if (!sessionFile || typeof sessionManager._rewriteFile !== "function") return;
233
+ fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
234
+ sessionManager._rewriteFile();
235
+ sessionManager.flushed = true;
236
+ } catch (error) {
237
+ console.error("Failed to persist slash session snapshot for export:", error);
238
+ }
239
+ }
240
+
197
241
  async function runSlashSubagent(
198
242
  pi: ExtensionAPI,
199
243
  ctx: ExtensionContext,
@@ -208,17 +252,18 @@ async function runSlashSubagent(
208
252
  display: true,
209
253
  details: initialDetails,
210
254
  });
255
+ persistSlashSessionSnapshot(ctx);
211
256
 
212
257
  try {
213
258
  const response = await requestSlashRun(pi, ctx, requestId, params);
214
259
  const finalDetails = finalizeSlashResult(response);
215
- const text = extractSlashMessageText(response.result.content) || response.errorText || "(no output)";
216
260
  pi.sendMessage({
217
261
  customType: SLASH_RESULT_TYPE,
218
- content: text,
219
- display: false,
262
+ content: buildSlashExportText(response),
263
+ display: true,
220
264
  details: finalDetails,
221
265
  });
266
+ persistSlashSessionSnapshot(ctx);
222
267
  if (ctx.hasUI) {
223
268
  ctx.ui.setStatus("subagent-slash", undefined);
224
269
  }
@@ -227,13 +272,14 @@ async function runSlashSubagent(
227
272
  }
228
273
  } catch (error) {
229
274
  const message = error instanceof Error ? error.message : String(error);
230
- const failedDetails = failSlashResult(requestId, params, message === "Cancelled" ? "Cancelled" : message);
275
+ const failedDetails = failSlashResult(requestId, params, message);
231
276
  pi.sendMessage({
232
277
  customType: SLASH_RESULT_TYPE,
233
- content: message,
234
- display: false,
278
+ content: `## Subagent result\n\n${message}`,
279
+ display: true,
235
280
  details: failedDetails,
236
281
  });
282
+ persistSlashSessionSnapshot(ctx);
237
283
  if (ctx.hasUI) {
238
284
  ctx.ui.setStatus("subagent-slash", undefined);
239
285
  }
@@ -263,48 +309,43 @@ async function openAgentManager(
263
309
  );
264
310
  if (!result) return;
265
311
 
312
+ const launchOptions: SubagentParamsLike = {
313
+ clarify: !result.skipClarify && !result.background,
314
+ agentScope: "both",
315
+ ...(result.fork ? { context: "fork" as const } : {}),
316
+ ...(result.background ? { async: true } : {}),
317
+ };
318
+
266
319
  if (result.action === "chain") {
267
320
  const chain = result.agents.map((name, i) => ({
268
321
  agent: name,
269
322
  ...(i === 0 ? { task: result.task } : {}),
270
323
  }));
271
- await runSlashSubagent(pi, ctx, {
272
- chain,
273
- task: result.task,
274
- clarify: true,
275
- agentScope: "both",
276
- });
324
+ await runSlashSubagent(pi, ctx, { chain, task: result.task, ...launchOptions });
277
325
  return;
278
326
  }
279
327
 
280
328
  if (result.action === "launch") {
281
- await runSlashSubagent(pi, ctx, {
282
- agent: result.agent,
283
- task: result.task,
284
- clarify: !result.skipClarify,
285
- agentScope: "both",
286
- });
329
+ await runSlashSubagent(pi, ctx, { agent: result.agent, task: result.task, ...launchOptions });
287
330
  } else if (result.action === "launch-chain") {
288
- const chainParam = result.chain.steps.map((step) => ({
289
- agent: step.agent,
290
- task: step.task || undefined,
291
- output: step.output,
292
- reads: step.reads,
293
- progress: step.progress,
294
- skill: step.skills,
295
- model: step.model,
296
- }));
297
- await runSlashSubagent(pi, ctx, {
298
- chain: chainParam,
299
- task: result.task,
300
- clarify: !result.skipClarify,
301
- agentScope: "both",
331
+ const chainParam = (result.chain.steps as unknown as ChainStep[]).map((step) => {
332
+ if (isParallelStep(step)) return result.worktree ? { ...step, worktree: true } : { ...step };
333
+ return {
334
+ agent: step.agent,
335
+ task: step.task || undefined,
336
+ output: step.output,
337
+ reads: step.reads,
338
+ progress: step.progress,
339
+ skill: step.skill ?? (step as typeof step & { skills?: string[] | false }).skills,
340
+ model: step.model,
341
+ };
302
342
  });
343
+ await runSlashSubagent(pi, ctx, { chain: chainParam, task: result.task, ...launchOptions });
303
344
  } else if (result.action === "parallel") {
304
345
  await runSlashSubagent(pi, ctx, {
305
346
  tasks: result.tasks,
306
- clarify: !result.skipClarify,
307
- agentScope: "both",
347
+ ...launchOptions,
348
+ ...(result.worktree ? { worktree: true } : {}),
308
349
  });
309
350
  }
310
351
  }
@@ -398,16 +439,15 @@ export function registerSlashCommands(
398
439
  });
399
440
 
400
441
  pi.registerCommand("run", {
401
- description: "Run a subagent directly: /run agent[output=file] task [--bg] [--fork]",
442
+ description: "Run a subagent directly: /run agent[output=file] [task] [--bg] [--fork]",
402
443
  getArgumentCompletions: makeAgentCompletions(state, false),
403
444
  handler: async (args, ctx) => {
404
445
  const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
405
446
  const input = cleanedArgs.trim();
406
447
  const firstSpace = input.indexOf(" ");
407
- if (firstSpace === -1) { ctx.ui.notify("Usage: /run <agent> <task> [--bg] [--fork]", "error"); return; }
408
- const { name: agentName, config: inline } = parseAgentToken(input.slice(0, firstSpace));
409
- const task = input.slice(firstSpace + 1).trim();
410
- if (!task) { ctx.ui.notify("Usage: /run <agent> <task> [--bg] [--fork]", "error"); return; }
448
+ if (!input) { ctx.ui.notify("Usage: /run <agent> [task] [--bg] [--fork]", "error"); return; }
449
+ const { name: agentName, config: inline } = parseAgentToken(firstSpace === -1 ? input : input.slice(0, firstSpace));
450
+ const task = firstSpace === -1 ? "" : input.slice(firstSpace + 1).trim();
411
451
 
412
452
  const agents = discoverAgents(state.baseCwd, "both").agents;
413
453
  if (!agents.find((a) => a.name === agentName)) { ctx.ui.notify(`Unknown agent: ${agentName}`, "error"); return; }
@@ -459,8 +499,11 @@ export function registerSlashCommands(
459
499
  const tasks = parsed.steps.map(({ name, config, task: stepTask }) => ({
460
500
  agent: name,
461
501
  task: stepTask ?? parsed.task,
502
+ ...(config.output !== undefined ? { output: config.output } : {}),
503
+ ...(config.reads !== undefined ? { reads: config.reads } : {}),
462
504
  ...(config.model ? { model: config.model } : {}),
463
505
  ...(config.skill !== undefined ? { skill: config.skill } : {}),
506
+ ...(config.progress !== undefined ? { progress: config.progress } : {}),
464
507
  }));
465
508
  const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
466
509
  if (bg) params.async = true;
@@ -278,11 +278,9 @@ export function restoreSlashFinalSnapshots(entries: unknown[]): void {
278
278
  liveSnapshots.clear();
279
279
  finalSnapshots.clear();
280
280
  for (const entry of entries) {
281
- const e = entry as { type?: string; message?: { role?: string; customType?: string; display?: boolean; details?: unknown } };
282
- if (e?.type !== "message") continue;
283
- const m = e.message;
284
- if (!m || m.role !== "custom" || m.customType !== SLASH_RESULT_TYPE || m.display !== false) continue;
285
- const details = resolveSlashMessageDetails(m.details);
281
+ const e = entry as { type?: string; customType?: string; details?: unknown };
282
+ if (e?.type !== "custom_message" || e.customType !== SLASH_RESULT_TYPE) continue;
283
+ const details = resolveSlashMessageDetails(e.details);
286
284
  if (!details) continue;
287
285
  finalSnapshots.set(details.requestId, { result: details.result, version: nextVersion() });
288
286
  }
@@ -14,16 +14,20 @@ import { resolveModelCandidate } from "./model-fallback.ts";
14
14
  import { aggregateParallelOutputs } from "./parallel-utils.ts";
15
15
  import { recordRun } from "./run-history.ts";
16
16
  import {
17
+ buildChainInstructions,
18
+ writeInitialProgressFile,
17
19
  getStepAgents,
18
20
  isParallelStep,
19
21
  resolveStepBehavior,
20
22
  type ChainStep,
23
+ type ResolvedStepBehavior,
21
24
  type SequentialStep,
25
+ type StepOverrides,
22
26
  } from "./settings.ts";
23
27
  import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
24
28
  import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.ts";
25
29
  import { createForkContextResolver } from "./fork-context.ts";
26
- import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "./intercom-bridge.ts";
30
+ import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "./intercom-bridge.ts";
27
31
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "./subagent-control.ts";
28
32
  import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
29
33
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "./utils.ts";
@@ -46,6 +50,7 @@ import {
46
50
  type ControlEvent,
47
51
  type Details,
48
52
  type ExtensionConfig,
53
+ type IntercomEventBus,
49
54
  type MaxOutputConfig,
50
55
  type ResolvedControlConfig,
51
56
  type SingleResult,
@@ -68,6 +73,9 @@ interface TaskParam {
68
73
  task: string;
69
74
  cwd?: string;
70
75
  count?: number;
76
+ output?: string | boolean;
77
+ reads?: string[] | boolean;
78
+ progress?: boolean;
71
79
  model?: string;
72
80
  skill?: string | string[] | boolean;
73
81
  }
@@ -326,7 +334,7 @@ function validateExecutionInput(
326
334
  function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
327
335
  if ((params.chain?.length ?? 0) > 0) return "chain";
328
336
  if ((params.tasks?.length ?? 0) > 0) return "parallel";
329
- if (params.agent && params.task) return "single";
337
+ if (params.agent) return "single";
330
338
  return "single";
331
339
  }
332
340
 
@@ -483,7 +491,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
483
491
  } = data;
484
492
  const hasChain = (params.chain?.length ?? 0) > 0;
485
493
  const hasTasks = (params.tasks?.length ?? 0) > 0;
486
- const hasSingle = Boolean(params.agent && params.task);
494
+ const hasSingle = !hasChain && !hasTasks && Boolean(params.agent);
487
495
  if (!effectiveAsync) return null;
488
496
 
489
497
  if (hasChain && params.chain) {
@@ -544,6 +552,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
544
552
  cwd: task.cwd,
545
553
  ...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),
546
554
  ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
555
+ ...(task.output === true ? (agentConfigs[index]?.output ? { output: agentConfigs[index]!.output } : {}) : task.output !== undefined ? { output: task.output } : {}),
556
+ ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
557
+ ...(task.progress !== undefined ? { progress: task.progress } : {}),
547
558
  }));
548
559
  return executeAsyncChain(id, {
549
560
  chain: [{
@@ -614,7 +625,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
614
625
  const modelOverride = resolveModelCandidate((params.model as string | undefined) ?? a.model, availableModels, currentProvider);
615
626
  return executeAsyncSingle(id, {
616
627
  agent: params.agent!,
617
- task: params.context === "fork" ? wrapForkTask(params.task!) : params.task!,
628
+ task: params.context === "fork" ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
618
629
  agentConfig: a,
619
630
  ctx: asyncCtx,
620
631
  availableModels,
@@ -669,6 +680,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
669
680
  task: params.task,
670
681
  agents,
671
682
  ctx,
683
+ intercomEvents: deps.pi.events,
672
684
  signal,
673
685
  runId,
674
686
  cwd: effectiveCwd,
@@ -741,6 +753,7 @@ interface ForegroundParallelRunInput {
741
753
  taskTexts: string[];
742
754
  agents: AgentConfig[];
743
755
  ctx: ExtensionContext;
756
+ intercomEvents: IntercomEventBus;
744
757
  signal: AbortSignal;
745
758
  runId: string;
746
759
  sessionDirForIndex: (idx?: number) => string | undefined;
@@ -749,12 +762,12 @@ interface ForegroundParallelRunInput {
749
762
  artifactConfig: ArtifactConfig;
750
763
  artifactsDir: string;
751
764
  maxOutput?: MaxOutputConfig;
752
- paramsCwd?: string;
765
+ paramsCwd: string;
753
766
  maxSubagentDepths: number[];
754
767
  availableModels: ModelInfo[];
755
768
  modelOverrides: (string | undefined)[];
756
- skillOverrides: (string[] | false | undefined)[];
757
769
  behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
770
+ firstProgressIndex: number;
758
771
  controlConfig: ResolvedControlConfig;
759
772
  onControlEvent?: (event: ControlEvent) => void;
760
773
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
@@ -822,12 +835,11 @@ function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string):
822
835
 
823
836
  function resolveParallelTaskCwd(
824
837
  task: TaskParam,
825
- paramsCwd: string | undefined,
838
+ paramsCwd: string,
826
839
  worktreeSetup: WorktreeSetup | undefined,
827
840
  index: number,
828
- ): string | undefined {
841
+ ): string {
829
842
  if (worktreeSetup) return worktreeSetup.worktrees[index]!.agentCwd;
830
- if (!paramsCwd) return task.cwd;
831
843
  return resolveChildCwd(paramsCwd, task.cwd);
832
844
  }
833
845
 
@@ -842,11 +854,46 @@ function buildParallelWorktreeSuffix(
842
854
  return formatWorktreeDiffSummary(diffs);
843
855
  }
844
856
 
857
+ function findDuplicateParallelOutputPath(input: {
858
+ tasks: TaskParam[];
859
+ behaviors: ResolvedStepBehavior[];
860
+ paramsCwd: string;
861
+ ctxCwd: string;
862
+ worktreeSetup?: WorktreeSetup;
863
+ }): string | undefined {
864
+ const seen = new Map<string, { index: number; agent: string }>();
865
+ for (let index = 0; index < input.tasks.length; index++) {
866
+ const behavior = input.behaviors[index];
867
+ if (!behavior?.output) continue;
868
+ const task = input.tasks[index]!;
869
+ const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
870
+ const outputPath = resolveSingleOutputPath(behavior.output, input.ctxCwd, taskCwd);
871
+ if (!outputPath) continue;
872
+ const previous = seen.get(outputPath);
873
+ if (previous) {
874
+ return `Parallel tasks ${previous.index + 1} (${previous.agent}) and ${index + 1} (${task.agent}) resolve output to the same path: ${outputPath}. Use distinct output paths.`;
875
+ }
876
+ seen.set(outputPath, { index, agent: task.agent });
877
+ }
878
+ return undefined;
879
+ }
880
+
845
881
  async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Promise<SingleResult[]> {
846
882
  return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => {
847
- const overrideSkills = input.skillOverrides[index];
848
- const effectiveSkills = overrideSkills === undefined ? input.behaviors[index]?.skills : overrideSkills;
883
+ const behavior = input.behaviors[index];
884
+ const effectiveSkills = behavior?.skills;
849
885
  const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
886
+ const readInstructions = behavior
887
+ ? buildChainInstructions({ ...behavior, output: false, progress: false }, taskCwd, false)
888
+ : { prefix: "", suffix: "" };
889
+ const progressInstructions = behavior
890
+ ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.paramsCwd, index === input.firstProgressIndex)
891
+ : { prefix: "", suffix: "" };
892
+ const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd);
893
+ const taskText = injectSingleOutputInstruction(
894
+ `${readInstructions.prefix}${input.taskTexts[index]!}${progressInstructions.suffix}`,
895
+ outputPath,
896
+ );
850
897
  const interruptController = new AbortController();
851
898
  if (input.foregroundControl) {
852
899
  input.foregroundControl.currentAgent = task.agent;
@@ -861,10 +908,13 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
861
908
  return true;
862
909
  };
863
910
  }
864
- return runSync(input.ctx.cwd, input.agents, task.agent, input.taskTexts[index]!, {
911
+ const agentConfig = input.agents.find((agent) => agent.name === task.agent);
912
+ return runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
865
913
  cwd: taskCwd,
866
914
  signal: input.signal,
867
915
  interruptSignal: interruptController.signal,
916
+ allowIntercomDetach: agentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
917
+ intercomEvents: input.intercomEvents,
868
918
  runId: input.runId,
869
919
  index,
870
920
  sessionDir: input.sessionDirForIndex(index),
@@ -873,6 +923,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
873
923
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
874
924
  artifactConfig: input.artifactConfig,
875
925
  maxOutput: input.maxOutput,
926
+ outputPath,
876
927
  maxSubagentDepth: input.maxSubagentDepths[index],
877
928
  controlConfig: input.controlConfig,
878
929
  onControlEvent: input.onControlEvent,
@@ -983,16 +1034,23 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
983
1034
  fullId: `${m.provider}/${m.id}`,
984
1035
  }));
985
1036
  let taskTexts = tasks.map((t) => t.task);
986
- const modelOverrides: (string | undefined)[] = tasks.map((t, i) =>
987
- resolveModelCandidate(t.model ?? agentConfigs[i]?.model, availableModels, currentProvider),
988
- );
989
1037
  const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) =>
990
1038
  normalizeSkillInput(t.skill),
991
1039
  );
1040
+ const behaviorOverrides: StepOverrides[] = tasks.map((task, index) => ({
1041
+ ...(task.output !== undefined ? { output: task.output === true ? agentConfigs[index]?.output ?? false : task.output } : {}),
1042
+ ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
1043
+ ...(task.progress !== undefined ? { progress: task.progress } : {}),
1044
+ ...(skillOverrides[index] !== undefined ? { skills: skillOverrides[index] } : {}),
1045
+ ...(task.model ? { model: task.model } : {}),
1046
+ }));
1047
+ const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
1048
+ resolveModelCandidate(behaviorOverrides[i]?.model ?? agentConfigs[i]?.model, availableModels, currentProvider),
1049
+ );
992
1050
 
993
1051
  if (params.clarify === true && ctx.hasUI) {
994
1052
  const behaviors = agentConfigs.map((c, i) =>
995
- resolveStepBehavior(c, { skills: skillOverrides[i] }),
1053
+ resolveStepBehavior(c, behaviorOverrides[i]!),
996
1054
  );
997
1055
  const availableSkills = discoverAvailableSkills(effectiveCwd);
998
1056
 
@@ -1021,8 +1079,17 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1021
1079
  taskTexts = result.templates;
1022
1080
  for (let i = 0; i < result.behaviorOverrides.length; i++) {
1023
1081
  const override = result.behaviorOverrides[i];
1024
- if (override?.model) modelOverrides[i] = override.model;
1025
- if (override?.skills !== undefined) skillOverrides[i] = override.skills;
1082
+ if (override?.model) {
1083
+ modelOverrides[i] = override.model;
1084
+ behaviorOverrides[i]!.model = override.model;
1085
+ }
1086
+ if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
1087
+ if (override?.reads !== undefined) behaviorOverrides[i]!.reads = override.reads;
1088
+ if (override?.progress !== undefined) behaviorOverrides[i]!.progress = override.progress;
1089
+ if (override?.skills !== undefined) {
1090
+ skillOverrides[i] = override.skills;
1091
+ behaviorOverrides[i]!.skills = override.skills;
1092
+ }
1026
1093
  }
1027
1094
 
1028
1095
  if (result.runInBackground) {
@@ -1046,6 +1113,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1046
1113
  cwd: t.cwd,
1047
1114
  ...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
1048
1115
  ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
1116
+ ...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
1117
+ ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
1118
+ ...(behaviorOverrides[i]?.progress !== undefined ? { progress: behaviorOverrides[i]!.progress } : {}),
1049
1119
  }));
1050
1120
  return executeAsyncChain(id, {
1051
1121
  chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
@@ -1070,7 +1140,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1070
1140
  }
1071
1141
  }
1072
1142
 
1073
- const behaviors = agentConfigs.map((config) => resolveStepBehavior(config, {}));
1143
+ const behaviors = agentConfigs.map((config, index) => resolveStepBehavior(config, behaviorOverrides[index]!));
1144
+ const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
1074
1145
  const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
1075
1146
  const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
1076
1147
  const foregroundControl = deps.state.foregroundControls.get(runId);
@@ -1085,6 +1156,18 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1085
1156
  if (errorResult) return errorResult;
1086
1157
 
1087
1158
  try {
1159
+ const duplicateOutputError = findDuplicateParallelOutputPath({
1160
+ tasks,
1161
+ behaviors,
1162
+ paramsCwd: effectiveCwd,
1163
+ ctxCwd: ctx.cwd,
1164
+ worktreeSetup,
1165
+ });
1166
+ if (duplicateOutputError) return buildParallelModeError(duplicateOutputError);
1167
+
1168
+ const parallelProgressPrecreated = firstProgressIndex !== -1;
1169
+ if (parallelProgressPrecreated) writeInitialProgressFile(effectiveCwd);
1170
+
1088
1171
  if (params.context === "fork") {
1089
1172
  for (let i = 0; i < taskTexts.length; i++) {
1090
1173
  taskTexts[i] = wrapForkTask(taskTexts[i]!);
@@ -1096,6 +1179,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1096
1179
  taskTexts,
1097
1180
  agents,
1098
1181
  ctx,
1182
+ intercomEvents: deps.pi.events,
1099
1183
  signal,
1100
1184
  runId,
1101
1185
  sessionDirForIndex,
@@ -1107,8 +1191,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1107
1191
  paramsCwd: effectiveCwd,
1108
1192
  availableModels,
1109
1193
  modelOverrides,
1110
- skillOverrides,
1111
1194
  behaviors,
1195
+ firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex,
1112
1196
  controlConfig,
1113
1197
  onControlEvent,
1114
1198
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
@@ -1211,7 +1295,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1211
1295
  id: m.id,
1212
1296
  fullId: `${m.provider}/${m.id}`,
1213
1297
  }));
1214
- let task = params.task!;
1298
+ let task = params.task ?? "";
1215
1299
  let modelOverride: string | undefined = resolveModelCandidate(
1216
1300
  (params.model as string | undefined) ?? agentConfig.model,
1217
1301
  availableModels,
@@ -1345,7 +1429,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1345
1429
  cwd: effectiveCwd,
1346
1430
  signal,
1347
1431
  interruptSignal: interruptController.signal,
1348
- allowIntercomDetach: agentConfig.systemPrompt?.includes("Intercom orchestration channel:") === true,
1432
+ allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
1349
1433
  intercomEvents: deps.pi.events,
1350
1434
  runId,
1351
1435
  sessionDir: sessionDirForIndex(0),
@@ -1548,7 +1632,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1548
1632
  const shareEnabled = effectiveParams.share === true;
1549
1633
  const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
1550
1634
  const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
1551
- const hasSingle = Boolean(effectiveParams.agent && effectiveParams.task);
1635
+ const hasSingle = !hasChain && !hasTasks && Boolean(effectiveParams.agent);
1552
1636
  const allowClarifyTaskPrompt = hasChain
1553
1637
  && effectiveParams.clarify === true
1554
1638
  && ctx.hasUI
@@ -1602,6 +1686,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1602
1686
  }
1603
1687
  const sessionDirForIndex = (idx?: number) =>
1604
1688
  path.join(sessionRoot, `run-${idx ?? 0}`);
1689
+ const childSessionFileForIndex = (idx?: number) =>
1690
+ sessionFileForIndex(idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
1605
1691
 
1606
1692
  const onUpdateWithContext = onUpdate
1607
1693
  ? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
@@ -1618,7 +1704,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1618
1704
  shareEnabled,
1619
1705
  sessionRoot,
1620
1706
  sessionDirForIndex,
1621
- sessionFileForIndex,
1707
+ sessionFileForIndex: childSessionFileForIndex,
1622
1708
  artifactConfig,
1623
1709
  artifactsDir,
1624
1710
  backgroundRequestedWhileClarifying,
@@ -51,6 +51,7 @@ import {
51
51
  formatWorktreeTaskCwdConflict,
52
52
  type WorktreeSetup,
53
53
  } from "./worktree.ts";
54
+ import { writeInitialProgressFile } from "./settings.ts";
54
55
 
55
56
  interface SubagentRunConfig {
56
57
  id: string;
@@ -817,6 +818,12 @@ function appendParallelWorktreeSummary(
817
818
  return `${previousOutput}\n\n${diffSummary}`;
818
819
  }
819
820
 
821
+ function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { parallel: SubagentStep[] }>): void {
822
+ const progressPath = path.join(cwd, "progress.md");
823
+ if (!group.parallel.some((task) => task.task.includes(`Update progress at: ${progressPath}`))) return;
824
+ writeInitialProgressFile(cwd);
825
+ }
826
+
820
827
  async function runSubagent(config: SubagentRunConfig): Promise<void> {
821
828
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
822
829
  config;
@@ -1039,6 +1046,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1039
1046
  }
1040
1047
 
1041
1048
  try {
1049
+ if (group.worktree) ensureParallelProgressFile(cwd, group);
1042
1050
  const groupStartTime = Date.now();
1043
1051
  markParallelGroupRunning({
1044
1052
  statusPayload,