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

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 (38) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +5 -5
  3. package/src/cli/update-cli.ts +2 -2
  4. package/src/config.ts +5 -5
  5. package/src/core/auth-storage.ts +6 -1
  6. package/src/core/custom-commands/loader.ts +3 -1
  7. package/src/core/custom-tools/loader.ts +1 -18
  8. package/src/core/extensions/loader.ts +5 -21
  9. package/src/core/hooks/loader.ts +1 -18
  10. package/src/core/keybindings.ts +3 -1
  11. package/src/core/logger.ts +1 -2
  12. package/src/core/prompt-templates.ts +5 -4
  13. package/src/core/sdk.ts +5 -3
  14. package/src/core/skills.ts +5 -4
  15. package/src/core/tools/bash.ts +27 -11
  16. package/src/core/tools/exa/mcp-client.ts +2 -2
  17. package/src/core/tools/render-utils.ts +4 -4
  18. package/src/core/tools/task/agents.ts +5 -64
  19. package/src/core/tools/task/commands.ts +7 -33
  20. package/src/core/tools/task/discovery.ts +4 -66
  21. package/src/core/tools/task/executor.ts +32 -3
  22. package/src/core/tools/task/index.ts +11 -2
  23. package/src/core/tools/task/render.ts +25 -15
  24. package/src/core/tools/task/types.ts +3 -0
  25. package/src/core/tools/task/worker-protocol.ts +2 -1
  26. package/src/core/tools/task/worker.ts +2 -1
  27. package/src/core/tools/web-scrapers/huggingface.ts +1 -1
  28. package/src/core/tools/web-scrapers/readthedocs.ts +1 -1
  29. package/src/core/tools/web-scrapers/types.ts +1 -1
  30. package/src/core/tools/web-search/auth.ts +5 -3
  31. package/src/discovery/codex.ts +3 -1
  32. package/src/discovery/helpers.ts +124 -3
  33. package/src/migrations.ts +11 -9
  34. package/src/modes/interactive/components/extensions/state-manager.ts +19 -18
  35. package/src/modes/interactive/components/tool-execution.ts +2 -2
  36. package/src/prompts/agents/frontmatter.md +1 -0
  37. package/src/prompts/agents/reviewer.md +32 -4
  38. package/src/prompts/tools/task.md +3 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [4.3.2] - 2026-01-11
6
+ ### Changed
7
+
8
+ - Increased default bash output preview from 5 to 10 lines when collapsed
9
+ - Updated expanded bash output view to show full untruncated output when available
10
+
11
+ ## [4.3.1] - 2026-01-11
12
+
13
+ ### Changed
14
+
15
+ - Expanded system prompt with defensive reasoning guidance and assumption checks
16
+ - Allowed agent frontmatter to override subagent thinking level, clamped to model capabilities
17
+
18
+ ### Fixed
19
+
20
+ - Ensured reviewer agents use structured output schemas and include reported findings in task outputs
21
+
5
22
  ## [4.3.0] - 2026-01-11
6
23
 
7
24
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "4.3.0",
3
+ "version": "4.3.2",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -39,10 +39,10 @@
39
39
  "prepublishOnly": "bun run generate-template && bun run clean && bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "4.3.0",
43
- "@oh-my-pi/pi-agent-core": "4.3.0",
44
- "@oh-my-pi/pi-git-tool": "4.3.0",
45
- "@oh-my-pi/pi-tui": "4.3.0",
42
+ "@oh-my-pi/pi-ai": "4.3.2",
43
+ "@oh-my-pi/pi-agent-core": "4.3.2",
44
+ "@oh-my-pi/pi-git-tool": "4.3.2",
45
+ "@oh-my-pi/pi-tui": "4.3.2",
46
46
  "@openai/agents": "^0.3.7",
47
47
  "@sinclair/typebox": "^0.34.46",
48
48
  "ajv": "^8.17.1",
@@ -144,8 +144,8 @@ async function updateViaBun(): Promise<void> {
144
144
  try {
145
145
  execSync(`bun update -g ${PACKAGE}`, { stdio: "inherit" });
146
146
  console.log(chalk.green(`\n${theme.status.success} Update complete`));
147
- } catch {
148
- throw new Error("bun update failed");
147
+ } catch (error) {
148
+ throw new Error("bun update failed", { cause: error });
149
149
  }
150
150
  }
151
151
 
package/src/config.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
-
5
4
  // Embed package.json at build time for config
6
5
  import packageJson from "../package.json" with { type: "json" };
