pi-subagents-j0k3r 1.0.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,182 @@
1
+ ---
2
+ name: subagents-configuration
3
+ description: "configuration-only guidance for Pi Subagents Extension, including markdown subagent definitions, .pi/subagents.json, model profiles, tool allowlists, background handoff shortcuts, history storage, and generic interaction handoff."
4
+ license: Apache-2.0
5
+ metadata:
6
+ author: j0k3r
7
+ version: "1.0"
8
+ ---
9
+
10
+ # Subagents Configuration
11
+
12
+ ## Registry Contract
13
+
14
+ Use this block as the machine-readable source for `.pi/skill-registry.json` generation. Keep it valid JSON.
15
+
16
+ ```json
17
+ {
18
+ "category": "workflow",
19
+ "domains": ["subagents-configuration", "subagent-config", "model-profile-config", "tool-allowlist-config", "subagent-history-config", "subagent-shortcut-config"],
20
+ "triggers": {
21
+ "paths": [
22
+ ".pi/subagents/**/*.md",
23
+ ".pi/subagents.json",
24
+ "subagents/**/*.md",
25
+ "subagents.json",
26
+ "~/.pi/agent/subagents/**/*.md",
27
+ "~/.pi/agent/subagents.json"
28
+ ],
29
+ "keywords": [
30
+ "subagents configuration",
31
+ "configure subagents",
32
+ "configurar subagents",
33
+ "configuro subagents",
34
+ "como configurar subagents",
35
+ "cómo configurar subagents",
36
+ "como configuro subagents",
37
+ "cómo configuro subagents",
38
+ "como se configura subagents",
39
+ "cómo se configura subagents",
40
+ "configurar subagentes",
41
+ "configuro subagentes",
42
+ "como configurar subagentes",
43
+ "cómo configurar subagentes",
44
+ "como configuro subagentes",
45
+ "cómo configuro subagentes",
46
+ "como se configura subagentes",
47
+ "cómo se configura subagentes",
48
+ "configuracion subagents",
49
+ "configuración subagents",
50
+ "configuracion subagentes",
51
+ "configuración subagentes",
52
+ "subagent config",
53
+ "subagents.json",
54
+ "model profiles configuration",
55
+ "tool allowlist configuration",
56
+ "subagent history configuration",
57
+ "background handoff shortcut",
58
+ "background_handoff_shortcut",
59
+ "interaction handoff configuration"
60
+ ]
61
+ },
62
+ "sdd_phases": [],
63
+ "related_skills": [],
64
+ "priority": 88
65
+ }
66
+ ```
67
+
68
+ Field conventions:
69
+
70
+ - `category`: short grouping such as `base`, `transversal`, `workflow`, `quality`, `security`, or `runtime`.
71
+ - `domains`: stable domain tags used for routing.
72
+ - `triggers.paths`: glob-like project paths that should activate this skill.
73
+ - `triggers.keywords`: configuration-only keywords that should activate this skill.
74
+ - `sdd_phases`: keep empty for configuration-only skills so phase routing alone does not load them.
75
+ - `related_skills`: configuration-adjacent skills only; do not add usage, implementation, or workflow skills.
76
+ - `priority`: routing priority from 0 to 100. Higher means consider earlier when multiple skills match.
77
+
78
+ ## Activation Contract
79
+
80
+ Use this skill only when the user asks how to configure Pi Subagents or when editing/reviewing subagent configuration files: markdown subagent definitions and project/global `subagents.json`. Cover model profiles, allowed tools, history settings, background task config, background handoff shortcuts, lean resources, and generic interaction handoff as configuration topics only.
81
+
82
+ Do not load this skill for ordinary subagent delegation/use (`subagent_run`, task status/result polling), extension implementation work, task history browsing, or editing this skill file; those are not configuration questions.
83
+
84
+ ## Hard Rules
85
+
86
+ - The main agent remains the orchestrator; subagents must not delegate to other subagents.
87
+ - Never allow `subagent_*` tools in subagent tool allowlists; the extension filters them, but configs should not include them.
88
+ - Prefer narrow tool allowlists per subagent. Do not grant write/bash tools unless the subagent purpose requires them.
89
+ - For SDD/PRD phase agents, prefer deterministic active-flow memory tools only: `memory_search`, `memory_get`, `memory_add`, and `memory_update`; avoid `memory_context` and `memory_recall` in subagent allowlists unless there is a specific reviewed need.
90
+ - For SDD phase agents, memory write tools may be allowed only for active SDD flow memory/artifacts according to `sdd-workflow`.
91
+ - Project subagents live in `.pi/subagents/*.md`; global user subagents live in `$PI_CODING_AGENT_DIR/subagents/*.md` or `~/.pi/agent/subagents/*.md`.
92
+ - Project definitions override global definitions with the same normalized name.
93
+ - Subagents config resolves as a cascade: project `.pi/subagents.json` overrides global `$PI_CODING_AGENT_DIR/subagents.json` or `~/.pi/agent/subagents.json`; missing project fields fall back to global config; fields missing from both fall back to built-in defaults. Communicate this precedence to users when explaining config behavior.
94
+ - `model_profiles` are deep-merged by agent name with project-local profile fields taking precedence over global profile fields.
95
+ - Nested subagent sessions should use `session_resources: "lean"` by default so the subagent markdown body becomes the nested session system prompt, the delegated user prompt contains only orchestrator context/task, and workflow skills, prompt templates, themes, context files, and startup context injections are not auto-loaded.
96
+ - In lean mode, extensions are loaded for allowlisted tools and tool-safety hooks only; prompt/context lifecycle hooks such as `before_agent_start` and `context` must not inject hidden messages into subagent turns.
97
+ - Subagent task history is stored globally under data storage, but rows remain project-scoped by `cwd`; history stores delegated prompt and subagent system prompt separately.
98
+ - Debug logging is disabled by default with `debug: false`; when enabled in global or project `subagents.json`, logs are written to the executing project's `cwd/.pi/subagents-debug.log`.
99
+ - After changing subagent markdown/config or extension code, tell the user to `/reload` or restart Pi.
100
+
101
+ Recommended `subagents.json` starter:
102
+
103
+ ```json
104
+ {
105
+ "mode": "opencode",
106
+ "timeout_ms": 600000,
107
+ "stall_timeout_ms": 120000,
108
+ "max_concurrency": 5,
109
+ "debug": false,
110
+ "session_resources": "lean",
111
+ "history_panel_shortcut": "ctrl+,",
112
+ "detail_cancel_shortcut": "x",
113
+ "background_handoff_shortcut": "ctrl+h",
114
+ "default_tools": [
115
+ "read",
116
+ "memory_context",
117
+ "memory_search",
118
+ "memory_recall",
119
+ "memory_get"
120
+ ],
121
+ "model_profiles": {}
122
+ }
123
+ ```
124
+
125
+ Markdown subagent frontmatter pattern:
126
+
127
+ ```md
128
+ ---
129
+ name: discovery
130
+ description: investigates isolated ideas, code, documentation, and context7 before deciding next workflow
131
+ tools:
132
+ - read
133
+ - memory_search
134
+ model: anthropic/claude-sonnet-4-5
135
+ effort: low
136
+ ---
137
+
138
+ # Discovery Subagent
139
+
140
+ Instructions...
141
+ ```
142
+
143
+ ## Decision Gates
144
+
145
+ - If the subagent will modify files, run bash, or write memory, ask whether a full SDD workflow or stricter review is required.
146
+ - If the subagent needs human input, require a structured `interaction_required` request with enough prompt, payload, and expected-response data for the parent to answer.
147
+ - If a project wants many subagents or broad tools, recommend starting with read-only discovery agents and expanding deliberately.
148
+ - If model profiles are global, confirm the user wants global behavior rather than project-only config.
149
+
150
+ ## Execution Steps
151
+
152
+ 1. Identify target scope: global subagent, project subagent, global config, or project config, and explain the cascade when relevant: project-local config first, then global config for missing fields, then built-in defaults.
153
+ 2. Read existing subagent markdown/config before editing.
154
+ 3. For new subagents, choose lowercase kebab-case names and clear trigger-focused descriptions.
155
+ 4. Set minimal tool allowlists; remove any `subagent_*` entries.
156
+ 5. Configure `model_profiles`, `default_model`, and `default_effort` only when the user wants explicit routing.
157
+ 6. Configure `debug: true` only for temporary diagnostics; keep `debug: false` by default and remember logs are written under the executing project's `.pi` directory.
158
+ 7. Validate JSON syntax for `subagents.json` and frontmatter/body structure for markdown agents.
159
+ 8. When configuring OpenCode-mode history opening, prefer `history_panel_shortcut` with `ctrl+<letter>` or `ctrl+,` values and document any built-in shortcut conflicts.
160
+ 9. When configuring history/detail cancellation, prefer `detail_cancel_shortcut` with `x` by default; it supports `ctrl+<letter>`, `ctrl+shift+<letter>`, `ctrl+,`, or one lowercase letter. It only cancels the selected queued/running task while the detail panel is active.
161
+ 10. When configuring Claude-mode background handoff, prefer `background_handoff_shortcut` with `ctrl+<letter>` values and document any built-in shortcut conflicts.
162
+ 11. Tell the user to `/reload` or restart Pi.
163
+ 12. If validating configuration after reload, use `subagent_list_agents` only when the user asks for runtime verification; do not run delegated tasks just to validate configuration.
164
+
165
+ ## Output Contract
166
+
167
+ Return:
168
+
169
+ - Skill applied: `subagents-configuration`.
170
+ - Scope/path configured or reviewed.
171
+ - Subagents/config fields added, changed, or preserved.
172
+ - Tool allowlist, system-prompt isolation, Context7 scope, memory-tool scope, debug logging, and model/effort decisions.
173
+ - Related configuration skills considered or loaded.
174
+ - Validation executed, or the concrete reason it was not run.
175
+ - Required reload/restart note and open risks.
176
+
177
+ ## References
178
+
179
+ - `extensions/subagents/README.md` — Subagents configuration, shortcuts, history settings, and model profiles.
180
+ - `extensions/subagents/src/config.ts` — subagent/config loading and tool filtering.
181
+ - `extensions/subagents/src/history.ts` — global history storage behavior.
182
+ - This package README — generic interaction handoff contract and runtime behavior.
package/src/config.ts ADDED
@@ -0,0 +1,262 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import type { ModelRef, SubagentDefinition, SubagentModelProfile, SubagentModelProfiles, SubagentSessionResources, SubagentsConfig, SubagentUiMode, ThinkingEffort } from './types.js';
5
+
6
+ const DEFAULT_TOOLS = ['read', 'memory_context', 'memory_search', 'memory_recall', 'memory_get'];
7
+ const DEFAULT_MAX_CONCURRENCY = 5;
8
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
9
+ const DEFAULT_STALL_TIMEOUT_MS = 2 * 60 * 1000;
10
+ const DEFAULT_BACKGROUND_HANDOFF_SHORTCUT = 'ctrl+h';
11
+ const DEFAULT_HISTORY_PANEL_SHORTCUT = 'ctrl+,';
12
+ const DEFAULT_DETAIL_CANCEL_SHORTCUT = 'x';
13
+ const BLOCKED_SUBAGENT_TOOLS = new Set([
14
+ 'subagent_run',
15
+ 'subagent_list_agents',
16
+ 'subagent_status',
17
+ 'subagent_result',
18
+ 'subagent_list_tasks',
19
+ 'subagent_cancel',
20
+ ]);
21
+
22
+ function sanitizeTools(tools: string[]): string[] {
23
+ return tools.map(String).filter((tool) => !BLOCKED_SUBAGENT_TOOLS.has(tool) && !tool.startsWith('subagent_'));
24
+ }
25
+
26
+ function parseScalar(value: string): any {
27
+ const trimmed = value.trim();
28
+ if (!trimmed) return '';
29
+ if (trimmed === 'true') return true;
30
+ if (trimmed === 'false') return false;
31
+ if (/^\d+$/.test(trimmed)) return Number(trimmed);
32
+ return trimmed.replace(/^['"]|['"]$/g, '');
33
+ }
34
+
35
+ export function parseFrontmatter(text: string): { data: Record<string, any>; body: string } {
36
+ if (!text.startsWith('---\n')) return { data: {}, body: text };
37
+ const end = text.indexOf('\n---', 4);
38
+ if (end === -1) return { data: {}, body: text };
39
+ const raw = text.slice(4, end).trim();
40
+ const body = text.slice(end + 4).replace(/^\r?\n/, '');
41
+ const data: Record<string, any> = {};
42
+ let currentKey: string | undefined;
43
+ for (const line of raw.split(/\r?\n/)) {
44
+ if (!line.trim()) continue;
45
+ const list = line.match(/^\s*-\s+(.+)$/);
46
+ if (list && currentKey) {
47
+ if (!Array.isArray(data[currentKey])) data[currentKey] = [];
48
+ data[currentKey].push(parseScalar(list[1]));
49
+ continue;
50
+ }
51
+ const m = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
52
+ if (!m) continue;
53
+ currentKey = m[1];
54
+ const value = m[2];
55
+ if (!value) data[currentKey] = [];
56
+ else data[currentKey] = parseScalar(value);
57
+ }
58
+ return { data, body };
59
+ }
60
+
61
+ function agentDir(): string {
62
+ return process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), '.pi', 'agent');
63
+ }
64
+
65
+ function subagentsConfigPath(dir = agentDir()): string {
66
+ return path.join(dir, 'subagents.json');
67
+ }
68
+
69
+ function readJson(file: string): any {
70
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return {}; }
71
+ }
72
+
73
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
74
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
75
+ }
76
+
77
+ function positiveNumber(value: any, fallback: number): number {
78
+ const parsed = Number(value);
79
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
80
+ }
81
+
82
+ function positiveInteger(value: any, fallback: number): number {
83
+ return Math.max(1, Math.floor(positiveNumber(value, fallback)));
84
+ }
85
+
86
+ export function parseModel(value: any): ModelRef | undefined {
87
+ if (!value || value === 'default') return undefined;
88
+ if (typeof value === 'string') {
89
+ const parts = value.split('/');
90
+ if (parts.length !== 2) return undefined;
91
+ const [provider, id] = parts.map((part) => part.trim());
92
+ return provider && id ? { provider, id } : undefined;
93
+ }
94
+ if (isPlainObject(value) && typeof value.provider === 'string' && typeof value.id === 'string') {
95
+ const provider = value.provider.trim();
96
+ const id = value.id.trim();
97
+ return provider && id ? { provider, id } : undefined;
98
+ }
99
+ return undefined;
100
+ }
101
+
102
+ const THINKING_EFFORTS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh']);
103
+
104
+ export function parseEffort(value: any): ThinkingEffort | undefined {
105
+ if (!value || value === 'default') return undefined;
106
+ const effort = String(value).trim().toLowerCase();
107
+ return THINKING_EFFORTS.has(effort) ? effort as ThinkingEffort : undefined;
108
+ }
109
+
110
+ function parseSessionResources(value: any): SubagentSessionResources {
111
+ const resources = String(value ?? 'lean').trim().toLowerCase();
112
+ return resources === 'full' ? 'full' : 'lean';
113
+ }
114
+
115
+ function parseMode(value: any): SubagentUiMode {
116
+ const mode = String(value ?? 'opencode').trim().toLowerCase();
117
+ return mode === 'claude' ? 'claude' : 'opencode';
118
+ }
119
+
120
+ function parseCtrlShortcut(value: any, fallback: string): string {
121
+ const shortcut = String(value ?? fallback).trim().toLowerCase();
122
+ return /^(?:ctrl\+(?:[a-z]|,)|ctrl\+shift\+[a-z])$/.test(shortcut) ? shortcut : fallback;
123
+ }
124
+
125
+ function parseDetailShortcut(value: any): string {
126
+ const shortcut = String(value ?? DEFAULT_DETAIL_CANCEL_SHORTCUT).trim().toLowerCase();
127
+ if (/^[a-z]$/.test(shortcut)) return shortcut;
128
+ return /^(?:ctrl\+(?:[a-z]|,)|ctrl\+shift\+[a-z])$/.test(shortcut) ? shortcut : DEFAULT_DETAIL_CANCEL_SHORTCUT;
129
+ }
130
+
131
+ function parseBackgroundHandoffShortcut(value: any): string {
132
+ return parseCtrlShortcut(value, DEFAULT_BACKGROUND_HANDOFF_SHORTCUT);
133
+ }
134
+
135
+ function parseBoolean(value: any, fallback = false): boolean {
136
+ if (value === undefined || value === null) return fallback;
137
+ if (typeof value === 'boolean') return value;
138
+ const normalized = String(value).trim().toLowerCase();
139
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
140
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
141
+ return fallback;
142
+ }
143
+
144
+ function parseModelProfile(value: unknown): SubagentModelProfile | undefined {
145
+ if (!isPlainObject(value)) return undefined;
146
+ const profile: SubagentModelProfile = {};
147
+ const model = parseModel(value.model);
148
+ const effort = parseEffort(value.effort ?? value.thinking_level ?? value.thinkingLevel);
149
+ if (model) profile.model = model;
150
+ if (effort) profile.effort = effort;
151
+ return Object.keys(profile).length ? profile : undefined;
152
+ }
153
+
154
+ function parseModelProfiles(value: unknown): SubagentModelProfiles {
155
+ if (!isPlainObject(value)) return {};
156
+ const profiles: SubagentModelProfiles = {};
157
+ for (const [name, rawProfile] of Object.entries(value)) {
158
+ const normalizedName = name.trim();
159
+ if (!normalizedName) continue;
160
+ const profile = parseModelProfile(rawProfile);
161
+ if (profile) profiles[normalizedName] = profile;
162
+ }
163
+ return profiles;
164
+ }
165
+
166
+ function mergeModelProfiles(globalRaw: unknown, projectRaw: unknown): SubagentModelProfiles {
167
+ const globalProfiles = parseModelProfiles(globalRaw);
168
+ const projectProfiles = parseModelProfiles(projectRaw);
169
+ const merged: SubagentModelProfiles = { ...globalProfiles };
170
+ for (const [name, projectProfile] of Object.entries(projectProfiles)) {
171
+ merged[name] = { ...(merged[name] ?? {}), ...projectProfile };
172
+ }
173
+ return merged;
174
+ }
175
+
176
+ function serializeModelRef(model: ModelRef): string {
177
+ return `${model.provider}/${model.id}`;
178
+ }
179
+
180
+ function cleanProfile(profile: SubagentModelProfile): Record<string, string> | undefined {
181
+ const cleaned: Record<string, string> = {};
182
+ if (profile.model) cleaned.model = serializeModelRef(profile.model);
183
+ if (profile.effort) cleaned.effort = profile.effort;
184
+ return Object.keys(cleaned).length ? cleaned : undefined;
185
+ }
186
+
187
+ export function readSubagentsConfig(cwd: string): SubagentsConfig {
188
+ const globalRaw = readJson(subagentsConfigPath());
189
+ const projectRaw = readJson(path.join(cwd, '.pi', 'subagents.json'));
190
+ const raw = { ...globalRaw, ...projectRaw };
191
+ return {
192
+ default_model: parseModel(raw.default_model),
193
+ default_effort: parseEffort(raw.default_effort ?? raw.default_thinking_level ?? raw.thinkingLevel),
194
+ model_profiles: mergeModelProfiles(globalRaw.model_profiles, projectRaw.model_profiles),
195
+ timeout_ms: positiveInteger(raw.timeout_ms, DEFAULT_TIMEOUT_MS),
196
+ stall_timeout_ms: positiveInteger(raw.stall_timeout_ms, DEFAULT_STALL_TIMEOUT_MS),
197
+ max_concurrency: positiveInteger(raw.max_concurrency, DEFAULT_MAX_CONCURRENCY),
198
+ default_tools: sanitizeTools(Array.isArray(raw.default_tools) ? raw.default_tools.map(String) : DEFAULT_TOOLS),
199
+ session_resources: parseSessionResources(raw.session_resources ?? raw.sessionResources),
200
+ mode: parseMode(raw.mode),
201
+ background_handoff_shortcut: parseBackgroundHandoffShortcut(raw.background_handoff_shortcut ?? raw.backgroundHandoffShortcut),
202
+ history_panel_shortcut: parseCtrlShortcut(raw.history_panel_shortcut ?? raw.historyPanelShortcut, DEFAULT_HISTORY_PANEL_SHORTCUT),
203
+ detail_cancel_shortcut: parseDetailShortcut(raw.detail_cancel_shortcut ?? raw.detailCancelShortcut),
204
+ debug: parseBoolean(raw.debug, false),
205
+ };
206
+ }
207
+
208
+ export function saveGlobalSubagentModelProfile(input: { agentName: string; profile: SubagentModelProfile; agentDir?: string }): void {
209
+ const file = subagentsConfigPath(input.agentDir);
210
+ const root = readJson(file);
211
+ const writableRoot: Record<string, unknown> = isPlainObject(root) ? { ...root } : {};
212
+ const modelProfiles = isPlainObject(writableRoot.model_profiles) ? { ...writableRoot.model_profiles } : {};
213
+ const agentName = input.agentName.trim().toLowerCase();
214
+ const cleaned = cleanProfile(input.profile);
215
+ if (agentName && cleaned) modelProfiles[agentName] = cleaned;
216
+ if (Object.keys(modelProfiles).length) writableRoot.model_profiles = modelProfiles;
217
+ else delete writableRoot.model_profiles;
218
+ fs.mkdirSync(path.dirname(file), { recursive: true });
219
+ fs.writeFileSync(file, `${JSON.stringify(writableRoot, null, 2)}\n`, 'utf8');
220
+ }
221
+
222
+ export function resetGlobalSubagentModelProfileField(input: { agentName: string; field: 'model' | 'effort'; agentDir?: string }): void {
223
+ const file = subagentsConfigPath(input.agentDir);
224
+ const root = readJson(file);
225
+ const writableRoot: Record<string, unknown> = isPlainObject(root) ? { ...root } : {};
226
+ const modelProfiles = isPlainObject(writableRoot.model_profiles) ? { ...writableRoot.model_profiles } : {};
227
+ const agentName = input.agentName.trim().toLowerCase();
228
+ const existing = isPlainObject(modelProfiles[agentName]) ? { ...modelProfiles[agentName] } : {};
229
+ delete existing[input.field];
230
+ if (Object.keys(existing).length) modelProfiles[agentName] = existing;
231
+ else delete modelProfiles[agentName];
232
+ if (Object.keys(modelProfiles).length) writableRoot.model_profiles = modelProfiles;
233
+ else delete writableRoot.model_profiles;
234
+ fs.mkdirSync(path.dirname(file), { recursive: true });
235
+ fs.writeFileSync(file, `${JSON.stringify(writableRoot, null, 2)}\n`, 'utf8');
236
+ }
237
+
238
+ function loadSubagentsFromDir(dir: string): SubagentDefinition[] {
239
+ if (!fs.existsSync(dir)) return [];
240
+ return fs.readdirSync(dir)
241
+ .filter((f) => f.endsWith('.md'))
242
+ .sort()
243
+ .map((file) => {
244
+ const filePath = path.join(dir, file);
245
+ const { data, body } = parseFrontmatter(fs.readFileSync(filePath, 'utf8'));
246
+ const name = String(data.name || path.basename(file, '.md')).trim().toLowerCase();
247
+ const description = String(data.description || `${name} subagent`).trim();
248
+ const tools = sanitizeTools(Array.isArray(data.tools) ? data.tools.map(String) : DEFAULT_TOOLS);
249
+ return { name, description, filePath, instructions: body.trim(), model: parseModel(data.model), effort: parseEffort(data.effort ?? data.thinking_level ?? data.thinkingLevel), tools };
250
+ });
251
+ }
252
+
253
+ export function loadSubagents(cwd: string): SubagentDefinition[] {
254
+ const byName = new Map<string, SubagentDefinition>();
255
+ for (const agent of loadSubagentsFromDir(path.join(agentDir(), 'subagents'))) byName.set(agent.name, agent);
256
+ for (const agent of loadSubagentsFromDir(path.join(cwd, '.pi', 'subagents'))) byName.set(agent.name, agent);
257
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
258
+ }
259
+
260
+ export function getSubagent(cwd: string, name: string): SubagentDefinition | undefined {
261
+ return loadSubagents(cwd).find((a) => a.name === name.toLowerCase());
262
+ }
package/src/debug.ts ADDED
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readSubagentsConfig } from './config.js';
4
+
5
+ export function isSubagentsDebugEnabled(cwd?: string): boolean {
6
+ if (!cwd) return false;
7
+ try {
8
+ return readSubagentsConfig(cwd).debug === true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ const DEBUG_LOG_RELATIVE_PATH = '.pi/subagents-debug.log';
15
+
16
+ function ensureDebugLogGitignored(root: string): void {
17
+ try {
18
+ if (!fs.existsSync(path.join(root, '.git'))) return;
19
+ const gitignorePath = path.join(root, '.gitignore');
20
+ let current = '';
21
+ try { current = fs.readFileSync(gitignorePath, 'utf8'); } catch {}
22
+ const lines = current.split(/\r?\n/).map((line) => line.trim());
23
+ if (lines.includes(DEBUG_LOG_RELATIVE_PATH)) return;
24
+ const prefix = current.length > 0 && !current.endsWith('\n') ? '\n' : '';
25
+ fs.appendFileSync(gitignorePath, `${prefix}${DEBUG_LOG_RELATIVE_PATH}\n`);
26
+ } catch {}
27
+ }
28
+
29
+ export function writeSubagentsDebugLog(cwd: string | undefined, scope: string, data: unknown): void {
30
+ const root = cwd ?? process.cwd();
31
+ if (!isSubagentsDebugEnabled(root)) return;
32
+ try {
33
+ const file = path.join(root, DEBUG_LOG_RELATIVE_PATH);
34
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
35
+ ensureDebugLogGitignored(root);
36
+ fs.appendFileSync(file, `${new Date().toISOString()} ${scope} ${JSON.stringify(data, (_key, value) => value instanceof Error ? { name: value.name, message: value.message, stack: value.stack } : value).slice(0, 4000)}\n`);
37
+ } catch {}
38
+ }