@oh-my-pi/pi-coding-agent 1.341.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 (151) hide show
  1. package/CHANGELOG.md +73 -0
  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 +5 -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 +157 -15
  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 +2 -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 +77 -35
  54. package/src/core/session-manager.ts +6 -6
  55. package/src/core/settings-manager.ts +16 -3
  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 +2 -2
  60. package/src/core/tools/bash.ts +32 -155
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +3 -3
  63. package/src/core/tools/edit.ts +18 -5
  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 +3 -3
  75. package/src/core/tools/index.ts +48 -34
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +161 -90
  78. package/src/core/tools/lsp/config.ts +1 -1
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +15 -13
  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/utils.ts +1 -1
  84. package/src/core/tools/notebook.ts +1 -1
  85. package/src/core/tools/output.ts +175 -0
  86. package/src/core/tools/read.ts +7 -7
  87. package/src/core/tools/renderers.ts +92 -13
  88. package/src/core/tools/review.ts +268 -0
  89. package/src/core/tools/task/agents.ts +1 -1
  90. package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
  91. package/src/core/tools/task/discovery.ts +2 -2
  92. package/src/core/tools/task/executor.ts +145 -28
  93. package/src/core/tools/task/index.ts +78 -30
  94. package/src/core/tools/task/model-resolver.ts +30 -20
  95. package/src/core/tools/task/parallel.ts +1 -1
  96. package/src/core/tools/task/render.ts +219 -30
  97. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  98. package/src/core/tools/task/types.ts +36 -2
  99. package/src/core/tools/web-fetch.ts +5 -3
  100. package/src/core/tools/web-search/auth.ts +1 -1
  101. package/src/core/tools/web-search/index.ts +17 -15
  102. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  103. package/src/core/tools/web-search/providers/exa.ts +3 -5
  104. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  105. package/src/core/tools/web-search/render.ts +3 -3
  106. package/src/core/tools/write.ts +4 -4
  107. package/src/index.ts +29 -18
  108. package/src/main.ts +37 -32
  109. package/src/migrations.ts +3 -3
  110. package/src/modes/index.ts +5 -5
  111. package/src/modes/interactive/components/armin.ts +1 -1
  112. package/src/modes/interactive/components/assistant-message.ts +1 -1
  113. package/src/modes/interactive/components/bash-execution.ts +4 -4
  114. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  115. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  116. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  117. package/src/modes/interactive/components/diff.ts +1 -1
  118. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  119. package/src/modes/interactive/components/footer.ts +5 -5
  120. package/src/modes/interactive/components/hook-editor.ts +2 -2
  121. package/src/modes/interactive/components/hook-input.ts +2 -2
  122. package/src/modes/interactive/components/hook-message.ts +3 -3
  123. package/src/modes/interactive/components/hook-selector.ts +2 -2
  124. package/src/modes/interactive/components/model-selector.ts +281 -59
  125. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  126. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  127. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  128. package/src/modes/interactive/components/session-selector.ts +4 -4
  129. package/src/modes/interactive/components/settings-defs.ts +1 -1
  130. package/src/modes/interactive/components/settings-selector.ts +5 -5
  131. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  132. package/src/modes/interactive/components/theme-selector.ts +2 -2
  133. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  134. package/src/modes/interactive/components/tool-execution.ts +26 -8
  135. package/src/modes/interactive/components/tree-selector.ts +3 -3
  136. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  137. package/src/modes/interactive/components/user-message.ts +1 -1
  138. package/src/modes/interactive/components/welcome.ts +2 -2
  139. package/src/modes/interactive/interactive-mode.ts +85 -41
  140. package/src/modes/interactive/theme/theme.ts +8 -7
  141. package/src/modes/print-mode.ts +4 -3
  142. package/src/modes/rpc/rpc-client.ts +4 -4
  143. package/src/modes/rpc/rpc-mode.ts +21 -11
  144. package/src/modes/rpc/rpc-types.ts +3 -3
  145. package/src/utils/changelog.ts +2 -2
  146. package/src/utils/clipboard.ts +1 -1
  147. package/src/utils/shell-snapshot.ts +218 -0
  148. package/src/utils/shell.ts +93 -13
  149. package/src/utils/tools-manager.ts +1 -1
  150. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  151. package/src/core/tools/exa/logger.ts +0 -56
