@oh-my-pi/pi-coding-agent 1.337.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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Task tool - Delegate tasks to specialized agents.
3
+ *
4
+ * Discovers agent definitions from:
5
+ * - Bundled agents (shipped with pi-coding-agent)
6
+ * - ~/.pi/agent/agents/*.md (user-level)
7
+ * - .pi/agents/*.md (project-level)
8
+ *
9
+ * Supports:
10
+ * - Single agent execution
11
+ * - Parallel execution with concurrency limits
12
+ * - Progress tracking via JSON events
13
+ * - Session artifacts for debugging
14
+ */
15
+
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";
23
+ import {
24
+ type AgentProgress,
25
+ MAX_AGENTS_IN_DESCRIPTION,
26
+ MAX_CONCURRENCY,
27
+ MAX_PARALLEL_TASKS,
28
+ PI_NO_SUBAGENTS_ENV,
29
+ type TaskToolDetails,
30
+ taskSchema,
31
+ } from "./types.js";
32
+
33
+ /** Session context interface */
34
+ interface SessionContext {
35
+ getSessionFile: () => string | null;
36
+ }
37
+
38
+ // 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";
44
+
45
+ /**
46
+ * Build dynamic tool description listing available agents.
47
+ */
48
+ function buildDescription(cwd: string): string {
49
+ const { agents } = discoverAgents(cwd);
50
+
51
+ const lines: string[] = [];
52
+
53
+ lines.push("Launch a new agent to handle complex, multi-step tasks autonomously.");
54
+ lines.push("");
55
+ lines.push(
56
+ "The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.",
57
+ );
58
+ lines.push("");
59
+ lines.push("Available agent types and the tools they have access to:");
60
+
61
+ for (const agent of agents.slice(0, MAX_AGENTS_IN_DESCRIPTION)) {
62
+ const tools = agent.tools?.join(", ") || "All tools";
63
+ lines.push(`- ${agent.name}: ${agent.description} (Tools: ${tools})`);
64
+ }
65
+ if (agents.length > MAX_AGENTS_IN_DESCRIPTION) {
66
+ lines.push(` ...and ${agents.length - MAX_AGENTS_IN_DESCRIPTION} more agents`);
67
+ }
68
+
69
+ lines.push("");
70
+ lines.push("When NOT to use the Task tool:");
71
+ lines.push(
72
+ "- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly",
73
+ );
74
+ lines.push(
75
+ '- If you are searching for a specific class definition like "class Foo", use the Glob tool instead, to find the match more quickly',
76
+ );
77
+ lines.push(
78
+ "- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly",
79
+ );
80
+ lines.push("- Other tasks that are not related to the agent descriptions above");
81
+ lines.push("");
82
+ lines.push("");
83
+ lines.push("Usage notes:");
84
+ lines.push("- Always include a short description of the task in the task parameter");
85
+ lines.push("- Launch multiple agents concurrently whenever possible, to maximize performance");
86
+ lines.push(
87
+ "- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.",
88
+ );
89
+ lines.push(
90
+ "- Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your task should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.",
91
+ );
92
+ lines.push(
93
+ "- IMPORTANT: Agent results are intermediate data, not task completions. Use the agent's findings to continue executing the user's request. Do not treat agent reports as 'task complete' signals - they provide context for you to perform the actual work.",
94
+ );
95
+ lines.push("- The agent's outputs should generally be trusted");
96
+ lines.push(
97
+ "- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent",
98
+ );
99
+ lines.push(
100
+ "- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.",
101
+ );
102
+ lines.push("");
103
+ lines.push("Parameters:");
104
+ lines.push(
105
+ `- tasks: Array of {agent, task, model?} - tasks to run in parallel (max ${MAX_PARALLEL_TASKS}, ${MAX_CONCURRENCY} concurrent)`,
106
+ );
107
+ lines.push(
108
+ ' - model: (optional) Override the agent\'s default model with fuzzy matching (e.g., "sonnet", "codex", "5.2"). Supports comma-separated fallbacks: "gpt, opus" tries gpt first, then opus. Use "default" for pi\'s default model',
109
+ );
110
+ lines.push(
111
+ "- context: (optional) Shared context string prepended to all task prompts - use this to avoid repeating instructions",
112
+ );
113
+ lines.push("");
114
+ lines.push("Results are always written to {tempdir}/pi-task-{runId}/task_{agent}_{index}.md");
115
+ lines.push("");
116
+ lines.push("Example usage:");
117
+ lines.push("");
118
+ lines.push("<example_agent_descriptions>");
119
+ lines.push('"code-reviewer": use this agent after you are done writing a significant piece of code');
120
+ lines.push('"explore": use this agent for fast codebase exploration and research');
121
+ lines.push("</example_agent_descriptions>");
122
+ lines.push("");
123
+ lines.push("<example>");
124
+ lines.push('user: "Please write a function that checks if a number is prime"');
125
+ lines.push("assistant: Sure let me write a function that checks if a number is prime");
126
+ lines.push("assistant: I'm going to use the Write tool to write the following code:");
127
+ lines.push("<code>");
128
+ lines.push("function isPrime(n) {");
129
+ lines.push(" if (n <= 1) return false");
130
+ lines.push(" for (let i = 2; i * i <= n; i++) {");
131
+ lines.push(" if (n % i === 0) return false");
132
+ lines.push(" }");
133
+ lines.push(" return true");
134
+ lines.push("}");
135
+ lines.push("</code>");
136
+ lines.push("<commentary>");
137
+ lines.push(
138
+ "Since a significant piece of code was written and the task was completed, now use the code-reviewer agent to review the code",
139
+ );
140
+ lines.push("</commentary>");
141
+ lines.push("assistant: Now let me use the code-reviewer agent to review the code");
142
+ lines.push(
143
+ 'assistant: Uses the Task tool: { tasks: [{ agent: "code-reviewer", task: "Review the isPrime function" }] }',
144
+ );
145
+ lines.push("</example>");
146
+ lines.push("");
147
+ lines.push("<example>");
148
+ lines.push('user: "Find all TODO comments in the codebase"');
149
+ lines.push("assistant: I'll use multiple explore agents to search different directories in parallel");
150
+ lines.push("assistant: Uses the Task tool:");
151
+ lines.push("{");
152
+ lines.push(' "context": "Find all TODO comments. Return file:line:content format.",');
153
+ lines.push(' "tasks": [');
154
+ lines.push(' { "agent": "explore", "task": "Search in src/" },');
155
+ lines.push(' { "agent": "explore", "task": "Search in lib/" },');
156
+ lines.push(' { "agent": "explore", "task": "Search in tests/" }');
157
+ lines.push(" ]");
158
+ lines.push("}");
159
+ lines.push("Results → {tempdir}/pi-task-{runId}/task_explore_*.md");
160
+ lines.push("</example>");
161
+
162
+ return lines.join("\n");
163
+ }
164
+
165
+ /**
166
+ * Create the task tool configured for a specific working directory.
167
+ */
168
+ export function createTaskTool(
169
+ cwd: string,
170
+ sessionContext?: SessionContext,
171
+ ): AgentTool<typeof taskSchema, TaskToolDetails, Theme> {
172
+ // Check if subagents are inhibited (recursion prevention)
173
+ if (process.env[PI_NO_SUBAGENTS_ENV]) {
174
+ return {
175
+ name: "task",
176
+ label: "Task",
177
+ description: "Sub-agents disabled (recursion prevention)",
178
+ parameters: taskSchema,
179
+ execute: async () => ({
180
+ content: [{ type: "text", text: "Sub-agents are disabled for this agent (recursion prevention)." }],
181
+ details: {
182
+ projectAgentsDir: null,
183
+ results: [],
184
+ totalDurationMs: 0,
185
+ },
186
+ }),
187
+ };
188
+ }
189
+
190
+ return {
191
+ name: "task",
192
+ label: "Task",
193
+ description: buildDescription(cwd),
194
+ parameters: taskSchema,
195
+ renderCall,
196
+ renderResult,
197
+ execute: async (_toolCallId, params, signal, onUpdate) => {
198
+ const startTime = Date.now();
199
+ const { agents, projectAgentsDir } = discoverAgents(cwd);
200
+ const context = params.context;
201
+
202
+ // Handle empty or missing tasks
203
+ if (!params.tasks || params.tasks.length === 0) {
204
+ const available = agents.map((a) => a.name).join(", ") || "none";
205
+ return {
206
+ content: [
207
+ {
208
+ type: "text",
209
+ text: `No tasks provided. Use: { tasks: [{agent, task}, ...] }\nAvailable agents: ${available}`,
210
+ },
211
+ ],
212
+ details: {
213
+ projectAgentsDir,
214
+ results: [],
215
+ totalDurationMs: 0,
216
+ },
217
+ };
218
+ }
219
+
220
+ // Validate task count
221
+ if (params.tasks.length > MAX_PARALLEL_TASKS) {
222
+ return {
223
+ content: [
224
+ {
225
+ type: "text",
226
+ text: `Too many tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
227
+ },
228
+ ],
229
+ details: {
230
+ projectAgentsDir,
231
+ results: [],
232
+ totalDurationMs: 0,
233
+ },
234
+ };
235
+ }
236
+
237
+ // Derive artifacts directory
238
+ const sessionFile = sessionContext?.getSessionFile() ?? null;
239
+ const artifactsDir = sessionFile ? getArtifactsDir(sessionFile) : null;
240
+ const tempArtifactsDir = artifactsDir ? null : createTempArtifactsDir();
241
+ const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
242
+
243
+ // Initialize progress tracking
244
+ const progressMap = new Map<number, AgentProgress>();
245
+
246
+ // Update callback
247
+ const emitProgress = () => {
248
+ const progress = Array.from(progressMap.values()).sort((a, b) => a.index - b.index);
249
+ onUpdate?.({
250
+ content: [{ type: "text", text: `Running ${params.tasks.length} agents...` }],
251
+ details: {
252
+ projectAgentsDir,
253
+ results: [],
254
+ totalDurationMs: Date.now() - startTime,
255
+ progress,
256
+ },
257
+ });
258
+ };
259
+
260
+ try {
261
+ const tasks = params.tasks;
262
+
263
+ // Validate all agents exist
264
+ for (const task of tasks) {
265
+ if (!getAgent(agents, task.agent)) {
266
+ const available = agents.map((a) => a.name).join(", ");
267
+ return {
268
+ content: [{ type: "text", text: `Unknown agent: ${task.agent}. Available: ${available}` }],
269
+ details: {
270
+ projectAgentsDir,
271
+ results: [],
272
+ totalDurationMs: Date.now() - startTime,
273
+ },
274
+ };
275
+ }
276
+ }
277
+
278
+ // Initialize progress for all tasks
279
+ for (let i = 0; i < tasks.length; i++) {
280
+ const agentCfg = getAgent(agents, tasks[i].agent);
281
+ progressMap.set(i, {
282
+ index: i,
283
+ agent: tasks[i].agent,
284
+ agentSource: agentCfg?.source ?? "user",
285
+ status: "pending",
286
+ task: tasks[i].task,
287
+ recentTools: [],
288
+ recentOutput: [],
289
+ toolCount: 0,
290
+ tokens: 0,
291
+ durationMs: 0,
292
+ modelOverride: tasks[i].model,
293
+ });
294
+ }
295
+ emitProgress();
296
+
297
+ // Build full prompts with context prepended
298
+ const tasksWithContext = tasks.map((t) => ({
299
+ agent: t.agent,
300
+ task: context ? `${context}\n\n${t.task}` : t.task,
301
+ model: t.model,
302
+ }));
303
+
304
+ // Execute in parallel with concurrency limit
305
+ const results = await mapWithConcurrencyLimit(tasksWithContext, MAX_CONCURRENCY, async (task, index) => {
306
+ const agent = getAgent(agents, task.agent)!;
307
+ return runSubprocess({
308
+ cwd,
309
+ agent,
310
+ task: task.task,
311
+ index,
312
+ context: undefined, // Already prepended above
313
+ modelOverride: task.model,
314
+ sessionFile,
315
+ persistArtifacts: !!artifactsDir,
316
+ artifactsDir: effectiveArtifactsDir,
317
+ signal,
318
+ onProgress: (progress) => {
319
+ progressMap.set(index, progress);
320
+ emitProgress();
321
+ },
322
+ });
323
+ });
324
+
325
+ // Write artifacts
326
+ const outputPaths: string[] = [];
327
+ 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;
339
+ }
340
+
341
+ // Build final output - match plugin format
342
+ const successCount = results.filter((r) => r.exitCode === 0).length;
343
+ const totalDuration = Date.now() - startTime;
344
+
345
+ const summaries = results.map((r, i) => {
346
+ const status = r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
347
+ const output = r.output.trim() || r.stderr.trim() || "(no output)";
348
+ const preview = output.split("\n").slice(0, 5).join("\n");
349
+ return `[${r.agent}] ${status} → ${outputPaths[i]}\n${preview}`;
350
+ });
351
+
352
+ const summary = `${successCount}/${results.length} succeeded [${formatDuration(totalDuration)}]\n\n${summaries.join("\n\n---\n\n")}`;
353
+
354
+ // Cleanup temp directory if used
355
+ if (tempArtifactsDir) {
356
+ await cleanupTempDir(tempArtifactsDir);
357
+ }
358
+
359
+ return {
360
+ content: [{ type: "text", text: summary }],
361
+ details: {
362
+ projectAgentsDir,
363
+ results,
364
+ totalDurationMs: totalDuration,
365
+ outputPaths,
366
+ },
367
+ };
368
+ } catch (err) {
369
+ // Cleanup temp directory on error
370
+ if (tempArtifactsDir) {
371
+ await cleanupTempDir(tempArtifactsDir);
372
+ }
373
+
374
+ return {
375
+ content: [{ type: "text", text: `Task execution failed: ${err}` }],
376
+ details: {
377
+ projectAgentsDir,
378
+ results: [],
379
+ totalDurationMs: Date.now() - startTime,
380
+ },
381
+ };
382
+ }
383
+ },
384
+ };
385
+ }
386
+
387
+ // Default task tool using process.cwd()
388
+ export const taskTool = createTaskTool(process.cwd());
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Model resolution with fuzzy pattern matching.
3
+ *
4
+ * Supports:
5
+ * - Exact match: "claude-opus-4-5"
6
+ * - Fuzzy match: "opus" → "claude-opus-4-5"
7
+ * - Comma fallback: "gpt, opus" → tries gpt first, then opus
8
+ * - "default" → undefined (use system default)
9
+ */
10
+
11
+ import { spawnSync } from "node:child_process";
12
+
13
+ /** pi command: 'pi.cmd' on Windows, 'pi' elsewhere */
14
+ const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
15
+
16
+ /** Windows shell option for spawn/spawnSync */
17
+ const PI_SHELL_OPT = process.platform === "win32";
18
+
19
+ /** Cache for available models */
20
+ let cachedModels: string[] | null = null;
21
+
22
+ /** Cache expiry time (5 minutes) */
23
+ let cacheExpiry = 0;
24
+
25
+ const CACHE_TTL_MS = 5 * 60 * 1000;
26
+
27
+ /**
28
+ * Get available models from `pi --list-models`.
29
+ * Caches the result for performance.
30
+ */
31
+ export function getAvailableModels(): string[] {
32
+ const now = Date.now();
33
+ if (cachedModels !== null && now < cacheExpiry) {
34
+ return cachedModels;
35
+ }
36
+
37
+ try {
38
+ const result = spawnSync(PI_CMD, ["--list-models"], {
39
+ encoding: "utf-8",
40
+ timeout: 5000,
41
+ shell: PI_SHELL_OPT,
42
+ });
43
+
44
+ if (result.status !== 0 || !result.stdout) {
45
+ cachedModels = [];
46
+ cacheExpiry = now + CACHE_TTL_MS;
47
+ return cachedModels;
48
+ }
49
+
50
+ // Parse output: skip header line, extract model column
51
+ const lines = result.stdout.trim().split("\n");
52
+ cachedModels = lines
53
+ .slice(1) // Skip header
54
+ .map((line) => {
55
+ const parts = line.trim().split(/\s+/);
56
+ return parts[1]; // Model name is second column
57
+ })
58
+ .filter(Boolean);
59
+
60
+ cacheExpiry = now + CACHE_TTL_MS;
61
+ return cachedModels;
62
+ } catch {
63
+ cachedModels = [];
64
+ cacheExpiry = now + CACHE_TTL_MS;
65
+ return cachedModels;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Clear the model cache (for testing).
71
+ */
72
+ export function clearModelCache(): void {
73
+ cachedModels = null;
74
+ cacheExpiry = 0;
75
+ }
76
+
77
+ /**
78
+ * Resolve a fuzzy model pattern to an actual model name.
79
+ *
80
+ * Supports comma-separated patterns (e.g., "gpt, opus") - tries each in order.
81
+ * Returns undefined if pattern is "default", undefined, or no match found.
82
+ *
83
+ * @param pattern - Model pattern to resolve
84
+ * @param availableModels - Optional pre-fetched list of available models
85
+ */
86
+ export function resolveModelPattern(pattern: string | undefined, availableModels?: string[]): string | undefined {
87
+ if (!pattern || pattern === "default") {
88
+ return undefined;
89
+ }
90
+
91
+ const models = availableModels ?? getAvailableModels();
92
+ if (models.length === 0) {
93
+ // Fallback: return pattern as-is if we can't get available models
94
+ return pattern;
95
+ }
96
+
97
+ // Split by comma, try each pattern in order
98
+ const patterns = pattern
99
+ .split(",")
100
+ .map((p) => p.trim().toLowerCase())
101
+ .filter(Boolean);
102
+
103
+ for (const p of patterns) {
104
+ // Try exact match first
105
+ const exactMatch = models.find((m) => m.toLowerCase() === p);
106
+ if (exactMatch) return exactMatch;
107
+
108
+ // Try fuzzy match (substring)
109
+ const fuzzyMatch = models.find((m) => m.toLowerCase().includes(p));
110
+ if (fuzzyMatch) return fuzzyMatch;
111
+ }
112
+
113
+ // No match found - use default model
114
+ return undefined;
115
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Parallel execution with concurrency control.
3
+ */
4
+
5
+ import { MAX_CONCURRENCY } from "./types.js";
6
+
7
+ /**
8
+ * Execute items with a concurrency limit using a worker pool pattern.
9
+ * Results are returned in the same order as input items.
10
+ *
11
+ * @param items - Items to process
12
+ * @param concurrency - Maximum concurrent operations
13
+ * @param fn - Async function to execute for each item
14
+ */
15
+ export async function mapWithConcurrencyLimit<T, R>(
16
+ items: T[],
17
+ concurrency: number,
18
+ fn: (item: T, index: number) => Promise<R>,
19
+ ): Promise<R[]> {
20
+ const limit = Math.max(1, Math.min(concurrency, items.length, MAX_CONCURRENCY));
21
+ const results: R[] = new Array(items.length);
22
+ let nextIndex = 0;
23
+
24
+ const worker = async (): Promise<void> => {
25
+ while (nextIndex < items.length) {
26
+ const index = nextIndex++;
27
+ results[index] = await fn(items[index], index);
28
+ }
29
+ };
30
+
31
+ // Create worker pool
32
+ const workers = Array(limit)
33
+ .fill(null)
34
+ .map(() => worker());
35
+
36
+ await Promise.all(workers);
37
+ return results;
38
+ }