pi-subagents 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +17 -79
  3. package/agents/reviewer.md +2 -2
  4. package/package.json +1 -1
  5. package/prompts/parallel-cleanup.md +11 -1
  6. package/prompts/parallel-review.md +11 -1
  7. package/skills/pi-subagents/SKILL.md +29 -13
  8. package/src/agents/agent-serializer.ts +0 -42
  9. package/src/agents/agents.ts +1 -1
  10. package/src/extension/index.ts +14 -8
  11. package/src/extension/schemas.ts +1 -1
  12. package/src/intercom/intercom-bridge.ts +4 -1
  13. package/src/intercom/result-intercom.ts +8 -3
  14. package/src/runs/background/async-execution.ts +10 -5
  15. package/src/runs/background/async-resume.ts +57 -31
  16. package/src/runs/background/async-status.ts +16 -50
  17. package/src/runs/background/result-watcher.ts +3 -1
  18. package/src/runs/background/run-status.ts +28 -26
  19. package/src/runs/background/stale-run-reconciler.ts +3 -0
  20. package/src/runs/background/subagent-runner.ts +21 -7
  21. package/src/runs/foreground/chain-clarify.ts +183 -218
  22. package/src/runs/foreground/chain-execution.ts +55 -21
  23. package/src/runs/foreground/execution.ts +6 -3
  24. package/src/runs/foreground/subagent-executor.ts +152 -20
  25. package/src/runs/shared/single-output.ts +21 -6
  26. package/src/shared/settings.ts +19 -0
  27. package/src/shared/status-format.ts +49 -0
  28. package/src/shared/types.ts +18 -5
  29. package/src/slash/slash-commands.ts +1 -74
  30. package/src/tui/render.ts +37 -61
  31. package/src/agents/agent-templates.ts +0 -60
  32. package/src/manager-ui/agent-manager-chain-detail.ts +0 -164
  33. package/src/manager-ui/agent-manager-detail.ts +0 -235
  34. package/src/manager-ui/agent-manager-edit.ts +0 -456
  35. package/src/manager-ui/agent-manager-list.ts +0 -283
  36. package/src/manager-ui/agent-manager-parallel.ts +0 -302
  37. package/src/manager-ui/agent-manager.ts +0 -732
  38. package/src/tui/subagents-status.ts +0 -621
  39. package/src/tui/text-editor.ts +0 -286
@@ -2,20 +2,14 @@
2
2
  * Chain Clarification TUI Component
3
3
  *
4
4
  * Shows templates and resolved behaviors for each step in a chain.
5
- * Supports editing templates, output paths, reads lists, and progress toggle.
5
+ * Supports runtime editing of templates, output paths, reads lists, and progress toggle.
6
6
  */
7
7
 
8
8
  import type { Theme } from "@mariozechner/pi-coding-agent";
9
9
  import type { Component, TUI } from "@mariozechner/pi-tui";
10
10
  import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
11
- import * as fs from "node:fs";
12
- import * as path from "node:path";
13
- import { getUserChainDir, type AgentConfig, type ChainConfig, type ChainStepConfig } from "../../agents/agents.ts";
11
+ import type { AgentConfig } from "../../agents/agents.ts";
14
12
  import type { ResolvedStepBehavior } from "../../shared/settings.ts";
15
- import type { TextEditorState } from "../../tui/text-editor.ts";
16
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../../tui/text-editor.ts";
17
- import { updateFrontmatterField } from "../../agents/agent-serializer.ts";
18
- import { serializeChain } from "../../agents/chain-serializer.ts";
19
13
  import { resolveModelCandidate, splitThinkingSuffix } from "../shared/model-fallback.ts";
20
14
  import { findModelInfo, getSupportedThinkingLevels, type ModelInfo, type ThinkingLevel } from "../../shared/model-info.ts";
21
15
 