@@ -1,59 +1,74 @@
1
1
  ---
2
2
  name: reviewer
3
- description: Expert code reviewer for PRs and implementation changes
4
- tools: read, grep, glob, ls, bash
3
+ description: Code review specialist for quality and security analysis
4
+ tools: read, grep, find, ls, bash, task, report_finding, submit_review
5
5
  model: pi/slow, gpt-5.2-codex, gpt-5.2, codex, gpt
6
6
  ---
7
7
 
8
- You are an expert code reviewer. Analyze code changes and provide thorough reviews.
8
+ You are acting as a reviewer for a proposed code change made by another engineer.
9
9
 
10
- ## For PR Reviews
10
+ Bash is for read-only commands only: `git diff`, `git log`, `git show`, `gh pr diff`. Do NOT modify files or run builds.
11
11
 
12
- 1. If no PR number provided, run `gh pr list` to show open PRs
13
- 2. If PR number provided:
14
- - `gh pr view <number>` to get PR details
15
- - `gh pr diff <number>` to get the diff
16
- 3. Analyze changes and provide review
12
+ # Review Strategy
17
13
 
18
- ## For Implementation Reviews
14
+ 1. Run `git diff` (or `gh pr diff <number>`) to see the changes
15
+ 2. Read the modified files for full context
16
+ 3. For large changes spanning multiple files/modules, use `task` with `explore` agents in parallel to gather context faster
17
+ 4. Analyze for bugs, security issues, and code quality problems
18
+ 5. Use `report_finding` for each issue found
19
+ 6. Use `submit_review` to provide final verdict
19
20
 
20
- When reviewing implementation output from another agent:
21
+ # Parallelization
21
22
 
22
- 1. Read the files that were changed
23
- 2. Understand the context and requirements
24
- 3. Analyze the implementation quality
23
+ For reviews touching many files, spawn `explore` agents to research in parallel:
24
+ - Each agent can investigate a different module or concern
25
+ - Example: one explores test coverage, another checks related implementations
26
+ - Gather their findings, then synthesize into your review
25
27
 
26
- ## Review Focus
28
+ # What to Flag
27
29
 
28
- - **Correctness**: Does the code do what it's supposed to?
29
- - **Project Conventions**: Does it follow existing patterns?
30
- - **Performance**: Any performance implications?
31
- - **Test Coverage**: Are changes adequately tested?
32
- - **Security**: Any security considerations?
33
- - **Edge Cases**: Are edge cases handled?
30
+ Only flag issues where ALL of these apply:
34
31
 
