@draht/coding-agent 2026.3.3 → 2026.3.5

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 (201) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/README.md +1 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +1 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +5 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config.d.ts +0 -7
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +0 -12
  12. package/dist/config.js.map +1 -1
  13. package/dist/core/agent-session.d.ts +14 -4
  14. package/dist/core/agent-session.d.ts.map +1 -1
  15. package/dist/core/agent-session.js +167 -49
  16. package/dist/core/agent-session.js.map +1 -1
  17. package/dist/core/auth-storage.d.ts +1 -1
  18. package/dist/core/auth-storage.d.ts.map +1 -1
  19. package/dist/core/auth-storage.js +2 -1
  20. package/dist/core/auth-storage.js.map +1 -1
  21. package/dist/core/builtins/subagent.d.ts +14 -0
  22. package/dist/core/builtins/subagent.d.ts.map +1 -0
  23. package/dist/core/builtins/subagent.js +492 -0
  24. package/dist/core/builtins/subagent.js.map +1 -0
  25. package/dist/core/compaction/compaction.d.ts.map +1 -1
  26. package/dist/core/compaction/compaction.js +4 -1
  27. package/dist/core/compaction/compaction.js.map +1 -1
  28. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  29. package/dist/core/export-html/tool-renderer.js +6 -0
  30. package/dist/core/export-html/tool-renderer.js.map +1 -1
  31. package/dist/core/extensions/loader.d.ts.map +1 -1
  32. package/dist/core/extensions/loader.js +19 -8
  33. package/dist/core/extensions/loader.js.map +1 -1
  34. package/dist/core/extensions/runner.d.ts.map +1 -1
  35. package/dist/core/extensions/runner.js +1 -0
  36. package/dist/core/extensions/runner.js.map +1 -1
  37. package/dist/core/extensions/types.d.ts +8 -2
  38. package/dist/core/extensions/types.d.ts.map +1 -1
  39. package/dist/core/extensions/types.js.map +1 -1
  40. package/dist/core/model-registry.d.ts +1 -0
  41. package/dist/core/model-registry.d.ts.map +1 -1
  42. package/dist/core/model-registry.js +9 -6
  43. package/dist/core/model-registry.js.map +1 -1
  44. package/dist/core/model-resolver.d.ts.map +1 -1
  45. package/dist/core/model-resolver.js +35 -5
  46. package/dist/core/model-resolver.js.map +1 -1
  47. package/dist/core/package-manager.d.ts.map +1 -1
  48. package/dist/core/package-manager.js +1 -10
  49. package/dist/core/package-manager.js.map +1 -1
  50. package/dist/core/prompt-templates.d.ts.map +1 -1
  51. package/dist/core/prompt-templates.js +4 -2
  52. package/dist/core/prompt-templates.js.map +1 -1
  53. package/dist/core/resource-loader.d.ts +2 -0
  54. package/dist/core/resource-loader.d.ts.map +1 -1
  55. package/dist/core/resource-loader.js +5 -1
  56. package/dist/core/resource-loader.js.map +1 -1
  57. package/dist/core/sdk.d.ts +1 -1
  58. package/dist/core/sdk.d.ts.map +1 -1
  59. package/dist/core/sdk.js.map +1 -1
  60. package/dist/core/session-manager.js.map +1 -1
  61. package/dist/core/settings-manager.d.ts +3 -0
  62. package/dist/core/settings-manager.d.ts.map +1 -1
  63. package/dist/core/settings-manager.js +4 -0
  64. package/dist/core/settings-manager.js.map +1 -1
  65. package/dist/core/system-prompt.d.ts +4 -0
  66. package/dist/core/system-prompt.d.ts.map +1 -1
  67. package/dist/core/system-prompt.js +34 -12
  68. package/dist/core/system-prompt.js.map +1 -1
  69. package/dist/core/tools/edit-diff.js.map +1 -1
  70. package/dist/core/tools/path-utils.js.map +1 -1
  71. package/dist/gsd/domain-validator.d.ts +18 -0
  72. package/dist/gsd/domain-validator.d.ts.map +1 -0
  73. package/dist/gsd/domain-validator.js +61 -0
  74. package/dist/gsd/domain-validator.js.map +1 -0
  75. package/dist/gsd/domain.d.ts +12 -0
  76. package/dist/gsd/domain.d.ts.map +1 -0
  77. package/dist/gsd/domain.js +113 -0
  78. package/dist/gsd/domain.js.map +1 -0
  79. package/dist/gsd/git.d.ts +20 -0
  80. package/dist/gsd/git.d.ts.map +1 -0
  81. package/dist/gsd/git.js +59 -0
  82. package/dist/gsd/git.js.map +1 -0
  83. package/dist/gsd/hook-utils.d.ts +22 -0
  84. package/dist/gsd/hook-utils.d.ts.map +1 -0
  85. package/dist/gsd/hook-utils.js +100 -0
  86. package/dist/gsd/hook-utils.js.map +1 -0
  87. package/dist/gsd/index.d.ts +9 -0
  88. package/dist/gsd/index.d.ts.map +1 -0
  89. package/dist/gsd/index.js +8 -0
  90. package/dist/gsd/index.js.map +1 -0
  91. package/dist/gsd/planning.d.ts +20 -0
  92. package/dist/gsd/planning.d.ts.map +1 -0
  93. package/dist/gsd/planning.js +167 -0
  94. package/dist/gsd/planning.js.map +1 -0
  95. package/dist/hooks/gsd/draht-post-task.js +44 -11
  96. package/dist/hooks/gsd/draht-quality-gate.js +99 -57
  97. package/dist/index.d.ts +2 -0
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +2 -0
  100. package/dist/index.js.map +1 -1
  101. package/dist/main.d.ts.map +1 -1
  102. package/dist/main.js +5 -5
  103. package/dist/main.js.map +1 -1
  104. package/dist/migrations.js.map +1 -1
  105. package/dist/modes/interactive/components/armin.js.map +1 -1
  106. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  107. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  108. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  109. package/dist/modes/interactive/components/extension-editor.d.ts +5 -2
  110. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  111. package/dist/modes/interactive/components/extension-editor.js +9 -1
  112. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  113. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  114. package/dist/modes/interactive/components/footer.js.map +1 -1
  115. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  116. package/dist/modes/interactive/components/login-dialog.js +1 -1
  117. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  118. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  119. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  120. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  121. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  122. package/dist/modes/interactive/components/oauth-selector.js +1 -1
  123. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  124. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  125. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/session-selector.js +1 -1
  127. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  128. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  129. package/dist/modes/interactive/components/tool-execution.d.ts +2 -0
  130. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  131. package/dist/modes/interactive/components/tool-execution.js +28 -3
  132. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  133. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  134. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  135. package/dist/modes/interactive/components/user-message.d.ts +1 -0
  136. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  137. package/dist/modes/interactive/components/user-message.js +11 -0
  138. package/dist/modes/interactive/components/user-message.js.map +1 -1
  139. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  140. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  141. package/dist/modes/interactive/interactive-mode.js +29 -28
  142. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  143. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  144. package/dist/modes/interactive/theme/theme.js +5 -0
  145. package/dist/modes/interactive/theme/theme.js.map +1 -1
  146. package/dist/prompts/agents/build.md +5 -1
  147. package/dist/prompts/agents/plan.md +5 -1
  148. package/dist/prompts/agents/verify.md +5 -1
  149. package/dist/prompts/commands/atomic-commit.md +8 -16
  150. package/dist/prompts/commands/discuss-phase.md +9 -3
  151. package/dist/prompts/commands/execute-phase.md +15 -8
  152. package/dist/prompts/commands/fix.md +35 -0
  153. package/dist/prompts/commands/init-project.md +55 -0
  154. package/dist/prompts/commands/map-codebase.md +7 -1
  155. package/dist/prompts/commands/new-project.md +8 -2
  156. package/dist/prompts/commands/next-milestone.md +48 -0
  157. package/dist/prompts/commands/pause-work.md +4 -0
  158. package/dist/prompts/commands/plan-phase.md +11 -5
  159. package/dist/prompts/commands/progress.md +4 -0
  160. package/dist/prompts/commands/quick.md +8 -2
  161. package/dist/prompts/commands/resume-work.md +4 -0
  162. package/dist/prompts/commands/review.md +32 -0
  163. package/dist/prompts/commands/verify-work.md +10 -4
  164. package/docs/custom-provider.md +10 -2
  165. package/docs/extensions.md +20 -1
  166. package/docs/providers.md +3 -1
  167. package/docs/settings.md +1 -0
  168. package/examples/extensions/README.md +1 -0
  169. package/examples/extensions/antigravity-image-gen.ts +3 -1
  170. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  171. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  172. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  173. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  174. package/examples/extensions/dynamic-tools.ts +74 -0
  175. package/examples/extensions/with-deps/package-lock.json +2 -2
  176. package/examples/extensions/with-deps/package.json +1 -1
  177. package/hooks/gsd/draht-post-task.js +44 -11
  178. package/hooks/gsd/draht-quality-gate.js +99 -57
  179. package/package.json +9 -8
  180. package/prompts/agents/build.md +5 -1
  181. package/prompts/agents/plan.md +5 -1
  182. package/prompts/agents/verify.md +5 -1
  183. package/prompts/commands/atomic-commit.md +8 -16
  184. package/prompts/commands/discuss-phase.md +9 -3
  185. package/prompts/commands/execute-phase.md +15 -8
  186. package/prompts/commands/fix.md +35 -0
  187. package/prompts/commands/init-project.md +55 -0
  188. package/prompts/commands/map-codebase.md +7 -1
  189. package/prompts/commands/new-project.md +8 -2
  190. package/prompts/commands/next-milestone.md +48 -0
  191. package/prompts/commands/pause-work.md +4 -0
  192. package/prompts/commands/plan-phase.md +11 -5
  193. package/prompts/commands/progress.md +4 -0
  194. package/prompts/commands/quick.md +8 -2
  195. package/prompts/commands/resume-work.md +4 -0
  196. package/prompts/commands/review.md +32 -0
  197. package/prompts/commands/verify-work.md +10 -4
  198. package/dist/extensions/gsd-commands.ts +0 -403
  199. package/dist/extensions/subagent.ts +0 -515
  200. package/extensions/gsd-commands.ts +0 -403
  201. package/extensions/subagent.ts +0 -515
