@agimon-ai/po-team 0.0.0-alpha.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 (110) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +156 -0
  3. package/dist/agentDiscovery-Whwzc4dP.cjs +1637 -0
  4. package/dist/agentDiscovery-Whwzc4dP.cjs.map +1 -0
  5. package/dist/agentDiscovery-ZQlFkKvB.mjs +1529 -0
  6. package/dist/agentDiscovery-ZQlFkKvB.mjs.map +1 -0
  7. package/dist/apiContracts.cjs +162 -0
  8. package/dist/apiContracts.cjs.map +1 -0
  9. package/dist/apiContracts.d.mts +53 -0
  10. package/dist/apiContracts.d.mts.map +1 -0
  11. package/dist/apiContracts.mjs +157 -0
  12. package/dist/apiContracts.mjs.map +1 -0
  13. package/dist/atomicJson-D0346bt5.cjs +178 -0
  14. package/dist/atomicJson-D0346bt5.cjs.map +1 -0
  15. package/dist/atomicJson-D2t0CDh9.mjs +137 -0
  16. package/dist/atomicJson-D2t0CDh9.mjs.map +1 -0
  17. package/dist/capabilityCeiling-C-Y7H8oC.cjs +191 -0
  18. package/dist/capabilityCeiling-C-Y7H8oC.cjs.map +1 -0
  19. package/dist/capabilityCeiling-CF7kAS18.mjs +156 -0
  20. package/dist/capabilityCeiling-CF7kAS18.mjs.map +1 -0
  21. package/dist/capabilityCeiling.cjs +10 -0
  22. package/dist/capabilityCeiling.d.mts +46 -0
  23. package/dist/capabilityCeiling.d.mts.map +1 -0
  24. package/dist/capabilityCeiling.mjs +3 -0
  25. package/dist/delegation.cjs +49 -0
  26. package/dist/delegation.cjs.map +1 -0
  27. package/dist/delegation.d.mts +269 -0
  28. package/dist/delegation.d.mts.map +1 -0
  29. package/dist/delegation.mjs +34 -0
  30. package/dist/delegation.mjs.map +1 -0
  31. package/dist/delegationApi.cjs +92 -0
  32. package/dist/delegationApi.d.mts +2 -0
  33. package/dist/delegationApi.mjs +2 -0
  34. package/dist/env.cjs +62 -0
  35. package/dist/env.d.mts +2 -0
  36. package/dist/env.mjs +2 -0
  37. package/dist/environment-CaCW3SgT.cjs +320 -0
  38. package/dist/environment-CaCW3SgT.cjs.map +1 -0
  39. package/dist/environment-DrqhrxG_.mjs +75 -0
  40. package/dist/environment-DrqhrxG_.mjs.map +1 -0
  41. package/dist/environment-ma5oQ7uM.d.mts +63 -0
  42. package/dist/environment-ma5oQ7uM.d.mts.map +1 -0
  43. package/dist/extensions/server.cjs +6940 -0
  44. package/dist/extensions/server.cjs.map +1 -0
  45. package/dist/extensions/server.d.mts +5 -0
  46. package/dist/extensions/server.d.mts.map +1 -0
  47. package/dist/extensions/server.mjs +6932 -0
  48. package/dist/extensions/server.mjs.map +1 -0
  49. package/dist/extensions/web.mjs +2308 -0
  50. package/dist/extensions/web.mjs.map +1 -0
  51. package/dist/externalProcessIpc-C-15lJrB.mjs +584 -0
  52. package/dist/externalProcessIpc-C-15lJrB.mjs.map +1 -0
  53. package/dist/externalProcessIpc-ojE0pvw0.cjs +690 -0
  54. package/dist/externalProcessIpc-ojE0pvw0.cjs.map +1 -0
  55. package/dist/fablePlan.cjs +17 -0
  56. package/dist/fablePlan.d.mts +2 -0
  57. package/dist/fablePlan.mjs +2 -0
  58. package/dist/fablePlanApi-BsHhgVFV.mjs +118 -0
  59. package/dist/fablePlanApi-BsHhgVFV.mjs.map +1 -0
  60. package/dist/fablePlanApi-CZ3uUcMi.d.mts +97 -0
  61. package/dist/fablePlanApi-CZ3uUcMi.d.mts.map +1 -0
  62. package/dist/fablePlanApi-wBu9bRXU.cjs +207 -0
  63. package/dist/fablePlanApi-wBu9bRXU.cjs.map +1 -0
  64. package/dist/fablePlanApi.cjs +17 -0
  65. package/dist/fablePlanApi.d.mts +2 -0
  66. package/dist/fablePlanApi.mjs +2 -0
  67. package/dist/index.cjs +49 -0
  68. package/dist/index.cjs.map +1 -0
  69. package/dist/index.d.mts +160 -0
  70. package/dist/index.d.mts.map +1 -0
  71. package/dist/index.mjs +45 -0
  72. package/dist/index.mjs.map +1 -0
  73. package/dist/runs/background/cliRunnerEntry.cjs +539 -0
  74. package/dist/runs/background/cliRunnerEntry.cjs.map +1 -0
  75. package/dist/runs/background/cliRunnerEntry.d.mts +1 -0
  76. package/dist/runs/background/cliRunnerEntry.mjs +537 -0
  77. package/dist/runs/background/cliRunnerEntry.mjs.map +1 -0
  78. package/dist/runtimeSubagentPolicy.cjs +5 -0
  79. package/dist/runtimeSubagentPolicy.d.mts +2 -0
  80. package/dist/runtimeSubagentPolicy.mjs +2 -0
  81. package/dist/runtimeSubagentTool.cjs +11 -0
  82. package/dist/runtimeSubagentTool.d.mts +2 -0
  83. package/dist/runtimeSubagentTool.mjs +2 -0
  84. package/dist/subagentPolicy-ALJam4GN.d.mts +33 -0
  85. package/dist/subagentPolicy-ALJam4GN.d.mts.map +1 -0
  86. package/dist/subagentPolicy-DQRyKDH-.cjs +39 -0
  87. package/dist/subagentPolicy-DQRyKDH-.cjs.map +1 -0
  88. package/dist/subagentPolicy-DxjsKeyj.mjs +22 -0
  89. package/dist/subagentPolicy-DxjsKeyj.mjs.map +1 -0
  90. package/dist/subagentPolicy.cjs +5 -0
  91. package/dist/subagentPolicy.d.mts +2 -0
  92. package/dist/subagentPolicy.mjs +2 -0
  93. package/dist/subagentTool-BTssGLw8.d.mts +103 -0
  94. package/dist/subagentTool-BTssGLw8.d.mts.map +1 -0
  95. package/dist/subagentTool-Ca2zQXKw.cjs +227 -0
  96. package/dist/subagentTool-Ca2zQXKw.cjs.map +1 -0
  97. package/dist/subagentTool-WOYrjxgT.mjs +186 -0
  98. package/dist/subagentTool-WOYrjxgT.mjs.map +1 -0
  99. package/dist/subagentTool.cjs +10 -0
  100. package/dist/subagentTool.d.mts +2 -0
  101. package/dist/subagentTool.mjs +2 -0
  102. package/dist/teamSnapshot-DgaANhBs.mjs +24 -0
  103. package/dist/teamSnapshot-DgaANhBs.mjs.map +1 -0
  104. package/dist/teamSnapshot-oKuGFdYc.cjs +35 -0
  105. package/dist/teamSnapshot-oKuGFdYc.cjs.map +1 -0
  106. package/dist/teamSnapshot.cjs +4 -0
  107. package/dist/teamSnapshot.d.mts +59 -0
  108. package/dist/teamSnapshot.d.mts.map +1 -0
  109. package/dist/teamSnapshot.mjs +2 -0
  110. package/package.json +201 -0