35
- ## Output Format
32
+ 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code
33
+ 2. The bug is discrete and actionable (not a general issue or combination of multiple issues)
34
+ 3. Fixing it doesn't demand rigor not present elsewhere in the codebase
35
+ 4. The bug was introduced in this commit (don't flag pre-existing bugs)
36
+ 5. The author would likely fix the issue if made aware of it
37
+ 6. The bug doesn't rely on unstated assumptions about the codebase or author's intent
38
+ 7. You can identify specific code that is provably affected (speculation is not enough)
39
+ 8. The issue is clearly not an intentional change by the author
36
40
 
37
- ### Overview
41
+ # Priority Levels
38
42
 
39
- What the changes do.
43
+ - **P0**: Drop everything to fix. Blocking release, operations, or major usage. Only use for universal issues that do not depend on assumptions about inputs.
44
+ - **P1**: Urgent. Should be addressed in the next cycle.
45
+ - **P2**: Normal. To be fixed eventually.
46
+ - **P3**: Low. Nice to have.
40
47
 
41
- ### Strengths
48
+ # Comment Guidelines
42
49
 
43
- What's done well.
50
+ 1. Be clear about WHY the issue is a bug
51
+ 2. Communicate severity appropriately - don't overstate
52
+ 3. Keep body to one paragraph max
53
+ 4. Code snippets should be ≤3 lines, wrapped in markdown code tags
54
+ 5. Clearly state what conditions are necessary for the bug to arise
55
+ 6. Tone: matter-of-fact, not accusatory or overly positive
56
+ 7. Write so the author can immediately grasp the idea without close reading
57
+ 8. Avoid flattery and phrases like "Great job...", "Thanks for..."
44
58
 
45
- ### Issues
59
+ # CRITICAL
46
60
 
47
- Problems that should be fixed (with file:line references).
61
+ You MUST call `submit_review` before ending your response, even if you found no issues.
62
+ The review is only considered complete when `submit_review` is called.
63
+ Failure to call `submit_review` means the review was not submitted.
48
64
 
49
- ### Suggestions
65
+ # Output
50
66
 
51
- Improvements to consider (optional, not blocking).
67
+ - Use `report_finding` for each issue. Continue until you've listed every qualifying finding.
68
+ - If there is no finding that a person would definitely want to fix, prefer outputting no findings.
69
+ - Ignore trivial style unless it obscures meaning or violates documented standards.
70
+ - Use `submit_review` at the end with your overall verdict:
71
+ - **correct**: Existing code and tests will not break, patch is free of bugs and blocking issues
72
+ - **incorrect**: Has bugs or blocking issues that must be addressed
52
73
 
53
- ### Verdict
54
-
55
- - ✅ **Approve**: Ready to merge/complete
56
- - 🔄 **Request Changes**: Issues must be addressed
57
- - 💬 **Comment**: Minor suggestions, can proceed
58
-
59
- Keep reviews concise but thorough. Focus on substance over style nitpicks.
74
+ Ignore non-blocking issues (style, formatting, typos, documentation, nits) when determining correctness.
@@ -13,8 +13,8 @@
13
13
  import * as fs from "node:fs";
14
14
  import * as os from "node:os";
15
15
  import * as path from "node:path";
16
- import { loadBundledAgents } from "./agents.js";
17
- import type { AgentDefinition, AgentSource } from "./types.js";
16
+ import { loadBundledAgents } from "./agents";
17
+ import type { AgentDefinition, AgentSource } from "./types";
18
18
 
19
19
  /** Result of agent discovery */
20
20
  export interface DiscoveryResult {
@@ -10,15 +10,17 @@ import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
  import * as readline from "node:readline";
13
- import { resolveModelPattern } from "./model-resolver.js";
13
+ import { ensureArtifactsDir, getArtifactPaths } from "./artifacts";
14
+ import { resolveModelPattern } from "./model-resolver";
15
+ import { subprocessToolRegistry } from "./subprocess-tool-registry";
14
16
  import {
15
17
  type AgentDefinition,
16
18
  type AgentProgress,
17
19
  MAX_OUTPUT_BYTES,
18
20
  MAX_OUTPUT_LINES,
19
- PI_NO_SUBAGENTS_ENV,
21
+ PI_BLOCKED_AGENT_ENV,
20
22
  type SingleResult,
21
- } from "./types.js";
23
+ } from "./types";
22
24
 
23
25
  /** pi command: 'pi.cmd' on Windows, 'pi' elsewhere */
24
26
  const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
@@ -166,6 +168,23 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
166
168
  // Build full task with context
167
169
  const fullTask = context ? `${context}\n\n${task}` : task;
168
170
 
171
+ // Set up artifact paths and write input file upfront if artifacts dir provided
172
+ let artifactPaths: { inputPath: string; outputPath: string; jsonlPath: string } | undefined;
173
+ let subtaskSessionFile: string | undefined;
174
+
175
+ if (options.artifactsDir) {
176
+ ensureArtifactsDir(options.artifactsDir);
177
+ artifactPaths = getArtifactPaths(options.artifactsDir, agent.name, index);
178
+ subtaskSessionFile = artifactPaths.jsonlPath;
179
+
180
+ // Write input file immediately (real-time visibility)
181
+ try {
182
+ fs.writeFileSync(artifactPaths.inputPath, fullTask, "utf-8");
183
+ } catch {
184
+ // Non-fatal, continue without input artifact
185
+ }
186
+ }
187
+
169
188
  // Build args
170
189
  const args: string[] = ["--mode", "json", "--non-interactive"];
171
190
 
@@ -183,8 +202,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
183
202
  args.push("--model", resolvedModel);
184
203
  }