6
+ import { logger } from "./core/logger";
7
7
 
8
8
  // =============================================================================
9
9
  // App Config (from embedded package.json)
@@ -244,8 +244,8 @@ export function readConfigFile<T = unknown>(
244
244
  content: JSON.parse(content) as T,
245
245
  };
246
246
  }
247
- } catch {
248
- // Continue to next file on parse error
247
+ } catch (error) {
248
+ logger.warn("Failed to parse config file", { path: filePath, error: String(error) });
249
249
  }
250
250
  }
251
251
 
@@ -275,8 +275,8 @@ export function readAllConfigFiles<T = unknown>(
275
275
  content: JSON.parse(content) as T,
276
276
  });
277
277
  }
278
- } catch {
279
- // Skip files that fail to parse
278
+ } catch (error) {
279
+ logger.warn("Failed to parse config file", { path: filePath, error: String(error) });
280
280
  }
281
281
  }
282
282
 
@@ -939,7 +939,12 @@ export class AuthStorage {
939
939
 
940
940
  this.recordSessionCredential(provider, sessionId, "oauth", selection.index);
941
941
  return result.apiKey;
942
- } catch {
942
+ } catch (error) {
943
+ logger.warn("OAuth token refresh failed, removing credential", {
944
+ provider,
945
+ index: selection.index,
946
+ error: String(error),
947
+ });
943
948
  this.removeCredentialAt(provider, selection.index);
944
949
  if (this.getCredentialsForProvider(provider).some((credential) => credential.type === "oauth")) {
945
950
  return this.getApiKey(provider, sessionId, options);
@@ -11,6 +11,7 @@ import * as typebox from "@sinclair/typebox";
11
11
  import { getAgentDir, getConfigDirs } from "../../config";
12
12
  import * as piCodingAgent from "../../index";
13
13
  import { execCommand } from "../exec";
14
+ import { logger } from "../logger";
14
15
  import { createReviewCommand } from "./bundled/review";
15
16
  import { createWorktreeCommand } from "./bundled/wt";
16
17
  import type {
@@ -110,7 +111,8 @@ export function discoverCustomCommands(options: DiscoverCustomCommandsOptions =
110
111
  let entries: Dirent[];
111
112
  try {
112
113
  entries = readdirSync(commandsDir, { withFileTypes: true });
113
- } catch {
114
+ } catch (error) {
115
+ logger.warn("Failed to read custom commands directory", { path: commandsDir, error: String(error) });
114
116
  continue;
115
117
  }
116
118
  for (const entry of entries) {
@@ -5,11 +5,11 @@
5
5
  * to avoid import resolution issues with custom tools loaded from user directories.
6
6
  */
7
7
 
8
- import * as os from "node:os";
9
8
  import * as path from "node:path";
10
9
  import * as typebox from "@sinclair/typebox";
11
10
  import { toolCapability } from "../../capability/tool";
12
11
  import { type CustomTool, loadCapability } from "../../discovery";
12
+ import { expandPath } from "../../discovery/helpers";
13
13
  import * as piCodingAgent from "../../index";
14
14
  import { theme } from "../../modes/interactive/theme/theme";
15
15
  import type { ExecOptions } from "../exec";
@@ -19,23 +19,6 @@ import { logger } from "../logger";
19
19
  import { getAllPluginToolPaths } from "../plugins/loader";
20
20
  import type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from "./types";
21
21
 
22
- const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
23
-
24
- function normalizeUnicodeSpaces(str: string): string {
25
- return str.replace(UNICODE_SPACES, " ");
26
- }
27
-
28
- function expandPath(p: string): string {
29
- const normalized = normalizeUnicodeSpaces(p);
30
- if (normalized.startsWith("~/")) {
31
- return path.join(os.homedir(), normalized.slice(2));
32
- }
33
- if (normalized.startsWith("~")) {
34
- return path.join(os.homedir(), normalized.slice(1));
35
- }
36
- return normalized;
37
- }
38
-
39
22
  /**
40
23
  * Resolve tool path.
41
24
  * - Absolute paths used as-is
@@ -3,13 +3,12 @@
3
3
  */
4
4
 
5
5
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
6
- import { homedir } from "node:os";
7
6
  import * as path from "node:path";
8
7
  import type { KeyId } from "@oh-my-pi/pi-tui";
9
8
  import * as TypeBox from "@sinclair/typebox";
10
9
  import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
11
10
  import { loadCapability } from "../../discovery";
12
- import { getExtensionNameFromPath } from "../../discovery/helpers";
11
+ import { expandPath, getExtensionNameFromPath } from "../../discovery/helpers";
13
12
  import * as piCodingAgent from "../../index";
14
13
  import { createEventBus, type EventBus } from "../event-bus";
15
14
  import type { ExecOptions } from "../exec";
@@ -27,23 +26,6 @@ import type {
27
26
  ToolDefinition,
28
27
  } from "./types";
29
28
 
30
- const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
31
-
32
- function normalizeUnicodeSpaces(str: string): string {
33
- return str.replace(UNICODE_SPACES, " ");
34
- }
35
-
36
- function expandPath(p: string): string {
37
- const normalized = normalizeUnicodeSpaces(p);
38
- if (normalized.startsWith("~/")) {
39
- return path.join(homedir(), normalized.slice(2));
40
- }
41
- if (normalized.startsWith("~")) {
42
- return path.join(homedir(), normalized.slice(1));
43
- }
44
- return normalized;
45
- }
46
-
47
29
  function resolvePath(extPath: string, cwd: string): string {
48
30
  const expanded = expandPath(extPath);
49
31
  if (path.isAbsolute(expanded)) {
@@ -291,7 +273,8 @@ function readExtensionManifest(packageJsonPath: string): ExtensionManifest | nul
291
273
  return manifest;
292
274
  }
293
275
  return null;
294
- } catch {
276
+ } catch (error) {
277
+ logger.warn("Failed to read extension manifest", { path: packageJsonPath, error: String(error) });
295
278
  return null;
296
279
  }
297
280
  }
@@ -370,7 +353,8 @@ function discoverExtensionsInDir(dir: string): string[] {
370
353
  }
371
354
  }
372
355
  }
373
- } catch {
356
+ } catch (error) {
357
+ logger.warn("Failed to discover extensions in directory", { path: dir, error: String(error) });
374
358
  return [];
375
359
  }
376
360
 
@@ -2,12 +2,12 @@
2
2
  * Hook loader - loads TypeScript hook modules using native Bun import.
3
3
  */
4
4
 
5
- import * as os from "node:os";
6
5
  import * as path from "node:path";
7
6
  import * as typebox from "@sinclair/typebox";
8
7
  import { hookCapability } from "../../capability/hook";
9
8
  import type { Hook } from "../../discovery";
10
9
  import { loadCapability } from "../../discovery";
10
+ import { expandPath } from "../../discovery/helpers";
11
11
  import * as piCodingAgent from "../../index";
12
12
  import { logger } from "../logger";
13
13
  import type { HookMessage } from "../messages";
@@ -84,23 +84,6 @@ export interface LoadHooksResult {
84
84
  errors: Array<{ path: string; error: string }>;
85
85
  }
86
86
 
87
- const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
88
-
89
- function normalizeUnicodeSpaces(str: string): string {
90
- return str.replace(UNICODE_SPACES, " ");
91
- }
92
-
93
- function expandPath(p: string): string {
94
- const normalized = normalizeUnicodeSpaces(p);
95
- if (normalized.startsWith("~/")) {
96
- return path.join(os.homedir(), normalized.slice(2));
97
- }
98
- if (normalized.startsWith("~")) {
99
- return path.join(os.homedir(), normalized.slice(1));
100
- }
101
- return normalized;
102
- }
103
-
104
87
  /**
105
88
  * Resolve hook path.
106
89
  * - Absolute paths used as-is
@@ -10,6 +10,7 @@ import {
10
10
  setEditorKeybindings,
11
11
  } from "@oh-my-pi/pi-tui";
12
12
  import { getAgentDir } from "../config";
13
+ import { logger } from "./logger";
13
14
 
14
15
  /**
15
16
  * Application-level actions (coding agent specific).
@@ -136,7 +137,8 @@ export class KeybindingsManager {
136
137
  if (!existsSync(path)) return {};
137
138
  try {
138
139
  return JSON.parse(readFileSync(path, "utf-8"));
139
- } catch {
140
+ } catch (error) {
141
+ logger.warn("Failed to parse keybindings config", { path, error: String(error) });
140
142
  return {};
141
143
  }
142
144
  }
@@ -10,11 +10,10 @@ import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import winston from "winston";
12
12
  import DailyRotateFile from "winston-daily-rotate-file";
13
- import { CONFIG_DIR_NAME } from "../config";
14
13
 
15
14
  /** Get the logs directory (~/.omp/logs/) */
16
15
  function getLogsDir(): string {
17
- return join(homedir(), CONFIG_DIR_NAME, "logs");
16
+ return join(homedir(), ".omp", "logs");
18
17
  }
19
18
 
20
19
  /** Ensure logs directory exists */
@@ -1,6 +1,7 @@
1
1
  import { join, resolve } from "node:path";
2
2
  import Handlebars from "handlebars";
3
3
  import { CONFIG_DIR_NAME, getPromptsDir } from "../config";
4
+ import { logger } from "./logger";
4
5
 
5
6
  /**
6
7
  * Represents a prompt template loaded from a markdown file
@@ -448,12 +449,12 @@ async function loadTemplatesFromDir(
448
449
  source: sourceStr,
449
450
  });
450
451
  }
451
- } catch (_error) {
452
- // Silently skip files that can't be read
452
+ } catch (error) {
453
+ logger.warn("Failed to load prompt template", { path: fullPath, error: String(error) });
453
454
  }
454
455
  }
455
- } catch (_error) {
456
- // Silently skip directories that can't be read
456
+ } catch (error) {
457
+ logger.warn("Failed to scan prompt templates directory", { dir, error: String(error) });
457
458
  }
458
459
 
459
460
  return templates;
package/src/core/sdk.ts CHANGED
@@ -28,7 +28,7 @@
28
28
 
29
29
  import { join } from "node:path";
30
30
  import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
31
- import type { Message, Model } from "@oh-my-pi/pi-ai";
31
+ import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai";
32
32
  import type { Component } from "@oh-my-pi/pi-tui";
33
33
  import chalk from "chalk";
34
34
  // Import discovery to register all providers on startup
@@ -631,6 +631,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
631
631
  // Clamp to model capabilities
632
632
  if (!model || !model.reasoning) {
633
633
  thinkingLevel = "off";
634
+ } else if (thinkingLevel === "xhigh" && !supportsXhigh(model)) {
635
+ thinkingLevel = "high";
634
636
  }
635
637
 
636
638
  let skills: Skill[];
@@ -1021,8 +1023,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1021
1023
  });
1022
1024
  lspServers = result.servers;
1023
1025
  time("warmupLspServers");
1024
- } catch {
1025
- // Ignore warmup errors
1026
+ } catch (error) {
1027
+ logger.warn("LSP server warmup failed", { cwd, error: String(error) });
1026
1028
  }
1027
1029
  }
1028
1030
 
@@ -7,6 +7,7 @@ import type { SourceMeta } from "../capability/types";
7
7
  import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
8
8
  import { loadCapability } from "../discovery";
9
9
  import { parseFrontmatter } from "../discovery/helpers";
10
+ import { logger } from "./logger";
10
11
  import type { SkillsSettings } from "./settings-manager";
11
12
 
12
13
  // Re-export SkillFrontmatter for backward compatibility
@@ -67,8 +68,8 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills
67
68
  source: options.source,
68
69
  });
69
70
  }
70
- } catch {
71
- // Skip invalid skills
71
+ } catch (error) {
72
+ logger.warn("Failed to load skill", { path: skillFile, error: String(error) });
72
73
  }
73
74
  }
74
75
 
@@ -131,8 +132,8 @@ function scanDirectoryForSkills(dir: string): LoadSkillsResult {
131
132
  source: "custom",
132
133
  });
133
134
  }
134
- } catch {
135
- // Skip invalid skills
135
+ } catch (error) {
136
+ logger.warn("Failed to load skill", { path: skillFile, error: String(error) });
136
137
  }
137
138
  }
138
139
 
@@ -15,6 +15,8 @@ import { resolveToCwd } from "./path-utils";
15
15
  import { createToolUIKit } from "./render-utils";
16
16
  import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
17
17
 
18
+ export const BASH_DEFAULT_PREVIEW_LINES = 10;
19
+
18
20
  const bashSchema = Type.Object({
19
21
  command: Type.String({ description: "Bash command to execute" }),
20
22
  timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
@@ -26,6 +28,7 @@ const bashSchema = Type.Object({
26
28
  export interface BashToolDetails {
27
29
  truncation?: TruncationResult;
28
30
  fullOutputPath?: string;
31
+ fullOutput?: string;
29
32
  }
30
33
 
31
34
  /**
@@ -101,9 +104,12 @@ export function createBashTool(session: ToolSession, options?: BashToolOptions):
101
104
  const truncation = truncateTail(currentOutput);
102
105
  onUpdate({
103
106
  content: [{ type: "text", text: truncation.content || "" }],
104
- details: {
105
- truncation: truncation.truncated ? truncation : undefined,
106
- },
107
+ details: truncation.truncated
108
+ ? {
109
+ truncation,
110
+ fullOutput: currentOutput,
111
+ }
112
+ : undefined,
107
113
  });
108
114
  }
109
115
  },
@@ -129,6 +135,7 @@ export function createBashTool(session: ToolSession, options?: BashToolOptions):
129
135
  details = {
130
136
  truncation,
131
137
  fullOutputPath: result.fullOutputPath,
138
+ fullOutput: currentOutput,
132
139
  };
133
140
 
134
141
  const startLine = truncation.totalLines - truncation.outputLines + 1;
@@ -173,6 +180,9 @@ interface BashRenderContext {
173
180
  previewLines?: number;
174
181
  }
175
182
 
183
+ // Preview line limit when not expanded (matches tool-execution behavior)
184
+ export const BASH_PREVIEW_LINES = 10;
185
+
176
186
  export const bashToolRenderer = {
177
187
  renderCall(args: BashRenderArgs, uiTheme: Theme): Component {
178
188
  const ui = createToolUIKit(uiTheme);
@@ -214,21 +224,25 @@ export const bashToolRenderer = {
214
224
  const { renderContext } = options;
215
225
  const details = result.details;
216
226
 
227
+ const expanded = renderContext?.expanded ?? options.expanded;
228
+ const previewLines = renderContext?.previewLines ?? BASH_DEFAULT_PREVIEW_LINES;
229
+
217
230
  // Get output from context (preferred) or fall back to result content
218
231
  const output = renderContext?.output ?? (result.content?.find((c) => c.type === "text")?.text ?? "").trim();
219
- const expanded = renderContext?.expanded ?? options.expanded;
220
- const previewLines = renderContext?.previewLines ?? 5;
232
+ const fullOutput = details?.fullOutput;
233
+ const displayOutput = expanded ? (fullOutput ?? output) : output;
234
+ const showingFullOutput = expanded && fullOutput !== undefined;
221
235
 
222
236
  // Build truncation warning lines (static, doesn't depend on width)
223
237
  const truncation = details?.truncation;
224
238
  const fullOutputPath = details?.fullOutputPath;
225
239
  let warningLine: string | undefined;
226
- if (truncation?.truncated || fullOutputPath) {
240
+ if (fullOutputPath || (truncation?.truncated && !showingFullOutput)) {
227
241
  const warnings: string[] = [];
228
242
  if (fullOutputPath) {
229
243
  warnings.push(`Full output: ${fullOutputPath}`);
230
244
  }
231
- if (truncation?.truncated) {
245
+ if (truncation?.truncated && !showingFullOutput) {
232
246
  if (truncation.truncatedBy === "lines") {
233
247
  warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
234
248
  } else {
@@ -237,17 +251,19 @@ export const bashToolRenderer = {
237
251
  );
238
252
  }
239
253
  }
240
- warningLine = uiTheme.fg("warning", ui.wrapBrackets(warnings.join(". ")));
254
+ if (warnings.length > 0) {
255
+ warningLine = uiTheme.fg("warning", ui.wrapBrackets(warnings.join(". ")));
256
+ }
241
257
  }
242
258
 
243
- if (!output) {
259
+ if (!displayOutput) {
244
260
  // No output - just show warning if any
245
261
  return new Text(warningLine ?? "", 0, 0);
246
262
  }
247
263
 
248
264
  if (expanded) {
249
265
  // Show all lines when expanded
250
- const styledOutput = output
266
+ const styledOutput = displayOutput
251
267
  .split("\n")
252
268
  .map((line) => uiTheme.fg("toolOutput", line))
253
269
  .join("\n");
@@ -256,7 +272,7 @@ export const bashToolRenderer = {
256
272
  }
257
273
 
258
274
  // Collapsed: use width-aware caching component
259
- const styledOutput = output
275
+ const styledOutput = displayOutput
260
276
  .split("\n")
261
277
  .map((line) => uiTheme.fg("toolOutput", line))
262
278
  .join("\n");
@@ -292,8 +292,8 @@ export async function fetchMCPToolSchema(
292
292
  mcpSchemaCache.set(cacheKey, tool);
293
293
  return tool;
294
294
  }
295
- } catch {
296
- // Fall through to return null
295
+ } catch (error) {
296
+ logger.warn("Failed to fetch MCP tool schema", { mcpToolName, isWebsetsTool, error: String(error) });
297
297
  }
298
298
  return null;
299
299
  }
@@ -359,11 +359,11 @@ export function formatDiagnostics(
359
359
  const isLastDiag = di === diagnostics.length - 1;
360
360
  const diagBranch = isLastFile
361
361
  ? isLastDiag
362
- ? ` ${theme.tree.last}`
363
- : ` ${theme.tree.branch}`
362
+ ? ` ${theme.tree.last}`
363
+ : ` ${theme.tree.branch}`
364
364
  : isLastDiag
365
- ? ` ${theme.tree.vertical} ${theme.tree.last}`
366
- : ` ${theme.tree.vertical} ${theme.tree.branch}`;
365
+ ? `${theme.tree.vertical} ${theme.tree.last}`
366
+ : `${theme.tree.vertical} ${theme.tree.branch}`;
367
367
 
368
368
  const sevIcon =
369
369
  d.severity === "error"
@@ -4,6 +4,7 @@
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
8
  import exploreMd from "../../../prompts/agents/explore.md" with { type: "text" };
8
9
  // Embed agent markdown files at build time
9
10
  import agentFrontmatterTemplate from "../../../prompts/agents/frontmatter.md" with { type: "text" };
@@ -18,6 +19,7 @@ interface AgentFrontmatter {
18
19
  description: string;
19
20
  spawns?: string;
20
21
  model?: string;
22
+ thinkingLevel?: string;
21
23
  }
22
24
 
23
25
  interface EmbeddedAgentDef {
@@ -71,80 +73,19 @@ const EMBEDDED_AGENTS: { name: string; content: string }[] = EMBEDDED_AGENT_DEFS
71
73
  content: buildAgentContent(def),
72
74
  }));
73
75
 
74
- /**
75
- * Parse YAML frontmatter from markdown content.
76
- */
77
- function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
78
- const frontmatter: Record<string, string> = {};
79
- const normalized = content.replace(/\r\n/g, "\n");
80
-
81
- if (!normalized.startsWith("---")) {
82
- return { frontmatter, body: normalized };
83
- }
84
-
85
- const endIndex = normalized.indexOf("\n---", 3);
86
- if (endIndex === -1) {
87
- return { frontmatter, body: normalized };
88
- }
89
-
90
- const frontmatterBlock = normalized.slice(4, endIndex);
91
- const body = normalized.slice(endIndex + 4).trim();
92
-
93
- for (const line of frontmatterBlock.split("\n")) {
94
- const match = line.match(/^([\w-]+):\s*(.*)$/);
95
- if (match) {
96
- let value = match[2].trim();
97
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
98
- value = value.slice(1, -1);
99
- }
100
- frontmatter[match[1]] = value;
101
- }
102
- }
103
-
104
- return { frontmatter, body };
105
- }
106
-
107
76
  /**
108
77
  * Parse an agent from embedded content.
109
78
  */