@@ -0,0 +1,1529 @@
1
+ import "./atomicJson-D2t0CDh9.mjs";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { loadHarnessState, loadMajorModesConfig, resolvePackageConfigurations } from "@agimon-ai/po-config";
5
+ import * as os from "node:os";
6
+ import { piAgentDirectory } from "@agimon-ai/po-core/piSettings";
7
+ //#region src/schemas/team/packageConfig.ts
8
+ const PO_TEAM_PACKAGE = "@agimon-ai/po-team";
9
+ const TEAM_CONFIG_KEYS = ["models", "excludeTools"];
10
+ const TEAM_MODEL_KEYS = ["model", "thinking"];
11
+ const THINKING_VALUES = /* @__PURE__ */ new Set([
12
+ "off",
13
+ "minimal",
14
+ "low",
15
+ "medium",
16
+ "high",
17
+ "xhigh",
18
+ "max"
19
+ ]);
20
+ function isRecord(value) {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+ function assertKnownKeys(value, keys, location) {
24
+ const unsupported = Object.keys(value).filter((key) => !keys.includes(key));
25
+ if (unsupported.length > 0) throw new Error(`${location} has unsupported field(s): ${unsupported.join(", ")}`);
26
+ }
27
+ /** Validate and normalize one Team-owned package configuration mapping. */
28
+ function parseTeamPackageConfig(value, location) {
29
+ if (!isRecord(value)) throw new Error(`${location} must be a mapping`);
30
+ assertKnownKeys(value, TEAM_CONFIG_KEYS, location);
31
+ const hasModels = Object.hasOwn(value, "models");
32
+ const hasExcludeTools = Object.hasOwn(value, "excludeTools");
33
+ if (!hasModels && !hasExcludeTools) throw new Error(`${location} must define models or excludeTools`);
34
+ let models;
35
+ if (hasModels) {
36
+ if (!Array.isArray(value.models) || value.models.length === 0) throw new Error(`${location}.models must be a non-empty array`);
37
+ models = value.models.map((entry, index) => {
38
+ const entryLocation = `${location}.models[${index}]`;
39
+ if (!isRecord(entry)) throw new Error(`${entryLocation} must be a mapping`);
40
+ assertKnownKeys(entry, TEAM_MODEL_KEYS, entryLocation);
41
+ if (typeof entry.model !== "string" || !entry.model.trim()) throw new Error(`${entryLocation}.model must be a non-empty string`);
42
+ if (entry.thinking !== void 0 && (typeof entry.thinking !== "string" || !THINKING_VALUES.has(entry.thinking))) throw new Error(`${entryLocation}.thinking must be one of: ${[...THINKING_VALUES].join(", ")}`);
43
+ return {
44
+ model: entry.model.trim(),
45
+ ...entry.thinking ? { thinking: entry.thinking } : {}
46
+ };
47
+ });
48
+ }
49
+ let excludeTools;
50
+ if (hasExcludeTools) {
51
+ if (!Array.isArray(value.excludeTools) || value.excludeTools.length === 0) throw new Error(`${location}.excludeTools must be a non-empty array`);
52
+ excludeTools = [...new Set(value.excludeTools.map((entry, index) => {
53
+ if (typeof entry !== "string" || !entry.trim()) throw new Error(`${location}.excludeTools[${index}] must be a non-empty string`);
54
+ return entry.trim();
55
+ }))];
56
+ }
57
+ return {
58
+ ...models ? { models } : {},
59
+ ...excludeTools ? { excludeTools } : {}
60
+ };
61
+ }
62
+ /** Merge Team configurations contributed by selected layers in selection order. */
63
+ function mergeTeamPackageConfigurations(entries) {
64
+ let models;
65
+ const excludeTools = /* @__PURE__ */ new Set();
66
+ let hasExcludeTools = false;
67
+ for (const entry of entries) {
68
+ const location = `Package "${entry.specifier}" config in layer "${entry.layer}" of .po/modes.yaml`;
69
+ const next = parseTeamPackageConfig(entry.config, location);
70
+ if (next.models) models = next.models.map((model) => ({ ...model }));
71
+ if (next.excludeTools) {
72
+ hasExcludeTools = true;
73
+ for (const tool of next.excludeTools) excludeTools.add(tool);
74
+ }
75
+ }
76
+ if (!models && !hasExcludeTools) return void 0;
77
+ return {
78
+ ...models ? { models } : {},
79
+ ...hasExcludeTools ? { excludeTools: [...excludeTools] } : {}
80
+ };
81
+ }
82
+ //#endregion
83
+ //#region src/services/agentFrontmatter/index.ts
84
+ /**
85
+ * Parsing the YAML frontmatter block at the head of an agent or chain file.
86
+ *
87
+ * This is a deliberately partial YAML reader, not a call into a YAML library.
88
+ * The block is read once, keys are kept as raw strings, and each consumer
89
+ * interprets its own key. That is what lets an unrecognised key survive a
90
+ * read/write round-trip: the loader copies it into `extraFields` verbatim
91
+ * instead of losing it to a typed parse.
92
+ *
93
+ * DESIGN PATTERNS:
94
+ * - Nested and folded blocks are captured as one string with newlines intact.
95
+ * A consumer that wants structure re-parses that string with a real YAML
96
+ * parser, so structural validation stays with the field that needs it
97
+ * - Common leading whitespace is stripped from a block, so a serializer can
98
+ * re-indent to its own level without the original indent accumulating
99
+ *
100
+ * AVOID:
101
+ * - Growing this into a general YAML parser; hand a block value to `yaml`
102
+ * instead
103
+ * - Interpreting field semantics here
104
+ */
105
+ const OPENING_FENCE = "---";
106
+ /** Length of the opening `---` fence, before the newline that follows it. */
107
+ const FENCE_LENGTH = 3;
108
+ /** Length of the fence plus its trailing newline, used to slice past it. */
109
+ const FENCE_WITH_NEWLINE_LENGTH = 4;
110
+ /** Offset of the first frontmatter character: past `---\n`. */
111
+ const FRONTMATTER_START_INDEX = FENCE_WITH_NEWLINE_LENGTH;
112
+ /** The closing fence as it appears mid-document, searched from after the opener. */
113
+ const CLOSING_FENCE = "\n---";
114
+ const FOLDED_BLOCK_INDICATORS = /* @__PURE__ */ new Set([">", ">-"]);
115
+ /** Escape regex metacharacters so a literal string can be used in a RegExp. */
116
+ function escapeRegex(value) {
117
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
118
+ }
119
+ /**
120
+ * Fold a YAML folded block scalar.
121
+ *
122
+ * Folding joins lines with a space, except that more-indented lines and blank
123
+ * separators are structural in YAML and must survive as real newlines. Getting
124
+ * this wrong silently reflows a user's prompt, so the two cases are tracked
125
+ * explicitly rather than approximated by a global replace.
126
+ */
127
+ function foldBlock(block) {
128
+ let folded = "";
129
+ let hasContent = false;
130
+ let previousIsMoreIndented = false;
131
+ let blankLines = 0;
132
+ for (const line of block.split("\n")) {
133
+ const current = line.trimEnd();
134
+ if (current.trim() === "") {
135
+ if (hasContent) blankLines++;
136
+ continue;
137
+ }
138
+ const currentIsMoreIndented = current.length > current.trimStart().length;
139
+ if (hasContent) {
140
+ if (blankLines > 0) folded += "\n".repeat(blankLines + (previousIsMoreIndented || currentIsMoreIndented ? 1 : 0));
141
+ else folded += previousIsMoreIndented || currentIsMoreIndented ? "\n" : " ";
142
+ }
143
+ folded += current;
144
+ hasContent = true;
145
+ previousIsMoreIndented = currentIsMoreIndented;
146
+ blankLines = 0;
147
+ }
148
+ return folded.trim();
149
+ }
150
+ /**
151
+ * Read a list-valued field written either comma-separated or as a block list.
152
+ *
153
+ * Only a leading `- ` marker is treated as a list bullet. A bare hyphen inside
154
+ * a value is left alone, so hyphenated names like `context-builder` survive.
155
+ */
156
+ function parseFrontmatterList(raw) {
157
+ if (raw === void 0) return void 0;
158
+ return raw.split("\n").flatMap((line) => {
159
+ const value = line.trim();
160
+ return (value.match(/^-\s+(.+)$/)?.[1] ?? value).split(",");
161
+ }).map((value) => value.trim()).filter(Boolean);
162
+ }
163
+ /** Strip the block's common leading whitespace so it can be re-indented later. */
164
+ function dedentBlock(blockLines) {
165
+ const rawBlock = blockLines.join("\n");
166
+ const prefix = rawBlock.match(/^[ \t]+(?=\S)/m)?.[0] ?? "";
167
+ if (!prefix) return rawBlock;
168
+ return rawBlock.replace(new RegExp(`^${escapeRegex(prefix)}`, "gm"), "").replace(/^\n/, "");
169
+ }
170
+ /**
171
+ * Split a file into its frontmatter map and its body.
172
+ *
173
+ * A file with no opening fence, or with an unterminated one, is treated as all
174
+ * body with empty frontmatter. The loader then rejects it for want of a name,
175
+ * which is the right outcome: a malformed file should be skipped, not crash a
176
+ * directory sweep.
177
+ */
178
+ function parseFrontmatter(content) {
179
+ const frontmatter = {};
180
+ const normalized = content.replace(/\r\n/g, "\n");
181
+ if (!normalized.startsWith(OPENING_FENCE)) return {
182
+ frontmatter,
183
+ body: normalized
184
+ };
185
+ const endIndex = normalized.indexOf(CLOSING_FENCE, FENCE_LENGTH);
186
+ if (endIndex === -1) return {
187
+ frontmatter,
188
+ body: normalized
189
+ };
190
+ const frontmatterBlock = normalized.slice(FRONTMATTER_START_INDEX, endIndex);
191
+ const body = normalized.slice(endIndex + FENCE_WITH_NEWLINE_LENGTH).trim();
192
+ let currentKey = null;
193
+ let currentBlockLines = null;
194
+ let currentIndent = null;
195
+ let currentFolded = false;
196
+ const flushBlock = () => {
197
+ if (currentKey === null || currentBlockLines === null) return;
198
+ const stripped = dedentBlock(currentBlockLines);
199
+ frontmatter[currentKey] = currentFolded ? foldBlock(stripped) : stripped;
200
+ currentKey = null;
201
+ currentBlockLines = null;
202
+ currentIndent = null;
203
+ currentFolded = false;
204
+ };
205
+ for (const line of frontmatterBlock.split("\n")) {
206
+ const indent = line.search(/\S|$/);
207
+ const trimmed = line.trim();
208
+ if (currentKey !== null && currentBlockLines !== null && (indent > (currentIndent ?? 0) || currentFolded && trimmed === "")) {
209
+ currentBlockLines.push(line);
210
+ continue;
211
+ }
212
+ flushBlock();
213
+ const match = line.match(/^([\w-]+):\s*(.*)$/);
214
+ if (!match) continue;
215
+ const key = match[1] ?? "";
216
+ const rawValue = (match[2] ?? "").trim();
217
+ const isQuoted = rawValue.startsWith("\"") && rawValue.endsWith("\"") || rawValue.startsWith("'") && rawValue.endsWith("'");
218
+ const value = isQuoted ? rawValue.slice(1, -1) : rawValue;
219
+ const isFolded = !isQuoted && FOLDED_BLOCK_INDICATORS.has(rawValue);
220
+ if (value === "" || isFolded) {
221
+ currentKey = key;
222
+ currentBlockLines = [];
223
+ currentIndent = indent;
224
+ currentFolded = isFolded;
225
+ } else frontmatter[key] = value;
226
+ }
227
+ flushBlock();
228
+ return {
229
+ frontmatter,
230
+ body
231
+ };
232
+ }
233
+ //#endregion
234
+ //#region src/services/configDir/index.ts
235
+ /**
236
+ * Locating the coding agent's config directory.
237
+ *
238
+ * The directory is normally `.pi`, but a rebranded build of the coding agent
239
+ * can rename it, so the real name is discovered from the running installation
240
+ * rather than assumed. Discovery order runs from most to least authoritative:
241
+ * the loaded module's own export, then an explicitly configured package root,
242
+ * then a walk upward from the process entry point, then the default.
243
+ *
244
+ * DESIGN PATTERNS:
245
+ * - Resolution is a pure function of its inputs; the process-wide convenience
246
+ * wrapper is a separate, memoized entry point
247
+ * - Every discovery step is best-effort. A detached runner must still start
248
+ * when package metadata is unreadable, so failures fall through to the default
249
+ *
250
+ * PERFORMANCE:
251
+ * `getConfigDirName` is memoized because the uncached path walks from the entry
252
+ * point to the filesystem root, reading and parsing a package.json per level.
253
+ * Callers join it into paths, so it was previously re-walked per path built.
254
+ *
255
+ * AVOID:
256
+ * - Hardcoding `.pi` at a call site
257
+ * - Calling the uncached resolver on a hot path
258
+ */
259
+ const DEFAULT_CONFIG_DIR_NAME = ".pi";
260
+ const PI_CODING_AGENT_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
261
+ /** Overrides the upward package walk when the installation root is known. */
262
+ const PI_CODING_AGENT_PACKAGE_ROOT_ENV = "PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT";
263
+ function validConfigDirName(value) {
264
+ return typeof value === "string" && value.trim() ? value : void 0;
265
+ }
266
+ /** Read the config dir name from a directory, if it is the coding agent's package root. */
267
+ function readConfigDirNameFromPackageRoot(packageRoot) {
268
+ if (!packageRoot) return void 0;
269
+ try {
270
+ const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8"));
271
+ if (typeof manifest !== "object" || manifest === null) return void 0;
272
+ const record = manifest;
273
+ if (record.name !== PI_CODING_AGENT_PACKAGE_NAME) return void 0;
274
+ return validConfigDirName((typeof record.piConfig === "object" && record.piConfig !== null ? record.piConfig : void 0)?.configDir);
275
+ } catch {
276
+ return;
277
+ }
278
+ }
279
+ function resolveConfigDirNameFromPackageJson(entryPoint = process.argv[1], packageRoot = process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV]) {
280
+ const configuredRootValue = readConfigDirNameFromPackageRoot(packageRoot);
281
+ if (configuredRootValue) return configuredRootValue;
282
+ if (!entryPoint) return void 0;
283
+ try {
284
+ let dir = path.dirname(fs.realpathSync(entryPoint));
285
+ while (dir !== path.dirname(dir)) {
286
+ const value = readConfigDirNameFromPackageRoot(dir);
287
+ if (value) return value;
288
+ dir = path.dirname(dir);
289
+ }
290
+ } catch {}
291
+ }
292
+ /**
293
+ * Resolve the config directory name from explicit inputs.
294
+ *
295
+ * Pure with respect to its arguments, which is what makes it testable; the
296
+ * process-wide answer comes from `getConfigDirName`.
297
+ */
298
+ function resolveConfigDirName(codingAgentModule, entryPoint, packageRoot) {
299
+ return (codingAgentModule && typeof codingAgentModule === "object" ? validConfigDirName(codingAgentModule.CONFIG_DIR_NAME) : void 0) ?? resolveConfigDirNameFromPackageJson(entryPoint, packageRoot) ?? DEFAULT_CONFIG_DIR_NAME;
300
+ }
301
+ let memoizedConfigDirName;
302
+ /** The config directory name for this process. Resolved once, then reused. */
303
+ function getConfigDirName(environment = process.env) {
304
+ if (environment !== process.env) return resolveConfigDirName(void 0, process.argv[1], environment["PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT"] ?? "");
305
+ memoizedConfigDirName ??= resolveConfigDirName();
306
+ return memoizedConfigDirName;
307
+ }
308
+ /** The project-local config directory, for example `<projectRoot>/.pi`. */
309
+ function getProjectConfigDir(projectRoot, environment = process.env) {
310
+ return path.join(projectRoot, getConfigDirName(environment));
311
+ }
312
+ /** Read a JSON settings file as an object, retaining strict path-specific errors. */
313
+ function readSettingsFileStrict(filePath) {
314
+ if (!fs.existsSync(filePath)) return {};
315
+ let raw;
316
+ try {
317
+ raw = fs.readFileSync(filePath, "utf-8");
318
+ } catch (error) {
319
+ const message = error instanceof Error ? error.message : String(error);
320
+ throw new Error(`Failed to read settings file '${filePath}': ${message}`, { cause: error });
321
+ }
322
+ let parsed;
323
+ try {
324
+ parsed = JSON.parse(raw);
325
+ } catch (error) {
326
+ const message = error instanceof Error ? error.message : String(error);
327
+ throw new Error(`Failed to parse settings file '${filePath}': ${message}`, { cause: error });
328
+ }
329
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Settings file '${filePath}' must contain a JSON object.`);
330
+ return parsed;
331
+ }
332
+ /** The user-level agent directory Po runs with, from Po's own resolver. */
333
+ function getAgentDir(environment = process.env) {
334
+ return piAgentDirectory(environment, environment.HOME ?? os.homedir());
335
+ }
336
+ //#endregion
337
+ //#region src/services/agentProjectRoot/index.ts
338
+ /**
339
+ * Locating the project root that agent configuration hangs off.
340
+ *
341
+ * A project root is any ancestor of the working directory that carries a config
342
+ * directory or a legacy `.agents` directory. There can be several: a package
343
+ * inside a monorepo and the monorepo itself both qualify. Which one wins is a
344
+ * setting, because both answers are legitimate. Teams that keep one shared set
345
+ * of agents at the repository root want `git-root`, teams that give each package
346
+ * its own want `nearest`, and neither can be inferred from the tree alone.
347
+ *
348
+ * DESIGN PATTERNS:
349
+ * - Candidates are collected once, nearest first, then a policy picks from them.
350
+ * Separating collection from policy is what lets `git-root` be answered
351
+ * without a second walk
352
+ * - Path derivation goes through `shared/configDir.ts`, so a rebranded build
353
+ * that renames its config directory needs no change here
354
+ *
355
+ * AVOID:
356
+ * - Hardcoding the config directory name; it is discovered, not assumed
357
+ * - Treating the nearest candidate as the project root without reading the
358
+ * setting first
359
+ */
360
+ const SETTINGS_FILE_NAME = "settings.json";
361
+ const GIT_DIR_NAME$1 = ".git";
362
+ /**
363
+ * Bare `.agents`. Two roles, one name: under a project root it is the legacy
364
+ * location that predates the config directory, under the home directory it is
365
+ * the current one. Both are read.
366
+ */
367
+ const DOT_AGENTS_DIR_NAME = ".agents";
368
+ /** Agents inside a config directory, project-level or user-level. */
369
+ const AGENTS_DIR_NAME = "agents";
370
+ const SUBAGENTS_SETTINGS_KEY$1 = "subagents";
371
+ const PROJECT_ROOT_RESOLUTION_KEY = "projectRootResolution";
372
+ /** Stable cache identity for equivalent absolute, relative, and symlinked cwd spellings. */
373
+ function canonicalizeDiscoveryCwd(cwd) {
374
+ const resolved = path.resolve(cwd);
375
+ try {
376
+ return fs.realpathSync.native(resolved);
377
+ } catch {
378
+ return resolved;
379
+ }
380
+ }
381
+ /** Whether a path exists and is a directory. Any failure reads as "no". */
382
+ function isDirectory$1(dirPath) {
383
+ try {
384
+ return fs.statSync(dirPath).isDirectory();
385
+ } catch {
386
+ return false;
387
+ }
388
+ }
389
+ /** True when `dir` carries either marker that makes it a project root. */
390
+ function isProjectRootCandidate(dir, environment = process.env) {
391
+ return isDirectory$1(getProjectConfigDir(dir, environment)) || isDirectory$1(path.join(dir, DOT_AGENTS_DIR_NAME));
392
+ }
393
+ /** Every project root above `cwd`, nearest first. */
394
+ function findProjectRootCandidates(cwd, environment = process.env) {
395
+ const roots = [];
396
+ let currentDir = cwd;
397
+ while (true) {
398
+ if (isProjectRootCandidate(currentDir, environment)) roots.push(currentDir);
399
+ const parentDir = path.dirname(currentDir);
400
+ if (parentDir === currentDir) return roots;
401
+ currentDir = parentDir;
402
+ }
403
+ }
404
+ /**
405
+ * Nearest ancestor containing `.git`.
406
+ *
407
+ * Existence is enough: a worktree or submodule has a `.git` file rather than a
408
+ * directory, and both are real repository roots.
409
+ */
410
+ function findNearestGitRoot(cwd) {
411
+ let currentDir = cwd;
412
+ while (true) {
413
+ if (fs.existsSync(path.join(currentDir, GIT_DIR_NAME$1))) return currentDir;
414
+ const parentDir = path.dirname(currentDir);
415
+ if (parentDir === currentDir) return null;
416
+ currentDir = parentDir;
417
+ }
418
+ }
419
+ /**
420
+ * Read a candidate's declared resolution policy.
421
+ *
422
+ * Deliberately throws on an unrecognised value rather than falling back: a typo
423
+ * here silently changes which agents the whole session sees.
424
+ */
425
+ function readProjectRootResolution(projectRoot, environment = process.env) {
426
+ const settingsPath = path.join(getProjectConfigDir(projectRoot, environment), SETTINGS_FILE_NAME);
427
+ if (!fs.existsSync(settingsPath)) return void 0;
428
+ const subagents = readSettingsFileStrict(settingsPath)[SUBAGENTS_SETTINGS_KEY$1];
429
+ if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return void 0;
430
+ const value = subagents[PROJECT_ROOT_RESOLUTION_KEY];
431
+ if (value === void 0) return void 0;
432
+ if (value === "nearest" || value === "git-root") return value;
433
+ throw new Error(`Subagent settings in '${settingsPath}' have invalid '${PROJECT_ROOT_RESOLUTION_KEY}'; expected 'nearest' or 'git-root'.`);
434
+ }
435
+ /**
436
+ * The project root to use, honouring the resolution policy.
437
+ *
438
+ * The policy is read from the nearest candidate first so a package can opt out
439
+ * of its monorepo's shared agents, and from the git root second so a repository
440
+ * can claim its packages without editing each one.
441
+ */
442
+ function findConfiguredProjectRoot(cwd, environment = process.env) {
443
+ const candidates = findProjectRootCandidates(cwd, environment);
444
+ const nearestRoot = candidates[0];
445
+ if (!nearestRoot) return null;
446
+ const nearestMode = readProjectRootResolution(nearestRoot, environment);
447
+ if (nearestMode === "nearest") return nearestRoot;
448
+ const gitRoot = findNearestGitRoot(cwd);
449
+ const gitProjectRoot = gitRoot ? candidates.find((candidate) => path.resolve(candidate) === path.resolve(gitRoot)) : void 0;
450
+ if (gitProjectRoot && (nearestMode === "git-root" || readProjectRootResolution(gitProjectRoot, environment) === "git-root")) return gitProjectRoot;
451
+ return nearestRoot;
452
+ }
453
+ /**
454
+ * User-level agent directories, in load order.
455
+ *
456
+ * Two locations, not one: agents used to live in the config directory and now
457
+ * also live in `~/.agents`, and both are still read so an existing setup keeps
458
+ * working after the move. Returned unfiltered because the caller reads each
459
+ * directory anyway and a missing one costs nothing.
460
+ */
461
+ function userAgentDirs(environment = process.env) {
462
+ return [path.join(getAgentDir(environment), AGENTS_DIR_NAME), path.join(environment.HOME ?? os.homedir(), DOT_AGENTS_DIR_NAME)];
463
+ }
464
+ /** The user-level settings file. Always resolvable, unlike the project one. */
465
+ function getUserAgentSettingsPath(environment = process.env) {
466
+ return path.join(getAgentDir(environment), SETTINGS_FILE_NAME);
467
+ }
468
+ /** The project settings file, or null when `cwd` sits under no project root. */
469
+ function getProjectAgentSettingsPath(cwd, environment = process.env) {
470
+ const projectRoot = findConfiguredProjectRoot(cwd, environment);
471
+ return projectRoot ? path.join(getProjectConfigDir(projectRoot, environment), SETTINGS_FILE_NAME) : null;
472
+ }
473
+ /**
474
+ * Project agent directories for `cwd`.
475
+ *
476
+ * `readDirs` holds only directories that exist, so discovery never stats twice,
477
+ * and lists the legacy location first so a project mid-migration still resolves.
478
+ * `preferredDir` is where a newly created agent should be written, and is
479
+ * returned whether or not it exists yet.
480
+ */
481
+ function resolveNearestProjectAgentDirs(cwd, environment = process.env) {
482
+ const projectRoot = findConfiguredProjectRoot(cwd, environment);
483
+ if (!projectRoot) return {
484
+ readDirs: [],
485
+ preferredDir: null
486
+ };
487
+ const legacyDir = path.join(projectRoot, DOT_AGENTS_DIR_NAME);
488
+ const preferredDir = path.join(getProjectConfigDir(projectRoot, environment), AGENTS_DIR_NAME);
489
+ const readDirs = [];
490
+ if (isDirectory$1(legacyDir)) readDirs.push(legacyDir);
491
+ if (isDirectory$1(preferredDir)) readDirs.push(preferredDir);
492
+ return {
493
+ readDirs,
494
+ preferredDir
495
+ };
496
+ }
497
+ //#endregion
498
+ //#region src/services/agentMemory/index.ts
499
+ /** Tools that let an agent append to its own memory file. */
500
+ const WRITE_TOOLS = /* @__PURE__ */ new Set([
501
+ "edit",
502
+ "write",
503
+ "bash"
504
+ ]);
505
+ function unquoteFrontmatterValue(value) {
506
+ const trimmed = value.trim();
507
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
508
+ return trimmed;
509
+ }
510
+ function parseMemoryScope(value) {
511
+ return value === "project" || value === "user" ? value : void 0;
512
+ }
513
+ function parseMemoryBoolean(value) {
514
+ if (value === "true") return true;
515
+ if (value === "false") return false;
516
+ }
517
+ function parsePositiveInteger(value) {
518
+ if (value === void 0) return void 0;
519
+ const parsed = Number(value);
520
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
521
+ }
522
+ /** Split a `memory` frontmatter block into key/value pairs, either spelling. */
523
+ function readMemoryEntries(raw) {
524
+ const entries = /* @__PURE__ */ new Map();
525
+ const inlineObject = raw.trim().match(/^\{(.*)\}$/s);
526
+ const parts = inlineObject?.[1] !== void 0 ? inlineObject[1].split(",") : raw.split("\n");
527
+ for (const part of parts) {
528
+ const match = part.trim().match(/^([\w-]+)\s*:\s*(.*)$/);
529
+ if (!match?.[1] || match[2] === void 0) continue;
530
+ entries.set(match[1], unquoteFrontmatterValue(match[2]));
531
+ }
532
+ return entries;
533
+ }
534
+ /**
535
+ * Parse a `memory` frontmatter block, or undefined when it declares nothing.
536
+ *
537
+ * Accepts both the inline `{ scope: project }` form and the indented block form,
538
+ * because agent files in the wild use each.
539
+ */
540
+ function parseMemoryFrontmatter(raw) {
541
+ if (!raw) return void 0;
542
+ const entries = readMemoryEntries(raw);
543
+ const enabled = parseMemoryBoolean(entries.get("enabled"));
544
+ const scope = parseMemoryScope(entries.get("scope"));
545
+ const maxEntries = parsePositiveInteger(entries.get("maxEntries"));
546
+ if (enabled === void 0 && scope === void 0 && maxEntries === void 0) return void 0;
547
+ const config = {};
548
+ if (enabled !== void 0) config.enabled = enabled;
549
+ if (scope !== void 0) config.scope = scope;
550
+ if (maxEntries !== void 0) config.maxEntries = maxEntries;
551
+ return config;
552
+ }
553
+ /** Whether an agent can write files this run. An unset `tools` inherits the builtins. */
554
+ function agentHasWriteTools(agent) {
555
+ const tools = agent.tools;
556
+ if (!tools) return true;
557
+ return tools.some((tool) => WRITE_TOOLS.has(tool));
558
+ }
559
+ //#endregion
560
+ //#region src/services/agentSettings/index.ts
561
+ const SUBAGENTS_SETTINGS_KEY = "subagents";
562
+ const AGENT_OVERRIDES_KEY = "agentOverrides";
563
+ /** Prefix marking a tool the child calls on an MCP server directly. */
564
+ const MCP_DIRECT_TOOL_PREFIX = "mcp:";
565
+ /**
566
+ * Returned whenever there is nothing to read.
567
+ *
568
+ * Shared rather than freshly built per call because it is returned on the hot
569
+ * discovery path for every session with no project settings file.
570
+ */
571
+ const EMPTY_SUBAGENT_SETTINGS = { overrides: {} };
572
+ /**
573
+ * Which fields of a config came from its file's frontmatter.
574
+ *
575
+ * Keyed by the config object rather than by agent name because configs are
576
+ * copied on write: each `apply*` pass produces new objects, and the new one
577
+ * inherits the entry so later passes still know what the author declared.
578
+ * A WeakMap means a discarded config's entry is collectable with it.
579
+ */
580
+ const agentFrontmatterFields = /* @__PURE__ */ new WeakMap();
581
+ /** Record which fields a freshly parsed config took from frontmatter. */
582
+ function setAgentFrontmatterFields(agent, fields) {
583
+ agentFrontmatterFields.set(agent, fields);
584
+ }
585
+ /** True when the agent's file declared any of `fields` in its frontmatter. */
586
+ function agentHasFrontmatterField(agent, ...fields) {
587
+ const frontmatterFields = agentFrontmatterFields.get(agent);
588
+ return frontmatterFields ? fields.some((field) => frontmatterFields.has(field)) : false;
589
+ }
590
+ /** Carry the frontmatter record onto a copy produced by an `apply*` pass. */
591
+ function inheritFrontmatterFields(source, next) {
592
+ const frontmatterFields = agentFrontmatterFields.get(source);
593
+ if (frontmatterFields) agentFrontmatterFields.set(next, frontmatterFields);
594
+ return next;
595
+ }
596
+ /** Split a flat tool list into host tools and direct MCP tools. */
597
+ function splitToolList$1(rawTools) {
598
+ const mcpDirectTools = [];
599
+ const tools = [];
600
+ for (const tool of rawTools ?? []) if (tool.startsWith(MCP_DIRECT_TOOL_PREFIX)) mcpDirectTools.push(tool.slice(4));
601
+ else tools.push(tool);
602
+ return {
603
+ ...rawTools !== void 0 ? { tools } : {},
604
+ ...mcpDirectTools.length > 0 ? { mcpDirectTools } : {}
605
+ };
606
+ }
607
+ /** Read a settings object at `key`, or undefined when it is absent or not an object. */
608
+ function readObjectField(source, key) {
609
+ const value = source[key];
610
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
611
+ return value;
612
+ }
613
+ /**
614
+ * Parse the model-scope block.
615
+ *
616
+ * Lives here rather than beside the scope enforcement logic because parsing is
617
+ * a settings concern and the agents domain may not reach into the runs domain.
618
+ */
619
+ function parseModelScopeConfig(value, filePath) {
620
+ if (value === void 0) return void 0;
621
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Subagent settings in '${filePath}' have invalid 'modelScope'; expected an object.`);
622
+ const input = value;
623
+ const config = {};
624
+ if ("enforce" in input) {
625
+ if (typeof input.enforce !== "boolean") throw new Error(`Subagent settings in '${filePath}' have invalid 'modelScope.enforce'; expected a boolean.`);
626
+ config.enforce = input.enforce;
627
+ }
628
+ if ("allow" in input) {
629
+ if (!Array.isArray(input.allow)) throw new Error(`Subagent settings in '${filePath}' have invalid 'modelScope.allow'; expected an array of strings.`);
630
+ const allow = [];
631
+ for (const entry of input.allow) {
632
+ if (typeof entry !== "string") throw new Error(`Subagent settings in '${filePath}' have invalid 'modelScope.allow'; expected an array of strings.`);
633
+ const trimmed = entry.trim();
634
+ if (trimmed) allow.push(trimmed);
635
+ }
636
+ if (allow.length === 0) throw new Error(`Subagent settings in '${filePath}' have invalid 'modelScope.allow'; expected a non-empty array of patterns.`);
637
+ config.allow = allow;
638
+ }
639
+ if (config.enforce === true && (!config.allow || config.allow.length === 0)) throw new Error(`Subagent settings in '${filePath}' set modelScope.enforce without a non-empty 'allow' list; supply allowed model patterns or disable enforcement.`);
640
+ return Object.keys(config).length > 0 ? config : void 0;
641
+ }
642
+ /**
643
+ * Parse a string-array override field, which may also be `false` to clear it.
644
+ *
645
+ * Empty entries are dropped rather than rejected: trailing commas in a hand
646
+ * edited list are common and unambiguous.
647
+ */
648
+ function parseOverrideStringArrayOrFalse(value, meta) {
649
+ if (value === void 0) return void 0;
650
+ if (value === false) return false;
651
+ if (!Array.isArray(value)) throw new Error(`Builtin override '${meta.name}' in '${meta.filePath}' has invalid '${meta.field}'; expected an array of strings or false.`);
652
+ const items = [];
653
+ for (const item of value) {
654
+ if (typeof item !== "string") throw new Error(`Builtin override '${meta.name}' in '${meta.filePath}' has invalid '${meta.field}'; expected an array of strings or false.`);
655
+ const trimmed = item.trim();
656
+ if (trimmed) items.push(trimmed);
657
+ }
658
+ return items;
659
+ }
660
+ /**
661
+ * Parse one entry of `subagents.agentOverrides`.
662
+ *
663
+ * Returns undefined for an entry that declared nothing, so an empty object in
664
+ * the file does not register as an override and mark the agent as overridden.
665
+ */
666
+ function parseBuiltinOverrideEntry(name, value, filePath) {
667
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Builtin override '${name}' in '${filePath}' must be an object.`);
668
+ const input = value;
669
+ const override = {};
670
+ if ("model" in input) {
671
+ if (typeof input.model === "string" || input.model === false) override.model = input.model;
672
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'model'; expected a string or false.`);
673
+ }
674
+ if ("thinking" in input) {
675
+ if (typeof input.thinking === "string" || input.thinking === false) override.thinking = input.thinking;
676
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'thinking'; expected a string or false.`);
677
+ }
678
+ if ("systemPromptMode" in input) {
679
+ if (input.systemPromptMode === "append" || input.systemPromptMode === "replace") override.systemPromptMode = input.systemPromptMode;
680
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPromptMode'; expected 'append' or 'replace'.`);
681
+ }
682
+ if ("inheritProjectContext" in input) {
683
+ if (typeof input.inheritProjectContext === "boolean") override.inheritProjectContext = input.inheritProjectContext;
684
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'inheritProjectContext'; expected a boolean.`);
685
+ }
686
+ if ("inheritSkills" in input) {
687
+ if (typeof input.inheritSkills === "boolean") override.inheritSkills = input.inheritSkills;
688
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'inheritSkills'; expected a boolean.`);
689
+ }
690
+ if ("defaultContext" in input) {
691
+ if (input.defaultContext === "fresh" || input.defaultContext === "fork" || input.defaultContext === false) override.defaultContext = input.defaultContext;
692
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'defaultContext'; expected 'fresh', 'fork', or false.`);
693
+ }
694
+ if ("disabled" in input) {
695
+ if (typeof input.disabled === "boolean") override.disabled = input.disabled;
696
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'disabled'; expected a boolean.`);
697
+ }
698
+ if ("completionGuard" in input) {
699
+ if (typeof input.completionGuard === "boolean") override.completionGuard = input.completionGuard;
700
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'completionGuard'; expected a boolean.`);
701
+ }
702
+ if ("toolBudget" in input) {
703
+ const toolBudget = parseToolBudgetOverride(input.toolBudget, name, filePath);
704
+ if (toolBudget !== void 0) override.toolBudget = toolBudget;
705
+ }
706
+ if ("systemPrompt" in input) {
707
+ if (typeof input.systemPrompt === "string") override.systemPrompt = input.systemPrompt;
708
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPrompt'; expected a string.`);
709
+ }
710
+ const parseArrayField = (field) => parseOverrideStringArrayOrFalse(input[field], {
711
+ filePath,
712
+ name,
713
+ field
714
+ });
715
+ const fallbackModels = parseArrayField("fallbackModels");
716
+ if (fallbackModels !== void 0) override.fallbackModels = fallbackModels;
717
+ const skills = parseArrayField("skills");
718
+ if (skills !== void 0) override.skills = skills;
719
+ const tools = parseArrayField("tools");
720
+ if (tools !== void 0) override.tools = tools;
721
+ const extensions = parseArrayField("extensions");
722
+ if (extensions !== void 0) override.extensions = extensions;
723
+ const subagentOnlyExtensions = parseArrayField("subagentOnlyExtensions");
724
+ if (subagentOnlyExtensions !== void 0) override.subagentOnlyExtensions = subagentOnlyExtensions;
725
+ return Object.keys(override).length > 0 ? override : void 0;
726
+ }
727
+ /**
728
+ * Parse a `toolBudget` override.
729
+ *
730
+ * `hard` is required by the contract, so an object without a usable one is
731
+ * rejected here instead of producing a budget that never blocks.
732
+ */
733
+ function parseToolBudgetOverride(value, name, filePath) {
734
+ if (value === false) return false;
735
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'toolBudget'; expected an object or false.`);
736
+ const input = value;
737
+ if (typeof input.hard !== "number") throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'toolBudget.hard'; expected a number.`);
738
+ const budget = { hard: input.hard };
739
+ if ("soft" in input) {
740
+ if (typeof input.soft !== "number") throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'toolBudget.soft'; expected a number.`);
741
+ budget.soft = input.soft;
742
+ }
743
+ if ("block" in input) {
744
+ if (input.block === "*") budget.block = "*";
745
+ else if (Array.isArray(input.block) && input.block.every((entry) => typeof entry === "string")) budget.block = [...input.block];
746
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'toolBudget.block'; expected an array of strings or '*'.`);
747
+ }
748
+ return budget;
749
+ }
750
+ /** Read and validate one settings file's `subagents` block. */
751
+ function readSubagentSettings(filePath) {
752
+ if (!filePath) return EMPTY_SUBAGENT_SETTINGS;
753
+ const subagentsObject = readObjectField(readSettingsFileStrict(filePath), SUBAGENTS_SETTINGS_KEY);
754
+ if (!subagentsObject) return EMPTY_SUBAGENT_SETTINGS;
755
+ let disableBuiltins;
756
+ if ("disableBuiltins" in subagentsObject) {
757
+ if (typeof subagentsObject.disableBuiltins === "boolean") disableBuiltins = subagentsObject.disableBuiltins;
758
+ else throw new Error(`Subagent settings in '${filePath}' have invalid 'disableBuiltins'; expected a boolean.`);
759
+ }
760
+ let disableThinking;
761
+ if ("disableThinking" in subagentsObject) {
762
+ if (typeof subagentsObject.disableThinking === "boolean") disableThinking = subagentsObject.disableThinking;
763
+ else throw new Error(`Subagent settings in '${filePath}' have invalid 'disableThinking'; expected a boolean.`);
764
+ }
765
+ let defaultModel;
766
+ if ("defaultModel" in subagentsObject) {
767
+ if (typeof subagentsObject.defaultModel === "string" && subagentsObject.defaultModel.trim()) defaultModel = subagentsObject.defaultModel.trim();
768
+ else throw new Error(`Subagent settings in '${filePath}' have invalid 'defaultModel'; expected a non-empty string.`);
769
+ }
770
+ let defaultThinking;
771
+ if ("defaultThinking" in subagentsObject) {
772
+ if (typeof subagentsObject.defaultThinking === "string" && subagentsObject.defaultThinking.trim()) defaultThinking = subagentsObject.defaultThinking.trim();
773
+ else throw new Error(`Subagent settings in '${filePath}' have invalid 'defaultThinking'; expected a non-empty string.`);
774
+ }
775
+ let defaultExtensions;
776
+ if ("defaultExtensions" in subagentsObject) {
777
+ const raw = subagentsObject.defaultExtensions;
778
+ if (!Array.isArray(raw) || raw.some((item) => typeof item !== "string" || !item.trim())) throw new Error(`Subagent settings in '${filePath}' have invalid 'defaultExtensions'; expected an array of non-empty strings.`);
779
+ defaultExtensions = raw.map((item) => String(item).trim());
780
+ }
781
+ const modelScope = parseModelScopeConfig(subagentsObject.modelScope, filePath);
782
+ const overrides = {};
783
+ const agentOverrides = readObjectField(subagentsObject, AGENT_OVERRIDES_KEY);
784
+ if (agentOverrides) for (const [name, value] of Object.entries(agentOverrides)) {
785
+ const override = parseBuiltinOverrideEntry(name, value, filePath);
786
+ if (override) overrides[name] = override;
787
+ }
788
+ return {
789
+ overrides,
790
+ defaultModel,
791
+ defaultThinking,
792
+ defaultExtensions,
793
+ disableBuiltins,
794
+ disableThinking,
795
+ modelScope
796
+ };
797
+ }
798
+ /** Which settings file supplies `defaultModel`, project winning over user. */
799
+ function resolveSubagentDefaultModel(userSettings, projectSettings, userSettingsPath, projectSettingsPath) {
800
+ if (projectSettingsPath && projectSettings.defaultModel !== void 0) return {
801
+ type: "subagents.defaultModel",
802
+ scope: "project",
803
+ path: projectSettingsPath,
804
+ model: projectSettings.defaultModel
805
+ };
806
+ return userSettings.defaultModel !== void 0 ? {
807
+ type: "subagents.defaultModel",
808
+ scope: "user",
809
+ path: userSettingsPath,
810
+ model: userSettings.defaultModel
811
+ } : void 0;
812
+ }
813
+ /**
814
+ * Fill in the default model for agents that declared none.
815
+ *
816
+ * `modelSource` is recorded alongside so callers can tell an inherited model
817
+ * from a requested one, which decides how hard model-scope enforcement bites.
818
+ */
819
+ function applySubagentDefaultModel(agents, defaultModel) {
820
+ if (!defaultModel) return agents;
821
+ return agents.map((agent) => {
822
+ if (agent.model !== void 0) return agent;
823
+ return inheritFrontmatterFields(agent, {
824
+ ...agent,
825
+ model: defaultModel.model,
826
+ modelSource: defaultModel
827
+ });
828
+ });
829
+ }
830
+ /** Fill in fallback models only when the agent declared no fallback policy. */
831
+ function applySubagentDefaultFallbackModels(agents, fallbackModels) {
832
+ if (fallbackModels === void 0) return agents;
833
+ return agents.map((agent) => {
834
+ if (agent.fallbackModels !== void 0) return agent;
835
+ return inheritFrontmatterFields(agent, {
836
+ ...agent,
837
+ fallbackModels: [...fallbackModels]
838
+ });
839
+ });
840
+ }
841
+ function resolveSubagentDefaultThinking(userSettings, projectSettings, projectSettingsPath) {
842
+ if (projectSettingsPath && projectSettings.defaultThinking !== void 0) return projectSettings.defaultThinking;
843
+ return userSettings.defaultThinking;
844
+ }
845
+ function applySubagentDefaultThinking(agents, defaultThinking) {
846
+ if (defaultThinking === void 0) return agents;
847
+ return agents.map((agent) => {
848
+ if (agent.thinking !== void 0) return agent;
849
+ return inheritFrontmatterFields(agent, {
850
+ ...agent,
851
+ thinking: defaultThinking
852
+ });
853
+ });
854
+ }
855
+ function resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath) {
856
+ if (projectSettingsPath && projectSettings.defaultExtensions !== void 0) return projectSettings.defaultExtensions;
857
+ return userSettings.defaultExtensions;
858
+ }
859
+ /**
860
+ * Fill in default extensions for agents that declared none.
861
+ *
862
+ * The `extensionsFromDefault` flag matters when the agent is later overridden:
863
+ * a snapshot must not record a settings default as if the builtin had declared
864
+ * it, or removing the override would bake the default in permanently.
865
+ */
866
+ function applySubagentDefaultExtensions(agents, defaultExtensions) {
867
+ if (defaultExtensions === void 0) return agents;
868
+ return agents.map((agent) => {
869
+ if (agent.extensions !== void 0) return agent;
870
+ return inheritFrontmatterFields(agent, {
871
+ ...agent,
872
+ extensions: [...defaultExtensions],
873
+ extensionsFromDefault: true
874
+ });
875
+ });
876
+ }
877
+ function applySubagentDefaults(agents, defaultModel, defaultThinking, defaultExtensions, defaultFallbackModels) {
878
+ return applySubagentDefaultExtensions(applySubagentDefaultThinking(applySubagentDefaultFallbackModels(applySubagentDefaultModel(agents, defaultModel), defaultFallbackModels), defaultThinking), defaultExtensions);
879
+ }
880
+ /**
881
+ * Snapshot the fields an override may replace, so it can be undone.
882
+ *
883
+ * `extensions` is omitted when it came from a settings default rather than the
884
+ * agent itself; restoring it would turn a default into a permanent value.
885
+ */
886
+ function cloneOverrideBase(agent) {
887
+ return {
888
+ model: agent.model,
889
+ fallbackModels: agent.fallbackModels ? [...agent.fallbackModels] : void 0,
890
+ thinking: agent.thinking,
891
+ systemPromptMode: agent.systemPromptMode,
892
+ inheritProjectContext: agent.inheritProjectContext,
893
+ inheritSkills: agent.inheritSkills,
894
+ defaultContext: agent.defaultContext,
895
+ disabled: agent.disabled,
896
+ systemPrompt: agent.systemPrompt,
897
+ skills: agent.skills ? [...agent.skills] : void 0,
898
+ skillPath: agent.skillPath ? [...agent.skillPath] : void 0,
899
+ tools: agent.tools ? [...agent.tools] : void 0,
900
+ mcpDirectTools: agent.mcpDirectTools ? [...agent.mcpDirectTools] : void 0,
901
+ extensions: agent.extensionsFromDefault ? void 0 : agent.extensions ? [...agent.extensions] : void 0,
902
+ subagentOnlyExtensions: agent.subagentOnlyExtensions ? [...agent.subagentOnlyExtensions] : void 0,
903
+ completionGuard: agent.completionGuard,
904
+ toolBudget: agent.toolBudget
905
+ };
906
+ }
907
+ /**
908
+ * Apply an override to a custom agent, filling only unset fields.
909
+ *
910
+ * A custom agent has a file the user could have edited, so a settings value
911
+ * must never contradict its frontmatter. Nothing is copied and no `override`
912
+ * is recorded unless at least one field was actually filled, which keeps agents
913
+ * that declared everything from being reported as overridden.
914
+ */
915
+ function applyCustomAgentOverride(agent, override, meta) {
916
+ let next;
917
+ let anyFilled = false;
918
+ const mutable = () => {
919
+ next ??= { ...agent };
920
+ return next;
921
+ };
922
+ const fill = (field, frontmatterFields, value) => {
923
+ if (agentHasFrontmatterField(agent, ...frontmatterFields)) return;
924
+ mutable()[field] = value;
925
+ anyFilled = true;
926
+ };
927
+ if (override.model !== void 0 && !agentHasFrontmatterField(agent, "model")) {
928
+ const target = mutable();
929
+ target.model = override.model === false ? void 0 : override.model;
930
+ target.modelSource = void 0;
931
+ anyFilled = true;
932
+ }
933
+ if (override.fallbackModels !== void 0) fill("fallbackModels", ["fallbackModels"], override.fallbackModels === false ? void 0 : [...override.fallbackModels]);
934
+ if (override.thinking !== void 0) fill("thinking", ["thinking"], override.thinking === false ? void 0 : override.thinking);
935
+ if (override.systemPromptMode !== void 0) fill("systemPromptMode", ["systemPromptMode"], override.systemPromptMode);
936
+ if (override.inheritProjectContext !== void 0) fill("inheritProjectContext", ["inheritProjectContext"], override.inheritProjectContext);
937
+ if (override.inheritSkills !== void 0) fill("inheritSkills", ["inheritSkills"], override.inheritSkills);
938
+ if (override.defaultContext !== void 0) fill("defaultContext", ["defaultContext"], override.defaultContext === false ? void 0 : override.defaultContext);
939
+ if (override.disabled !== void 0 && agent.disabled === void 0) {
940
+ mutable().disabled = override.disabled;
941
+ anyFilled = true;
942
+ }
943
+ if (override.skills !== void 0) fill("skills", ["skill", "skills"], override.skills === false ? void 0 : [...override.skills]);
944
+ if (override.tools !== void 0 && !agentHasFrontmatterField(agent, "tools")) {
945
+ const { tools, mcpDirectTools } = splitToolList$1(override.tools === false ? [] : override.tools);
946
+ const target = mutable();
947
+ target.tools = tools;
948
+ target.mcpDirectTools = mcpDirectTools;
949
+ anyFilled = true;
950
+ }
951
+ if (override.extensions !== void 0) fill("extensions", ["extensions"], override.extensions === false ? void 0 : [...override.extensions]);
952
+ if (override.subagentOnlyExtensions !== void 0) fill("subagentOnlyExtensions", ["subagentOnlyExtensions"], override.subagentOnlyExtensions === false ? void 0 : [...override.subagentOnlyExtensions]);
953
+ if (override.completionGuard !== void 0) fill("completionGuard", ["completionGuard"], override.completionGuard);
954
+ if (override.toolBudget !== void 0) fill("toolBudget", ["toolBudget"], override.toolBudget === false ? void 0 : override.toolBudget);
955
+ if (!anyFilled || !next) return agent;
956
+ next.override = {
957
+ ...meta,
958
+ base: cloneOverrideBase(agent)
959
+ };
960
+ return inheritFrontmatterFields(agent, next);
961
+ }
962
+ function applyCustomAgentOverrides(agents, userSettings, projectSettings, userSettingsPath, projectSettingsPath) {
963
+ return agents.map((agent) => {
964
+ const projectOverride = projectSettings.overrides[agent.name];
965
+ if (projectOverride && projectSettingsPath) return applyCustomAgentOverride(agent, projectOverride, {
966
+ scope: "project",
967
+ path: projectSettingsPath
968
+ });
969
+ const userOverride = userSettings.overrides[agent.name];
970
+ if (userOverride) return applyCustomAgentOverride(agent, userOverride, {
971
+ scope: "user",
972
+ path: userSettingsPath
973
+ });
974
+ return agent;
975
+ });
976
+ }
977
+ //#endregion
978
+ //#region src/services/agentLoader/index.ts
979
+ /**
980
+ * Reading agent definitions off disk into `AgentConfig` values.
981
+ *
982
+ * One markdown file is one agent: YAML frontmatter declares how the child run
983
+ * should be shaped, and the body is its system prompt. This module owns the walk
984
+ * that finds those files and the field-by-field interpretation of what it finds.
985
+ * Precedence between sources, settings defaults, and builtin overrides are
986
+ * applied by discovery afterwards, never here.
987
+ *
988
+ * DESIGN PATTERNS:
989
+ * - A malformed value fails loudly, a malformed file is skipped quietly. A file
990
+ * missing `name` or `description` is simply not an agent and must not stop the
991
+ * sweep, but a `timeoutMs` of "soon" is an authoring mistake that has to
992
+ * surface rather than be silently dropped
993
+ * - Frontmatter keys this module does not interpret are copied into
994
+ * `extraFields` verbatim, so a later writer can round-trip a file it did not
995
+ * fully understand
996
+ * - The directory walk prunes at project boundaries, so an agent directory that
997
+ * happens to contain a checked-out sub-repository does not absorb its agents
998
+ *
999
+ * ASYNC-ONLY:
1000
+ * This package has no synchronous run mode, so the `async` frontmatter field is
1001
+ * not interpreted. It falls through into `extraFields` like any other key,
1002
+ * which keeps a definition written for the predecessor readable here.
1003
+ *
1004
+ * AVOID:
1005
+ * - Applying settings defaults or builtin overrides in this module
1006
+ * - Adding a field here without adding it to `LOADER_KNOWN_FIELDS`, which would
1007
+ * leak it into `extraFields` and get it written back twice
1008
+ */
1009
+ /**
1010
+ * The one builtin that extends the host's context instead of replacing it.
1011
+ *
1012
+ * `delegate` exists to carry out work on the operator's behalf in the same
1013
+ * project, so it inherits the host system prompt and project context. Every
1014
+ * other agent starts clean, which is what makes its output reproducible.
1015
+ */
1016
+ const CONTEXT_INHERITING_BUILTIN = "delegate";
1017
+ function defaultSystemPromptMode(name) {
1018
+ return name === CONTEXT_INHERITING_BUILTIN ? "append" : "replace";
1019
+ }
1020
+ function defaultInheritProjectContext(name) {
1021
+ return name === CONTEXT_INHERITING_BUILTIN;
1022
+ }
1023
+ function defaultInheritSkills() {
1024
+ return false;
1025
+ }
1026
+ /**
1027
+ * Every frontmatter key this module interprets.
1028
+ *
1029
+ * Anything absent from this set is preserved in `extraFields` instead. `async`
1030
+ * is deliberately absent: this package is async-only, so the key is data to
1031
+ * carry, not a setting to honour.
1032
+ */
1033
+ const LOADER_KNOWN_FIELDS = /* @__PURE__ */ new Set([
1034
+ "name",
1035
+ "package",
1036
+ "description",
1037
+ "tools",
1038
+ "model",
1039
+ "fallbackModels",
1040
+ "thinking",
1041
+ "systemPromptMode",
1042
+ "inheritProjectContext",
1043
+ "inheritSkills",
1044
+ "defaultContext",
1045
+ "timeoutMs",
1046
+ "skill",
1047
+ "skills",
1048
+ "skillPath",
1049
+ "extensions",
1050
+ "subagentOnlyExtensions",
1051
+ "output",
1052
+ "defaultReads",
1053
+ "defaultProgress",
1054
+ "interactive",
1055
+ "maxSubagentDepth",
1056
+ "completionGuard",
1057
+ "toolBudget",
1058
+ "memory"
1059
+ ]);
1060
+ const TRUE_LITERAL = "true";
1061
+ const FALSE_LITERAL = "false";
1062
+ /** Frontmatter carries strings, so a boolean field is a tri-state until parsed. */
1063
+ function parseOptionalBoolean(raw) {
1064
+ if (raw === TRUE_LITERAL) return true;
1065
+ if (raw === FALSE_LITERAL) return false;
1066
+ }
1067
+ /** A package-qualified agent name: dot-separated lowercase segments. */
1068
+ const IDENTIFIER_PATTERN = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)*$/;
1069
+ const PACKAGE_NAME_SEPARATOR = ".";
1070
+ /**
1071
+ * Reduce an authored package name to the canonical form used in runtime names.
1072
+ *
1073
+ * Sanitizing rather than rejecting outright keeps a human-written `My Tools`
1074
+ * usable, while the pattern check afterwards still rejects anything that
1075
+ * sanitizes down to nothing meaningful.
1076
+ */
1077
+ function normalizePackageName(value) {
1078
+ const trimmed = value?.trim();
1079
+ if (!trimmed) return void 0;
1080
+ return trimmed.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9.-]/g, "").replace(/-+/g, "-").replace(/\.+/g, ".").replace(/(?:^[-.]+|[-.]+$)/g, "");
1081
+ }
1082
+ /** Validate a `package` frontmatter value. An absent or empty value is not an error. */
1083
+ function parsePackageName(value, label = "package") {
1084
+ if (value === void 0 || value === false || value === "") return { packageName: void 0 };
1085
+ if (typeof value !== "string") return { error: `${label} must be a string or false when provided.` };
1086
+ const packageName = normalizePackageName(value);
1087
+ if (!packageName || !IDENTIFIER_PATTERN.test(packageName)) return { error: `${label} is invalid after sanitization.` };
1088
+ return { packageName };
1089
+ }
1090
+ /** The name an agent is addressed by, qualified when it belongs to a package. */
1091
+ function buildRuntimeName(localName, packageName) {
1092
+ const trimmedPackage = packageName?.trim();
1093
+ return trimmedPackage ? `${trimmedPackage}${PACKAGE_NAME_SEPARATOR}${localName}` : localName;
1094
+ }
1095
+ /** Marks a tool that is reached through an MCP server rather than the host. */
1096
+ const MCP_TOOL_PREFIX = "mcp:";
1097
+ /**
1098
+ * Split an authored `tools` list into host tools and MCP tools.
1099
+ *
1100
+ * The two are separate fields on `AgentConfig` because they are enforced by
1101
+ * different layers, but a single flat list is what an author wants to write.
1102
+ */
1103
+ function splitToolList(rawTools) {
1104
+ const mcpDirectTools = [];
1105
+ const tools = [];
1106
+ for (const tool of rawTools ?? []) if (tool.startsWith(MCP_TOOL_PREFIX)) mcpDirectTools.push(tool.slice(4));
1107
+ else tools.push(tool);
1108
+ return {
1109
+ ...rawTools !== void 0 ? { tools } : {},
1110
+ ...mcpDirectTools.length > 0 ? { mcpDirectTools } : {}
1111
+ };
1112
+ }
1113
+ const DISCOVERY_PRUNED_DIR_NAMES = /* @__PURE__ */ new Set([".git", "node_modules"]);
1114
+ const GIT_DIR_NAME = ".git";
1115
+ const LEGACY_AGENTS_DIR_NAME = ".agents";
1116
+ const LEGACY_SKILLS_DIR_NAME = "skills";
1117
+ const AGENT_FILE_EXTENSION = ".md";
1118
+ const CHAIN_FILE_EXTENSION = ".chain.md";
1119
+ function isDirectory(target) {
1120
+ try {
1121
+ return fs.statSync(target).isDirectory();
1122
+ } catch {
1123
+ return false;
1124
+ }
1125
+ }
1126
+ /** A directory that owns its own agent configuration is a separate project. */
1127
+ function isDiscoveryNestedProjectRoot(dir, environment) {
1128
+ return isDirectory(getProjectConfigDir(dir, environment)) || isDirectory(path.join(dir, LEGACY_AGENTS_DIR_NAME));
1129
+ }
1130
+ /**
1131
+ * Decide whether the walk should skip a subdirectory.
1132
+ *
1133
+ * A nested repository or nested project root is pruned because its agents
1134
+ * belong to that project, not to the one being scanned. The root itself is
1135
+ * exempt: scanning a project's own agent directory must not prune it.
1136
+ */
1137
+ function shouldPruneDiscoveryDir(rootDir, dir, dirName, environment) {
1138
+ if (DISCOVERY_PRUNED_DIR_NAMES.has(dirName)) return true;
1139
+ if (fs.existsSync(path.join(dir, GIT_DIR_NAME))) return true;
1140
+ return path.resolve(dir) !== path.resolve(rootDir) && isDiscoveryNestedProjectRoot(dir, environment);
1141
+ }
1142
+ /**
1143
+ * Collect matching files under `dir`, sorted so discovery order is stable.
1144
+ *
1145
+ * Symlinked entries are followed as files, because staging an agent by symlink
1146
+ * is a supported way to share one definition across projects.
1147
+ */
1148
+ function listFilesRecursive(dir, predicate, rootDir = dir, environment = process.env) {
1149
+ const files = [];
1150
+ if (!fs.existsSync(dir)) return files;
1151
+ let entries;
1152
+ try {
1153
+ entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
1154
+ } catch {
1155
+ return files;
1156
+ }
1157
+ for (const entry of entries) {
1158
+ const filePath = path.join(dir, entry.name);
1159
+ if (entry.isDirectory()) {
1160
+ if (!shouldPruneDiscoveryDir(rootDir, filePath, entry.name, environment)) files.push(...listFilesRecursive(filePath, predicate, rootDir, environment));
1161
+ continue;
1162
+ }
1163
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
1164
+ if (!predicate(entry.name)) continue;
1165
+ files.push(filePath);
1166
+ }
1167
+ return files;
1168
+ }
1169
+ /**
1170
+ * Recognise a file that lives under a legacy `.agents/skills/` directory.
1171
+ *
1172
+ * Those markdown files are skill documents, not agent definitions, and they
1173
+ * predate the split. They would otherwise be loaded as nameless agents.
1174
+ */
1175
+ function isLegacyAgentSkillPath(rootDir, filePath) {
1176
+ const parts = path.relative(rootDir, filePath).split(path.sep).map((part) => part.toLowerCase());
1177
+ if (path.basename(rootDir).toLowerCase() === LEGACY_AGENTS_DIR_NAME) parts.unshift(LEGACY_AGENTS_DIR_NAME);
1178
+ return parts.some((part, index) => part === LEGACY_AGENTS_DIR_NAME && parts[index + 1] === LEGACY_SKILLS_DIR_NAME);
1179
+ }
1180
+ function parseSystemPromptMode(raw, localName) {
1181
+ if (raw === "replace" || raw === "append") return raw;
1182
+ return defaultSystemPromptMode(localName);
1183
+ }
1184
+ function parseDefaultContext(raw) {
1185
+ return raw === "fork" || raw === "fresh" ? raw : void 0;
1186
+ }
1187
+ function parseTimeoutMs(raw, localName) {
1188
+ if (raw === void 0) return void 0;
1189
+ const parsed = Number(raw);
1190
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`Agent '${localName}' has invalid timeoutMs frontmatter; expected a positive integer.`);
1191
+ return parsed;
1192
+ }
1193
+ function parseToolBudget(raw, localName) {
1194
+ if (raw === void 0 || !raw.trim()) return void 0;
1195
+ const parsed = JSON.parse(raw);
1196
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`Agent '${localName}' has invalid toolBudget frontmatter; expected a JSON object.`);
1197
+ return parsed;
1198
+ }
1199
+ /** True for an agent definition file, false for a chain or anything else. */
1200
+ function isAgentFileName(fileName) {
1201
+ return fileName.endsWith(AGENT_FILE_EXTENSION) && !fileName.endsWith(CHAIN_FILE_EXTENSION);
1202
+ }
1203
+ /**
1204
+ * Load every agent definition under `dir`.
1205
+ *
1206
+ * The returned configs carry only what the file itself declared. Settings
1207
+ * defaults, builtin overrides, and cross-source precedence are the caller's job.
1208
+ */
1209
+ function loadAgentsFromDir(dir, source, environment = process.env) {
1210
+ const agents = [];
1211
+ for (const filePath of listFilesRecursive(dir, isAgentFileName, dir, environment)) {
1212
+ if (isLegacyAgentSkillPath(dir, filePath)) continue;
1213
+ let content;
1214
+ try {
1215
+ content = fs.readFileSync(filePath, "utf-8");
1216
+ } catch {
1217
+ continue;
1218
+ }
1219
+ const { frontmatter, body } = parseFrontmatter(content);
1220
+ if (!frontmatter.name || !frontmatter.description) continue;
1221
+ const localName = frontmatter.name;
1222
+ const parsedPackage = parsePackageName(frontmatter.package, `Agent '${localName}' package`);
1223
+ if (parsedPackage.error) continue;
1224
+ const rawTools = parseFrontmatterList(frontmatter.tools);
1225
+ const { tools, mcpDirectTools } = splitToolList(rawTools);
1226
+ const skills = parseFrontmatterList(frontmatter.skill || frontmatter.skills);
1227
+ const skillPath = parseFrontmatterList(frontmatter.skillPath);
1228
+ const fallbackModels = parseFrontmatterList(frontmatter.fallbackModels);
1229
+ const defaultReads = parseFrontmatterList(frontmatter.defaultReads);
1230
+ const maxSubagentDepth = Number(frontmatter.maxSubagentDepth);
1231
+ const extraFields = {};
1232
+ for (const [key, value] of Object.entries(frontmatter)) if (!LOADER_KNOWN_FIELDS.has(key)) extraFields[key] = value;
1233
+ const agent = {
1234
+ name: buildRuntimeName(localName, parsedPackage.packageName),
1235
+ localName,
1236
+ packageName: parsedPackage.packageName,
1237
+ description: frontmatter.description,
1238
+ tools: rawTools !== void 0 ? tools ?? [] : void 0,
1239
+ mcpDirectTools: mcpDirectTools && mcpDirectTools.length > 0 ? mcpDirectTools : void 0,
1240
+ model: frontmatter.model,
1241
+ fallbackModels: fallbackModels && fallbackModels.length > 0 ? fallbackModels : void 0,
1242
+ thinking: frontmatter.thinking === FALSE_LITERAL ? false : frontmatter.thinking,
1243
+ systemPromptMode: parseSystemPromptMode(frontmatter.systemPromptMode, localName),
1244
+ inheritProjectContext: parseOptionalBoolean(frontmatter.inheritProjectContext) ?? defaultInheritProjectContext(localName),
1245
+ inheritSkills: parseOptionalBoolean(frontmatter.inheritSkills) ?? defaultInheritSkills(),
1246
+ defaultContext: parseDefaultContext(frontmatter.defaultContext),
1247
+ defaultTimeoutMs: parseTimeoutMs(frontmatter.timeoutMs, localName),
1248
+ systemPrompt: body,
1249
+ source,
1250
+ filePath,
1251
+ skills: skills && skills.length > 0 ? skills : void 0,
1252
+ skillPath: skillPath && skillPath.length > 0 ? skillPath : void 0,
1253
+ extensions: parseFrontmatterList(frontmatter.extensions),
1254
+ subagentOnlyExtensions: parseFrontmatterList(frontmatter.subagentOnlyExtensions),
1255
+ output: frontmatter.output,
1256
+ defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : void 0,
1257
+ defaultProgress: frontmatter.defaultProgress === TRUE_LITERAL,
1258
+ interactive: frontmatter.interactive === TRUE_LITERAL,
1259
+ maxSubagentDepth: Number.isInteger(maxSubagentDepth) && maxSubagentDepth >= 0 ? maxSubagentDepth : void 0,
1260
+ completionGuard: parseOptionalBoolean(frontmatter.completionGuard),
1261
+ toolBudget: parseToolBudget(frontmatter.toolBudget, localName),
1262
+ memory: parseMemoryFrontmatter(frontmatter.memory),
1263
+ extraFields: Object.keys(extraFields).length > 0 ? extraFields : void 0
1264
+ };
1265
+ setAgentFrontmatterFields(agent, new Set(Object.keys(frontmatter)));
1266
+ agents.push(agent);
1267
+ }
1268
+ return agents;
1269
+ }
1270
+ /**
1271
+ * Read-only agent directories contributed by plugins.
1272
+ *
1273
+ * Read from the environment on every call rather than captured once, because a
1274
+ * host can stage a plugin directory after this module is first imported.
1275
+ */
1276
+ function pluginAgentDirs(environment = process.env) {
1277
+ return (environment["PI_SUBAGENT_EXTRA_AGENT_DIRS"] ?? "").split(path.delimiter).map((directory) => directory.trim()).filter((directory) => directory.length > 0);
1278
+ }
1279
+ //#endregion
1280
+ //#region src/services/lruCache/index.ts
1281
+ /**
1282
+ * LruCache - a bounded cache with least-recently-used eviction.
1283
+ *
1284
+ * DESIGN PATTERNS:
1285
+ * - Backed by a Map, which iterates in insertion order. Re-inserting a key on
1286
+ * read moves it to the end, so the first key is always the least recently used
1287
+ *
1288
+ * WHY LRU AND NOT FIFO:
1289
+ * The predecessor package evicted in insertion order at a cap of 50. Once more
1290
+ * than 50 runs were live it evicted the entry it was about to read next, so the
1291
+ * cache degenerated to a permanent miss while still paying for the stat that
1292
+ * populated it. Recency-based eviction keeps the hot entries.
1293
+ *
1294
+ * AVOID:
1295
+ * - Reading through `map.get` directly anywhere; that skips the recency update
1296
+ */
1297
+ const MINIMUM_CAPACITY = 1;
1298
+ var LruCache = class {
1299
+ entries = /* @__PURE__ */ new Map();
1300
+ capacity;
1301
+ constructor(capacity) {
1302
+ if (!Number.isInteger(capacity) || capacity < MINIMUM_CAPACITY) throw new Error(`LruCache capacity must be an integer of at least ${MINIMUM_CAPACITY}.`);
1303
+ this.capacity = capacity;
1304
+ }
1305
+ get size() {
1306
+ return this.entries.size;
1307
+ }
1308
+ has(key) {
1309
+ return this.entries.has(key);
1310
+ }
1311
+ /** Read a value, marking it most recently used. */
1312
+ get(key) {
1313
+ if (!this.entries.has(key)) return void 0;
1314
+ const value = this.entries.get(key);
1315
+ this.entries.delete(key);
1316
+ this.entries.set(key, value);
1317
+ return value;
1318
+ }
1319
+ /** Insert or replace a value, marking it most recently used. */
1320
+ set(key, value) {
1321
+ if (this.entries.has(key)) this.entries.delete(key);
1322
+ this.entries.set(key, value);
1323
+ while (this.entries.size > this.capacity) {
1324
+ const oldest = this.entries.keys().next();
1325
+ if (oldest.done === true) break;
1326
+ this.entries.delete(oldest.value);
1327
+ }
1328
+ }
1329
+ delete(key) {
1330
+ return this.entries.delete(key);
1331
+ }
1332
+ clear() {
1333
+ this.entries.clear();
1334
+ }
1335
+ /** Keys from least to most recently used. Intended for tests and diagnostics. */
1336
+ keysFromOldest() {
1337
+ return [...this.entries.keys()];
1338
+ }
1339
+ };
1340
+ //#endregion
1341
+ //#region src/services/agentDiscovery/index.ts
1342
+ /**
1343
+ * Cached discovery of the agents visible from a working directory.
1344
+ *
1345
+ * WHY THIS IS A SERVICE AND NOT A FUNCTION:
1346
+ * Discovery reads two settings files and walks every agent directory in scope,
1347
+ * parsing the frontmatter of every file it finds. The slash-command completion
1348
+ * path asks for the agent list on each keystroke, and preflight asks again for
1349
+ * each launch, so the predecessor's plain function re-walked the tree many times
1350
+ * per second. Caching needs somewhere to live, and invalidation needs an owner.
1351
+ *
1352
+ * DESIGN PATTERNS:
1353
+ * - Two-layer validity. A short TTL bounds how stale an answer can be, and a
1354
+ * cheap source fingerprint catches the common edits sooner than the TTL would
1355
+ * - Bounded by an LRU keyed on cwd and scope, so a long session that visits many
1356
+ * directories cannot grow the cache without limit
1357
+ * - Mutations invalidate explicitly rather than relying on the TTL, so an agent
1358
+ * the user just created is visible immediately
1359
+ *
1360
+ * WHY THE FINGERPRINT IS NOT ENOUGH ON ITS OWN:
1361
+ * It stats the settings files and the agent directories, not every agent file.
1362
+ * A directory's mtime moves when a file is added or removed but not when an
1363
+ * existing file's contents change, so an in-place edit is caught by the TTL
1364
+ * rather than by the fingerprint. Stat'ing every agent file on every keystroke
1365
+ * would reintroduce the cost this cache exists to remove.
1366
+ *
1367
+ * AVOID:
1368
+ * - Handing out the cached array without copying it; a caller that sorts or
1369
+ * splices in place would corrupt every later reader
1370
+ * - Raising the TTL to hide a missing invalidate call
1371
+ */
1372
+ /** How long a discovery result may be reused without revalidation. */
1373
+ const DISCOVERY_TTL_MS = 5e3;
1374
+ /**
1375
+ * How many (cwd, scope) combinations stay cached.
1376
+ *
1377
+ * A session works in a handful of directories and there are three scopes, so
1378
+ * this is far above real usage; it exists to bound a pathological case.
1379
+ */
1380
+ const MAX_CACHED_DISCOVERIES = 64;
1381
+ const FINGERPRINT_FIELD_SEPARATOR = ":";
1382
+ const FINGERPRINT_ENTRY_SEPARATOR = "|";
1383
+ const MISSING_SOURCE_MARKER = "-";
1384
+ const MAJOR_MODES_CONFIG_RELATIVE_PATH = path.join(".po", "modes.yaml");
1385
+ const THINKING_SUFFIX = /:(?:off|minimal|low|medium|high|xhigh|max)$/;
1386
+ /**
1387
+ * Merge the sources into one list, later sources shadowing earlier ones.
1388
+ *
1389
+ * Precedence is plugin, then user, then project: the definition closest to the
1390
+ * work being done wins, which is what lets a repository pin an agent that the
1391
+ * user has also defined globally.
1392
+ */
1393
+ function mergeAgentsForScope(scope, userAgents, projectAgents, pluginAgents = []) {
1394
+ const byName = /* @__PURE__ */ new Map();
1395
+ for (const agent of pluginAgents) byName.set(agent.name, agent);
1396
+ if (scope !== "project") for (const agent of userAgents) byName.set(agent.name, agent);
1397
+ if (scope !== "user") for (const agent of projectAgents) byName.set(agent.name, agent);
1398
+ return [...byName.values()];
1399
+ }
1400
+ /** Size and mtime of one path, or a marker when it does not exist. */
1401
+ function stampOf(target) {
1402
+ try {
1403
+ const stat = fs.statSync(target);
1404
+ return `${target}${FINGERPRINT_FIELD_SEPARATOR}${stat.mtimeMs}${FINGERPRINT_FIELD_SEPARATOR}${stat.size}`;
1405
+ } catch {
1406
+ return `${target}${FINGERPRINT_FIELD_SEPARATOR}${MISSING_SOURCE_MARKER}`;
1407
+ }
1408
+ }
1409
+ /** Resolve Team-owned package configuration from the current parent harness state. */
1410
+ function resolveActiveTeamPackageConfig(environment = process.env) {
1411
+ const harness = loadHarnessState(environment).state;
1412
+ if (!harness.root) return void 0;
1413
+ const layers = loadMajorModesConfig(harness.root, environment.HOME);
1414
+ const config = mergeTeamPackageConfigurations(resolvePackageConfigurations(layers, harness.layers, PO_TEAM_PACKAGE));
1415
+ return config ? {
1416
+ config,
1417
+ path: path.join(harness.root, MAJOR_MODES_CONFIG_RELATIVE_PATH)
1418
+ } : void 0;
1419
+ }
1420
+ function teamModelSpecs(config) {
1421
+ if (!config?.models) return void 0;
1422
+ return config.models.map(({ model, thinking }) => thinking ? `${model.replace(THINKING_SUFFIX, "")}:${thinking}` : model);
1423
+ }
1424
+ /** Resolve Team's ordered package model policy without merging it into an agent definition. */
1425
+ function resolveActiveTeamModelSpecs(environment = process.env) {
1426
+ return teamModelSpecs(resolveActiveTeamPackageConfig(environment)?.config);
1427
+ }
1428
+ var AgentDiscoveryService = class {
1429
+ environment;
1430
+ constructor(environment = process.env) {
1431
+ this.environment = environment;
1432
+ }
1433
+ cache = new LruCache(MAX_CACHED_DISCOVERIES);
1434
+ /**
1435
+ * Seams for tests, as protected members rather than constructor arguments.
1436
+ *
1437
+ * Runtime tuning is not part of this service's dependency contract. A test
1438
+ * subclass overrides these values without abstracting the clock.
1439
+ */
1440
+ ttlMs = DISCOVERY_TTL_MS;
1441
+ now() {
1442
+ return Date.now();
1443
+ }
1444
+ discover(cwd, scope) {
1445
+ const resolvedCwd = path.resolve(cwd);
1446
+ const canonicalCwd = canonicalizeDiscoveryCwd(resolvedCwd);
1447
+ const key = `${scope}${FINGERPRINT_ENTRY_SEPARATOR}${canonicalCwd}`;
1448
+ const cached = this.cache.get(key);
1449
+ const timestamp = this.now();
1450
+ if (cached && cached.expiresAt > timestamp && cached.fingerprint === this.fingerprint(canonicalCwd)) return cloneResult(cached.result);
1451
+ const result = this.load(resolvedCwd, scope);
1452
+ this.cache.set(key, {
1453
+ result,
1454
+ expiresAt: timestamp + this.ttlMs,
1455
+ fingerprint: this.fingerprint(canonicalCwd)
1456
+ });
1457
+ return cloneResult(result);
1458
+ }
1459
+ find(cwd, scope, name) {
1460
+ return this.discover(cwd, scope).agents.find((agent) => agent.name === name);
1461
+ }
1462
+ invalidate() {
1463
+ this.cache.clear();
1464
+ }
1465
+ /**
1466
+ * A cheap stamp of everything discovery reads at the directory level.
1467
+ *
1468
+ * Deliberately independent of scope: a user-scoped and a project-scoped lookup
1469
+ * from the same directory read overlapping sources, and computing one stamp
1470
+ * keeps the stat count fixed regardless of how many scopes are queried.
1471
+ */
1472
+ fingerprint(cwd) {
1473
+ const harness = loadHarnessState(this.environment).state;
1474
+ const sources = [
1475
+ getUserAgentSettingsPath(this.environment),
1476
+ getProjectAgentSettingsPath(cwd, this.environment) ?? MISSING_SOURCE_MARKER,
1477
+ ...userAgentDirs(this.environment),
1478
+ ...resolveNearestProjectAgentDirs(cwd, this.environment).readDirs,
1479
+ ...pluginAgentDirs(this.environment)
1480
+ ];
1481
+ if (harness.root) sources.push(path.join(harness.root, MAJOR_MODES_CONFIG_RELATIVE_PATH));
1482
+ return [...sources.map(stampOf), `active-layers${FINGERPRINT_FIELD_SEPARATOR}${harness.layers.join(",")}`].join(FINGERPRINT_ENTRY_SEPARATOR);
1483
+ }
1484
+ /**
1485
+ * The uncached read. Protected so a test can count how often it actually runs,
1486
+ * which is the only direct way to assert that the cache is doing its job.
1487
+ */
1488
+ load(cwd, scope) {
1489
+ const userSettingsPath = getUserAgentSettingsPath(this.environment);
1490
+ const projectSettingsPath = getProjectAgentSettingsPath(cwd, this.environment);
1491
+ const userSettings = scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath);
1492
+ const projectSettings = scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath);
1493
+ const legacyDefaultModel = resolveSubagentDefaultModel(userSettings, projectSettings, userSettingsPath, projectSettingsPath);
1494
+ const teamPackage = resolveActiveTeamPackageConfig(this.environment);
1495
+ const packagePrimaryModel = teamModelSpecs(teamPackage?.config)?.[0];
1496
+ const defaultModel = packagePrimaryModel && teamPackage ? {
1497
+ type: "packages.team.defaultModel",
1498
+ scope: "package",
1499
+ path: teamPackage.path,
1500
+ model: packagePrimaryModel
1501
+ } : legacyDefaultModel;
1502
+ const defaultThinking = resolveSubagentDefaultThinking(userSettings, projectSettings, projectSettingsPath);
1503
+ const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);
1504
+ const resolve = (agents) => applyCustomAgentOverrides(applySubagentDefaults(agents, defaultModel, defaultThinking, defaultExtensions), userSettings, projectSettings, userSettingsPath, projectSettingsPath);
1505
+ const plugin = resolve(pluginAgentDirs(this.environment).flatMap((directory) => loadAgentsFromDir(directory, "plugin", this.environment)));
1506
+ return {
1507
+ agents: mergeAgentsForScope(scope, scope === "project" ? [] : resolve(userAgentDirs(this.environment).flatMap((directory) => loadAgentsFromDir(directory, "user", this.environment))), scope === "user" ? [] : resolve(resolveNearestProjectAgentDirs(cwd, this.environment).readDirs.flatMap((directory) => loadAgentsFromDir(directory, "project", this.environment))), plugin).filter((agent) => agent.disabled !== true),
1508
+ projectAgentsDir: resolveNearestProjectAgentDirs(cwd, this.environment).preferredDir,
1509
+ modelScope: projectSettings.modelScope ?? userSettings.modelScope
1510
+ };
1511
+ }
1512
+ };
1513
+ /**
1514
+ * Copy the parts a caller could mutate in place.
1515
+ *
1516
+ * The agent objects themselves are treated as immutable by every consumer, so
1517
+ * only the array is copied; deep-cloning every config on each keystroke would
1518
+ * cost more than the walk this cache replaced.
1519
+ */
1520
+ function cloneResult(result) {
1521
+ return {
1522
+ ...result,
1523
+ agents: [...result.agents]
1524
+ };
1525
+ }
1526
+ //#endregion
1527
+ export { agentHasWriteTools as a, getProjectConfigDir as c, LruCache as i, resolveActiveTeamModelSpecs as n, canonicalizeDiscoveryCwd as o, resolveActiveTeamPackageConfig as r, getAgentDir as s, AgentDiscoveryService as t };
1528
+
1529
+ //# sourceMappingURL=agentDiscovery-ZQlFkKvB.mjs.map