pi-subagents 0.4.1 → 0.5.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,35 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.5.1] - 2026-01-27
6
+
7
+ ### Fixed
8
+ - Google API compatibility: Use `Type.Any()` for mixed-type unions (`SkillOverride`, `output`, `reads`, `ChainItem`) to avoid unsupported `anyOf`/`const` JSON Schema patterns
9
+
10
+ ## [0.5.0] - 2026-01-27
11
+
12
+ ### Added
13
+ - **Skill support** - Agents can declare skills in frontmatter that get injected into system prompts
14
+ - Agent frontmatter: `skill: tmux, chrome-devtools` (comma-separated)
15
+ - Runtime override: `skill: "name"` or `skill: false` to disable all skills
16
+ - Chain-level skills additive to agent skills, step-level override supported
17
+ - Skills injected as XML: `<skill name="...">content</skill>` after agent system prompt
18
+ - Missing skills warn but continue execution (warning shown in result summary)
19
+ - **TUI skill selector** - Press `[s]` to browse and select skills for any step
20
+ - Multi-select with space bar
21
+ - Fuzzy search by name or description
22
+ - Shows skill source (project/user) and description
23
+ - Project skills (`.pi/skills/`) override user skills (`~/.pi/agent/skills/`)
24
+ - **Skill display** - Skills shown in TUI, progress tracking, summary, artifacts, and async status
25
+ - **Parallel task skills** - Each parallel task can specify its own skills via `skill` parameter
26
+
27
+ ### Fixed
28
+ - **Chain summary formatting** - Fixed extra blank line when no skills are present
29
+ - **Duplicate skill deduplication** - `skill: "foo,foo"` now correctly deduplicates to `["foo"]`
30
+ - **Consistent skill tracking in async mode** - Both chain and single modes now track only resolved skills
31
+
32
+ ## [0.4.1] - 2026-01-26
33
+
5
34
  ### Changed
6
35
  - Added `pi-package` keyword for npm discoverability (pi v0.50.0 package system)
7
36
 
package/README.md CHANGED
@@ -11,10 +11,10 @@ https://github.com/user-attachments/assets/702554ec-faaf-4635-80aa-fb5d6e292fd1
11
11
  ## Installation
12
12
 
13
13
  ```bash
14
- npx pi-subagents
14
+ pi install npm:pi-subagents
15
15
  ```
16
16
 
17
- This clones the extension to `~/.pi/agent/extensions/subagent/`. To update, run the same command. To remove:
17
+ To remove:
18
18
 
