diff --git a/packages/agent/test/utils/get-current-time.ts b/packages/agent/test/utils/get-current-time.ts index 45e2dea9d..097b09c37 100644 --- a/packages/agent/test/utils/get-current-time.ts +++ b/packages/agent/test/utils/get-current-time.ts @@ -16,7 +16,7 @@ export async function getCurrentTime(timezone?: string): Promise= 3) { throw new Error("Too many consecutive errors during Cursor auth polling"); diff --git a/packages/ai/src/utils/oauth/index.ts b/packages/ai/src/utils/oauth/index.ts index beabf2472..6af6ae2ba 100644 --- a/packages/ai/src/utils/oauth/index.ts +++ b/packages/ai/src/utils/oauth/index.ts @@ -124,7 +124,7 @@ export async function getOAuthApiKey( if (Date.now() >= creds.expires) { try { creds = await refreshOAuthToken(provider, creds); - } catch (_error) { + } catch { throw new Error(`Failed to refresh OAuth token for ${provider}`); } } diff --git a/packages/ai/src/utils/validation.ts b/packages/ai/src/utils/validation.ts index 707d533d2..7bc94ec39 100644 --- a/packages/ai/src/utils/validation.ts +++ b/packages/ai/src/utils/validation.ts @@ -266,7 +266,7 @@ if (!isBrowserExtension) { strict: false, }); addFormats(ajv); - } catch (_e) { + } catch { // AJV initialization failed (likely CSP restriction) console.warn("AJV validation disabled due to CSP restrictions"); } @@ -333,7 +333,9 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): any { } : originalArgs; - const errorMessage = `Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(receivedArgs, null, 2)}`; + const errorMessage = `Validation failed for tool "${ + toolCall.name + }":\n${errors}\n\nReceived arguments:\n${JSON.stringify(receivedArgs, null, 2)}`; throw new Error(errorMessage); } diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index d0a3a85e2..4c41d228f 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -468,7 +468,7 @@ describe("Context overflow error handling", () => { console.log("Pulling gpt-oss:20b model for Ollama overflow tests..."); try { execSync("ollama pull gpt-oss:20b", { stdio: "inherit" }); - } catch (_e) { + } catch { console.warn("Failed to pull gpt-oss:20b model, tests will be skipped"); return; } diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 988e68702..ea30a3bca 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -185,7 +185,9 @@ async function handleThinking(model: Model, options?: Op messages: [ { role: "user", - content: `Think long and hard about ${(Math.random() * 255) | 0} + 27. Think step by step. Then output the result.`, + content: `Think long and hard about ${ + (Math.random() * 255) | 0 + } + 27. Think step by step. Then output the result.`, timestamp: Date.now(), }, ], @@ -930,7 +932,7 @@ describe("Generate E2E Tests", () => { console.log("Pulling gpt-oss:20b model for Ollama tests..."); try { execSync("ollama pull gpt-oss:20b", { stdio: "inherit" }); - } catch (_e) { + } catch { console.warn("Failed to pull gpt-oss:20b model, tests will be skipped"); return; } diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5d84fd938..42d92cacc 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased] +### Changed + +- Expanded system prompt with defensive reasoning guidance and assumption checks +- Allowed agent frontmatter to override subagent thinking level, clamped to model capabilities + +### Fixed + +- Ensured reviewer agents use structured output schemas and include reported findings in task outputs + ## [4.3.0] - 2026-01-11 ### Added diff --git a/packages/coding-agent/src/cli/update-cli.ts b/packages/coding-agent/src/cli/update-cli.ts index 0224a1cdb..e81704165 100644 --- a/packages/coding-agent/src/cli/update-cli.ts +++ b/packages/coding-agent/src/cli/update-cli.ts @@ -144,8 +144,8 @@ async function updateViaBun(): Promise { try { execSync(`bun update -g ${PACKAGE}`, { stdio: "inherit" }); console.log(chalk.green(`\n${theme.status.success} Update complete`)); - } catch { - throw new Error("bun update failed"); + } catch (error) { + throw new Error("bun update failed", { cause: error }); } } diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index cf9fe8990..749795eea 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -1,9 +1,9 @@ import { existsSync, readFileSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; - // Embed package.json at build time for config import packageJson from "../package.json" with { type: "json" }; +import { logger } from "./core/logger"; // ============================================================================= // App Config (from embedded package.json) @@ -244,8 +244,8 @@ export function readConfigFile( content: JSON.parse(content) as T, }; } - } catch { - // Continue to next file on parse error + } catch (error) { + logger.warn("Failed to parse config file", { path: filePath, error: String(error) }); } } @@ -275,8 +275,8 @@ export function readAllConfigFiles( content: JSON.parse(content) as T, }); } - } catch { - // Skip files that fail to parse + } catch (error) { + logger.warn("Failed to parse config file", { path: filePath, error: String(error) }); } } diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 35099af92..d7a43c59f 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -939,7 +939,12 @@ export class AuthStorage { this.recordSessionCredential(provider, sessionId, "oauth", selection.index); return result.apiKey; - } catch { + } catch (error) { + logger.warn("OAuth token refresh failed, removing credential", { + provider, + index: selection.index, + error: String(error), + }); this.removeCredentialAt(provider, selection.index); if (this.getCredentialsForProvider(provider).some((credential) => credential.type === "oauth")) { return this.getApiKey(provider, sessionId, options); diff --git a/packages/coding-agent/src/core/custom-commands/loader.ts b/packages/coding-agent/src/core/custom-commands/loader.ts index b87b28082..c2f2504a3 100644 --- a/packages/coding-agent/src/core/custom-commands/loader.ts +++ b/packages/coding-agent/src/core/custom-commands/loader.ts @@ -11,6 +11,7 @@ import * as typebox from "@sinclair/typebox"; import { getAgentDir, getConfigDirs } from "../../config"; import * as piCodingAgent from "../../index"; import { execCommand } from "../exec"; +import { logger } from "../logger"; import { createReviewCommand } from "./bundled/review"; import { createWorktreeCommand } from "./bundled/wt"; import type { @@ -110,7 +111,8 @@ export function discoverCustomCommands(options: DiscoverCustomCommandsOptions = let entries: Dirent[]; try { entries = readdirSync(commandsDir, { withFileTypes: true }); - } catch { + } catch (error) { + logger.warn("Failed to read custom commands directory", { path: commandsDir, error: String(error) }); continue; } for (const entry of entries) { diff --git a/packages/coding-agent/src/core/custom-tools/loader.ts b/packages/coding-agent/src/core/custom-tools/loader.ts index 75a4a7b7b..810606434 100644 --- a/packages/coding-agent/src/core/custom-tools/loader.ts +++ b/packages/coding-agent/src/core/custom-tools/loader.ts @@ -5,11 +5,11 @@ * to avoid import resolution issues with custom tools loaded from user directories. */ -import * as os from "node:os"; import * as path from "node:path"; import * as typebox from "@sinclair/typebox"; import { toolCapability } from "../../capability/tool"; import { type CustomTool, loadCapability } from "../../discovery"; +import { expandPath } from "../../discovery/helpers"; import * as piCodingAgent from "../../index"; import { theme } from "../../modes/interactive/theme/theme"; import type { ExecOptions } from "../exec"; @@ -19,23 +19,6 @@ import { logger } from "../logger"; import { getAllPluginToolPaths } from "../plugins/loader"; import type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from "./types"; -const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; - -function normalizeUnicodeSpaces(str: string): string { - return str.replace(UNICODE_SPACES, " "); -} - -function expandPath(p: string): string { - const normalized = normalizeUnicodeSpaces(p); - if (normalized.startsWith("~/")) { - return path.join(os.homedir(), normalized.slice(2)); - } - if (normalized.startsWith("~")) { - return path.join(os.homedir(), normalized.slice(1)); - } - return normalized; -} - /** * Resolve tool path. * - Absolute paths used as-is diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 563ad28f5..f61bded9e 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -3,13 +3,12 @@ */ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; -import { homedir } from "node:os"; import * as path from "node:path"; import type { KeyId } from "@oh-my-pi/pi-tui"; import * as TypeBox from "@sinclair/typebox"; import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module"; import { loadCapability } from "../../discovery"; -import { getExtensionNameFromPath } from "../../discovery/helpers"; +import { expandPath, getExtensionNameFromPath } from "../../discovery/helpers"; import * as piCodingAgent from "../../index"; import { createEventBus, type EventBus } from "../event-bus"; import type { ExecOptions } from "../exec"; @@ -27,23 +26,6 @@ import type { ToolDefinition, } from "./types"; -const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; - -function normalizeUnicodeSpaces(str: string): string { - return str.replace(UNICODE_SPACES, " "); -} - -function expandPath(p: string): string { - const normalized = normalizeUnicodeSpaces(p); - if (normalized.startsWith("~/")) { - return path.join(homedir(), normalized.slice(2)); - } - if (normalized.startsWith("~")) { - return path.join(homedir(), normalized.slice(1)); - } - return normalized; -} - function resolvePath(extPath: string, cwd: string): string { const expanded = expandPath(extPath); if (path.isAbsolute(expanded)) { @@ -291,7 +273,8 @@ function readExtensionManifest(packageJsonPath: string): ExtensionManifest | nul return manifest; } return null; - } catch { + } catch (error) { + logger.warn("Failed to read extension manifest", { path: packageJsonPath, error: String(error) }); return null; } } @@ -370,7 +353,8 @@ function discoverExtensionsInDir(dir: string): string[] { } } } - } catch { + } catch (error) { + logger.warn("Failed to discover extensions in directory", { path: dir, error: String(error) }); return []; } diff --git a/packages/coding-agent/src/core/hooks/loader.ts b/packages/coding-agent/src/core/hooks/loader.ts index 79c04bd0b..d6692a143 100644 --- a/packages/coding-agent/src/core/hooks/loader.ts +++ b/packages/coding-agent/src/core/hooks/loader.ts @@ -2,12 +2,12 @@ * Hook loader - loads TypeScript hook modules using native Bun import. */ -import * as os from "node:os"; import * as path from "node:path"; import * as typebox from "@sinclair/typebox"; import { hookCapability } from "../../capability/hook"; import type { Hook } from "../../discovery"; import { loadCapability } from "../../discovery"; +import { expandPath } from "../../discovery/helpers"; import * as piCodingAgent from "../../index"; import { logger } from "../logger"; import type { HookMessage } from "../messages"; @@ -84,23 +84,6 @@ export interface LoadHooksResult { errors: Array<{ path: string; error: string }>; } -const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; - -function normalizeUnicodeSpaces(str: string): string { - return str.replace(UNICODE_SPACES, " "); -} - -function expandPath(p: string): string { - const normalized = normalizeUnicodeSpaces(p); - if (normalized.startsWith("~/")) { - return path.join(os.homedir(), normalized.slice(2)); - } - if (normalized.startsWith("~")) { - return path.join(os.homedir(), normalized.slice(1)); - } - return normalized; -} - /** * Resolve hook path. * - Absolute paths used as-is diff --git a/packages/coding-agent/src/core/keybindings.ts b/packages/coding-agent/src/core/keybindings.ts index b2a979cc2..4a2c52f84 100644 --- a/packages/coding-agent/src/core/keybindings.ts +++ b/packages/coding-agent/src/core/keybindings.ts @@ -10,6 +10,7 @@ import { setEditorKeybindings, } from "@oh-my-pi/pi-tui"; import { getAgentDir } from "../config"; +import { logger } from "./logger"; /** * Application-level actions (coding agent specific). @@ -136,7 +137,8 @@ export class KeybindingsManager { if (!existsSync(path)) return {}; try { return JSON.parse(readFileSync(path, "utf-8")); - } catch { + } catch (error) { + logger.warn("Failed to parse keybindings config", { path, error: String(error) }); return {}; } } diff --git a/packages/coding-agent/src/core/logger.ts b/packages/coding-agent/src/core/logger.ts index 8e76152fa..3a3bd0432 100644 --- a/packages/coding-agent/src/core/logger.ts +++ b/packages/coding-agent/src/core/logger.ts @@ -10,11 +10,10 @@ import { homedir } from "node:os"; import { join } from "node:path"; import winston from "winston"; import DailyRotateFile from "winston-daily-rotate-file"; -import { CONFIG_DIR_NAME } from "../config"; /** Get the logs directory (~/.omp/logs/) */ function getLogsDir(): string { - return join(homedir(), CONFIG_DIR_NAME, "logs"); + return join(homedir(), ".omp", "logs"); } /** Ensure logs directory exists */ diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index 3058f5c02..63f1e3218 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -1,6 +1,7 @@ import { join, resolve } from "node:path"; import Handlebars from "handlebars"; import { CONFIG_DIR_NAME, getPromptsDir } from "../config"; +import { logger } from "./logger"; /** * Represents a prompt template loaded from a markdown file @@ -448,12 +449,12 @@ async function loadTemplatesFromDir( source: sourceStr, }); } - } catch (_error) { - // Silently skip files that can't be read + } catch (error) { + logger.warn("Failed to load prompt template", { path: fullPath, error: String(error) }); } } - } catch (_error) { - // Silently skip directories that can't be read + } catch (error) { + logger.warn("Failed to scan prompt templates directory", { dir, error: String(error) }); } return templates; diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index c9c2651fe..e327376ff 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -28,7 +28,7 @@ import { join } from "node:path"; import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core"; -import type { Message, Model } from "@oh-my-pi/pi-ai"; +import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai"; import type { Component } from "@oh-my-pi/pi-tui"; import chalk from "chalk"; // Import discovery to register all providers on startup @@ -631,6 +631,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} // Clamp to model capabilities if (!model || !model.reasoning) { thinkingLevel = "off"; + } else if (thinkingLevel === "xhigh" && !supportsXhigh(model)) { + thinkingLevel = "high"; } let skills: Skill[]; @@ -1021,8 +1023,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} }); lspServers = result.servers; time("warmupLspServers"); - } catch { - // Ignore warmup errors + } catch (error) { + logger.warn("LSP server warmup failed", { cwd, error: String(error) }); } } diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index ee80c7947..73abc4a28 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -7,6 +7,7 @@ import type { SourceMeta } from "../capability/types"; import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery"; import { loadCapability } from "../discovery"; import { parseFrontmatter } from "../discovery/helpers"; +import { logger } from "./logger"; import type { SkillsSettings } from "./settings-manager"; // Re-export SkillFrontmatter for backward compatibility @@ -67,8 +68,8 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills source: options.source, }); } - } catch { - // Skip invalid skills + } catch (error) { + logger.warn("Failed to load skill", { path: skillFile, error: String(error) }); } } @@ -131,8 +132,8 @@ function scanDirectoryForSkills(dir: string): LoadSkillsResult { source: "custom", }); } - } catch { - // Skip invalid skills + } catch (error) { + logger.warn("Failed to load skill", { path: skillFile, error: String(error) }); } } diff --git a/packages/coding-agent/src/core/tools/exa/mcp-client.ts b/packages/coding-agent/src/core/tools/exa/mcp-client.ts index 12e087794..7793c086e 100644 --- a/packages/coding-agent/src/core/tools/exa/mcp-client.ts +++ b/packages/coding-agent/src/core/tools/exa/mcp-client.ts @@ -292,8 +292,8 @@ export async function fetchMCPToolSchema( mcpSchemaCache.set(cacheKey, tool); return tool; } - } catch { - // Fall through to return null + } catch (error) { + logger.warn("Failed to fetch MCP tool schema", { mcpToolName, isWebsetsTool, error: String(error) }); } return null; } diff --git a/packages/coding-agent/src/core/tools/task/agents.ts b/packages/coding-agent/src/core/tools/task/agents.ts index 9bd68e6b8..e4485a6d4 100644 --- a/packages/coding-agent/src/core/tools/task/agents.ts +++ b/packages/coding-agent/src/core/tools/task/agents.ts @@ -4,6 +4,7 @@ * Agents are embedded at build time via Bun's import with { type: "text" }. */ +import { parseAgentFields, parseFrontmatter } from "../../../discovery/helpers"; import exploreMd from "../../../prompts/agents/explore.md" with { type: "text" }; // Embed agent markdown files at build time import agentFrontmatterTemplate from "../../../prompts/agents/frontmatter.md" with { type: "text" }; @@ -18,6 +19,7 @@ interface AgentFrontmatter { description: string; spawns?: string; model?: string; + thinkingLevel?: string; } interface EmbeddedAgentDef { @@ -71,80 +73,19 @@ const EMBEDDED_AGENTS: { name: string; content: string }[] = EMBEDDED_AGENT_DEFS content: buildAgentContent(def), })); -/** - * Parse YAML frontmatter from markdown content. - */ -function parseFrontmatter(content: string): { frontmatter: Record; body: string } { - const frontmatter: Record = {}; - const normalized = content.replace(/\r\n/g, "\n"); - - if (!normalized.startsWith("---")) { - return { frontmatter, body: normalized }; - } - - const endIndex = normalized.indexOf("\n---", 3); - if (endIndex === -1) { - return { frontmatter, body: normalized }; - } - - const frontmatterBlock = normalized.slice(4, endIndex); - const body = normalized.slice(endIndex + 4).trim(); - - for (const line of frontmatterBlock.split("\n")) { - const match = line.match(/^([\w-]+):\s*(.*)$/); - if (match) { - let value = match[2].trim(); - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - frontmatter[match[1]] = value; - } - } - - return { frontmatter, body }; -} - /** * Parse an agent from embedded content. */ function parseAgent(fileName: string, content: string, source: AgentSource): AgentDefinition | null { const { frontmatter, body } = parseFrontmatter(content); + const fields = parseAgentFields(frontmatter); - if (!frontmatter.name || !frontmatter.description) { + if (!fields) { return null; } - const tools = frontmatter.tools - ?.split(",") - .map((t) => t.trim()) - .filter(Boolean); - - // Parse spawns field - let spawns: string[] | "*" | undefined; - if (frontmatter.spawns !== undefined) { - const spawnsRaw = frontmatter.spawns.trim(); - if (spawnsRaw === "*") { - spawns = "*"; - } else if (spawnsRaw) { - spawns = spawnsRaw - .split(",") - .map((s) => s.trim()) - .filter(Boolean); - if (spawns.length === 0) spawns = undefined; - } - } - - // Backward compat: infer spawns: "*" when tools includes "task" - if (spawns === undefined && tools?.includes("task")) { - spawns = "*"; - } - return { - name: frontmatter.name, - description: frontmatter.description, - tools: tools && tools.length > 0 ? tools : undefined, - spawns, - model: frontmatter.model, + ...fields, systemPrompt: body, source, filePath: `embedded:${fileName}`, diff --git a/packages/coding-agent/src/core/tools/task/commands.ts b/packages/coding-agent/src/core/tools/task/commands.ts index 469329b0a..b0e3e14c3 100644 --- a/packages/coding-agent/src/core/tools/task/commands.ts +++ b/packages/coding-agent/src/core/tools/task/commands.ts @@ -7,6 +7,7 @@ import * as path from "node:path"; import { type SlashCommand, slashCommandCapability } from "../../../capability/slash-command"; import { loadCapability } from "../../../discovery"; +import { parseFrontmatter } from "../../../discovery/helpers"; // Embed command markdown files at build time import initMd from "../../../prompts/agents/init.md" with { type: "text" }; @@ -27,37 +28,10 @@ export interface WorkflowCommand { filePath: string; } -/** - * Parse YAML frontmatter from markdown content. - */ -function parseFrontmatter(content: string): { frontmatter: Record; body: string } { - const frontmatter: Record = {}; - const normalized = content.replace(/\r\n/g, "\n"); - - if (!normalized.startsWith("---")) { - return { frontmatter, body: normalized }; - } - - const endIndex = normalized.indexOf("\n---", 3); - if (endIndex === -1) { - return { frontmatter, body: normalized }; - } - - const frontmatterBlock = normalized.slice(4, endIndex); - const body = normalized.slice(endIndex + 4).trim(); - - for (const line of frontmatterBlock.split("\n")) { - const match = line.match(/^([\w-]+):\s*(.*)$/); - if (match) { - let value = match[2].trim(); - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - frontmatter[match[1]] = value; - } - } - - return { frontmatter, body }; +/** Extract string value from frontmatter field */ +function getString(frontmatter: Record, key: string): string { + const value = frontmatter[key]; + return typeof value === "string" ? value : ""; } /** Cache for bundled commands */ @@ -79,7 +53,7 @@ export function loadBundledCommands(): WorkflowCommand[] { commands.push({ name: cmdName, - description: frontmatter.description || "", + description: getString(frontmatter, "description"), instructions: body, source: "bundled", filePath: `embedded:${name}`, @@ -115,7 +89,7 @@ export async function discoverCommands(cwd: string): Promise commands.push({ name: cmd.name, - description: frontmatter.description || "", + description: getString(frontmatter, "description"), instructions: body, source, filePath: cmd.path, diff --git a/packages/coding-agent/src/core/tools/task/discovery.ts b/packages/coding-agent/src/core/tools/task/discovery.ts index 46e2e3a10..1f7831f45 100644 --- a/packages/coding-agent/src/core/tools/task/discovery.ts +++ b/packages/coding-agent/src/core/tools/task/discovery.ts @@ -15,6 +15,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { findAllNearestProjectConfigDirs, getConfigDirs } from "../../../config"; +import { parseAgentFields, parseFrontmatter } from "../../../discovery/helpers"; import { loadBundledAgents } from "./agents"; import type { AgentDefinition, AgentSource } from "./types"; @@ -24,40 +25,6 @@ export interface DiscoveryResult { projectAgentsDir: string | null; } -/** - * Parse YAML frontmatter from markdown content. - */ -function parseFrontmatter(content: string): { frontmatter: Record; body: string } { - const frontmatter: Record = {}; - const normalized = content.replace(/\r\n/g, "\n"); - - if (!normalized.startsWith("---")) { - return { frontmatter, body: normalized }; - } - - const endIndex = normalized.indexOf("\n---", 3); - if (endIndex === -1) { - return { frontmatter, body: normalized }; - } - - const frontmatterBlock = normalized.slice(4, endIndex); - const body = normalized.slice(endIndex + 4).trim(); - - for (const line of frontmatterBlock.split("\n")) { - const match = line.match(/^([\w-]+):\s*(.*)$/); - if (match) { - let value = match[2].trim(); - // Strip quotes - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - frontmatter[match[1]] = value; - } - } - - return { frontmatter, body }; -} - /** * Load agents from a directory. */ @@ -95,43 +62,14 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[] } const { frontmatter, body } = parseFrontmatter(content); + const fields = parseAgentFields(frontmatter); - // Require name and description - if (!frontmatter.name || !frontmatter.description) { + if (!fields) { continue; } - const tools = frontmatter.tools - ?.split(",") - .map((t) => t.trim()) - .filter(Boolean); - - // Parse spawns field - let spawns: string[] | "*" | undefined; - if (frontmatter.spawns !== undefined) { - const spawnsRaw = frontmatter.spawns.trim(); - if (spawnsRaw === "*") { - spawns = "*"; - } else if (spawnsRaw) { - spawns = spawnsRaw - .split(",") - .map((s) => s.trim()) - .filter(Boolean); - if (spawns.length === 0) spawns = undefined; - } - } - - // Backward compat: infer spawns: "*" when tools includes "task" - if (spawns === undefined && tools?.includes("task")) { - spawns = "*"; - } - agents.push({ - name: frontmatter.name, - description: frontmatter.description, - tools: tools && tools.length > 0 ? tools : undefined, - spawns, - model: frontmatter.model, + ...fields, systemPrompt: body, source, filePath, diff --git a/packages/coding-agent/src/core/tools/task/executor.ts b/packages/coding-agent/src/core/tools/task/executor.ts index da6df11e3..546fa3d32 100644 --- a/packages/coding-agent/src/core/tools/task/executor.ts +++ b/packages/coding-agent/src/core/tools/task/executor.ts @@ -4,7 +4,7 @@ * Runs each subagent in a Bun Worker and forwards AgentEvents for progress tracking. */ -import type { AgentEvent } from "@oh-my-pi/pi-agent-core"; +import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import type { AuthStorage } from "../../auth-storage"; import type { EventBus } from "../../event-bus"; import { callTool } from "../../mcp/client"; @@ -18,6 +18,7 @@ import { type AgentProgress, MAX_OUTPUT_BYTES, MAX_OUTPUT_LINES, + type ReviewFinding, type SingleResult, TASK_SUBAGENT_EVENT_CHANNEL, TASK_SUBAGENT_PROGRESS_CHANNEL, @@ -39,6 +40,7 @@ export interface ExecutorOptions { taskId: string; context?: string; modelOverride?: string; + thinkingLevel?: ThinkingLevel; outputSchema?: unknown; enableLsp?: boolean; signal?: AbortSignal; @@ -183,8 +185,20 @@ function extractMCPToolMetadata(mcpManager: MCPManager): MCPToolMetadata[] { * Run a single agent in a worker. */ export async function runSubprocess(options: ExecutorOptions): Promise { - const { cwd, agent, task, index, taskId, context, modelOverride, outputSchema, enableLsp, signal, onProgress } = - options; + const { + cwd, + agent, + task, + index, + taskId, + context, + modelOverride, + thinkingLevel, + outputSchema, + enableLsp, + signal, + onProgress, + } = options; const startTime = Date.now(); // Initialize progress @@ -578,6 +592,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise 0 && + completeData && + typeof completeData === "object" && + !Array.isArray(completeData) + ) { + const record = completeData as Record; + if (!("findings" in record)) { + completeData = { ...record, findings: reportFindings }; + } + } try { rawOutput = JSON.stringify(completeData, null, 2) ?? "null"; } catch (err) { diff --git a/packages/coding-agent/src/core/tools/task/index.ts b/packages/coding-agent/src/core/tools/task/index.ts index 69c7e1558..197d27000 100644 --- a/packages/coding-agent/src/core/tools/task/index.ts +++ b/packages/coding-agent/src/core/tools/task/index.ts @@ -156,6 +156,11 @@ export async function createTaskTool( const shouldInheritSessionModel = model === undefined && isDefaultModelAlias(agent.model); const sessionModel = shouldInheritSessionModel ? session.getActiveModelString?.() : undefined; const modelOverride = model ?? sessionModel ?? session.getModelString?.(); + const thinkingLevelOverride = agent.thinkingLevel; + + // Output schema priority: agent frontmatter > params > inherited from parent session + const schemaOverridden = outputSchema !== undefined && agent.output !== undefined; + const effectiveOutputSchema = agent.output ?? outputSchema ?? session.outputSchema; // Handle empty or missing tasks if (!params.tasks || params.tasks.length === 0) { @@ -345,7 +350,8 @@ export async function createTaskTool( taskId: task.taskId, context: undefined, // Already prepended above modelOverride, - outputSchema, + thinkingLevel: thinkingLevelOverride, + outputSchema: effectiveOutputSchema, sessionFile, persistArtifacts: !!artifactsDir, artifactsDir: effectiveArtifactsDir, @@ -399,9 +405,12 @@ export async function createTaskTool( const outputIds = results.map((r) => r.taskId); const outputHint = outputIds.length > 0 ? `\n\nUse output tool for full logs: output ids ${outputIds.join(", ")}` : ""; + const schemaNote = schemaOverridden + ? `\n\nNote: Agent '${agentName}' has a fixed output schema; your 'output' parameter was ignored.\nRequired schema: ${JSON.stringify(agent.output)}` + : ""; const summary = `${successCount}/${results.length} succeeded [${formatDuration( totalDuration, - )}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}`; + )}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}${schemaNote}`; // Cleanup temp directory if used if (tempArtifactsDir) { diff --git a/packages/coding-agent/src/core/tools/task/render.ts b/packages/coding-agent/src/core/tools/task/render.ts index 1d89487fb..e1e136be7 100644 --- a/packages/coding-agent/src/core/tools/task/render.ts +++ b/packages/coding-agent/src/core/tools/task/render.ts @@ -369,18 +369,28 @@ function renderAgentProgress( } for (const [toolName, dataArray] of Object.entries(progress.extractedToolData)) { + // Handle report_finding with tree formatting + if (toolName === "report_finding" && (dataArray as ReportFindingDetails[]).length > 0) { + const findings = dataArray as ReportFindingDetails[]; + lines.push(`${continuePrefix}${formatFindingSummary(findings, theme)}`); + lines.push(...renderFindings(findings, continuePrefix, expanded, theme)); + continue; + } + const handler = subprocessToolRegistry.getHandler(toolName); if (handler?.renderInline) { - // Show last few items inline - const recentData = (dataArray as unknown[]).slice(-3); + const displayCount = expanded ? (dataArray as unknown[]).length : 3; + const recentData = (dataArray as unknown[]).slice(-displayCount); for (const data of recentData) { const component = handler.renderInline(data, theme); if (component instanceof Text) { lines.push(`${continuePrefix}${component.getText()}`); } } - if (dataArray.length > 3) { - lines.push(`${continuePrefix}${theme.fg("dim", formatMoreItems(dataArray.length - 3, "item", theme))}`); + if ((dataArray as unknown[]).length > displayCount) { + lines.push( + `${continuePrefix}${theme.fg("dim", formatMoreItems((dataArray as unknown[]).length - displayCount, "item", theme))}`, + ); } } } @@ -436,7 +446,6 @@ function renderReviewResult( lines.push(`${continuePrefix}${formatFindingSummary(findings, theme)}`); if (findings.length > 0) { - lines.push(`${continuePrefix}`); // Spacing lines.push(...renderFindings(findings, continuePrefix, expanded, theme)); } @@ -453,11 +462,14 @@ function renderFindings( theme: Theme, ): string[] { const lines: string[] = []; - const displayCount = expanded ? findings.length : Math.min(3, findings.length); + + // Sort by priority (lower = more severe) when collapsed to show most important first + const sortedFindings = expanded ? findings : [...findings].sort((a, b) => a.priority - b.priority); + const displayCount = expanded ? sortedFindings.length : Math.min(3, sortedFindings.length); for (let i = 0; i < displayCount; i++) { - const finding = findings[i]; - const isLastFinding = i === displayCount - 1 && (expanded || findings.length <= 3); + const finding = sortedFindings[i]; + const isLastFinding = i === displayCount - 1 && (expanded || sortedFindings.length <= 3); const findingPrefix = isLastFinding ? theme.tree.last : theme.tree.branch; const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `; @@ -538,14 +550,12 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool return lines; } if (reportFindingData && reportFindingData.length > 0) { - lines.push( - `${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg( - "dim", - "Review summary missing (complete not called)", - )}`, - ); + const hasCompleteData = completeData && completeData.length > 0; + const message = hasCompleteData + ? "Review verdict missing expected fields" + : "Review incomplete (complete not called)"; + lines.push(`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg("dim", message)}`); lines.push(`${continuePrefix}${formatFindingSummary(reportFindingData, theme)}`); - lines.push(`${continuePrefix}`); // Spacing lines.push(...renderFindings(reportFindingData, continuePrefix, expanded, theme)); return lines; } diff --git a/packages/coding-agent/src/core/tools/task/types.ts b/packages/coding-agent/src/core/tools/task/types.ts index 6735729a6..ca64cecfd 100644 --- a/packages/coding-agent/src/core/tools/task/types.ts +++ b/packages/coding-agent/src/core/tools/task/types.ts @@ -1,3 +1,4 @@ +import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import type { Usage } from "@oh-my-pi/pi-ai"; import { type Static, Type } from "@sinclair/typebox"; @@ -106,6 +107,8 @@ export interface AgentDefinition { tools?: string[]; spawns?: string[] | "*"; model?: string; + thinkingLevel?: ThinkingLevel; + output?: unknown; source: AgentSource; filePath?: string; } diff --git a/packages/coding-agent/src/core/tools/task/worker-protocol.ts b/packages/coding-agent/src/core/tools/task/worker-protocol.ts index 3553eb2e2..49e3dc54a 100644 --- a/packages/coding-agent/src/core/tools/task/worker-protocol.ts +++ b/packages/coding-agent/src/core/tools/task/worker-protocol.ts @@ -1,4 +1,4 @@ -import type { AgentEvent } from "@oh-my-pi/pi-agent-core"; +import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import type { SerializedAuthStorage } from "../../auth-storage"; import type { SerializedModelRegistry } from "../../model-registry"; @@ -43,6 +43,7 @@ export interface SubagentWorkerStartPayload { task: string; systemPrompt: string; model?: string; + thinkingLevel?: ThinkingLevel; toolNames?: string[]; outputSchema?: unknown; enableLsp?: boolean; diff --git a/packages/coding-agent/src/core/tools/task/worker.ts b/packages/coding-agent/src/core/tools/task/worker.ts index 66c47d2f4..966768c88 100644 --- a/packages/coding-agent/src/core/tools/task/worker.ts +++ b/packages/coding-agent/src/core/tools/task/worker.ts @@ -287,7 +287,8 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload): const mcpProxyTools = payload.mcpTools?.map(createMCPProxyTool) ?? []; // Resolve model override (equivalent to CLI's parseModelPattern with --model) - const { model, thinkingLevel } = resolveModelOverride(payload.model, modelRegistry); + const { model, thinkingLevel: modelThinkingLevel } = resolveModelOverride(payload.model, modelRegistry); + const thinkingLevel = modelThinkingLevel ?? payload.thinkingLevel; // Create session manager (equivalent to CLI's --session or --no-session) const sessionManager = payload.sessionFile diff --git a/packages/coding-agent/src/core/tools/web-scrapers/huggingface.ts b/packages/coding-agent/src/core/tools/web-scrapers/huggingface.ts index 6efd30e87..2ccf5db1f 100644 --- a/packages/coding-agent/src/core/tools/web-scrapers/huggingface.ts +++ b/packages/coding-agent/src/core/tools/web-scrapers/huggingface.ts @@ -379,7 +379,7 @@ export const handleHuggingFace: SpecialHandler = async (url: string, timeout: nu default: return null; } - } catch (_err) { + } catch { return null; } }; diff --git a/packages/coding-agent/src/core/tools/web-scrapers/readthedocs.ts b/packages/coding-agent/src/core/tools/web-scrapers/readthedocs.ts index e87df5b0a..bbfc18225 100644 --- a/packages/coding-agent/src/core/tools/web-scrapers/readthedocs.ts +++ b/packages/coding-agent/src/core/tools/web-scrapers/readthedocs.ts @@ -95,7 +95,7 @@ export const handleReadTheDocs: SpecialHandler = async ( content = sourceResult.content; notes.push(`Fetched raw source from ${sourceUrl}`); } - } catch (_err) { + } catch { // Ignore errors, fall back to HTML } } diff --git a/packages/coding-agent/src/core/tools/web-scrapers/types.ts b/packages/coding-agent/src/core/tools/web-scrapers/types.ts index dd00779c4..90818f0d8 100644 --- a/packages/coding-agent/src/core/tools/web-scrapers/types.ts +++ b/packages/coding-agent/src/core/tools/web-scrapers/types.ts @@ -170,7 +170,7 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom } return { content, contentType, finalUrl, ok: true, status: response.status }; - } catch (_err) { + } catch { if (signal?.aborted) { return { content: "", contentType: "", finalUrl: url, ok: false }; } diff --git a/packages/coding-agent/src/core/tools/web-search/auth.ts b/packages/coding-agent/src/core/tools/web-search/auth.ts index a2ebc93a7..c8396cb34 100644 --- a/packages/coding-agent/src/core/tools/web-search/auth.ts +++ b/packages/coding-agent/src/core/tools/web-search/auth.ts @@ -14,6 +14,7 @@ import { buildBetaHeader, claudeCodeHeaders, claudeCodeVersion } from "@oh-my-pi import { getAgentDbPath, getConfigDirPaths } from "../../../config"; import { AgentStorage } from "../../agent-storage"; import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "../../auth-storage"; +import { logger } from "../../logger"; import { migrateJsonStorage } from "../../storage-migration"; import type { AnthropicAuthConfig, AnthropicOAuthCredential, ModelsJson } from "./types"; @@ -48,8 +49,8 @@ async function parseEnvFile(filePath: string): Promise> { result[key] = value; } - } catch { - // Ignore read errors + } catch (error) { + logger.warn("Failed to read .env file", { path: filePath, error: String(error) }); } return result; } @@ -82,7 +83,8 @@ async function readJson(filePath: string): Promise { if (!(await file.exists())) return null; const content = await file.text(); return JSON.parse(content) as T; - } catch { + } catch (error) { + logger.warn("Failed to parse JSON file", { path: filePath, error: String(error) }); return null; } } diff --git a/packages/coding-agent/src/discovery/codex.ts b/packages/coding-agent/src/discovery/codex.ts index 63d97e339..dffdacba6 100644 --- a/packages/coding-agent/src/discovery/codex.ts +++ b/packages/coding-agent/src/discovery/codex.ts @@ -29,6 +29,7 @@ import { slashCommandCapability } from "../capability/slash-command"; import type { CustomTool } from "../capability/tool"; import { toolCapability } from "../capability/tool"; import type { LoadContext, LoadResult } from "../capability/types"; +import { logger } from "../core/logger"; import { createSourceMeta, discoverExtensionModulePaths, @@ -117,7 +118,8 @@ async function loadTomlConfig(_ctx: LoadContext, path: string): Promise; - } catch (_err) { + } catch (error) { + logger.warn("Failed to parse TOML config", { path, error: String(error) }); return null; } } diff --git a/packages/coding-agent/src/discovery/helpers.ts b/packages/coding-agent/src/discovery/helpers.ts index 17d6332d6..9898587b8 100644 --- a/packages/coding-agent/src/discovery/helpers.ts +++ b/packages/coding-agent/src/discovery/helpers.ts @@ -2,11 +2,38 @@ * Shared helpers for discovery providers. */ +import { homedir } from "node:os"; import { join, resolve } from "node:path"; +import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import { parse as parseYAML } from "yaml"; import { readDirEntries, readFile } from "../capability/fs"; import type { Skill, SkillFrontmatter } from "../capability/skill"; import type { LoadContext, LoadResult, SourceMeta } from "../capability/types"; +import { logger } from "../core/logger"; + +const VALID_THINKING_LEVELS: readonly string[] = ["off", "minimal", "low", "medium", "high", "xhigh"]; +const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; + +/** + * Normalize unicode spaces to regular spaces. + */ +export function normalizeUnicodeSpaces(str: string): string { + return str.replace(UNICODE_SPACES, " "); +} + +/** + * Expand ~ to home directory and normalize unicode spaces. + */ +export function expandPath(p: string): string { + const normalized = normalizeUnicodeSpaces(p); + if (normalized.startsWith("~/")) { + return join(homedir(), normalized.slice(2)); + } + if (normalized.startsWith("~")) { + return join(homedir(), normalized.slice(1)); + } + return normalized; +} /** * Standard paths for each config source. @@ -117,14 +144,108 @@ export function parseFrontmatter(content: string): { const body = normalized.slice(endIndex + 4).trim(); try { - const frontmatter = parseYAML(raw) as Record | null; + // Replace tabs with spaces for YAML compatibility, use failsafe mode for robustness + const frontmatter = parseYAML(raw.replaceAll("\t", " "), { compat: "failsafe" }) as Record< + string, + unknown + > | null; return { frontmatter: frontmatter ?? {}, body, raw }; - } catch { - // Fallback to empty frontmatter on parse error + } catch (error) { + logger.warn("Failed to parse YAML frontmatter", { error: String(error) }); return { frontmatter: {}, body, raw }; } } +/** + * Parse thinking level from frontmatter. + * Supports keys: thinkingLevel, thinking-level, thinking + */ +export function parseThinkingLevel(frontmatter: Record): ThinkingLevel | undefined { + const raw = frontmatter.thinkingLevel ?? frontmatter["thinking-level"] ?? frontmatter.thinking; + if (typeof raw === "string" && VALID_THINKING_LEVELS.includes(raw)) { + return raw as ThinkingLevel; + } + return undefined; +} + +/** + * Parse a comma-separated string into an array of trimmed, non-empty strings. + */ +export function parseCSV(value: string): string[] { + return value + .split(",") + .map((s) => s.trim()) + .filter(Boolean); +} + +/** + * Parse a value that may be an array of strings or a comma-separated string. + * Returns undefined if the result would be empty. + */ +export function parseArrayOrCSV(value: unknown): string[] | undefined { + if (Array.isArray(value)) { + const filtered = value.filter((item): item is string => typeof item === "string"); + return filtered.length > 0 ? filtered : undefined; + } + if (typeof value === "string") { + const parsed = parseCSV(value); + return parsed.length > 0 ? parsed : undefined; + } + return undefined; +} + +/** Parsed agent fields from frontmatter (excludes source/filePath/systemPrompt) */ +export interface ParsedAgentFields { + name: string; + description: string; + tools?: string[]; + spawns?: string[] | "*"; + model?: string; + output?: unknown; + thinkingLevel?: ThinkingLevel; +} + +/** + * Parse agent fields from frontmatter. + * Returns null if required fields (name, description) are missing. + */ +export function parseAgentFields(frontmatter: Record): ParsedAgentFields | null { + const name = typeof frontmatter.name === "string" ? frontmatter.name : undefined; + const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined; + + if (!name || !description) { + return null; + } + + const tools = parseArrayOrCSV(frontmatter.tools); + + // Parse spawns field (array, "*", or CSV) + let spawns: string[] | "*" | undefined; + if (frontmatter.spawns === "*") { + spawns = "*"; + } else if (typeof frontmatter.spawns === "string") { + const trimmed = frontmatter.spawns.trim(); + if (trimmed === "*") { + spawns = "*"; + } else { + spawns = parseArrayOrCSV(trimmed); + } + } else { + spawns = parseArrayOrCSV(frontmatter.spawns); + } + + // Backward compat: infer spawns: "*" when tools includes "task" + if (spawns === undefined && tools?.includes("task")) { + spawns = "*"; + } + + const output = frontmatter.output !== undefined ? frontmatter.output : undefined; + const model = typeof frontmatter.model === "string" ? frontmatter.model : undefined; + const thinkingLevel = parseThinkingLevel(frontmatter); + + return { name, description, tools, spawns, model, output, thinkingLevel }; +} + export async function loadSkillsFromDir( _ctx: LoadContext, options: { diff --git a/packages/coding-agent/src/migrations.ts b/packages/coding-agent/src/migrations.ts index 14f1b6f5f..68c913da7 100644 --- a/packages/coding-agent/src/migrations.ts +++ b/packages/coding-agent/src/migrations.ts @@ -8,6 +8,7 @@ import chalk from "chalk"; import { getAgentDbPath, getAgentDir, getBinDir } from "./config"; import { AgentStorage } from "./core/agent-storage"; import type { AuthCredential } from "./core/auth-storage"; +import { logger } from "./core/logger"; /** * Migrate PI_* environment variables to OMP_* equivalents. @@ -55,8 +56,8 @@ export function migrateAuthToAgentDb(): string[] { providers.push(provider); } renameSync(oauthPath, `${oauthPath}.migrated`); - } catch { - // Skip on error + } catch (error) { + logger.warn("Failed to migrate oauth.json", { path: oauthPath, error: String(error) }); } } @@ -75,8 +76,8 @@ export function migrateAuthToAgentDb(): string[] { delete settings.apiKeys; writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); } - } catch { - // Skip on error + } catch (error) { + logger.warn("Failed to migrate settings.json apiKeys", { path: settingsPath, error: String(error) }); } } @@ -105,7 +106,8 @@ export function migrateSessionsFromAgentRoot(): void { files = readdirSync(agentDir) .filter((f) => f.endsWith(".jsonl")) .map((f) => join(agentDir, f)); - } catch { + } catch (error) { + logger.warn("Failed to read agent directory for session migration", { path: agentDir, error: String(error) }); return; } @@ -137,8 +139,8 @@ export function migrateSessionsFromAgentRoot(): void { if (existsSync(newPath)) continue; // Skip if target exists renameSync(file, newPath); - } catch { - // Skip files that can't be migrated + } catch (error) { + logger.warn("Failed to migrate session file", { path: file, error: String(error) }); } } } @@ -168,8 +170,8 @@ function migrateToolsToBin(): void { try { renameSync(oldPath, newPath); movedAny = true; - } catch { - // Ignore errors + } catch (error) { + logger.warn("Failed to migrate binary", { from: oldPath, to: newPath, error: String(error) }); } } else { // Target exists, just delete the old one diff --git a/packages/coding-agent/src/modes/interactive/components/extensions/state-manager.ts b/packages/coding-agent/src/modes/interactive/components/extensions/state-manager.ts index e3ea3df9c..ad24eb741 100644 --- a/packages/coding-agent/src/modes/interactive/components/extensions/state-manager.ts +++ b/packages/coding-agent/src/modes/interactive/components/extensions/state-manager.ts @@ -13,6 +13,7 @@ import type { Skill } from "../../../../capability/skill"; import type { SlashCommand } from "../../../../capability/slash-command"; import type { CustomTool } from "../../../../capability/tool"; import type { SourceMeta } from "../../../../capability/types"; +import { logger } from "../../../../core/logger"; import { disableProvider, enableProvider, @@ -105,8 +106,8 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P getDescription: (s) => s.frontmatter?.description, getTrigger: (s) => s.frontmatter?.globs?.join(", "), }); - } catch { - // Capability may not be registered + } catch (error) { + logger.warn("Failed to load skills capability", { error: String(error) }); } // Load rules @@ -116,8 +117,8 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P getDescription: (r) => r.description, getTrigger: (r) => r.globs?.join(", ") || (r.alwaysApply ? "always" : undefined), }); - } catch { - // Capability may not be registered + } catch (error) { + logger.warn("Failed to load rules capability", { error: String(error) }); } // Load custom tools @@ -126,8 +127,8 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P addItems(tools.all, "tool", { getDescription: (t) => t.description, }); - } catch { - // Capability may not be registered + } catch (error) { + logger.warn("Failed to load tools capability", { error: String(error) }); } // Load extension modules @@ -135,8 +136,8 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P const modules = await loadCapability("extension-modules", loadOpts); const nativeModules = modules.all.filter((module) => module._source.provider === "native"); addItems(nativeModules, "extension-module"); - } catch { - // Capability may not be registered + } catch (error) { + logger.warn("Failed to load extension-modules capability", { error: String(error) }); } // Load MCP servers @@ -178,8 +179,8 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P raw: server, }); } - } catch { - // Capability may not be registered + } catch (error) { + logger.warn("Failed to load mcps capability", { error: String(error) }); } // Load prompts @@ -189,8 +190,8 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P getDescription: () => undefined, getTrigger: (p) => `/prompts:${p.name}`, }); - } catch { - // Capability may not be registered + } catch (error) { + logger.warn("Failed to load prompts capability", { error: String(error) }); } // Load slash commands @@ -200,8 +201,8 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P getDescription: () => undefined, getTrigger: (c) => `/${c.name}`, }); - } catch { - // Capability may not be registered + } catch (error) { + logger.warn("Failed to load slash-commands capability", { error: String(error) }); } // Load hooks @@ -243,8 +244,8 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P raw: hook, }); } - } catch { - // Capability may not be registered + } catch (error) { + logger.warn("Failed to load hooks capability", { error: String(error) }); } // Load context files @@ -288,8 +289,8 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P raw: file, }); } - } catch { - // Capability may not be registered + } catch (error) { + logger.warn("Failed to load context-files capability", { error: String(error) }); } return extensions; diff --git a/packages/coding-agent/src/prompts/agents/frontmatter.md b/packages/coding-agent/src/prompts/agents/frontmatter.md index 0c1246aeb..02060c0d6 100644 --- a/packages/coding-agent/src/prompts/agents/frontmatter.md +++ b/packages/coding-agent/src/prompts/agents/frontmatter.md @@ -3,5 +3,6 @@ name: {{name}} description: {{description}} {{#if spawns}}spawns: {{spawns}} {{/if}}{{#if model}}model: {{model}} +{{/if}}{{#if thinkingLevel}}thinkingLevel: {{thinkingLevel}} {{/if}}--- {{body}} diff --git a/packages/coding-agent/src/prompts/agents/reviewer.md b/packages/coding-agent/src/prompts/agents/reviewer.md index b5af14045..70962bd3b 100644 --- a/packages/coding-agent/src/prompts/agents/reviewer.md +++ b/packages/coding-agent/src/prompts/agents/reviewer.md @@ -4,6 +4,33 @@ description: Code review specialist for quality and security analysis tools: read, grep, find, ls, bash, report_finding spawns: explore, task model: pi/slow, gpt-5.2-codex, gpt-5.2, codex, gpt +output: + properties: + overall_correctness: + enum: [correct, incorrect] + explanation: + type: string + confidence: + type: number + optionalProperties: + findings: + elements: + properties: + title: + type: string + body: + type: string + priority: + type: number + confidence: + type: number + file_path: + type: string + line_start: + type: number + line_end: + type: number + required: [overall_correctness, explanation, confidence] --- You are a senior engineer reviewing a proposed code change. Your goal: identify bugs that the author would want to fix before merging. @@ -64,11 +91,12 @@ Each `report_finding` requires: - `file_path`: Absolute path - `line_start`, `line_end`: Range ≤10 lines, must overlap the diff -Final `complete` call: +Final `complete` call (payload goes under `data`): -- `overall_correctness`: "correct" (no bugs/blockers) or "incorrect" -- `explanation`: 1-3 sentences -- `confidence`: 0.0-1.0 +- `data.overall_correctness`: "correct" (no bugs/blockers) or "incorrect" +- `data.explanation`: Plain text, 1-3 sentences summarizing your verdict. Do NOT include JSON, do NOT repeat findings here (they're already captured via `report_finding`). +- `data.confidence`: 0.0-1.0 +- `data.findings`: Optional; MUST omit (it is populated from `report_finding` calls) Correctness judgment ignores non-blocking issues (style, docs, nits). diff --git a/packages/coding-agent/src/prompts/tools/task.md b/packages/coding-agent/src/prompts/tools/task.md index 704bc4676..fb6791b2e 100644 --- a/packages/coding-agent/src/prompts/tools/task.md +++ b/packages/coding-agent/src/prompts/tools/task.md @@ -12,12 +12,14 @@ If you discussed requirements, plans, schemas, or decisions with the user, you M ## Available Agents {{#list agents prefix="- " join="\n"}} -{{name}}: {{description}} (Tools: {{default (join tools ", ") "All tools"}}) +{{name}}: {{description}} (Tools: {{default (join tools ", ") "All tools"}}{{#if output}}, Output: structured{{/if}}) {{/list}} {{#if moreAgents}} ...and {{moreAgents}} more agents {{/if}} +Agents with "Output: structured" have a fixed schema enforced via frontmatter; your `output` parameter will be ignored for these agents. + ## When NOT to Use - Reading a specific file path → Use Read tool instead diff --git a/packages/tui/test/image-test.ts b/packages/tui/test/image-test.ts index 60b9f73e2..82a04fda9 100644 --- a/packages/tui/test/image-test.ts +++ b/packages/tui/test/image-test.ts @@ -14,7 +14,7 @@ let imageBuffer: Uint8Array; try { const file = Bun.file(testImagePath); imageBuffer = await file.bytes(); -} catch (_e) { +} catch { console.error(`Failed to load image: ${testImagePath}`); console.error("Usage: bun test/image-test.ts [path-to-image.png]"); process.exit(1);