pi-subagents-lite 1.3.0 → 1.4.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.
Files changed (53) hide show
  1. package/README.md +184 -235
  2. package/package.json +1 -1
  3. package/src/{agent-discovery.ts → agents/agent-discovery.ts} +8 -5
  4. package/src/{agent-manager.ts → agents/agent-manager.ts} +34 -74
  5. package/src/{agent-runner.ts → agents/agent-runner.ts} +115 -173
  6. package/src/{agent-status.ts → agents/agent-status.ts} +4 -4
  7. package/src/agents/agent-types.ts +339 -0
  8. package/src/{default-agents.ts → agents/default-agents.ts} +2 -5
  9. package/src/{output-file.ts → agents/output-file.ts} +68 -1
  10. package/src/{tool-execution.ts → agents/tool-execution.ts} +60 -222
  11. package/src/agents/types.ts +54 -0
  12. package/src/{usage.ts → agents/usage.ts} +7 -0
  13. package/src/{config-io.ts → config/config-io.ts} +20 -3
  14. package/src/config/config-store.ts +472 -0
  15. package/src/config/types.ts +26 -0
  16. package/src/events.ts +185 -0
  17. package/src/index.ts +8 -281
  18. package/src/{model-precedence.ts → models/model-precedence.ts} +33 -0
  19. package/src/{model-selector.ts → models/model-selector.ts} +1 -1
  20. package/src/{context.ts → prompt/context.ts} +1 -1
  21. package/src/prompt/prompts.ts +180 -0
  22. package/src/prompt/skill-loader.ts +195 -0
  23. package/src/registration.ts +101 -0
  24. package/src/shell.ts +101 -0
  25. package/src/spawn/spawn-coordinator.ts +232 -0
  26. package/src/status-note.ts +10 -0
  27. package/src/types.ts +47 -71
  28. package/src/ui/agent-widget.ts +61 -49
  29. package/src/{format.ts → ui/format.ts} +64 -26
  30. package/src/ui/menu/helpers.ts +93 -0
  31. package/src/ui/menu/menu-concurrency.ts +192 -0
  32. package/src/ui/menu/menu-debug.ts +125 -0
  33. package/src/ui/menu/menu-model-settings.ts +208 -0
  34. package/src/ui/menu/menu-running-agents.ts +224 -0
  35. package/src/ui/menu/menu-spawn-options.ts +87 -0
  36. package/src/ui/menu/menu-spawn-wizard.ts +418 -0
  37. package/src/ui/menu/menu-system-prompt.ts +109 -0
  38. package/src/ui/menu/menu-widget-settings.ts +130 -0
  39. package/src/ui/menu/menus.ts +101 -0
  40. package/src/ui/menu/submenus/confirm.ts +47 -0
  41. package/src/ui/menu/submenus/model-select.ts +70 -0
  42. package/src/ui/menu/submenus/numeric-input.ts +98 -0
  43. package/src/ui/menu/wrappers/settings-list.ts +205 -0
  44. package/src/{renderer.ts → ui/renderer.ts} +7 -6
  45. package/src/{result-viewer.ts → ui/result-viewer.ts} +7 -2
  46. package/src/ui/types.ts +11 -0
  47. package/src/agent-types.ts +0 -184
  48. package/src/config-mutator.ts +0 -183
  49. package/src/menus.ts +0 -1333
  50. package/src/prompts.ts +0 -94
  51. package/src/skill-loader.ts +0 -178
  52. package/src/state.ts +0 -83
  53. /package/src/{worktree-validator.ts → spawn/worktree-validator.ts} +0 -0
package/src/index.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * Config:
13
13
  * - Loaded from ~/.pi/agent/subagents-lite.json at session_start
14
- * - Module-level __config cache; tool_call reads from cache
15
- * - Config mutations update cache + atomic write to disk
14
+ * - ConfigStore owns config + session overrides + persistence + side effects
15
+ * - Tool execution and menus read/write through store
16
16
  *
