@oh-my-pi/pi-coding-agent 3.30.0 → 3.31.0

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 (155) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +5 -5
  3. package/src/cli/args.ts +4 -0
  4. package/src/core/agent-session.ts +29 -2
  5. package/src/core/bash-executor.ts +2 -1
  6. package/src/core/custom-commands/bundled/review/index.ts +369 -14
  7. package/src/core/custom-commands/bundled/wt/index.ts +1 -1
  8. package/src/core/session-manager.ts +158 -246
  9. package/src/core/session-storage.ts +379 -0
  10. package/src/core/settings-manager.ts +155 -4
  11. package/src/core/system-prompt.ts +62 -64
  12. package/src/core/tools/ask.ts +5 -4
  13. package/src/core/tools/bash-interceptor.ts +26 -61
  14. package/src/core/tools/bash.ts +13 -8
  15. package/src/core/tools/edit-diff.ts +11 -4
  16. package/src/core/tools/edit.ts +7 -13
  17. package/src/core/tools/find.ts +111 -50
  18. package/src/core/tools/gemini-image.ts +128 -147
  19. package/src/core/tools/grep.ts +397 -415
  20. package/src/core/tools/index.test.ts +5 -1
  21. package/src/core/tools/index.ts +6 -8
  22. package/src/core/tools/ls.ts +12 -10
  23. package/src/core/tools/lsp/client.ts +58 -9
  24. package/src/core/tools/lsp/config.ts +205 -656
  25. package/src/core/tools/lsp/defaults.json +465 -0
  26. package/src/core/tools/lsp/index.ts +55 -32
  27. package/src/core/tools/lsp/rust-analyzer.ts +49 -10
  28. package/src/core/tools/lsp/types.ts +1 -0
  29. package/src/core/tools/lsp/utils.ts +1 -1
  30. package/src/core/tools/read.ts +150 -74
  31. package/src/core/tools/render-utils.ts +70 -10
  32. package/src/core/tools/review.ts +38 -126
  33. package/src/core/tools/task/artifacts.ts +5 -4
  34. package/src/core/tools/task/executor.ts +94 -83
  35. package/src/core/tools/task/index.ts +129 -92
  36. package/src/core/tools/task/parallel.ts +30 -3
  37. package/src/core/tools/task/render.ts +85 -39
  38. package/src/core/tools/task/types.ts +15 -6
  39. package/src/core/tools/task/worker.ts +124 -89
  40. package/src/core/tools/web-fetch.ts +112 -377
  41. package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
  42. package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
  43. package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
  44. package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
  45. package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
  46. package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
  47. package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
  48. package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
  49. package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
  50. package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
  51. package/src/core/tools/web-scrapers/clojars.ts +180 -0
  52. package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
  53. package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
  54. package/src/core/tools/web-scrapers/crossref.ts +149 -0
  55. package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
  56. package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
  57. package/src/core/tools/web-scrapers/discourse.ts +221 -0
  58. package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
  59. package/src/core/tools/web-scrapers/fdroid.ts +158 -0
  60. package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
  61. package/src/core/tools/web-scrapers/flathub.ts +239 -0
  62. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
  63. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
  64. package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
  65. package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
  66. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
  67. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
  68. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
  69. package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
  70. package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
  71. package/src/core/tools/web-scrapers/index.ts +250 -0
  72. package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
  73. package/src/core/tools/web-scrapers/lemmy.ts +220 -0
  74. package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
  75. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
  76. package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
  77. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
  78. package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
  79. package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
  80. package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
  81. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
  82. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
  83. package/src/core/tools/web-scrapers/ollama.ts +267 -0
  84. package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
  85. package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
  86. package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
  87. package/src/core/tools/web-scrapers/orcid.ts +299 -0
  88. package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
  89. package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
  90. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
  91. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
  92. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
  93. package/src/core/tools/web-scrapers/rawg.ts +124 -0
  94. package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
  95. package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
  96. package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
  97. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
  98. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
  99. package/src/core/tools/web-scrapers/searchcode.ts +217 -0
  100. package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
  101. package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
  102. package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
  103. package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
  104. package/src/core/tools/web-scrapers/spdx.ts +121 -0
  105. package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
  106. package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
  107. package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
  108. package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
  109. package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
  110. package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
  111. package/src/core/tools/web-scrapers/utils.ts +162 -0
  112. package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
  113. package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
  114. package/src/core/tools/web-scrapers/w3c.ts +163 -0
  115. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
  116. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
  117. package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
  118. package/src/core/tools/write.ts +21 -18
  119. package/src/core/voice.ts +3 -2
  120. package/src/lib/worktree/collapse.ts +2 -1
  121. package/src/lib/worktree/git.ts +2 -18
  122. package/src/main.ts +59 -3
  123. package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
  124. package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
  125. package/src/modes/interactive/components/hook-editor.ts +2 -1
  126. package/src/modes/interactive/components/model-selector.ts +19 -4
  127. package/src/modes/interactive/interactive-mode.ts +41 -38
  128. package/src/modes/interactive/theme/theme.ts +58 -58
  129. package/src/modes/rpc/rpc-mode.ts +10 -9
  130. package/src/prompts/review-request.md +27 -0
  131. package/src/prompts/reviewer.md +64 -68
  132. package/src/prompts/tools/output.md +22 -3
  133. package/src/prompts/tools/task.md +32 -33
  134. package/src/utils/clipboard.ts +2 -1
  135. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  136. package/src/core/tools/web-fetch-handlers/index.ts +0 -69
  137. package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
  138. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
  139. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
  140. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
  141. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
  142. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
  143. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
  144. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
  145. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
  146. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
  147. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
  148. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
  149. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
  150. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
  151. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
  152. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
  153. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
  154. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
  155. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Review tools - report_finding and submit_review
