@oh-my-pi/pi-coding-agent 1.340.0 → 2.0.1337

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 (153) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +5 -3
  5. package/src/cli/args.ts +13 -6
  6. package/src/cli/file-processor.ts +3 -3
  7. package/src/cli/list-models.ts +2 -2
  8. package/src/cli/plugin-cli.ts +1 -1
  9. package/src/cli/session-picker.ts +2 -2
  10. package/src/cli.ts +1 -1
  11. package/src/config.ts +3 -3
  12. package/src/core/agent-session.ts +189 -29
  13. package/src/core/bash-executor.ts +50 -10
  14. package/src/core/compaction/branch-summarization.ts +5 -5
  15. package/src/core/compaction/compaction.ts +3 -3
  16. package/src/core/compaction/index.ts +3 -3
  17. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  18. package/src/core/custom-commands/index.ts +15 -0
  19. package/src/core/custom-commands/loader.ts +232 -0
  20. package/src/core/custom-commands/types.ts +112 -0
  21. package/src/core/custom-tools/index.ts +3 -3
  22. package/src/core/custom-tools/loader.ts +10 -8
  23. package/src/core/custom-tools/types.ts +11 -6
  24. package/src/core/custom-tools/wrapper.ts +2 -1
  25. package/src/core/exec.ts +22 -12
  26. package/src/core/export-html/index.ts +5 -5
  27. package/src/core/file-mentions.ts +54 -0
  28. package/src/core/hooks/index.ts +5 -5
  29. package/src/core/hooks/loader.ts +21 -16
  30. package/src/core/hooks/runner.ts +6 -6
  31. package/src/core/hooks/tool-wrapper.ts +2 -2
  32. package/src/core/hooks/types.ts +12 -15
  33. package/src/core/index.ts +6 -6
  34. package/src/core/logger.ts +112 -0
  35. package/src/core/mcp/client.ts +3 -3
  36. package/src/core/mcp/config.ts +1 -1
  37. package/src/core/mcp/index.ts +12 -12
  38. package/src/core/mcp/loader.ts +2 -2
  39. package/src/core/mcp/manager.ts +6 -6
  40. package/src/core/mcp/tool-bridge.ts +3 -3
  41. package/src/core/mcp/transports/http.ts +1 -1
  42. package/src/core/mcp/transports/index.ts +2 -2
  43. package/src/core/mcp/transports/stdio.ts +1 -1
  44. package/src/core/messages.ts +22 -0
  45. package/src/core/model-registry.ts +2 -2
  46. package/src/core/model-resolver.ts +103 -2
  47. package/src/core/plugins/doctor.ts +1 -1
  48. package/src/core/plugins/index.ts +6 -6
  49. package/src/core/plugins/installer.ts +4 -4
  50. package/src/core/plugins/loader.ts +4 -9
  51. package/src/core/plugins/manager.ts +5 -5
  52. package/src/core/plugins/paths.ts +3 -3
  53. package/src/core/sdk.ts +127 -52
  54. package/src/core/session-manager.ts +123 -20
  55. package/src/core/settings-manager.ts +106 -22
  56. package/src/core/skills.ts +5 -5
  57. package/src/core/slash-commands.ts +60 -45
  58. package/src/core/system-prompt.ts +6 -6
  59. package/src/core/title-generator.ts +94 -0
  60. package/src/core/tools/bash.ts +33 -157
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +5 -5
  63. package/src/core/tools/edit.ts +60 -9
  64. package/src/core/tools/exa/company.ts +3 -3
  65. package/src/core/tools/exa/index.ts +16 -17
  66. package/src/core/tools/exa/linkedin.ts +3 -3
  67. package/src/core/tools/exa/mcp-client.ts +9 -9
  68. package/src/core/tools/exa/render.ts +5 -5
  69. package/src/core/tools/exa/researcher.ts +3 -3
  70. package/src/core/tools/exa/search.ts +6 -5
  71. package/src/core/tools/exa/types.ts +5 -6
  72. package/src/core/tools/exa/websets.ts +3 -3
  73. package/src/core/tools/find.ts +3 -3
  74. package/src/core/tools/grep.ts +6 -5
  75. package/src/core/tools/index.ts +114 -40
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +204 -108
  78. package/src/core/tools/lsp/config.ts +709 -35
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +432 -30
  81. package/src/core/tools/lsp/render.ts +2 -2
  82. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  83. package/src/core/tools/lsp/types.ts +5 -0
  84. package/src/core/tools/lsp/utils.ts +1 -1
  85. package/src/core/tools/notebook.ts +1 -1
  86. package/src/core/tools/output.ts +175 -0
  87. package/src/core/tools/read.ts +7 -7
  88. package/src/core/tools/renderers.ts +92 -13
  89. package/src/core/tools/review.ts +268 -0
  90. package/src/core/tools/task/agents.ts +1 -1
  91. package/src/core/tools/task/bundled-agents/explore.md +1 -1
  92. package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
  93. package/src/core/tools/task/discovery.ts +2 -2
  94. package/src/core/tools/task/executor.ts +145 -28
  95. package/src/core/tools/task/index.ts +78 -30
  96. package/src/core/tools/task/model-resolver.ts +72 -13
  97. package/src/core/tools/task/parallel.ts +1 -1
  98. package/src/core/tools/task/render.ts +219 -30
  99. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  100. package/src/core/tools/task/types.ts +36 -2
  101. package/src/core/tools/web-fetch.ts +5 -3
  102. package/src/core/tools/web-search/auth.ts +1 -1
  103. package/src/core/tools/web-search/index.ts +17 -15
  104. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  105. package/src/core/tools/web-search/providers/exa.ts +3 -5
  106. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  107. package/src/core/tools/web-search/render.ts +3 -3
  108. package/src/core/tools/write.ts +70 -7
  109. package/src/index.ts +33 -17
  110. package/src/main.ts +60 -34
  111. package/src/migrations.ts +3 -3
  112. package/src/modes/index.ts +5 -5
  113. package/src/modes/interactive/components/armin.ts +1 -1
  114. package/src/modes/interactive/components/assistant-message.ts +1 -1
  115. package/src/modes/interactive/components/bash-execution.ts +4 -4
  116. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  117. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  118. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  119. package/src/modes/interactive/components/diff.ts +1 -1
  120. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  121. package/src/modes/interactive/components/footer.ts +5 -5
  122. package/src/modes/interactive/components/hook-editor.ts +2 -2
  123. package/src/modes/interactive/components/hook-input.ts +2 -2
  124. package/src/modes/interactive/components/hook-message.ts +3 -3
  125. package/src/modes/interactive/components/hook-selector.ts +2 -2
  126. package/src/modes/interactive/components/model-selector.ts +341 -41
  127. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  128. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  129. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  130. package/src/modes/interactive/components/session-selector.ts +24 -11
  131. package/src/modes/interactive/components/settings-defs.ts +51 -3
  132. package/src/modes/interactive/components/settings-selector.ts +13 -16
  133. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  134. package/src/modes/interactive/components/theme-selector.ts +2 -2
  135. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  136. package/src/modes/interactive/components/tool-execution.ts +44 -8
  137. package/src/modes/interactive/components/tree-selector.ts +5 -5
  138. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  139. package/src/modes/interactive/components/user-message.ts +1 -1
  140. package/src/modes/interactive/components/welcome.ts +42 -5
  141. package/src/modes/interactive/interactive-mode.ts +169 -48
  142. package/src/modes/interactive/theme/theme.ts +8 -7
  143. package/src/modes/print-mode.ts +4 -3
  144. package/src/modes/rpc/rpc-client.ts +4 -4
  145. package/src/modes/rpc/rpc-mode.ts +21 -11
  146. package/src/modes/rpc/rpc-types.ts +3 -3
  147. package/src/utils/changelog.ts +2 -2
  148. package/src/utils/clipboard.ts +1 -1
  149. package/src/utils/shell-snapshot.ts +218 -0
  150. package/src/utils/shell.ts +93 -13
  151. package/src/utils/tools-manager.ts +1 -1
  152. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  153. package/src/core/tools/exa/logger.ts +0 -56
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Output tool for reading agent/task outputs by ID.
3
+ *
4
+ * Resolves IDs like "reviewer_0" to artifact paths in the current session.
5
+ */
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
10
+ import type { TextContent } from "@oh-my-pi/pi-ai";
11
+ import { Type } from "@sinclair/typebox";
12
+ import type { SessionContext } from "./index";
13
+ import { getArtifactsDir } from "./task/artifacts";
14
+
15
+ const outputSchema = Type.Object({
16
+ ids: Type.Array(Type.String(), {
17
+ description: "Agent output IDs to read (e.g., ['reviewer_0', 'explore_1'])",
18
+ minItems: 1,
19
+ }),
20
+ format: Type.Optional(
21
+ Type.Union([Type.Literal("raw"), Type.Literal("json"), Type.Literal("stripped")], {
22
+ description: "Output format: raw (default), json (structured), stripped (no ANSI)",
23
+ }),
24
+ ),
25
+ });
26
+
27
+ /** Metadata for a single output file */
28
+ interface OutputEntry {
29
+ id: string;
30
+ path: string;
31
+ lineCount: number;
32
+ charCount: number;
33
+ }
34
+
35
+ export interface OutputToolDetails {
36
+ outputs: OutputEntry[];
37
+ notFound?: string[];
38
+ availableIds?: string[];
39
+ }
40
+
41
+ /** Strip ANSI escape codes from text */
42
+ function stripAnsi(text: string): string {
43
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
44
+ }
45
+
46
+ /** List available output IDs in artifacts directory */
47
+ function listAvailableOutputs(artifactsDir: string): string[] {
48
+ try {
49
+ const files = fs.readdirSync(artifactsDir);
50
+ return files.filter((f) => f.endsWith(".out.md")).map((f) => f.replace(".out.md", ""));
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+
56
+ /** Format byte count for display */
57
+ function formatBytes(bytes: number): string {
58
+ if (bytes < 1024) return `${bytes}B`;
59
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
60
+ return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
61
+ }
62
+
63
+ export function createOutputTool(
64
+ _cwd: string,
65
+ sessionContext?: SessionContext,
66
+ ): AgentTool<typeof outputSchema, OutputToolDetails> {
67
+ return {
68
+ name: "output",
69
+ label: "Output",
70
+ description: `Read full agent/task output by ID.
71
+
72
+ Use when the Task tool's truncated preview isn't sufficient for your needs.
73
+ The Task tool already returns summaries with line/char counts in its result.
74
+
75
+ Parameters:
76
+ - ids: Array of output IDs (e.g., ["reviewer_0", "explore_1"])
77
+ - format: "raw" (default), "json" (structured object), or "stripped" (no ANSI codes)
78
+
79
+ Returns the full output content. For unknown IDs, returns an error with available IDs.
80
+
81
+ Example: { "ids": ["reviewer_0"] }`,
82
+ parameters: outputSchema,
83
+ execute: async (
84
+ _toolCallId: string,
85
+ params: { ids: string[]; format?: "raw" | "json" | "stripped" },
86
+ ): Promise<{ content: TextContent[]; details: OutputToolDetails }> => {
87
+ const sessionFile = sessionContext?.getSessionFile();
88
+
89
+ if (!sessionFile) {
90
+ return {
91
+ content: [{ type: "text", text: "No session - output artifacts unavailable" }],
92
+ details: { outputs: [], notFound: params.ids },
93
+ };
94
+ }
95
+
96
+ const artifactsDir = getArtifactsDir(sessionFile);
97
+ if (!artifactsDir || !fs.existsSync(artifactsDir)) {
98
+ return {
99
+ content: [{ type: "text", text: "No artifacts directory found" }],
100
+ details: { outputs: [], notFound: params.ids },
101
+ };
102
+ }
103
+
104
+ const outputs: OutputEntry[] = [];
105
+ const notFound: string[] = [];
106
+ const format = params.format ?? "raw";
107
+
108
+ for (const id of params.ids) {
109
+ const outputPath = path.join(artifactsDir, `${id}.out.md`);
110
+
111
+ if (!fs.existsSync(outputPath)) {
112
+ notFound.push(id);
113
+ continue;
114
+ }
115
+
116
+ const content = fs.readFileSync(outputPath, "utf-8");
117
+ outputs.push({
118
+ id,
119
+ path: outputPath,
120
+ lineCount: content.split("\n").length,
121
+ charCount: content.length,
122
+ });
123
+ }
124
+
125
+ // Error case: some IDs not found
126
+ if (notFound.length > 0) {
127
+ const available = listAvailableOutputs(artifactsDir);
128
+ const errorMsg =
129
+ available.length > 0
130
+ ? `Not found: ${notFound.join(", ")}\nAvailable: ${available.join(", ")}`
131
+ : `Not found: ${notFound.join(", ")}\nNo outputs available in current session`;
132
+
133
+ return {
134
+ content: [{ type: "text", text: errorMsg }],
135
+ details: { outputs, notFound, availableIds: available },
136
+ };
137
+ }
138
+
139
+ // Success: build response based on format
140
+ let contentText: string;
141
+
142
+ if (format === "json") {
143
+ const jsonData = outputs.map((o) => ({
144
+ id: o.id,
145
+ lineCount: o.lineCount,
146
+ charCount: o.charCount,
147
+ content: fs.readFileSync(o.path, "utf-8"),
148
+ }));
149
+ contentText = JSON.stringify(jsonData, null, 2);
150
+ } else {
151
+ // raw or stripped
152
+ const parts = outputs.map((o) => {
153
+ let content = fs.readFileSync(o.path, "utf-8");
154
+ if (format === "stripped") {
155
+ content = stripAnsi(content);
156
+ }
157
+ // Add header for multiple outputs
158
+ if (outputs.length > 1) {
159
+ return `=== ${o.id} (${o.lineCount} lines, ${formatBytes(o.charCount)}) ===\n${content}`;
160
+ }
161
+ return content;
162
+ });
163
+ contentText = parts.join("\n\n");
164
+ }
165
+
166
+ return {
167
+ content: [{ type: "text", text: contentText }],
168
+ details: { outputs },
169
+ };
170
+ },
171
+ };
172
+ }
173
+
174
+ /** Default output tool using process.cwd() - for backwards compatibility */
175
+ export const outputTool = createOutputTool(process.cwd());
@@ -1,13 +1,13 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { constants } from "node:fs";
3
+ import { access, readFile } from "node:fs/promises";
4
+ import { extname } from "node:path";
1
5
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
6
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
3
7
  import { Type } from "@sinclair/typebox";
4
- import { spawnSync } from "child_process";
5
- import { constants } from "fs";
6
- import { access, readFile } from "fs/promises";
7
- import { extname } from "path";
8
- import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
9
- import { resolveReadPath } from "./path-utils.js";
10
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
8
+ import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
9
+ import { resolveReadPath } from "./path-utils";
10
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate";
11
11
 
12
12
  // Document types convertible via markitdown
13
13
  const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
@@ -6,19 +6,20 @@
6
6
 
7
7
  import type { Component } from "@oh-my-pi/pi-tui";
8
8
  import { Text } from "@oh-my-pi/pi-tui";
9
- import type { Theme } from "../../modes/interactive/theme/theme.js";
10
- import type { RenderResultOptions } from "../custom-tools/types.js";
11
- import type { AskToolDetails } from "./ask.js";
12
- import type { FindToolDetails } from "./find.js";
13
- import type { GrepToolDetails } from "./grep.js";
14
- import type { LsToolDetails } from "./ls.js";
15
- import { renderCall as renderLspCall, renderResult as renderLspResult } from "./lsp/render.js";
16
- import type { LspToolDetails } from "./lsp/types.js";
17
- import type { NotebookToolDetails } from "./notebook.js";
18
- import { renderCall as renderTaskCall, renderResult as renderTaskResult } from "./task/render.js";
19
- import type { TaskToolDetails } from "./task/types.js";
20
- import { renderWebFetchCall, renderWebFetchResult, type WebFetchToolDetails } from "./web-fetch.js";
21
- import { renderWebSearchCall, renderWebSearchResult, type WebSearchRenderDetails } from "./web-search/render.js";
9
+ import type { Theme } from "../../modes/interactive/theme/theme";
10
+ import type { RenderResultOptions } from "../custom-tools/types";
11
+ import type { AskToolDetails } from "./ask";
12
+ import type { FindToolDetails } from "./find";
13
+ import type { GrepToolDetails } from "./grep";
14
+ import type { LsToolDetails } from "./ls";
15
+ import { renderCall as renderLspCall, renderResult as renderLspResult } from "./lsp/render";
16
+ import type { LspToolDetails } from "./lsp/types";
17
+ import type { NotebookToolDetails } from "./notebook";
18
+ import type { OutputToolDetails } from "./output";
19
+ import { renderCall as renderTaskCall, renderResult as renderTaskResult } from "./task/render";
20
+ import type { TaskToolDetails } from "./task/types";
21
+ import { renderWebFetchCall, renderWebFetchResult, type WebFetchToolDetails } from "./web-fetch";
22
+ import { renderWebSearchCall, renderWebSearchResult, type WebSearchRenderDetails } from "./web-search/render";
22
23
 
23
24
  // Tree drawing characters
24
25
  const TREE_MID = "├─";
@@ -416,6 +417,83 @@ const lspRenderer: ToolRenderer<LspArgs, LspToolDetails> = {
416
417
  renderResult: renderLspResult,
417
418
  };
418
419
 
420
+ // ============================================================================
421
+ // Output Renderer
422
+ // ============================================================================
423
+
424
+ interface OutputArgs {
425
+ ids: string[];
426
+ format?: "raw" | "json" | "stripped";
427
+ }
428
+
429
+ /** Format byte count for display */
430
+ function formatBytes(bytes: number): string {
431
+ if (bytes < 1024) return `${bytes}B`;
432
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
433
+ return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
434
+ }
435
+
436
+ const outputRenderer: ToolRenderer<OutputArgs, OutputToolDetails> = {
437
+ renderCall(args, theme) {
438
+ const ids = args.ids?.join(", ") ?? "?";
439
+ const label = theme.fg("toolTitle", theme.bold("output"));
440
+ const format = args.format && args.format !== "raw" ? theme.fg("muted", ` (${args.format})`) : "";
441
+ return new Text(`${label} ${theme.fg("dim", ids)}${format}`, 0, 0);
442
+ },
443
+
444
+ renderResult(result, { expanded }, theme) {
445
+ const details = result.details;
446
+
447
+ // Error case: some IDs not found
448
+ if (details?.notFound?.length) {
449
+ let text = `${theme.fg("error", ICON_ERROR)} Not found: ${details.notFound.join(", ")}`;
450
+ if (details.availableIds?.length) {
451
+ text += `\n${theme.fg("dim", "Available:")} ${details.availableIds.join(", ")}`;
452
+ } else {
453
+ text += `\n${theme.fg("dim", "No outputs available in current session")}`;
454
+ }
455
+ return new Text(text, 0, 0);
456
+ }
457
+
458
+ const outputs = details?.outputs ?? [];
459
+
460
+ // No session case
461
+ if (outputs.length === 0) {
462
+ const textContent = result.content?.find((c: any) => c.type === "text")?.text;
463
+ return new Text(
464
+ `${theme.fg("warning", ICON_WARNING)} ${theme.fg("muted", textContent || "No outputs")}`,
465
+ 0,
466
+ 0,
467
+ );
468
+ }
469
+
470
+ // Success: single output
471
+ if (outputs.length === 1) {
472
+ const o = outputs[0];
473
+ const summary = `read ${o.id}.out.md (${o.lineCount} lines, ${formatBytes(o.charCount)})`;
474
+ return new Text(`${theme.fg("success", ICON_SUCCESS)} ${theme.fg("dim", summary)}`, 0, 0);
475
+ }
476
+
477
+ // Success: multiple outputs (tree display)
478
+ const expandHint = expanded ? "" : theme.fg("dim", " (Ctrl+O to expand)");
479
+ let text = `${theme.fg("success", ICON_SUCCESS)} ${theme.fg("dim", `read ${outputs.length} outputs`)}${expandHint}`;
480
+
481
+ const maxOutputs = expanded ? outputs.length : Math.min(outputs.length, 5);
482
+ for (let i = 0; i < maxOutputs; i++) {
483
+ const o = outputs[i];
484
+ const isLast = i === maxOutputs - 1 && (expanded || outputs.length <= 5);
485
+ const branch = isLast ? TREE_END : TREE_MID;
486
+ text += `\n ${theme.fg("dim", branch)} ${theme.fg("accent", o.id)} ${theme.fg("dim", `(${o.lineCount} lines)`)}`;
487
+ }
488
+
489
+ if (!expanded && outputs.length > 5) {
490
+ text += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${outputs.length - 5} more outputs`)}`;
491
+ }
492
+
493
+ return new Text(text, 0, 0);
494
+ },
495
+ };
496
+
419
497
  // ============================================================================
420
498
  // Task Renderer
421
499
  // ============================================================================
@@ -534,6 +612,7 @@ export const toolRenderers: Record<
534
612
  notebook: notebookRenderer,
535
613
  ls: lsRenderer,
536
614
  lsp: lspRenderer,
615
+ output: outputRenderer,
537
616
  task: taskRenderer,
538
617
  web_fetch: webFetchRenderer,
539
618
  web_search: webSearchRenderer,
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Review tools - report_finding and submit_review
3
+ *
4
+ * Used by the reviewer agent to report findings in a structured way.
5
+ * Both tools are hidden by default - only enabled when explicitly listed in agent's tools.
6
+ */
7
+
8
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
9
+ import type { Component } from "@oh-my-pi/pi-tui";
10
+ import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
11
+ import { Type } from "@sinclair/typebox";
12
+ import type { Theme } from "../../modes/interactive/theme/theme";
13
+
14
+ const PRIORITY_LABELS: Record<number, string> = {
15
+ 0: "P0",
16
+ 1: "P1",
17
+ 2: "P2",
18
+ 3: "P3",
19
+ };
20
+
21
+ const _PRIORITY_DESCRIPTIONS: Record<number, string> = {
22
+ 0: "Drop everything to fix. Blocking release, operations, or major usage.",
23
+ 1: "Urgent. Should be addressed in the next cycle.",
24
+ 2: "Normal. To be fixed eventually.",
25
+ 3: "Low. Nice to have.",
26
+ };
27
+
28
+ // report_finding schema
29
+ const ReportFindingParams = Type.Object({
30
+ title: Type.String({
31
+ description: "≤80 chars, imperative, prefixed with [P0-P3]. E.g., '[P1] Un-padding slices along wrong dimension'",
32
+ }),
33
+ body: Type.String({
34
+ description: "Markdown explaining why this is a problem. One paragraph max.",
35
+ }),
36
+ priority: Type.Union([Type.Literal(0), Type.Literal(1), Type.Literal(2), Type.Literal(3)], {
37
+ description: "0=P0 (critical), 1=P1 (urgent), 2=P2 (normal), 3=P3 (low)",
38
+ }),
39
+ confidence: Type.Number({
40
+ minimum: 0,
41
+ maximum: 1,
42
+ description: "Confidence score 0.0-1.0",
43
+ }),
44
+ file_path: Type.String({ description: "Absolute path to the file" }),
45
+ line_start: Type.Number({ description: "Start line of the issue" }),
46
+ line_end: Type.Number({ description: "End line of the issue" }),
47
+ });
48
+
49
+ interface ReportFindingDetails {
50
+ title: string;
51
+ body: string;
52
+ priority: number;
53
+ confidence: number;
54
+ file_path: string;
55
+ line_start: number;
56
+ line_end: number;
57
+ }
58
+
59
+ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFindingDetails, Theme> = {
60
+ name: "report_finding",
61
+ label: "Report Finding",
62
+ description: "Report a code review finding. Use this for each issue found. Call submit_review when done.",
63
+ parameters: ReportFindingParams,
64
+ hidden: true,
65
+
66
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
67
+ const { title, body, priority, confidence, file_path, line_start, line_end } = params;
68
+ const location = `${file_path}:${line_start}${line_end !== line_start ? `-${line_end}` : ""}`;
69
+
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: `Finding recorded: ${PRIORITY_LABELS[priority]} ${title}\nLocation: ${location}\nConfidence: ${(confidence * 100).toFixed(0)}%`,
75
+ },
76
+ ],
77
+ details: { title, body, priority, confidence, file_path, line_start, line_end },
78
+ };
79
+ },
80
+
81
+ renderCall(args, theme): Component {
82
+ const priority = PRIORITY_LABELS[args.priority as number] ?? "P?";
83
+ const color = args.priority === 0 ? "error" : args.priority === 1 ? "warning" : "muted";
84
+ const titleText = String(args.title).replace(/^\[P\d\]\s*/, "");
85
+ return new Text(
86
+ `${theme.fg("toolTitle", theme.bold("report_finding "))}${theme.fg(color, `[${priority}]`)} ${theme.fg("dim", titleText)}`,
87
+ 0,
88
+ 0,
89
+ );
90
+ },
91
+
92
+ renderResult(result, _options, theme): Component {
93
+ const { details } = result;
94
+ if (!details) {
95
+ const text = result.content[0];
96
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
97
+ }
98
+
99
+ const priority = PRIORITY_LABELS[details.priority] ?? "P?";
100
+ const color = details.priority === 0 ? "error" : details.priority === 1 ? "warning" : "muted";
101
+ const location = `${details.file_path}:${details.line_start}${details.line_end !== details.line_start ? `-${details.line_end}` : ""}`;
102
+
103
+ return new Text(
104
+ `${theme.fg("success", "✓")} ${theme.fg(color, `[${priority}]`)} ${theme.fg("dim", location)}`,
105
+ 0,
106
+ 0,
107
+ );
108
+ },
109
+ };
110
+
111
+ // submit_review schema
112
+ const SubmitReviewParams = Type.Object({
113
+ overall_correctness: Type.Union([Type.Literal("correct"), Type.Literal("incorrect")], {
114
+ description: "Whether the patch is correct (no bugs, tests won't break)",
115
+ }),
116
+ explanation: Type.String({
117
+ description: "1-3 sentence explanation justifying the verdict",
118
+ }),
119
+ confidence: Type.Number({
120
+ minimum: 0,
121
+ maximum: 1,
122
+ description: "Overall confidence score 0.0-1.0",
123
+ }),
124
+ });
125
+
126
+ interface SubmitReviewDetails {
127
+ overall_correctness: "correct" | "incorrect";
128
+ explanation: string;
129
+ confidence: number;
130
+ }
131
+
132
+ export const submitReviewTool: AgentTool<typeof SubmitReviewParams, SubmitReviewDetails, Theme> = {
133
+ name: "submit_review",
134
+ label: "Submit Review",
135
+ description: "Submit the final review verdict. Call this after all findings have been reported.",
136
+ parameters: SubmitReviewParams,
137
+ hidden: true,
138
+
139
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
140
+ const { overall_correctness, explanation, confidence } = params;
141
+
142
+ let summary = `## Review Summary\n\n`;
143
+ summary += `**Verdict:** ${overall_correctness === "correct" ? "✓ Patch is correct" : "✗ Patch is incorrect"}\n`;
144
+ summary += `**Confidence:** ${(confidence * 100).toFixed(0)}%\n\n`;
145
+ summary += explanation;
146
+
147
+ return {
148
+ content: [{ type: "text", text: summary }],
149
+ details: { overall_correctness, explanation, confidence },
150
+ };
151
+ },
152
+
153
+ renderCall(args, theme): Component {
154
+ const verdict = args.overall_correctness === "correct" ? "correct" : "incorrect";
155
+ const color = args.overall_correctness === "correct" ? "success" : "error";
156
+ return new Text(
157
+ `${theme.fg("toolTitle", theme.bold("submit_review "))}${theme.fg(color, verdict)} ${theme.fg("dim", `(${((args.confidence as number) * 100).toFixed(0)}%)`)}`,
158
+ 0,
159
+ 0,
160
+ );
161
+ },
162
+
163
+ renderResult(result, { expanded }, theme): Component {
164
+ const { details } = result;
165
+ if (!details) {
166
+ const text = result.content[0];
167
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
168
+ }
169
+
170
+ const container = new Container();
171
+ const verdictColor = details.overall_correctness === "correct" ? "success" : "error";
172
+ const verdictIcon = details.overall_correctness === "correct" ? "✓" : "✗";
173
+
174
+ container.addChild(
175
+ new Text(
176
+ `${theme.fg(verdictColor, verdictIcon)} Patch is ${theme.fg(verdictColor, details.overall_correctness)} ${theme.fg("dim", `(${(details.confidence * 100).toFixed(0)}% confidence)`)}`,
177
+ 0,
178
+ 0,
179
+ ),
180
+ );
181
+
182
+ if (expanded) {
183
+ container.addChild(new Spacer(1));
184
+ container.addChild(new Text(theme.fg("dim", details.explanation), 0, 0));
185
+ }
186
+
187
+ return container;
188
+ },
189
+ };
190
+
191
+ export function createReportFindingTool(): AgentTool<typeof ReportFindingParams, ReportFindingDetails, Theme> {
192
+ return reportFindingTool;
193
+ }
194
+
195
+ export function createSubmitReviewTool(): AgentTool<typeof SubmitReviewParams, SubmitReviewDetails, Theme> {
196
+ return submitReviewTool;
197
+ }
198
+
199
+ // Re-export types for external use
200
+ export type { ReportFindingDetails, SubmitReviewDetails };
201
+
202
+ // ─────────────────────────────────────────────────────────────────────────────
203
+ // Subprocess tool handlers - registered for extraction/rendering in task tool
204
+ // ─────────────────────────────────────────────────────────────────────────────
205
+
206
+ import path from "node:path";
207
+ import { subprocessToolRegistry } from "./task/subprocess-tool-registry";
208
+
209
+ // Register report_finding handler
210
+ subprocessToolRegistry.register<ReportFindingDetails>("report_finding", {
211
+ extractData: (event) => event.result?.details as ReportFindingDetails | undefined,
212
+
213
+ renderInline: (data, theme) => {
214
+ const priority = PRIORITY_LABELS[data.priority] ?? "P?";
215
+ const color = data.priority === 0 ? "error" : data.priority === 1 ? "warning" : "muted";
216
+ const titleText = data.title.replace(/^\[P\d\]\s*/, "");
217
+ const loc = `${path.basename(data.file_path)}:${data.line_start}`;
218
+ return new Text(`${theme.fg(color, `[${priority}]`)} ${titleText} ${theme.fg("dim", loc)}`, 0, 0);
219
+ },
220
+
221
+ renderFinal: (allData, theme, expanded) => {
222
+ const container = new Container();
223
+ const displayCount = expanded ? allData.length : Math.min(3, allData.length);
224
+
225
+ for (let i = 0; i < displayCount; i++) {
226
+ const data = allData[i];
227
+ const priority = PRIORITY_LABELS[data.priority] ?? "P?";
228
+ const color = data.priority === 0 ? "error" : data.priority === 1 ? "warning" : "muted";
229
+ const titleText = data.title.replace(/^\[P\d\]\s*/, "");
230
+ const loc = `${path.basename(data.file_path)}:${data.line_start}`;
231
+
232
+ container.addChild(
233
+ new Text(` ${theme.fg(color, `[${priority}]`)} ${titleText} ${theme.fg("dim", loc)}`, 0, 0),
234
+ );
235
+
236
+ if (expanded && data.body) {
237
+ container.addChild(new Text(` ${theme.fg("dim", data.body)}`, 0, 0));
238
+ }
239
+ }
240
+
241
+ if (allData.length > displayCount) {
242
+ container.addChild(new Text(theme.fg("dim", ` ... ${allData.length - displayCount} more findings`), 0, 0));
243
+ }
244
+
245
+ return container;
246
+ },
247
+ });
248
+
249
+ // Register submit_review handler
250
+ subprocessToolRegistry.register<SubmitReviewDetails>("submit_review", {
251
+ extractData: (event) => event.result?.details as SubmitReviewDetails | undefined,
252
+
253
+ // Terminate subprocess after review is submitted
254
+ shouldTerminate: () => true,
255
+
256
+ renderInline: (data, theme) => {
257
+ const verdictColor = data.overall_correctness === "correct" ? "success" : "error";
258
+ const verdictIcon = data.overall_correctness === "correct" ? "✓" : "✗";
259
+ return new Text(
260
+ `${theme.fg(verdictColor, verdictIcon)} Review: ${theme.fg(verdictColor, data.overall_correctness)} (${(data.confidence * 100).toFixed(0)}%)`,
261
+ 0,
262
+ 0,
263
+ );
264
+ },
265
+
266
+ // Note: renderFinal is NOT used for submit_review - we use the combined
267
+ // renderReviewResult in render.ts to show verdict + findings together
268
+ });
@@ -8,7 +8,7 @@
8
8
  import * as fs from "node:fs";
9
9
  import * as path from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
- import type { AgentDefinition, AgentSource } from "./types.js";
11
+ import type { AgentDefinition, AgentSource } from "./types";
12
12
 
13
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
14
  const BUNDLED_AGENTS_DIR = path.join(__dirname, "bundled-agents");
@@ -2,7 +2,7 @@
2
2
  name: explore
3
3
  description: Fast read-only codebase scout that returns compressed context for handoff
4
4
  tools: read, grep, glob, ls, bash
5
- model: claude-haiku-4-5, haiku, flash, mini
5
+ model: pi/smol, haiku, flash, mini
6
6
  ---
7
7
 
8
8
  You are a file search specialist and codebase scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.