@oh-my-pi/pi-coding-agent 4.2.3 → 4.3.1

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 +23 -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 +13 -1
  6. package/src/core/cursor/exec-bridge.ts +234 -0
  7. package/src/core/custom-commands/loader.ts +3 -1
  8. package/src/core/custom-tools/loader.ts +1 -18
  9. package/src/core/extensions/loader.ts +5 -21
  10. package/src/core/hooks/loader.ts +1 -18
  11. package/src/core/keybindings.ts +3 -1
  12. package/src/core/logger.ts +1 -2
  13. package/src/core/model-resolver.ts +1 -0
  14. package/src/core/prompt-templates.ts +5 -4
  15. package/src/core/sdk.ts +17 -4
  16. package/src/core/skills.ts +5 -4
  17. package/src/core/tools/edit-diff.ts +44 -21
  18. package/src/core/tools/exa/mcp-client.ts +2 -2
  19. package/src/core/tools/task/agents.ts +5 -64
  20. package/src/core/tools/task/commands.ts +7 -33
  21. package/src/core/tools/task/discovery.ts +4 -66
  22. package/src/core/tools/task/executor.ts +32 -3
  23. package/src/core/tools/task/index.ts +11 -2
  24. package/src/core/tools/task/render.ts +25 -15
  25. package/src/core/tools/task/types.ts +3 -0
  26. package/src/core/tools/task/worker-protocol.ts +2 -1
  27. package/src/core/tools/task/worker.ts +2 -1
  28. package/src/core/tools/web-scrapers/huggingface.ts +1 -1
  29. package/src/core/tools/web-scrapers/readthedocs.ts +1 -1
  30. package/src/core/tools/web-scrapers/types.ts +1 -1
  31. package/src/core/tools/web-search/auth.ts +5 -3
  32. package/src/discovery/codex.ts +3 -1
  33. package/src/discovery/helpers.ts +124 -3
  34. package/src/migrations.ts +11 -9
  35. package/src/modes/interactive/components/extensions/state-manager.ts +19 -18
  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
@@ -49,9 +49,9 @@ function countLeadingWhitespace(line: string): number {
49
49
  const char = line[i];
50
50
  if (char === " " || char === "\t") {
51
51
  count++;
52
- continue;
52
+ } else {
53
+ break;
53
54
  }
54
- break;
55
55
  }
56
56
  return count;
57
57
  }
@@ -80,15 +80,16 @@ function computeRelativeIndentDepths(lines: string[]): number[] {
80
80
  });
81
81
  }
82
82
 
