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 +29 -0
- package/README.md +63 -4
- package/agents.ts +8 -0
- package/async-execution.ts +30 -4
- package/chain-clarify.ts +200 -4
- package/chain-execution.ts +16 -3
- package/execution.ts +31 -16
- package/formatters.ts +7 -2
- package/index.ts +45 -7
- package/package.json +1 -1
- package/render.ts +16 -0
- package/schemas.ts +15 -22
- package/settings.ts +40 -3
- package/skills.ts +206 -0
- package/subagent-runner.ts +5 -1
- package/types.ts +17 -1
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
|
-
|
|
14
|
+
pi install npm:pi-subagents
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
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
|
-
| `
|
|
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,
|
package/async-execution.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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]
|
|
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]
|
|
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 •
|
|
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) {
|
package/chain-execution.ts
CHANGED
|
@@ -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
|