@johnnywu/pi-subagents 1.2.0 → 1.3.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,3 +1,19 @@
1
+ # [1.3.0](https://github.com/jwu/pi-subagents/compare/v1.2.0...v1.3.0) (2026-06-02)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * filter available subagents by allowed agents ([adc6025](https://github.com/jwu/pi-subagents/commit/adc60252d96ef532719a0da1c128a8e433c74bb7))
7
+ * preserve append-mode subagent context ([ada0a26](https://github.com/jwu/pi-subagents/commit/ada0a264565d686917b73126301884345f824bce))
8
+
9
+
10
+ ### Features
11
+
12
+ * add debug subagent prompt command ([1c49d43](https://github.com/jwu/pi-subagents/commit/1c49d43af9569856407e2a97d512d9d3b617e6d9))
13
+ * default subagent prompts to append ([5693c17](https://github.com/jwu/pi-subagents/commit/5693c17b2aea4244b106bff82fe7ebd5c954322c))
14
+ * export subagent debug prompts from frontmatter ([afe8b23](https://github.com/jwu/pi-subagents/commit/afe8b23700b9a86ef1db97463225cd828f884026))
15
+ * resolve package skills for subagents ([48651e1](https://github.com/jwu/pi-subagents/commit/48651e15010c501cacec9a2e71771a13c0e7c9ab))
16
+
1
17
  # [1.2.0](https://github.com/jwu/pi-subagents/compare/v1.1.0...v1.2.0) (2026-06-02)
2
18
 
3
19
 
package/README.md CHANGED
@@ -50,11 +50,11 @@ Run the code-reviewer agent on the last three commits
50
50
 
51
51
  ### Available subagents in the prompt
52
52
 
53
- The extension exposes discovered sub-agents to the model in two places:
53
+ The extension exposes discovered sub-agents to the model when the active tool set includes `subagent`:
54
54
 
55
55
  - The `subagent` tool keeps a one-line prompt guideline:
56
56
  `Available subagents: code-reviewer, refactor, test-writer`
57
- - The system prompt also gets an independent block:
57
+ - At agent-start time, the system prompt gets an independent block:
58
58
 
59
59
  ```text
60
60
  Available subagents:
@@ -63,7 +63,25 @@ Available subagents:
63
63
  - test-writer
64
64
  ```
65
65
 
66
- For sub-agents launched with `systemPrompt: replace` or `systemPrompt: append`, the same block is written into the prompt passed via `--system-prompt` or `--append-system-prompt` when that agent has the `subagent` tool.
66
+ The prompt file passed to the child process contains only the agent prompt plus skills. Runtime prompt assembly then depends on `systemPrompt` mode:
67
+
68
+ - `append`: the child process uses pi's default prompt and project context files, then appends the agent prompt.
69
+ - `replace`: the child process replaces pi's default prompt with the agent prompt and skips project context files; pi-subagents reinjects the active `Available tools` and `Guidelines` blocks at agent-start time so custom replace prompts still expose the selected tool affordances.
70
+
71
+ The `Available subagents` block is injected by the child process at agent-start time, after `PI_SUBAGENT_ALLOWED` and recursion depth filtering are applied.
72
+
73
+ ### Debug a sub-agent prompt
74
+
75
+ Set `debug: true` in an agent's frontmatter to export that sub-agent's effective runtime system prompt on each run:
76
+
77
+ ```markdown
78
+ ---
79
+ name: scout
80
+ debug: true
81
+ ---
82
+ ```
83
+
84
+ The child process writes `debug-system-prompt.md` in the project cwd. The file contains the prompt visible during `before_agent_start`, including `systemPrompt` append/replace behavior, tools/guidelines, skills, project context files in append mode, and pi-subagents' runtime `Available subagents` block when applicable.
67
85
 
68
86
  ## Agent configuration
69
87
 
@@ -76,9 +94,10 @@ Agents are Markdown files with YAML frontmatter.
76
94
  | `tools` | no | _none_ | Comma-separated tool whitelist (`read, write, bash, grep`, etc.) |
77
95
  | `model` | no | parent's model | Provider/model-id (`anthropic/claude-sonnet-4-6`) |
78
96
  | `thinking` | no | `off` | Reasoning level: `off`, `low`, `medium`, `high` |
79
- | `systemPrompt` | no | `replace` | How the body is applied: `replace` (default system prompt) or `append` |
97
+ | `systemPrompt` | no | `append` | How the body is applied: `append` (append to pi default system prompt and project context) or `replace` (replace default prompt and skip project context) |
80
98
  | `allowedAgents` | no | _all_ | Comma-separated list of sub-agents this agent may spawn |
81
99
  | `maxDepth` | no | `10` | Maximum recursion depth (`0` = no sub-agents, `1` = one level, etc.) |
100
+ | `debug` | no | `false` | When `true`, export the effective runtime system prompt to `debug-system-prompt.md` |
82
101
 
83
102
  The Markdown body after the frontmatter is the agent's system prompt.
84
103
 
@@ -91,9 +110,10 @@ description: High-level planner that delegates to specialists
91
110
  tools: subagent, read, grep, find
92
111
  model: anthropic/claude-sonnet-4-6
93
112
  thinking: high
94
- systemPrompt: replace
113
+ systemPrompt: append
95
114
  allowedAgents: code-reviewer, refactor, test-writer
96
115
  maxDepth: 2
116
+ debug: false
97
117
  ---
98
118
 
99
119
  You are an orchestrator. Break complex tasks into sub-tasks and delegate
@@ -121,7 +141,7 @@ Sub-agents can spawn their own sub-agents (if the `subagent` tool is in their wh
121
141
 
122
142
  The available-subagents prompt entries respect the same filtering: parent sessions use the currently visible agents, and child sessions only list agents allowed by their parent.
123
143
 
124
- These are passed via environment variables (`PI_SUBAGENT_DEPTH`, `PI_SUBAGENT_MAX_DEPTH`, `PI_SUBAGENT_ALLOWED`).
144
+ These are passed via environment variables (`PI_SUBAGENT_DEPTH`, `PI_SUBAGENT_MAX_DEPTH`, `PI_SUBAGENT_ALLOWED`). Child processes also receive `PI_SUBAGENT_NAME` and `PI_SUBAGENT_SYSTEM_PROMPT_MODE` so runtime hooks can distinguish append vs. replace behavior.
125
145
 
126
146
  ## Session storage
127
147
 
@@ -15,6 +15,7 @@ export interface AgentConfig {
15
15
  systemPromptMode: SystemPromptMode;
16
16
  allowedAgents?: string[];
17
17
  maxDepth: number;
18
+ debug: boolean;
18
19
  skills?: string[];
19
20
  prompt: string;
20
21
  source: AgentSource;
@@ -107,7 +108,7 @@ function parseAgentFile(content: string, filePath: string, source: AgentSource):
107
108
  throw new Error(`invalid thinking: ${data.thinking}`);
108
109
  }
109
110
 
110
- const systemPromptMode = (data.systemPrompt ?? 'replace') as SystemPromptMode;
111
+ const systemPromptMode = (data.systemPrompt ?? 'append') as SystemPromptMode;
111
112
  if (!['replace', 'append'].includes(systemPromptMode)) {
112
113
  throw new Error(`invalid systemPrompt: ${data.systemPrompt}`);
113
114
  }
@@ -117,6 +118,14 @@ function parseAgentFile(content: string, filePath: string, source: AgentSource):
117
118
  throw new Error(`invalid maxDepth: ${data.maxDepth}`);
118
119
  }
119
120
 
121
+ let debug = false;
122
+ if (data.debug !== undefined) {
123
+ if (data.debug !== 'true' && data.debug !== 'false') {
124
+ throw new Error(`invalid debug: ${data.debug}`);
125
+ }
126
+ debug = data.debug === 'true';
127
+ }
128
+
120
129
  const allowedAgents = splitCsv(data.allowedAgents);
121
130
  const skills = splitCsv(data.skills);
122
131
 
@@ -130,6 +139,7 @@ function parseAgentFile(content: string, filePath: string, source: AgentSource):
130
139
  systemPromptMode,
131
140
  allowedAgents: allowedAgents.length > 0 ? allowedAgents : undefined,
132
141
  maxDepth,
142
+ debug,
133
143
  prompt: body,
134
144
  source,
135
145
  filePath,
@@ -1,5 +1,14 @@
1
+ export type SystemPromptModeEnv = 'replace' | 'append';
2
+
1
3
  export type RecursionEnv = Partial<
2
- Record<'PI_SUBAGENT_ALLOWED' | 'PI_SUBAGENT_DEPTH' | 'PI_SUBAGENT_MAX_DEPTH', string>
4
+ Record<
5
+ | 'PI_SUBAGENT_ALLOWED'
6
+ | 'PI_SUBAGENT_DEPTH'
7
+ | 'PI_SUBAGENT_MAX_DEPTH'
8
+ | 'PI_SUBAGENT_NAME'
9
+ | 'PI_SUBAGENT_SYSTEM_PROMPT_MODE',
10
+ string
11
+ >
3
12
  >;
4
13
 
5
14
  export function parseEnvNumber(value: string | undefined): number | undefined {
@@ -24,3 +33,18 @@ export function isPastMaxDepth(env: RecursionEnv): boolean {
24
33
  const maxDepth = parseEnvNumber(env?.PI_SUBAGENT_MAX_DEPTH);
25
34
  return depth !== undefined && maxDepth !== undefined && depth > maxDepth;
26
35
  }
36
+
37
+ export function isSubagentProcess(env: RecursionEnv): boolean {
38
+ const depth = parseEnvNumber(env?.PI_SUBAGENT_DEPTH);
39
+ return depth !== undefined && depth > 0;
40
+ }
41
+
42
+ export function subagentSystemPromptMode(env: RecursionEnv): SystemPromptModeEnv | undefined {
43
+ const mode = env?.PI_SUBAGENT_SYSTEM_PROMPT_MODE;
44
+ if (mode === 'replace' || mode === 'append') return mode;
45
+ return undefined;
46
+ }
47
+
48
+ export function isSubagentReplaceSystemPrompt(env: RecursionEnv): boolean {
49
+ return isSubagentProcess(env) && subagentSystemPromptMode(env) === 'replace';
50
+ }
@@ -1,7 +1,12 @@
1
1
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
2
4
  import { loadAgentDefinitions } from './agent-loader.ts';
3
- import { allowedAgentNames, isPastMaxDepth } from './env-utils.ts';
4
- import { appendAvailableSubagentsBlock } from './subagent-prompt.ts';
5
+ import { allowedAgentNames, isPastMaxDepth, isSubagentReplaceSystemPrompt } from './env-utils.ts';
6
+ import {
7
+ appendAvailableSubagentsBlock,
8
+ appendAvailableToolsAndGuidelinesBlock,
9
+ } from './subagent-prompt.ts';
5
10
  import { registerSubagentTool } from './subagent-tool.ts';
6
11
 
7
12
  export default async function (pi: ExtensionAPI) {
@@ -17,10 +22,32 @@ export default async function (pi: ExtensionAPI) {
17
22
  : result.agents;
18
23
  const agentNames = agents.map((agent) => agent.name);
19
24
 
25
+ if (isSubagentReplaceSystemPrompt(process.env)) {
26
+ pi.on('before_agent_start', (event) => {
27
+ return {
28
+ systemPrompt: appendAvailableToolsAndGuidelinesBlock(
29
+ event.systemPrompt,
30
+ event.systemPromptOptions,
31
+ ),
32
+ };
33
+ });
34
+ }
35
+
20
36
  if (!isPastMaxDepth(process.env) && agentNames.length > 0) {
21
- pi.on('before_agent_start', (event) => ({
22
- systemPrompt: appendAvailableSubagentsBlock(event.systemPrompt, agentNames),
23
- }));
37
+ pi.on('before_agent_start', (event) => {
38
+ if (!event.systemPromptOptions.selectedTools?.includes('subagent')) return;
39
+ return {
40
+ systemPrompt: appendAvailableSubagentsBlock(event.systemPrompt, agentNames),
41
+ };
42
+ });
43
+ }
44
+
45
+ if (process.env.PI_SUBAGENT_DEBUG === 'true') {
46
+ pi.on('before_agent_start', (_event, ctx) => {
47
+ const prompt = ctx.getSystemPrompt();
48
+ const outputPath = path.join(ctx.cwd, 'debug-system-prompt.md');
49
+ fs.writeFileSync(outputPath, prompt, 'utf-8');
50
+ });
24
51
  }
25
52
 
26
53
  registerSubagentTool(pi, { agents: result.agents });
@@ -1,3 +1,9 @@
1
+ import {
2
+ DefaultPackageManager,
3
+ SettingsManager,
4
+ getAgentDir,
5
+ type ResolvedResource,
6
+ } from '@earendil-works/pi-coding-agent';
1
7
  import * as fs from 'node:fs';
2
8
  import * as os from 'node:os';
3
9
  import * as path from 'node:path';
@@ -15,8 +21,16 @@ export interface SkillResolverFs {
15
21
 
16
22
  export interface ResolveSkillsOptions {
17
23
  cwd: string;
24
+ agentDir?: string;
18
25
  globalDir?: string;
19
26
  fs?: SkillResolverFs;
27
+ packageSkillFiles?: string[];
28
+ }
29
+
30
+ export interface ResolveSkillsResult {
31
+ resolved: ResolvedSkill[];
32
+ missing: string[];
33
+ skippedPackages: string[];
20
34
  }
21
35
 
22
36
  const defaultSkillFs: SkillResolverFs = {
@@ -79,7 +93,7 @@ function parseSkillFrontmatter(content: string): { name: string; description: st
79
93
  return { name: data.name || '', description: data.description.trim() };
80
94
  }
81
95
 
82
- function findSkillFile(
96
+ function findLocalSkillFile(
83
97
  skillName: string,
84
98
  cwd: string,
85
99
  globalDir: string,
@@ -104,43 +118,111 @@ function findSkillFile(
104
118
  return undefined;
105
119
  }
106
120
 
107
- export function resolveSkills(
121
+ async function resolvePackageSkillFiles(options: ResolveSkillsOptions): Promise<{
122
+ files: string[];
123
+ skippedPackages: string[];
124
+ }> {
125
+ if (options.packageSkillFiles) {
126
+ return { files: options.packageSkillFiles, skippedPackages: [] };
127
+ }
128
+ if (options.fs) {
129
+ return { files: [], skippedPackages: [] };
130
+ }
131
+
132
+ const skippedPackages: string[] = [];
133
+ const agentDir = options.agentDir ?? getAgentDir();
134
+ const settingsManager = SettingsManager.create(options.cwd, agentDir);
135
+ const packageManager = new DefaultPackageManager({
136
+ cwd: options.cwd,
137
+ agentDir,
138
+ settingsManager,
139
+ });
140
+ const resolvedPaths = await packageManager.resolve(async (source) => {
141
+ skippedPackages.push(source);
142
+ return 'skip';
143
+ });
144
+
145
+ return {
146
+ files: resolvedPaths.skills
147
+ .filter((resource: ResolvedResource) => resource.enabled)
148
+ .map((resource: ResolvedResource) => resource.path),
149
+ skippedPackages,
150
+ };
151
+ }
152
+
153
+ function readSkill(
154
+ filePath: string,
155
+ requestedName: string,
156
+ fileSystem: SkillResolverFs,
157
+ requireNameMatch: boolean,
158
+ ): ResolvedSkill | undefined {
159
+ const content = fileSystem.readFile(filePath);
160
+ const frontmatter = parseSkillFrontmatter(content);
161
+ if (!frontmatter) return undefined;
162
+
163
+ const fileSkillName = frontmatter.name || path.basename(path.dirname(filePath));
164
+ const markdownName = path.basename(filePath, '.md');
165
+ if (requireNameMatch && fileSkillName !== requestedName && markdownName !== requestedName) {
166
+ return undefined;
167
+ }
168
+
169
+ return {
170
+ name: fileSkillName || requestedName,
171
+ description: frontmatter.description,
172
+ location: filePath,
173
+ };
174
+ }
175
+
176
+ export async function resolveSkills(
108
177
  skillNames: string[],
109
178
  options: ResolveSkillsOptions,
110
- ): { resolved: ResolvedSkill[]; missing: string[] } {
179
+ ): Promise<ResolveSkillsResult> {
111
180
  const cwd = options.cwd;
112
181
  const globalDir = options.globalDir ?? defaultGlobalSkillsDir();
113
182
  const fileSystem = options.fs ?? defaultSkillFs;
114
183
  const resolved: ResolvedSkill[] = [];
115
184
  const missing: string[] = [];
185
+ const pendingPackageResolution: string[] = [];
116
186
 
117
187
  for (const name of skillNames) {
118
188
  const trimmed = name.trim();
119
189
  if (!trimmed) continue;
120
190
 
121
- const filePath = findSkillFile(trimmed, cwd, globalDir, fileSystem);
122
- if (!filePath) {
123
- missing.push(trimmed);
124
- continue;
191
+ const localFilePath = findLocalSkillFile(trimmed, cwd, globalDir, fileSystem);
192
+ if (localFilePath) {
193
+ try {
194
+ const skill = readSkill(localFilePath, trimmed, fileSystem, false);
195
+ if (skill) {
196
+ resolved.push(skill);
197
+ continue;
198
+ }
199
+ } catch {
200
+ // Try package skills before marking the skill as missing.
201
+ }
125
202
  }
126
203
 
127
- try {
128
- const content = fileSystem.readFile(filePath);
129
- const frontmatter = parseSkillFrontmatter(content);
130
- if (!frontmatter) {
131
- missing.push(trimmed);
132
- continue;
133
- }
204
+ pendingPackageResolution.push(trimmed);
205
+ }
134
206
 
135
- resolved.push({
136
- name: frontmatter.name || trimmed,
137
- description: frontmatter.description,
138
- location: filePath,
139
- });
140
- } catch {
141
- missing.push(trimmed);
207
+ const packageSkills =
208
+ pendingPackageResolution.length > 0
209
+ ? await resolvePackageSkillFiles(options)
210
+ : { files: [], skippedPackages: [] };
211
+
212
+ for (const trimmed of pendingPackageResolution) {
213
+ let packageSkill: ResolvedSkill | undefined;
214
+ for (const filePath of packageSkills.files) {
215
+ try {
216
+ packageSkill = readSkill(filePath, trimmed, fileSystem, true);
217
+ if (packageSkill) break;
218
+ } catch {
219
+ // Try the next package skill file.
220
+ }
142
221
  }
222
+
223
+ if (packageSkill) resolved.push(packageSkill);
224
+ else missing.push(trimmed);
143
225
  }
144
226
 
145
- return { resolved, missing };
227
+ return { resolved, missing, skippedPackages: packageSkills.skippedPackages };
146
228
  }
@@ -5,7 +5,6 @@ import * as os from 'node:os';
5
5
  import * as path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import type { AgentConfig } from './agent-loader.ts';
8
- import { formatAvailableSubagentsBlock } from './subagent-prompt.ts';
9
8
  import { resolveSkills } from './skill-resolver.ts';
10
9
 
11
10
  export interface AgentUsage {
@@ -89,10 +88,38 @@ export interface RunSubagentOptions {
89
88
  now?: () => number;
90
89
  }
91
90
 
91
+ export interface BuildSubagentSystemPromptOptions {
92
+ agent: AgentConfig;
93
+ cwd: string;
94
+ agentDir?: string;
95
+ }
96
+
97
+ export interface BuildSubagentSystemPromptResult {
98
+ prompt: string;
99
+ missingSkills: string[];
100
+ skippedSkillPackages: string[];
101
+ }
102
+
92
103
  const TASK_FILE_THRESHOLD = 8000;
93
104
  const OUTPUT_MAX_BYTES = 50 * 1024;
94
105
  const OUTPUT_MAX_LINES = 2000;
95
106
 
107
+ export function availableSubagentsForAgent(
108
+ agent: AgentConfig,
109
+ candidateNames?: string[],
110
+ ): string[] {
111
+ const names = candidateNames ?? agent.allowedAgents ?? [];
112
+ const allowed = agent.allowedAgents ? new Set(agent.allowedAgents) : undefined;
113
+ const seen = new Set<string>();
114
+
115
+ return names.filter((name) => {
116
+ if (seen.has(name)) return false;
117
+ if (allowed && !allowed.has(name)) return false;
118
+ seen.add(name);
119
+ return true;
120
+ });
121
+ }
122
+
96
123
  const defaultFs: ExecutorFs = {
97
124
  makeTempDir(prefix) {
98
125
  return fs.mkdtemp(prefix);
@@ -333,6 +360,31 @@ function safeFilePart(value: string): string {
333
360
  return value.replace(/[^a-zA-Z0-9_.-]+/g, '_');
334
361
  }
335
362
 
363
+ export async function buildSubagentSystemPrompt(
364
+ options: BuildSubagentSystemPromptOptions,
365
+ ): Promise<BuildSubagentSystemPromptResult> {
366
+ let prompt = options.agent.prompt;
367
+
368
+ const missingSkills: string[] = [];
369
+ const skippedSkillPackages: string[] = [];
370
+ const skillNames = options.agent.skills;
371
+ if (skillNames && skillNames.length > 0) {
372
+ const resolvedSkills = await resolveSkills(skillNames, {
373
+ cwd: options.cwd,
374
+ agentDir: options.agentDir,
375
+ });
376
+ missingSkills.push(...resolvedSkills.missing);
377
+ skippedSkillPackages.push(...resolvedSkills.skippedPackages);
378
+
379
+ const skillInjection = formatSkillsForPrompt(resolvedSkills.resolved);
380
+ if (skillInjection) {
381
+ prompt = `${prompt}${skillInjection}`;
382
+ }
383
+ }
384
+
385
+ return { prompt, missingSkills, skippedSkillPackages };
386
+ }
387
+
336
388
  function progressFromPartialResult(partialResult: unknown): AgentProgress | undefined {
337
389
  if (!partialResult || typeof partialResult !== 'object') return undefined;
338
390
  const details = (partialResult as { details?: unknown }).details;
@@ -377,28 +429,18 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
377
429
  try {
378
430
  const promptFilePath = path.join(tempDir, 'system-prompt.md');
379
431
 
380
- let promptContent = options.agent.prompt;
381
- if (options.agent.tools.includes('subagent')) {
382
- const availableSubagentsBlock = formatAvailableSubagentsBlock(
383
- options.availableAgents ?? options.agent.allowedAgents ?? [],
384
- );
385
- if (availableSubagentsBlock) {
386
- promptContent = `${promptContent.trimEnd()}\n\n${availableSubagentsBlock}`;
387
- }
432
+ const promptResult = await buildSubagentSystemPrompt({
433
+ agent: options.agent,
434
+ cwd: options.cwd,
435
+ agentDir: options.agentDir,
436
+ });
437
+ for (const source of promptResult.skippedSkillPackages) {
438
+ console.warn(`[pi-subagents] package not installed, skipping skills: ${source}`);
388
439
  }
389
-
390
- const skillNames = options.agent.skills;
391
- if (skillNames && skillNames.length > 0) {
392
- const { resolved, missing } = resolveSkills(skillNames, { cwd: options.cwd });
393
- for (const name of missing) {
394
- console.warn(`[pi-subagents] skill not found: ${name}`);
395
- }
396
- const skillInjection = formatSkillsForPrompt(resolved);
397
- if (skillInjection) {
398
- promptContent = `${promptContent}${skillInjection}`;
399
- }
440
+ for (const name of promptResult.missingSkills) {
441
+ console.warn(`[pi-subagents] skill not found: ${name}`);
400
442
  }
401
- await fileSystem.writeFile(promptFilePath, promptContent);
443
+ await fileSystem.writeFile(promptFilePath, promptResult.prompt);
402
444
 
403
445
  let taskFilePath: string | undefined;
404
446
  if (options.task.length > TASK_FILE_THRESHOLD) {
@@ -411,16 +453,9 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
411
453
  AuthStorage.create(options.agentDir ? path.join(options.agentDir, 'auth.json') : undefined),
412
454
  options.agentDir ? path.join(options.agentDir, 'models.json') : undefined,
413
455
  );
414
- const args = [
415
- pi.entryPoint,
416
- '--mode',
417
- 'json',
418
- '-p',
419
- '--no-skills',
420
- '--no-prompt-templates',
421
- '--no-context-files',
422
- ];
456
+ const args = [pi.entryPoint, '--mode', 'json', '-p', '--no-skills', '--no-prompt-templates'];
423
457
 
458
+ if (options.agent.systemPromptMode === 'replace') args.push('--no-context-files');
424
459
  if (options.agent.model) args.push('--model', options.agent.model);
425
460
  args.push('--thinking', options.agent.thinking);
426
461
  if (options.agent.tools.length > 0) args.push('--tools', options.agent.tools.join(','));
@@ -435,10 +470,14 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
435
470
  ...process.env,
436
471
  PI_SUBAGENT_DEPTH: String(options.depth ?? 1),
437
472
  PI_SUBAGENT_MAX_DEPTH: String(options.agent.maxDepth),
473
+ PI_SUBAGENT_NAME: options.agent.name,
474
+ PI_SUBAGENT_SYSTEM_PROMPT_MODE: options.agent.systemPromptMode,
438
475
  };
439
- if (options.agent.tools.includes('subagent') && options.agent.allowedAgents?.length) {
440
- env.PI_SUBAGENT_ALLOWED = options.agent.allowedAgents.join(',');
476
+ const visibleAgents = availableSubagentsForAgent(options.agent, options.availableAgents);
477
+ if (visibleAgents.length > 0) {
478
+ env.PI_SUBAGENT_ALLOWED = visibleAgents.join(',');
441
479
  }
480
+ env.PI_SUBAGENT_DEBUG = options.agent.debug ? 'true' : 'false';
442
481
 
443
482
  const processLine = (line: string) => {
444
483
  if (!line.trim()) return;
@@ -1,3 +1,77 @@
1
+ export interface ToolGuidelinePromptOptions {
2
+ selectedTools?: string[];
3
+ toolSnippets?: Record<string, string>;
4
+ promptGuidelines?: string[];
5
+ }
6
+
7
+ function uniqueNonEmpty(values: string[]): string[] {
8
+ const seen = new Set<string>();
9
+ const result: string[] = [];
10
+ for (const value of values) {
11
+ const normalized = value.trim();
12
+ if (!normalized || seen.has(normalized)) continue;
13
+ seen.add(normalized);
14
+ result.push(normalized);
15
+ }
16
+ return result;
17
+ }
18
+
19
+ export function formatAvailableToolsAndGuidelinesBlock(
20
+ options: ToolGuidelinePromptOptions,
21
+ ): string | undefined {
22
+ const selectedTools = options.selectedTools ?? ['read', 'bash', 'edit', 'write'];
23
+ const visibleTools = selectedTools.filter((name) => options.toolSnippets?.[name]);
24
+ const toolsList =
25
+ visibleTools.length > 0
26
+ ? visibleTools.map((name) => `- ${name}: ${options.toolSnippets![name]}`).join('\n')
27
+ : '(none)';
28
+
29
+ const hasBashOnlyForFileExploration =
30
+ selectedTools.includes('bash') &&
31
+ !selectedTools.includes('grep') &&
32
+ !selectedTools.includes('find') &&
33
+ !selectedTools.includes('ls');
34
+ const guidelines = uniqueNonEmpty([
35
+ ...(hasBashOnlyForFileExploration ? ['Use bash for file operations like ls, rg, find'] : []),
36
+ ...(options.promptGuidelines ?? []),
37
+ 'Be concise in your responses',
38
+ 'Show file paths clearly when working with files',
39
+ ]);
40
+ const guidelineLines = guidelines.map((guideline) => `- ${guideline}`).join('\n');
41
+
42
+ return [
43
+ 'Available tools:',
44
+ toolsList,
45
+ '',
46
+ 'In addition to the tools above, you may have access to other custom tools depending on the project.',
47
+ '',
48
+ 'Guidelines:',
49
+ guidelineLines || '(none)',
50
+ ].join('\n');
51
+ }
52
+
53
+ function appendBeforeTrailingRuntimeMetadata(systemPrompt: string, block: string): string {
54
+ const marker = '\nCurrent date:';
55
+ const index = systemPrompt.lastIndexOf(marker);
56
+ if (index === -1) return `${systemPrompt.trimEnd()}\n\n${block}`;
57
+
58
+ const before = systemPrompt.slice(0, index).trimEnd();
59
+ const after = systemPrompt.slice(index);
60
+ return `${before}\n\n${block}${after}`;
61
+ }
62
+
63
+ export function appendAvailableToolsAndGuidelinesBlock(
64
+ systemPrompt: string,
65
+ options: ToolGuidelinePromptOptions,
66
+ ): string {
67
+ const block = formatAvailableToolsAndGuidelinesBlock(options);
68
+ if (!block || systemPrompt.includes('Available tools:') || systemPrompt.includes('Guidelines:')) {
69
+ return systemPrompt;
70
+ }
71
+
72
+ return appendBeforeTrailingRuntimeMetadata(systemPrompt, block);
73
+ }
74
+
1
75
  export function formatAvailableSubagentsBlock(agentNames: string[]): string | undefined {
2
76
  const names = [...new Set(agentNames.map((name) => name.trim()).filter(Boolean))].sort();
3
77
  if (names.length === 0) return undefined;
@@ -8,7 +8,12 @@ import {
8
8
  } from '@earendil-works/pi-coding-agent';
9
9
  import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui';
10
10
  import type { AgentConfig } from './agent-loader.ts';
11
- import { type AgentProgress, type AgentResult, runSubagent } from './subagent-executor.ts';
11
+ import {
12
+ availableSubagentsForAgent,
13
+ type AgentProgress,
14
+ type AgentResult,
15
+ runSubagent,
16
+ } from './subagent-executor.ts';
12
17
  import {
13
18
  contextUsageSeverity,
14
19
  formatSubagentCall,
@@ -250,7 +255,7 @@ export function registerSubagentTool(
250
255
  cwd: params.cwd ?? ctx.cwd,
251
256
  signal,
252
257
  depth: Number(env.PI_SUBAGENT_DEPTH ?? '0') + 1,
253
- availableAgents: availableSubagents,
258
+ availableAgents: availableSubagentsForAgent(agent, availableSubagents),
254
259
  onProgress: (progress) => onUpdate?.(toProgressResult(progress)),
255
260
  });
256
261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnnywu/pi-subagents",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Sub-agents extension for pi coding agent.",
5
5
  "homepage": "https://github.com/jwu/pi-subagents#readme",
6
6
  "repository": {