83
- function normalizeLinesForMatch(lines: string[]): string[] {
84
- const indentDepths = computeRelativeIndentDepths(lines);
83
+ function normalizeLinesForMatch(lines: string[], includeDepth = true): string[] {
84
+ const indentDepths = includeDepth ? computeRelativeIndentDepths(lines) : null;
85
85
  return lines.map((line, index) => {
86
86
  const trimmed = line.trim();
87
+ const prefix = indentDepths ? `${indentDepths[index]}|` : "|";
87
88
  if (trimmed.length === 0) {
88
- return `${indentDepths[index]}|`;
89
+ return prefix;
89
90
  }
90
91
  const collapsed = trimmed.replace(/[ \t]+/g, " ");
91
- return `${indentDepths[index]}|${collapsed}`;
92
+ return `${prefix}${collapsed}`;
92
93
  });
93
94
  }
94
95
 
@@ -148,22 +149,14 @@ function computeLineOffsets(lines: string[]): number[] {
148
149
  return offsets;
149
150
  }
150
151
 
151
- function findBestFuzzyMatch(
152
- content: string,
153
- target: string,
152
+ function findBestFuzzyMatchCore(
153
+ contentLines: string[],
154
+ targetLines: string[],
155
+ offsets: number[],
154
156
  threshold: number,
157
+ includeDepth: boolean,
155
158
  ): { best?: EditMatch; aboveThresholdCount: number } {
156
- const contentLines = content.split("\n");
157
- const targetLines = target.split("\n");
158
- if (targetLines.length === 0 || target.length === 0) {
159
- return { aboveThresholdCount: 0 };
160
- }
161
- if (targetLines.length > contentLines.length) {
162
- return { aboveThresholdCount: 0 };
163
- }
164
-
165
- const targetNormalized = normalizeLinesForMatch(targetLines);
166
- const offsets = computeLineOffsets(contentLines);
159
+ const targetNormalized = normalizeLinesForMatch(targetLines, includeDepth);
167
160
 
168
161
  let best: EditMatch | undefined;
169
162
  let bestScore = -1;
@@ -171,7 +164,7 @@ function findBestFuzzyMatch(
171
164
 
172
165
  for (let start = 0; start <= contentLines.length - targetLines.length; start++) {
173
166
  const windowLines = contentLines.slice(start, start + targetLines.length);
174
- const windowNormalized = normalizeLinesForMatch(windowLines);
167
+ const windowNormalized = normalizeLinesForMatch(windowLines, includeDepth);
175
168
  let score = 0;
176
169
  for (let i = 0; i < targetLines.length; i++) {
177
170
  score += similarityScore(targetNormalized[i], windowNormalized[i]);
@@ -196,6 +189,36 @@ function findBestFuzzyMatch(
196
189
  return { best, aboveThresholdCount };
197
190
  }
198
191
 
192
+ const FALLBACK_THRESHOLD = 0.8;
193
+
194
+ function findBestFuzzyMatch(
195
+ content: string,
196
+ target: string,
197
+ threshold: number,
198
+ ): { best?: EditMatch; aboveThresholdCount: number } {
199
+ const contentLines = content.split("\n");
200
+ const targetLines = target.split("\n");
201
+ if (targetLines.length === 0 || target.length === 0) {
202
+ return { aboveThresholdCount: 0 };
203
+ }
204
+ if (targetLines.length > contentLines.length) {
205
+ return { aboveThresholdCount: 0 };
206
+ }
207
+
208
+ const offsets = computeLineOffsets(contentLines);
209
+
210
+ let result = findBestFuzzyMatchCore(contentLines, targetLines, offsets, threshold, true);
211
+
212
+ if (result.best && result.best.confidence < threshold && result.best.confidence >= FALLBACK_THRESHOLD) {
213
+ const noDepthResult = findBestFuzzyMatchCore(contentLines, targetLines, offsets, threshold, false);
214
+ if (noDepthResult.best && noDepthResult.best.confidence > result.best.confidence) {
215
+ result = noDepthResult;
216
+ }
217
+ }
218
+
219
+ return result;
220
+ }
221
+
199
222
  export function findEditMatch(
200
223
  content: string,
201
224
  target: string,
@@ -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
  }
@@ -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}`,
@@ -7,6 +7,7 @@
7
7
  import * as path from "node:path";
8
8
  import { type SlashCommand, slashCommandCapability } from "../../../capability/slash-command";
9
9
  import { loadCapability } from "../../../discovery";
10
+ import { parseFrontmatter } from "../../../discovery/helpers";
10
11
 
11
12
  // Embed command markdown files at build time
12
13
  import initMd from "../../../prompts/agents/init.md" with { type: "text" };
@@ -27,37 +28,10 @@ export interface WorkflowCommand {
27
28
  filePath: string;
28
29
  }
29
30
 
30
- /**
31
- * Parse YAML frontmatter from markdown content.
32
- */
33
- function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
34
- const frontmatter: Record<string, string> = {};
35
- const normalized = content.replace(/\r\n/g, "\n");
36
-
37
- if (!normalized.startsWith("---")) {
38
- return { frontmatter, body: normalized };
39
- }
40
-
41
- const endIndex = normalized.indexOf("\n---", 3);
42
- if (endIndex === -1) {
43
- return { frontmatter, body: normalized };
44
- }
45
-
46
- const frontmatterBlock = normalized.slice(4, endIndex);
47
- const body = normalized.slice(endIndex + 4).trim();
48
-
49
- for (const line of frontmatterBlock.split("\n")) {
50
- const match = line.match(/^([\w-]+):\s*(.*)$/);
51
- if (match) {
52
- let value = match[2].trim();
53
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
54
- value = value.slice(1, -1);
55
- }
56
- frontmatter[match[1]] = value;
57
- }
58
- }
59
-
60
- return { frontmatter, body };
31
+ /** Extract string value from frontmatter field */
32
+ function getString(frontmatter: Record<string, unknown>, key: string): string {
33
+ const value = frontmatter[key];
34
+ return typeof value === "string" ? value : "";
61
35
  }
62
36
 
63
37
  /** Cache for bundled commands */
@@ -79,7 +53,7 @@ export function loadBundledCommands(): WorkflowCommand[] {
79
53
 
80
54
  commands.push({
81
55
  name: cmdName,
82
- description: frontmatter.description || "",
56
+ description: getString(frontmatter, "description"),
83
57
  instructions: body,
84
58
  source: "bundled",
85
59
  filePath: `embedded:${name}`,
@@ -115,7 +89,7 @@ export async function discoverCommands(cwd: string): Promise<WorkflowCommand[]>
115
89
 
116
90
  commands.push({
117
91
  name: cmd.name,
118
- description: frontmatter.description || "",
92
+ description: getString(frontmatter, "description"),
119
93
  instructions: body,
120
94
  source,
121
95
  filePath: cmd.path,
@@ -15,6 +15,7 @@
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
19
  import { loadBundledAgents } from "./agents";
19
20
  import type { AgentDefinition, AgentSource } from "./types";
20
21
 
@@ -24,40 +25,6 @@ export interface DiscoveryResult {
24
25
  projectAgentsDir: string | null;
25
26
  }
26
27
 
27
- /**
28
- * Parse YAML frontmatter from markdown content.
29
- */
30
- function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
31
- const frontmatter: Record<string, string> = {};
32
- const normalized = content.replace(/\r\n/g, "\n");
33
-
34
- if (!normalized.startsWith("---")) {
35
- return { frontmatter, body: normalized };
36
- }
37
-
38
- const endIndex = normalized.indexOf("\n---", 3);
39
- if (endIndex === -1) {
40
- return { frontmatter, body: normalized };
41
- }
42
-
43
- const frontmatterBlock = normalized.slice(4, endIndex);
44
- const body = normalized.slice(endIndex + 4).trim();
45
-
46
- for (const line of frontmatterBlock.split("\n")) {
47
- const match = line.match(/^([\w-]+):\s*(.*)$/);
48
- if (match) {
49
- let value = match[2].trim();
50
- // Strip quotes
51
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
52
- value = value.slice(1, -1);
53
- }
54
- frontmatter[match[1]] = value;
55
- }
56
- }
57
-
58
- return { frontmatter, body };
59
- }
60
-
61
28
  /**
62
29
  * Load agents from a directory.
63
30
  */
@@ -95,43 +62,14 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
95
62
  }
96
63
 
97
64
  const { frontmatter, body } = parseFrontmatter(content);
65
+ const fields = parseAgentFields(frontmatter);
98
66
 
99
- // Require name and description
100
- if (!frontmatter.name || !frontmatter.description) {
67
+ if (!fields) {
101
68
  continue;
102
69
  }
103
70
 
104
- const tools = frontmatter.tools
105
- ?.split(",")
106
- .map((t) => t.trim())
107
- .filter(Boolean);
108
-
109
- // Parse spawns field
110
- let spawns: string[] | "*" | undefined;
111
- if (frontmatter.spawns !== undefined) {
112
- const spawnsRaw = frontmatter.spawns.trim();
113
- if (spawnsRaw === "*") {
114
- spawns = "*";
115
- } else if (spawnsRaw) {
116
- spawns = spawnsRaw
117
- .split(",")
118
- .map((s) => s.trim())
119
- .filter(Boolean);
120
- if (spawns.length === 0) spawns = undefined;
121
- }
122
- }
123
-
124
- // Backward compat: infer spawns: "*" when tools includes "task"
125
- if (spawns === undefined && tools?.includes("task")) {
126
- spawns = "*";
127
- }
128
-
129
71
  agents.push({
130
- name: frontmatter.name,
131
- description: frontmatter.description,
132
- tools: tools && tools.length > 0 ? tools : undefined,
133
- spawns,
134
- model: frontmatter.model,
72
+ ...fields,
135
73
  systemPrompt: body,
136
74
  source,
137
75
  filePath,
@@ -4,7 +4,7 @@
4
4
  * Runs each subagent in a Bun Worker and forwards AgentEvents for progress tracking.
5
5
  */
6
6
 
7
- import type { AgentEvent } from "@oh-my-pi/pi-agent-core";
7
+ import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
8
  import type { AuthStorage } from "../../auth-storage";
9
9
  import type { EventBus } from "../../event-bus";
10
10
  import { callTool } from "../../mcp/client";
@@ -18,6 +18,7 @@ import {
18
18
  type AgentProgress,
19
19
  MAX_OUTPUT_BYTES,
20
20
  MAX_OUTPUT_LINES,
21
+ type ReviewFinding,
21
22
  type SingleResult,
22
23
  TASK_SUBAGENT_EVENT_CHANNEL,
23
24
  TASK_SUBAGENT_PROGRESS_CHANNEL,
@@ -39,6 +40,7 @@ export interface ExecutorOptions {
39
40
  taskId: string;
40
41
  context?: string;
41
42
  modelOverride?: string;
43
+ thinkingLevel?: ThinkingLevel;
42
44
  outputSchema?: unknown;
43
45
  enableLsp?: boolean;
44
46
  signal?: AbortSignal;
@@ -183,8 +185,20 @@ function extractMCPToolMetadata(mcpManager: MCPManager): MCPToolMetadata[] {
183
185
  * Run a single agent in a worker.
184
186
  */
185
187
  export async function runSubprocess(options: ExecutorOptions): Promise<SingleResult> {
186
- const { cwd, agent, task, index, taskId, context, modelOverride, outputSchema, enableLsp, signal, onProgress } =
187
- options;
188
+ const {
189
+ cwd,
190
+ agent,
191
+ task,
192
+ index,
193
+ taskId,
194
+ context,
195
+ modelOverride,
196
+ thinkingLevel,
197
+ outputSchema,
198
+ enableLsp,
199
+ signal,
200
+ onProgress,
201
+ } = options;
188
202
  const startTime = Date.now();
189
203
 
190
204
  // Initialize progress
@@ -578,6 +592,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
578
592
  task: fullTask,
579
593
  systemPrompt: agent.systemPrompt,
580
594
  model: resolvedModel,
595
+ thinkingLevel,
581
596
  toolNames,
582
597
  outputSchema,
583
598
  sessionFile,
@@ -751,6 +766,20 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
751
766
  // Not valid JSON, keep as string
752
767
  }
753
768
  }
769
+ // Special case: merge report_finding data into review output for parent visibility
770
+ const reportFindings = progress.extractedToolData?.report_finding as ReviewFinding[] | undefined;
771
+ if (
772
+ Array.isArray(reportFindings) &&
773
+ reportFindings.length > 0 &&
774
+ completeData &&
775
+ typeof completeData === "object" &&
776
+ !Array.isArray(completeData)
777
+ ) {
778
+ const record = completeData as Record<string, unknown>;
779
+ if (!("findings" in record)) {
780
+ completeData = { ...record, findings: reportFindings };
781
+ }
782
+ }
754
783
  try {
755
784
  rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
756
785
  } catch (err) {
@@ -156,6 +156,11 @@ export async function createTaskTool(
156
156
  const shouldInheritSessionModel = model === undefined && isDefaultModelAlias(agent.model);
157
157
  const sessionModel = shouldInheritSessionModel ? session.getActiveModelString?.() : undefined;
158
158
  const modelOverride = model ?? sessionModel ?? session.getModelString?.();
159
+ const thinkingLevelOverride = agent.thinkingLevel;
160
+
161
+ // Output schema priority: agent frontmatter > params > inherited from parent session
162
+ const schemaOverridden = outputSchema !== undefined && agent.output !== undefined;
163
+ const effectiveOutputSchema = agent.output ?? outputSchema ?? session.outputSchema;
159
164
 
160
165
  // Handle empty or missing tasks
161
166
  if (!params.tasks || params.tasks.length === 0) {
@@ -345,7 +350,8 @@ export async function createTaskTool(
345
350
  taskId: task.taskId,
346
351
  context: undefined, // Already prepended above
347
352
  modelOverride,
348
- outputSchema,
353
+ thinkingLevel: thinkingLevelOverride,
354
+ outputSchema: effectiveOutputSchema,
349
355
  sessionFile,
350
356
  persistArtifacts: !!artifactsDir,
351
357
  artifactsDir: effectiveArtifactsDir,
@@ -399,9 +405,12 @@ export async function createTaskTool(
399
405
  const outputIds = results.map((r) => r.taskId);
400
406
  const outputHint =
401
407
  outputIds.length > 0 ? `\n\nUse output tool for full logs: output ids ${outputIds.join(", ")}` : "";
408
+ const schemaNote = schemaOverridden
409
+ ? `\n\nNote: Agent '${agentName}' has a fixed output schema; your 'output' parameter was ignored.\nRequired schema: ${JSON.stringify(agent.output)}`
410
+ : "";
402
411
  const summary = `${successCount}/${results.length} succeeded [${formatDuration(
403
412
  totalDuration,
404
- )}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}`;
413
+ )}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}${schemaNote}`;
405
414
 
406
415
  // Cleanup temp directory if used
407
416
  if (tempArtifactsDir) {
@@ -369,18 +369,28 @@ function renderAgentProgress(
369
369
  }
370
370
 
371
371
  for (const [toolName, dataArray] of Object.entries(progress.extractedToolData)) {
372
+ // Handle report_finding with tree formatting
373
+ if (toolName === "report_finding" && (dataArray as ReportFindingDetails[]).length > 0) {
374
+ const findings = dataArray as ReportFindingDetails[];
375
+ lines.push(`${continuePrefix}${formatFindingSummary(findings, theme)}`);
376
+ lines.push(...renderFindings(findings, continuePrefix, expanded, theme));
377
+ continue;
378
+ }
379
+
372
380
  const handler = subprocessToolRegistry.getHandler(toolName);
373
381
  if (handler?.renderInline) {
374
- // Show last few items inline
375
- const recentData = (dataArray as unknown[]).slice(-3);
382
+ const displayCount = expanded ? (dataArray as unknown[]).length : 3;
383
+ const recentData = (dataArray as unknown[]).slice(-displayCount);
376
384
  for (const data of recentData) {
377
385
  const component = handler.renderInline(data, theme);
378
386
  if (component instanceof Text) {
379
387
  lines.push(`${continuePrefix}${component.getText()}`);
380
388
  }
381
389
  }
382
- if (dataArray.length > 3) {
383
- lines.push(`${continuePrefix}${theme.fg("dim", formatMoreItems(dataArray.length - 3, "item", theme))}`);
390
+ if ((dataArray as unknown[]).length > displayCount) {
391
+ lines.push(
392
+ `${continuePrefix}${theme.fg("dim", formatMoreItems((dataArray as unknown[]).length - displayCount, "item", theme))}`,
393
+ );
384
394
  }
385
395
  }
386
396
  }
@@ -436,7 +446,6 @@ function renderReviewResult(
436
446
  lines.push(`${continuePrefix}${formatFindingSummary(findings, theme)}`);
437
447
 
438
448
  if (findings.length > 0) {
439
- lines.push(`${continuePrefix}`); // Spacing
440
449
  lines.push(...renderFindings(findings, continuePrefix, expanded, theme));
441
450
  }
442
451
 
@@ -453,11 +462,14 @@ function renderFindings(
453
462
  theme: Theme,
454
463
  ): string[] {
455
464
  const lines: string[] = [];
456
- const displayCount = expanded ? findings.length : Math.min(3, findings.length);
465
+
466
+ // Sort by priority (lower = more severe) when collapsed to show most important first
467
+ const sortedFindings = expanded ? findings : [...findings].sort((a, b) => a.priority - b.priority);
468
+ const displayCount = expanded ? sortedFindings.length : Math.min(3, sortedFindings.length);
457
469
 
458
470
  for (let i = 0; i < displayCount; i++) {
459
- const finding = findings[i];
460
- const isLastFinding = i === displayCount - 1 && (expanded || findings.length <= 3);
471
+ const finding = sortedFindings[i];
472
+ const isLastFinding = i === displayCount - 1 && (expanded || sortedFindings.length <= 3);
461
473
  const findingPrefix = isLastFinding ? theme.tree.last : theme.tree.branch;
462
474
  const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
463
475
 
@@ -538,14 +550,12 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
538
550
  return lines;
539
551
  }
540
552
  if (reportFindingData && reportFindingData.length > 0) {
541
- lines.push(
542
- `${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg(
543
- "dim",
544
- "Review summary missing (complete not called)",
545
- )}`,
546
- );
553
+ const hasCompleteData = completeData && completeData.length > 0;
554
+ const message = hasCompleteData
555
+ ? "Review verdict missing expected fields"
556
+ : "Review incomplete (complete not called)";
557
+ lines.push(`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg("dim", message)}`);
547
558
  lines.push(`${continuePrefix}${formatFindingSummary(reportFindingData, theme)}`);
548
- lines.push(`${continuePrefix}`); // Spacing
549
559
  lines.push(...renderFindings(reportFindingData, continuePrefix, expanded, theme));
550
560
  return lines;
551
561
  }
@@ -1,3 +1,4 @@
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
2
  import type { Usage } from "@oh-my-pi/pi-ai";
2
3
  import { type Static, Type } from "@sinclair/typebox";
3
4
 
@@ -106,6 +107,8 @@ export interface AgentDefinition {
106
107
  tools?: string[];
107
108
  spawns?: string[] | "*";
108
109
  model?: string;
110
+ thinkingLevel?: ThinkingLevel;
111
+ output?: unknown;
109
112
  source: AgentSource;
110
113
  filePath?: string;
111
114
  }
@@ -1,4 +1,4 @@
1
- import type { AgentEvent } from "@oh-my-pi/pi-agent-core";
1
+ import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { SerializedAuthStorage } from "../../auth-storage";
3
3
  import type { SerializedModelRegistry } from "../../model-registry";
4
4
 
@@ -43,6 +43,7 @@ export interface SubagentWorkerStartPayload {
43
43
  task: string;
44
44
  systemPrompt: string;
45
45
  model?: string;
46
+ thinkingLevel?: ThinkingLevel;
46
47
  toolNames?: string[];
47
48
  outputSchema?: unknown;
48
49
  enableLsp?: boolean;
@@ -287,7 +287,8 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
287
287
  const mcpProxyTools = payload.mcpTools?.map(createMCPProxyTool) ?? [];
288
288
 
289
289
  // Resolve model override (equivalent to CLI's parseModelPattern with --model)
290
- const { model, thinkingLevel } = resolveModelOverride(payload.model, modelRegistry);
290
+ const { model, thinkingLevel: modelThinkingLevel } = resolveModelOverride(payload.model, modelRegistry);
291
+ const thinkingLevel = modelThinkingLevel ?? payload.thinkingLevel;
291
292
 
292
293
  // Create session manager (equivalent to CLI's --session or --no-session)
293
294
  const sessionManager = payload.sessionFile
@@ -379,7 +379,7 @@ export const handleHuggingFace: SpecialHandler = async (url: string, timeout: nu
379
379
  default:
380
380
  return null;
381
381
  }
382
- } catch (_err) {
382
+ } catch {
383
383
  return null;
384
384
  }
385
385
  };
@@ -95,7 +95,7 @@ export const handleReadTheDocs: SpecialHandler = async (
95
95
  content = sourceResult.content;
96
96
  notes.push(`Fetched raw source from ${sourceUrl}`);
97
97
  }
98
- } catch (_err) {
98
+ } catch {
99
99
  // Ignore errors, fall back to HTML
100
100
  }
101
101
  }
@@ -170,7 +170,7 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom
170
170
  }
171
171
 
172
172
  return { content, contentType, finalUrl, ok: true, status: response.status };
173
- } catch (_err) {
173
+ } catch {
174
174
  if (signal?.aborted) {
175
175
  return { content: "", contentType: "", finalUrl: url, ok: false };
176
176
  }
@@ -14,6 +14,7 @@ import { buildBetaHeader, claudeCodeHeaders, claudeCodeVersion } from "@oh-my-pi
14
14
  import { getAgentDbPath, getConfigDirPaths } from "../../../config";
15
15
  import { AgentStorage } from "../../agent-storage";
16
16
  import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "../../auth-storage";
17
+ import { logger } from "../../logger";
17
18
  import { migrateJsonStorage } from "../../storage-migration";
18
19
  import type { AnthropicAuthConfig, AnthropicOAuthCredential, ModelsJson } from "./types";
19
20
 
@@ -48,8 +49,8 @@ async function parseEnvFile(filePath: string): Promise<Record<string, string>> {
48
49
 
49
50
  result[key] = value;
50
51
  }
51
- } catch {
52
- // Ignore read errors
52
+ } catch (error) {
53
+ logger.warn("Failed to read .env file", { path: filePath, error: String(error) });
53
54
  }
54
55
  return result;
55
56
  }
@@ -82,7 +83,8 @@ async function readJson<T>(filePath: string): Promise<T | null> {
82
83
  if (!(await file.exists())) return null;
83
84
  const content = await file.text();
84
85
  return JSON.parse(content) as T;
85
- } catch {
86
+ } catch (error) {
87
+ logger.warn("Failed to parse JSON file", { path: filePath, error: String(error) });
86
88
  return null;
87
89
  }
88
90
  }