19
19
  ```bash
20
20
  npx pi-subagents --remove
@@ -22,9 +22,10 @@ npx pi-subagents --remove
22
22
 
23
23
  ## Features (beyond base)
24
24
 
25
+ - **Skill Injection**: Agents declare skills in frontmatter; skills get injected into system prompts
25
26
  - **Parallel-in-Chain**: Fan-out/fan-in patterns with `{ parallel: [...] }` steps within chains
26
27
  - **Chain Clarification TUI**: Interactive preview/edit of chain templates and behaviors before execution
27
- - **Agent Frontmatter Extensions**: Agents declare default chain behavior (`output`, `defaultReads`, `defaultProgress`)
28
+ - **Agent Frontmatter Extensions**: Agents declare default chain behavior (`output`, `defaultReads`, `defaultProgress`, `skill`)
28
29
  - **Chain Artifacts**: Shared directory at `/tmp/pi-chain-runs/{runId}/` for inter-step files
29
30
  - **Solo Agent Output**: Agents with `output` write to temp dir and return path to caller
30
31
  - **Live Progress Display**: Real-time visibility during sync execution showing current tool, recent output, tokens, and duration
@@ -66,6 +67,7 @@ Single and parallel modes also support the clarify TUI for previewing/editing pa
66
67
  - `e` - Edit task/template (all modes)
67
68
  - `m` - Select model (all modes)
68
69
  - `t` - Select thinking level (all modes)
70
+ - `s` - Select skills (all modes)
69
71
  - `w` - Edit writes/output file (single, chain only)
70
72
  - `r` - Edit reads list (chain only)
71
73
  - `p` - Toggle progress tracking (chain only)
@@ -81,6 +83,13 @@ Single and parallel modes also support the clarify TUI for previewing/editing pa
81
83
  - `Enter` - Select level
82
84
  - `Esc` - Cancel (keep current level)
83
85
 
86
+ *Skill selector mode:*
87
+ - `↑↓` - Navigate skill list
88
+ - `Space` - Toggle skill selection
89
+ - `Enter` - Confirm selection
90
+ - `Esc` - Cancel (keep current skills)
91
+ - Type to filter (fuzzy search by name or description)
92
+
84
93
  *Edit mode (full-screen editor with word wrapping):*
85
94
  - `Esc` - Save changes and exit
86
95
  - `Ctrl+C` - Discard changes and exit
@@ -100,6 +109,7 @@ name: scout
100
109
  description: Fast codebase recon
101
110
  tools: read, grep, find, ls, bash
102
111
  model: claude-haiku-4-5
112
+ skill: safe-bash, chrome-devtools # comma-separated skills to inject
103
113
  output: context.md # writes to {chain_dir}/context.md
104
114
  defaultReads: context.md # comma-separated files to read
105
115
  defaultProgress: true # maintain progress.md
@@ -109,6 +119,44 @@ interactive: true # (parsed but not enforced in v1)
109
119
 
110
120
  **Resolution priority:** step override > agent frontmatter > disabled
111
121
 
122
+ ## Skills
123
+
124
+ Skills are specialized instructions loaded from SKILL.md files and injected into the agent's system prompt.
125
+
126
+ **Skill locations:**
127
+ - Project: `.pi/skills/{name}/SKILL.md` (higher priority)
128
+ - User: `~/.pi/agent/skills/{name}/SKILL.md`
129
+
130
+ **Usage:**
131
+ ```typescript
132
+ // Agent with skills from frontmatter
133
+ { agent: "scout", task: "..." } // uses agent's default skills
134
+
135
+ // Override skills at runtime
136
+ { agent: "scout", task: "...", skill: "tmux, safe-bash" }
137
+
138
+ // Disable all skills (including agent defaults)
139
+ { agent: "scout", task: "...", skill: false }
140
+
141
+ // Chain with chain-level skills (additive to agent skills)
142
+ { chain: [...], skill: "code-review" }
143
+
144
+ // Chain step with skill override
145
+ { chain: [
146
+ { agent: "scout", skill: "safe-bash" }, // only safe-bash
147
+ { agent: "worker", skill: false } // no skills at all
148
+ ]}
149
+ ```
150
+
151
+ **Skill injection format:**
152
+ ```xml
153
+ <skill name="safe-bash">
154
+ [skill content from SKILL.md, frontmatter stripped]
155
+ </skill>
156
+ ```
157
+
158
+ **Missing skills:** If a skill cannot be found, execution continues with a warning shown in the result summary.
159
+
112
160
  ## Usage
113
161
 
114
162
  **subagent tool:**
@@ -172,7 +220,8 @@ interactive: true # (parsed but not enforced in v1)
172
220
  | `agent` | string | - | Agent name (single mode) |
173
221
  | `task` | string | - | Task string (single mode) |
174
222
  | `output` | `string \| false` | agent default | Override output file for single agent |
175
- | `tasks` | `{agent, task, cwd?}[]` | - | Parallel tasks (sync only) |
223
+ | `skill` | `string \| string[] \| false` | agent default | Override skills (comma-separated string, array, or false to disable) |
224
+ | `tasks` | `{agent, task, cwd?, skill?}[]` | - | Parallel tasks (sync only) |
176
225
  | `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
177
226
  | `clarify` | boolean | true (chains) | Show TUI to preview/edit chain; implies sync mode |
178
227
  | `agentScope` | `"user" \| "project" \| "both"` | `user` | Agent discovery scope |
@@ -196,6 +245,7 @@ interactive: true # (parsed but not enforced in v1)
196
245
  | `output` | `string \| false` | agent default | Override output filename or disable |
197
246
  | `reads` | `string[] \| false` | agent default | Override files to read from chain dir |
198
247
  | `progress` | boolean | agent default | Override progress.md tracking |
248
+ | `skill` | `string \| string[] \| false` | agent default | Override skills or disable all |
199
249
 
200
250
  *Parallel step fields:*
201
251
 
@@ -215,6 +265,7 @@ interactive: true # (parsed but not enforced in v1)
215
265
  | `output` | `string \| false` | agent default | Override output (namespaced to parallel-N/M-agent/) |
216
266
  | `reads` | `string[] \| false` | agent default | Override files to read |
