pi-ui-extend 1.0.39 → 1.0.41

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 (113) hide show
  1. package/README.md +1 -1
  2. package/dist/app/commands/command-registry.js +2 -2
  3. package/dist/app/commands/command-session-actions.d.ts +0 -1
  4. package/dist/app/commands/command-session-actions.js +22 -13
  5. package/dist/app/icons.d.ts +14 -0
  6. package/dist/app/icons.js +33 -0
  7. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  8. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  9. package/dist/app/rendering/dcp-stats.js +214 -46
  10. package/dist/app/rendering/editor-panels.js +8 -5
  11. package/dist/app/session/lazy-session-manager.js +12 -1
  12. package/dist/app/session/tabs-controller.d.ts +2 -5
  13. package/dist/app/session/tabs-controller.js +12 -21
  14. package/dist/app/subagents/subagents-model.d.ts +14 -1
  15. package/dist/app/subagents/subagents-model.js +34 -15
  16. package/dist/app/types.d.ts +2 -0
  17. package/dist/bundled-extensions/session-title/config.js +1 -1
  18. package/dist/markdown-format.js +27 -9
  19. package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
  20. package/dist/schemas/pi-tools-suite-schema.js +46 -31
  21. package/external/pi-tools-suite/README.md +392 -52
  22. package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
  23. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  24. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  25. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  26. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  27. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  28. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  29. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  30. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  31. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  32. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  33. package/external/pi-tools-suite/docs/evals.md +684 -0
  34. package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
  35. package/external/pi-tools-suite/package.json +10 -3
  36. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  37. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  38. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  39. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  40. package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  42. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
  44. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  46. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
  47. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
  49. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  50. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  52. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
  55. package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
  56. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
  57. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  58. package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
  59. package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
  60. package/external/pi-tools-suite/src/config.ts +1 -22
  61. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  62. package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
  63. package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
  64. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  65. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  66. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
  67. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  68. package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
  69. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  70. package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
  71. package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
  72. package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
  73. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
  74. package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
  75. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  76. package/external/pi-tools-suite/src/dcp/config.ts +36 -61
  77. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  78. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  79. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  80. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  81. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  82. package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
  83. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  84. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
  85. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  86. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  87. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
  88. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  89. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  90. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
  91. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  92. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  93. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  94. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  95. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  96. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
  98. package/external/pi-tools-suite/src/index.ts +9 -0
  99. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  100. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  101. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  102. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  103. package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
  104. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  105. package/package.json +6 -6
  106. package/schemas/pi-tools-suite.json +159 -78
  107. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  108. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  109. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  110. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  111. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  112. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  113. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
