pi-open-agents 0.1.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.
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Agent definition schema: validation, normalization, and default application.
3
+ *
4
+ * This is the brain of the parsing pipeline. Raw frontmatter goes in,
5
+ * a fully validated AgentDefinition comes out.
6
+ *
7
+ * Handles:
8
+ * - Required field validation (name)
9
+ * - Enum validation (mode, thinking, systemPrompt)
10
+ * - Type coercion (numbers, booleans, arrays)
11
+ * - OpenCode aliases (prompt → body, tools map → permission, maxSteps → steps)
12
+ * - Default application (with settings fallbacks for thinking)
13
+ */
14
+
15
+ import type {
16
+ AgentDefinition,
17
+ AgentMode,
18
+ AgentSource,
19
+ PermissionAction,
20
+ PermissionConfig,
21
+ SystemPromptMode,
22
+ ThinkingLevel,
23
+ } from "../types.ts";
24
+ import {
25
+ AGENT_MODES,
26
+ PERMISSION_ACTIONS,
27
+ SYSTEM_PROMPT_MODES,
28
+ THINKING_LEVELS,
29
+ } from "../types.ts";
30
+ import { parseFrontmatter } from "./frontmatter.ts";
31
+
32
+ // ─── Defaults ────────────────────────────────────────────────────────────────
33
+
34
+ export interface SchemaDefaults {
35
+ /** Default thinking level, typically from settings.json defaultThinkingLevel */
36
+ thinking: ThinkingLevel;
37
+ }
38
+
39
+ export const SCHEMA_DEFAULTS: SchemaDefaults = {
40
+ thinking: "off",
41
+ };
42
+
43
+ // ─── Parse Error ─────────────────────────────────────────────────────────────
44
+
45
+ export class AgentParseError extends Error {
46
+ constructor(message: string) {
47
+ super(message);
48
+ this.name = "AgentParseError";
49
+ }
50
+ }
51
+
52
+ // ─── Main Entry Point ────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Parse raw .md file content into a validated AgentDefinition.
56
+ *
57
+ * @param content Raw file content (with frontmatter)
58
+ * @param filePath Absolute path to the file
59
+ * @param source Discovery origin (global/project)
60
+ * @param defaults Schema defaults (thinking fallback)
61
+ * @param nameHint Optional name derived from file path (used if no frontmatter name)
62
+ */
63
+ export function parseAgentDefinition(
64
+ content: string,
65
+ filePath: string,
66
+ source: AgentSource,
67
+ defaults: SchemaDefaults = SCHEMA_DEFAULTS,
68
+ nameHint?: string,
69
+ ): AgentDefinition {
70
+ const { data, body } = parseFrontmatter(content);
71
+
72
+ // ── Name (required) ──
73
+ const name = parseString(data.name) ?? nameHint;
74
+ if (!name) {
75
+ throw new AgentParseError("missing required field: name");
76
+ }
77
+
78
+ // ── Prompt: body wins over frontmatter "prompt" field ──
79
+ // OpenCode uses frontmatter "prompt" field; pi uses the body.
80
+ // If both exist, body takes precedence (pi convention).
81
+ const frontmatterPrompt = parseString(data.prompt);
82
+ const prompt = body || frontmatterPrompt || "";
83
+
84
+ // ── Identity fields ──
85
+ const mode = parseEnum<AgentMode>(data.mode, AGENT_MODES, "all");
86
+ const hidden = parseBoolean(data.hidden, false);
87
+ const color = parseOptionalString(data.color);
88
+ const disable = parseBoolean(data.disable, false);
89
+ const description = parseOptionalString(data.description);
90
+
91
+ // ── Model fields ──
92
+ const model = parseOptionalString(data.model);
93
+ const variant = parseOptionalString(data.variant);
94
+ const temperature = parseOptionalNumber(data.temperature);
95
+ const topP = parseOptionalNumber(data["top_p"] ?? data.topP);
96
+
97
+ // ── Execution control ──
98
+ const thinking = parseEnum<ThinkingLevel>(data.thinking, THINKING_LEVELS, defaults.thinking);
99
+ const steps = parseOptionalNumber(data.steps ?? data.maxSteps); // maxSteps is OC alias
100
+ const systemPrompt = parseEnum<SystemPromptMode>(
101
+ data.systemPrompt ?? data["system-prompt"],
102
+ SYSTEM_PROMPT_MODES,
103
+ "append",
104
+ );
105
+
106
+ // ── Tools & permissions ──
107
+ const { permission, tools } = parseToolsAndPermission(data);
108
+
109
+ // ── Subagent controls ──
110
+ const maxDepth = parseNumber(data.maxDepth, 10);
111
+ const allowedAgents = parseStringArray(data.allowedAgents);
112
+ const skills = parseStringArray(data.skills);
113
+
114
+ return {
115
+ name,
116
+ description,
117
+ mode,
118
+ hidden,
119
+ color,
120
+ disable,
121
+ model,
122
+ variant,
123
+ temperature,
124
+ topP,
125
+ thinking,
126
+ steps,
127
+ systemPrompt,
128
+ permission,
129
+ tools,
130
+ maxDepth,
131
+ allowedAgents: allowedAgents.length > 0 ? allowedAgents : undefined,
132
+ skills: skills.length > 0 ? skills : undefined,
133
+ prompt,
134
+ source,
135
+ filePath,
136
+ };
137
+ }
138
+
139
+ // ─── Tools & Permission Resolution ───────────────────────────────────────────
140
+
141
+ /**
142
+ * Resolve tools and permission from raw frontmatter data.
143
+ *
144
+ * Logic:
145
+ * 1. If `permission` field exists → use it
146
+ * 2. Else if `tools` field exists:
147
+ * a. If tools is a map (OpenCode style) → convert to permission rules
148
+ * b. If tools is a CSV string/array (pi style) → keep as whitelist
149
+ * 3. Both can coexist: permission for rules, tools for simple whitelist
150
+ */
151
+ function parseToolsAndPermission(
152
+ data: Record<string, unknown>,
153
+ ): { permission?: PermissionConfig; tools?: string[] } {
154
+ let permission: PermissionConfig | undefined;
155
+ let tools: string[] | undefined;
156
+
157
+ const rawPermission = data.permission;
158
+ const rawTools = data.tools;
159
+
160
+ // Parse permission block
161
+ if (rawPermission !== undefined && rawPermission !== null) {
162
+ permission = parsePermissionConfig(rawPermission);
163
+ }
164
+
165
+ // Parse tools field
166
+ if (rawTools !== undefined && rawTools !== null) {
167
+ if (typeof rawTools === "object" && !Array.isArray(rawTools)) {
168
+ // OpenCode-style tools map: { "read": true, "write": false }
169
+ // Convert to permission if no explicit permission block
170
+ if (!permission) {
171
+ permission = toolsMapToPermission(rawTools as Record<string, unknown>);
172
+ }
173
+ } else {
174
+ // pi-style CSV or array: "read, bash, edit"
175
+ tools = parseStringArray(rawTools);
176
+ }
177
+ }
178
+
179
+ return { permission, tools };
180
+ }
181
+
182
+ /**
183
+ * Convert OpenCode-style tools map to PermissionConfig.
184
+ *
185
+ * { "read": true, "write": false } → { "read": "allow", "write": "deny" }
186
+ */
187
+ function toolsMapToPermission(toolsMap: Record<string, unknown>): PermissionConfig {
188
+ const config: PermissionConfig = {};
189
+
190
+ for (const [toolName, enabled] of Object.entries(toolsMap)) {
191
+ // Skip non-boolean values
192
+ if (typeof enabled !== "boolean") continue;
193
+
194
+ // Map edit-like tools to "edit" permission (OpenCode convention)
195
+ const permKey = toolName === "write" || toolName === "edit" || toolName === "apply_patch"
196
+ ? "edit"
197
+ : toolName;
198
+
199
+ config[permKey] = enabled ? "allow" : "deny";
200
+ }
201
+
202
+ return config;
203
+ }
204
+
205
+ /**
206
+ * Parse and validate a PermissionConfig from raw frontmatter data.
207
+ */
208
+ function parsePermissionConfig(raw: unknown): PermissionConfig | undefined {
209
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
210
+ throw new AgentParseError(`invalid permission: expected object, got ${typeof raw}`);
211
+ }
212
+
213
+ const config: PermissionConfig = {};
214
+ const obj = raw as Record<string, unknown>;
215
+
216
+ for (const [key, value] of Object.entries(obj)) {
217
+ if (typeof value === "string") {
218
+ // Simple form: bash: allow
219
+ const action = validateAction(value, key);
220
+ config[key] = action;
221
+ } else if (typeof value === "object" && value !== null) {
222
+ // Pattern form: edit: { "*.env": deny }
223
+ const nested: Record<string, PermissionAction> = {};
224
+ for (const [pattern, action] of Object.entries(value as Record<string, unknown>)) {
225
+ if (typeof action === "string") {
226
+ nested[pattern] = validateAction(action, `${key}.${pattern}`);
227
+ }
228
+ }
229
+ config[key] = nested;
230
+ }
231
+ }
232
+
233
+ return Object.keys(config).length > 0 ? config : undefined;
234
+ }
235
+
236
+ function validateAction(value: string, context: string): PermissionAction {
237
+ const action = value as PermissionAction;
238
+ if (!PERMISSION_ACTIONS.includes(action)) {
239
+ throw new AgentParseError(
240
+ `invalid permission action "${value}" for "${context}": must be allow, deny, or ask`,
241
+ );
242
+ }
243
+ return action;
244
+ }
245
+
246
+ // ─── Type Helpers ────────────────────────────────────────────────────────────
247
+
248
+ function parseString(value: unknown): string | undefined {
249
+ if (value === undefined || value === null) return undefined;
250
+ return String(value).trim() || undefined;
251
+ }
252
+
253
+ function parseOptionalString(value: unknown): string | undefined {
254
+ return parseString(value);
255
+ }
256
+
257
+ function parseBoolean(value: unknown, fallback: boolean): boolean {
258
+ if (value === undefined || value === null) return fallback;
259
+ if (typeof value === "boolean") return value;
260
+ if (typeof value === "string") {
261
+ if (value === "true") return true;
262
+ if (value === "false") return false;
263
+ }
264
+ return fallback;
265
+ }
266
+
267
+ function parseNumber(value: unknown, fallback: number): number {
268
+ if (value === undefined || value === null) return fallback;
269
+ if (typeof value === "number") return value;
270
+ const parsed = Number(value);
271
+ return Number.isNaN(parsed) ? fallback : parsed;
272
+ }
273
+
274
+ function parseOptionalNumber(value: unknown): number | undefined {
275
+ if (value === undefined || value === null) return undefined;
276
+ if (typeof value === "number") return value;
277
+ const parsed = Number(value);
278
+ return Number.isNaN(parsed) ? undefined : parsed;
279
+ }
280
+
281
+ function parseEnum<T extends string>(
282
+ value: unknown,
283
+ validValues: readonly T[],
284
+ fallback: T,
285
+ ): T {
286
+ if (value === undefined || value === null) return fallback;
287
+ const str = String(value).trim() as T;
288
+ if (!validValues.includes(str)) {
289
+ throw new AgentParseError(
290
+ `invalid value "${str}": must be one of ${validValues.join(", ")}`,
291
+ );
292
+ }
293
+ return str;
294
+ }
295
+
296
+ function parseStringArray(value: unknown): string[] {
297
+ if (value === undefined || value === null) return [];
298
+
299
+ if (Array.isArray(value)) {
300
+ // Already an array (from YAML parser for inline arrays)
301
+ return value.map((v) => String(v).trim()).filter(Boolean);
302
+ }
303
+
304
+ if (typeof value === "string") {
305
+ // CSV string: "read, bash, edit"
306
+ return value
307
+ .split(",")
308
+ .map((s) => s.trim())
309
+ .filter(Boolean);
310
+ }
311
+
312
+ return [];
313
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Agent discovery and loading.
3
+ *
4
+ * Scans all discovery paths (pi, OpenCode, shared), parses each .md file
5
+ * into an AgentDefinition, and merges by name with project overriding global.
6
+ */
7
+
8
+ import * as fs from "node:fs/promises";
9
+ import * as path from "node:path";
10
+
11
+ import type { AgentDefinition, AgentFs, AgentLoadResult } from "../types.ts";
12
+ import type { SchemaDefaults } from "../config/schema.ts";
13
+ import type { DiscoveryPath } from "../config/paths.ts";
14
+ import { deriveAgentName, getDiscoveryPaths } from "../config/paths.ts";
15
+ import { parseAgentDefinition } from "../config/schema.ts";
16
+ import { SCHEMA_DEFAULTS } from "../config/schema.ts";
17
+
18
+ // ─── Default Filesystem ──────────────────────────────────────────────────────
19
+
20
+ const defaultFs: AgentFs = {
21
+ async listFiles(dir: string): Promise<string[]> {
22
+ try {
23
+ const entries = await fs.readdir(dir, { withFileTypes: true });
24
+ const files: string[] = [];
25
+ for (const entry of entries) {
26
+ const fullPath = path.join(dir, entry.name);
27
+
28
+ if (entry.isFile() || entry.isSymbolicLink()) {
29
+ if (entry.name.endsWith(".md")) {
30
+ files.push(fullPath);
31
+ }
32
+ } else if (entry.isDirectory()) {
33
+ // Recurse into subdirectories (for OpenCode-style nested agents)
34
+ const nested = await this.listFiles(fullPath);
35
+ files.push(...nested);
36
+ }
37
+ }
38
+ return files.sort();
39
+ } catch {
40
+ return [];
41
+ }
42
+ },
43
+
44
+ async readFile(filePath: string): Promise<string> {
45
+ return fs.readFile(filePath, "utf8");
46
+ },
47
+
48
+ async exists(dir: string): Promise<boolean> {
49
+ try {
50
+ await fs.access(dir);
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ },
56
+ };
57
+
58
+ // ─── Load Options ────────────────────────────────────────────────────────────
59
+
60
+ export interface LoadAgentsOptions {
61
+ /** Current working directory (project root). Defaults to process.cwd() */
62
+ cwd?: string;
63
+ /** pi agent directory. Defaults to ~/.pi/agent */
64
+ agentDir?: string;
65
+ /** Schema defaults (thinking level fallback). */
66
+ defaults?: SchemaDefaults;
67
+ /** Custom filesystem implementation (for testing). */
68
+ fs?: AgentFs;
69
+ /** Custom discovery paths (overrides auto-detection). */
70
+ paths?: DiscoveryPath[];
71
+ }
72
+
73
+ // ─── Main Loader ─────────────────────────────────────────────────────────────
74
+
75
+ /**
76
+ * Load all agent definitions from discovery paths.
77
+ *
78
+ * Scan order: global paths first, then project paths.
79
+ * Merge rule: project overrides global by name.
80
+ */
81
+ export async function loadAgents(options: LoadAgentsOptions = {}): Promise<AgentLoadResult> {
82
+ const fileSystem = options.fs ?? defaultFs;
83
+ const cwd = options.cwd ?? process.cwd();
84
+ const agentDir = options.agentDir ?? defaultAgentDir();
85
+ const defaults = options.defaults ?? SCHEMA_DEFAULTS;
86
+
87
+ // Build discovery paths
88
+ const paths = options.paths ?? getDiscoveryPaths(cwd, agentDir);
89
+
90
+ // Scan each path, collecting agents grouped by source
91
+ const globalAgents = new Map<string, AgentDefinition>();
92
+ const projectAgents = new Map<string, AgentDefinition>();
93
+ const warnings: AgentLoadResult["warnings"] = [];
94
+
95
+ for (const discPath of paths) {
96
+ // For each subdir pattern, scan the directory
97
+ for (const subdir of discPath.subdirs) {
98
+ const scanDir = subdir ? path.join(discPath.dir, subdir) : discPath.dir;
99
+
100
+ if (!(await fileSystem.exists(scanDir))) continue;
101
+
102
+ const files = await fileSystem.listFiles(scanDir);
103
+
104
+ for (const filePath of files) {
105
+ if (!filePath.endsWith(".md")) continue;
106
+
107
+ try {
108
+ const content = await fileSystem.readFile(filePath);
109
+ const nameHint = deriveAgentName(filePath, discPath.dir, subdir);
110
+ const agent = parseAgentDefinition(content, filePath, discPath.source, defaults, nameHint);
111
+
112
+ // Skip disabled agents entirely
113
+ if (agent.disable) continue;
114
+
115
+ // Add to the appropriate map
116
+ const target = discPath.source === "project" ? projectAgents : globalAgents;
117
+ target.set(agent.name, agent);
118
+ } catch (error) {
119
+ warnings.push({
120
+ filePath,
121
+ message: error instanceof Error ? error.message : String(error),
122
+ });
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ // Merge: project overrides global
129
+ const merged = new Map<string, AgentDefinition>();
130
+ for (const agent of globalAgents.values()) merged.set(agent.name, agent);
131
+ for (const agent of projectAgents.values()) merged.set(agent.name, agent);
132
+
133
+ return {
134
+ agents: [...merged.values()],
135
+ warnings,
136
+ };
137
+ }
138
+
139
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
140
+
141
+ function defaultAgentDir(): string {
142
+ return path.join(process.env.HOME ?? "", ".pi", "agent");
143
+ }
144
+
145
+ // ─── Filtering Utilities ────────────────────────────────────────────────────
146
+
147
+ /**
148
+ * Filter agents that are visible in the TUI selector.
149
+ * Shows: mode primary|all, excludes hidden and disable.
150
+ */
151
+ export function selectableAgents(agents: AgentDefinition[]): AgentDefinition[] {
152
+ return agents.filter(
153
+ (a) => !a.hidden && (a.mode === "primary" || a.mode === "all"),
154
+ );
155
+ }
156
+
157
+ /**
158
+ * Filter agents that can be spawned as subagents.
159
+ * Shows: mode subagent|all.
160
+ */
161
+ export function spawnableAgents(agents: AgentDefinition[]): AgentDefinition[] {
162
+ return agents.filter((a) => a.mode === "subagent" || a.mode === "all");
163
+ }
164
+
165
+ /**
166
+ * Filter agents available to a parent for delegation.
167
+ * Applies the parent's allowedAgents whitelist if present.
168
+ */
169
+ export function agentsAvailableTo(
170
+ agents: AgentDefinition[],
171
+ parent?: AgentDefinition,
172
+ ): AgentDefinition[] {
173
+ const spawnable = spawnableAgents(agents);
174
+
175
+ if (!parent?.allowedAgents || parent.allowedAgents.length === 0) {
176
+ return spawnable;
177
+ }
178
+
179
+ return spawnable.filter((a) => parent.allowedAgents!.includes(a.name));
180
+ }
package/src/index.ts ADDED
@@ -0,0 +1,204 @@
1
+ /**
2
+ * pi-open-agents — Unified agent and subagent management for pi.
3
+ *
4
+ * Replaces pi-agent-mode and pi-subagents with a single, coherent system.
5
+ * OpenCode-compatible agent definitions.
6
+ *
7
+ * F1: Foundation (types, parser, schema, discovery) ✅
8
+ * F4: Permission engine (parser, matcher, evaluator) ✅
9
+ * F2: Primary agents (this phase) ✅
10
+ * F3: Subagent engine (planned)
11
+ * F5: OpenCode compat (planned)
12
+ * F6: Polish (planned)
13
+ */
14
+
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+
18
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
20
+
21
+ import type { AgentDefinition, ThinkingLevel } from "./types.ts";
22
+ import { loadAgents, selectableAgents, spawnableAgents } from "./discovery/loader.ts";
23
+ import { AgentManager } from "./primary/manager.ts";
24
+ import { refreshAgentTools, buildSystemPrompt } from "./primary/executor.ts";
25
+ import { updateBanner } from "./tui/banner.ts";
26
+ import { registerTuiControls } from "./tui/selector.ts";
27
+ import { registerAgentTools } from "./tui/tools.ts";
28
+ import { registerSubagentTool } from "./subagent/tool.ts";
29
+ import { isSubagentReplaceSystemPrompt } from "./subagent/env.ts";
30
+
31
+ // ─── Settings ────────────────────────────────────────────────────────────────
32
+
33
+ interface Settings {
34
+ defaultAgent?: string;
35
+ defaultThinkingLevel?: ThinkingLevel;
36
+ }
37
+
38
+ function loadSettings(cwd: string): Settings {
39
+ const globalPath = join(getAgentDir(), "settings.json");
40
+ const projectPath = join(cwd, ".pi", "settings.json");
41
+
42
+ let settings: Settings = {};
43
+
44
+ for (const p of [globalPath, projectPath]) {
45
+ if (existsSync(p)) {
46
+ try {
47
+ const content = readFileSync(p, "utf-8");
48
+ const parsed = JSON.parse(content) as Settings;
49
+ settings = { ...settings, ...parsed };
50
+ } catch {
51
+ // ignore invalid json
52
+ }
53
+ }
54
+ }
55
+
56
+ return settings;
57
+ }
58
+
59
+ // ─── Extension Entry Point ───────────────────────────────────────────────────
60
+
61
+ export default function piOpenAgents(pi: ExtensionAPI): void {
62
+ const manager = new AgentManager();
63
+
64
+ // ── Register CLI flag immediately (before session_start) ──────────────────
65
+ // Extension flags are parsed during startup, before session_start fires.
66
+ // Must register here, not inside session_start.
67
+ pi.registerFlag("agent", {
68
+ description: "Default agent to use at startup",
69
+ type: "string",
70
+ });
71
+
72
+ // ── Session Start: Load agents and apply default ───────────────────────────
73
+
74
+ pi.on("session_start", async (event, ctx) => {
75
+ // Load settings for thinking fallback
76
+ const settings = loadSettings(ctx.cwd);
77
+
78
+ // Load all agent definitions from discovery paths
79
+ const result = await loadAgents({
80
+ cwd: ctx.cwd,
81
+ agentDir: getAgentDir(),
82
+ defaults: { thinking: settings.defaultThinkingLevel ?? "off" },
83
+ });
84
+
85
+ // Log any parse warnings
86
+ for (const w of result.warnings) {
87
+ ctx.ui.notify(`Agent parse warning: ${w.filePath}: ${w.message}`, "warning");
88
+ }
89
+
90
+ manager.setAgents(result.agents);
91
+
92
+ // Register TUI controls + tools + subagent tool
93
+ registerTuiControls(manager, pi);
94
+ registerAgentTools(manager, pi);
95
+ registerSubagentTool(pi, {
96
+ agents: result.agents,
97
+ agentDir: getAgentDir(),
98
+ getActiveAgent: () => manager.getActive(),
99
+ });
100
+
101
+ // Check for --agent CLI flag first (highest priority)
102
+ const agentFlag = pi.getFlag("agent");
103
+ if (typeof agentFlag === "string" && agentFlag) {
104
+ const agent = manager.getAgent(agentFlag);
105
+ if (agent) {
106
+ await manager.apply(agentFlag, pi, ctx);
107
+ }
108
+ updateBanner(ctx.ui, manager.getActive(), selectableAgents(manager.getAgents()).length);
109
+ return;
110
+ }
111
+
112
+ // Try to restore from session state (if resuming)
113
+ if (event.reason === "resume" || event.reason === "fork") {
114
+ const entries = ctx.sessionManager.getEntries();
115
+ const agentEntry = entries
116
+ .filter((e: { type: string; customType?: string }) =>
117
+ e.type === "custom" && e.customType === "open-agents-state")
118
+ .pop() as { data?: { name: string } } | undefined;
119
+
120
+ if (agentEntry?.data?.name) {
121
+ manager.restoreFromSession(agentEntry.data.name);
122
+ // Re-apply tools on resume (model preserved from session)
123
+ const active = manager.getActive();
124
+ if (active) {
125
+ refreshAgentTools(active, pi);
126
+ }
127
+ updateBanner(ctx.ui, manager.getActive(), selectableAgents(manager.getAgents()).length);
128
+ return;
129
+ }
130
+ }
131
+
132
+ // Try to load from settings (defaultAgent)
133
+ if (settings.defaultAgent) {
134
+ const agent = manager.getAgent(settings.defaultAgent);
135
+ if (agent) {
136
+ await manager.apply(settings.defaultAgent, pi, ctx);
137
+ }
138
+ }
139
+
140
+ // Show status
141
+ updateBanner(ctx.ui, manager.getActive(), selectableAgents(manager.getAgents()).length);
142
+ });
143
+
144
+ // ── Before Agent Start: Inject system prompt + refresh tools ───────────────
145
+
146
+ pi.on("before_agent_start", async (event) => {
147
+ const active = manager.getActive();
148
+ if (!active) return;
149
+
150
+ // Re-evaluate tool set (catches async-registered tools like MCP)
151
+ refreshAgentTools(active, pi);
152
+
153
+ // Inject system prompt
154
+ const modifiedPrompt = buildSystemPrompt(active, event.systemPrompt);
155
+ if (modifiedPrompt) {
156
+ return { systemPrompt: modifiedPrompt };
157
+ }
158
+ });
159
+
160
+ // ── Before Agent Start (subagent replace mode): Inject available subagents ─
161
+
162
+ pi.on("before_agent_start", async (event) => {
163
+ if (!isSubagentReplaceSystemPrompt(process.env)) return;
164
+ // In replace mode, append available subagent list to the system prompt
165
+ const agents = manager.getAgents();
166
+ const spawnable = spawnableAgents(agents);
167
+ if (spawnable.length === 0) return;
168
+ const names = spawnable.map((a: AgentDefinition) => a.name).sort().join(", ");
169
+ const block = `\n\nAvailable subagents:\n- ${names.split(", ").join("\n- ")}`;
170
+ return { systemPrompt: event.systemPrompt + block };
171
+ });
172
+
173
+ // ── Turn Start: Persist agent state (only on change) ───────────────────────
174
+
175
+ pi.on("turn_start", async () => {
176
+ if (manager.shouldPersist()) {
177
+ pi.appendEntry("open-agents-state", { name: manager.getActiveName() });
178
+ manager.markPersisted();
179
+ }
180
+ });
181
+ }
182
+
183
+ // ─── Re-exports ──────────────────────────────────────────────────────────────
184
+
185
+ export * from "./types.ts";
186
+ export { parseFrontmatter } from "./config/frontmatter.ts";
187
+ export type { ParsedFrontmatter } from "./config/frontmatter.ts";
188
+ export { parseAgentDefinition, AgentParseError, SCHEMA_DEFAULTS } from "./config/schema.ts";
189
+ export type { SchemaDefaults } from "./config/schema.ts";
190
+ export { getDiscoveryPaths, deriveAgentName } from "./config/paths.ts";
191
+ export type { DiscoveryPath } from "./config/paths.ts";
192
+ export { loadAgents, selectableAgents, spawnableAgents, agentsAvailableTo } from "./discovery/loader.ts";
193
+ export type { LoadAgentsOptions } from "./discovery/loader.ts";
194
+ export { parsePermissionRules, mergePermissionConfigs } from "./permission/parser.ts";
195
+ export { matchPattern, isWildcardPattern } from "./permission/matcher.ts";
196
+ export { evaluate, toolToPermission, getDisabledTools, filterDisabledTools } from "./permission/evaluator.ts";
197
+
198
+ // Subagent re-exports
199
+ export { runSubagent, resolveSubagentSession, resolvePiEntryPoint } from "./subagent/executor.ts";
200
+ export type { AgentProgress, AgentResult, RunSubagentOptions, SubagentSessionInfo, SubagentSessionMode } from "./subagent/executor.ts";
201
+ export { resolveSkills } from "./subagent/skills.ts";
202
+ export type { ResolvedSkill } from "./subagent/skills.ts";
203
+ export { buildSubagentPrompt } from "./subagent/prompt.ts";
204
+ export { formatResultLines, formatUsage } from "./subagent/render.ts";