@@ -38,6 +32,166 @@ export interface ChainClarifyResult {
38
32
 
39
33
  type EditMode = "template" | "output" | "reads" | "model" | "thinking" | "skills";
40
34
 
35
+
36
+ interface TextEditorState {
37
+ buffer: string;
38
+ cursor: number;
39
+ viewportOffset: number;
40
+ }
41
+
42
+ function createEditorState(initial = ""): TextEditorState {
43
+ return { buffer: initial, cursor: 0, viewportOffset: 0 };
44
+ }
45
+
46
+ function wrapText(text: string, width: number): { lines: string[]; starts: number[] } {
47
+ if (width <= 0) return { lines: [text], starts: [0] };
48
+ if (text.length === 0) return { lines: [""], starts: [0] };
49
+
50
+ const lines: string[] = [];
51
+ const starts: number[] = [];
52
+ let offset = 0;
53
+ const segments = text.split("\n");
54
+ for (const [index, segment] of segments.entries()) {
55
+ if (segment.length === 0) {
56
+ starts.push(offset);
57
+ lines.push("");
58
+ } else {
59
+ let lineStart = 0;
60
+ let pos = 0;
61
+ let lineWidth = 0;
62
+ while (pos < segment.length) {
63
+ const char = String.fromCodePoint(segment.codePointAt(pos)!);
64
+ const charWidth = visibleWidth(char);
65
+ if (lineWidth > 0 && lineWidth + charWidth > width) {
66
+ starts.push(offset + lineStart);
67
+ lines.push(segment.slice(lineStart, pos));
68
+ lineStart = pos;
69
+ lineWidth = 0;
70
+ continue;
71
+ }
72
+ pos += char.length;
73
+ lineWidth += charWidth;
74
+ }
75
+ starts.push(offset + lineStart);
76
+ lines.push(segment.slice(lineStart));
77
+ }
78
+ offset += segment.length + (index < segments.length - 1 ? 1 : 0);
79
+ }
80
+ if (!text.endsWith("\n") && text.length > 0 && visibleWidth(lines[lines.length - 1] ?? "") === width) {
81
+ starts.push(text.length);
82
+ lines.push("");
83
+ }
84
+ return { lines, starts };
85
+ }
86
+
87
+ function getCursorDisplayPos(cursor: number, starts: number[]): { line: number; col: number } {
88
+ for (let i = starts.length - 1; i >= 0; i--) {
89
+ if (cursor >= starts[i]!) return { line: i, col: cursor - starts[i]! };
90
+ }
91
+ return { line: 0, col: 0 };
92
+ }
93
+
94
+ function ensureCursorVisible(cursorLine: number, viewportHeight: number, currentOffset: number): number {
95
+ if (cursorLine < currentOffset) return Math.max(0, cursorLine);
96
+ if (cursorLine >= currentOffset + viewportHeight) return Math.max(0, cursorLine - viewportHeight + 1);
97
+ return Math.max(0, currentOffset);
98
+ }
99
+
100
+ function isWordChar(ch: string): boolean {
101
+ const code = ch.charCodeAt(0);
102
+ return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95;
103
+ }
104
+
105
+ function wordBackward(buffer: string, cursor: number): number {
106
+ let pos = cursor;
107
+ while (pos > 0 && !isWordChar(buffer[pos - 1]!)) pos--;
108
+ while (pos > 0 && isWordChar(buffer[pos - 1]!)) pos--;
109
+ return pos;
110
+ }
111
+
112
+ function wordForward(buffer: string, cursor: number): number {
113
+ let pos = cursor;
114
+ while (pos < buffer.length && isWordChar(buffer[pos]!)) pos++;
115
+ while (pos < buffer.length && !isWordChar(buffer[pos]!)) pos++;
116
+ return pos;
117
+ }
118
+
119
+ function normalizeInsertText(data: string): string | null {
120
+ let text = data.split("\x1b[200~").join("").split("\x1b[201~").join("");
121
+ text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
122
+ const newline = text.indexOf("\n");
123
+ if (newline !== -1) text = text.slice(0, newline);
124
+ text = text.replace(/\t/g, " ");
125
+ if (text.length === 0) return null;
126
+ for (let i = 0; i < text.length; i++) {
127
+ if (text.charCodeAt(i) < 32) return null;
128
+ }
129
+ return text;
130
+ }
131
+
132
+ function handleEditorInput(state: TextEditorState, data: string, textWidth: number): TextEditorState | null {
133
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || matchesKey(data, "return")) return null;
134
+
135
+ const { lines: wrapped, starts } = wrapText(state.buffer, textWidth);
136
+ const cursorPos = getCursorDisplayPos(state.cursor, starts);
137
+
138
+ if (matchesKey(data, "alt+left") || matchesKey(data, "ctrl+left")) return { ...state, cursor: wordBackward(state.buffer, state.cursor) };
139
+ if (matchesKey(data, "alt+right") || matchesKey(data, "ctrl+right")) return { ...state, cursor: wordForward(state.buffer, state.cursor) };
140
+ if (matchesKey(data, "left")) return state.cursor > 0 ? { ...state, cursor: state.cursor - 1 } : state;
141
+ if (matchesKey(data, "right")) return state.cursor < state.buffer.length ? { ...state, cursor: state.cursor + 1 } : state;
142
+ if (matchesKey(data, "up") && cursorPos.line > 0) {
143
+ const targetLine = cursorPos.line - 1;
144
+ return { ...state, cursor: starts[targetLine]! + Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0) };
145
+ }
146
+ if (matchesKey(data, "down") && cursorPos.line < wrapped.length - 1) {
147
+ const targetLine = cursorPos.line + 1;
148
+ return { ...state, cursor: starts[targetLine]! + Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0) };
149
+ }
150
+ if (matchesKey(data, "home")) return { ...state, cursor: starts[cursorPos.line]! };
151
+ if (matchesKey(data, "end")) return { ...state, cursor: starts[cursorPos.line]! + (wrapped[cursorPos.line]?.length ?? 0) };
152
+ if (matchesKey(data, "ctrl+home")) return { ...state, cursor: 0 };
153
+ if (matchesKey(data, "ctrl+end")) return { ...state, cursor: state.buffer.length };
154
+ if (matchesKey(data, "alt+backspace")) {
155
+ const target = wordBackward(state.buffer, state.cursor);
156
+ return target === state.cursor ? state : { ...state, buffer: state.buffer.slice(0, target) + state.buffer.slice(state.cursor), cursor: target };
157
+ }
158
+ if (matchesKey(data, "backspace")) {
159
+ return state.cursor > 0
160
+ ? { ...state, buffer: state.buffer.slice(0, state.cursor - 1) + state.buffer.slice(state.cursor), cursor: state.cursor - 1 }
161
+ : state;
162
+ }
163
+ if (matchesKey(data, "delete")) {
164
+ return state.cursor < state.buffer.length
165
+ ? { ...state, buffer: state.buffer.slice(0, state.cursor) + state.buffer.slice(state.cursor + 1) }
166
+ : state;
167
+ }
168
+
169
+ const insert = normalizeInsertText(data);
170
+ return insert
171
+ ? { ...state, buffer: state.buffer.slice(0, state.cursor) + insert + state.buffer.slice(state.cursor), cursor: state.cursor + insert.length }
172
+ : null;
173
+ }
174
+
175
+ function renderWithCursor(text: string, cursorPos: number): string {
176
+ const before = text.slice(0, cursorPos);
177
+ const cursorChar = text[cursorPos] ?? " ";
178
+ const after = text.slice(cursorPos + 1);
179
+ return `${before}\x1b[7m${cursorChar}\x1b[27m${after}`;
180
+ }
181
+
182
+ function renderEditor(state: TextEditorState, width: number, viewportHeight: number): string[] {
183
+ const { lines: wrapped, starts } = wrapText(state.buffer, width);
184
+ const cursorPos = getCursorDisplayPos(state.cursor, starts);
185
+ const lines: string[] = [];
186
+ for (let i = 0; i < viewportHeight; i++) {
187
+ const lineIdx = state.viewportOffset + i;
188
+ let content = lineIdx < wrapped.length ? wrapped[lineIdx] ?? "" : "";
189
+ if (lineIdx === cursorPos.line) content = renderWithCursor(content, cursorPos.col);
190
+ lines.push(content);
191
+ }
192
+ return lines;
193
+ }
194
+
41
195
  /**
42
196
  * TUI component for chain clarification.
43
197
  * Factory signature matches ctx.ui.custom: (tui, theme, kb, done) => Component
@@ -61,10 +215,8 @@ export class ChainClarifyComponent implements Component {
61
215
  private skillSelectedNames: Set<string> = new Set();
62
216
  private skillCursorIndex: number = 0;
63
217
  private filteredSkills: Array<{ name: string; source: string; description?: string }> = [];
64
- private saveMessage: { text: string; type: "info" | "error" } | null = null;
65
- private saveMessageTimer: ReturnType<typeof setTimeout> | null = null;
66
- private saveChainNameState: TextEditorState = createEditorState();
67
- private savingChain = false;
218
+ private noticeMessage: { text: string; type: "info" | "error" } | null = null;
219
+ private noticeMessageTimer: ReturnType<typeof setTimeout> | null = null;
68
220
  /** Run in background (async) mode */