185
204
 
186
- // Add session options
187
- if (options.sessionFile) {
205
+ // Add session options - use subtask-specific session file for real-time streaming
206
+ if (subtaskSessionFile) {
207
+ args.push("--session", subtaskSessionFile);
208
+ } else if (options.sessionFile) {
188
209
  args.push("--session", options.sessionFile);
189
210
  } else {
190
211
  args.push("--no-session");
@@ -193,10 +214,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
193
214
  // Add task as prompt
194
215
  args.push("--prompt", fullTask);
195
216
 
196
- // Set up environment
217
+ // Set up environment - block same-agent recursion unless explicitly recursive
197
218
  const env = { ...process.env };
198
219
  if (!agent.recursive) {
199
- env[PI_NO_SUBAGENTS_ENV] = "1";
220
+ env[PI_BLOCKED_AGENT_ENV] = agent.name;
200
221
  }
201
222
 
202
223
  // Spawn subprocess
@@ -211,6 +232,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
211
232
  let stderr = "";
212
233
  let finalOutput = "";
213
234
  let resolved = false;
235
+ let pendingTermination = false; // Set when shouldTerminate fires, wait for message_end
214
236
  const jsonlEvents: string[] = [];
215
237
 
216
238
  // Handle abort signal
@@ -242,7 +264,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
242
264
  progress.currentToolStartMs = now;
243
265
  break;
244
266
 
245
- case "tool_execution_end":
267
+ case "tool_execution_end": {
246
268
  if (progress.currentTool) {
247
269
  progress.recentTools.unshift({
248
270
  tool: progress.currentTool,
@@ -257,24 +279,73 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
257
279
  progress.currentTool = undefined;
258
280
  progress.currentToolArgs = undefined;
259
281
  progress.currentToolStartMs = undefined;
282
+
283
+ // Check for registered subprocess tool handler
284
+ const handler = subprocessToolRegistry.getHandler(event.toolName);
285
+ if (handler) {
286
+ // Extract data using handler
287
+ if (handler.extractData) {
288
+ const data = handler.extractData({
289
+ toolName: event.toolName,
290
+ toolCallId: event.toolCallId,
291
+ args: event.args,
292
+ result: event.result,
293
+ isError: event.isError,
294
+ });
295
+ if (data !== undefined) {
296
+ progress.extractedToolData = progress.extractedToolData || {};
297
+ progress.extractedToolData[event.toolName] = progress.extractedToolData[event.toolName] || [];
298
+ progress.extractedToolData[event.toolName].push(data);
299
+ }
300
+ }
301
+
302
+ // Check if handler wants to terminate subprocess
303
+ if (
304
+ handler.shouldTerminate?.({
305
+ toolName: event.toolName,
306
+ toolCallId: event.toolCallId,
307
+ args: event.args,
308
+ result: event.result,
309
+ isError: event.isError,
310
+ })
311
+ ) {
312
+ // Don't kill immediately - wait for message_end to get token counts
313
+ pendingTermination = true;
314
+ // Safety timeout in case message_end never arrives
315
+ setTimeout(() => {
316
+ if (!resolved) {
317
+ resolved = true;
318
+ proc.kill("SIGTERM");
319
+ }
320
+ }, 2000);
321
+ }
322
+ }
323
+ break;
324
+ }
325
+
326
+ case "message_update": {
327
+ // Extract text for progress display only (replace, don't accumulate)
328
+ const updateContent = event.message?.content || event.content;
329
+ if (updateContent && Array.isArray(updateContent)) {
330
+ const allText: string[] = [];
331
+ for (const block of updateContent) {
332
+ if (block.type === "text" && block.text) {
333
+ const lines = block.text.split("\n").filter((l: string) => l.trim());
334
+ allText.push(...lines);
335
+ }
336
+ }
337
+ // Show last 8 lines from current state (not accumulated)
338
+ progress.recentOutput = allText.slice(-8).reverse();
339
+ }
260
340
  break;
341
+ }
261
342
 
262
- case "message_update":
263
343
  case "message_end": {
264
- // Extract text content for recent output (prefer message.content, fallback to event.content)
344
+ // Extract final text content from completed message
265
345
  const messageContent = event.message?.content || event.content;
266
346
  if (messageContent && Array.isArray(messageContent)) {
267
347
  for (const block of messageContent) {
268
348
  if (block.type === "text" && block.text) {
269
- const lines = block.text.split("\n").filter((l: string) => l.trim());
270
- for (const l of lines) {
271
- if (!progress.recentOutput.includes(l)) {
272
- progress.recentOutput.unshift(l);
273
- if (progress.recentOutput.length > 8) {
274
- progress.recentOutput.pop();
275
- }
276
- }
277
- }
278
349
  output += block.text;
279
350
  }
280
351
  }
@@ -282,7 +353,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
282
353
  // Extract usage (prefer message.usage, fallback to event.usage)
283
354
  const messageUsage = event.message?.usage || event.usage;
284
355
  if (messageUsage) {
285
- progress.tokens = (messageUsage.input_tokens || 0) + (messageUsage.output_tokens || 0);
356
+ // Accumulate tokens across messages (not overwrite)
357
+ progress.tokens += (messageUsage.input_tokens || 0) + (messageUsage.output_tokens || 0);
358
+ }
359
+ // If pending termination, now we have tokens - terminate
360
+ if (pendingTermination && !resolved) {
361
+ resolved = true;
362
+ proc.kill("SIGTERM");
286
363
  }
287
364
  break;
288
365
  }
@@ -304,7 +381,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
304
381
  }
305
382
 
306
383
  progress.durationMs = now - startTime;
307
- onProgress?.(progress);
384
+ // Clone progress object before passing to callback to prevent mutation during render
385
+ onProgress?.({ ...progress });
308
386
  } catch {
309
387
  // Ignore non-JSON lines
310
388
  }
@@ -316,16 +394,35 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
316
394
  stderr += stderrDecoder.decode(chunk, { stream: true });
317
395
  });
318
396
 
319
- // Wait for process to exit
397
+ // Wait for readline to finish BEFORE resolving
320
398
  const exitCode = await new Promise<number>((resolve) => {
321
- proc.on("close", (code) => {
322
- resolved = true;
323
- resolve(code ?? 1);
399
+ let code: number | null = null;
400
+ let rlClosed = false;
401
+ let procClosed = false;
402
+
403
+ const maybeResolve = () => {
404
+ if (rlClosed && procClosed) {
405
+ resolved = true;
406
+ resolve(code ?? 1);
407
+ }
408
+ };
409
+
410
+ rl.on("close", () => {
411
+ rlClosed = true;
412
+ maybeResolve();
413
+ });
414
+
415
+ proc.on("close", (c) => {
416
+ code = c;
417
+ procClosed = true;
418
+ maybeResolve();
324
419
  });
420
+
325
421
  proc.on("error", (err) => {
326
- resolved = true;
327
422
  stderr += `\nProcess error: ${err.message}`;
328
- resolve(1);
423
+ code = 1;
424
+ procClosed = true;
425
+ maybeResolve();
329
426
  });
330
427
  });
331
428
 
@@ -344,8 +441,24 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
344
441
  const rawOutput = finalOutput || output;
345
442
  const { text: truncatedOutput, truncated } = truncateOutput(rawOutput);
346
443
 
444
+ // Write output artifact (input and jsonl already written in real-time)
445
+ // Compute output metadata for Output tool integration
446
+ let outputMeta: { lineCount: number; charCount: number } | undefined;
447
+ if (artifactPaths) {
448
+ try {
449
+ fs.writeFileSync(artifactPaths.outputPath, rawOutput, "utf-8");
450
+ outputMeta = {
451
+ lineCount: rawOutput.split("\n").length,
452
+ charCount: rawOutput.length,
453
+ };
454
+ } catch {
455
+ // Non-fatal
456
+ }
457
+ }
458
+
347
459
  // Update final progress
348
- progress.status = exitCode === 0 ? "completed" : "failed";
460
+ const wasAborted = signal?.aborted ?? false;
461
+ progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
349
462
  progress.durationMs = Date.now() - startTime;
350
463
  onProgress?.(progress);
351
464
 
@@ -362,6 +475,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
362
475
  tokens: progress.tokens,
363
476
  modelOverride,
364
477
  error: exitCode !== 0 && stderr ? stderr : undefined,
478
+ aborted: wasAborted,
365
479
  jsonlEvents,
480
+ artifactPaths,
481
+ extractedToolData: progress.extractedToolData,
482
+ outputMeta,
366
483
  };
367
484
  }
@@ -14,33 +14,50 @@
14
14
  */
15
15
 
16
16
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
17
- import type { Theme } from "../../../modes/interactive/theme/theme.js";
18
- import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir, writeArtifacts } from "./artifacts.js";
19
- import { discoverAgents, getAgent } from "./discovery.js";
20
- import { runSubprocess } from "./executor.js";
21
- import { mapWithConcurrencyLimit } from "./parallel.js";
22
- import { formatDuration, renderCall, renderResult } from "./render.js";
17
+ import type { Theme } from "../../../modes/interactive/theme/theme";
18
+ import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir } from "./artifacts";
19
+ import { discoverAgents, getAgent } from "./discovery";
20
+ import { runSubprocess } from "./executor";
21
+ import { mapWithConcurrencyLimit } from "./parallel";
22
+ import { formatDuration, renderCall, renderResult } from "./render";
23
23
  import {
24
24
  type AgentProgress,
25
25
  MAX_AGENTS_IN_DESCRIPTION,
26
26
  MAX_CONCURRENCY,
27
27
  MAX_PARALLEL_TASKS,
28
+ PI_BLOCKED_AGENT_ENV,
28
29
  PI_NO_SUBAGENTS_ENV,
29
30
  type TaskToolDetails,
30
31
  taskSchema,
31
- } from "./types.js";
32
+ } from "./types";
33
+
34
+ // Import review tools for side effects (registers subprocess tool handlers)
35
+ import "../review";
36
+
37
+ /** Format byte count for display */
38
+ function formatBytes(bytes: number): string {
39
+ if (bytes < 1024) return `${bytes}B`;
40
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
41
+ return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
42
+ }
32
43
 