2
+ * Review tools - report_finding for structured code review.
3
3
  *
4
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.
5
+ * Hidden by default - only enabled when explicitly listed in agent's tools.
6
+ * Reviewers finish via `complete` tool with SubmitReviewDetails schema.
6
7
  */
7
8
 
8
9
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
9
10
  import type { Component } from "@oh-my-pi/pi-tui";
10
- import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
11
+ import { Container, Text } from "@oh-my-pi/pi-tui";
11
12
  import { Type } from "@sinclair/typebox";
12
- import type { Theme } from "../../modes/interactive/theme/theme";
13
- import { theme } from "../../modes/interactive/theme/theme";
13
+ import type { Theme, ThemeColor } from "../../modes/interactive/theme/theme";
14
14
 
15
15
  const PRIORITY_LABELS: Record<number, string> = {
16
16
  0: "P0",
@@ -19,6 +19,24 @@ const PRIORITY_LABELS: Record<number, string> = {
19
19
  3: "P3",
20
20
  };
21
21
 
22
+ const PRIORITY_META: Record<number, { symbol: "status.error" | "status.warning" | "status.info"; color: ThemeColor }> =
23
+ {
24
+ 0: { symbol: "status.error", color: "error" },
25
+ 1: { symbol: "status.warning", color: "warning" },
26
+ 2: { symbol: "status.warning", color: "muted" },
27
+ 3: { symbol: "status.info", color: "accent" },
28
+ };
29
+
30
+ function getPriorityDisplay(priority: number, theme: Theme): { label: string; icon: string; color: ThemeColor } {
31
+ const label = PRIORITY_LABELS[priority] ?? "P?";
32
+ const meta = PRIORITY_META[priority] ?? { symbol: "status.info", color: "muted" as const };
33
+ return {
34
+ label,
35
+ icon: theme.styledSymbol(meta.symbol, meta.color),
36
+ color: meta.color,
37
+ };
38
+ }
39
+
22
40
  // report_finding schema
23
41
  const ReportFindingParams = Type.Object({
24
42
  title: Type.String({
@@ -53,7 +71,7 @@ interface ReportFindingDetails {
53
71
  export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFindingDetails, Theme> = {
54
72
  name: "report_finding",
55
73
  label: "Report Finding",
56
- description: "Report a code review finding. Use this for each issue found. Call submit_review when done.",
74
+ description: "Report a code review finding. Use this for each issue found. Call complete when done.",
57
75
  parameters: ReportFindingParams,
58
76
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
59
77
  const { title, body, priority, confidence, file_path, line_start, line_end } = params;
@@ -73,11 +91,10 @@ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFind
73
91
  },
74
92
 
75
93
  renderCall(args, theme): Component {
76
- const priority = PRIORITY_LABELS[args.priority as number] ?? "P?";
77
- const color = args.priority === 0 ? "error" : args.priority === 1 ? "warning" : "muted";
94
+ const { label, icon, color } = getPriorityDisplay(args.priority as number, theme);
78
95
  const titleText = String(args.title).replace(/^\[P\d\]\s*/, "");
79
96
  return new Text(
80
- `${theme.fg("toolTitle", theme.bold("report_finding "))}${theme.fg(color, `[${priority}]`)} ${theme.fg(
97
+ `${theme.fg("toolTitle", theme.bold("report_finding "))}${icon} ${theme.fg(color, `[${label}]`)} ${theme.fg(
81
98
  "dim",
82
99
  titleText,
83
100
  )}`,
@@ -93,111 +110,31 @@ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFind
93
110
  return new Text(text?.type === "text" ? text.text : "", 0, 0);
94
111
  }
95
112
 
96
- const priority = PRIORITY_LABELS[details.priority] ?? "P?";
97
- const color = details.priority === 0 ? "error" : details.priority === 1 ? "warning" : "muted";
113
+ const { label, icon, color } = getPriorityDisplay(details.priority, theme);
98
114
  const location = `${details.file_path}:${details.line_start}${
99
115
  details.line_end !== details.line_start ? `-${details.line_end}` : ""
100
116
  }`;
101
117
 
102
118
  return new Text(
103
- `${theme.fg("success", theme.status.success)} ${theme.fg(color, `[${priority}]`)} ${theme.fg("dim", location)}`,
119
+ `${theme.fg("success", theme.status.success)} ${icon} ${theme.fg(color, `[${label}]`)} ${theme.fg(
120
+ "dim",
121
+ location,
122
+ )}`,
104
123
  0,
105
124
  0,
106
125
  );
107
126
  },
108
127
  };
109
128
 
110
- // submit_review schema
111
- const SubmitReviewParams = Type.Object({
112
- overall_correctness: Type.Union([Type.Literal("correct"), Type.Literal("incorrect")], {
113
- description: "Whether the patch is correct (no bugs, tests won't break)",
114
- }),
115
- explanation: Type.String({
116
- description: "1-3 sentence explanation justifying the verdict",
117
- }),
118
- confidence: Type.Number({
119
- minimum: 0,
120
- maximum: 1,
121
- description: "Overall confidence score 0.0-1.0",
122
- }),
123
- });
124
-
125
- interface SubmitReviewDetails {
129
+ /** SubmitReviewDetails - used for rendering review results from complete tool */
130
+ export interface SubmitReviewDetails {
126
131
  overall_correctness: "correct" | "incorrect";
127
132
  explanation: string;
128
133
  confidence: number;
129
134
  }
130
135
 
131
- export const submitReviewTool: AgentTool<typeof SubmitReviewParams, SubmitReviewDetails, Theme> = {
132
- name: "submit_review",
133
- label: "Submit Review",
134
- description: "Submit the final review verdict. Call this after all findings have been reported.",
135
- parameters: SubmitReviewParams,
136
-
137
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
138
- const { overall_correctness, explanation, confidence } = params;
139
-
140
- let summary = `## Review Summary\n\n`;
141
- summary += `**Verdict:** ${
142
- overall_correctness === "correct"
143
- ? `${theme.status.success} Patch is correct`
144
- : `${theme.status.error} Patch is incorrect`
145
- }\n`;
146
- summary += `**Confidence:** ${(confidence * 100).toFixed(0)}%\n\n`;
147
- summary += explanation;
148
-
149
- return {
150
- content: [{ type: "text", text: summary }],
151
- details: { overall_correctness, explanation, confidence },
152
- };
153
- },
154
-
155
- renderCall(args, theme): Component {
156
- const verdict = args.overall_correctness === "correct" ? "correct" : "incorrect";
157
- const color = args.overall_correctness === "correct" ? "success" : "error";
158
- return new Text(
159
- `${theme.fg("toolTitle", theme.bold("submit_review "))}${theme.fg(color, verdict)} ${theme.fg(
160
- "dim",
161
- `(${((args.confidence as number) * 100).toFixed(0)}%)`,
162
- )}`,
163
- 0,
164
- 0,
165
- );
166
- },
167
-
168
- renderResult(result, { expanded }, theme): Component {
169
- const { details } = result;
170
- if (!details) {
171
- const text = result.content[0];
172
- return new Text(text?.type === "text" ? text.text : "", 0, 0);
173
- }
174
-
175
- const container = new Container();
176
- const verdictColor = details.overall_correctness === "correct" ? "success" : "error";
177
- const verdictIcon = details.overall_correctness === "correct" ? theme.status.success : theme.status.error;
178
-
179
- container.addChild(
180
- new Text(
181
- `${theme.fg(verdictColor, verdictIcon)} Patch is ${theme.fg(
182
- verdictColor,
183
- details.overall_correctness,
184
- )} ${theme.fg("dim", `(${(details.confidence * 100).toFixed(0)}% confidence)`)}`,
185
- 0,
186
- 0,
187
- ),
188
- );
189
-
190
- if (expanded) {
191
- container.addChild(new Spacer(1));
192
- container.addChild(new Text(theme.fg("dim", details.explanation), 0, 0));
193
- }
194
-
195
- return container;
196
- },
197
- };
198
-
199
136
  // Re-export types for external use
200
- export type { ReportFindingDetails, SubmitReviewDetails };
137
+ export type { ReportFindingDetails };
201
138
 
202
139
  // ─────────────────────────────────────────────────────────────────────────────
203
140
  // Subprocess tool handlers - registered for extraction/rendering in task tool
@@ -211,11 +148,10 @@ subprocessToolRegistry.register<ReportFindingDetails>("report_finding", {
211
148
  extractData: (event) => event.result?.details as ReportFindingDetails | undefined,
212
149
 
213
150
  renderInline: (data, theme) => {
214
- const priority = PRIORITY_LABELS[data.priority] ?? "P?";
215
- const color = data.priority === 0 ? "error" : data.priority === 1 ? "warning" : "muted";
151
+ const { label, icon, color } = getPriorityDisplay(data.priority, theme);
216
152
  const titleText = data.title.replace(/^\[P\d\]\s*/, "");
217
153
  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);
154
+ return new Text(`${icon} ${theme.fg(color, `[${label}]`)} ${titleText} ${theme.fg("dim", loc)}`, 0, 0);
219
155
  },
220
156
 
221
157
  renderFinal: (allData, theme, expanded) => {
@@ -224,13 +160,12 @@ subprocessToolRegistry.register<ReportFindingDetails>("report_finding", {
224
160
 
225
161
  for (let i = 0; i < displayCount; i++) {
226
162
  const data = allData[i];
227
- const priority = PRIORITY_LABELS[data.priority] ?? "P?";
228
- const color = data.priority === 0 ? "error" : data.priority === 1 ? "warning" : "muted";
163
+ const { label, icon, color } = getPriorityDisplay(data.priority, theme);
229
164
  const titleText = data.title.replace(/^\[P\d\]\s*/, "");
230
165
  const loc = `${path.basename(data.file_path)}:${data.line_start}`;
231
166
 
232
167
  container.addChild(
233
- new Text(` ${theme.fg(color, `[${priority}]`)} ${titleText} ${theme.fg("dim", loc)}`, 0, 0),
168
+ new Text(` ${icon} ${theme.fg(color, `[${label}]`)} ${titleText} ${theme.fg("dim", loc)}`, 0, 0),
234
169
  );
235
170
 
236
171
  if (expanded && data.body) {
@@ -251,26 +186,3 @@ subprocessToolRegistry.register<ReportFindingDetails>("report_finding", {
251
186
  return container;
252
187
  },
253
188
  });
254
-
255
- // Register submit_review handler
256
- subprocessToolRegistry.register<SubmitReviewDetails>("submit_review", {
257
- extractData: (event) => event.result?.details as SubmitReviewDetails | undefined,
258
-
259
- // Terminate subprocess after review is submitted
260
- shouldTerminate: () => true,
261
-
262
- renderInline: (data, theme) => {
263
- const verdictColor = data.overall_correctness === "correct" ? "success" : "error";
264
- const verdictIcon = data.overall_correctness === "correct" ? theme.status.success : theme.status.error;
265
- return new Text(
266
- `${theme.fg(verdictColor, verdictIcon)} Review: ${theme.fg(verdictColor, data.overall_correctness)} (${(
267
- data.confidence * 100
268
- ).toFixed(0)}%)`,
269
- 0,
270
- 0,
271
- );
272
- },
273
-
274
- // Note: renderFinal is NOT used for submit_review - we use the combined
275
- // renderReviewResult in render.ts to show verdict + findings together
276
- });
@@ -8,6 +8,7 @@
8
8
  import * as fs from "node:fs";
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
+ import { nanoid } from "nanoid";
11
12
 
12
13
  /**
13
14
  * Derive artifacts directory from session file path.
@@ -62,14 +63,14 @@ export async function writeArtifacts(
62
63
  const paths = getArtifactPaths(dir, taskId);
63
64
 
64
65
  // Write input
65
- await fs.promises.writeFile(paths.inputPath, input, "utf-8");
66
+ await Bun.write(paths.inputPath, input);
66
67
 
67
68
  // Write output
68
- await fs.promises.writeFile(paths.outputPath, output, "utf-8");
69
+ await Bun.write(paths.outputPath, output);
69
70
 
70
71
  // Write JSONL if events provided
71
72
  if (jsonlEvents && jsonlEvents.length > 0) {
72
- await fs.promises.writeFile(paths.jsonlPath, jsonlEvents.join("\n"), "utf-8");
73
+ await Bun.write(paths.jsonlPath, jsonlEvents.join("\n"));
73
74
  return paths;
74
75
  }
75
76
 
@@ -80,7 +81,7 @@ export async function writeArtifacts(
80
81
  * Create a temporary artifacts directory.
81
82
  */
82
83
  export function createTempArtifactsDir(runId?: string): string {
83
- const id = runId || `${Date.now()}-${Math.random().toString(36).slice(2)}`;
84
+ const id = runId || nanoid();
84
85
  const dir = path.join(os.tmpdir(), `omp-task-${id}`);
85
86
  ensureArtifactsDir(dir);
86
87
  return dir;
@@ -4,7 +4,6 @@
4
4
  * Runs each subagent in a Bun Worker and forwards AgentEvents for progress tracking.
5
5
  */
6
6
 
7
- import { writeFileSync } from "node:fs";
8
7
  import type { AgentEvent } from "@oh-my-pi/pi-agent-core";
9
8
  import type { EventBus } from "../../event-bus";
10
9
  import { ensureArtifactsDir, getArtifactPaths } from "./artifacts";
@@ -50,20 +49,26 @@ function truncateOutput(output: string): { text: string; truncated: boolean } {
50
49
 
51
50
  let i = 0;
52
51
  let lastNewlineIndex = -1;
53
- while (i < output.length && byteBudget > 0) {
54
- const ch = output.charCodeAt(i);
55
- byteBudget--;
52
+ while (i < output.length) {
53
+ const codePoint = output.codePointAt(i);
54
+ if (codePoint === undefined) break;
55
+ const codeUnitLength = codePoint > 0xffff ? 2 : 1;
56
+ const byteLen = codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4;
57
+ if (byteBudget - byteLen < 0) {
58
+ truncated = true;
59
+ break;
60
+ }
61
+ byteBudget -= byteLen;
62
+ i += codeUnitLength;
56
63
 
57
- if (ch === 10 /* \n */) {
64
+ if (codePoint === 0x0a) {
58
65
  lineBudget--;
59
- lastNewlineIndex = i;
66
+ lastNewlineIndex = i - 1;
60
67
  if (lineBudget <= 0) {
61
68
  truncated = true;
62
69
  break;
63
70
  }
64
71
  }
65
-
66
- i++;
67
72
  }
68
73
 
69
74
  if (i < output.length) {
@@ -186,7 +191,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
186
191
 
187
192
  // Write input file immediately (real-time visibility)
188
193
  try {
189
- writeFileSync(artifactPaths.inputPath, fullTask, "utf-8");
194
+ await Bun.write(artifactPaths.inputPath, fullTask);
190
195
  } catch {
191
196
  // Non-fatal, continue without input artifact
192
197
  }
@@ -229,17 +234,18 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
229
234
  };
230
235
  }
231
236
 
232
- let output = "";
237
+ const outputChunks: string[] = [];
238
+ const finalOutputChunks: string[] = [];
233
239
  let stderr = "";
234
- let finalOutput = "";
235
240
  let resolved = false;
236
- let pendingTermination = false; // Set when shouldTerminate fires, wait for message_end
237
241
  type AbortReason = "signal" | "terminate";
238
242
  let abortSent = false;
239
243
  let abortReason: AbortReason | undefined;
240
- let abortTerminateTimer: ReturnType<typeof setTimeout> | undefined;
241
- let pendingTerminationTimer: ReturnType<typeof setTimeout> | undefined;
244
+ let terminationScheduled = false;
245
+ let pendingTerminationController: AbortController | null = null;
242
246
  let finalize: ((message: Extract<SubagentWorkerResponse, { type: "done" }>) => void) | null = null;
247
+ const listenerController = new AbortController();
248
+ const listenerSignal = listenerController.signal;
243
249
 
244
250
  // Accumulate usage incrementally from message_end events (no memory for streaming events)
245
251
  const accumulatedUsage = {
@@ -252,11 +258,31 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
252
258
  };
253
259
  let hasUsage = false;
254
260
 
255
- const clearTimers = (): void => {
256
- if (abortTerminateTimer) clearTimeout(abortTerminateTimer);
257
- abortTerminateTimer = undefined;
258
- if (pendingTerminationTimer) clearTimeout(pendingTerminationTimer);
259
- pendingTerminationTimer = undefined;
261
+ const scheduleTermination = () => {
262
+ if (terminationScheduled) return;
263
+ terminationScheduled = true;
264
+ const timeoutSignal = AbortSignal.timeout(2000);
265
+ timeoutSignal.addEventListener(
266
+ "abort",
267
+ () => {
268
+ if (resolved) return;
269
+ try {
270
+ worker.terminate();
271
+ } catch {
272
+ // Ignore termination errors
273
+ }
274
+ if (finalize && !resolved) {
275
+ finalize({
276
+ type: "done",
277
+ exitCode: 1,
278
+ durationMs: Date.now() - startTime,
279
+ error: abortReason === "signal" ? "Aborted" : "Worker terminated after tool completion",
280
+ aborted: abortReason === "signal",
281
+ });
282
+ }
283
+ },
284
+ { once: true, signal: listenerSignal },
285
+ );
260
286
  };
261
287
 
262
288
  const requestAbort = (reason: AbortReason) => {
@@ -269,33 +295,35 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
269
295
  if (resolved) return;
270
296
  abortSent = true;
271
297
  abortReason = reason;
272
- if (pendingTerminationTimer) clearTimeout(pendingTerminationTimer);
273
- pendingTerminationTimer = undefined;
274
298
  const abortMessage: SubagentWorkerRequest = { type: "abort" };
275
299
  try {
276
300
  worker.postMessage(abortMessage);
277
301
  } catch {
278
302
  // Worker already terminated, nothing to do
279
303
  }
280
- if (abortTerminateTimer) clearTimeout(abortTerminateTimer);
281
- abortTerminateTimer = setTimeout(() => {
282
- if (!resolved) {
283
- try {
284
- worker.terminate();
285
- } catch {
286
- // Ignore termination errors
287
- }
288
- if (finalize && !resolved) {
289
- finalize({
290
- type: "done",
291
- exitCode: 1,
292
- durationMs: Date.now() - startTime,
293
- error: reason === "signal" ? "Aborted" : "Worker terminated after tool completion",
294
- aborted: reason === "signal",
295
- });
304
+ // Cancel pending termination if it exists
305
+ if (pendingTerminationController) {
306
+ pendingTerminationController.abort();
307
+ pendingTerminationController = null;
308
+ }
309
+ scheduleTermination();
310
+ };
311
+
312
+ const schedulePendingTermination = () => {
313
+ if (pendingTerminationController || abortSent || terminationScheduled || resolved) return;
314
+ const readyController = new AbortController();
315
+ pendingTerminationController = readyController;
316
+ const pendingSignal = AbortSignal.any([AbortSignal.timeout(2000), readyController.signal]);
317
+ pendingSignal.addEventListener(
318
+ "abort",
319
+ () => {
320
+ pendingTerminationController = null;
321
+ if (!resolved) {
322
+ requestAbort("terminate");
296
323
  }
297
- }
298
- }, 2000);
324
+ },
325
+ { once: true, signal: listenerSignal },
326
+ );
299
327
  };
300
328
 
301
329
  // Handle abort signal
@@ -303,7 +331,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
303
331
  if (!resolved) requestAbort("signal");
304
332
  };
305
333
  if (signal) {
306
- signal.addEventListener("abort", onAbort, { once: true });
334
+ signal.addEventListener("abort", onAbort, { once: true, signal: listenerSignal });
307
335
  }
308
336
 
309
337
  const emitProgress = () => {
@@ -406,14 +434,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
406
434
  })
407
435
  ) {
408
436
  // Don't terminate immediately - wait for message_end to get token counts
409
- pendingTermination = true;
410
- // Safety timeout in case message_end never arrives
411
- if (pendingTerminationTimer) clearTimeout(pendingTerminationTimer);
412
- pendingTerminationTimer = setTimeout(() => {
413
- if (!resolved) {
414
- requestAbort("terminate");
415
- }
416
- }, 2000);
437
+ schedulePendingTermination();
417
438
  }
418
439
  }
419
440
  break;
@@ -446,7 +467,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
446
467
  if (messageContent && Array.isArray(messageContent)) {
447
468
  for (const block of messageContent) {
448
469
  if (block.type === "text" && block.text) {
449
- output += block.text;
470
+ outputChunks.push(block.text);
450
471
  }
451
472
  }
452
473
  }
@@ -455,33 +476,29 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
455
476
  const messageUsage = getMessageUsage(event.message) || (event as AgentEvent & { usage?: unknown }).usage;
456
477
  if (messageUsage && typeof messageUsage === "object") {
457
478
  // Only count assistant messages (not tool results, etc.)
458
- if (
459
- role === "assistant" &&
460
- event.message?.stopReason !== "aborted" &&
461
- event.message?.stopReason !== "error"
462
- ) {
463
- const usageRecord = messageUsage as Record<string, number | undefined>;
464
- const costRecord = (messageUsage as { cost?: Record<string, number | undefined> }).cost;
479
+ if (role === "assistant") {
480
+ const usageRecord = messageUsage as Record<string, unknown>;
481
+ const costRecord = (messageUsage as { cost?: Record<string, unknown> }).cost;
465
482
  hasUsage = true;
466
- accumulatedUsage.input += usageRecord.input ?? 0;
467
- accumulatedUsage.output += usageRecord.output ?? 0;
468
- accumulatedUsage.cacheRead += usageRecord.cacheRead ?? 0;
469
- accumulatedUsage.cacheWrite += usageRecord.cacheWrite ?? 0;
470
- accumulatedUsage.totalTokens += usageRecord.totalTokens ?? 0;
483
+ accumulatedUsage.input += getNumberField(usageRecord, "input") ?? 0;
484
+ accumulatedUsage.output += getNumberField(usageRecord, "output") ?? 0;
485
+ accumulatedUsage.cacheRead += getNumberField(usageRecord, "cacheRead") ?? 0;
486
+ accumulatedUsage.cacheWrite += getNumberField(usageRecord, "cacheWrite") ?? 0;
487
+ accumulatedUsage.totalTokens += getNumberField(usageRecord, "totalTokens") ?? 0;
471
488
  if (costRecord) {
472
- accumulatedUsage.cost.input += costRecord.input ?? 0;
473
- accumulatedUsage.cost.output += costRecord.output ?? 0;
474
- accumulatedUsage.cost.cacheRead += costRecord.cacheRead ?? 0;
475
- accumulatedUsage.cost.cacheWrite += costRecord.cacheWrite ?? 0;
476
- accumulatedUsage.cost.total += costRecord.total ?? 0;
489
+ accumulatedUsage.cost.input += getNumberField(costRecord, "input") ?? 0;
490
+ accumulatedUsage.cost.output += getNumberField(costRecord, "output") ?? 0;
491
+ accumulatedUsage.cost.cacheRead += getNumberField(costRecord, "cacheRead") ?? 0;
492
+ accumulatedUsage.cost.cacheWrite += getNumberField(costRecord, "cacheWrite") ?? 0;
493
+ accumulatedUsage.cost.total += getNumberField(costRecord, "total") ?? 0;
477
494
  }
478
495
  }
479
496
  // Accumulate tokens for progress display
480
497
  progress.tokens += getUsageTokens(messageUsage);
481
498
  }
482
499
  // If pending termination, now we have tokens - terminate
483
- if (pendingTermination && !resolved) {
484
- requestAbort("terminate");
500
+ if (pendingTerminationController) {
501
+ pendingTerminationController.abort();
485
502
  }
486
503
  break;
487
504
  }
@@ -495,7 +512,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
495
512
  if (messageContent && Array.isArray(messageContent)) {
496
513
  for (const block of messageContent) {
497
514
  if (block.type === "text" && block.text) {
498
- finalOutput += block.text;
515
+ finalOutputChunks.push(block.text);
499
516
  }
500
517
  }
501
518
  }
@@ -530,11 +547,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
530
547
 
531
548
  const done = await new Promise<Extract<SubagentWorkerResponse, { type: "done" }>>((resolve) => {
532
549
  const cleanup = () => {
533
- worker.removeEventListener("message", onMessage);
534
- worker.removeEventListener("error", onError);
535
- worker.removeEventListener("close", onClose);
536
- worker.removeEventListener("messageerror", onMessageError);
537
- clearTimers();
550
+ pendingTerminationController = null;
551
+ listenerController.abort();
538
552
  };
539
553
  finalize = (message) => {
540
554
  if (resolved) return;
@@ -594,10 +608,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
594
608
  aborted: abortReason === "signal",
595
609
  });
596
610
  };
597
- worker.addEventListener("message", onMessage);
598
- worker.addEventListener("error", onError);
599
- worker.addEventListener("close", onClose);
600
- worker.addEventListener("messageerror", onMessageError);
611
+ worker.addEventListener("message", onMessage, { signal: listenerSignal });
612
+ worker.addEventListener("error", onError, { signal: listenerSignal });
613
+ worker.addEventListener("close", onClose, { signal: listenerSignal });
614
+ worker.addEventListener("messageerror", onMessageError, { signal: listenerSignal });
601
615
  try {
602
616
  worker.postMessage(startMessage);
603
617
  } catch (err) {
@@ -611,9 +625,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
611
625
  });
612
626
 
613
627
  // Cleanup
614
- if (signal) {
615
- signal.removeEventListener("abort", onAbort);
616
- }
617
628
  try {
618
629
  worker.terminate();
619
630
  } catch {
@@ -626,7 +637,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
626
637
  }
627
638
 
628
639
  // Use final output if available, otherwise accumulated output
629
- let rawOutput = finalOutput || output;
640
+ let rawOutput = finalOutputChunks.length > 0 ? finalOutputChunks.join("") : outputChunks.join("");
630
641
  let abortedViaComplete = false;
631
642
  const completeItems = progress.extractedToolData?.complete as
632
643
  | Array<{ data?: unknown; status?: "success" | "aborted"; error?: string }>
@@ -675,7 +686,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
675
686
  let outputMeta: { lineCount: number; charCount: number } | undefined;
676
687
  if (artifactPaths) {
677
688
  try {
678
- writeFileSync(artifactPaths.outputPath, rawOutput, "utf-8");
689
+ await Bun.write(artifactPaths.outputPath, rawOutput);
679
690
  outputMeta = {
680
691
  lineCount: rawOutput.split("\n").length,
681
692
  charCount: rawOutput.length,