@oh-my-pi/pi-coding-agent 4.3.2 → 4.4.6

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 (54) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/package.json +5 -6
  3. package/src/core/frontmatter.ts +98 -0
  4. package/src/core/keybindings.ts +1 -1
  5. package/src/core/prompt-templates.ts +5 -34
  6. package/src/core/sdk.ts +3 -0
  7. package/src/core/skills.ts +3 -3
  8. package/src/core/slash-commands.ts +14 -5
  9. package/src/core/tools/calculator.ts +1 -1
  10. package/src/core/tools/edit.ts +2 -2
  11. package/src/core/tools/exa/render.ts +23 -11
  12. package/src/core/tools/index.test.ts +2 -0
  13. package/src/core/tools/index.ts +3 -0
  14. package/src/core/tools/jtd-to-json-schema.ts +1 -6
  15. package/src/core/tools/ls.ts +5 -2
  16. package/src/core/tools/lsp/config.ts +2 -2
  17. package/src/core/tools/lsp/render.ts +33 -12
  18. package/src/core/tools/notebook.ts +1 -1
  19. package/src/core/tools/output.ts +1 -1
  20. package/src/core/tools/read.ts +15 -49
  21. package/src/core/tools/render-utils.ts +61 -24
  22. package/src/core/tools/renderers.ts +2 -0
  23. package/src/core/tools/schema-validation.test.ts +501 -0
  24. package/src/core/tools/task/agents.ts +6 -2
  25. package/src/core/tools/task/commands.ts +9 -3
  26. package/src/core/tools/task/discovery.ts +3 -2
  27. package/src/core/tools/task/render.ts +10 -7
  28. package/src/core/tools/todo-write.ts +256 -0
  29. package/src/core/tools/web-fetch.ts +4 -2
  30. package/src/core/tools/web-scrapers/choosealicense.ts +2 -2
  31. package/src/core/tools/web-search/render.ts +13 -10
  32. package/src/core/tools/write.ts +2 -2
  33. package/src/discovery/builtin.ts +4 -4
  34. package/src/discovery/cline.ts +4 -3
  35. package/src/discovery/codex.ts +3 -3
  36. package/src/discovery/cursor.ts +2 -2
  37. package/src/discovery/github.ts +3 -2
  38. package/src/discovery/helpers.test.ts +14 -10
  39. package/src/discovery/helpers.ts +2 -39
  40. package/src/discovery/windsurf.ts +3 -3
  41. package/src/modes/interactive/components/custom-editor.ts +4 -11
  42. package/src/modes/interactive/components/index.ts +2 -1
  43. package/src/modes/interactive/components/read-tool-group.ts +118 -0
  44. package/src/modes/interactive/components/todo-display.ts +112 -0
  45. package/src/modes/interactive/components/tool-execution.ts +18 -2
  46. package/src/modes/interactive/controllers/command-controller.ts +2 -2
  47. package/src/modes/interactive/controllers/event-controller.ts +91 -32
  48. package/src/modes/interactive/controllers/input-controller.ts +19 -13
  49. package/src/modes/interactive/interactive-mode.ts +103 -3
  50. package/src/modes/interactive/theme/theme.ts +4 -0
  51. package/src/modes/interactive/types.ts +14 -2
  52. package/src/modes/interactive/utils/ui-helpers.ts +55 -26
  53. package/src/prompts/system/system-prompt.md +177 -126
  54. package/src/prompts/tools/todo-write.md +187 -0
