pi-subagents 0.3.2 → 0.3.3

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
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.3] - 2026-01-25
4
+
5
+ ### Added
6
+ - **Thinking level selector in chain TUI** - Press `[t]` to set thinking level for any step
7
+ - Options: off, minimal, low, medium, high, xhigh (ultrathink)
8
+ - Appends to model as suffix (e.g., `anthropic/claude-sonnet-4-5:high`)
9
+ - Pre-selects current thinking level if already set
10
+ - **Model selector in chain TUI** - Press `[m]` to select a different model for any step
11
+ - Fuzzy search through all available models
12
+ - Shows current model with ✓ indicator
13
+ - Provider/model format (e.g., `anthropic/claude-haiku-4-5`)
14
+ - Override indicator (✎) when model differs from agent default
15
+ - **Model visibility in chain execution** - Shows which model each step is using
16
+ - Display format: `Step 1: scout (claude-haiku-4-5) | 3 tools, 16.8s`
17
+ - Model shown in both running and completed steps
18
+ - **Auto-propagate output changes to reads** - When you change a step's output filename,
19
+ downstream steps that read from it are automatically updated to use the new filename
20
+ - Maintains chain dependencies without manual updates
21
+ - Example: Change scout's output from `context.md` to `summary.md`, planner's reads updates automatically
22
+
23
+ ### Changed
24
+ - **Progress is now chain-level** - `[p]` toggles progress for ALL steps at once
25
+ - Progress setting shown at chain level (not per-step)
26
+ - Chains share a single progress.md, so chain-wide toggle is more intuitive
27
+ - **Clearer output/writes labeling** - Renamed `output:` to `writes:` to clarify it's a file
28
+ - Hotkey changed from `[o]` to `[w]` for consistency
29
+ - **{previous} data flow indicator** - Shows on the PRODUCING step (not receiving):
30
+ - `↳ response → {previous}` appears after scout's reads line
31
+ - Only shows when next step's template uses `{previous}`
32
+ - Clearer mental model: output flows DOWN the chain
33
+ - Chain TUI footer updated: `[e]dit [m]odel [t]hinking [w]rites [r]eads [p]rogress`
34
+
35
+ ### Fixed
36
+ - **Chain READ/WRITE instructions now prepended** - Instructions restructured:
37
+ - `[Read from: /path/file.md]` and `[Write to: /path/file.md]` prepended BEFORE task
38
+ - Overrides any hardcoded filenames in task text from parent agent
39
+ - Previously: instructions were appended at end and could be overlooked
40
+ - **Output file validation** - After each step, validates expected file was created:
41
+ - If missing, warns: "Agent wrote to different file(s): X instead of Y"
42
+ - Helps diagnose when agents don't create expected outputs
43
+ - **Root cause: agents need `write` tool** - Agents without `write` in their tools list
44
+ cannot create output files (they tried MCP workarounds which failed)
45
+ - **Thinking level suffixes now preserved** - Models with thinking levels (e.g., `claude-sonnet-4-5:high`)
46
+ now correctly resolve to `anthropic/claude-sonnet-4-5:high` instead of losing the provider prefix
47
+
48
+ ### Improved
49
+ - **Per-step progress indicators** - When progress is enabled, each step shows its role:
50
+ - Step 1: `● creates & updates progress.md`
51
+ - Step 2+: `↔ reads & updates progress.md`
52
+ - Clear visualization of progress.md data flow through the chain
53
+ - **Comprehensive tool descriptions** - Better documentation of chain variables:
54
+ - Tool description now explains `{task}`, `{previous}`, `{chain_dir}` in detail
55
+ - Schema descriptions clarify what each variable means and when to use them
56
+ - Helps agents construct proper chain queries for any use case
57
+
3
58
  ## [0.3.2] - 2026-01-25
4
59
 
5
60
  ### Performance
package/README.md CHANGED
@@ -50,9 +50,22 @@ npx pi-subagents --remove
50
50
  - `Esc` - Cancel
51
51
  - `↑↓` - Navigate between steps
52
52
  - `e` - Edit task/template
53
- - `o` - Edit output path
53
+ - `m` - Select model (fuzzy search with all available models)
54
+ - `t` - Select thinking level (off, minimal, low, medium, high, xhigh)
55
+ - `w` - Edit writes (output file path)
54
56
  - `r` - Edit reads list
55
- - `p` - Toggle progress tracking on/off
57
+ - `p` - Toggle progress tracking for ALL steps (chains share one progress.md)
58
+
59
+ *Model selector mode:*
60
+ - `↑↓` - Navigate model list
61
+ - `Enter` - Select model
62
+ - `Esc` - Cancel (keep current model)
63
+ - Type to filter (fuzzy search by model name or provider)
64
+
65
+ *Thinking level selector mode:*
66
+ - `↑↓` - Navigate level list
67
+ - `Enter` - Select level
68
+ - `Esc` - Cancel (keep current level)
56
69
 
57
70
  *Edit mode (full-screen editor with word wrapping):*
58
71
  - `Esc` - Save changes and exit
package/chain-clarify.ts CHANGED
@@ -11,11 +11,19 @@ import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui"
11
11
  import type { AgentConfig } from "./agents.js";
12
12
  import type { ResolvedStepBehavior } from "./settings.js";
13
13
 
14
+ /** Model info for display */
15
+ export interface ModelInfo {
16
+ provider: string;
17
+ id: string;
18
+ fullId: string; // "provider/id"
19
+ }
20
+
14
21
  /** Modified behavior overrides from TUI editing */
15
22
  export interface BehaviorOverride {
16
23
  output?: string | false;
17
24
  reads?: string[] | false;
18
25
  progress?: boolean;
26
+ model?: string; // Override agent's default model (format: "provider/id")
19
27
  }
20
28
 