69
221
  private runInBackground = false;
70
222
  private tui: TUI;
@@ -260,171 +412,18 @@ export class ChainClarifyComponent implements Component {
260
412
  this.behaviorOverrides.set(stepIndex, { ...existing, [field]: value });
261
413
  }
262
414
 
263
- private buildChainConfig(name: string): ChainConfig {
264
- const steps: ChainStepConfig[] = [];
265
- for (let i = 0; i < this.agentConfigs.length; i++) {
266
- const agent = this.agentConfigs[i]!;
267
- const behavior = this.getEffectiveBehavior(i);
268
- const override = this.behaviorOverrides.get(i);
269
- const template = this.templates[i] ?? "";
270
- const step: ChainStepConfig = { agent: agent.name, task: template };
271
- if (override?.output !== undefined) step.output = behavior.output;
272
- if (behavior.outputMode !== "inline") step.outputMode = behavior.outputMode;
273
- if (override?.reads !== undefined) step.reads = behavior.reads;
274
- if (override?.model !== undefined) step.model = behavior.model;
275
- if (override?.skills !== undefined) step.skills = behavior.skills;
276
- if (override?.progress !== undefined) step.progress = behavior.progress;
277
- steps.push(step);
278
- }
279
- return {
280
- name,
281
- description: `Chain: ${steps.map((s) => s.agent).join(" → ")}`,
282
- source: "user",
283
- filePath: "",
284
- steps,
285
- };
286
- }
287
-
288
- private enterSaveChainName(): void {
289
- this.savingChain = true;
290
- this.saveChainNameState = createEditorState();
291
- this.tui.requestRender();
292
- }
293
-
294
- private handleSaveChainNameInput(data: string): void {
295
- if (matchesKey(data, "tab")) return;
296
- const innerW = this.width - 2;
297
- const boxInnerWidth = Math.max(10, innerW - 4);
298
- const nextState = handleEditorInput(this.saveChainNameState, data, boxInnerWidth);
299
- if (nextState) {
300
- this.saveChainNameState = nextState;
301
- this.tui.requestRender();
302
- return;
303
- }
304
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
305
- this.savingChain = false;
306
- this.saveChainNameState = createEditorState();
307
- this.tui.requestRender();
308
- return;
309
- }
310
- if (matchesKey(data, "return")) {
311
- const name = this.saveChainNameState.buffer.trim();
312
- if (!name) {
313
- this.showSaveMessage("Name is required", "error");
314
- this.savingChain = false;
315
- this.saveChainNameState = createEditorState();
316
- return;
317
- }
318
- try {
319
- const dir = getUserChainDir();
320
- fs.mkdirSync(dir, { recursive: true });
321
- const filePath = path.join(dir, `${name}.chain.md`);
322
- const config = this.buildChainConfig(name);
323
- config.filePath = filePath;
324
- fs.writeFileSync(filePath, serializeChain(config), "utf-8");
325
- this.showSaveMessage(`Saved ${name}.chain.md`, "info");
326
- } catch (err) {
327
- this.showSaveMessage(err instanceof Error ? err.message : String(err), "error");
328
- }
329
- this.savingChain = false;
330
- this.saveChainNameState = createEditorState();
331
- }
332
- }
333
-
334
- private showSaveMessage(text: string, type: "info" | "error"): void {
335
- this.saveMessage = { text, type };
336
- if (this.saveMessageTimer) clearTimeout(this.saveMessageTimer);
337
- this.saveMessageTimer = setTimeout(() => {
338
- this.saveMessage = null;
339
- this.saveMessageTimer = null;
415
+ private showNotice(text: string, type: "info" | "error"): void {
416
+ this.noticeMessage = { text, type };
417
+ if (this.noticeMessageTimer) clearTimeout(this.noticeMessageTimer);
418
+ this.noticeMessageTimer = setTimeout(() => {
419
+ this.noticeMessage = null;
420
+ this.noticeMessageTimer = null;
340
421
  this.tui.requestRender();
341
422
  }, 2000);
342
423
  this.tui.requestRender();
343
424
  }
344
425
 
345
- private arraysEqual(a: string[] | false, b: string[] | false): boolean {
346
- if (a === b) return true;
347
- if (a === false || b === false) return false;
348
- if (a.length !== b.length) return false;
349
- for (let i = 0; i < a.length; i++) {
350
- if (a[i] !== b[i]) return false;
351
- }
352
- return true;
353
- }
354
-
355
- private saveOverridesToAgent(): void {
356
- const stepIndex = this.selectedStep;
357
- const agent = this.agentConfigs[stepIndex];
358
- if (!agent?.filePath) {
359
- this.showSaveMessage("Agent file not found", "error");
360
- return;
361
- }
362
-
363
- const override = this.behaviorOverrides.get(stepIndex);
364
- if (!override) {
365
- this.showSaveMessage("No changes to save", "info");
366
- return;
367
- }
368
-
369
- const base = this.resolvedBehaviors[stepIndex]!;
370
- const updates: Array<{ field: string; value: string | undefined }> = [];
371
-
372
- if (override.output !== undefined && override.output !== base.output) {
373
- updates.push({
374
- field: "output",
375
- value: override.output === false ? undefined : override.output,
376
- });
377
- }
378
-
379
- if (override.reads !== undefined && !this.arraysEqual(override.reads, base.reads)) {
380
- updates.push({
381
- field: "defaultReads",
382
- value: override.reads === false ? undefined : override.reads.join(", "),
383
- });
384
- }
385
-
386
- if (override.progress !== undefined && override.progress !== base.progress) {
387
- updates.push({
388
- field: "defaultProgress",
389
- value: override.progress ? "true" : undefined,
390
- });
391
- }
392
-
393
- if (override.skills !== undefined && !this.arraysEqual(override.skills, base.skills)) {
394
- updates.push({
395
- field: "skills",
396
- value: override.skills === false || override.skills.length === 0 ? undefined : override.skills.join(", "),
397
- });
398
- }
399
-
400
- if (override.model !== undefined) {
401
- const baseModel = agent.model ? this.resolveModelFullId(agent.model) : undefined;
402
- if (override.model !== baseModel) {
403
- updates.push({ field: "model", value: override.model });
404
- }
405
- }
406
-
407
- if (updates.length === 0) {
408
- this.showSaveMessage("No changes to save", "info");
409
- return;
410
- }
411
-
412
- try {
413
- for (const update of updates) {
414
- updateFrontmatterField(agent.filePath, update.field, update.value);
415
- }
416
- this.showSaveMessage("Saved agent settings", "info");
417
- } catch (err) {
418
- this.showSaveMessage(err instanceof Error ? err.message : String(err), "error");
419
- }
420
- }
421
-
422
426
  handleInput(data: string): void {
423
- if (this.savingChain) {
424
- this.handleSaveChainNameInput(data);
425
- return;
426
- }
427
-
428
427
  if (this.editingStep !== null) {
429
428
  if (this.editMode === "model") {
430
429
  this.handleModelSelectorInput(data);
@@ -521,15 +520,6 @@ export class ChainClarifyComponent implements Component {
521
520
  return;
522
521
  }
523
522
 
524
- if (data === "S") {
525
- this.saveOverridesToAgent();
526
- return;
527
- }
528
-
529
- if (data === "W" && this.mode === "chain") {
530
- this.enterSaveChainName();
531
- return;
532
- }
533
523
  }
534
524
 
535
525
  private enterEditMode(mode: EditMode): void {
@@ -646,7 +636,7 @@ export class ChainClarifyComponent implements Component {
646
636
  /** Enter thinking level selector mode */
647
637
  private enterThinkingSelector(): void {
648
638
  if (!this.getEffectiveBehavior(this.selectedStep).model) {
649
- this.showSaveMessage("Select a model first", "error");
639
+ this.showNotice("Select a model first", "error");
650
640
  return;
651
641
  }
652
642
  this.editingStep = this.selectedStep;
@@ -888,32 +878,7 @@ export class ChainClarifyComponent implements Component {
888
878
  }
889
879
  }
890
880
 
891
- private renderSaveChainName(): string[] {
892
- const lines: string[] = [];
893
- const innerW = this.width - 2;
894
- const boxInnerWidth = Math.max(10, innerW - 4);
895
- lines.push(this.renderHeader(" Save Chain "));
896
- lines.push(this.row(""));
897
- lines.push(this.row(` ${this.theme.fg("dim", "Name:")}`));
898
- const top = `┌${"─".repeat(boxInnerWidth)}┐`;
899
- const bottom = `└${"─".repeat(boxInnerWidth)}┘`;
900
- lines.push(this.row(` ${top}`));
901
- const editorState = { ...this.saveChainNameState };
902
- const wrapped = wrapText(editorState.buffer, boxInnerWidth);
903
- const cursorPos = getCursorDisplayPos(editorState.cursor, wrapped.starts);
904
- editorState.viewportOffset = ensureCursorVisible(cursorPos.line, 1, editorState.viewportOffset);
905
- const editorLine = renderEditor(editorState, boxInnerWidth, 1)[0] ?? "";
906
- lines.push(this.row(` │${this.pad(editorLine, boxInnerWidth)}│`));
907
- lines.push(this.row(` ${bottom}`));
908
- lines.push(this.row(""));
909
- lines.push(this.renderFooter(" [Enter] Save • [Esc] Cancel "));
910
- return lines;
911
- }
912
-
913
881
  render(_width: number): string[] {
914
- if (this.savingChain) {
915
- return this.renderSaveChainName();
916
- }
917
882
  if (this.editingStep !== null) {
918
883
  if (this.editMode === "model") {
919
884
  return this.renderModelSelector();
@@ -1141,18 +1106,18 @@ export class ChainClarifyComponent implements Component {
1141
1106
  const bgLabel = this.runInBackground ? '[b]g:ON' : '[b]g';
1142
1107
  switch (this.mode) {
1143
1108
  case 'single':
1144
- return ` [Enter] Run • [Esc] Cancel • e m t w s ${bgLabel} S `;
1109
+ return ` [Enter] Run • [Esc] Cancel • e m t w s ${bgLabel} `;
1145
1110
  case 'parallel':
1146
- return ` [Enter] Run • [Esc] Cancel • e m t s ${bgLabel} S • ↑↓ Nav `;
1111
+ return ` [Enter] Run • [Esc] Cancel • e m t s ${bgLabel} • ↑↓ Nav `;
1147
1112
  case 'chain':
1148
- return ` [Enter] Run • [Esc] Cancel • e m t w r p s ${bgLabel} S W • ↑↓ Nav `;
1113
+ return ` [Enter] Run • [Esc] Cancel • e m t w r p s ${bgLabel} • ↑↓ Nav `;
1149
1114
  }
1150
1115
  }
1151
1116
 
1152
- private appendSaveMessage(lines: string[]): void {
1153
- if (!this.saveMessage) return;
1154
- const color = this.saveMessage.type === "error" ? "error" : "success";
1155
- lines.push(this.row(` ${this.theme.fg(color, this.saveMessage.text)}`));
1117
+ private appendNotice(lines: string[]): void {
1118
+ if (!this.noticeMessage) return;
1119
+ const color = this.noticeMessage.type === "error" ? "error" : "success";
1120
+ lines.push(this.row(` ${this.theme.fg(color, this.noticeMessage.text)}`));
1156
1121
  }
1157
1122
 
1158
1123
  private renderSingleMode(): string[] {
@@ -1199,7 +1164,7 @@ export class ChainClarifyComponent implements Component {
1199
1164
 
1200
1165
  lines.push(this.row(""));
1201
1166
 
1202
- this.appendSaveMessage(lines);
1167
+ this.appendNotice(lines);
1203
1168
  lines.push(this.renderFooter(this.getFooterText()));
1204
1169
 
1205
1170
  return lines;
@@ -1251,7 +1216,7 @@ export class ChainClarifyComponent implements Component {
1251
1216
  lines.push(this.row(""));
1252
1217
  }
1253
1218
 
1254
- this.appendSaveMessage(lines);
1219
+ this.appendNotice(lines);
1255
1220
  lines.push(this.renderFooter(this.getFooterText()));
1256
1221
 
1257
1222
  return lines;
@@ -1354,7 +1319,7 @@ export class ChainClarifyComponent implements Component {
1354
1319
  lines.push(this.row(""));
1355
1320
  }
1356
1321
 
1357
- this.appendSaveMessage(lines);
1322
+ this.appendNotice(lines);
1358
1323
  lines.push(this.renderFooter(this.getFooterText()));
1359
1324
 
1360
1325
  return lines;
@@ -1362,7 +1327,7 @@ export class ChainClarifyComponent implements Component {
1362
1327
 
1363
1328
  invalidate(): void {}
1364
1329
  dispose(): void {
1365
- if (this.saveMessageTimer) clearTimeout(this.saveMessageTimer);
1366
- this.saveMessageTimer = null;
1330
+ if (this.noticeMessageTimer) clearTimeout(this.noticeMessageTimer);
1331
+ this.noticeMessageTimer = null;
1367
1332
  }
1368
1333
  }
@@ -18,6 +18,7 @@ import {
18
18
  buildChainInstructions,
19
19
  writeInitialProgressFile,
20
20
  createParallelDirs,
21
+ suppressProgressForReadOnlyTask,
21
22
  aggregateParallelOutputs,
22
23
  isParallelStep,
23
24
  type StepOverrides,
@@ -178,8 +179,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
178
179
  } as SingleResult;
179
180
  }
180
181
 
181
- const behavior = input.parallelBehaviors[taskIndex]!;
182
182
  const taskTemplate = input.parallelTemplates[taskIndex] ?? "{previous}";
183
+ const behavior = suppressProgressForReadOnlyTask(input.parallelBehaviors[taskIndex]!, taskTemplate, input.originalTask);
183
184
  const templateHasPrevious = taskTemplate.includes("{previous}");
184
185
  const { prefix, suffix } = buildChainInstructions(
185
186
  behavior,
@@ -537,7 +538,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
537
538
 
538
539
  try {
539
540
  const agentNames = step.parallel.map((task) => task.agent);
540
- const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills);
541
+ const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
542
+ .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
541
543
  for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
542
544
  const behavior = parallelBehaviors[taskIndex]!;
543
545
  const outputPath = typeof behavior.output === "string"
@@ -616,6 +618,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
616
618
  }),
617
619
  };
618
620
  }
621
+ const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
622
+ const detached = detachedIndexInStep >= 0 ? parallelResults[detachedIndexInStep] : undefined;
623
+ if (detached) {
624
+ return {
625
+ content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
626
+ details: buildChainExecutionDetails({
627
+ results,
628
+ includeProgress,
629
+ allProgress,
630
+ allArtifactPaths,
631
+ artifactsDir,
632
+ chainAgents,
633
+ totalSteps,
634
+ currentStepIndex: stepIndex,
635
+ }),
636
+ };
637
+ }
619
638
 
620
639
  const failures = parallelResults
621
640
  .map((result, originalIndex) => ({ ...result, originalIndex }))
@@ -695,7 +714,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
695
714
  ? tuiOverride.skills
696
715
  : normalizeSkillInput(seqStep.skill),
697
716
  };
698
- const behavior = resolveStepBehavior(agentConfig, stepOverride, chainSkills);
717
+ const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfig, stepOverride, chainSkills), stepTemplate, originalTask);
699
718
 
700
719
  const isFirstProgress = behavior.progress && !progressCreated;
701
720
  if (isFirstProgress) {
@@ -822,24 +841,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
822
841
  if (r.progress) allProgress.push(r.progress);
823
842
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
824
843
 
825
- if (behavior.output && r.exitCode === 0) {
826
- try {
827
- const expectedPath = path.isAbsolute(behavior.output)
828
- ? behavior.output
829
- : path.join(chainDir, behavior.output);
830
- if (!fs.existsSync(expectedPath)) {
831
- const dirFiles = fs.readdirSync(chainDir);
832
- const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
833
- const warning = mdFiles.length > 0
834
- ? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
835
- : `Agent did not create expected output file: ${behavior.output}`;
836
- r.error = r.error ? `${r.error}\n${warning}` : warning;
837
- }
838
- } catch {
839
- // Ignore validation errors - this is just a diagnostic
840
- }
841
- }
842
-
843
844
  if (r.interrupted) {
844
845
  return {
845
846
  content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
@@ -855,6 +856,21 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
855
856
  }),
856
857
  };
857
858
  }
859
+ if (r.detached) {
860
+ return {
861
+ content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${r.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
862
+ details: buildChainExecutionDetails({
863
+ results,
864
+ includeProgress,
865
+ allProgress,
866
+ allArtifactPaths,
867
+ artifactsDir,
868
+ chainAgents,
869
+ totalSteps,
870
+ currentStepIndex: stepIndex,
871
+ }),
872
+ };
873
+ }
858
874
 
859
875
  if (r.exitCode !== 0) {
860
876
  const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
@@ -877,6 +893,24 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
877
893
  };
878
894
  }
879
895
 
896
+ if (behavior.output) {
897
+ try {
898
+ const expectedPath = path.isAbsolute(behavior.output)
899
+ ? behavior.output
900
+ : path.join(chainDir, behavior.output);
901
+ if (!fs.existsSync(expectedPath)) {
902
+ const dirFiles = fs.readdirSync(chainDir);
903
+ const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
904
+ const warning = mdFiles.length > 0
905
+ ? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
906
+ : `Agent did not create expected output file: ${behavior.output}`;
907
+ r.error = r.error ? `${r.error}\n${warning}` : warning;
908
+ }
909
+ } catch {
910
+ // Ignore validation errors; this diagnostic should not mask successful chain output.
911
+ }
912
+ }
913
+
880
914
  prev = getSingleResultOutput(r);
881
915
  }
882
916
  }
@@ -421,7 +421,7 @@ async function runSingleAttempt(
421
421
  const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
422
422
  ? evt.args as Record<string, unknown>
423
423
  : {};
424
- if (options.allowIntercomDetach && evt.toolName === "intercom") {
424
+ if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
425
425
  intercomStarted = true;
426
426
  }
427
427
  progress.toolCount++;
@@ -633,7 +633,10 @@ async function runSingleAttempt(
633
633
  return result;
634
634
  }
635
635
 
636
- if (exitCode === 0 && !result.error) {
636
+ if (result.error && result.exitCode === 0) {
637
+ result.exitCode = 1;
638
+ }
639
+ if (result.exitCode === 0 && !result.error) {
637
640
  const errInfo = detectSubagentError(result.messages);
638
641
  if (errInfo.hasError) {
639
642
  result.exitCode = errInfo.exitCode ?? 1;
@@ -746,7 +749,6 @@ export async function runSync(
746
749
 
747
750
  const shareEnabled = options.share === true;
748
751
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
749
- const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
750
752
  const skillNames = options.skills ?? agent.skills ?? [];
751
753
  const skillCwd = options.cwd ?? runtimeCwd;
752
754
  const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, skillCwd, runtimeCwd);
@@ -797,6 +799,7 @@ export async function runSync(
797
799
  for (let i = 0; i < modelsToTry.length; i++) {
798
800
  const candidate = modelsToTry[i];
799
801
  if (candidate) attemptedModels.push(candidate);
802
+ const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
800
803
  const result = await runSingleAttempt(runtimeCwd, agent, task, candidate, options, {
801
804
  sessionEnabled,
802
805
  systemPrompt,