@@ -0,0 +1,501 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { sanitizeSchemaForGoogle } from "@oh-my-pi/pi-ai";
3
+ import { BUILTIN_TOOLS, createTools, HIDDEN_TOOLS, type ToolSession } from "./index";
4
+
5
+ /**
6
+ * Problematic JSON Schema features that cause issues with various providers.
7
+ *
8
+ * These are checked AFTER sanitization (sanitizeSchemaForGoogle) is applied,
9
+ * so features like `const` that are transformed by sanitization are not flagged.
10
+ *
11
+ * Prohibited (error):
12
+ * - $schema: Explicit schema declarations
13
+ * - $ref / $defs: Schema references (must inline everything)
14
+ * - prefixItems: Draft 2020-12 feature (use items array)
15
+ * - $dynamicRef / $dynamicAnchor: Draft 2020-12 features
16
+ * - unevaluatedProperties / unevaluatedItems: Draft 2020-12 features
17
+ * - const: Should be converted to enum by sanitization
18
+ * - examples: Should be stripped
19
+ *
20
+ * Warnings (non-blocking):
21
+ * - additionalProperties: false - Sometimes causes validation issues
22
+ * - format: Some validators don't recognize format keywords
23
+ */
24
+
25
+ const PROHIBITED_KEYS = new Set([
26
+ "$schema",
27
+ "$ref",
28
+ "$defs",
29
+ "$dynamicRef",
30
+ "$dynamicAnchor",
31
+ "prefixItems",
32
+ "unevaluatedProperties",
33
+ "unevaluatedItems",
34
+ "const", // Should be converted to enum by sanitizeSchemaForGoogle
35
+ "examples",
36
+ ]);
37
+
38
+ const WARNING_KEYS = new Set(["additionalProperties", "format"]);
39
+
40
+ interface SchemaViolation {
41
+ path: string;
42
+ key: string;
43
+ value: unknown;
44
+ severity: "error" | "warning";
45
+ }
46
+
47
+ function validateSchema(schema: unknown, path = "root"): SchemaViolation[] {
48
+ const violations: SchemaViolation[] = [];
49
+
50
+ if (schema === null || typeof schema !== "object") {
51
+ return violations;
52
+ }
53
+
54
+ if (Array.isArray(schema)) {
55
+ for (let i = 0; i < schema.length; i++) {
56
+ violations.push(...validateSchema(schema[i], `${path}[${i}]`));
57
+ }
58
+ return violations;
59
+ }
60
+
61
+ const obj = schema as Record<string, unknown>;
62
+
63
+ for (const [key, value] of Object.entries(obj)) {
64
+ const currentPath = `${path}.${key}`;
65
+
66
+ if (PROHIBITED_KEYS.has(key)) {
67
+ violations.push({
68
+ path: currentPath,
69
+ key,
70
+ value,
71
+ severity: "error",
72
+ });
73
+ }
74
+
75
+ if (WARNING_KEYS.has(key)) {
76
+ // additionalProperties: false is the problematic case
77
+ if (key === "additionalProperties" && value === false) {
78
+ violations.push({
79
+ path: currentPath,
80
+ key,
81
+ value,
82
+ severity: "warning",
83
+ });
84
+ }
85
+ // format is always potentially problematic
86
+ if (key === "format" && typeof value === "string") {
87
+ violations.push({
88
+ path: currentPath,
89
+ key,
90
+ value,
91
+ severity: "warning",
92
+ });
93
+ }
94
+ }
95
+
96
+ // Recurse into nested objects
97
+ if (value !== null && typeof value === "object") {
98
+ violations.push(...validateSchema(value, currentPath));
99
+ }
100
+ }
101
+
102
+ return violations;
103
+ }
104
+
105
+ function createTestSession(): ToolSession {
106
+ return {
107
+ cwd: "/tmp/test",
108
+ hasUI: true,
109
+ getSessionFile: () => null,
110
+ getSessionSpawns: () => "*",
111
+ };
112
+ }
113
+
114
+ describe("sanitizeSchemaForGoogle", () => {
115
+ it("converts const to enum", () => {
116
+ const schema = { type: "string", const: "active" };
117
+ const sanitized = sanitizeSchemaForGoogle(schema);
118
+ expect(sanitized).toEqual({ type: "string", enum: ["active"] });
119
+ });
120
+
121
+ it("merges const into existing enum", () => {
122
+ const schema = { type: "string", const: "active", enum: ["inactive"] };
123
+ const sanitized = sanitizeSchemaForGoogle(schema);
124
+ expect(sanitized).toEqual({ type: "string", enum: ["inactive", "active"] });
125
+ });
126
+
127
+ it("does not duplicate const in enum", () => {
128
+ const schema = { type: "string", const: "active", enum: ["active", "inactive"] };
129
+ const sanitized = sanitizeSchemaForGoogle(schema);
130
+ expect(sanitized).toEqual({ type: "string", enum: ["active", "inactive"] });
131
+ });
132
+
133
+ it("handles nested const in anyOf", () => {
134
+ const schema = {
135
+ anyOf: [
136
+ { type: "string", const: "file" },
137
+ { type: "string", const: "dir" },
138
+ ],
139
+ };
140
+ const sanitized = sanitizeSchemaForGoogle(schema);
141
+ expect(sanitized).toEqual({
142
+ anyOf: [
143
+ { type: "string", enum: ["file"] },
144
+ { type: "string", enum: ["dir"] },
145
+ ],
146
+ });
147
+ });
148
+
149
+ it("handles deeply nested schemas", () => {
150
+ const schema = {
151
+ type: "object",
152
+ properties: {
153
+ nested: {
154
+ type: "object",
155
+ properties: {
156
+ status: { type: "string", const: "active" },
157
+ },
158
+ },
159
+ },
160
+ };
161
+ const sanitized = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
162
+ const props = sanitized.properties as Record<string, unknown>;
163
+ const nested = props.nested as Record<string, unknown>;
164
+ const nestedProps = nested.properties as Record<string, unknown>;
165
+ const status = nestedProps.status as Record<string, unknown>;
166
+ expect(status.const).toBeUndefined();
167
+ expect(status.enum).toEqual(["active"]);
168
+ });
169
+
170
+ it("preserves other schema properties", () => {
171
+ const schema = {
172
+ type: "string",
173
+ const: "value",
174
+ description: "A description",
175
+ minLength: 1,
176
+ };
177
+ const sanitized = sanitizeSchemaForGoogle(schema);
178
+ expect(sanitized).toEqual({
179
+ type: "string",
180
+ enum: ["value"],
181
+ description: "A description",
182
+ minLength: 1,
183
+ });
184
+ });
185
+
186
+ it("handles arrays correctly", () => {
187
+ const schema = {
188
+ type: "array",
189
+ items: { type: "string", const: "only" },
190
+ };
191
+ const sanitized = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
192
+ const items = sanitized.items as Record<string, unknown>;
193
+ expect(items.const).toBeUndefined();
194
+ expect(items.enum).toEqual(["only"]);
195
+ });
196
+
197
+ it("passes through primitives unchanged", () => {
198
+ expect(sanitizeSchemaForGoogle("string")).toBe("string");
199
+ expect(sanitizeSchemaForGoogle(123)).toBe(123);
200
+ expect(sanitizeSchemaForGoogle(true)).toBe(true);
201
+ expect(sanitizeSchemaForGoogle(null)).toBe(null);
202
+ });
203
+ });
204
+
205
+ describe("tool schema validation (post-sanitization)", () => {
206
+ it("all builtin tool schemas are valid after sanitization", async () => {
207
+ const session = createTestSession();
208
+ const tools = await createTools(session);
209
+
210
+ const allViolations: { tool: string; violations: SchemaViolation[] }[] = [];
211
+
212
+ for (const tool of tools) {
213
+ const schema = tool.parameters;
214
+ if (!schema) continue;
215
+
216
+ // Apply the same sanitization that happens before sending to providers
217
+ const sanitized = sanitizeSchemaForGoogle(schema);
218
+ const violations = validateSchema(sanitized, tool.name);
219
+ const errors = violations.filter((v) => v.severity === "error");
220
+
221
+ if (errors.length > 0) {
222
+ allViolations.push({ tool: tool.name, violations: errors });
223
+ }
224
+ }
225
+
226
+ if (allViolations.length > 0) {
227
+ const message = allViolations
228
+ .map(({ tool, violations }) => {
229
+ const details = violations.map((v) => ` - ${v.path}: ${v.key} = ${JSON.stringify(v.value)}`).join("\n");
230
+ return `${tool}:\n${details}`;
231
+ })
232
+ .join("\n\n");
233
+
234
+ throw new Error(`Prohibited JSON Schema features found after sanitization:\n\n${message}`);
235
+ }
236
+
237
+ expect(allViolations).toEqual([]);
238
+ });
239
+
240
+ it("no sanitized schema contains $schema declaration", async () => {
241
+ const session = createTestSession();
242
+ const tools = await createTools(session);
243
+
244
+ for (const tool of tools) {
245
+ const schema = tool.parameters;
246
+ if (!schema) continue;
247
+
248
+ const sanitized = sanitizeSchemaForGoogle(schema);
249
+ const violations = validateSchema(sanitized, tool.name).filter((v) => v.key === "$schema");
250
+ expect(violations).toEqual([]);
251
+ }
252
+ });
253
+
254
+ it("no sanitized schema contains $ref or $defs", async () => {
255
+ const session = createTestSession();
256
+ const tools = await createTools(session);
257
+
258
+ for (const tool of tools) {
259
+ const schema = tool.parameters;
260
+ if (!schema) continue;
261
+
262
+ const sanitized = sanitizeSchemaForGoogle(schema);
263
+ const violations = validateSchema(sanitized, tool.name).filter((v) => v.key === "$ref" || v.key === "$defs");
264
+ expect(violations).toEqual([]);
265
+ }
266
+ });
267
+
268
+ it("no sanitized schema contains Draft 2020-12 specific features", async () => {
269
+ const session = createTestSession();
270
+ const tools = await createTools(session);
271
+
272
+ const draft2020Features = [
273
+ "prefixItems",
274
+ "$dynamicRef",
275
+ "$dynamicAnchor",
276
+ "unevaluatedProperties",
277
+ "unevaluatedItems",
278
+ ];
279
+
280
+ for (const tool of tools) {
281
+ const schema = tool.parameters;
282
+ if (!schema) continue;
283
+
284
+ const sanitized = sanitizeSchemaForGoogle(schema);
285
+ const violations = validateSchema(sanitized, tool.name).filter((v) => draft2020Features.includes(v.key));
286
+ expect(violations).toEqual([]);
287
+ }
288
+ });
289
+
290
+ it("sanitization removes const (converts to enum)", async () => {
291
+ const session = createTestSession();
292
+ const tools = await createTools(session);
293
+
294
+ for (const tool of tools) {
295
+ const schema = tool.parameters;
296
+ if (!schema) continue;
297
+
298
+ const sanitized = sanitizeSchemaForGoogle(schema);
299
+ const violations = validateSchema(sanitized, tool.name).filter((v) => v.key === "const");
300
+ expect(violations).toEqual([]);
301
+ }
302
+ });
303
+
304
+ it("no sanitized schema contains examples field", async () => {
305
+ const session = createTestSession();
306
+ const tools = await createTools(session);
307
+
308
+ for (const tool of tools) {
309
+ const schema = tool.parameters;
310
+ if (!schema) continue;
311
+
312
+ const sanitized = sanitizeSchemaForGoogle(schema);
313
+ const violations = validateSchema(sanitized, tool.name).filter((v) => v.key === "examples");
314
+ expect(violations).toEqual([]);
315
+ }
316
+ });
317
+
318
+ it("hidden tools also have valid sanitized schemas", async () => {
319
+ const session = createTestSession();
320
+
321
+ for (const [name, factory] of Object.entries(HIDDEN_TOOLS)) {
322
+ const tool = await factory(session);
323
+ if (!tool) continue;
324
+
325
+ const schema = tool.parameters;
326
+ if (!schema) continue;
327
+
328
+ const sanitized = sanitizeSchemaForGoogle(schema);
329
+ const violations = validateSchema(sanitized, name);
330
+ const errors = violations.filter((v) => v.severity === "error");
331
+
332
+ if (errors.length > 0) {
333
+ const details = errors.map((v) => ` - ${v.path}: ${v.key} = ${JSON.stringify(v.value)}`).join("\n");
334
+ throw new Error(`Hidden tool ${name} has prohibited schema features after sanitization:\n${details}`);
335
+ }
336
+ }
337
+ });
338
+
339
+ it("BUILTIN_TOOLS registry matches expected tools", () => {
340
+ const expectedTools = [
341
+ "ask",
342
+ "bash",
343
+ "calc",
344
+ "ssh",
345
+ "edit",
346
+ "find",
347
+ "git",
348
+ "grep",
349
+ "ls",
350
+ "lsp",
351
+ "notebook",
352
+ "output",
353
+ "read",
354
+ "task",
355
+ "todo_write",
356
+ "web_fetch",
357
+ "web_search",
358
+ "write",
359
+ ];
360
+
361
+ expect(Object.keys(BUILTIN_TOOLS).sort()).toEqual(expectedTools.sort());
362
+ });
363
+
364
+ it("logs warnings for potentially problematic features (non-blocking)", async () => {
365
+ const session = createTestSession();
366
+ const tools = await createTools(session);
367
+
368
+ const warnings: { tool: string; violations: SchemaViolation[] }[] = [];
369
+
370
+ for (const tool of tools) {
371
+ const schema = tool.parameters;
372
+ if (!schema) continue;
373
+
374
+ const sanitized = sanitizeSchemaForGoogle(schema);
375
+ const violations = validateSchema(sanitized, tool.name);
376
+ const toolWarnings = violations.filter((v) => v.severity === "warning");
377
+
378
+ if (toolWarnings.length > 0) {
379
+ warnings.push({ tool: tool.name, violations: toolWarnings });
380
+ }
381
+ }
382
+
383
+ // Log warnings but don't fail - these are advisory
384
+ if (warnings.length > 0) {
385
+ const message = warnings
386
+ .map(({ tool, violations }) => {
387
+ const details = violations.map((v) => ` - ${v.path}: ${v.key} = ${JSON.stringify(v.value)}`).join("\n");
388
+ return `${tool}:\n${details}`;
389
+ })
390
+ .join("\n\n");
391
+
392
+ console.log(`Schema warnings (non-blocking):\n\n${message}`);
393
+ }
394
+
395
+ // This test passes regardless - warnings are informational
396
+ expect(true).toBe(true);
397
+ });
398
+ });
399
+
400
+ describe("validateSchema helper", () => {
401
+ it("detects $schema declarations", () => {
402
+ const schema = { $schema: "http://json-schema.org/draft-07/schema#", type: "object" };
403
+ const violations = validateSchema(schema);
404
+ expect(violations.some((v) => v.key === "$schema")).toBe(true);
405
+ });
406
+
407
+ it("detects $ref usage", () => {
408
+ const schema = { type: "object", properties: { foo: { $ref: "#/$defs/Foo" } } };
409
+ const violations = validateSchema(schema);
410
+ expect(violations.some((v) => v.key === "$ref")).toBe(true);
411
+ });
412
+
413
+ it("detects $defs usage", () => {
414
+ const schema = { $defs: { Foo: { type: "string" } }, type: "object" };
415
+ const violations = validateSchema(schema);
416
+ expect(violations.some((v) => v.key === "$defs")).toBe(true);
417
+ });
418
+
419
+ it("detects const usage (should be sanitized away)", () => {
420
+ const schema = { type: "object", properties: { status: { const: "active" } } };
421
+ const violations = validateSchema(schema);
422
+ expect(violations.some((v) => v.key === "const")).toBe(true);
423
+ });
424
+
425
+ it("detects examples field", () => {
426
+ const schema = { type: "string", examples: ["foo", "bar"] };
427
+ const violations = validateSchema(schema);
428
+ expect(violations.some((v) => v.key === "examples")).toBe(true);
429
+ });
430
+
431
+ it("detects prefixItems (Draft 2020-12)", () => {
432
+ const schema = { type: "array", prefixItems: [{ type: "string" }] };
433
+ const violations = validateSchema(schema);
434
+ expect(violations.some((v) => v.key === "prefixItems")).toBe(true);
435
+ });
436
+
437
+ it("detects unevaluatedProperties (Draft 2020-12)", () => {
438
+ const schema = { type: "object", unevaluatedProperties: false };
439
+ const violations = validateSchema(schema);
440
+ expect(violations.some((v) => v.key === "unevaluatedProperties")).toBe(true);
441
+ });
442
+
443
+ it("warns on additionalProperties: false", () => {
444
+ const schema = { type: "object", additionalProperties: false };
445
+ const violations = validateSchema(schema);
446
+ const warning = violations.find((v) => v.key === "additionalProperties");
447
+ expect(warning?.severity).toBe("warning");
448
+ });
449
+
450
+ it("does not warn on additionalProperties: true", () => {
451
+ const schema = { type: "object", additionalProperties: true };
452
+ const violations = validateSchema(schema);
453
+ expect(violations.some((v) => v.key === "additionalProperties")).toBe(false);
454
+ });
455
+
456
+ it("warns on format keyword", () => {
457
+ const schema = { type: "string", format: "uri" };
458
+ const violations = validateSchema(schema);
459
+ const warning = violations.find((v) => v.key === "format");
460
+ expect(warning?.severity).toBe("warning");
461
+ });
462
+
463
+ it("recursively validates nested schemas", () => {
464
+ const schema = {
465
+ type: "object",
466
+ properties: {
467
+ nested: {
468
+ type: "object",
469
+ properties: {
470
+ deep: { $ref: "#/$defs/Deep" },
471
+ },
472
+ },
473
+ },
474
+ };
475
+ const violations = validateSchema(schema);
476
+ expect(violations.some((v) => v.key === "$ref")).toBe(true);
477
+ expect(violations.find((v) => v.key === "$ref")?.path).toContain("nested");
478
+ });
479
+
480
+ it("validates array items", () => {
481
+ const schema = {
482
+ type: "array",
483
+ items: [{ const: "first" }, { type: "string" }],
484
+ };
485
+ const violations = validateSchema(schema);
486
+ expect(violations.some((v) => v.key === "const")).toBe(true);
487
+ });
488
+
489
+ it("returns empty array for valid schema", () => {
490
+ const schema = {
491
+ type: "object",
492
+ properties: {
493
+ name: { type: "string", description: "User name" },
494
+ age: { type: "number", minimum: 0 },
495
+ },
496
+ required: ["name"],
497
+ };
498
+ const violations = validateSchema(schema);
499
+ expect(violations.filter((v) => v.severity === "error")).toEqual([]);
500
+ });
501
+ });
@@ -4,7 +4,8 @@
4
4
  * Agents are embedded at build time via Bun's import with { type: "text" }.