@@ -0,0 +1,494 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ /**
5
+ * Project-local sub-agent definitions from `.pi/agents/*.md`.
6
+ *
7
+ * Each Markdown file defines one sub-agent type (Claude Code `.claude/agents`
8
+ * style): a YAML frontmatter subset for the `SubagentTypeConfig` fields plus a
9
+ * markdown body that becomes `promptAppend`. The type name comes from the
10
+ * filename (without `.md`).
11
+ *
12
+ * This module only discovers and parses; field normalization happens in
13
+ * config.ts via `normalizeSubagentTypeProfile` so JSONC and markdown sources
14
+ * apply identical validation. There is intentionally no caching: callers load
15
+ * definitions on every config read so edits (and `/reload`) take effect on the
16
+ * next spawn without a restart.
17
+ *
18
+ * Supported frontmatter YAML subset (bounded, dependency-free):
19
+ * - `key: value` scalars: plain, single/double-quoted strings, numbers, booleans
20
+ * - `#` comments (full-line and trailing)
21
+ * - inline arrays: `[a, b, "c"]`
22
+ * - block lists: `- item` (indented or at the parent key's indent)
23
+ * - one+ levels of nested maps for `modelByParent` / `retry`
24
+ * - comma-separated strings for array fields (`tools: read, grep, bash`)
25
+ *
26
+ * Anything else (tabs, block scalars `|`/`>`, anchors/aliases, flow maps,
27
+ * multi-document markers, list-item maps) is a hard error naming file and line.
28
+ */
29
+
30
+ export interface AgentDefinition {
31
+ /** Raw type profile fields; normalized later by config.ts. */
32
+ raw: Record<string, unknown>;
33
+ /** Absolute path of the source .md file (used in error messages). */
34
+ file: string;
35
+ }
36
+
37
+ /** Backwards-compatible name for project-local definitions. */
38
+ export type ProjectAgentDefinition = AgentDefinition;
39
+
40
+ /** Keys allowed in agent frontmatter. `name` is a filename-consistency check. */
41
+ const KNOWN_FRONTMATTER_KEYS = new Set([
42
+ "name",
43
+ "description",
44
+ "icon",
45
+ "models",
46
+ "model",
47
+ "fallbackModels",
48
+ "modelByParent",
49
+ "thinking",
50
+ "tools",
51
+ "isolatedSkills",
52
+ "extraArgs",
53
+ "promptAppend",
54
+ "promptOverride",
55
+ "retry",
56
+ "maxResultBytes",
57
+ "timeoutMs",
58
+ ]);
59
+
60
+ /** String fields whose comma-separated form expands to an array. */
61
+ const COMMA_SEPARATED_ARRAY_KEYS = new Set([
62
+ "tools",
63
+ "models",
64
+ "fallbackModels",
65
+ "isolatedSkills",
66
+ "extraArgs",
67
+ "retryableExitCodes",
68
+ ]);
69
+
70
+ const AGENT_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
71
+ const NUMERIC_PATTERN = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
72
+
73
+ /** Discover and parse `.pi/agents/*.md` for the project containing `cwd`. */
74
+ export function readProjectAgentDefinitions(cwd: string): Record<string, ProjectAgentDefinition> {
75
+ const dir = findProjectAgentsDir(cwd);
76
+ return dir ? readAgentDefinitionsFromDir(dir) : {};
77
+ }
78
+
79
+ /** Discover and parse top-level `*.md` agent definitions in one directory. */
80
+ export function readAgentDefinitionsFromDir(dir: string): Record<string, AgentDefinition> {
81
+ const definitions: Record<string, AgentDefinition> = {};
82
+ for (const file of agentDefinitionFiles(dir)) {
83
+ const name = agentNameFromFilename(path.basename(file), file);
84
+ const raw = parseAgentMarkdownFile(file);
85
+ if (!raw) continue; // no frontmatter: not an agent definition (e.g. README.md)
86
+ definitions[name] = { raw, file };
87
+ }
88
+ return definitions;
89
+ }
90
+
91
+ /**
92
+ * List project agent files: the first `.pi/agents` directory found walking up
93
+ * from `cwd`, its top-level `*.md` files (dotfiles excluded), name-sorted for
94
+ * deterministic merge order.
95
+ */
96
+ export function projectAgentDefinitionFiles(cwd: string): string[] {
97
+ const dir = findProjectAgentsDir(cwd);
98
+ return dir ? agentDefinitionFiles(dir) : [];
99
+ }
100
+
101
+ /** Top-level non-dot Markdown files in deterministic filename order. */
102
+ export function agentDefinitionFiles(dir: string): string[] {
103
+ if (!isDirectory(dir)) return [];
104
+ return fs
105
+ .readdirSync(dir, { withFileTypes: true })
106
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".md") && !entry.name.startsWith("."))
107
+ .map((entry) => path.join(dir, entry.name))
108
+ .sort();
109
+ }
110
+
111
+ function findProjectAgentsDir(startDir: string): string | undefined {
112
+ let dir = path.resolve(startDir);
113
+ const root = path.parse(dir).root;
114
+ while (true) {
115
+ const candidate = path.join(dir, ".pi", "agents");
116
+ if (isDirectory(candidate)) return candidate;
117
+ if (dir === root) return undefined;
118
+ const parent = path.dirname(dir);
119
+ if (parent === dir) return undefined;
120
+ dir = parent;
121
+ }
122
+ }
123
+
124
+ function isDirectory(candidate: string): boolean {
125
+ try {
126
+ return fs.statSync(candidate).isDirectory();
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ /** Derive the sub-agent type name from an agent filename; reject invalid names. */
133
+ export function agentNameFromFilename(filename: string, file: string): string {
134
+ const name = filename.endsWith(".md") ? filename.slice(0, -3) : filename;
135
+ if (!AGENT_NAME_PATTERN.test(name)) {
136
+ throw new Error(
137
+ `Agent file name "${filename}" is not a valid sub-agent type name (${AGENT_NAME_PATTERN}): ${file}`,
138
+ );
139
+ }
140
+ return name;
141
+ }
142
+
143
+ /**
144
+ * Parse one agent .md file into raw profile fields.
145
+ * Returns undefined when the file has no frontmatter (skipped, not an error).
146
+ * Returns raw fields with the markdown body folded into `promptAppend`.
147
+ */
148
+ function parseAgentMarkdownFile(file: string): Record<string, unknown> | undefined {
149
+ const source = fs.readFileSync(file, "utf-8");
150
+ const parsed = parseAgentMarkdown(source, file);
151
+ if (!parsed) return undefined;
152
+ const { frontmatter, body } = parsed;
153
+
154
+ if (frontmatter.name !== undefined && frontmatter.name !== path.basename(file, ".md")) {
155
+ throw new Error(
156
+ `Agent frontmatter "name: ${String(frontmatter.name)}" does not match the file name "${path.basename(file, ".md")}": ${file}. Rename the file or remove the "name" key.`,
157
+ );
158
+ }
159
+
160
+ const raw: Record<string, unknown> = { ...frontmatter };
161
+ delete raw.name;
162
+ expandCommaSeparatedArrays(raw);
163
+ if (body) {
164
+ raw.promptAppend = typeof raw.promptAppend === "string" && raw.promptAppend.trim()
165
+ ? `${raw.promptAppend.trim()}\n\n${body}`
166
+ : body;
167
+ }
168
+ return raw;
169
+ }
170
+
171
+ /** Parse agent markdown source; exported for tests. */
172
+ export function parseAgentMarkdown(
173
+ source: string,
174
+ file: string,
175
+ ): { frontmatter: Record<string, unknown>; body: string } | undefined {
176
+ const text = source.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
177
+ const lines = text.split("\n");
178
+ if (lines[0]?.trim() !== "---") return undefined;
179
+
180
+ let end = -1;
181
+ for (let i = 1; i < lines.length; i++) {
182
+ const line = lines[i].trim();
183
+ if (line === "---" || line === "...") {
184
+ end = i;
185
+ break;
186
+ }
187
+ if (line === "---" || /^---\s/.test(line)) {
188
+ throw new Error(`Unsupported YAML document separator in agent frontmatter: ${file}:${i + 1}`);
189
+ }
190
+ }
191
+ if (end < 0) {
192
+ throw new Error(`Agent frontmatter is not terminated with "---": ${file}`);
193
+ }
194
+
195
+ const frontmatter = parseFrontmatter(lines.slice(1, end), file);
196
+ const body = lines.slice(end + 1).join("\n").replace(/^\n+/, "").trimEnd();
197
+ return { frontmatter, body };
198
+ }
199
+
200
+ interface SourceLine {
201
+ text: string;
202
+ number: number;
203
+ indent: number;
204
+ }
205
+
206
+ function parseFrontmatter(lines: string[], file: string): Record<string, unknown> {
207
+ const sourceLines: SourceLine[] = [];
208
+ for (let i = 0; i < lines.length; i++) {
209
+ const rawLine = lines[i];
210
+ if (rawLine.includes("\t")) {
211
+ const leading = rawLine.slice(0, rawLine.search(/\S/));
212
+ if (leading.includes("\t")) {
213
+ throw new Error(`Tabs are not allowed for YAML indentation: ${file}:${i + 1}`);
214
+ }
215
+ }
216
+ const trimmedStart = rawLine.replace(/^ +/, "");
217
+ const indent = rawLine.length - trimmedStart.length;
218
+ sourceLines.push({ text: trimmedStart.replace(/\s+$/, ""), number: i + 2, indent });
219
+ }
220
+ const [value, next] = parseBlock(sourceLines, 0, 0, file);
221
+ if (next < sourceLines.length) {
222
+ const line = sourceLines[next];
223
+ throw new Error(`Unexpected content in agent frontmatter: ${file}:${line.number} ("${line.text}")`);
224
+ }
225
+ if (!isPlainObject(value)) {
226
+ throw new Error(`Agent frontmatter must be a mapping of "key: value" lines: ${file}`);
227
+ }
228
+ for (const key of Object.keys(value)) {
229
+ if (!KNOWN_FRONTMATTER_KEYS.has(key)) {
230
+ throw new Error(
231
+ `Unknown agent frontmatter key "${key}": ${file}. Known keys: ${[...KNOWN_FRONTMATTER_KEYS].sort().join(", ")}.`,
232
+ );
233
+ }
234
+ }
235
+ return value;
236
+ }
237
+
238
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
239
+ return typeof value === "object" && value !== null && !Array.isArray(value);
240
+ }
241
+
242
+ /** Skip blank lines and full-line comments. Returns the next meaningful index. */
243
+ function skipIgnorable(lines: SourceLine[], start: number): number {
244
+ let i = start;
245
+ while (i < lines.length && (lines[i].text === "" || lines[i].text.startsWith("#"))) i++;
246
+ return i;
247
+ }
248
+
249
+ function isListItem(text: string): boolean {
250
+ return text === "-" || text.startsWith("- ");
251
+ }
252
+
253
+ /** Parse a block (map or list) whose items live at `indent`. Returns [value, nextIndex]. */
254
+ function parseBlock(
255
+ lines: SourceLine[],
256
+ start: number,
257
+ indent: number,
258
+ file: string,
259
+ ): [unknown, number] {
260
+ const first = skipIgnorable(lines, start);
261
+ if (first >= lines.length || lines[first].indent < indent) return [undefined, first];
262
+ if (isListItem(lines[first].text)) return parseList(lines, first, indent, file);
263
+ return parseMap(lines, first, indent, file);
264
+ }
265
+
266
+ function parseMap(
267
+ lines: SourceLine[],
268
+ start: number,
269
+ indent: number,
270
+ file: string,
271
+ ): [Record<string, unknown>, number] {
272
+ const result: Record<string, unknown> = {};
273
+ let i = start;
274
+ while (true) {
275
+ i = skipIgnorable(lines, i);
276
+ if (i >= lines.length) break;
277
+ const line = lines[i];
278
+ if (line.indent < indent) break;
279
+ if (line.indent > indent) {
280
+ throw new Error(
281
+ `Unexpected indentation in agent frontmatter (expected ${indent}, got ${line.indent}): ${file}:${line.number}`,
282
+ );
283
+ }
284
+ if (isListItem(line.text)) {
285
+ throw new Error(`Unexpected list item in agent frontmatter mapping: ${file}:${line.number}`);
286
+ }
287
+
288
+ const { key, rest } = splitKey(line.text, file, line.number);
289
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
290
+ throw new Error(`Duplicate agent frontmatter key "${key}": ${file}:${line.number}`);
291
+ }
292
+
293
+ if (rest === "") {
294
+ const peek = skipIgnorable(lines, i + 1);
295
+ if (peek < lines.length && (lines[peek].indent > indent || (lines[peek].indent === indent && isListItem(lines[peek].text)))) {
296
+ const [child, next] = parseBlock(lines, peek, lines[peek].indent, file);
297
+ result[key] = child;
298
+ i = next;
299
+ continue;
300
+ }
301
+ result[key] = undefined;
302
+ i++;
303
+ continue;
304
+ }
305
+
306
+ result[key] = parseInlineValue(rest, file, line.number);
307
+ i++;
308
+ }
309
+ return [result, i];
310
+ }
311
+
312
+ function parseList(
313
+ lines: SourceLine[],
314
+ start: number,
315
+ indent: number,
316
+ file: string,
317
+ ): [unknown[], number] {
318
+ const items: unknown[] = [];
319
+ let i = start;
320
+ while (true) {
321
+ i = skipIgnorable(lines, i);
322
+ if (i >= lines.length) break;
323
+ const line = lines[i];
324
+ if (line.indent < indent || !isListItem(line.text)) break;
325
+ if (line.indent > indent) {
326
+ throw new Error(
327
+ `Unexpected indentation in agent frontmatter list (expected ${indent}, got ${line.indent}): ${file}:${line.number}`,
328
+ );
329
+ }
330
+ const rest = line.text === "-" ? "" : line.text.slice(2);
331
+ if (rest === "") {
332
+ throw new Error(`Nested blocks under list items are not supported: ${file}:${line.number}`);
333
+ }
334
+ if (/^[^'"[][^:]*:(\s|$)/.test(rest) && findTopLevelColon(rest) >= 0) {
335
+ throw new Error(`Map items ("- key: value") are not supported in agent frontmatter lists: ${file}:${line.number}`);
336
+ }
337
+ items.push(parseInlineValue(rest, file, line.number));
338
+ i++;
339
+ }
340
+ return [items, i];
341
+ }
342
+
343
+ /** Split "key: rest" honoring quoted keys; `rest` keeps trailing comments. */
344
+ function splitKey(text: string, file: string, lineNumber: number): { key: string; rest: string } {
345
+ if (text.startsWith('"') || text.startsWith("'")) {
346
+ const quote = text[0];
347
+ const end = text.indexOf(quote, 1);
348
+ if (end < 0) throw new Error(`Unterminated quoted key in agent frontmatter: ${file}:${lineNumber}`);
349
+ const key = unquote(text.slice(0, end + 1), file, lineNumber);
350
+ const after = text.slice(end + 1);
351
+ if (!after.startsWith(":")) {
352
+ throw new Error(`Expected ":" after quoted key in agent frontmatter: ${file}:${lineNumber}`);
353
+ }
354
+ return { key, rest: after.slice(1).trim() };
355
+ }
356
+ const colon = findTopLevelColon(text);
357
+ if (colon < 0) {
358
+ throw new Error(`Expected "key: value" in agent frontmatter: ${file}:${lineNumber} ("${text}")`);
359
+ }
360
+ const key = text.slice(0, colon).trim();
361
+ if (!key) {
362
+ throw new Error(`Empty key in agent frontmatter: ${file}:${lineNumber}`);
363
+ }
364
+ return { key, rest: text.slice(colon + 1).trim() };
365
+ }
366
+
367
+ /** Find a mapping colon (": " or line-ending ":") outside quotes. */
368
+ function findTopLevelColon(text: string): number {
369
+ let quote = "";
370
+ for (let i = 0; i < text.length; i++) {
371
+ const ch = text[i];
372
+ if (quote) {
373
+ if (ch === quote) quote = "";
374
+ continue;
375
+ }
376
+ if (ch === '"' || ch === "'") {
377
+ quote = ch;
378
+ continue;
379
+ }
380
+ if (ch === ":" && (i === text.length - 1 || text[i + 1] === " ")) return i;
381
+ }
382
+ return -1;
383
+ }
384
+
385
+ /** Parse an inline value: scalar, quoted string, or inline array. */
386
+ function parseInlineValue(text: string, file: string, lineNumber: number): unknown {
387
+ const stripped = stripTrailingComment(text);
388
+ if (stripped === "") return undefined;
389
+ if (stripped.startsWith("[")) return parseInlineArray(stripped, file, lineNumber);
390
+ if (stripped.startsWith("{")) {
391
+ throw new Error(`Flow maps "{...}" are not supported; use block (indented) form: ${file}:${lineNumber}`);
392
+ }
393
+ if (stripped.startsWith("|") || stripped.startsWith(">")) {
394
+ throw new Error(`Block scalars ("|"/">") are not supported in frontmatter; put long text in the markdown body: ${file}:${lineNumber}`);
395
+ }
396
+ if (stripped.startsWith("&") || stripped.startsWith("*") || stripped.startsWith("!")) {
397
+ throw new Error(`YAML anchors, aliases, and tags are not supported: ${file}:${lineNumber}`);
398
+ }
399
+ if (stripped.startsWith('"') || stripped.startsWith("'")) {
400
+ return unquote(stripped, file, lineNumber);
401
+ }
402
+ return parseScalar(stripped);
403
+ }
404
+
405
+ function parseInlineArray(text: string, file: string, lineNumber: number): unknown[] {
406
+ if (!text.endsWith("]")) {
407
+ throw new Error(`Unterminated inline array in agent frontmatter: ${file}:${lineNumber}`);
408
+ }
409
+ const inner = text.slice(1, -1).trim();
410
+ if (inner === "") return [];
411
+ const items: string[] = [];
412
+ let current = "";
413
+ let quote = "";
414
+ for (let i = 0; i < inner.length; i++) {
415
+ const ch = inner[i];
416
+ if (quote) {
417
+ current += ch;
418
+ if (ch === quote && current[current.length - 2] !== "\\") quote = "";
419
+ continue;
420
+ }
421
+ if (ch === '"' || ch === "'") {
422
+ quote = ch;
423
+ current += ch;
424
+ continue;
425
+ }
426
+ if (ch === ",") {
427
+ items.push(current.trim());
428
+ current = "";
429
+ continue;
430
+ }
431
+ current += ch;
432
+ }
433
+ if (quote) throw new Error(`Unterminated quoted string in inline array: ${file}:${lineNumber}`);
434
+ if (current.trim() !== "" || items.length > 0) items.push(current.trim());
435
+ return items.map((item) => {
436
+ if (item.startsWith('"') || item.startsWith("'")) return unquote(item, file, lineNumber);
437
+ return parseScalar(item);
438
+ });
439
+ }
440
+
441
+ function unquote(text: string, file: string, lineNumber: number): string {
442
+ const quote = text[0];
443
+ if (!text.endsWith(quote) || text.length < 2) {
444
+ throw new Error(`Unterminated quoted string in agent frontmatter: ${file}:${lineNumber}`);
445
+ }
446
+ const inner = text.slice(1, -1);
447
+ if (quote === "'") return inner.replace(/''/g, "'");
448
+ return inner
449
+ .replace(/\\n/g, "\n")
450
+ .replace(/\\t/g, "\t")
451
+ .replace(/\\"/g, '"')
452
+ .replace(/\\\\/g, "\\");
453
+ }
454
+
455
+ function parseScalar(text: string): unknown {
456
+ const value = text.trim();
457
+ if (value === "") return undefined;
458
+ if (value === "true") return true;
459
+ if (value === "false") return false;
460
+ if (value === "null" || value === "~") return undefined;
461
+ if (NUMERIC_PATTERN.test(value)) return Number(value);
462
+ return value;
463
+ }
464
+
465
+ /** Strip a trailing `# comment` outside quotes. */
466
+ function stripTrailingComment(text: string): string {
467
+ let quote = "";
468
+ for (let i = 0; i < text.length; i++) {
469
+ const ch = text[i];
470
+ if (quote) {
471
+ if (ch === quote) quote = "";
472
+ continue;
473
+ }
474
+ if (ch === '"' || ch === "'") {
475
+ quote = ch;
476
+ continue;
477
+ }
478
+ if (ch === "#" && (i === 0 || text[i - 1] === " " || text[i - 1] === "\t")) {
479
+ return text.slice(0, i).trimEnd();
480
+ }
481
+ }
482
+ return text.trim();
483
+ }
484
+
485
+ /** Expand comma-separated strings for known array fields, at any depth. */
486
+ function expandCommaSeparatedArrays(value: Record<string, unknown>): void {
487
+ for (const [key, item] of Object.entries(value)) {
488
+ if (typeof item === "string" && COMMA_SEPARATED_ARRAY_KEYS.has(key)) {
489
+ value[key] = item.split(",").map((part) => part.trim()).filter((part) => part !== "");
490
+ } else if (isPlainObject(item)) {
491
+ expandCommaSeparatedArrays(item);
492
+ }
493
+ }
494
+ }
@@ -0,0 +1,9 @@
1
+ import { fileURLToPath } from "node:url";
2
+
3
+ /** Non-secret, launcher-owned path supplied only to browser-qa children. */
4
+ export const BROWSER_QA_RUNNER_ENV = "PI_BROWSER_QA_RUNNER";
5
+
6
+ /** Resolve against the installed package, never the delegated project's cwd. */
7
+ export function getBrowserQaRunnerPath(): string {
8
+ return fileURLToPath(new URL("../agents/browser-qa/scripts/browser-qa-runner.mjs", import.meta.url));
9
+ }