17
17
  * Commands:
18
18
  * - /agents: Management menu (model settings, concurrency, running agents, debug)
@@ -23,286 +23,13 @@
23
23
  * - session_shutdown: Abort all, dispose manager
24
24
  */
25
25
 
26
- import { Type } from "@sinclair/typebox";
27
- import * as path from "node:path";
28
- import type {
29
- ExtensionAPI,
30
- ExtensionCommandContext,
31
- ExtensionContext,
32
- } from "@earendil-works/pi-coding-agent";
33
- import { DEFAULT_AGENTS } from "./default-agents.js";
34
- import { registerAgents, getAvailableTypes, setAgentScanDirs } from "./agent-types.js";
35
- import { scanAgentFilesInDir, mergeAgents } from "./agent-discovery.js";
36
- import { AgentManager } from "./agent-manager.js";
37
- import { AgentWidget, type UICtx } from "./ui/agent-widget.js";
38
- import { showAgentsMainMenu } from "./menus.js";
39
- import { loadConfig } from "./config-io.js";
40
- import { executeAgentTool, executeStopAgentTool, toolCallListener, backgroundAgentIds, scheduleNudge } from "./tool-execution.js";
41
- import { executeAgentStatusTool } from "./agent-status.js";
42
- import { renderAgentToolCall, renderAgentToolResult, renderSubagentResult } from "./renderer.js";
43
- import {
44
- __config,
45
- sessionOverrides,
46
- agentActivity,
47
- piInstance,
48
- setConfig,
49
- setManager,
50
- clearManager,
51
- setWidget,
52
- setPiInstance,
53
- setSessionCtx,
54
- resetSessionOverrides,
55
- resetLastToolsExpanded,
56
- syncWidgetSettings,
57
- syncCompactFromToolsExpanded,
58
- getManager,
59
- getWidget,
60
- } from "./state.js";
61
-
62
- // Re-exports for backward compatibility
63
- export {
64
- __config,
65
- sessionOverrides,
66
- agentActivity,
67
- piInstance,
68
- setShowCostEnabled,
69
- syncWidgetSettings,
70
- syncCompactFromToolsExpanded,
71
- } from "./state.js";
72
-
73
-
74
-
75
- // ============================================================================
76
- // Config loader — session_start handler logic
77
- // ============================================================================
78
-
79
- /**
80
- * Ensure the manager and widget singletons exist.
81
- * Idempotent — safe to call on every session_start.
82
- */
83
- function ensureManagerAndWidget(): void {
84
- const currentManager = getManager();
85
- const currentWidget = getWidget();
86
- // Create manager if missing
87
- if (!currentManager) {
88
- const newManager = new AgentManager(
89
- (record) => {
90
- // Only nudge for background (async) agents — sync agents already returned via tool result
91
- if (backgroundAgentIds.has(record.id)) {
92
- scheduleNudge(record.id);
93
- backgroundAgentIds.delete(record.id);
94
- }
95
-
96
- // Mark finished and update widget BEFORE deleting activity —
97
- // renderFinishedLine reads activity for turn count, tokens, etc.
98
- getWidget()?.markFinished(record.id);
99
- getWidget()?.update();
100
-
101
- // Remove from live activity tracking
102
- agentActivity.delete(record.id);
103
- },
104
- __config.concurrency,
105
- );
106
- setManager(newManager);
107
- }
108
-
109
- // Create widget if missing (uses existing or newly created manager)
110
- if (!currentWidget) {
111
- const newWidget = new AgentWidget(getManager(), agentActivity);
112
- newWidget.setShowCost(__config.agent.showCost === true);
113
- setWidget(newWidget);
114
- syncWidgetSettings();
115
- }
116
- }
117
-
118
- /**
119
- * Scan agent files from user and project directories, merge with defaults,
120
- * and register into the type registry.
121
- */
122
- async function scanAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
123
- const homeDir = process.env.HOME || "";
124
- const userAgentDir = path.join(homeDir, ".pi", "agent", "agents");
125
- const projectAgentDir = path.join(ctx.cwd, ".pi", "agents");
126
-
127
- // Store scan dirs for on-demand discovery (agents added during the session)
128
- setAgentScanDirs(userAgentDir, projectAgentDir);
129
-
130
- const [userAgents, projectAgents] = await Promise.all([
131
- scanAgentFilesInDir(userAgentDir, "user"),
132
- scanAgentFilesInDir(projectAgentDir, "project"),
133
- ]);
134
-
135
- // Merge with defaults
136
- const merged = mergeAgents(DEFAULT_AGENTS, userAgents, projectAgents);
137
-
138
- // Register into the type registry
139
- registerAgents(merged);
140
- }
141
-
142
- async function loadConfigAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
143
- setConfig(loadConfig());
144
- ensureManagerAndWidget();
145
- await scanAndRegisterAgents(ctx);
146
- }
147
-
148
-
149
-
150
- // ============================================================================
151
- // Agent tool registration helper — dynamic enum for agent types
152
- // ============================================================================
153
-
154
- /**
155
- * Register (or re-register) the Agent tool with current agent types.
156
- * At init time only defaults exist; call again from session_start after
157
- * user/project agents are loaded to update the enum.
158
- */
159
- function registerAgentTool(pi: ExtensionAPI): void {
160
- const types = getAvailableTypes();
161
- // Use plain string to avoid verbose anyOf in prompt.
162
- // Available types are listed in description for discoverability.
163
- const agentParam = types.length > 0
164
- ? Type.Optional(Type.String({ description: types.join(",") }))
165
- : Type.Optional(Type.String());
166
- // @ts-expect-error — description removed to save prompt tokens
167
- pi.registerTool({
168
- name: "Agent",
169
- label: "Agent",
170
- parameters: Type.Object({
171
- prompt: Type.String(),
172
- description: Type.Optional(Type.String()),
173
- agent: agentParam,
174
- run_in_background: Type.Optional(Type.Boolean()),
175
- worktree_path: Type.Optional(Type.String()),
176
- }),
177
- execute: executeAgentTool,
178
-
179
- renderCall: (args, theme) => renderAgentToolCall(args as Record<string, unknown>, theme),
180
-
181
- renderResult: (result, options, theme) => renderAgentToolResult(
182
- result as { content: Array<{ type: string; text?: string }>; details?: Record<string, unknown>; isError?: boolean },
183
- options as { expanded?: boolean },
184
- theme,
185
- ),
186
- });
187
- }
188
-
189
- // ============================================================================
190
- // Extension factory
191
- // ============================================================================
26
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
27
+ import { setPiInstance } from "./shell.js";
28
+ import { registerTools } from "./registration.js";
29
+ import { setupEventListeners } from "./events.js";
192
30
 