@@ -1,515 +0,0 @@
1
- /**
2
- * Subagent Tool for Draht — adapted from the pi subagent example
3
- *
4
- * Spawns isolated draht processes for delegated tasks.
5
- * Agents are defined in .draht/agents/*.md (project) or ~/.draht/agent/agents/*.md (global).
6
- *
7
- * Modes:
8
- * single — { agent, task }
9
- * parallel — { tasks: [{ agent, task }] } (max 8, concurrency 4)
10
- * chain — { chain: [{ agent, task }] } (sequential, {previous} placeholder)
11
- */
12
-
13
- import { spawn } from "node:child_process";
14
- import * as fs from "node:fs";
15
- import * as os from "node:os";
16
- import * as path from "node:path";
17
- import type { Message } from "@draht/ai";
18
- import { StringEnum } from "@draht/ai";
19
- import { type ExtensionAPI, getAgentDir, getPackageDir, isBunBinary, parseFrontmatter } from "@draht/coding-agent";
20
- import { Text } from "@draht/tui";
21
- import { Type } from "@sinclair/typebox";
22
-
23
- const MAX_PARALLEL = 8;
24
- const MAX_CONCURRENCY = 4;
25
-
26
- // Build the command to spawn a subagent process.
27
- // Compiled Bun binary: process.execPath IS the CLI binary, args go directly.
28
- // Source/dev mode: process.execPath is the runtime (bun/node), process.argv[1] is the CLI script.
29
- const DRAHT_BIN = process.execPath;
30
- const DRAHT_ARGS_PREFIX: string[] = isBunBinary ? [] : [process.argv[1]];
31
-
32
- // ─── Agent discovery ────────────────────────────────────────────────────────
33
-
34
- interface AgentConfig {
35
- name: string;
36
- description: string;
37
- tools?: string[];
38
- model?: string;
39
- systemPrompt: string;
40
- source: "user" | "project";
41
- }
42
-
43
- function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
44
- if (!fs.existsSync(dir)) return [];
45
- let entries: fs.Dirent[];
46
- try {
47
- entries = fs.readdirSync(dir, { withFileTypes: true });
48
- } catch {
49
- return [];
50
- }
51
- const agents: AgentConfig[] = [];
52
- for (const entry of entries) {
53
- if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
54
- try {
55
- const content = fs.readFileSync(path.join(dir, entry.name), "utf-8");
56
- const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
57
- if (!frontmatter.name || !frontmatter.description) continue;
58
- const tools = frontmatter.tools?.split(",").map((t: string) => t.trim()).filter(Boolean);
59
- agents.push({
60
- name: frontmatter.name,
61
- description: frontmatter.description,
62
- tools: tools?.length ? tools : undefined,
63
- model: frontmatter.model,
64
- systemPrompt: body,
65
- source,
66
- });
67
- } catch {
68
- continue;
69
- }
70
- }
71
- return agents;
72
- }
73
-
74
- function findProjectAgentsDir(cwd: string): string | null {
75
- let dir = cwd;
76
- while (true) {
77
- const candidate = path.join(dir, ".draht", "agents");
78
- try {
79
- if (fs.statSync(candidate).isDirectory()) return candidate;
80
- } catch {}
81
- const parent = path.dirname(dir);
82
- if (parent === dir) return null;
83
- dir = parent;
84
- }
85
- }
86
-
87
- type AgentScope = "user" | "project" | "both";
88
-
89
- function discoverAgents(cwd: string, scope: AgentScope): AgentConfig[] {
90
- // Shipped agents (bundled with the package) — lowest priority
91
- const shippedDir = path.join(getPackageDir(), "agents");
92
- const shippedAgents = loadAgentsFromDir(shippedDir, "user");
93
-
94
- const userDir = path.join(getAgentDir(), "agents");
95
- const projectDir = findProjectAgentsDir(cwd);
96
- const userAgents = scope !== "project" ? loadAgentsFromDir(userDir, "user") : [];
97
- const projectAgents = scope !== "user" && projectDir ? loadAgentsFromDir(projectDir, "project") : [];
98
-
99
- // Priority: shipped < user < project
100
- const map = new Map<string, AgentConfig>();
101
- for (const a of shippedAgents) map.set(a.name, a);
102
- for (const a of userAgents) map.set(a.name, a);
103
- for (const a of projectAgents) map.set(a.name, a);
104
- return Array.from(map.values());
105
- }
106
-
107
- // ─── Runner ─────────────────────────────────────────────────────────────────
108
-
109
- interface RunResult {
110
- agent: string;
111
- task: string;
112
- exitCode: number;
113
- output: string;
114
- stderr: string;
115
- step?: number;
116
- }
117
-
118
- function writeTemp(name: string, content: string): { file: string; dir: string } {
119
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "draht-subagent-"));
120
- const file = path.join(dir, `${name.replace(/[^\w.-]/g, "_")}.md`);
121
- fs.writeFileSync(file, content, { encoding: "utf-8", mode: 0o600 });
122
- return { file, dir };
123
- }
124
-
125
- function cleanTemp(file: string, dir: string) {
126
- try { fs.unlinkSync(file); } catch {}
127
- try { fs.rmdirSync(dir); } catch {}
128
- }
129
-
130
- function getFinalText(messages: Message[]): string {
131
- for (let i = messages.length - 1; i >= 0; i--) {
132
- const msg = messages[i];
133
- if (msg.role === "assistant") {
134
- for (const part of msg.content) {
135
- if (part.type === "text") return part.text;
136
- }
137
- }
138
- }
139
- return "";
140
- }
141
-
142
- type ProgressFn = (activity: string) => void;
143
-
144
- async function runAgent(
145
- cwd: string,
146
- agent: AgentConfig,
147
- task: string,
148
- signal?: AbortSignal,
149
- step?: number,
150
- onProgress?: ProgressFn,
151
- ): Promise<RunResult> {
152
- const args: string[] = ["--mode", "json", "-p", "--no-session"];
153
- if (agent.model) args.push("--model", agent.model);
154
- if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
155
-
156
- let tmpFile: string | null = null;
157
- let tmpDir: string | null = null;
158
-
159
- if (agent.systemPrompt.trim()) {
160
- const tmp = writeTemp(agent.name, agent.systemPrompt);
161
- tmpFile = tmp.file;
162
- tmpDir = tmp.dir;
163
- args.push("--append-system-prompt", tmpFile);
164
- }
165
-
166
- args.push(`Task: ${task}`);
167
-
168
- const messages: Message[] = [];
169
- let stderr = "";
170
-
171
- try {
172
- const exitCode = await new Promise<number>((resolve) => {
173
- const proc = spawn(DRAHT_BIN, [...DRAHT_ARGS_PREFIX, ...args], { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
174
- let buf = "";
175
-
176
- const processLine = (line: string) => {
177
- if (!line.trim()) return;
178
- try {
179
- const event = JSON.parse(line);
180
- if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
181
- messages.push(event.message as Message);
182
- }
183
- // Stream activity updates to the parent
184
- if (onProgress) {
185
- if (event.type === "tool_execution_start") {
186
- const toolArgs = event.args ?? {};
187
- const detail = toolArgs.command || toolArgs.path || toolArgs.file_path || toolArgs.pattern || "";
188
- const short = typeof detail === "string" && detail.length > 60 ? `${detail.slice(0, 60)}...` : detail;
189
- onProgress(short ? `${event.toolName} ${short}` : event.toolName);
190
- } else if (event.type === "text_delta") {
191
- // Show first line of streaming text as activity
192
- const text = event.text?.trim();
193
- if (text) {
194
- const firstLine = text.split("\n")[0];
195
- const short = firstLine.length > 80 ? `${firstLine.slice(0, 80)}...` : firstLine;
196
- onProgress(short);
197
- }
198
- }
199
- }
200
- } catch {}
201
- };
202
-
203
- proc.stdout.on("data", (d) => {
204
- buf += d.toString();
205
- const lines = buf.split("\n");
206
- buf = lines.pop() || "";
207
- for (const l of lines) processLine(l);
208
- });
209
- proc.stderr.on("data", (d) => { stderr += d.toString(); });
210
- proc.on("close", (code) => {
211
- if (buf.trim()) processLine(buf);
212
- resolve(code ?? 0);
213
- });
214
- proc.on("error", () => resolve(1));
215
-
216
- if (signal) {
217
- const kill = () => { proc.kill("SIGTERM"); setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 5000); };
218
- if (signal.aborted) kill();
219
- else signal.addEventListener("abort", kill, { once: true });
220
- }
221
- });
222
-
223
- return { agent: agent.name, task, exitCode, output: getFinalText(messages), stderr, step };
224
- } finally {
225
- if (tmpFile && tmpDir) cleanTemp(tmpFile, tmpDir);
226
- }
227
- }
228
-
229
- async function runParallel<T>(
230
- items: T[],
231
- concurrency: number,
232
- fn: (item: T, i: number) => Promise<T extends unknown ? RunResult : never>,
233
- ): Promise<RunResult[]> {
234
- const results: RunResult[] = new Array(items.length);
235
- let next = 0;
236
- await Promise.all(
237
- Array.from({ length: Math.min(concurrency, items.length) }, async () => {
238
- while (true) {
239
- const i = next++;
240
- if (i >= items.length) return;
241
- results[i] = await (fn as (item: T, i: number) => Promise<RunResult>)(items[i], i);
242
- }
243
- }),
244
- );
245
- return results;
246
- }
247
-
248
- // ─── Extension ──────────────────────────────────────────────────────────────
249
-
250
- const TaskItem = Type.Object({
251
- agent: Type.String({ description: "Agent name" }),
252
- task: Type.String({ description: "Task description" }),
253
- });
254
-
255
- const ChainItem = Type.Object({
256
- agent: Type.String({ description: "Agent name" }),
257
- task: Type.String({ description: "Task, optionally using {previous} placeholder" }),
258
- });
259
-
260
- const Params = Type.Object({
261
- agent: Type.Optional(Type.String()),
262
- task: Type.Optional(Type.String()),
263
- tasks: Type.Optional(Type.Array(TaskItem, { description: "Parallel tasks" })),
264
- chain: Type.Optional(Type.Array(ChainItem, { description: "Chained tasks" })),
265
- agentScope: Type.Optional(
266
- StringEnum(["user", "project", "both"] as const, { default: "both" }),
267
- ),
268
- });
269
-
270
- // ─── Rendering helpers ──────────────────────────────────────────────────────
271
-
272
- function truncateTask(task: string, maxLen = 120): string {
273
- const oneLine = task.replace(/\n/g, " ").trim();
274
- return oneLine.length > maxLen ? `${oneLine.slice(0, maxLen)}...` : oneLine;
275
- }
276
-
277
- interface SubagentDetails {
278
- status?: string;
279
- }
280
-
281
- export default function (pi: ExtensionAPI) {
282
- pi.registerTool({
283
- name: "subagent",
284
- label: "Subagent",
285
- description:
286
- "Delegate to specialized agents. single: {agent,task} | parallel: {tasks:[]} | chain: {chain:[]} with {previous} placeholder. agentScope: 'both' (default) uses project .draht/agents/ + global.",
287
- parameters: Params,
288
-
289
- renderCall(args, theme) {
290
- const lines: string[] = [];
291
-
292
- if (args.chain?.length) {
293
- const agents = args.chain.map((s: { agent: string }) => s.agent).join(" -> ");
294
- lines.push(theme.fg("toolTitle", theme.bold(`subagent chain`)) + " " + theme.fg("accent", agents));
295
- for (let i = 0; i < args.chain.length; i++) {
296
- const step = args.chain[i];
297
- const prefix = theme.fg("muted", ` ${i + 1}.`);
298
- lines.push(`${prefix} ${theme.fg("accent", step.agent)} ${theme.fg("toolOutput", truncateTask(step.task))}`);
299
- }
300
- } else if (args.tasks?.length) {
301
- lines.push(theme.fg("toolTitle", theme.bold(`subagent parallel`)) + theme.fg("muted", ` (${args.tasks.length} tasks)`));
302
- for (const t of args.tasks) {
303
- lines.push(` ${theme.fg("accent", t.agent)} ${theme.fg("toolOutput", truncateTask(t.task))}`);
304
- }
305
- } else if (args.agent) {
306
- lines.push(
307
- theme.fg("toolTitle", theme.bold("subagent")) +
308
- " " +
309
- theme.fg("accent", args.agent) +
310
- (args.task ? " " + theme.fg("toolOutput", truncateTask(args.task)) : ""),
311
- );
312
- } else {
313
- lines.push(theme.fg("toolTitle", theme.bold("subagent")));
314
- }
315
-
316
- return new Text(lines.join("\n"), 0, 0);
317
- },
318
-
319
- renderResult(result, options, theme) {
320
- const status = result.details?.status;
321
- const output = result.content
322
- ?.filter((c) => c.type === "text")
323
- .map((c) => ("text" in c ? c.text : ""))
324
- .join("\n") || "";
325
-
326
- const lines: string[] = [];
327
-
328
- if (status) {
329
- lines.push(theme.fg("muted", status));
330
- }
331
-
332
- if (output.trim()) {
333
- const trimmed = output.trim();
334
- if (options.expanded || options.isPartial) {
335
- // When expanded or during execution, show full activity log
336
- for (const line of trimmed.split("\n")) {
337
- lines.push(theme.fg("toolOutput", line));
338
- }
339
- } else {
340
- // Collapsed final result — show preview
341
- const allLines = trimmed.split("\n");
342
- const previewLines = allLines.slice(0, 8);
343
- lines.push(theme.fg("toolOutput", previewLines.join("\n")));
344
- if (allLines.length > 8) {
345
- lines.push(theme.fg("muted", `... (${allLines.length - 8} more lines)`));
346
- }
347
- }
348
- }
349
-
350
- if (lines.length === 0) return new Text("", 0, 0);
351
- return new Text(lines.join("\n"), 0, 0);
352
- },
353
-
354
- async execute(_id, params, signal, onUpdate, ctx) {
355
- const scope: AgentScope = (params.agentScope as AgentScope) ?? "both";
356
- const agents = discoverAgents(ctx.cwd, scope);
357
- const available = agents.map((a) => a.name).join(", ") || "none";
358
-
359
- const find = (name: string) => agents.find((a) => a.name === name);
360
- const notFound = (name: string) => ({
361
- content: [{ type: "text" as const, text: `Unknown agent "${name}". Available: ${available}` }],
362
- isError: true,
363
- });
364
-
365
- // Stream live activity from the subagent process
366
- const activityLines: string[] = [];
367
- const emitProgress = (agentName: string, activity?: string) => {
368
- const status = activity ? `${agentName}: ${activity}` : `${agentName} working...`;
369
- onUpdate?.({
370
- content: [{ type: "text" as const, text: activityLines.join("\n") }],
371
- details: { status },
372
- });
373
- };
374
- const makeProgressFn = (agentName: string): ProgressFn => (activity) => {
375
- // Keep a rolling log of recent activities
376
- activityLines.push(`${agentName}: ${activity}`);
377
- if (activityLines.length > 50) activityLines.splice(0, activityLines.length - 50);
378
- emitProgress(agentName, activity);
379
- };
380
-
381
- // ── Chain mode ──
382
- if (params.chain?.length) {
383
- let previous = "";
384
- const results: RunResult[] = [];
385
- for (let i = 0; i < params.chain.length; i++) {
386
- const step = params.chain[i];
387
- const agent = find(step.agent);
388
- if (!agent) return notFound(step.agent);
389
- activityLines.length = 0;
390
- emitProgress(step.agent, `step ${i + 1}/${params.chain.length}`);
391
- const task = step.task.replace(/\{previous\}/g, previous);
392
- const result = await runAgent(ctx.cwd, agent, task, signal, i + 1, makeProgressFn(step.agent));
393
- results.push(result);
394
- if (result.exitCode !== 0) {
395
- return {
396
- content: [{ type: "text" as const, text: `Chain failed at step ${i + 1} (${step.agent}):\n${result.output || result.stderr}` }],
397
- isError: true,
398
- };
399
- }
400
- previous = result.output;
401
- }
402
- return { content: [{ type: "text" as const, text: results[results.length - 1].output || "(no output)" }] };
403
- }
404
-
405
- // ── Parallel mode ──
406
- if (params.tasks?.length) {
407
- if (params.tasks.length > MAX_PARALLEL) {
408
- return { content: [{ type: "text" as const, text: `Too many tasks (max ${MAX_PARALLEL})` }], isError: true };
409
- }
410
- for (const t of params.tasks) { if (!find(t.agent)) return notFound(t.agent); }
411
-
412
- const agentNames = params.tasks.map((t: { agent: string }) => t.agent).join(", ");
413
- emitProgress(agentNames);
414
-
415
- const results = await runParallel(params.tasks, MAX_CONCURRENCY, async (t, i) => {
416
- return runAgent(ctx.cwd, find(t.agent)!, t.task, signal, undefined, makeProgressFn(t.agent));
417
- });
418
-
419
- const ok = results.filter((r) => r.exitCode === 0).length;
420
- const summary = results
421
- .map((r) => `[${r.agent}] ${r.exitCode === 0 ? "ok" : "fail"} ${r.output.slice(0, 200)}`)
422
- .join("\n\n");
423
- return { content: [{ type: "text" as const, text: `Parallel: ${ok}/${results.length} succeeded\n\n${summary}` }] };
424
- }
425
-
426
- // ── Single mode ──
427
- if (params.agent && params.task) {
428
- const agent = find(params.agent);
429
- if (!agent) return notFound(params.agent);
430
- emitProgress(params.agent);
431
- const result = await runAgent(ctx.cwd, agent, params.task, signal, undefined, makeProgressFn(params.agent));
432
- const isError = result.exitCode !== 0;
433
- return {
434
- content: [{ type: "text" as const, text: result.output || result.stderr || "(no output)" }],
435
- ...(isError ? { isError: true } : {}),
436
- };
437
- }
438
-
439
- return { content: [{ type: "text" as const, text: `Provide exactly one mode. Available agents: ${available}` }], isError: true };
440
- },
441
- });
442
-
443
- // ── Agent selection for user prompts ─────────────────────────────────────
444
-
445
- let selectedAgent: string | undefined;
446
-
447
- function updateAgentStatus(ctx: { ui: { setStatus: (key: string, text: string | undefined) => void } }) {
448
- ctx.ui.setStatus("agent", selectedAgent ? `agent: ${selectedAgent}` : undefined);
449
- }
450
-
451
- // /agent command — select an agent or clear selection
452
- pi.registerCommand("agent", {
453
- description: "Select an agent to handle your next prompts, or clear selection. Usage: /agent [name]",
454
- handler: async (args, ctx) => {
455
- const agents = discoverAgents(ctx.cwd, "both");
456
-
457
- if (args.trim()) {
458
- // Direct selection: /agent architect
459
- const name = args.trim();
460
- if (name === "none" || name === "off" || name === "clear") {
461
- selectedAgent = undefined;
462
- updateAgentStatus(ctx);
463
- ctx.ui.notify("Agent cleared — prompts go to default model", "info");
464
- return;
465
- }
466
- const agent = agents.find((a) => a.name === name);
467
- if (!agent) {
468
- const available = agents.map((a) => a.name).join(", ") || "none";
469
- ctx.ui.notify(`Unknown agent "${name}". Available: ${available}`, "warning");
470
- return;
471
- }
472
- selectedAgent = name;
473
- updateAgentStatus(ctx);
474
- ctx.ui.notify(`Agent set to "${name}" — your prompts will be handled by this agent`, "info");
475
- return;
476
- }
477
-
478
- // Interactive selection
479
- if (!ctx.hasUI) {
480
- ctx.ui.notify("Usage: /agent <name> or /agent none", "warning");
481
- return;
482
- }
483
-
484
- const options = ["(none — default model)", ...agents.map((a) => `${a.name} — ${a.description}`)];
485
- const choice = await ctx.ui.select("Select agent for your prompts", options);
486
- if (choice === undefined) return; // cancelled
487
-
488
- if (choice === options[0]) {
489
- selectedAgent = undefined;
490
- updateAgentStatus(ctx);
491
- ctx.ui.notify("Agent cleared", "info");
492
- } else {
493
- const name = choice.split(" — ")[0];
494
- selectedAgent = name;
495
- updateAgentStatus(ctx);
496
- ctx.ui.notify(`Agent set to "${name}"`, "info");
497
- }
498
- },
499
- getArgumentCompletions: (partial) => {
500
- const agents = discoverAgents(process.cwd(), "both");
501
- const names = ["none", ...agents.map((a) => a.name)];
502
- return names.filter((n) => n.startsWith(partial));
503
- },
504
- });
505
-
506
- // Intercept user input when an agent is selected
507
- pi.on("input", (event) => {
508
- if (!selectedAgent) return { action: "continue" as const };
509
- // Don't intercept slash commands
510
- if (event.text.startsWith("/") || event.text.startsWith("!")) return { action: "continue" as const };
511
-
512
- const wrapped = `Use the subagent tool to delegate to the "${selectedAgent}" agent with this task:\n\n"${event.text}"\n\nSet agentScope to "both".`;
513
- return { action: "transform" as const, text: wrapped, images: event.images };
514
- });
515
- }