5
5
  */
6
6
 
7
- import { parseAgentFields, parseFrontmatter } from "../../../discovery/helpers";
7
+ import { parseFrontmatter } from "../../../core/frontmatter";
8
+ import { parseAgentFields } from "../../../discovery/helpers";
8
9
  import exploreMd from "../../../prompts/agents/explore.md" with { type: "text" };
9
10
  // Embed agent markdown files at build time
10
11
  import agentFrontmatterTemplate from "../../../prompts/agents/frontmatter.md" with { type: "text" };
@@ -77,7 +78,10 @@ const EMBEDDED_AGENTS: { name: string; content: string }[] = EMBEDDED_AGENT_DEFS
77
78
  * Parse an agent from embedded content.
78
79
  */
79
80
  function parseAgent(fileName: string, content: string, source: AgentSource): AgentDefinition | null {
80
- const { frontmatter, body } = parseFrontmatter(content);
81
+ const { frontmatter, body } = parseFrontmatter(content, {
82
+ source: `embedded:${fileName}`,
83
+ level: "fatal",
84
+ });
81
85
  const fields = parseAgentFields(frontmatter);
82
86
 
83
87
  if (!fields) {
@@ -6,8 +6,8 @@
6
6
 
7
7
  import * as path from "node:path";
8
8
  import { type SlashCommand, slashCommandCapability } from "../../../capability/slash-command";
9
+ import { parseFrontmatter } from "../../../core/frontmatter";
9
10
  import { loadCapability } from "../../../discovery";
10
- import { parseFrontmatter } from "../../../discovery/helpers";
11
11
 
12
12
  // Embed command markdown files at build time
13
13
  import initMd from "../../../prompts/agents/init.md" with { type: "text" };
@@ -48,7 +48,10 @@ export function loadBundledCommands(): WorkflowCommand[] {
48
48
  const commands: WorkflowCommand[] = [];
49
49
 
50
50
  for (const { name, content } of EMBEDDED_COMMANDS) {
51
- const { frontmatter, body } = parseFrontmatter(content);
51
+ const { frontmatter, body } = parseFrontmatter(content, {
52
+ source: `embedded:${name}`,
53
+ level: "fatal",
54
+ });
52
55
  const cmdName = name.replace(/\.md$/, "");
53
56
 
54
57
  commands.push({
@@ -82,7 +85,10 @@ export async function discoverCommands(cwd: string): Promise<WorkflowCommand[]>
82
85
  for (const cmd of result.items) {
83
86
  if (seen.has(cmd.name)) continue;
84
87
 
85
- const { frontmatter, body } = parseFrontmatter(cmd.content);
88
+ const { frontmatter, body } = parseFrontmatter(cmd.content, {
89
+ source: cmd.path ?? `workflow-command:${cmd.name}`,
90
+ level: cmd.level === "native" ? "fatal" : "warn",
91
+ });
86
92
 
87
93
  // Map capability levels to WorkflowCommand source
88
94
  const source: "bundled" | "user" | "project" = cmd.level === "native" ? "bundled" : cmd.level;
@@ -15,7 +15,8 @@
15
15
  import * as fs from "node:fs";
16
16
  import * as path from "node:path";
17
17
  import { findAllNearestProjectConfigDirs, getConfigDirs } from "../../../config";
18
- import { parseAgentFields, parseFrontmatter } from "../../../discovery/helpers";
18
+ import { parseFrontmatter } from "../../../core/frontmatter";
19
+ import { parseAgentFields } from "../../../discovery/helpers";
19
20
  import { loadBundledAgents } from "./agents";
20
21
  import type { AgentDefinition, AgentSource } from "./types";
21
22
 
@@ -61,7 +62,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
61
62
  continue;
62
63
  }
63
64
 
64
- const { frontmatter, body } = parseFrontmatter(content);
65
+ const { frontmatter, body } = parseFrontmatter(content, { source: filePath });
65
66
  const fields = parseAgentFields(frontmatter);
66
67
 
67
68
  if (!fields) {
@@ -14,8 +14,8 @@ import {
14
14
  formatBadge,
15
15
  formatDuration,
16
16
  formatMoreItems,
17
+ formatStatusIcon,
17
18
  formatTokens,
18
- getStyledStatusIcon,
19
19
  truncate,
20
20
  } from "../render-utils";
21
21
  import type { ReportFindingDetails, SubmitReviewDetails } from "../review";
@@ -38,15 +38,15 @@ const PRIORITY_LABELS: Record<number, string> = {
38
38
  function getStatusIcon(status: AgentProgress["status"], theme: Theme, spinnerFrame?: number): string {
39
39
  switch (status) {
40
40
  case "pending":
41
- return getStyledStatusIcon("pending", theme);
41
+ return formatStatusIcon("pending", theme);
42
42
  case "running":
43
- return getStyledStatusIcon("running", theme, spinnerFrame);
43
+ return formatStatusIcon("running", theme, spinnerFrame);
44
44
  case "completed":
45
- return getStyledStatusIcon("success", theme);
45
+ return formatStatusIcon("success", theme);
46
46
  case "failed":
47
- return getStyledStatusIcon("error", theme);
47
+ return formatStatusIcon("error", theme);
48
48
  case "aborted":
49
- return getStyledStatusIcon("aborted", theme);
49
+ return formatStatusIcon("aborted", theme);
50
50
  }
51
51
  }
52
52
 
@@ -389,7 +389,10 @@ function renderAgentProgress(
389
389
  }
390
390
  if ((dataArray as unknown[]).length > displayCount) {
391
391
  lines.push(
392
- `${continuePrefix}${theme.fg("dim", formatMoreItems((dataArray as unknown[]).length - displayCount, "item", theme))}`,
392
+ `${continuePrefix}${theme.fg(
393
+ "dim",
394
+ formatMoreItems((dataArray as unknown[]).length - displayCount, "item", theme),
395
+ )}`,
393
396
  );
394
397
  }
395
398
  }