193
31
  export default function (pi: ExtensionAPI) {
194
- // Store pi for execute callbacks
195
32
  setPiInstance(pi);
196
-
197
- // ========================================================================
198
- // Tool registration (stealth schemas — at init time)
199
- // ========================================================================
200
-
201
- // Agent tool — stealth schema with dynamic agent type enum
202
- registerAgentTool(pi);
203
-
204
- // StopAgent tool — stealth schema, stop a running agent by ID
205
- // @ts-expect-error — description removed to save prompt tokens
206
- pi.registerTool({
207
- name: "StopAgent",
208
- label: "StopAgent",
209
- parameters: Type.Object({
210
- agent_id: Type.String(),
211
- }),
212
- execute: executeStopAgentTool,
213
- });
214
-
215
- // AgentStatus tool — stealth schema, list all agents and their statuses
216
- // @ts-expect-error — description removed to save prompt tokens
217
- pi.registerTool({
218
- name: "AgentStatus",
219
- label: "AgentStatus",
220
- parameters: Type.Object({}),
221
- execute: executeAgentStatusTool,
222
- });
223
-
224
- // Message renderer — subagent-result (background agent completion)
225
- pi.registerMessageRenderer("subagent-result", (message, options, theme) =>
226
- renderSubagentResult(
227
- message as { content?: string; details?: Record<string, unknown> },
228
- options as { expanded?: boolean },
229
- theme,
230
- ),
231
- );
232
-
233
- // Command registration
234
- pi.registerCommand("agents", {
235
- description: "Manage subagents: agent briefing, model settings, concurrency, running agents, agent types",
236
- handler: async (_args: string, ctx: ExtensionCommandContext) => {
237
- const modelOptions = ctx.modelRegistry.getAvailable().map((m) => `${m.provider}/${m.id}`);
238
- await showAgentsMainMenu(ctx, modelOptions);
239
- },
240
- });
241
-
242
- // Event listeners
243
- pi.on("tool_call", toolCallListener);
244
-
245
- pi.on("tool_execution_start", async (_event, ctx) => {
246
- // Set UI context on first tool execution
247
- if (!getWidget()) {
248
- ensureManagerAndWidget();
249
- }
250
- getWidget()?.setUICtx(ctx.ui as unknown as UICtx);
251
- getWidget()?.onTurnStart();
252
- });
253
-
254
-
255
-
256
- // session_start — load config, scan agents, register into registry,
257
- // then re-register Agent tool with dynamic agent type enum
258
- // Listen for ctrl+o keypress to sync compact mode (push-based, no polling)
259
- let unregisterTerminalInput: (() => void) | undefined;
260
-
261
- pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
262
- setSessionCtx(ctx);
263
- resetSessionOverrides();
264
- agentActivity.clear();
265
- resetLastToolsExpanded();
266
- await loadConfigAndRegisterAgents(ctx);
267
- // Re-register with updated agent type list (now includes user/project agents)
268
- registerAgentTool(pi);
269
- // Register ctrl+o listener
270
- if (ctx.hasUI && !unregisterTerminalInput) {
271
- unregisterTerminalInput = ctx.ui.onTerminalInput((data: string) => {
272
- // ctrl+o = 0x0F (15) — toggles tool expansion
273
- if (data === "\u000f") {
274
- // Read state after a tick to let the built-in handler process it first
275
- setTimeout(() => {
276
- const ui = ctx.ui as unknown as { getToolsExpanded?: () => boolean };
277
- const expanded = ui.getToolsExpanded?.();
278
- if (expanded !== undefined) {
279
- getWidget()?.notifyToolsExpansionChanged(expanded);
280
- }
281
- }, 0);
282
- }
283
- return undefined; // Don't consume the input
284
- });
285
- }
286
- // Sync compact mode with initial tool expansion state
287
- syncCompactFromToolsExpanded(false);
288
- });
289
-
290
- pi.on("session_shutdown", async (_event: unknown, ctx: ExtensionContext) => {
291
- // Warn if agents were killed
292
- const currentManager = getManager();
293
- if (currentManager) {
294
- const records = currentManager.listAgents();
295
- const active = records.filter(r => r.lifecycle.status === "running" || r.lifecycle.status === "queued");
296
- if (active.length > 0 && ctx.hasUI) {
297
- ctx.ui.notify(`${active.length} agent(s) killed by reload`, "warning");
298
- }
299
- }
300
- getWidget()?.dispose();
301
- setWidget(undefined);
302
- const mgr = getManager();
303
- if (mgr) {
304
- await mgr.dispose();
305
- clearManager();
306
- }
307
- });
33
+ registerTools(pi);
34
+ setupEventListeners(pi);
308
35
  }
