pi-subagents 0.18.1 → 0.19.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.19.1] - 2026-04-26
6
+
7
+ ### Added
8
+ - Added `subagent({ action: "doctor" })` and `/subagents-doctor` for read-only subagent environment diagnostics.
9
+ - Added `/run-chain` to launch saved `.chain.md` workflows directly from slash commands with completion, shared task input, and `--bg`/`--fork` support.
10
+
11
+ ## [0.19.0] - 2026-04-26
12
+
13
+ ### Added
14
+ - Added top-level parallel task support for per-task `output`, `reads`, and `progress`, including `/parallel` inline forwarding and async preservation.
15
+ - Added `/agents` launch toggles for forked context, background execution, and worktree-isolated parallel runs.
16
+ - Added a read-only detail view to `/subagents-status` for inspecting selected async runs, including recent events, output tails, and useful run paths.
17
+ - Added a packaged `/parallel-review` prompt template for launching fresh-context adversarial review subagents.
18
+
19
+ ### Fixed
20
+ - Parallel and chain child runs now detach cleanly when a child uses intercom, preventing incoming handoff messages from aborting the parent foreground run.
21
+
5
22
  ## [0.18.1] - 2026-04-25
6
23
 
7
24
  ### Changed
package/README.md CHANGED
@@ -977,9 +977,9 @@ The inherited thread is reference-only. Do not continue that conversation or sen
977
977
  Use `intercom` only to coordinate with the orchestrator session `{orchestratorTarget}`.
978
978
 
979
979
  - Need a decision or you're blocked: `intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })`
980
- - Need to report progress or a completion handoff: `intercom({ action: "send", to: "{orchestratorTarget}", message: "DONE: <summary>" })`
980
+ - Blocked or explicitly asked to send progress: `intercom({ action: "send", to: "{orchestratorTarget}", message: "UPDATE: <summary>" })`
981
981
 
