@dreki-gg/pi-subagent 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @dreki-gg/pi-subagent
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`5e853af`](https://github.com/dreki-gg/pi-extensions/commit/5e853af054a31c4bf87d80f944513e537a39201d) Thanks [@jalbarrang](https://github.com/jalbarrang)! - Sync the extensions repo with Pi 0.68.0 and improve direct agent runs.
8
+
9
+ - `@dreki-gg/pi-context7`: remove stale alias docs and align compatibility tests with the canonical tool names actually exported.
10
+ - `@dreki-gg/pi-modes`: use `before_agent_start.systemPromptOptions.selectedTools` when available so mode prompt text reflects the active prompt tool set.
11
+ - `@dreki-gg/pi-subagent`: add `/run-agent`, support `sessionStrategy: fork-at` in agent frontmatter, default bundled `worker` and `reviewer` to forked direct runs, and add a custom renderer for run summaries.
12
+
13
+ ## 0.5.0
14
+
15
+ ### Minor Changes
16
+
17
+ - [`4f2f148`](https://github.com/dreki-gg/pi-extensions/commit/4f2f148488e32ff43f97216a5c221fd9d1716a11) Thanks [@jalbarrang](https://github.com/jalbarrang)! - added agents to the registry so my fork is able to discover subagent files
18
+
3
19
  ## 0.4.0
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -28,6 +28,12 @@ Examples:
28
28
  - "have planner make a plan, then worker implement it"
29
29
  - "send this to reviewer"
30
30
 
31
+ For direct user-invoked single-agent runs, use:
32
+
33
+ ```text
34
+ /run-agent worker implement the auth flow we discussed
35
+ ```
36
+
31
37
  `/delegate` is optional. It exists for rigid gated workflows, plan approval, and explicit workflow control.
32
38
 
33
39
  ## Optional Delegate Command
@@ -65,6 +71,24 @@ Pin a workflow and skip the project-agent confirmation prompt:
65
71
  5. **Plan gate** — shows planner output for approval before worker runs
66
72
  6. **Summary** — full phase-by-phase report with usage totals
67
73
 
74
+ ## Direct Agent Runs
75
+
76
+ Run one agent directly from the current session:
77
+
78
+ ```text
79
+ /run-agent [--scope user|project|both] [--yes-project-agents] <agent> [task]
80
+ ```
81
+
82
+ Examples:
83
+
84
+ ```text
85
+ /run-agent scout trace how auth state is loaded
86
+ /run-agent worker implement the refactor we just planned
87
+ /run-agent --scope project reviewer review the latest changes
88
+ ```
89
+
90
+ If the chosen agent frontmatter sets `sessionStrategy: fork-at`, the command clones the current active path into a new session before running the agent. That keeps long implementation runs isolated in their own branch while preserving the original conversation.
91
+
68
92
  ### Delegate Flags
69
93
 
70
94
  | Flag | Meaning |
@@ -94,6 +118,7 @@ name: my-agent
94
118
  description: What this agent does
95
119
  tools: read, grep, find, ls
96
120
  model: anthropic/claude-haiku-4-5
121
+ sessionStrategy: fork-at
97
122
  ---
98
123
 
99
124
  System prompt for the agent.
@@ -106,13 +131,17 @@ The package ships with these agents out of the box:
106
131
  - `scout` — fast codebase recon
107
132
  - `docs-scout` — Context7-first documentation lookup
108
133
  - `planner` — implementation planning
109
- - `worker` — general-purpose implementation
110
- - `reviewer` — code review
134
+ - `worker` — general-purpose implementation (`sessionStrategy: fork-at` by default)
135
+ - `reviewer` — code review (`sessionStrategy: fork-at` by default)
111
136
  - `ux-designer` — frontend UI design
112
137
 
113
138
  Resolution order is: bundled → user (`~/.pi/agent/agents/`) → project (`.pi/agents/`). Project agents override user and bundled agents by name.
114
139
 
115
- > Note: pi packages do not currently expose a first-class `agents` manifest. If you mirror package agents into `~/.pi/agent/agents/`, treat those files as managed package artifacts rather than hand-authored overrides.
140
+ Optional frontmatter:
141
+ - `thinking` — reasoning effort for the spawned pi process
142
+ - `sessionStrategy: fork-at` — when used with `/run-agent`, clone the current active branch into a new session before running
143
+
144
+ > Note: pi now supports package-shipped agents via `pi.agents` (or conventional `agents/` directories). This package publishes its bundled agents that way, while user agents in `~/.pi/agent/agents/` and project agents in `.pi/agents/` still override them by name.
116
145
 
117
146
  ### Managing Agents
118
147
 
@@ -4,6 +4,7 @@ description: Code review specialist for quality and security analysis
4
4
  tools: read, grep, find, ls, bash
5
5
  model: openai/gpt-5.4
6
6
  thinking: medium
7
+ sessionStrategy: fork-at
7
8
  ---
8
9
 
9
10
  You are a senior code reviewer. Analyze code for quality, security, and maintainability.
package/agents/worker.md CHANGED
@@ -3,6 +3,7 @@ name: worker
3
3
  description: General-purpose subagent with full capabilities, isolated context
4
4
  model: openai/gpt-5.4
5
5
  thinking: medium
6
+ sessionStrategy: fork-at
6
7
  ---
7
8
 
8
9
  You are a worker agent with full capabilities. You operate in an isolated context window to handle delegated tasks without polluting the main conversation.
@@ -1,13 +1,22 @@
1
1
  /**
2
2
  * Agent discovery and configuration
3
+ *
4
+ * Supports two discovery strategies:
5
+ * 1. Package-resolved: Uses pi's ResolvedPaths.agents from the package manager
6
+ * when available (pi forks with first-class agents resource support).
7
+ * Package agents are resolved, filtered, and toggleable via pi config.
8
+ * 2. Legacy: Manual filesystem discovery from bundled, user, and project dirs.
9
+ * Used as fallback when ResolvedPaths.agents is not available.
3
10
  */
4
11
 
5
12
  import * as fs from 'node:fs';
6
13
  import * as path from 'node:path';
7
14
  import { getAgentDir, parseFrontmatter } from '@mariozechner/pi-coding-agent';
15
+ import type { ResolvedPaths } from '@mariozechner/pi-coding-agent';
8
16
 
9
17
  export type AgentScope = 'user' | 'project' | 'both';
10
- export type AgentSource = 'bundled' | 'user' | 'project';
18
+ export type AgentSource = 'bundled' | 'user' | 'project' | 'package';
19
+ export type AgentSessionStrategy = 'inline' | 'fork-at';
11
20
 
12
21
  export interface AgentConfig {
13
22
  name: string;
@@ -15,6 +24,7 @@ export interface AgentConfig {
15
24
  tools?: string[];
16
25
  model?: string;
17
26
  thinking?: string;
27
+ sessionStrategy?: AgentSessionStrategy;
18
28
  systemPrompt: string;
19
29
  source: AgentSource;
20
30
  filePath: string;
@@ -28,6 +38,13 @@ export interface AgentDiscoveryResult {
28
38
  /** Bundled agents ship with the package (../../agents relative to extensions/subagent/) */
29
39
  const bundledAgentsDir = path.resolve(import.meta.dirname, '..', '..', 'agents');
30
40
 
41
+ function parseSessionStrategy(value?: string): AgentSessionStrategy | undefined {
42
+ if (!value) return undefined;
43
+ const normalized = value.trim().toLowerCase();
44
+ if (normalized === 'inline' || normalized === 'fork-at') return normalized;
45
+ return undefined;
46
+ }
47
+
31
48
  function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
32
49
  const agents: AgentConfig[] = [];
33
50
 
@@ -71,6 +88,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
71
88
  tools: tools && tools.length > 0 ? tools : undefined,
72
89
  model: frontmatter.model,
73
90
  thinking: frontmatter.thinking,
91
+ sessionStrategy: parseSessionStrategy(frontmatter.sessionStrategy),
74
92
  systemPrompt: body,
75
93
  source,
76
94
  filePath,
@@ -100,6 +118,57 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
100
118
  }
101
119
  }
102
120
 
121
+ /**
122
+ * Load agents from package-manager resolved paths (enabled only).
123
+ * This is called with pre-resolved paths from an async context.
124
+ */
125
+ export function loadAgentsFromResolvedPaths(resolvedPaths: ResolvedPaths): AgentConfig[] {
126
+ // Check if the agents field exists (compatibility with upstream pi)
127
+ const agentResources = (resolvedPaths as unknown as Record<string, unknown>).agents;
128
+ if (!agentResources || !Array.isArray(agentResources)) {
129
+ return [];
130
+ }
131
+
132
+ const agents: AgentConfig[] = [];
133
+ for (const resource of agentResources) {
134
+ if (!resource.enabled) continue;
135
+ const agent = loadAgentFromFile(resource.path, 'package');
136
+ if (agent) agents.push(agent);
137
+ }
138
+ return agents;
139
+ }
140
+
141
+ function loadAgentFromFile(filePath: string, source: AgentSource): AgentConfig | null {
142
+ let content: string;
143
+ try {
144
+ content = fs.readFileSync(filePath, 'utf-8');
145
+ } catch {
146
+ return null;
147
+ }
148
+
149
+ const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
150
+ if (!frontmatter.name || !frontmatter.description) {
151
+ return null;
152
+ }
153
+
154
+ const tools = frontmatter.tools
155
+ ?.split(',')
156
+ .map((t: string) => t.trim())
157
+ .filter(Boolean);
158
+
159
+ return {
160
+ name: frontmatter.name,
161
+ description: frontmatter.description,
162
+ tools: tools && tools.length > 0 ? tools : undefined,
163
+ model: frontmatter.model,
164
+ thinking: frontmatter.thinking,
165
+ sessionStrategy: parseSessionStrategy(frontmatter.sessionStrategy),
166
+ systemPrompt: body,
167
+ source,
168
+ filePath,
169
+ };
170
+ }
171
+
103
172
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
104
173
  const userDir = path.join(getAgentDir(), 'agents');
105
174
  const projectAgentsDir = findNearestProjectAgentsDir(cwd);
@@ -128,6 +197,34 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
128
197
  return { agents: Array.from(agentMap.values()), projectAgentsDir };
129
198
  }
130
199
 
200
+ /**
201
+ * Discover agents with package-resolved paths merged in.
202
+ * Package-resolved agents (from pi.agents in installed packages) are loaded
203
+ * at lowest priority, then overridden by bundled, user, and project agents.
204
+ *
205
+ * Falls back to discoverAgents() when resolvedPaths is not provided or
206
+ * does not contain the agents field (upstream pi compatibility).
207
+ */
208
+ export function discoverAgentsWithPackages(
209
+ cwd: string,
210
+ scope: AgentScope,
211
+ resolvedPaths?: ResolvedPaths,
212
+ ): AgentDiscoveryResult {
213
+ const base = discoverAgents(cwd, scope);
214
+
215
+ if (!resolvedPaths) return base;
216
+
217
+ const packageAgents = loadAgentsFromResolvedPaths(resolvedPaths);
218
+ if (packageAgents.length === 0) return base;
219
+
220
+ // Package agents are lowest priority: existing agents override by name
221
+ const agentMap = new Map<string, AgentConfig>();
222
+ for (const agent of packageAgents) agentMap.set(agent.name, agent);
223
+ for (const agent of base.agents) agentMap.set(agent.name, agent);
224
+
225
+ return { agents: Array.from(agentMap.values()), projectAgentsDir: base.projectAgentsDir };
226
+ }
227
+
131
228
  export { bundledAgentsDir };
132
229
 
133
230
  export function formatAgentList(
@@ -8,6 +8,13 @@ export interface DelegateCommandOptions {
8
8
  explicitTask?: string;
9
9
  }
10
10
 
11
+ export interface RunAgentCommandOptions {
12
+ agentScope: AgentScope;
13
+ confirmProjectAgents: boolean;
14
+ agentName?: string;
15
+ explicitTask?: string;
16
+ }
17
+
11
18
  const WORKFLOW_IDS = new Set<WorkflowId>([
12
19
  'scout-only',
13
20
  'scout-and-plan',
@@ -82,3 +89,57 @@ export function parseDelegateArgs(rawArgs?: string):
82
89
  },
83
90
  };
84
91
  }
92
+
93
+ export function formatRunAgentUsage(): string {
94
+ return 'Usage: /run-agent [--scope user|project|both] [--yes-project-agents] <agent> [task]';
95
+ }
96
+
97
+ export function parseRunAgentArgs(rawArgs?: string):
98
+ | { ok: true; options: RunAgentCommandOptions }
99
+ | { ok: false; error: string } {
100
+ const tokens = tokenize(rawArgs?.trim() ?? '');
101
+ const taskTokens: string[] = [];
102
+
103
+ let agentScope: AgentScope = 'user';
104
+ let confirmProjectAgents = true;
105
+ let agentName: string | undefined;
106
+
107
+ for (let i = 0; i < tokens.length; i++) {
108
+ const token = tokens[i];
109
+
110
+ if (token === '--scope') {
111
+ const value = tokens[++i];
112
+ if (!value || !['user', 'project', 'both'].includes(value)) {
113
+ return { ok: false, error: `Invalid --scope value.\n\n${formatRunAgentUsage()}` };
114
+ }
115
+ agentScope = value as AgentScope;
116
+ continue;
117
+ }
118
+
119
+ if (token === '--yes-project-agents') {
120
+ confirmProjectAgents = false;
121
+ continue;
122
+ }
123
+
124
+ if (token.startsWith('--')) {
125
+ return { ok: false, error: `Unknown option: ${token}\n\n${formatRunAgentUsage()}` };
126
+ }
127
+
128
+ if (!agentName) {
129
+ agentName = token;
130
+ continue;
131
+ }
132
+
133
+ taskTokens.push(token);
134
+ }
135
+
136
+ return {
137
+ ok: true,
138
+ options: {
139
+ agentScope,
140
+ confirmProjectAgents,
141
+ agentName,
142
+ explicitTask: taskTokens.join(' ').trim() || undefined,
143
+ },
144
+ };
145
+ }
@@ -3,8 +3,9 @@ import * as fs from 'node:fs';
3
3
  import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { withFileMutationQueue } from '@mariozechner/pi-coding-agent';
6
+ import type { ResolvedPaths } from '@mariozechner/pi-coding-agent';
6
7
  import type { Message } from '@mariozechner/pi-ai';
7
- import { discoverAgents, type AgentScope } from './agents';
8
+ import { discoverAgentsWithPackages, type AgentScope } from './agents';
8
9
  import type { AgentResult, UsageStats } from './delegate-types';
9
10
 
10
11
  function emptyUsage(): UsageStats {
@@ -45,6 +46,7 @@ export interface RunAgentOptions {
45
46
  onUpdate?: OnPhaseUpdate;
46
47
  phaseName?: string;
47
48
  signal?: AbortSignal;
49
+ resolvedPaths?: ResolvedPaths;
48
50
  }
49
51
 
50
52
  export async function runAgent(
@@ -53,7 +55,7 @@ export async function runAgent(
53
55
  task: string,
54
56
  options: RunAgentOptions = {},
55
57
  ): Promise<AgentResult> {
56
- const { agents } = discoverAgents(cwd, options.agentScope ?? 'user');
58
+ const { agents } = discoverAgentsWithPackages(cwd, options.agentScope ?? 'user', options.resolvedPaths);
57
59
  const agent = agents.find((a) => a.name === agentName);
58
60
 
59
61
  if (!agent) {
@@ -25,11 +25,14 @@ import { StringEnum } from '@mariozechner/pi-ai';
25
25
  import {
26
26
  type ExtensionAPI,
27
27
  type ExtensionCommandContext,
28
+ DefaultPackageManager,
28
29
  getAgentDir,
29
30
  getMarkdownTheme,
31
+ SettingsManager,
30
32
  withFileMutationQueue,
31
33
  } from '@mariozechner/pi-coding-agent';
32
- import { Container, Markdown, Spacer, Text } from '@mariozechner/pi-tui';
34
+ import type { ResolvedPaths } from '@mariozechner/pi-coding-agent';
35
+ import { Box, Container, Markdown, Spacer, Text } from '@mariozechner/pi-tui';
33
36
  import { Type } from '@sinclair/typebox';
34
37
  import {
35
38
  type AgentConfig,
@@ -37,8 +40,14 @@ import {
37
40
  type AgentSource,
38
41
  bundledAgentsDir,
39
42
  discoverAgents,
43
+ discoverAgentsWithPackages,
40
44
  } from './agents.js';
41
- import { formatDelegateUsage, parseDelegateArgs } from './delegate-args.js';
45
+ import {
46
+ formatDelegateUsage,
47
+ formatRunAgentUsage,
48
+ parseDelegateArgs,
49
+ parseRunAgentArgs,
50
+ } from './delegate-args.js';
42
51
  import { runAgent, runParallel } from './delegate-executor.js';
43
52
  import { buildSynthesisPrompt, extractRecentConversation } from './synthesis.js';
44
53
  import type {
@@ -178,6 +187,20 @@ interface UsageStats {
178
187
  turns: number;
179
188
  }
180
189
 
190
+ interface RunAgentSummaryDetails {
191
+ agent: string;
192
+ agentSource: AgentSource;
193
+ agentScope: AgentScope;
194
+ sessionStrategy: string;
195
+ forked: boolean;
196
+ exitCode: number;
197
+ stopReason?: string;
198
+ errorMessage?: string;
199
+ usage: UsageStats;
200
+ task: string;
201
+ output: string;
202
+ }
203
+
181
204
  interface SingleResult {
182
205
  agent: string;
183
206
  agentSource: AgentSource | 'unknown';
@@ -507,6 +530,26 @@ const SubagentParams = Type.Object({
507
530
  ),
508
531
  });
509
532
 
533
+ /**
534
+ * Try to resolve package paths including the agents resource.
535
+ * Returns null if the pi version does not support ResolvedPaths.agents.
536
+ */
537
+ async function tryResolvePackagePaths(cwd: string): Promise<ResolvedPaths | undefined> {
538
+ try {
539
+ const agentDir = getAgentDir();
540
+ const settingsManager = SettingsManager.create(cwd, agentDir);
541
+ const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
542
+ const resolved = await packageManager.resolve();
543
+ // Check if this pi version has agents support
544
+ if ('agents' in resolved && Array.isArray((resolved as Record<string, unknown>).agents)) {
545
+ return resolved;
546
+ }
547
+ } catch {
548
+ // Incompatible pi version or missing API
549
+ }
550
+ return undefined;
551
+ }
552
+
510
553
  export default function (pi: ExtensionAPI) {
511
554
  pi.registerTool({
512
555
  name: 'subagent',
@@ -521,7 +564,8 @@ export default function (pi: ExtensionAPI) {
521
564
 
522
565
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
523
566
  const agentScope: AgentScope = params.agentScope ?? 'user';
524
- const discovery = discoverAgents(ctx.cwd, agentScope);
567
+ const resolvedPaths = await tryResolvePackagePaths(ctx.cwd);
568
+ const discovery = discoverAgentsWithPackages(ctx.cwd, agentScope, resolvedPaths);
525
569
  const agents = discovery.agents;
526
570
  const confirmProjectAgents = params.confirmProjectAgents ?? true;
527
571
 
@@ -1135,6 +1179,130 @@ export default function (pi: ExtensionAPI) {
1135
1179
  .join('\n\n');
1136
1180
  }
1137
1181
 
1182
+ async function confirmProjectAgentsIfNeeded(
1183
+ ctx: ExtensionCommandContext,
1184
+ agents: AgentConfig[],
1185
+ projectAgentsDir: string | null,
1186
+ confirmProjectAgents: boolean,
1187
+ ): Promise<boolean> {
1188
+ if (!ctx.hasUI || !confirmProjectAgents || agents.length === 0) return true;
1189
+
1190
+ const names = Array.from(new Set(agents.map((agent) => agent.name))).join(', ');
1191
+ const dir = projectAgentsDir ?? '(unknown)';
1192
+ return ctx.ui.confirm(
1193
+ 'Run project-local agents?',
1194
+ `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
1195
+ );
1196
+ }
1197
+
1198
+ function buildRunAgentTask(conversation: string, explicitTask?: string): string | undefined {
1199
+ const trimmedConversation = conversation.trim();
1200
+ const trimmedTask = explicitTask?.trim();
1201
+
1202
+ if (trimmedTask && trimmedConversation) {
1203
+ return `Assigned task:\n${trimmedTask}\n\nRelevant main-session context:\n${trimmedConversation}`;
1204
+ }
1205
+
1206
+ if (trimmedTask) return trimmedTask;
1207
+ if (trimmedConversation) {
1208
+ return `Use the following main-session context to determine and complete the task:\n\n${trimmedConversation}`;
1209
+ }
1210
+
1211
+ return undefined;
1212
+ }
1213
+
1214
+ function formatRunAgentSummary(options: {
1215
+ agent: AgentConfig;
1216
+ agentScope: AgentScope;
1217
+ task: string;
1218
+ result: AgentResult;
1219
+ forked: boolean;
1220
+ }): string {
1221
+ const { agent, agentScope, task, result, forked } = options;
1222
+ const output = getFinalText(result).trim();
1223
+ const usage = formatUsageStats(result.usage, result.model);
1224
+ const failed = result.exitCode !== 0 || result.stopReason === 'error';
1225
+ const status = failed ? 'failed' : 'completed';
1226
+ const lines = [
1227
+ '## Agent Run',
1228
+ `- Agent: \`${agent.name}\``,
1229
+ `- Source: ${agent.source}`,
1230
+ `- Scope: ${agentScope}`,
1231
+ `- Session strategy: ${agent.sessionStrategy ?? 'inline'}`,
1232
+ `- Session handling: ${forked ? 'forked current branch at active entry' : 'inline in current session'}`,
1233
+ `- Status: ${status}`,
1234
+ '',
1235
+ '## Task',
1236
+ task,
1237
+ '',
1238
+ '## Output',
1239
+ output || '_No output returned._',
1240
+ ];
1241
+
1242
+ if (failed) {
1243
+ const errorText = result.errorMessage?.trim() || result.stderr.trim();
1244
+ if (errorText) {
1245
+ lines.push('', '## Error', errorText);
1246
+ }
1247
+ }
1248
+
1249
+ if (usage) {
1250
+ lines.push('', '## Usage', usage);
1251
+ }
1252
+
1253
+ return lines.join('\n');
1254
+ }
1255
+
1256
+ pi.registerMessageRenderer('run-agent-summary', (message, { expanded }, theme) => {
1257
+ const details = message.details as RunAgentSummaryDetails | undefined;
1258
+ if (!details) return new Text(typeof message.content === 'string' ? message.content : '', 0, 0);
1259
+
1260
+ const failed = details.exitCode !== 0 || details.stopReason === 'error';
1261
+ const icon = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');
1262
+ const title = `${icon} ${theme.fg('toolTitle', theme.bold(details.agent))}${theme.fg('muted', ` (${details.agentSource})`)}`;
1263
+ const modeLine = theme.fg(
1264
+ 'dim',
1265
+ `${details.forked ? 'forked branch' : 'inline'} • ${details.agentScope} • ${details.sessionStrategy}`,
1266
+ );
1267
+ const usageLine = formatUsageStats(details.usage);
1268
+ const outputPreview = details.output.trim() || '(no output)';
1269
+
1270
+ if (!expanded) {
1271
+ const previewLines = outputPreview.split('\n').slice(0, 3).join('\n');
1272
+ const text = [
1273
+ title,
1274
+ modeLine,
1275
+ theme.fg('muted', `Task: ${details.task}`),
1276
+ failed && details.errorMessage ? theme.fg('error', `Error: ${details.errorMessage}`) : previewLines,
1277
+ usageLine ? theme.fg('dim', usageLine) : undefined,
1278
+ theme.fg('muted', '(Ctrl+O to expand)'),
1279
+ ]
1280
+ .filter((line): line is string => Boolean(line))
1281
+ .join('\n');
1282
+ const box = new Box(1, 1, (text) => theme.bg('customMessageBg', text));
1283
+ box.addChild(new Text(text, 0, 0));
1284
+ return box;
1285
+ }
1286
+
1287
+ const mdTheme = getMarkdownTheme();
1288
+ const box = new Box(1, 1, (text) => theme.bg('customMessageBg', text));
1289
+ box.addChild(new Text(title, 0, 0));
1290
+ box.addChild(new Text(modeLine, 0, 0));
1291
+ if (usageLine) box.addChild(new Text(theme.fg('dim', usageLine), 0, 0));
1292
+ box.addChild(new Spacer(1));
1293
+ box.addChild(new Text(theme.fg('muted', 'Task'), 0, 0));
1294
+ box.addChild(new Text(theme.fg('text', details.task), 0, 0));
1295
+ if (failed && details.errorMessage) {
1296
+ box.addChild(new Spacer(1));
1297
+ box.addChild(new Text(theme.fg('error', 'Error'), 0, 0));
1298
+ box.addChild(new Text(theme.fg('error', details.errorMessage), 0, 0));
1299
+ }
1300
+ box.addChild(new Spacer(1));
1301
+ box.addChild(new Text(theme.fg('muted', 'Output'), 0, 0));
1302
+ box.addChild(new Markdown(outputPreview, 0, 0, mdTheme));
1303
+ return box;
1304
+ });
1305
+
1138
1306
  async function executePhase(
1139
1307
  cwd: string,
1140
1308
  phase: PhaseDefinition,
@@ -1142,6 +1310,7 @@ export default function (pi: ExtensionAPI) {
1142
1310
  previousOutput: string,
1143
1311
  ctx: ExtensionCommandContext,
1144
1312
  agentScope: AgentScope,
1313
+ resolvedPaths?: ResolvedPaths,
1145
1314
  ): Promise<{ results: AgentResult[]; output: string; aborted: boolean }> {
1146
1315
  const task = phase.taskTemplate
1147
1316
  .replace(/\{synthesis\}/g, synthesis)
@@ -1160,11 +1329,13 @@ export default function (pi: ExtensionAPI) {
1160
1329
  // Streaming updates happen per-message
1161
1330
  },
1162
1331
  phaseName: phase.name,
1332
+ resolvedPaths,
1163
1333
  });
1164
1334
  } else {
1165
1335
  const result = await runAgent(cwd, phase.agents[0], task, {
1166
1336
  agentScope,
1167
1337
  phaseName: phase.name,
1338
+ resolvedPaths,
1168
1339
  });
1169
1340
  results = [result];
1170
1341
  }
@@ -1300,6 +1471,114 @@ export default function (pi: ExtensionAPI) {
1300
1471
  },
1301
1472
  });
1302
1473
 
1474
+ pi.registerCommand('run-agent', {
1475
+ description:
1476
+ 'Run a single agent directly — honors agent sessionStrategy metadata such as fork-at',
1477
+ handler: async (args, ctx) => {
1478
+ const parsed = parseRunAgentArgs(args);
1479
+ if (!parsed.ok) {
1480
+ ctx.ui.notify(parsed.error, 'warning');
1481
+ return;
1482
+ }
1483
+
1484
+ const { agentName, explicitTask, agentScope, confirmProjectAgents } = parsed.options;
1485
+ if (!agentName) {
1486
+ ctx.ui.notify(formatRunAgentUsage(), 'warning');
1487
+ return;
1488
+ }
1489
+
1490
+ const resolvedPaths = await tryResolvePackagePaths(ctx.cwd);
1491
+ const discovery = discoverAgentsWithPackages(ctx.cwd, agentScope, resolvedPaths);
1492
+ const agent = discovery.agents.find((candidate) => candidate.name === agentName);
1493
+
1494
+ if (!agent) {
1495
+ const available = discovery.agents.map((candidate) => candidate.name).join(', ') || 'none';
1496
+ ctx.ui.notify(`Unknown agent: "${agentName}". Available: ${available}`, 'warning');
1497
+ return;
1498
+ }
1499
+
1500
+ const requestedProjectAgents = agent.source === 'project' ? [agent] : [];
1501
+ const approved = await confirmProjectAgentsIfNeeded(
1502
+ ctx,
1503
+ requestedProjectAgents,
1504
+ discovery.projectAgentsDir,
1505
+ confirmProjectAgents,
1506
+ );
1507
+ if (!approved) {
1508
+ ctx.ui.notify('Run cancelled — project-local agents not approved.', 'info');
1509
+ return;
1510
+ }
1511
+
1512
+ const conversation = extractRecentConversation(ctx);
1513
+ const task = buildRunAgentTask(conversation, explicitTask);
1514
+ if (!task) {
1515
+ ctx.ui.notify(`No conversation context and no task specified.\n\n${formatRunAgentUsage()}`, 'warning');
1516
+ return;
1517
+ }
1518
+
1519
+ let forked = false;
1520
+ if (agent.sessionStrategy === 'fork-at') {
1521
+ const leafId = ctx.sessionManager.getLeafId?.();
1522
+ if (!leafId) {
1523
+ ctx.ui.notify(
1524
+ `Agent "${agent.name}" prefers fork-at, but the current session cannot be forked. Running inline instead.`,
1525
+ 'warning',
1526
+ );
1527
+ } else {
1528
+ const forkCompat = ctx.fork as unknown as (
1529
+ entryId: string,
1530
+ options?: { position: 'at' },
1531
+ ) => Promise<{ cancelled: boolean }>;
1532
+ const forkResult = await forkCompat(leafId, { position: 'at' });
1533
+ if (forkResult.cancelled) {
1534
+ ctx.ui.notify(`Run cancelled — unable to fork session for "${agent.name}".`, 'info');
1535
+ return;
1536
+ }
1537
+ forked = true;
1538
+ }
1539
+ }
1540
+
1541
+ ctx.ui.notify(
1542
+ `Running agent: ${agent.name} [${agent.source}${forked ? ', forked' : ', inline'}]`,
1543
+ 'info',
1544
+ );
1545
+
1546
+ const result = await runAgent(ctx.cwd, agent.name, task, { agentScope, resolvedPaths });
1547
+ const failed = result.exitCode !== 0 || result.stopReason === 'error';
1548
+ const output = getFinalText(result).trim();
1549
+
1550
+ ctx.ui.notify(
1551
+ failed ? `Agent "${agent.name}" failed.` : `Agent "${agent.name}" complete.`,
1552
+ failed ? 'error' : 'info',
1553
+ );
1554
+
1555
+ pi.sendMessage({
1556
+ customType: 'run-agent-summary',
1557
+ content: formatRunAgentSummary({
1558
+ agent,
1559
+ agentScope,
1560
+ task,
1561
+ result,
1562
+ forked,
1563
+ }),
1564
+ display: true,
1565
+ details: {
1566
+ agent: agent.name,
1567
+ agentSource: agent.source,
1568
+ agentScope,
1569
+ sessionStrategy: agent.sessionStrategy ?? 'inline',
1570
+ forked,
1571
+ exitCode: result.exitCode,
1572
+ stopReason: result.stopReason,
1573
+ errorMessage: result.errorMessage,
1574
+ usage: result.usage,
1575
+ task,
1576
+ output,
1577
+ } satisfies RunAgentSummaryDetails,
1578
+ });
1579
+ },
1580
+ });
1581
+
1303
1582
  pi.registerCommand('delegate', {
1304
1583
  description:
1305
1584
  'Orchestrate subagent workflows — supports --scope, --workflow, and project-local agents',
@@ -1311,6 +1590,7 @@ export default function (pi: ExtensionAPI) {
1311
1590
  }
1312
1591
 
1313
1592
  const { explicitTask, agentScope, workflowId, confirmProjectAgents } = parsed.options;
1593
+ const resolvedPaths = await tryResolvePackagePaths(ctx.cwd);
1314
1594
 
1315
1595
  // Step 1: Synthesize
1316
1596
  const conversation = extractRecentConversation(ctx);
@@ -1322,7 +1602,7 @@ export default function (pi: ExtensionAPI) {
1322
1602
  const prompt = buildSynthesisPrompt(conversation, explicitTask);
1323
1603
  ctx.ui.notify('⏳ Synthesizing task from conversation...', 'info');
1324
1604
 
1325
- const synthResult = await runAgent(ctx.cwd, 'planner', prompt, { agentScope });
1605
+ const synthResult = await runAgent(ctx.cwd, 'planner', prompt, { agentScope, resolvedPaths });
1326
1606
  const synthesis = getFinalText(synthResult);
1327
1607
 
1328
1608
  if (!synthesis.trim()) {
@@ -1350,24 +1630,22 @@ export default function (pi: ExtensionAPI) {
1350
1630
  return;
1351
1631
  }
1352
1632
 
1353
- if ((agentScope === 'project' || agentScope === 'both') && confirmProjectAgents && ctx.hasUI) {
1354
- const discovery = discoverAgents(ctx.cwd, agentScope);
1633
+ if (agentScope === 'project' || agentScope === 'both') {
1634
+ const discovery = discoverAgentsWithPackages(ctx.cwd, agentScope, resolvedPaths);
1355
1635
  const requestedProjectAgents = workflow.phases
1356
1636
  .flatMap((phase) => phase.agents)
1357
1637
  .map((name) => discovery.agents.find((agent) => agent.name === name))
1358
1638
  .filter((agent): agent is AgentConfig => agent?.source === 'project');
1359
1639
 
1360
- if (requestedProjectAgents.length > 0) {
1361
- const names = Array.from(new Set(requestedProjectAgents.map((agent) => agent.name))).join(', ');
1362
- const dir = discovery.projectAgentsDir ?? '(unknown)';
1363
- const ok = await ctx.ui.confirm(
1364
- 'Run project-local agents?',
1365
- `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
1366
- );
1367
- if (!ok) {
1368
- ctx.ui.notify('Delegate cancelled — project-local agents not approved.', 'info');
1369
- return;
1370
- }
1640
+ const ok = await confirmProjectAgentsIfNeeded(
1641
+ ctx,
1642
+ requestedProjectAgents,
1643
+ discovery.projectAgentsDir,
1644
+ confirmProjectAgents,
1645
+ );
1646
+ if (!ok) {
1647
+ ctx.ui.notify('Delegate cancelled — project-local agents not approved.', 'info');
1648
+ return;
1371
1649
  }
1372
1650
  }
1373
1651
 
@@ -1391,6 +1669,7 @@ export default function (pi: ExtensionAPI) {
1391
1669
  previousOutput,
1392
1670
  ctx,
1393
1671
  agentScope,
1672
+ resolvedPaths,
1394
1673
  );
1395
1674
 
1396
1675
  const phase: PhaseResult = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreki-gg/pi-subagent",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Subagent tool and delegate orchestration for pi — isolated agents, parallel scouts, planning gates, and workflow presets",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -28,6 +28,9 @@
28
28
  ],
29
29
  "prompts": [
30
30
  "./prompts"
31
+ ],
32
+ "agents": [
33
+ "./agents"
31
34
  ]
32
35
  },
33
36
  "devDependencies": {