@@ -12,6 +12,9 @@
12
12
  * 6. parentModelId (inherit from parent)
13
13
  */
14
14
 
15
+ import type { ThinkingLevel } from "../types.js";
16
+ import type { SystemPromptMode } from "../agents/types.js";
17
+
15
18
  /** Shape of the subagents-lite.json config file. */
16
19
  export interface SubagentsConfig {
17
20
  agent: {
@@ -23,6 +26,36 @@ export interface SubagentsConfig {
23
26
  widgetMaxLinesCompact?: number;
24
27
  widgetCompact?: boolean;
25
28
  widgetShortcut?: boolean;
29
+ /** System prompt mode: replace (default), inherit parent, or custom file. */
30
+ systemPromptMode?: SystemPromptMode;
31
+ /** Whether to include AGENTS.md context files in the subagent system prompt. Default: true. */
32
+ includeContextFiles?: boolean;
33
+ /** Default thinking level for spawned agents. Undefined = inherit from agent config. */
34
+ defaultThinking?: ThinkingLevel;
35
+ /** Default max turns for spawned agents. Undefined = unlimited. */
36
+ defaultMaxTurns?: number;
37
+ /** Global default for skills loading when agent doesn't explicitly set skills. true (default) or false. */
38
+ loadSkillsImplicitly?: boolean;
39
+ /** Global default for extensions loading when agent doesn't explicitly set extensions. true (default) or false. */
40
+ loadExtensionsImplicitly?: boolean;
41
+ /** When true, skip built-in default agents (general-purpose, Explore) at registration. */
42
+ disableDefaultAgents?: boolean;
43
+ /** Whether to show toolUses count in widget stats line. Default: true. */
44
+ showTools?: boolean;
45
+ /** Whether to show turn count in widget stats line. Default: true. */
46
+ showTurns?: boolean;
47
+ /** Whether to show input tokens in widget stats line. Default: true. */
48
+ showInput?: boolean;
49
+ /** Whether to show output tokens in widget stats line. Default: true. */
50
+ showOutput?: boolean;
51
+ /** Whether to show context percent and compactions in widget stats line. Default: true. */
52
+ showContext?: boolean;
53
+ /** Whether to show elapsed time in widget stats line. Default: true. */
54
+ showTime?: boolean;
55
+ /** Max description length in widget full mode. Default: 50. */
56
+ widgetDescLengthFull?: number;
57
+ /** Max description length in widget compact mode. Default: 30. */
58
+ widgetDescLengthCompact?: number;
26
59
  [agentType: string]: string | null | undefined | boolean | number;
27
60
  };
28
61
  concurrency: {
@@ -15,7 +15,7 @@ import {
15
15
  Text,
16
16
  } from "@earendil-works/pi-tui";
17
17
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
18
- import type { Theme } from "./ui/agent-widget.js";
18
+ import type { Theme } from "../ui/agent-widget.js";
19
19
 
20
20
  /* ------------------------------------------------------------------ */
21
21
  /* Types */
@@ -5,7 +5,7 @@
5
5
  * buildSnapshotMarkdown: format agent conversation as markdown for snapshot viewer.
6
6
  */
7
7
 
8
- import { summarizeToolArgs } from "./format.js";
8
+ import { summarizeToolArgs } from "../ui/format.js";
9
9
 
10
10
  function isTextBlock(c: unknown): c is { type: "text"; text: string } {
11
11
  return typeof c === "object" && c !== null && (c as Record<string, unknown>).type === "text";
@@ -0,0 +1,180 @@
1
+ /**
2
+ * prompts.ts — System prompt builder for agents.
3
+ *
4
+ * Every agent gets a fresh context — no inherited parent identity.
5
+ * EnvInfo is imported from types.ts — branch is a string (empty when unknown).
6
+ */
7
+
8
+ import type { EnvInfo } from "../types.js";
9
+ import type { AgentConfig, SystemPromptMode } from "../agents/types.js";
10
+ import type { SkillMeta, PreloadedSkill } from "./skill-loader.js";
11
+ import { formatSkillsForPrompt, type Skill } from "@earendil-works/pi-coding-agent";
12
+
13
+ /** Extra sections to inject into the system prompt (skills). */
14
+ export interface PromptExtras {
15
+ /** Preloaded skill contents to inject (full content + description). */
16
+ skillBlocks?: PreloadedSkill[];
17
+ /** Skill metadata for whitelist display (name, description, location only). */
18
+ skillMetas?: SkillMeta[];
19
+ /** Parent system prompt (for inherit mode). */
20
+ parentSystemPrompt?: string;
21
+ /** Custom system prompt content (for custom mode). */
22
+ customSystemPrompt?: string;
23
+ /** Project context files (AGENTS.md) for custom mode. */
24
+ contextFiles?: Array<{ path: string; content: string }>;
25
+ }
26
+
27
+ /**
28
+ * Strip pi scaffolding sections from a parent system prompt.
29
+ *
30
+ * In inherit mode, the parent's prompt already contains:
31
+ * - <project_context>...</project_context> (AGENTS.md)
32
+ * - Skills block (text intro + <available_skills>...</available_skills>)
33
+ * - Current date: YYYY-MM-DD
34
+ * - Current working directory: /path
35
+ *
36
+ * These are re-added by subagents-lite from the subagent's own config,
37
+ * so we strip them to avoid duplication.
38
+ *
39
+ * @param prompt The parent system prompt to clean.
40
+ * @returns The prompt with scaffolding sections removed.
41
+ */
42
+ function stripScaffolding(prompt: string): string {
43
+ let result = prompt;
44
+
45
+ // 1. Strip <project_context>...</project_context> block
46
+ result = result.replace(/\n?<\s*project_context\s*>[\s\S]*?<\/\s*project_context\s*>\n?/g, "\n");
47
+
48
+ // 2. Strip skills block: optional intro text + <available_skills>...</available_skills>
49
+ result = result.replace(/\n?(?:The following skills provide[\s\S]*?)?<\s*available_skills\s*>[\s\S]*?<\/\s*available_skills\s*>\n?/g, "\n");
50
+
51
+ // 3. Strip Current date: line
52
+ result = result.replace(/\n?Current date:.*\n?/g, "\n");
53
+
54
+ // 4. Strip Current working directory: line
55
+ result = result.replace(/\n?Current working directory:.*\n?/g, "\n");
56
+
57
+ // Clean up: collapse runs of 3+ newlines into 2
58
+ result = result.replace(/\n{3,}/g, "\n\n");
59
+
60
+ return result.trim();
61
+ }
62
+
63
+ /**
64
+ * Build the system prompt for an agent from its config.
65
+ *
66
+ * Three modes:
67
+ * - replace (default): generic header + env + agent's systemPrompt
68
+ * - inherit: parent's system prompt (stripped of scaffolding) + env + agent's systemPrompt
69
+ * - custom: content of ~/.pi/agent/subagents-lite-prompt.md + env + agent's systemPrompt
70
+ *
71
+ * Agent's own systemPrompt is always included in <agent_instructions> tags.
72
+ *
73
+ * @param config Agent configuration.
74
+ * @param cwd Current working directory.
75
+ * @param env Environment info.
76
+ * @param extras Optional extra sections to inject (skills, parent/custom prompts).
77
+ * @param mode System prompt mode (replace, inherit, custom).
78
+ */
79
+ export function buildAgentPrompt(
80
+ config: AgentConfig,
81
+ cwd: string,
82
+ env: EnvInfo,
83
+ extras?: PromptExtras,
84
+ mode: SystemPromptMode = "replace",
85
+ ): string {
86
+ const envLines = [
87
+ "# Environment",
88
+ `Working directory: ${cwd}`,
89
+ env.isGitRepo ? "Git repository: yes" : "Not a git repository",
90
+ ];
91
+ if (env.isGitRepo && env.branch) {
92
+ envLines.push(`Branch: ${env.branch}`);
93
+ }
94
+ envLines.push(`Platform: ${env.platform}`);
95
+ const envBlock = envLines.join("\n");
96
+
97
+ // Unified skill index — all skills in one <available_skills> block
98
+ const hasSkills = extras?.skillMetas?.length || extras?.skillBlocks?.length;
99
+ let extrasSuffix = "";
100
+ if (hasSkills) {
101
+ const skillLines: string[] = [];
102
+
103
+ // Location-based skills: use Pi's formatSkillsForPrompt for XML escaping and
104
+ // disable-model-invocation filtering, then extract the <skill> elements.
105
+ if (extras?.skillMetas?.length) {
106
+ const piSkills: Skill[] = extras.skillMetas.map((m) => ({
107
+ name: m.name,
108
+ description: m.description,
109
+ filePath: m.location,
110
+ baseDir: "",
111
+ sourceInfo: {} as any,
112
+ disableModelInvocation: m.disableModelInvocation,
113
+ }));
114
+ const formatted = formatSkillsForPrompt(piSkills);
115
+ const skillElements = formatted.match(/<skill>[\s\S]*?<\/skill>/g);
116
+ if (skillElements) skillLines.push(...skillElements);
117
+ }
118
+
119
+ // Preloaded skills: content tags (not in Pi's formatSkillsForPrompt)
120
+ for (const skill of extras?.skillBlocks ?? []) {
121
+ skillLines.push(`<skill><name>${escapeXml(skill.name)}</name><description>${escapeXml(skill.description)}</description><content>${escapeXml(skill.content)}</content></skill>`);
122
+ }
123
+
124
+ const lines = [
125
+ "The following skills provide specialized instructions for specific tasks.",
126
+ "Use the read tool to load a skill's file when the task matches its description.",
127
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
128
+ "",
129
+ "<available_skills>",
130
+ ...skillLines,
131
+ "</available_skills>",
132
+ ];
133
+ extrasSuffix = `\n\n${lines.join("\n")}`;
134
+ }
135
+
136
+ // Agent's own system prompt wrapped in <agent_instructions> tags
137
+ const agentInstructions = `\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`;
138
+
139
+ // Project context files (AGENTS.md) — placed after agent_instructions, before extras
140
+ let contextSuffix = "";
141
+ if (extras?.contextFiles?.length) {
142
+ const lines = [
143
+ "<project_context>",
144
+ "",
145
+ "Project-specific instructions and guidelines:",
146
+ "",
147
+ ];
148
+ for (const file of extras.contextFiles) {
149
+ lines.push(`<project_instructions path="${escapeXml(file.path)}">`);
150
+ lines.push(file.content);
151
+ lines.push(`</project_instructions>`);
152
+ lines.push("");
153
+ }
154
+ lines.push("</project_context>");
155
+ contextSuffix = `\n\n${lines.join("\n")}`;
156
+ }
157
+
158
+ // Build base prompt: mode-specific header if provided, otherwise default
159
+ const activeAgentTag = `<active_agent name="${config.name}"/>`;
160
+ const rawHeader = mode === "inherit" ? extras?.parentSystemPrompt
161
+ : mode === "custom" ? extras?.customSystemPrompt
162
+ : undefined;
163
+ // Parent/custom headers carry pi's scaffolding (context, skills, date, cwd);
164
+ // strip it — we re-add these from the subagent's own config. (rawHeader is
165
+ // undefined in replace mode, so nothing to strip there.)
166
+ const customHeader = rawHeader ? stripScaffolding(rawHeader) : rawHeader;
167
+ const basePrompt = customHeader
168
+ ? `${customHeader}\n\n${envBlock}`
169
+ : `You are a Pi, an expert coding sub-agent.\nYou have been invoked to handle a specific task autonomously.\n\n${envBlock}`;
170
+
171
+ // active_agent goes AFTER shared prefix (header + env + context) for KV cache
172
+ return `${basePrompt}${contextSuffix}\n${activeAgentTag}\n${agentInstructions}${extrasSuffix}`;
173
+ }
174
+
175
+ function escapeXml(value: string): string {
176
+ // Only escape < and > — enough for XML-like tags, keeps text readable for LLMs
177
+ return value
178
+ .replace(/</g, "&lt;")
179
+ .replace(/>/g, "&gt;");
180
+ }