217
267
  | `progress` | boolean | agent default | Override progress tracking |
268
+ | `skill` | `string \| string[] \| false` | agent default | Override skills or disable all |
218
269
 
219
270
  Status tool:
220
271
 
@@ -318,9 +369,17 @@ Legacy events (still emitted):
318
369
  ```
319
370
  ├── index.ts # Main extension (registerTool)
320
371
  ├── agents.ts # Agent discovery + frontmatter parsing
372
+ ├── skills.ts # Skill resolution, caching, and discovery
321
373
  ├── settings.ts # Chain behavior resolution, templates, chain dir
322
374
  ├── chain-clarify.ts # TUI component for chain clarification
375
+ ├── chain-execution.ts # Chain orchestration (sequential + parallel)
376
+ ├── async-execution.ts # Async/background execution support
377
+ ├── execution.ts # Core runSync for single agent execution
378
+ ├── render.ts # TUI rendering (widget, tool result display)
323
379
  ├── artifacts.ts # Artifact management
380
+ ├── formatters.ts # Output formatting utilities
381
+ ├── schemas.ts # TypeBox parameter schemas
382
+ ├── utils.ts # Shared utility functions
324
383
  ├── types.ts # Shared types
325
384
  ├── subagent-runner.ts # Async runner
326
385
  └── notify.ts # Async completion notifications
package/agents.ts CHANGED
@@ -16,6 +16,7 @@ export interface AgentConfig {
16
16
  systemPrompt: string;
17
17
  source: "user" | "project";
18
18
  filePath: string;
19
+ skills?: string[];
19
20
  // Chain behavior fields
20
21
  output?: string;
21
22
  defaultReads?: string[];
@@ -101,6 +102,12 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
101
102
  .map((f) => f.trim())
102
103
  .filter(Boolean);
103
104
 
105
+ const skillStr = frontmatter.skill || frontmatter.skills;
106
+ const skills = skillStr
107
+ ?.split(",")
108
+ .map((s) => s.trim())
109
+ .filter(Boolean);
110
+
104
111
  agents.push({
105
112
  name: frontmatter.name,
106
113
  description: frontmatter.description,
@@ -109,6 +116,7 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
109
116
  systemPrompt: body,
110
117
  source,
111
118
  filePath,
119
+ skills: skills && skills.length > 0 ? skills : undefined,
112
120
  // Chain behavior fields
113
121
  output: frontmatter.output,
114
122
  defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined,
@@ -10,7 +10,8 @@ import { fileURLToPath } from "node:url";
10
10
  import { createRequire } from "node:module";
11
11
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
12
  import type { AgentConfig } from "./agents.js";
13
- import { isParallelStep, type ChainStep, type SequentialStep } from "./settings.js";
13
+ import { isParallelStep, resolveStepBehavior, type ChainStep, type SequentialStep, type StepOverrides } from "./settings.js";
14
+ import { buildSkillInjection, normalizeSkillInput, resolveSkills } from "./skills.js";
14
15
  import {
15
16
  type ArtifactConfig,
16
17
  type Details,
@@ -44,6 +45,7 @@ export interface AsyncChainParams {
44
45
  artifactConfig: ArtifactConfig;
45
46
  shareEnabled: boolean;
46
47
  sessionRoot?: string;
48
+ chainSkills?: string[];
47
49
  }
48
50
 
49
51
  export interface AsyncSingleParams {
@@ -57,6 +59,7 @@ export interface AsyncSingleParams {
57
59
  artifactConfig: ArtifactConfig;
58
60
  shareEnabled: boolean;
59
61
  sessionRoot?: string;
62
+ skills?: string[];
60
63
  }
61
64
 
62
65
  export interface AsyncExecutionResult {
@@ -99,6 +102,7 @@ export function executeAsyncChain(
99
102
  params: AsyncChainParams,
100
103
  ): AsyncExecutionResult {
101
104
  const { chain, agents, ctx, cwd, maxOutput, artifactsDir, artifactConfig, shareEnabled, sessionRoot } = params;
105
+ const chainSkills = params.chainSkills ?? [];
102
106
 
103
107
  // Async mode doesn't support parallel steps (v1 limitation)
104
108
  const hasParallelInChain = chain.some(isParallelStep);
@@ -129,8 +133,19 @@ export function executeAsyncChain(
129
133
  fs.mkdirSync(asyncDir, { recursive: true });
130
134
  } catch {}
131
135
 
132
- const steps = seqSteps.map((s, i) => {
136
+ const steps = seqSteps.map((s) => {
133
137
  const a = agents.find((x) => x.name === s.agent)!;
138
+ const stepSkillInput = normalizeSkillInput(s.skill);
139
+ const stepOverrides: StepOverrides = { skills: stepSkillInput };
140
+ const behavior = resolveStepBehavior(a, stepOverrides, chainSkills);
141
+ const skillNames = behavior.skills === false ? [] : behavior.skills;
142
+ const { resolved: resolvedSkills } = resolveSkills(skillNames, ctx.cwd);
143
+
144
+ let systemPrompt = a.systemPrompt?.trim() || null;
145
+ if (resolvedSkills.length > 0) {
146
+ const injection = buildSkillInjection(resolvedSkills);
147
+ systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
148
+ }
134
149
  return {
135
150
  agent: s.agent,
136
151
  // First step validated to have task; others default to {previous} (replaced by runner)
@@ -138,7 +153,9 @@ export function executeAsyncChain(
138
153
  cwd: s.cwd,
139
154
  model: a.model,
140
155
  tools: a.tools,
141
- systemPrompt: a.systemPrompt?.trim() || null,
156
+ systemPrompt,
157
+ // Only track skills that were actually resolved (consistent with single mode)
158
+ skills: resolvedSkills.map((r) => r.name),
142
159
  };
143
160
  });
144
161
 
@@ -200,6 +217,13 @@ export function executeAsyncSingle(
200
217
  params: AsyncSingleParams,
201
218
  ): AsyncExecutionResult {
202
219
  const { agent, task, agentConfig, ctx, cwd, maxOutput, artifactsDir, artifactConfig, shareEnabled, sessionRoot } = params;
220
+ const skillNames = params.skills ?? agentConfig.skills ?? [];
221
+ const { resolved: resolvedSkills } = resolveSkills(skillNames, ctx.cwd);
222
+ let systemPrompt = agentConfig.systemPrompt?.trim() || null;
223
+ if (resolvedSkills.length > 0) {
224
+ const injection = buildSkillInjection(resolvedSkills);
225
+ systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
226
+ }
203
227
 
204
228
  const asyncDir = path.join(ASYNC_DIR, id);
205
229
  try {
@@ -217,7 +241,9 @@ export function executeAsyncSingle(
217
241
  cwd,
218
242
  model: agentConfig.model,
219
243
  tools: agentConfig.tools,
220
- systemPrompt: agentConfig.systemPrompt?.trim() || null,
244
+ systemPrompt,
245
+ // Only track skills that were actually resolved
246
+ skills: resolvedSkills.map((r) => r.name),
221
247
  },
222
248
  ],
223
249
  resultPath: path.join(RESULTS_DIR, `${id}.json`),
package/chain-clarify.ts CHANGED
@@ -27,6 +27,7 @@ export interface BehaviorOverride {
27
27
  reads?: string[] | false;
28
28
  progress?: boolean;
29
29
  model?: string; // Override agent's default model (format: "provider/id")
30
+ skills?: string[] | false;
30
31
  }
31
32
 
32
33
  export interface ChainClarifyResult {
@@ -36,7 +37,7 @@ export interface ChainClarifyResult {
36
37
  behaviorOverrides: (BehaviorOverride | undefined)[];
37
38
  }
38
39
 
39
- type EditMode = "template" | "output" | "reads" | "model" | "thinking";
40
+ type EditMode = "template" | "output" | "reads" | "model" | "thinking" | "skills";
40
41
 
41
42
  /** Valid thinking levels */
42
43
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
@@ -73,6 +74,12 @@ export class ChainClarifyComponent implements Component {
73
74
  /** Thinking level selector state */
74
75
  private thinkingSelectedIndex: number = 0;
75
76
 
77
+ /** Skill selector state */
78
+ private skillSearchQuery: string = "";
79
+ private skillSelectedNames: Set<string> = new Set();
80
+ private skillCursorIndex: number = 0;
81
+ private filteredSkills: Array<{ name: string; source: string; description?: string }> = [];
82
+
76
83
  constructor(
77
84
  private tui: TUI,
78
85
  private theme: Theme,
@@ -82,11 +89,13 @@ export class ChainClarifyComponent implements Component {
82
89
  private chainDir: string | undefined, // undefined for single/parallel modes
83
90
  private resolvedBehaviors: ResolvedStepBehavior[],
84
91
  private availableModels: ModelInfo[],
92
+ private availableSkills: Array<{ name: string; source: string; description?: string }>,
85
93
  private done: (result: ChainClarifyResult) => void,
86
94
  private mode: ClarifyMode = 'chain', // Mode: 'single', 'parallel', or 'chain'
87
95
  ) {
88
96
  // Initialize filtered models
89
97
  this.filteredModels = [...availableModels];
98
+ this.filteredSkills = [...availableSkills];
90
99
  }
91
100
 
92
101
  // ─────────────────────────────────────────────────────────────────────────────
@@ -290,6 +299,7 @@ export class ChainClarifyComponent implements Component {
290
299
  output: override.output !== undefined ? override.output : base.output,
291
300
  reads: override.reads !== undefined ? override.reads : base.reads,
292
301
  progress: override.progress !== undefined ? override.progress : base.progress,
302
+ skills: override.skills !== undefined ? override.skills : base.skills,
293
303
  model: override.model,
294
304
  };
295
305
  }
@@ -340,6 +350,8 @@ export class ChainClarifyComponent implements Component {
340
350
  this.handleModelSelectorInput(data);
341
351
  } else if (this.editMode === "thinking") {
342
352
  this.handleThinkingSelectorInput(data);
353
+ } else if (this.editMode === "skills") {
354
+ this.handleSkillSelectorInput(data);
343
355
  } else {
344
356
  this.handleEditInput(data);
345
357
  }
@@ -393,6 +405,22 @@ export class ChainClarifyComponent implements Component {
393
405
  return;
394
406
  }
395
407
 
408
+ // 's' to select skills (all modes)
409
+ if (data === "s") {
410
+ this.editingStep = this.selectedStep;
411
+ this.editMode = "skills";
412
+ this.skillSearchQuery = "";
413
+ this.skillCursorIndex = 0;
414
+ this.filteredSkills = [...this.availableSkills];
415
+ const current = this.getEffectiveBehavior(this.selectedStep).skills;
416
+ this.skillSelectedNames.clear();
417
+ if (current !== false && current.length > 0) {
418
+ current.forEach((skillName) => this.skillSelectedNames.add(skillName));
419
+ }
420
+ this.tui.requestRender();
421
+ return;
422
+ }
423
+
396
424
  // 'w' to edit writes (single and chain only - not parallel)
397
425
  if (data === "w" && this.mode !== 'parallel') {
398
426
  this.enterEditMode("output");
@@ -607,6 +635,84 @@ export class ChainClarifyComponent implements Component {
607
635
  this.updateBehavior(stepIndex, "model", newModel);
608
636
  }
609
637
 
638
+ private filterSkills(): void {
639
+ const query = this.skillSearchQuery.toLowerCase();
640
+ if (!query) {
641
+ this.filteredSkills = [...this.availableSkills];
642
+ } else {
643
+ this.filteredSkills = this.availableSkills.filter((s) =>
644
+ s.name.toLowerCase().includes(query) ||
645
+ (s.description?.toLowerCase().includes(query) ?? false),
646
+ );
647
+ }
648
+ this.skillCursorIndex = Math.min(this.skillCursorIndex, Math.max(0, this.filteredSkills.length - 1));
649
+ }
650
+
651
+ private handleSkillSelectorInput(data: string): void {
652
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
653
+ this.exitEditMode();
654
+ return;
655
+ }
656
+
657
+ if (matchesKey(data, "return")) {
658
+ const selected = [...this.skillSelectedNames];
659
+ this.updateBehavior(this.editingStep!, "skills", selected);
660
+ this.exitEditMode();
661
+ return;
662
+ }
663
+
664
+ if (data === " ") {
665
+ if (this.filteredSkills.length > 0) {
666
+ const skill = this.filteredSkills[this.skillCursorIndex];
667
+ if (skill) {
668
+ if (this.skillSelectedNames.has(skill.name)) {
669
+ this.skillSelectedNames.delete(skill.name);
670
+ } else {
671
+ this.skillSelectedNames.add(skill.name);
672
+ }
673
+ }
674
+ }
675
+ this.tui.requestRender();
676
+ return;
677
+ }
678
+
679
+ if (matchesKey(data, "up")) {
680
+ if (this.filteredSkills.length > 0) {
681
+ this.skillCursorIndex = this.skillCursorIndex === 0
682
+ ? this.filteredSkills.length - 1
683
+ : this.skillCursorIndex - 1;
684
+ }
685
+ this.tui.requestRender();
686
+ return;
687
+ }
688
+
689
+ if (matchesKey(data, "down")) {
690
+ if (this.filteredSkills.length > 0) {
691
+ this.skillCursorIndex = this.skillCursorIndex === this.filteredSkills.length - 1
692
+ ? 0
693
+ : this.skillCursorIndex + 1;
694
+ }
695
+ this.tui.requestRender();
696
+ return;
697
+ }
698
+
699
+ if (matchesKey(data, "backspace")) {
700
+ if (this.skillSearchQuery.length > 0) {
701
+ this.skillSearchQuery = this.skillSearchQuery.slice(0, -1);
702
+ this.filterSkills();
703
+ }
704
+ this.tui.requestRender();
705
+ return;
706
+ }
707
+
708
+ if (data.length === 1 && data.charCodeAt(0) >= 32) {
709
+ this.skillSearchQuery += data;
710
+ this.filterSkills();
711
+ this.tui.requestRender();
712
+ return;
713
+ }
714
+ }
715
+
610
716
  private handleEditInput(data: string): void {
611
717
  const textWidth = this.width - 4; // Must match render: innerW - 2 = (width - 2) - 2
612
718
  const { lines: wrapped, starts } = this.wrapText(this.editBuffer, textWidth);
@@ -817,6 +923,9 @@ export class ChainClarifyComponent implements Component {
817
923
  if (this.editMode === "thinking") {
818
924
  return this.renderThinkingSelector();
819
925
  }
926
+ if (this.editMode === "skills") {
927
+ return this.renderSkillSelector();
928
+ }
820
929
  return this.renderFullEditMode();
821
930
  }
822
931
  // Mode-based navigation rendering
@@ -972,15 +1081,83 @@ export class ChainClarifyComponent implements Component {
972
1081
  return lines;
973
1082
  }
974
1083
 
1084
+ private renderSkillSelector(): string[] {
1085
+ const innerW = this.width - 2;
1086
+ const th = this.theme;
1087
+ const lines: string[] = [];
1088
+
1089
+ const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
1090
+ const stepLabel = this.mode === 'single'
1091
+ ? agentName
1092
+ : this.mode === 'parallel'
1093
+ ? `Task ${this.editingStep! + 1}: ${agentName}`
1094
+ : `Step ${this.editingStep! + 1}: ${agentName}`;
1095
+ lines.push(this.renderHeader(` Select Skills (${stepLabel}) `));
1096
+ lines.push(this.row(""));
1097
+
1098
+ const cursor = "\x1b[7m \x1b[27m";
1099
+ lines.push(this.row(` ${th.fg("dim", "Search: ")}${this.skillSearchQuery}${cursor}`));
1100
+ lines.push(this.row(""));
1101
+
1102
+ const selected = [...this.skillSelectedNames].join(", ") || th.fg("dim", "(none)");
1103
+ lines.push(this.row(` ${th.fg("dim", "Selected: ")}${truncateToWidth(selected, innerW - 12)}`));
1104
+ lines.push(this.row(""));
1105
+
1106
+ const selectorHeight = 10;
1107
+ if (this.filteredSkills.length === 0) {
1108
+ lines.push(this.row(` ${th.fg("dim", "No matching skills")}`));
1109
+ } else {
1110
+ let startIdx = 0;
1111
+ if (this.filteredSkills.length > selectorHeight) {
1112
+ startIdx = Math.max(0, this.skillCursorIndex - Math.floor(selectorHeight / 2));
1113
+ startIdx = Math.min(startIdx, this.filteredSkills.length - selectorHeight);
1114
+ }
1115
+ const endIdx = Math.min(startIdx + selectorHeight, this.filteredSkills.length);
1116
+
1117
+ if (startIdx > 0) {
1118
+ lines.push(this.row(` ${th.fg("dim", ` ↑ ${startIdx} more`)}`));
1119
+ }
1120
+
1121
+ for (let i = startIdx; i < endIdx; i++) {
1122
+ const skill = this.filteredSkills[i]!;
1123
+ const isCursor = i === this.skillCursorIndex;
1124
+ const isSelected = this.skillSelectedNames.has(skill.name);
1125
+
1126
+ const prefix = isCursor ? th.fg("accent", "→ ") : " ";
1127
+ const checkbox = isSelected ? th.fg("success", "[x]") : "[ ]";
1128
+ const nameText = isCursor ? th.fg("accent", skill.name) : skill.name;
1129
+ const sourceBadge = th.fg("dim", ` [${skill.source}]`);
1130
+ const desc = skill.description
1131
+ ? th.fg("dim", ` - ${truncateToWidth(skill.description, 25)}`)
1132
+ : "";
1133
+
1134
+ lines.push(this.row(` ${prefix}${checkbox} ${nameText}${sourceBadge}${desc}`));
1135
+ }
1136
+
1137
+ const remaining = this.filteredSkills.length - endIdx;
1138
+ if (remaining > 0) {
1139
+ lines.push(this.row(` ${th.fg("dim", ` ↓ ${remaining} more`)}`));
1140
+ }
1141
+ }
1142
+
1143
+ const targetHeight = 18;
1144
+ for (let i = lines.length; i < targetHeight; i++) {
1145
+ lines.push(this.row(""));
1146
+ }
1147
+
1148
+ lines.push(this.renderFooter(" [Enter] Confirm • [Space] Toggle • [Esc] Cancel "));
1149
+ return lines;
1150
+ }
1151
+
975
1152
  /** Get footer text based on mode */
976
1153
  private getFooterText(): string {
977
1154
  switch (this.mode) {
978
1155
  case 'single':
979
- return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking [w]rites ';
1156
+ return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hink [w]rite [s]kill ';
980
1157
  case 'parallel':
981
- return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking • ↑↓ Navigate ';
1158
+ return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hink [s]kill • ↑↓ Nav ';
982
1159
  case 'chain':
983
- return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking [w]rites [r]eads [p]rogress ';
1160
+ return ' [Enter] Run • [Esc] Cancel • e m t w r p s • ↑↓ Nav ';
984
1161
  }
985
1162
  }
986
1163
 
@@ -1027,6 +1204,12 @@ export class ChainClarifyComponent implements Component {
1027
1204
  const writesLabel = th.fg("dim", "writes: ");
1028
1205
  lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
1029
1206
 
1207
+ const skillsValue = behavior.skills === false
1208
+ ? th.fg("dim", "(disabled)")
1209
+ : (behavior.skills?.length ? behavior.skills.join(", ") : th.fg("dim", "(none)"));
1210
+ const skillsLabel = th.fg("dim", "skills: ");
1211
+ lines.push(this.row(` ${skillsLabel}${truncateToWidth(skillsValue, innerW - 14)}`));
1212
+
1030
1213
  lines.push(this.row(""));
1031
1214
 
1032
1215
  // Footer
@@ -1077,6 +1260,13 @@ export class ChainClarifyComponent implements Component {
1077
1260
  const modelLabel = th.fg("dim", "model: ");
1078
1261
  lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1079
1262
 
1263
+ const behavior = this.getEffectiveBehavior(i);
1264
+ const skillsValue = behavior.skills === false
1265
+ ? th.fg("dim", "(disabled)")
1266
+ : (behavior.skills?.length ? behavior.skills.join(", ") : th.fg("dim", "(none)"));
1267
+ const skillsLabel = th.fg("dim", "skills: ");
1268
+ lines.push(this.row(` ${skillsLabel}${truncateToWidth(skillsValue, innerW - 14)}`));
1269
+
1080
1270
  lines.push(this.row(""));
1081
1271
  }
1082
1272
 
@@ -1168,6 +1358,12 @@ export class ChainClarifyComponent implements Component {
1168
1358
  const readsLabel = th.fg("dim", "reads: ");
1169
1359
  lines.push(this.row(` ${readsLabel}${truncateToWidth(readsValue, innerW - 13)}`));
1170
1360
 
1361
+ const skillsValue = behavior.skills === false
1362
+ ? th.fg("dim", "(disabled)")
1363
+ : (behavior.skills?.length ? behavior.skills.join(", ") : th.fg("dim", "(none)"));
1364
+ const skillsLabel = th.fg("dim", "skills: ");
1365
+ lines.push(this.row(` ${skillsLabel}${truncateToWidth(skillsValue, innerW - 14)}`));
1366
+
1171
1367
  // Progress line - show when chain-wide progress is enabled
1172
1368
  // First step creates & updates, subsequent steps read & update
1173
1369
  if (progressEnabled) {
@@ -24,6 +24,7 @@ import {
24
24
  type ParallelTaskResult,
25
25
  type ResolvedTemplates,
26
26
  } from "./settings.js";
27
+ import { discoverAvailableSkills, normalizeSkillInput } from "./skills.js";
27
28
  import { runSync } from "./execution.js";
28
29
  import { buildChainSummary } from "./formatters.js";
29
30
  import { getFinalOutput, mapConcurrent } from "./utils.js";
@@ -72,6 +73,7 @@ export interface ChainExecutionParams {
72
73
  includeProgress?: boolean;
73
74
  clarify?: boolean;
74
75
  onUpdate?: (r: AgentToolResult<Details>) => void;
76
+ chainSkills?: string[];
75
77
  }
76
78
 
77
79
  export interface ChainExecutionResult {
@@ -98,7 +100,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
98
100
  includeProgress,
99
101
  clarify,
100
102
  onUpdate,
103
+ chainSkills: chainSkillsParam,
101
104
  } = params;
105
+ const chainSkills = chainSkillsParam ?? [];
102
106
 
103
107
  const allProgress: AgentProgress[] = [];
104
108
  const allArtifactPaths: ArtifactPaths[] = [];
@@ -138,6 +142,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
138
142
  id: m.id,
139
143
  fullId: `${m.provider}/${m.id}`,
140
144
  }));
145
+ const availableSkills = discoverAvailableSkills(ctx.cwd);
141
146
 
142
147
  if (shouldClarify) {
143
148
  // Sequential-only chain: use existing TUI
@@ -163,11 +168,12 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
163
168
  output: step.output,
164
169
  reads: step.reads,
165
170
  progress: step.progress,
171
+ skills: normalizeSkillInput(step.skill),
166
172
  }));
167
173
 
168
174
  // Pre-resolve behaviors for TUI display
169
175
  const resolvedBehaviors = agentConfigs.map((config, i) =>
170
- resolveStepBehavior(config, stepOverrides[i]!),
176
+ resolveStepBehavior(config, stepOverrides[i]!, chainSkills),
171
177
  );
172
178
 
173
179
  // Flatten templates for TUI (all strings for sequential)
@@ -184,6 +190,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
184
190
  chainDir,
185
191
  resolvedBehaviors,
186
192
  availableModels,
193
+ availableSkills,
187
194
  done,
188
195
  ),
189
196
  {
@@ -226,7 +233,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
226
233
  createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
227
234
 
228
235
  // Resolve behaviors for parallel tasks
229
- const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex);
236
+ const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills);
230
237
 
231
238
  // If any parallel task has progress enabled and progress.md hasn't been created,
232
239
  // create it now to avoid race conditions
@@ -293,6 +300,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
293
300
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
294
301
  artifactConfig,
295
302
  modelOverride: effectiveModel,
303
+ skills: behavior.skills === false ? [] : behavior.skills,
296
304
  onUpdate: onUpdate
297
305
  ? (p) => {
298
306
  // Use concat instead of spread for better performance
@@ -390,8 +398,12 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
390
398
  output: tuiOverride?.output !== undefined ? tuiOverride.output : seqStep.output,
391
399
  reads: tuiOverride?.reads !== undefined ? tuiOverride.reads : seqStep.reads,
392
400
  progress: tuiOverride?.progress !== undefined ? tuiOverride.progress : seqStep.progress,
401
+ skills:
402
+ tuiOverride?.skills !== undefined
403
+ ? tuiOverride.skills
404
+ : normalizeSkillInput(seqStep.skill),
393
405
  };
394
- const behavior = resolveStepBehavior(agentConfig, stepOverride);
406
+ const behavior = resolveStepBehavior(agentConfig, stepOverride, chainSkills);
395
407
 
396
408
  // Determine if this is the first agent to create progress.md
397
409
  const isFirstProgress = behavior.progress && !progressCreated;
@@ -431,6 +443,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
431
443
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
432
444
  artifactConfig,
433
445
  modelOverride: effectiveModel,
446
+ skills: behavior.skills === false ? [] : behavior.skills,
434
447
  onUpdate: onUpdate
435
448
  ? (p) => {
436
449
  // Use concat instead of spread for better performance