33
44
  /** Session context interface */
34
45
  interface SessionContext {
35
46
  getSessionFile: () => string | null;
36
47
  }
37
48
 
49
+ /** Task tool options */
50
+ interface TaskToolOptions {
51
+ /** Set of available tool names (for cross-tool awareness) */
52
+ availableTools?: Set<string>;
53
+ }
54
+
38
55
  // Re-export types and utilities
39
- export { loadBundledAgents as BUNDLED_AGENTS } from "./agents.js";
40
- export { discoverCommands, expandCommand, getCommand } from "./commands.js";
41
- export { discoverAgents, getAgent } from "./discovery.js";
42
- export type { AgentDefinition, AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types.js";
43
- export { taskSchema } from "./types.js";
56
+ export { loadBundledAgents as BUNDLED_AGENTS } from "./agents";
57
+ export { discoverCommands, expandCommand, getCommand } from "./commands";
58
+ export { discoverAgents, getAgent } from "./discovery";
59
+ export type { AgentDefinition, AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types";
60
+ export { taskSchema } from "./types";
44
61
 
45
62
  /**
46
63
  * Build dynamic tool description listing available agents.
@@ -168,8 +185,10 @@ function buildDescription(cwd: string): string {
168
185
  export function createTaskTool(
169
186
  cwd: string,
170
187
  sessionContext?: SessionContext,
188
+ options?: TaskToolOptions,
171
189
  ): AgentTool<typeof taskSchema, TaskToolDetails, Theme> {
172
- // Check if subagents are inhibited (recursion prevention)
190
+ const hasOutputTool = options?.availableTools?.has("output") ?? false;
191
+ // Check if subagents are completely inhibited (legacy recursion prevention)
173
192
  if (process.env[PI_NO_SUBAGENTS_ENV]) {
174
193
  return {
175
194
  name: "task",
@@ -187,6 +206,9 @@ export function createTaskTool(
187
206
  };
188
207
  }
189
208
 
209
+ // Check for same-agent blocking (allows other agent types)
210
+ const blockedAgent = process.env[PI_BLOCKED_AGENT_ENV];
211
+
190
212
  return {
191
213
  name: "task",
192
214
  label: "Task",
@@ -258,7 +280,31 @@ export function createTaskTool(
258
280
  };
259
281
 
260
282
  try {
261
- const tasks = params.tasks;
283
+ let tasks = params.tasks;
284
+ let skippedSelfRecursion = 0;
285
+
286
+ // Filter out blocked agent (self-recursion prevention)
287
+ if (blockedAgent) {
288
+ const blockedTasks = tasks.filter((t) => t.agent === blockedAgent);
289
+ tasks = tasks.filter((t) => t.agent !== blockedAgent);
290
+ skippedSelfRecursion = blockedTasks.length;
291
+
292
+ if (skippedSelfRecursion > 0 && tasks.length === 0) {
293
+ return {
294
+ content: [
295
+ {
296
+ type: "text",
297
+ text: `Cannot spawn ${blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
298
+ },
299
+ ],
300
+ details: {
301
+ projectAgentsDir,
302
+ results: [],
303
+ totalDurationMs: Date.now() - startTime,
304
+ },
305
+ };
306
+ }
307
+ }
262
308
 
263
309
  // Validate all agents exist
264
310
  for (const task of tasks) {
@@ -316,40 +362,42 @@ export function createTaskTool(
316
362
  artifactsDir: effectiveArtifactsDir,
317
363
  signal,
318
364
  onProgress: (progress) => {
319
- progressMap.set(index, progress);
365
+ progressMap.set(index, structuredClone(progress));
320
366
  emitProgress();
321
367
  },
322
368
  });
323
369
  });
324
370
 
325
- // Write artifacts
371
+ // Collect output paths (artifacts already written by executor in real-time)
326
372
  const outputPaths: string[] = [];
327
373
  for (const result of results) {
328
- const fullTask = context ? `${context}\n\n${result.task}` : result.task;
329
- const paths = await writeArtifacts(
330
- effectiveArtifactsDir,
331
- result.agent,
332
- result.index,
333
- fullTask,
334
- result.output,
335
- result.jsonlEvents,
336
- );
337
- outputPaths.push(paths.outputPath);
338
- result.artifactPaths = paths;
374
+ if (result.artifactPaths) {
375
+ outputPaths.push(result.artifactPaths.outputPath);
376
+ }
339
377
  }
340
378
 
341
379
  // Build final output - match plugin format
342
380
  const successCount = results.filter((r) => r.exitCode === 0).length;
343
381
  const totalDuration = Date.now() - startTime;
344
382
 
345
- const summaries = results.map((r, i) => {
383
+ const summaries = results.map((r) => {
346
384
  const status = r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
347
385
  const output = r.output.trim() || r.stderr.trim() || "(no output)";
348
386
  const preview = output.split("\n").slice(0, 5).join("\n");
349
- return `[${r.agent}] ${status} ${outputPaths[i]}\n${preview}`;
387
+ // Include output metadata and ID; include path only if Output tool unavailable (for Read fallback)
388
+ const outputId = `${r.agent}_${r.index}`;
389
+ const meta = r.outputMeta
390
+ ? ` [${r.outputMeta.lineCount} lines, ${formatBytes(r.outputMeta.charCount)}]`
391
+ : "";
392
+ const pathInfo = !hasOutputTool && r.artifactPaths?.outputPath ? ` (${r.artifactPaths.outputPath})` : "";
393
+ return `[${r.agent}] ${status}${meta} → ${outputId}${pathInfo}\n${preview}`;
350
394
  });
351
395
 
352
- const summary = `${successCount}/${results.length} succeeded [${formatDuration(totalDuration)}]\n\n${summaries.join("\n\n---\n\n")}`;
396
+ const skippedNote =
397
+ skippedSelfRecursion > 0
398
+ ? ` (${skippedSelfRecursion} ${blockedAgent} task${skippedSelfRecursion > 1 ? "s" : ""} skipped - self-recursion blocked)`
399
+ : "";
400
+ const summary = `${successCount}/${results.length} succeeded${skippedNote} [${formatDuration(totalDuration)}]\n\n${summaries.join("\n\n---\n\n")}`;
353
401
 
354
402
  // Cleanup temp directory if used
355
403
  if (tempArtifactsDir) {