21
29
  export interface ChainClarifyResult {
@@ -25,7 +33,11 @@ export interface ChainClarifyResult {
25
33
  behaviorOverrides: (BehaviorOverride | undefined)[];
26
34
  }
27
35
 
28
- type EditMode = "template" | "output" | "reads";
36
+ type EditMode = "template" | "output" | "reads" | "model" | "thinking";
37
+
38
+ /** Valid thinking levels */
39
+ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
40
+ type ThinkingLevel = typeof THINKING_LEVELS[number];
29
41
 
30
42
  /**
31
43
  * TUI component for chain clarification.
@@ -47,6 +59,17 @@ export class ChainClarifyComponent implements Component {
47
59
  /** Track user modifications to behaviors (sparse - only stores changes) */
48
60
  private behaviorOverrides: Map<number, BehaviorOverride> = new Map();
49
61
 
62
+ /** Model selector state */
63
+ private modelSearchQuery: string = "";
64
+ private modelSelectedIndex: number = 0;
65
+ private filteredModels: ModelInfo[] = [];
66
+
67
+ /** Max models visible in selector */
68
+ private readonly MODEL_SELECTOR_HEIGHT = 10;
69
+
70
+ /** Thinking level selector state */
71
+ private thinkingSelectedIndex: number = 0;
72
+
50
73
  constructor(
51
74
  private tui: TUI,
52
75
  private theme: Theme,
@@ -55,8 +78,12 @@ export class ChainClarifyComponent implements Component {
55
78
  private originalTask: string,
56
79
  private chainDir: string,
57
80
  private resolvedBehaviors: ResolvedStepBehavior[],
81
+ private availableModels: ModelInfo[],
58
82
  private done: (result: ChainClarifyResult) => void,
59
- ) {}
83
+ ) {
84
+ // Initialize filtered models
85
+ this.filteredModels = [...availableModels];
86
+ }
60
87
 
61
88
  // ─────────────────────────────────────────────────────────────────────────────
62
89
  // Helper methods for rendering
@@ -244,7 +271,7 @@ export class ChainClarifyComponent implements Component {
244
271
  // ─────────────────────────────────────────────────────────────────────────────
245
272
 
246
273
  /** Get effective behavior for a step (with user overrides applied) */
247
- private getEffectiveBehavior(stepIndex: number): ResolvedStepBehavior {
274
+ private getEffectiveBehavior(stepIndex: number): ResolvedStepBehavior & { model?: string } {
248
275
  const base = this.resolvedBehaviors[stepIndex]!;
249
276
  const override = this.behaviorOverrides.get(stepIndex);
250
277
  if (!override) return base;
@@ -253,9 +280,44 @@ export class ChainClarifyComponent implements Component {
253
280
  output: override.output !== undefined ? override.output : base.output,
254
281
  reads: override.reads !== undefined ? override.reads : base.reads,
255
282
  progress: override.progress !== undefined ? override.progress : base.progress,
283
+ model: override.model,
256
284
  };
257
285
  }
258
286
 
287
+ /** Get the effective model for a step (override or agent default) */
288
+ private getEffectiveModel(stepIndex: number): string {
289
+ const override = this.behaviorOverrides.get(stepIndex);
290
+ if (override?.model) return override.model; // Override is already in provider/model format
291
+
292
+ // Use agent's configured model or "default"
293
+ const agentModel = this.agentConfigs[stepIndex]?.model;
294
+ if (!agentModel) return "default";
295
+
296
+ // Resolve model name to full provider/model format
297
+ return this.resolveModelFullId(agentModel);
298
+ }
299
+
300
+ /** Resolve a model name to its full provider/model format */
301
+ private resolveModelFullId(modelName: string): string {
302
+ // If already in provider/model format, return as-is
303
+ if (modelName.includes("/")) return modelName;
304
+
305
+ // Handle thinking level suffixes (e.g., "claude-sonnet-4-5:high")
306
+ // Strip the suffix for lookup, then add it back
307
+ const colonIdx = modelName.lastIndexOf(":");
308
+ const baseModel = colonIdx !== -1 ? modelName.substring(0, colonIdx) : modelName;
309
+ const thinkingSuffix = colonIdx !== -1 ? modelName.substring(colonIdx) : "";
310
+
311
+ // Look up base model in available models to find provider
312
+ const match = this.availableModels.find(m => m.id === baseModel);
313
+ if (match) {
314
+ return thinkingSuffix ? `${match.fullId}${thinkingSuffix}` : match.fullId;
315
+ }
316
+
317
+ // Fallback to just the model name if not found
318
+ return modelName;
319
+ }
320
+
259
321
  /** Update a behavior override for a step */
260
322
  private updateBehavior(stepIndex: number, field: keyof BehaviorOverride, value: string | boolean | string[] | false): void {
261
323
  const existing = this.behaviorOverrides.get(stepIndex) ?? {};
@@ -264,7 +326,13 @@ export class ChainClarifyComponent implements Component {
264
326
 
265
327
  handleInput(data: string): void {
266
328
  if (this.editingStep !== null) {
267
- this.handleEditInput(data);
329
+ if (this.editMode === "model") {
330
+ this.handleModelSelectorInput(data);
331
+ } else if (this.editMode === "thinking") {
332
+ this.handleThinkingSelectorInput(data);
333
+ } else {
334
+ this.handleEditInput(data);
335
+ }
268
336
  return;
269
337
  }
270
338
 
@@ -303,8 +371,8 @@ export class ChainClarifyComponent implements Component {
303
371
  return;
304
372
  }
305
373
 
306
- // 'o' to edit output
307
- if (data === "o") {
374
+ // 'w' to edit writes (output file)
375
+ if (data === "w") {
308
376
  this.enterEditMode("output");
309
377
  return;
310
378
  }
@@ -315,13 +383,30 @@ export class ChainClarifyComponent implements Component {
315
383
  return;
316
384
  }
317
385
 
318
- // 'p' to toggle progress
386
+ // 'p' to toggle progress for ALL steps (chains share a single progress.md)
319
387
  if (data === "p") {
320
- const current = this.getEffectiveBehavior(this.selectedStep);
321
- this.updateBehavior(this.selectedStep, "progress", !current.progress);
388
+ // Check if any step has progress enabled
389
+ const anyEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
390
+ // Toggle all steps to the opposite state
391
+ const newState = !anyEnabled;
392
+ for (let i = 0; i < this.agentConfigs.length; i++) {
393
+ this.updateBehavior(i, "progress", newState);
394
+ }
322
395
  this.tui.requestRender();
323
396
  return;
324
397
  }
398
+
399
+ // 'm' to select model
400
+ if (data === "m") {
401
+ this.enterModelSelector();
402
+ return;
403
+ }
404
+
405
+ // 't' to select thinking level
406
+ if (data === "t") {
407
+ this.enterThinkingSelector();
408
+ return;
409
+ }
325
410
  }
326
411
 
327
412
  private enterEditMode(mode: EditMode): void {
@@ -345,6 +430,173 @@ export class ChainClarifyComponent implements Component {
345
430
  this.tui.requestRender();
346
431
  }
347
432
 
433
+ /** Enter model selector mode */
434
+ private enterModelSelector(): void {
435
+ this.editingStep = this.selectedStep;
436
+ this.editMode = "model";
437
+ this.modelSearchQuery = "";
438
+ this.modelSelectedIndex = 0;
439
+ this.filteredModels = [...this.availableModels];
440
+
441
+ // Pre-select current model if it exists in the list
442
+ const currentModel = this.getEffectiveModel(this.selectedStep);
443
+ const currentIndex = this.filteredModels.findIndex(m => m.fullId === currentModel || m.id === currentModel);
444
+ if (currentIndex >= 0) {
445
+ this.modelSelectedIndex = currentIndex;
446
+ }
447
+
448
+ this.tui.requestRender();
449
+ }
450
+
451
+ /** Filter models based on search query (fuzzy match) */
452
+ private filterModels(): void {
453
+ const query = this.modelSearchQuery.toLowerCase();
454
+ if (!query) {
455
+ this.filteredModels = [...this.availableModels];
456
+ } else {
457
+ this.filteredModels = this.availableModels.filter(m =>
458
+ m.fullId.toLowerCase().includes(query) ||
459
+ m.id.toLowerCase().includes(query) ||
460
+ m.provider.toLowerCase().includes(query)
461
+ );
462
+ }
463
+ // Clamp selected index
464
+ this.modelSelectedIndex = Math.min(this.modelSelectedIndex, Math.max(0, this.filteredModels.length - 1));
465
+ }
466
+
467
+ /** Handle input in model selector mode */
468
+ private handleModelSelectorInput(data: string): void {
469
+ // Escape or Ctrl+C - cancel and exit
470
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
471
+ this.exitEditMode();
472
+ return;
473
+ }
474
+
475
+ // Enter - select current model
476
+ if (matchesKey(data, "return")) {
477
+ const selected = this.filteredModels[this.modelSelectedIndex];
478
+ if (selected) {
479
+ this.updateBehavior(this.editingStep!, "model", selected.fullId);
480
+ }
481
+ this.exitEditMode();
482
+ return;
483
+ }
484
+
485
+ // Up arrow - move selection up
486
+ if (matchesKey(data, "up")) {
487
+ if (this.filteredModels.length > 0) {
488
+ this.modelSelectedIndex = this.modelSelectedIndex === 0
489
+ ? this.filteredModels.length - 1
490
+ : this.modelSelectedIndex - 1;
491
+ }
492
+ this.tui.requestRender();
493
+ return;
494
+ }
495
+
496
+ // Down arrow - move selection down
497
+ if (matchesKey(data, "down")) {
498
+ if (this.filteredModels.length > 0) {
499
+ this.modelSelectedIndex = this.modelSelectedIndex === this.filteredModels.length - 1
500
+ ? 0
501
+ : this.modelSelectedIndex + 1;
502
+ }
503
+ this.tui.requestRender();
504
+ return;
505
+ }
506
+
507
+ // Backspace - delete last character from search
508
+ if (matchesKey(data, "backspace")) {
509
+ if (this.modelSearchQuery.length > 0) {
510
+ this.modelSearchQuery = this.modelSearchQuery.slice(0, -1);
511
+ this.filterModels();
512
+ }
513
+ this.tui.requestRender();
514
+ return;
515
+ }
516
+
517
+ // Printable character - add to search query
518
+ if (data.length === 1 && data.charCodeAt(0) >= 32) {
519
+ this.modelSearchQuery += data;
520
+ this.filterModels();
521
+ this.tui.requestRender();
522
+ return;
523
+ }
524
+ }
525
+
526
+ /** Enter thinking level selector mode */
527
+ private enterThinkingSelector(): void {
528
+ this.editingStep = this.selectedStep;
529
+ this.editMode = "thinking";
530
+
531
+ // Pre-select current thinking level if set
532
+ const currentModel = this.getEffectiveModel(this.selectedStep);
533
+ const colonIdx = currentModel.lastIndexOf(":");
534
+ if (colonIdx !== -1) {
535
+ const suffix = currentModel.substring(colonIdx + 1);
536
+ const levelIdx = THINKING_LEVELS.indexOf(suffix as ThinkingLevel);
537
+ this.thinkingSelectedIndex = levelIdx >= 0 ? levelIdx : 0;
538
+ } else {
539
+ this.thinkingSelectedIndex = 0; // Default to "off"
540
+ }
541
+
542
+ this.tui.requestRender();
543
+ }
544
+
545
+ /** Handle input in thinking level selector mode */
546
+ private handleThinkingSelectorInput(data: string): void {
547
+ // Escape or Ctrl+C - cancel and exit
548
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
549
+ this.exitEditMode();
550
+ return;
551
+ }
552
+
553
+ // Enter - select current thinking level
554
+ if (matchesKey(data, "return")) {
555
+ const selectedLevel = THINKING_LEVELS[this.thinkingSelectedIndex];
556
+ this.applyThinkingLevel(selectedLevel);
557
+ this.exitEditMode();
558
+ return;
559
+ }
560
+
561
+ // Up arrow - move selection up
562
+ if (matchesKey(data, "up")) {
563
+ this.thinkingSelectedIndex = this.thinkingSelectedIndex === 0
564
+ ? THINKING_LEVELS.length - 1
565
+ : this.thinkingSelectedIndex - 1;
566
+ this.tui.requestRender();
567
+ return;
568
+ }
569
+
570
+ // Down arrow - move selection down
571
+ if (matchesKey(data, "down")) {
572
+ this.thinkingSelectedIndex = this.thinkingSelectedIndex === THINKING_LEVELS.length - 1
573
+ ? 0
574
+ : this.thinkingSelectedIndex + 1;
575
+ this.tui.requestRender();
576
+ return;
577
+ }
578
+ }
579
+
580
+ /** Apply thinking level to the current step's model */
581
+ private applyThinkingLevel(level: ThinkingLevel): void {
582
+ const stepIndex = this.editingStep!;
583
+ const currentModel = this.getEffectiveModel(stepIndex);
584
+
585
+ // Strip any existing thinking level suffix
586
+ const colonIdx = currentModel.lastIndexOf(":");
587
+ let baseModel = currentModel;
588
+ if (colonIdx !== -1) {
589
+ const suffix = currentModel.substring(colonIdx + 1);
590
+ if (THINKING_LEVELS.includes(suffix as ThinkingLevel)) {
591
+ baseModel = currentModel.substring(0, colonIdx);
592
+ }
593
+ }
594
+
595
+ // Apply new thinking level (don't add suffix for "off")
596
+ const newModel = level === "off" ? baseModel : `${baseModel}:${level}`;
597
+ this.updateBehavior(stepIndex, "model", newModel);
598
+ }
599
+
348
600
  private handleEditInput(data: string): void {
349
601
  const textWidth = this.width - 4; // Must match render: innerW - 2 = (width - 2) - 2
350
602
  const { lines: wrapped, starts } = this.wrapText(this.editBuffer, textWidth);
@@ -495,9 +747,19 @@ export class ChainClarifyComponent implements Component {
495
747
  originalLines[0] = this.editBuffer;
496
748
  this.templates[stepIndex] = originalLines.join("\n");
497
749
  } else if (this.editMode === "output") {
750
+ // Capture OLD output before updating (for downstream propagation)
751
+ const oldBehavior = this.getEffectiveBehavior(stepIndex);
752
+ const oldOutput = typeof oldBehavior.output === "string" ? oldBehavior.output : null;
753
+
498
754
  // Empty string or whitespace means disable output
499
755
  const trimmed = this.editBuffer.trim();
500
- this.updateBehavior(stepIndex, "output", trimmed === "" ? false : trimmed);
756
+ const newOutput = trimmed === "" ? false : trimmed;
757
+ this.updateBehavior(stepIndex, "output", newOutput);
758
+
759
+ // Propagate output filename change to downstream steps' reads
760
+ if (oldOutput && typeof newOutput === "string" && oldOutput !== newOutput) {
761
+ this.propagateOutputChange(stepIndex, oldOutput, newOutput);
762
+ }
501
763
  } else if (this.editMode === "reads") {
502
764
  // Parse comma-separated list, empty means disable reads
503
765
  const trimmed = this.editBuffer.trim();
@@ -510,13 +772,181 @@ export class ChainClarifyComponent implements Component {
510
772
  }
511
773
  }
512
774
 
775
+ /**
776
+ * When a step's output filename changes, update downstream steps that read from it.
777
+ * This maintains the chain dependency automatically.
778
+ */
779
+ private propagateOutputChange(changedStepIndex: number, oldOutput: string, newOutput: string): void {
780
+ // Check all downstream steps (steps that come after the changed step)
781
+ for (let i = changedStepIndex + 1; i < this.agentConfigs.length; i++) {
782
+ const behavior = this.getEffectiveBehavior(i);
783
+
784
+ // Skip if reads is disabled or empty
785
+ if (behavior.reads === false || !behavior.reads || behavior.reads.length === 0) {
786
+ continue;
787
+ }
788
+
789
+ // Check if this step reads the old output file
790
+ const readsArray = behavior.reads;
791
+ const oldIndex = readsArray.indexOf(oldOutput);
792
+
793
+ if (oldIndex !== -1) {
794
+ // Replace old filename with new filename in reads
795
+ const newReads = [...readsArray];
796
+ newReads[oldIndex] = newOutput;
797
+ this.updateBehavior(i, "reads", newReads);
798
+ }
799
+ }
800
+ }
801
+
513
802
  render(_width: number): string[] {
514
803
  if (this.editingStep !== null) {
804
+ if (this.editMode === "model") {
805
+ return this.renderModelSelector();
806
+ }
807
+ if (this.editMode === "thinking") {
808
+ return this.renderThinkingSelector();
809
+ }
515
810
  return this.renderFullEditMode();
516
811
  }
517
812
  return this.renderNavigationMode();
518
813
  }
519
814
 
815
+ /** Render the model selector view */
816
+ private renderModelSelector(): string[] {
817
+ const innerW = this.width - 2;
818
+ const th = this.theme;
819
+ const lines: string[] = [];
820
+
821
+ // Header
822
+ const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
823
+ const headerText = ` Select Model (Step ${this.editingStep! + 1}: ${agentName}) `;
824
+ lines.push(this.renderHeader(headerText));
825
+ lines.push(this.row(""));
826
+
827
+ // Search input
828
+ const searchPrefix = th.fg("dim", "Search: ");
829
+ const cursor = "\x1b[7m \x1b[27m"; // Reverse video space for cursor
830
+ const searchDisplay = this.modelSearchQuery + cursor;
831
+ lines.push(this.row(` ${searchPrefix}${searchDisplay}`));
832
+ lines.push(this.row(""));
833
+
834
+ // Current model info
835
+ const currentModel = this.getEffectiveModel(this.editingStep!);
836
+ const currentLabel = th.fg("dim", "Current: ");
837
+ lines.push(this.row(` ${currentLabel}${th.fg("warning", currentModel)}`));
838
+ lines.push(this.row(""));
839
+
840
+ // Model list with scroll
841
+ if (this.filteredModels.length === 0) {
842
+ lines.push(this.row(` ${th.fg("dim", "No matching models")}`));
843
+ } else {
844
+ // Calculate visible range (scroll to keep selection visible)
845
+ const maxVisible = this.MODEL_SELECTOR_HEIGHT;
846
+ let startIdx = 0;
847
+
848
+ // Keep selection centered if possible
849
+ if (this.filteredModels.length > maxVisible) {
850
+ startIdx = Math.max(0, this.modelSelectedIndex - Math.floor(maxVisible / 2));
851
+ startIdx = Math.min(startIdx, this.filteredModels.length - maxVisible);
852
+ }
853
+
854
+ const endIdx = Math.min(startIdx + maxVisible, this.filteredModels.length);
855
+
856
+ // Show scroll indicator if needed
857
+ if (startIdx > 0) {
858
+ lines.push(this.row(` ${th.fg("dim", ` ↑ ${startIdx} more`)}`));
859
+ }
860
+
861
+ for (let i = startIdx; i < endIdx; i++) {
862
+ const model = this.filteredModels[i]!;
863
+ const isSelected = i === this.modelSelectedIndex;
864
+ const isCurrent = model.fullId === currentModel || model.id === currentModel;
865
+
866
+ const prefix = isSelected ? th.fg("accent", "→ ") : " ";
867
+ const modelText = isSelected ? th.fg("accent", model.id) : model.id;
868
+ const providerBadge = th.fg("dim", ` [${model.provider}]`);
869
+ const currentBadge = isCurrent ? th.fg("success", " ✓") : "";
870
+
871
+ lines.push(this.row(` ${prefix}${modelText}${providerBadge}${currentBadge}`));
872
+ }
873
+
874
+ // Show scroll indicator if needed
875
+ const remaining = this.filteredModels.length - endIdx;
876
+ if (remaining > 0) {
877
+ lines.push(this.row(` ${th.fg("dim", ` ↓ ${remaining} more`)}`));
878
+ }
879
+ }
880
+
881
+ // Pad to consistent height
882
+ const contentLines = lines.length;
883
+ const targetHeight = 18; // Consistent height
884
+ for (let i = contentLines; i < targetHeight; i++) {
885
+ lines.push(this.row(""));
886
+ }
887
+
888
+ // Footer
889
+ const footerText = " [Enter] Select • [Esc] Cancel • Type to search ";
890
+ lines.push(this.renderFooter(footerText));
891
+
892
+ return lines;
893
+ }
894
+
895
+ /** Render the thinking level selector view */
896
+ private renderThinkingSelector(): string[] {
897
+ const innerW = this.width - 2;
898
+ const th = this.theme;
899
+ const lines: string[] = [];
900
+
901
+ // Header
902
+ const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
903
+ const headerText = ` Thinking Level (Step ${this.editingStep! + 1}: ${agentName}) `;
904
+ lines.push(this.renderHeader(headerText));
905
+ lines.push(this.row(""));
906
+
907
+ // Current model info
908
+ const currentModel = this.getEffectiveModel(this.editingStep!);
909
+ const currentLabel = th.fg("dim", "Model: ");
910
+ lines.push(this.row(` ${currentLabel}${th.fg("accent", currentModel)}`));
911
+ lines.push(this.row(""));
912
+
913
+ // Description
914
+ lines.push(this.row(` ${th.fg("dim", "Select thinking level (extended thinking budget):")}`));
915
+ lines.push(this.row(""));
916
+
917
+ // Thinking level options
918
+ const levelDescriptions: Record<ThinkingLevel, string> = {
919
+ "off": "No extended thinking",
920
+ "minimal": "Brief reasoning",
921
+ "low": "Light reasoning",
922
+ "medium": "Moderate reasoning",
923
+ "high": "Deep reasoning",
924
+ "xhigh": "Maximum reasoning (ultrathink)",
925
+ };
926
+
927
+ for (let i = 0; i < THINKING_LEVELS.length; i++) {
928
+ const level = THINKING_LEVELS[i];
929
+ const isSelected = i === this.thinkingSelectedIndex;
930
+ const prefix = isSelected ? th.fg("accent", "→ ") : " ";
931
+ const levelText = isSelected ? th.fg("accent", level) : level;
932
+ const desc = th.fg("dim", ` - ${levelDescriptions[level]}`);
933
+ lines.push(this.row(` ${prefix}${levelText}${desc}`));
934
+ }
935
+
936
+ // Pad to consistent height
937
+ const contentLines = lines.length;
938
+ const targetHeight = 16;
939
+ for (let i = contentLines; i < targetHeight; i++) {
940
+ lines.push(this.row(""));
941
+ }
942
+
943
+ // Footer
944
+ const footerText = " [Enter] Select • [Esc] Cancel • ↑↓ Navigate ";
945
+ lines.push(this.renderFooter(footerText));
946
+
947
+ return lines;
948
+ }
949
+
520
950
  /** Render navigation mode (step selection, preview) */
521
951
  private renderNavigationMode(): string[] {
522
952
  const innerW = this.width - 2;
@@ -536,6 +966,11 @@ export class ChainClarifyComponent implements Component {
536
966
  lines.push(this.row(` Original Task: ${taskPreview}`));
537
967
  const chainDirPreview = truncateToWidth(this.chainDir, innerW - 12);
538
968
  lines.push(this.row(` Chain Dir: ${th.fg("dim", chainDirPreview)}`));
969
+
970
+ // Chain-wide progress setting
971
+ const progressEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
972
+ const progressValue = progressEnabled ? th.fg("success", "✓ enabled") : th.fg("dim", "✗ disabled");
973
+ lines.push(this.row(` Progress: ${progressValue} ${th.fg("dim", "(press [p] to toggle)")}`));
539
974
  lines.push(this.row(""));
540
975
 
541
976
  // Each step
@@ -567,12 +1002,22 @@ export class ChainClarifyComponent implements Component {
567
1002
  const templateLabel = th.fg("dim", "task: ");
568
1003
  lines.push(this.row(` ${templateLabel}${truncateToWidth(highlighted, innerW - 12)}`));
569
1004
 
570
- // Output line
571
- const outputValue = behavior.output === false
1005
+ // Model line (show override indicator if modified)
1006
+ const effectiveModel = this.getEffectiveModel(i);
1007
+ const override = this.behaviorOverrides.get(i);
1008
+ const isOverridden = override?.model !== undefined;
1009
+ const modelValue = isOverridden
1010
+ ? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
1011
+ : effectiveModel;
1012
+ const modelLabel = th.fg("dim", "model: ");
1013
+ lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1014
+
1015
+ // Writes line (output file) - renamed from "output" for clarity
1016
+ const writesValue = behavior.output === false
572
1017
  ? th.fg("dim", "(disabled)")
573
1018
  : (behavior.output || th.fg("dim", "(none)"));
574
- const outputLabel = th.fg("dim", "output: ");
575
- lines.push(this.row(` ${outputLabel}${truncateToWidth(outputValue, innerW - 14)}`));
1019
+ const writesLabel = th.fg("dim", "writes: ");
1020
+ lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
576
1021
 
577
1022
  // Reads line
578
1023
  const readsValue = behavior.reads === false
@@ -583,16 +1028,32 @@ export class ChainClarifyComponent implements Component {
583
1028
  const readsLabel = th.fg("dim", "reads: ");
584
1029
  lines.push(this.row(` ${readsLabel}${truncateToWidth(readsValue, innerW - 13)}`));
585
1030
 
586
- // Progress line
587
- const progressValue = behavior.progress ? th.fg("success", "✓ enabled") : th.fg("dim", "✗ disabled");
588
- const progressLabel = th.fg("dim", "progress: ");
589
- lines.push(this.row(` ${progressLabel}${progressValue}`));
1031
+ // Progress line - show when chain-wide progress is enabled
1032
+ // First step creates & updates, subsequent steps read & update
1033
+ if (progressEnabled) {
1034
+ const isFirstStep = i === 0;
1035
+ const progressAction = isFirstStep
1036
+ ? th.fg("success", "●") + th.fg("dim", " creates & updates progress.md")
1037
+ : th.fg("accent", "↔") + th.fg("dim", " reads & updates progress.md");
1038
+ const progressLabel = th.fg("dim", "progress: ");
1039
+ lines.push(this.row(` ${progressLabel}${progressAction}`));
1040
+ }
1041
+
1042
+ // Show {previous} indicator for all steps except the last
1043
+ // This shows that this step's text response becomes {previous} for the next step
1044
+ if (i < this.agentConfigs.length - 1) {
1045
+ const nextStepUsePrevious = (this.templates[i + 1] ?? "").includes("{previous}");
1046
+ if (nextStepUsePrevious) {
1047
+ const indicator = th.fg("dim", " ↳ response → ") + th.fg("warning", "{previous}");
1048
+ lines.push(this.row(indicator));
1049
+ }
1050
+ }
590
1051
 
591
1052
  lines.push(this.row(""));
592
1053
  }
593
1054
 
594
1055
  // Footer with keybindings
595
- const footerText = " [Enter] Run • [Esc] Cancel • [e]dit [o]utput [r]eads [p]rogress ";
1056
+ const footerText = " [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking [w]rites [r]eads [p]rogress ";
596
1057
  lines.push(this.renderFooter(footerText));
597
1058
 
598
1059
  return lines;
@@ -7,7 +7,7 @@ import * as path from "node:path";
7
7
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
8
8
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
9
9
  import type { AgentConfig } from "./agents.js";
10
- import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride } from "./chain-clarify.js";
10
+ import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride, type ModelInfo } from "./chain-clarify.js";
11
11
  import {
12
12
  resolveChainTemplates,
13
13
  createChainDir,
@@ -36,6 +36,28 @@ import {
36
36
  MAX_CONCURRENCY,
37
37
  } from "./types.js";
38
38
 
39
+ /** Resolve a model name to its full provider/model format */
40
+ function resolveModelFullId(modelName: string | undefined, availableModels: ModelInfo[]): string | undefined {
41
+ if (!modelName) return undefined;
42
+ // If already in provider/model format, return as-is
43
+ if (modelName.includes("/")) return modelName;
44
+
45
+ // Handle thinking level suffixes (e.g., "claude-sonnet-4-5:high")
46
+ // Strip the suffix for lookup, then add it back
47
+ const colonIdx = modelName.lastIndexOf(":");
48
+ const baseModel = colonIdx !== -1 ? modelName.substring(0, colonIdx) : modelName;
49
+ const thinkingSuffix = colonIdx !== -1 ? modelName.substring(colonIdx) : "";
50
+
51
+ // Look up base model in available models to find provider
52
+ const match = availableModels.find(m => m.id === baseModel);
53
+ if (match) {
54
+ return thinkingSuffix ? `${match.fullId}${thinkingSuffix}` : match.fullId;
55
+ }
56
+
57
+ // Fallback: return as-is
58
+ return modelName;
59
+ }
60
+
39
61
  export interface ChainExecutionParams {
40
62
  chain: ChainStep[];
41
63
  agents: AgentConfig[];
@@ -110,6 +132,13 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
110
132
  // Behavior overrides from TUI (set if TUI is shown, undefined otherwise)
111
133
  let tuiBehaviorOverrides: (BehaviorOverride | undefined)[] | undefined;
112
134
 
135
+ // Get available models for model resolution (used in TUI and execution)
136
+ const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
137
+ provider: m.provider,
138
+ id: m.id,
139
+ fullId: `${m.provider}/${m.id}`,
140
+ }));
141
+
113
142
  if (shouldClarify) {
114
143
  // Sequential-only chain: use existing TUI
115
144
  const seqSteps = chainSteps as SequentialStep[];
@@ -154,6 +183,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
154
183
  originalTask,
155
184
  chainDir,
156
185
  resolvedBehaviors,
186
+ availableModels,
157
187
  done,
158
188
  ),
159
189
  {
@@ -227,18 +257,31 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
227
257
  } as SingleResult;
228
258
  }
229
259
 
230
- // Build task string
260
+ // Resolve behavior for this parallel task
261
+ const behavior = parallelBehaviors[taskIndex]!;
262
+
263
+ // Build chain instructions (prefix goes BEFORE task, suffix goes AFTER)
231
264
  const taskTemplate = parallelTemplates[taskIndex] ?? "{previous}";
232
265
  const templateHasPrevious = taskTemplate.includes("{previous}");
266
+ const { prefix, suffix } = buildChainInstructions(
267
+ behavior,
268
+ chainDir,
269
+ false, // parallel tasks don't create progress (pre-created above)
270
+ templateHasPrevious ? undefined : prev
271
+ );
272
+
273
+ // Build task string with variable substitution
233
274
  let taskStr = taskTemplate;
234
275
  taskStr = taskStr.replace(/\{task\}/g, originalTask);
235
276
  taskStr = taskStr.replace(/\{previous\}/g, prev);
236
277
  taskStr = taskStr.replace(/\{chain_dir\}/g, chainDir);
237
278
 
238
- // Add chain instructions (include previous summary only if not already in template)
239
- const behavior = parallelBehaviors[taskIndex]!;
240
- // For parallel, no single "first progress" - each manages independently
241
- taskStr += buildChainInstructions(behavior, chainDir, false, templateHasPrevious ? undefined : prev);
279
+ // Assemble final task: prefix (READ/WRITE instructions) + task + suffix
280
+ taskStr = prefix + taskStr + suffix;
281
+
282
+ // Resolve model to full provider/model format for consistent display
283
+ const taskAgentConfig = agents.find((a) => a.name === task.agent);
284
+ const effectiveModel = resolveModelFullId(taskAgentConfig?.model, availableModels);
242
285
 
243
286
  const r = await runSync(ctx.cwd, agents, task.agent, taskStr, {
244
287
  cwd: task.cwd ?? cwd,
@@ -249,6 +292,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
249
292
  share: shareEnabled,
250
293
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
251
294
  artifactConfig,
295
+ modelOverride: effectiveModel,
252
296
  onUpdate: onUpdate
253
297
  ? (p) => {
254
298
  // Use concat instead of spread for better performance
@@ -340,14 +384,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
340
384
  };
341
385
  }
342
386
 
343
- // Build task string (check if template has {previous} before replacement)
344
- const templateHasPrevious = stepTemplate.includes("{previous}");
345
- let stepTask = stepTemplate;
346
- stepTask = stepTask.replace(/\{task\}/g, originalTask);
347
- stepTask = stepTask.replace(/\{previous\}/g, prev);
348
- stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
349
-
350
- // Resolve behavior (TUI overrides take precedence over step config)
387
+ // Resolve behavior first (TUI overrides take precedence over step config)
351
388
  const tuiOverride = tuiBehaviorOverrides?.[stepIndex];
352
389
  const stepOverride: StepOverrides = {
353
390
  output: tuiOverride?.output !== undefined ? tuiOverride.output : seqStep.output,
@@ -362,8 +399,26 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
362
399
  progressCreated = true;
363
400
  }
364
401
 
365
- // Add chain instructions (include previous summary only if not already in template)
366
- stepTask += buildChainInstructions(behavior, chainDir, isFirstProgress, templateHasPrevious ? undefined : prev);
402
+ // Build chain instructions (prefix goes BEFORE task, suffix goes AFTER)
403
+ const templateHasPrevious = stepTemplate.includes("{previous}");
404
+ const { prefix, suffix } = buildChainInstructions(
405
+ behavior,
406
+ chainDir,
407
+ isFirstProgress,
408
+ templateHasPrevious ? undefined : prev
409
+ );
410
+
411
+ // Build task string with variable substitution
412
+ let stepTask = stepTemplate;
413
+ stepTask = stepTask.replace(/\{task\}/g, originalTask);
414
+ stepTask = stepTask.replace(/\{previous\}/g, prev);
415
+ stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
416
+
417
+ // Assemble final task: prefix (READ/WRITE instructions) + task + suffix (progress, previous summary)
418
+ stepTask = prefix + stepTask + suffix;
419
+
420
+ // Resolve model: TUI override (already full format) or agent's model resolved to full format
421
+ const effectiveModel = tuiOverride?.model ?? resolveModelFullId(agentConfig.model, availableModels);
367
422
 
368
423
  // Run step
369
424
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
@@ -375,6 +430,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
375
430
  share: shareEnabled,
376
431
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
377
432
  artifactConfig,
433
+ modelOverride: effectiveModel,
378
434
  onUpdate: onUpdate
379
435
  ? (p) => {
380
436
  // Use concat instead of spread for better performance
@@ -400,6 +456,27 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
400
456
  if (r.progress) allProgress.push(r.progress);
401
457
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
402
458
 
459
+ // Validate expected output file was created
460
+ if (behavior.output && r.exitCode === 0) {
461
+ try {
462
+ const expectedPath = behavior.output.startsWith("/")
463
+ ? behavior.output
464
+ : path.join(chainDir, behavior.output);
465
+ if (!fs.existsSync(expectedPath)) {
466
+ // Look for similar files that might have been created instead
467
+ const dirFiles = fs.readdirSync(chainDir);
468
+ const mdFiles = dirFiles.filter(f => f.endsWith(".md") && f !== "progress.md");
469
+ const warning = mdFiles.length > 0
470
+ ? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
471
+ : `Agent did not create expected output file: ${behavior.output}`;
472
+ // Add warning to result but don't fail
473
+ r.error = r.error ? `${r.error}\n⚠️ ${warning}` : `⚠️ ${warning}`;
474
+ }
475
+ } catch {
476
+ // Ignore validation errors - this is just a diagnostic
477
+ }
478
+ }
479
+
403
480
  // On failure, leave chain_dir for debugging
404
481
  if (r.exitCode !== 0) {
405
482
  const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
package/execution.ts CHANGED
@@ -43,7 +43,7 @@ export async function runSync(
43
43
  task: string,
44
44
  options: RunSyncOptions,
45
45
  ): Promise<SingleResult> {
46
- const { cwd, signal, onUpdate, maxOutput, artifactsDir, artifactConfig, runId, index } = options;
46
+ const { cwd, signal, onUpdate, maxOutput, artifactsDir, artifactConfig, runId, index, modelOverride } = options;
47
47
  const agent = agents.find((a) => a.name === agentName);
48
48
  if (!agent) {
49
49
  return {
@@ -68,7 +68,9 @@ export async function runSync(
68
68
  } catch {}
69
69
  args.push("--session-dir", options.sessionDir);
70
70
  }
71
- if (agent.model) args.push("--model", agent.model);
71
+ // Use model override if provided, otherwise use agent's default model
72
+ const effectiveModel = modelOverride ?? agent.model;
73
+ if (effectiveModel) args.push("--model", effectiveModel);
72
74
  if (agent.tools?.length) {
73
75
  const builtinTools: string[] = [];
74
76
  const extensionPaths: string[] = [];
@@ -101,6 +103,7 @@ export async function runSync(
101
103
  exitCode: 0,
102
104
  messages: [],
103
105
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
106
+ model: effectiveModel, // Initialize with the model we're using
104
107
  };
105
108
 
106
109
  const progress: AgentProgress = {
package/index.ts CHANGED
@@ -144,9 +144,20 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
144
144
  label: "Subagent",
145
145
  description: `Delegate to subagents. Use exactly ONE mode:
146
146
  • SINGLE: { agent, task } - one task
147
- • CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential, {previous} passes output
148
- • PARALLEL: { tasks: [{agent,task}, ...] } - concurrent
149
- For "scout → planner" or multi-step flows, use chain (not multiple single calls).`,
147
+ • CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential pipeline
148
+ • PARALLEL: { tasks: [{agent,task}, ...] } - concurrent execution
149
+
150
+ CHAIN TEMPLATE VARIABLES (use in task strings):
151
+ • {task} - The original task/request from the user
152
+ • {previous} - Text response from the previous step (empty for first step)
153
+ • {chain_dir} - Shared directory for chain files (e.g., /tmp/pi-chain-runs/abc123/)
154
+
155
+ CHAIN DATA FLOW:
156
+ 1. Each step's text response automatically becomes {previous} for the next step
157
+ 2. Steps can also write files to {chain_dir} (via agent's "output" config)
158
+ 3. Later steps can read those files (via agent's "reads" config)
159
+
160
+ Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", task:"Plan based on {previous}"}] }`,
150
161
  parameters: SubagentParams,
151
162
 
152
163
  async execute(_id, params, onUpdate, ctx, signal) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
package/render.ts CHANGED
@@ -260,9 +260,11 @@ export function renderSubagentResult(
260
260
  ? theme.fg("success", "✓")
261
261
  : theme.fg("error", "✗");
262
262
  const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
263
+ // Show model if available (full provider/model format)
264
+ const modelDisplay = r.model ? theme.fg("dim", ` (${r.model})`) : "";
263
265
  const stepHeader = rRunning
264
- ? `${statusIcon} Step ${i + 1}: ${theme.bold(theme.fg("warning", r.agent))}${stats}`
265
- : `${statusIcon} Step ${i + 1}: ${theme.bold(r.agent)}${stats}`;
266
+ ? `${statusIcon} Step ${i + 1}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
267
+ : `${statusIcon} Step ${i + 1}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
266
268
  c.addChild(new Text(stepHeader, 0, 0));
267
269
 
268
270
  // Task (truncated)
package/schemas.ts CHANGED
@@ -13,34 +13,36 @@ export const TaskItem = Type.Object({
13
13
  // Sequential chain step (single agent)
14
14
  export const SequentialStepSchema = Type.Object({
15
15
  agent: Type.String(),
16
- task: Type.Optional(Type.String({ description: "Task template. Use {task}, {previous}, {chain_dir}. Required for first step." })),
16
+ task: Type.Optional(Type.String({
17
+ description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder. Required for first step, defaults to '{previous}' for subsequent steps."
18
+ })),
17
19
  cwd: Type.Optional(Type.String()),
18
20
  // Chain behavior overrides
19
21
  output: Type.Optional(Type.Union([
20
22
  Type.String(),
21
23
  Type.Boolean(),
22
- ], { description: "Override output filename (string), or false for text-only" })),
24
+ ], { description: "Output filename to write in {chain_dir} (string), or false to disable file output" })),
23
25
  reads: Type.Optional(Type.Union([
24
26
  Type.Array(Type.String()),
25
27
  Type.Boolean(),
26
- ], { description: "Override files to read from {chain_dir} (array), or false to disable" })),
27
- progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
28
+ ], { description: "Files to read from {chain_dir} before running (array of filenames), or false to disable" })),
29
+ progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
28
30
  });
29
31
 
30
32
  // Parallel task item (within a parallel step)
31
33
  export const ParallelTaskSchema = Type.Object({
32
34
  agent: Type.String(),
33
- task: Type.Optional(Type.String({ description: "Task template. Defaults to {previous}." })),
35
+ task: Type.Optional(Type.String({ description: "Task template with {task}, {previous}, {chain_dir} variables. Defaults to {previous}." })),
34
36
  cwd: Type.Optional(Type.String()),
35
37
  output: Type.Optional(Type.Union([
36
38
  Type.String(),
37
39
  Type.Boolean(),
38
- ], { description: "Override output filename (string), or false for text-only" })),
40
+ ], { description: "Output filename to write in {chain_dir} (string), or false to disable file output" })),
39
41
  reads: Type.Optional(Type.Union([
40
42
  Type.Array(Type.String()),
41
43
  Type.Boolean(),
42
- ], { description: "Override files to read from {chain_dir} (array), or false to disable" })),
43
- progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
44
+ ], { description: "Files to read from {chain_dir} before running (array of filenames), or false to disable" })),
45
+ progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
44
46
  });
45
47
 
46
48
  // Parallel chain step (multiple agents running concurrently)
@@ -64,7 +66,7 @@ export const SubagentParams = Type.Object({
64
66
  agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode)" })),
65
67
  task: Type.Optional(Type.String({ description: "Task (SINGLE mode)" })),
66
68
  tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task}, ...]" })),
67
- chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: [{agent}, {agent, task:'{previous}'}] - sequential pipeline" })),
69
+ chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential pipeline where each step's response becomes {previous} for the next. Use {task}, {previous}, {chain_dir} in task templates." })),
68
70
  async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
69
71
  agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'user')" })),
70
72
  cwd: Type.Optional(Type.String()),
package/settings.ts CHANGED
@@ -198,42 +198,46 @@ export function buildChainInstructions(
198
198
  chainDir: string,
199
199
  isFirstProgressAgent: boolean,
200
200
  previousSummary?: string,
201
- ): string {
202
- const instructions: string[] = [];
201
+ ): { prefix: string; suffix: string } {
202
+ const prefixParts: string[] = [];
203
+ const suffixParts: string[] = [];
203
204
 
204
- // Include previous step's summary if available (prose output from prior agent)
205
- if (previousSummary && previousSummary.trim()) {
206
- instructions.push(`Previous step summary:\n\n${previousSummary.trim()}`);
207
- }
208
-
209
- // Reads (supports both absolute and relative paths)
205
+ // READS - prepend to override any hardcoded filenames in task text
210
206
  if (behavior.reads && behavior.reads.length > 0) {
211
- const files = behavior.reads.map((f) => resolveChainPath(f, chainDir)).join(", ");
212
- instructions.push(`Read these files: ${files}`);
207
+ const files = behavior.reads.map((f) => resolveChainPath(f, chainDir));
208
+ prefixParts.push(`[Read from: ${files.join(", ")}]`);
213
209
  }
214
210
 
215
- // Output (supports both absolute and relative paths)
211
+ // OUTPUT - prepend so agent knows where to write
216
212
  if (behavior.output) {
217
213
  const outputPath = resolveChainPath(behavior.output, chainDir);
218
- instructions.push(`Write your output to: ${outputPath}`);
214
+ prefixParts.push(`[Write to: ${outputPath}]`);
219
215
  }
220
216
 
221
- // Progress
217
+ // Progress instructions in suffix (less critical)
222
218
  if (behavior.progress) {
223
219
  const progressPath = `${chainDir}/progress.md`;
224
220
  if (isFirstProgressAgent) {
225
- instructions.push(`Create and maintain: ${progressPath}`);
226
- instructions.push("Format: Status, Tasks (checkboxes), Files Changed, Notes");
221
+ suffixParts.push(`Create and maintain progress at: ${progressPath}`);
227
222
  } else {
228
- instructions.push(`Read and update: ${progressPath}`);
223
+ suffixParts.push(`Update progress at: ${progressPath}`);
229
224
  }
230
225
  }
231
226
 
232
- if (instructions.length === 0) return "";
227
+ // Include previous step's summary in suffix if available
228
+ if (previousSummary && previousSummary.trim()) {
229
+ suffixParts.push(`Previous step output:\n${previousSummary.trim()}`);
230
+ }
231
+
232
+ const prefix = prefixParts.length > 0
233
+ ? prefixParts.join("\n") + "\n\n"
234
+ : "";
235
+
236
+ const suffix = suffixParts.length > 0
237
+ ? "\n\n---\n" + suffixParts.join("\n")
238
+ : "";
233
239
 
234
- return (
235
- "\n\n---\n**Chain Instructions:**\n" + instructions.map((i) => `- ${i}`).join("\n")
236
- );
240
+ return { prefix, suffix };
237
241
  }
238
242
 
239
243
  // =============================================================================
package/types.ts CHANGED
@@ -193,6 +193,8 @@ export interface RunSyncOptions {
193
193
  index?: number;
194
194
  sessionDir?: string;
195
195
  share?: boolean;
196
+ /** Override the agent's default model (format: "provider/id" or just "id") */
197
+ modelOverride?: string;
196
198
  }
197
199
 
198
200
  export interface ExtensionConfig {