982
- If no upstream coordination is needed, continue the task normally and return a focused task result.
982
+ Do not send routine completion handoffs through intercom. If no coordination is needed, return a focused task result.
983
983
  ```
984
984
 
985
985
  Bridge activation also requires all of the following:
@@ -2,6 +2,7 @@ import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
3
  import type { ChainConfig, ChainStepConfig } from "./agents.ts";
4
4
  import { row, renderFooter, renderHeader, formatPath, formatScrollInfo } from "./render-helpers.ts";
5
+ import { isParallelStep, type ChainStep } from "./settings.ts";
5
6
 
6
7
  export interface ChainDetailState {
7
8
  scrollOffset: number;
@@ -14,11 +15,24 @@ export type ChainDetailAction =
14
15
 
15
16
  const CHAIN_DETAIL_VIEWPORT_HEIGHT = 12;
16
17
 
17
- export function buildDependencyMap(steps: ChainStepConfig[]): Map<number, number[]> {
18
+ type DetailChainStep = ChainStepConfig | ChainStep;
19
+
20
+ export function buildDependencyMap(steps: DetailChainStep[]): Map<number, number[]> {
18
21
  const outputMap = new Map<string, number>();
19
22
  const deps = new Map<number, number[]>();
20
23
  for (let i = 0; i < steps.length; i++) {
21
24
  const step = steps[i]!;
25
+ if (isParallelStep(step as ChainStep)) {
26
+ const reads = step.parallel.flatMap((task) => Array.isArray(task.reads) ? task.reads : []);
27
+ const sources = reads
28
+ .map((file) => outputMap.get(file))
29
+ .filter((idx): idx is number => idx !== undefined);
30
+ if (sources.length > 0) deps.set(i, [...new Set(sources)]);
31
+ for (const task of step.parallel) {
32
+ if (typeof task.output === "string" && task.output.length > 0) outputMap.set(task.output, i);
33
+ }
34
+ continue;
35
+ }
22
36
  if (typeof step.output === "string" && step.output.length > 0) outputMap.set(step.output, i);
23
37
  if (Array.isArray(step.reads) && step.reads.length > 0) {
24
38
  const sources = step.reads
@@ -33,21 +47,51 @@ export function buildDependencyMap(steps: ChainStepConfig[]): Map<number, number
33
47
  function buildChainDetailLines(chain: ChainConfig, width: number): string[] {
34
48
  const contentWidth = width - 3;
35
49
  const lines: string[] = [];
36
- const dependencyMap = buildDependencyMap(chain.steps);
50
+ const steps = chain.steps as DetailChainStep[];
51
+ const dependencyMap = buildDependencyMap(steps);
37
52
  lines.push(truncateToWidth(chain.description, contentWidth));
38
53
  lines.push("");
39
54
  lines.push(truncateToWidth(`File: ${formatPath(chain.filePath)}`, contentWidth));
40
55
  lines.push("");
41
56
  lines.push(truncateToWidth("── Flow ──", contentWidth));
42
57
 
43
- for (let i = 0; i < chain.steps.length; i++) {
44
- const step = chain.steps[i]!;
58
+ for (let i = 0; i < steps.length; i++) {
59
+ const step = steps[i]!;
60
+ const sources = dependencyMap.get(i);
61
+ const fromText = sources && sources.length > 0 ? ` (from ${sources.map((s) => s + 1).join(", ")})` : "";
62
+ if (isParallelStep(step as ChainStep)) {
63
+ lines.push(truncateToWidth(` ${i + 1} Parallel: ${step.parallel.map((task) => task.agent).join(" + ")}`, contentWidth));
64
+ if (step.concurrency !== undefined) lines.push(truncateToWidth(` concurrency: ${step.concurrency}`, contentWidth));
65
+ if (step.failFast !== undefined) lines.push(truncateToWidth(` fail fast: ${step.failFast ? "on" : "off"}`, contentWidth));
66
+ if (step.worktree !== undefined) lines.push(truncateToWidth(` worktree: ${step.worktree ? "on" : "off"}`, contentWidth));
67
+ for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
68
+ const task = step.parallel[taskIndex]!;
69
+ lines.push(truncateToWidth(` ${taskIndex + 1}. ${task.agent}`, contentWidth));
70
+ const taskPreview = (task.task ?? "").split("\n")[0] ?? "";
71
+ if (taskPreview) lines.push(truncateToWidth(` task: ${taskPreview}`, contentWidth));
72
+ if (Array.isArray(task.reads) && task.reads.length > 0) lines.push(truncateToWidth(` ← reads: ${task.reads.join(", ")}${fromText}`, contentWidth));
73
+ else if (task.reads === false) lines.push(truncateToWidth(" ← reads: (disabled)", contentWidth));
74
+ if (typeof task.output === "string" && task.output.length > 0) lines.push(truncateToWidth(` → output: ${task.output}`, contentWidth));
75
+ else if (task.output === false) lines.push(truncateToWidth(" → output: (disabled)", contentWidth));
76
+ if (task.model) lines.push(truncateToWidth(` model: ${task.model}`, contentWidth));
77
+ if (task.skill !== undefined) {
78
+ const skillsText =
79
+ task.skill === false
80
+ ? "(disabled)"
81
+ : Array.isArray(task.skill)
82
+ ? (task.skill.length > 0 ? task.skill.join(", ") : "(none)")
83
+ : task.skill;
84
+ lines.push(truncateToWidth(` skills: ${skillsText}`, contentWidth));
85
+ }
86
+ if (task.progress !== undefined) lines.push(truncateToWidth(` progress: ${task.progress ? "on" : "off"}`, contentWidth));
87
+ }
88
+ lines.push("");
89
+ continue;
90
+ }
45
91
  lines.push(truncateToWidth(` ${i + 1} ${step.agent}`, contentWidth));
46
92
  const taskPreview = step.task.split("\n")[0] ?? "";
47
93
  lines.push(truncateToWidth(` task: ${taskPreview || "(none)"}`, contentWidth));
48
94
  if (Array.isArray(step.reads) && step.reads.length > 0) {
49
- const sources = dependencyMap.get(i);
50
- const fromText = sources && sources.length > 0 ? ` (from ${sources.map((s) => s + 1).join(", ")})` : "";
51
95
  lines.push(truncateToWidth(` ← reads: ${step.reads.join(", ")}${fromText}`, contentWidth));
52
96
  } else if (step.reads === false) {
53
97
  lines.push(truncateToWidth(" ← reads: (disabled)", contentWidth));
@@ -180,12 +180,19 @@ export function renderDetail(
180
180
  return lines;
181
181
  }
182
182
 
183
+ export interface LaunchToggleState {
184
+ fork: boolean;
185
+ background: boolean;
186
+ worktree?: boolean;
187
+ }
188
+
183
189
  export function renderTaskInput(
184
190
  title: string,
185
191
  editor: TextEditorState,
186
192
  skipClarify: boolean,
187
193
  width: number,
188
194
  theme: Theme,
195
+ launchToggles?: LaunchToggleState,
189
196
  ): string[] {
190
197
  const lines: string[] = [];
191
198
  lines.push(renderHeader(` ${title} `, width, theme));
@@ -209,8 +216,14 @@ export function renderTaskInput(
209
216
  lines.push(row(` ${bottom}`, width, theme));
210
217
 
211
218
  lines.push(row("", width, theme));
212
- const enterLabel = skipClarify ? "quick run" : "run";
213
219
  const quickLabel = skipClarify ? "on" : "off";
214
- lines.push(renderFooter(` [enter] ${enterLabel} [tab] quick: ${quickLabel} [esc] cancel `, width, theme));
220
+ const footerParts = ["[enter] run", `[tab] quick:${quickLabel}`];
221
+ if (launchToggles) {
222
+ footerParts.push(`[ctrl+f] fork:${launchToggles.fork ? "on" : "off"}`);
223
+ footerParts.push(`[ctrl+b] bg:${launchToggles.background ? "on" : "off"}`);
224
+ if (launchToggles.worktree !== undefined) footerParts.push(`[ctrl+w] worktree:${launchToggles.worktree ? "on" : "off"}`);
225
+ }
226
+ footerParts.push("[esc]");
227
+ lines.push(renderFooter(` ${footerParts.join(" ")} `, width, theme));
215
228
  return lines;
216
229
  }
package/agent-manager.ts CHANGED
@@ -20,19 +20,20 @@ import { TEMPLATE_ITEMS, type AgentTemplate, type TemplateItem } from "./agent-t
20
20
  import { parseChain, serializeChain } from "./chain-serializer.ts";
21
21
  import { renderList, handleListInput, type ListAgent, type ListState, type ListAction } from "./agent-manager-list.ts";
22
22
  import { createParallelState, handleParallelInput, renderParallel, formatParallelTitle, type ParallelState, type AgentOption } from "./agent-manager-parallel.ts";
23
- import { renderDetail, handleDetailInput, renderTaskInput, type DetailState, type DetailAction } from "./agent-manager-detail.ts";
23
+ import { renderDetail, handleDetailInput, renderTaskInput, type DetailState, type DetailAction, type LaunchToggleState } from "./agent-manager-detail.ts";
24
24
  import { renderChainDetail, handleChainDetailInput, type ChainDetailAction, type ChainDetailState } from "./agent-manager-chain-detail.ts";
25
25
  import { createEditState, handleEditInput, renderEdit, type EditField, type EditScreen, type EditState, type ModelInfo, type SkillInfo } from "./agent-manager-edit.ts";
26
26
  import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
27
27
  import type { TextEditorState } from "./text-editor.ts";
28
28
  import { loadRunsForAgent } from "./run-history.ts";
29
29
  import { pad, row, renderHeader, renderFooter } from "./render-helpers.ts";
30
+ import { isParallelStep, type ChainStep } from "./settings.ts";
30
31
 
31
32
  export type ManagerResult =
32
- | { action: "launch"; agent: string; task: string; skipClarify?: boolean }
33
- | { action: "chain"; agents: string[]; task: string; skipClarify?: boolean }
34
- | { action: "parallel"; tasks: Array<{ agent: string; task: string }>; skipClarify?: boolean }
35
- | { action: "launch-chain"; chain: ChainConfig; task: string; skipClarify?: boolean }
33
+ | { action: "launch"; agent: string; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean }
34
+ | { action: "chain"; agents: string[]; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean }
35
+ | { action: "parallel"; tasks: Array<{ agent: string; task: string }>; skipClarify?: boolean; fork?: boolean; background?: boolean; worktree?: boolean }
36
+ | { action: "launch-chain"; chain: ChainConfig; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean; worktree?: boolean }
36
37
  | undefined;
37
38
 
38
39
  export interface AgentData { builtin: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[]; chains: ChainConfig[]; userDir: string; projectDir: string | null; userSettingsPath: string; projectSettingsPath: string | null; cwd: string; }
@@ -69,7 +70,27 @@ function cloneConfig(config: AgentConfig): AgentConfig {
69
70
  : undefined,
70
71
  };
71
72
  }
72
- function cloneChainConfig(config: ChainConfig): ChainConfig { return { ...config, steps: config.steps.map((step) => ({ ...step, reads: Array.isArray(step.reads) ? [...step.reads] : step.reads, skills: Array.isArray(step.skills) ? [...step.skills] : step.skills })), extraFields: config.extraFields ? { ...config.extraFields } : undefined }; }
73
+ function cloneChainConfig(config: ChainConfig): ChainConfig {
74
+ const steps = (config.steps as unknown as ChainStep[]).map((step) => {
75
+ if (isParallelStep(step)) {
76
+ return {
77
+ ...step,
78
+ parallel: step.parallel.map((task) => ({
79
+ ...task,
80
+ reads: Array.isArray(task.reads) ? [...task.reads] : task.reads,
81
+ skill: Array.isArray(task.skill) ? [...task.skill] : task.skill,
82
+ })),
83
+ };
84
+ }
85
+ return {
86
+ ...step,
87
+ reads: Array.isArray(step.reads) ? [...step.reads] : step.reads,
88
+ ...(Array.isArray((step as typeof step & { skills?: string[] | false }).skills) ? { skills: [...(step as typeof step & { skills: string[] }).skills] } : { skills: (step as typeof step & { skills?: false }).skills }),
89
+ ...(Array.isArray(step.skill) ? { skill: [...step.skill] } : { skill: step.skill }),
90
+ };
91
+ });
92
+ return { ...config, steps: steps as unknown as ChainConfig["steps"], extraFields: config.extraFields ? { ...config.extraFields } : undefined };
93
+ }
73
94
  function slugTemplateName(name: string): string { return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); }
74
95
  function nextSelectableIndex(items: TemplateItem[], current: number, direction: 1 | -1): number { let next = current + direction; while (next >= 0 && next < items.length && items[next]!.type === "separator") next += direction; if (next < 0 || next >= items.length) return current; return next; }
75
96
  const CHAIN_EDIT_VIEWPORT = 10;
@@ -90,6 +111,9 @@ export class AgentManagerComponent implements Component {
90
111
  private chainEditState: { editor: TextEditorState; error?: string } | null = null;
91
112
  private taskEditor: TextEditorState = createEditorState();
92
113
  private skipClarify = false;
114
+ private launchFork = false;
115
+ private launchBackground = false;
116
+ private launchWorktree = false;
93
117
  private chainAgentIds: string[] = [];
94
118
  private chainLaunchId: string | null = null;
95
119
  private parallelMode = false;
@@ -187,10 +211,21 @@ export class AgentManagerComponent implements Component {
187
211
  this.parallelState = createParallelState(names);
188
212
  this.screen = "parallel-builder";
189
213
  }
214
+ private resetLaunchToggles(): void { this.launchFork = false; this.launchBackground = false; this.launchWorktree = false; }
215
+ private enterParallelTaskInput(): void {
216
+ this.chainAgentIds = [];
217
+ this.chainLaunchId = null;
218
+ this.parallelMode = true;
219
+ this.taskBackScreen = "parallel-builder";
220
+ this.taskEditor = createEditorState();
221
+ this.skipClarify = true;
222
+ this.resetLaunchToggles();
223
+ this.screen = "task-input";
224
+ }
190
225
  private enterTaskInput(ids: string[], backScreen: ManagerScreen = "list"): void {
191
- this.chainAgentIds = ids; this.chainLaunchId = null; this.parallelMode = false; this.taskBackScreen = backScreen; this.taskEditor = createEditorState(); this.skipClarify = true; this.screen = "task-input";
226
+ this.chainAgentIds = ids; this.chainLaunchId = null; this.parallelMode = false; this.taskBackScreen = backScreen; this.taskEditor = createEditorState(); this.skipClarify = true; this.resetLaunchToggles(); this.screen = "task-input";
192
227
  }
193
- private enterSavedChainLaunch(entry: ChainEntry): void { this.chainLaunchId = entry.id; this.chainAgentIds = []; this.parallelMode = false; this.taskBackScreen = "chain-detail"; this.taskEditor = createEditorState(); this.skipClarify = true; this.screen = "task-input"; }
228
+ private enterSavedChainLaunch(entry: ChainEntry): void { this.chainLaunchId = entry.id; this.chainAgentIds = []; this.parallelMode = false; this.taskBackScreen = "chain-detail"; this.taskEditor = createEditorState(); this.skipClarify = true; this.resetLaunchToggles(); this.screen = "task-input"; }
194
229
  private enterTemplateSelect(): void { this.templateCursor = TEMPLATE_ITEMS.findIndex((item) => item.type !== "separator"); if (this.templateCursor < 0) this.templateCursor = 0; this.screen = "template-select"; }
195
230
 
196
231
  private enterChainEdit(entry: ChainEntry): void {
@@ -276,6 +311,27 @@ export class AgentManagerComponent implements Component {
276
311
  catch (err) { state.error = err instanceof Error ? err.message : "Failed to save chain."; return false; }
277
312
  }
278
313
 
314
+ private canToggleLaunchWorktree(): boolean {
315
+ if (this.parallelMode && this.parallelState) return true;
316
+ if (!this.chainLaunchId) return false;
317
+ const chainEntry = this.getChainEntry(this.chainLaunchId);
318
+ return chainEntry ? (chainEntry.config.steps as unknown as ChainStep[]).some(isParallelStep) : false;
319
+ }
320
+ private launchFlags(): { fork?: boolean; background?: boolean; worktree?: boolean } {
321
+ return {
322
+ ...(this.launchFork ? { fork: true } : {}),
323
+ ...(this.launchBackground ? { background: true } : {}),
324
+ ...(this.launchWorktree && this.canToggleLaunchWorktree() ? { worktree: true } : {}),
325
+ };
326
+ }
327
+ private launchToggleState(): LaunchToggleState {
328
+ return {
329
+ fork: this.launchFork,
330
+ background: this.launchBackground,
331
+ ...(this.canToggleLaunchWorktree() ? { worktree: this.launchWorktree } : {}),
332
+ };
333
+ }
334
+
279
335
  private handleTemplateSelectInput(data: string): void {
280
336
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.screen = "list"; this.tui.requestRender(); return; }
281
337
  if (matchesKey(data, "up")) { this.templateCursor = nextSelectableIndex(TEMPLATE_ITEMS, this.templateCursor, -1); this.tui.requestRender(); return; }
@@ -476,13 +532,7 @@ export class AgentManagerComponent implements Component {
476
532
  const agentOptions: AgentOption[] = this.agents.map((e) => ({ name: e.config.name, description: e.config.description, model: e.config.model }));
477
533
  const pAction = handleParallelInput(this.parallelState, agentOptions, data, this.overlayWidth);
478
534
  if (pAction?.type === "proceed") {
479
- this.chainAgentIds = [];
480
- this.chainLaunchId = null;
481
- this.parallelMode = true;
482
- this.taskBackScreen = "parallel-builder";
483
- this.taskEditor = createEditorState();
484
- this.skipClarify = true;
485
- this.screen = "task-input";
535
+ this.enterParallelTaskInput();
486
536
  } else if (pAction?.type === "back") {
487
537
  this.parallelState = null;
488
538
  this.parallelMode = false;
@@ -493,28 +543,31 @@ export class AgentManagerComponent implements Component {
493
543
  }
494
544
  case "task-input": {
495
545
  if (matchesKey(data, "tab")) { this.skipClarify = !this.skipClarify; this.tui.requestRender(); return; }
546
+ if (matchesKey(data, "ctrl+f")) { this.launchFork = !this.launchFork; this.tui.requestRender(); return; }
547
+ if (matchesKey(data, "ctrl+b")) { this.launchBackground = !this.launchBackground; this.tui.requestRender(); return; }
548
+ if (matchesKey(data, "ctrl+w") && this.canToggleLaunchWorktree()) { this.launchWorktree = !this.launchWorktree; this.tui.requestRender(); return; }
496
549
  const innerW = this.overlayWidth - 2; const boxInnerWidth = Math.max(10, innerW - 4); const nextState = handleEditorInput(this.taskEditor, data, boxInnerWidth);
497
550
  if (nextState) { this.taskEditor = nextState; this.tui.requestRender(); return; }
498
551
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.screen = this.taskBackScreen; this.tui.requestRender(); return; }
499
552
  if (matchesKey(data, "return")) {
500
553
  if (this.chainLaunchId) {
501
554
  const chainEntry = this.getChainEntry(this.chainLaunchId); if (!chainEntry) { this.screen = "list"; this.tui.requestRender(); return; }
502
- this.done({ action: "launch-chain", chain: cloneChainConfig(chainEntry.config), task: this.taskEditor.buffer, skipClarify: this.skipClarify }); return;
555
+ this.done({ action: "launch-chain", chain: cloneChainConfig(chainEntry.config), task: this.taskEditor.buffer, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
503
556
  } else if (this.parallelMode && this.parallelState) {
504
557
  const sharedTask = this.taskEditor.buffer;
505
558
  const tasks = this.parallelState.slots.map((slot) => ({ agent: slot.agentName, task: slot.customTask || sharedTask }));
506
- this.done({ action: "parallel", tasks, skipClarify: this.skipClarify }); return;
559
+ this.done({ action: "parallel", tasks, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
507
560
  }
508
561
  if (this.chainAgentIds.length > 1) {
509
562
  const agents = this.chainAgentIds
510
563
  .map((id) => this.getAgentEntry(id)?.config.name)
511
564
  .filter((name): name is string => Boolean(name));
512
565
  if (agents.length !== this.chainAgentIds.length) { this.screen = "list"; this.tui.requestRender(); return; }
513
- this.done({ action: "chain", agents, task: this.taskEditor.buffer, skipClarify: this.skipClarify }); return;
566
+ this.done({ action: "chain", agents, task: this.taskEditor.buffer, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
514
567
  }
515
568
  const name = this.getAgentEntry(this.chainAgentIds[0] ?? null)?.config.name;
516
569
  if (!name) { this.screen = "list"; this.tui.requestRender(); return; }
517
- this.done({ action: "launch", agent: name, task: this.taskEditor.buffer, skipClarify: this.skipClarify }); return;
570
+ this.done({ action: "launch", agent: name, task: this.taskEditor.buffer, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
518
571
  }
519
572
  return;
520
573
  }
@@ -628,16 +681,16 @@ export class AgentManagerComponent implements Component {
628
681
  return renderParallel(this.parallelState, agentOptions, w, this.theme);
629
682
  }
630
683
  case "task-input": {
631
- if (this.chainLaunchId) { const entry = this.getChainEntry(this.chainLaunchId); const title = entry ? `Chain: ${entry.config.name}` : "Chain"; return renderTaskInput(title, this.taskEditor, this.skipClarify, w, this.theme); }
632
- if (this.parallelMode && this.parallelState) return renderTaskInput(formatParallelTitle(this.parallelState.slots), this.taskEditor, this.skipClarify, w, this.theme);
684
+ if (this.chainLaunchId) { const entry = this.getChainEntry(this.chainLaunchId); const title = entry ? `Chain: ${entry.config.name}` : "Chain"; return renderTaskInput(title, this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState()); }
685
+ if (this.parallelMode && this.parallelState) return renderTaskInput(formatParallelTitle(this.parallelState.slots), this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState());
633
686
  if (this.chainAgentIds.length > 1) {
634
687
  const names = this.chainAgentIds
635
688
  .map((id) => this.getAgentEntry(id)?.config.name)
636
689
  .filter((name): name is string => Boolean(name));
637
- return renderTaskInput(`Chain: ${names.join(" → ")}`, this.taskEditor, this.skipClarify, w, this.theme);
690
+ return renderTaskInput(`Chain: ${names.join(" → ")}`, this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState());
638
691
  }
639
692
  const name = this.getAgentEntry(this.chainAgentIds[0] ?? null)?.config.name ?? "Agent";
640
- return renderTaskInput(`Run: ${name}`, this.taskEditor, this.skipClarify, w, this.theme);
693
+ return renderTaskInput(`Run: ${name}`, this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState());
641
694
  }
642
695
  case "confirm-delete": return this.renderConfirmDelete(w);
643
696
  case "name-input": return this.renderNameInput(w);
@@ -12,12 +12,13 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
12
  import type { AgentConfig } from "./agents.ts";
13
13
  import { applyThinkingSuffix } from "./pi-args.ts";
14
14
  import { injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
15
- import { isParallelStep, resolveStepBehavior, type ChainStep, type SequentialStep, type StepOverrides } from "./settings.ts";
15
+ import { buildChainInstructions, isParallelStep, resolveStepBehavior, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "./settings.ts";
16
16
  import type { RunnerStep } from "./parallel-utils.ts";
17
17
  import { resolvePiPackageRoot } from "./pi-spawn.ts";
18
18
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "./skills.ts";
19
19
  import { resolveChildCwd } from "./utils.ts";
20
20
  import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "./model-fallback.ts";
21
+ import { resolveExpectedWorktreeAgentCwd } from "./worktree.ts";
21
22
  import {
22
23
  type ArtifactConfig,
23
24
  type Details,
@@ -221,12 +222,22 @@ export function executeAsyncChain(
221
222
  };
222
223
  }
223
224
 
224
- const buildSeqStep = (s: SequentialStep, sessionFile?: string) => {
225
+ let progressInstructionCreated = false;
226
+ const buildStepOverrides = (s: SequentialStep): StepOverrides => {
227
+ const stepSkillInput = normalizeSkillInput(s.skill);
228
+ return {
229
+ ...(s.output !== undefined ? { output: s.output } : {}),
230
+ ...(s.reads !== undefined ? { reads: s.reads } : {}),
231
+ ...(s.progress !== undefined ? { progress: s.progress } : {}),
232
+ ...(stepSkillInput !== undefined ? { skills: stepSkillInput } : {}),
233
+ ...(s.model ? { model: s.model } : {}),
234
+ };
235
+ };
236
+ const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior) => {
225
237
  const a = agents.find((x) => x.name === s.agent)!;
226
238
  const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
227
- const stepSkillInput = normalizeSkillInput(s.skill);
228
- const stepOverrides: StepOverrides = { skills: stepSkillInput };
229
- const behavior = resolveStepBehavior(a, stepOverrides, chainSkills);
239
+ const instructionCwd = behaviorCwd ?? stepCwd;
240
+ const behavior = resolvedBehavior ?? resolveStepBehavior(a, buildStepOverrides(s), chainSkills);
230
241
  const skillNames = behavior.skills === false ? [] : behavior.skills;
231
242
  const { resolved: resolvedSkills } = resolveSkillsWithFallback(skillNames, stepCwd, ctx.cwd);
232
243
 
@@ -236,16 +247,20 @@ export function executeAsyncChain(
236
247
  systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
237
248
  }
238
249
 
239
- const outputPath = resolveSingleOutputPath(s.output, ctx.cwd, stepCwd);
240
- const task = injectSingleOutputInstruction(s.task ?? "{previous}", outputPath);
250
+ const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false);
251
+ const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
252
+ if (behavior.progress) progressInstructionCreated = true;
253
+ const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, runnerCwd, isFirstProgressAgent);
254
+ const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd);
255
+ const task = injectSingleOutputInstruction(`${readInstructions.prefix}${s.task ?? "{previous}"}${progressInstructions.suffix}`, outputPath);
241
256
 
242
- const primaryModel = resolveModelCandidate(s.model ?? a.model, availableModels, ctx.currentModelProvider);
257
+ const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
243
258
  return {
244
259
  agent: s.agent,
245
260
  task,
246
261
  cwd: stepCwd,
247
262
  model: applyThinkingSuffix(primaryModel, a.thinking),
248
- modelCandidates: buildModelCandidates(s.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
263
+ modelCandidates: buildModelCandidates(behavior.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
249
264
  applyThinkingSuffix(candidate, a.thinking),
250
265
  ),
251
266
  tools: a.tools,
@@ -269,17 +284,29 @@ export function executeAsyncChain(
269
284
  return sessionFile;
270
285
  };
271
286
 
272
- const steps: RunnerStep[] = chain.map((s) => {
287
+ const steps: RunnerStep[] = chain.map((s, stepIndex) => {
273
288
  if (isParallelStep(s)) {
289
+ const parallelBehaviors = s.parallel.map((task) => {
290
+ const agent = agents.find((candidate) => candidate.name === task.agent)!;
291
+ return resolveStepBehavior(agent, buildStepOverrides(task), chainSkills);
292
+ });
293
+ const progressPrecreated = parallelBehaviors.some((behavior) => behavior.progress);
294
+ if (progressPrecreated) {
295
+ if (!s.worktree) writeInitialProgressFile(runnerCwd);
296
+ progressInstructionCreated = true;
297
+ }
274
298
  return {
275
- parallel: s.parallel.map((t) => buildSeqStep({
276
- agent: t.agent,
277
- task: t.task,
278
- cwd: t.cwd,
279
- skill: t.skill,
280
- model: t.model,
281
- output: t.output,
282
- }, nextSessionFile())),
299
+ parallel: s.parallel.map((t, taskIndex) => {
300
+ let behaviorCwd: string | undefined;
301
+ if (s.worktree) {
302
+ try {
303
+ behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, taskIndex);
304
+ } catch {
305
+ behaviorCwd = undefined;
306
+ }
307
+ }
308
+ return buildSeqStep(t, nextSessionFile(), behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex]);
309
+ }),
283
310
  concurrency: s.concurrency,
284
311
  failFast: s.failFast,
285
312
  worktree: s.worktree,
@@ -15,6 +15,7 @@ import {
15
15
  resolveStepBehavior,
16
16
  resolveParallelBehaviors,
17
17
  buildChainInstructions,
18
+ writeInitialProgressFile,
18
19
  createParallelDirs,
19
20
  aggregateParallelOutputs,
20
21
  isParallelStep,
@@ -26,6 +27,7 @@ import {
26
27
  type ResolvedTemplates,
27
28
  } from "./settings.ts";
28
29
  import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
30
+ import { INTERCOM_BRIDGE_MARKER } from "./intercom-bridge.ts";
29
31
  import { runSync } from "./execution.ts";
30
32
  import { buildChainSummary } from "./formatters.ts";
31
33
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "./utils.ts";
@@ -46,6 +48,7 @@ import {
46
48
  type ArtifactPaths,
47
49
  type ControlEvent,
48
50
  type Details,
51
+ type IntercomEventBus,
49
52
  type ResolvedControlConfig,
50
53
  type SingleResult,
51
54
  MAX_CONCURRENCY,
@@ -75,6 +78,7 @@ interface ParallelChainRunInput {
75
78
  prev: string;
76
79
  originalTask: string;
77
80
  ctx: ExtensionContext;
81
+ intercomEvents?: IntercomEventBus;
78
82
  cwd?: string;
79
83
  runId: string;
80
84
  globalTaskIndex: number;
@@ -134,8 +138,7 @@ function ensureParallelProgressFile(
134
138
  if (progressCreated || !parallelBehaviors.some((behavior) => behavior.progress)) {
135
139
  return progressCreated;
136
140
  }
137
- const progressPath = path.join(chainDir, "progress.md");
138
- fs.writeFileSync(progressPath, "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n");
141
+ writeInitialProgressFile(chainDir);
139
142
  return true;
140
143
  }
141
144
 
@@ -221,6 +224,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
221
224
  cwd: taskCwd,
222
225
  signal: input.signal,
223
226
  interruptSignal: interruptController.signal,
227
+ allowIntercomDetach: taskAgentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
228
+ intercomEvents: input.intercomEvents,
224
229
  runId: input.runId,
225
230
  index: input.globalTaskIndex + taskIndex,
226
231
  sessionDir: input.sessionDirForIndex(input.globalTaskIndex + taskIndex),
@@ -287,6 +292,7 @@ export interface ChainExecutionParams {
287
292
  task?: string;
288
293
  agents: AgentConfig[];
289
294
  ctx: ExtensionContext;
295
+ intercomEvents?: IntercomEventBus;
290
296
  signal?: AbortSignal;
291
297
  runId: string;
292
298
  cwd?: string;
@@ -352,6 +358,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
352
358
  controlConfig,
353
359
  childIntercomTarget,
354
360
  foregroundControl,
361
+ intercomEvents,
355
362
  chainSkills: chainSkillsParam,
356
363
  chainDir: chainDirBase,
357
364
  } = params;
@@ -536,6 +543,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
536
543
  prev,
537
544
  originalTask,
538
545
  ctx,
546
+ intercomEvents,
539
547
  cwd,
540
548
  runId,
541
549
  globalTaskIndex,
@@ -709,6 +717,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
709
717
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
710
718
  signal,
711
719
  interruptSignal: interruptController.signal,
720
+ allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
721
+ intercomEvents,
712
722
  runId,
713
723
  index: globalTaskIndex,
714
724
  sessionDir: sessionDirForIndex(globalTaskIndex),