110
79
  function parseAgent(fileName: string, content: string, source: AgentSource): AgentDefinition | null {
111
80
  const { frontmatter, body } = parseFrontmatter(content);
81
+ const fields = parseAgentFields(frontmatter);
112
82
 
113
- if (!frontmatter.name || !frontmatter.description) {
83
+ if (!fields) {
114
84
  return null;
115
85
  }
116
86
 
117
- const tools = frontmatter.tools
118
- ?.split(",")
119
- .map((t) => t.trim())
120
- .filter(Boolean);
121
-
122
- // Parse spawns field
123
- let spawns: string[] | "*" | undefined;
124
- if (frontmatter.spawns !== undefined) {
125
- const spawnsRaw = frontmatter.spawns.trim();
126
- if (spawnsRaw === "*") {
127
- spawns = "*";
128
- } else if (spawnsRaw) {
129
- spawns = spawnsRaw
130
- .split(",")
131
- .map((s) => s.trim())
132
- .filter(Boolean);
133
- if (spawns.length === 0) spawns = undefined;
134
- }
135
- }
136
-
137
- // Backward compat: infer spawns: "*" when tools includes "task"
138
- if (spawns === undefined && tools?.includes("task")) {
139
- spawns = "*";
140
- }
141
-
142
87
  return {
143
- name: frontmatter.name,
144
- description: frontmatter.description,
145
- tools: tools && tools.length > 0 ? tools : undefined,
146
- spawns,
147
- model: frontmatter.model,
88
+ ...fields,
148
89
  systemPrompt: body,
149
90
  source,
150
91
  filePath: `embedded:${fileName}`,