@pi-agents/orchid 0.1.0-beta.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 (163) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/agents/AGENTS-MANIFEST.md +42 -0
  5. package/agents/brain.md +42 -0
  6. package/agents/context-builder.md +46 -0
  7. package/agents/delegate.md +12 -0
  8. package/agents/dev-1.md +42 -0
  9. package/agents/oracle.md +73 -0
  10. package/agents/planner.md +55 -0
  11. package/agents/researcher.md +52 -0
  12. package/agents/reviewer.md +79 -0
  13. package/agents/scout.md +50 -0
  14. package/agents/tester.md +45 -0
  15. package/agents/worker.md +55 -0
  16. package/extensions/ralph.ts +1 -0
  17. package/extensions/reviewer-extension.ts +125 -0
  18. package/extensions/task-orchestrator.ts +28 -0
  19. package/package.json +63 -0
  20. package/prompts/gather-context-and-clarify.md +13 -0
  21. package/prompts/parallel-cleanup.md +59 -0
  22. package/prompts/parallel-context-build.md +53 -0
  23. package/prompts/parallel-handoff-plan.md +59 -0
  24. package/prompts/parallel-research.md +50 -0
  25. package/prompts/parallel-review.md +54 -0
  26. package/prompts/review-loop.md +41 -0
  27. package/skills/orchid/SKILL.md +214 -0
  28. package/skills/orchid/orchid-cleanup/SKILL.md +122 -0
  29. package/skills/orchid/orchid-converge/SKILL.md +124 -0
  30. package/skills/orchid/orchid-decompose/SKILL.md +201 -0
  31. package/skills/orchid/orchid-doctor/SKILL.md +162 -0
  32. package/skills/orchid/orchid-investigate/SKILL.md +102 -0
  33. package/skills/orchid/orchid-launch/SKILL.md +147 -0
  34. package/skills/ralph/SKILL.md +73 -0
  35. package/skills/subagents/pi-subagents/SKILL.md +813 -0
  36. package/src/index.ts +7 -0
  37. package/src/orchestrator/abort.ts +534 -0
  38. package/src/orchestrator/agent-bridge-extension.ts +1020 -0
  39. package/src/orchestrator/agent-host.ts +954 -0
  40. package/src/orchestrator/cleanup.ts +776 -0
  41. package/src/orchestrator/config-loader.ts +1412 -0
  42. package/src/orchestrator/config-schema.ts +690 -0
  43. package/src/orchestrator/config.ts +81 -0
  44. package/src/orchestrator/context-window.ts +66 -0
  45. package/src/orchestrator/diagnostic-reports.ts +475 -0
  46. package/src/orchestrator/diagnostics.ts +394 -0
  47. package/src/orchestrator/discovery.ts +1833 -0
  48. package/src/orchestrator/engine-worker.ts +415 -0
  49. package/src/orchestrator/engine.ts +5940 -0
  50. package/src/orchestrator/execution.ts +3104 -0
  51. package/src/orchestrator/extension.ts +5934 -0
  52. package/src/orchestrator/formatting.ts +785 -0
  53. package/src/orchestrator/git.ts +88 -0
  54. package/src/orchestrator/index.ts +28 -0
  55. package/src/orchestrator/lane-runner.ts +1787 -0
  56. package/src/orchestrator/mailbox.ts +780 -0
  57. package/src/orchestrator/merge.ts +3414 -0
  58. package/src/orchestrator/messages.ts +1062 -0
  59. package/src/orchestrator/migrations.ts +278 -0
  60. package/src/orchestrator/naming.ts +117 -0
  61. package/src/orchestrator/path-resolver.ts +275 -0
  62. package/src/orchestrator/persistence.ts +2625 -0
  63. package/src/orchestrator/process-registry.ts +452 -0
  64. package/src/orchestrator/quality-gate.ts +1085 -0
  65. package/src/orchestrator/resume.ts +3488 -0
  66. package/src/orchestrator/sessions.ts +57 -0
  67. package/src/orchestrator/settings-loader.ts +136 -0
  68. package/src/orchestrator/settings-tui.ts +2208 -0
  69. package/src/orchestrator/sidecar-telemetry.ts +267 -0
  70. package/src/orchestrator/supervisor.ts +4548 -0
  71. package/src/orchestrator/task-executor-core.ts +675 -0
  72. package/src/orchestrator/tmux-compat.ts +37 -0
  73. package/src/orchestrator/tool-allowlist-constants.ts +37 -0
  74. package/src/orchestrator/types.ts +4465 -0
  75. package/src/orchestrator/verification.ts +547 -0
  76. package/src/orchestrator/waves.ts +1564 -0
  77. package/src/orchestrator/workspace.ts +707 -0
  78. package/src/orchestrator/worktree.ts +2725 -0
  79. package/src/ralph/index.ts +825 -0
  80. package/src/subagents/agents/agent-management.ts +648 -0
  81. package/src/subagents/agents/agent-scope.ts +6 -0
  82. package/src/subagents/agents/agent-selection.ts +23 -0
  83. package/src/subagents/agents/agent-serializer.ts +86 -0
  84. package/src/subagents/agents/agents.ts +832 -0
  85. package/src/subagents/agents/chain-serializer.ts +137 -0
  86. package/src/subagents/agents/frontmatter.ts +29 -0
  87. package/src/subagents/agents/identity.ts +30 -0
  88. package/src/subagents/agents/skills.ts +632 -0
  89. package/src/subagents/extension/config.ts +16 -0
  90. package/src/subagents/extension/control-notices.ts +92 -0
  91. package/src/subagents/extension/doctor.ts +199 -0
  92. package/src/subagents/extension/fanout-child.ts +170 -0
  93. package/src/subagents/extension/index.ts +573 -0
  94. package/src/subagents/extension/schemas.ts +168 -0
  95. package/src/subagents/intercom/intercom-bridge.ts +379 -0
  96. package/src/subagents/intercom/result-intercom.ts +377 -0
  97. package/src/subagents/runs/background/async-execution.ts +712 -0
  98. package/src/subagents/runs/background/async-job-tracker.ts +310 -0
  99. package/src/subagents/runs/background/async-resume.ts +345 -0
  100. package/src/subagents/runs/background/async-status.ts +325 -0
  101. package/src/subagents/runs/background/completion-dedupe.ts +63 -0
  102. package/src/subagents/runs/background/notify.ts +108 -0
  103. package/src/subagents/runs/background/parallel-groups.ts +45 -0
  104. package/src/subagents/runs/background/result-watcher.ts +307 -0
  105. package/src/subagents/runs/background/run-id-resolver.ts +83 -0
  106. package/src/subagents/runs/background/run-status.ts +269 -0
  107. package/src/subagents/runs/background/stale-run-reconciler.ts +336 -0
  108. package/src/subagents/runs/background/subagent-runner.ts +1808 -0
  109. package/src/subagents/runs/background/top-level-async.ts +13 -0
  110. package/src/subagents/runs/foreground/chain-clarify.ts +1333 -0
  111. package/src/subagents/runs/foreground/chain-execution.ts +938 -0
  112. package/src/subagents/runs/foreground/execution.ts +918 -0
  113. package/src/subagents/runs/foreground/subagent-executor.ts +2527 -0
  114. package/src/subagents/runs/shared/completion-guard.ts +147 -0
  115. package/src/subagents/runs/shared/long-running-guard.ts +175 -0
  116. package/src/subagents/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
  117. package/src/subagents/runs/shared/model-fallback.ts +103 -0
  118. package/src/subagents/runs/shared/nested-events.ts +819 -0
  119. package/src/subagents/runs/shared/nested-path.ts +52 -0
  120. package/src/subagents/runs/shared/nested-render.ts +115 -0
  121. package/src/subagents/runs/shared/parallel-utils.ts +109 -0
  122. package/src/subagents/runs/shared/pi-args.ts +220 -0
  123. package/src/subagents/runs/shared/pi-spawn.ts +115 -0
  124. package/src/subagents/runs/shared/run-history.ts +60 -0
  125. package/src/subagents/runs/shared/single-output.ts +164 -0
  126. package/src/subagents/runs/shared/subagent-control.ts +226 -0
  127. package/src/subagents/runs/shared/subagent-prompt-runtime.ts +170 -0
  128. package/src/subagents/runs/shared/worktree.ts +577 -0
  129. package/src/subagents/shared/artifacts.ts +98 -0
  130. package/src/subagents/shared/atomic-json.ts +16 -0
  131. package/src/subagents/shared/file-coalescer.ts +40 -0
  132. package/src/subagents/shared/fork-context.ts +76 -0
  133. package/src/subagents/shared/formatters.ts +133 -0
  134. package/src/subagents/shared/jsonl-writer.ts +81 -0
  135. package/src/subagents/shared/model-info.ts +78 -0
  136. package/src/subagents/shared/post-exit-stdio-guard.ts +85 -0
  137. package/src/subagents/shared/session-identity.ts +10 -0
  138. package/src/subagents/shared/session-tokens.ts +44 -0
  139. package/src/subagents/shared/settings.ts +397 -0
  140. package/src/subagents/shared/status-format.ts +49 -0
  141. package/src/subagents/shared/types.ts +822 -0
  142. package/src/subagents/shared/utils.ts +450 -0
  143. package/src/subagents/slash/prompt-template-bridge.ts +397 -0
  144. package/src/subagents/slash/slash-bridge.ts +174 -0
  145. package/src/subagents/slash/slash-commands.ts +528 -0
  146. package/src/subagents/slash/slash-live-state.ts +292 -0
  147. package/src/subagents/tui/render-helpers.ts +80 -0
  148. package/src/subagents/tui/render.ts +1358 -0
  149. package/templates/agents/local/supervisor.md +33 -0
  150. package/templates/agents/local/task-merger.md +27 -0
  151. package/templates/agents/local/task-reviewer.md +30 -0
  152. package/templates/agents/local/task-worker.md +34 -0
  153. package/templates/agents/supervisor-routing.md +92 -0
  154. package/templates/agents/supervisor.md +229 -0
  155. package/templates/agents/task-merger.md +214 -0
  156. package/templates/agents/task-reviewer.md +260 -0
  157. package/templates/agents/task-worker-segment.md +44 -0
  158. package/templates/agents/task-worker.md +557 -0
  159. package/templates/tasks/CONTEXT.md +30 -0
  160. package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -0
  161. package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
  162. package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -0
  163. package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -0
@@ -0,0 +1,528 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import { Key, matchesKey } from "@earendil-works/pi-tui";
6
+ import { discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
7
+ import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
8
+ import { isParallelStep, type ChainStep } from "../shared/settings.ts";
9
+ import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
10
+ import {
11
+ applySlashUpdate,
12
+ buildSlashInitialResult,
13
+ failSlashResult,
14
+ finalizeSlashResult,
15
+ } from "./slash-live-state.ts";
16
+ import {
17
+ SLASH_RESULT_TYPE,
18
+ SLASH_SUBAGENT_CANCEL_EVENT,
19
+ SLASH_SUBAGENT_REQUEST_EVENT,
20
+ SLASH_SUBAGENT_RESPONSE_EVENT,
21
+ SLASH_SUBAGENT_STARTED_EVENT,
22
+ SLASH_SUBAGENT_UPDATE_EVENT,
23
+ type SingleResult,
24
+ type SubagentState,
25
+ } from "../shared/types.ts";
26
+
27
+ interface InlineConfig {
28
+ output?: string | false;
29
+ outputMode?: "inline" | "file-only";
30
+ reads?: string[] | false;
31
+ model?: string;
32
+ skill?: string[] | false;
33
+ progress?: boolean;
34
+ }
35
+
36
+ const parseInlineConfig = (raw: string): InlineConfig => {
37
+ const config: InlineConfig = {};
38
+ for (const part of raw.split(",")) {
39
+ const trimmed = part.trim();
40
+ if (!trimmed) continue;
41
+ const eq = trimmed.indexOf("=");
42
+ if (eq === -1) {
43
+ if (trimmed === "progress") config.progress = true;
44
+ continue;
45
+ }
46
+ const key = trimmed.slice(0, eq).trim();
47
+ const val = trimmed.slice(eq + 1).trim();
48
+ switch (key) {
49
+ case "output": config.output = val === "false" ? false : val; break;
50
+ case "outputMode": if (val === "inline" || val === "file-only") config.outputMode = val; break;
51
+ case "reads": config.reads = val === "false" ? false : val.split("+").filter(Boolean); break;
52
+ case "model": config.model = val || undefined; break;
53
+ case "skill": case "skills": config.skill = val === "false" ? false : val.split("+").filter(Boolean); break;
54
+ case "progress": config.progress = val !== "false"; break;
55
+ }
56
+ }
57
+ return config;
58
+ };
59
+
60
+ const parseAgentToken = (token: string): { name: string; config: InlineConfig } => {
61
+ const bracket = token.indexOf("[");
62
+ if (bracket === -1) return { name: token, config: {} };
63
+ const end = token.lastIndexOf("]");
64
+ return { name: token.slice(0, bracket), config: parseInlineConfig(token.slice(bracket + 1, end !== -1 ? end : undefined)) };
65
+ };
66
+
67
+ const extractExecutionFlags = (rawArgs: string): { args: string; bg: boolean; fork: boolean } => {
68
+ let args = rawArgs.trim();
69
+ let bg = false;
70
+ let fork = false;
71
+
72
+ while (true) {
73
+ if (args.endsWith(" --bg") || args === "--bg") {
74
+ bg = true;
75
+ args = args === "--bg" ? "" : args.slice(0, -5).trim();
76
+ continue;
77
+ }
78
+ if (args.endsWith(" --fork") || args === "--fork") {
79
+ fork = true;
80
+ args = args === "--fork" ? "" : args.slice(0, -7).trim();
81
+ continue;
82
+ }
83
+ break;
84
+ }
85
+
86
+ return { args, bg, fork };
87
+ };
88
+
89
+ const makeAgentCompletions = (state: SubagentState, multiAgent: boolean) => (prefix: string) => {
90
+ if (!state.baseCwd) return null;
91
+ const agents = discoverAgents(state.baseCwd, "both").agents;
92
+ if (!multiAgent) {
93
+ if (prefix.includes(" ")) return null;
94
+ return agents.filter((a) => a.name.startsWith(prefix)).map((a) => ({ value: a.name, label: a.name }));
95
+ }
96
+
97
+ const lastArrow = prefix.lastIndexOf(" -> ");
98
+ const segment = lastArrow !== -1 ? prefix.slice(lastArrow + 4) : prefix;
99
+ if (segment.includes(" -- ") || segment.includes('"') || segment.includes("'")) return null;
100
+
101
+ const lastWord = (prefix.match(/(\S*)$/) || ["", ""])[1];
102
+ const beforeLastWord = prefix.slice(0, prefix.length - lastWord.length);
103
+
104
+ if (lastWord === "->") {
105
+ return agents.map((a) => ({ value: `${prefix} ${a.name}`, label: a.name }));
106
+ }
107
+
108
+ return agents.filter((a) => a.name.startsWith(lastWord)).map((a) => ({ value: `${beforeLastWord}${a.name}`, label: a.name }));
109
+ };
110
+
111
+ const discoverSavedChains = (cwd: string): ChainConfig[] => {
112
+ const chainsByName = new Map<string, ChainConfig>();
113
+ for (const chain of discoverAgentsAll(cwd).chains) {
114
+ chainsByName.set(chain.name, chain);
115
+ }
116
+ return Array.from(chainsByName.values());
117
+ };
118
+
119
+ const makeChainCompletions = (state: SubagentState) => (prefix: string) => {
120
+ if (prefix.includes(" ") || !state.baseCwd) return null;
121
+ return discoverSavedChains(state.baseCwd)
122
+ .filter((chain) => chain.name.startsWith(prefix))
123
+ .map((chain) => ({ value: chain.name, label: chain.name }));
124
+ };
125
+
126
+ const mapSavedChainSteps = (chain: ChainConfig, worktree = false): ChainStep[] => {
127
+ return (chain.steps as Array<ChainStep & { skills?: string[] | false }>).map((step) => {
128
+ if (isParallelStep(step)) return worktree ? { ...step, worktree: true } : { ...step };
129
+ return {
130
+ agent: step.agent,
131
+ task: step.task || undefined,
132
+ output: step.output,
133
+ outputMode: step.outputMode,
134
+ reads: step.reads,
135
+ progress: step.progress,
136
+ skill: step.skill ?? step.skills,
137
+ model: step.model,
138
+ };
139
+ });
140
+ };
141
+
142
+ async function requestSlashRun(
143
+ pi: ExtensionAPI,
144
+ ctx: ExtensionContext,
145
+ requestId: string,
146
+ params: SubagentParamsLike,
147
+ ): Promise<SlashSubagentResponse> {
148
+ return new Promise((resolve, reject) => {
149
+ let done = false;
150
+ let started = false;
151
+
152
+ const startTimeoutMs = 15_000;
153
+ const startTimeout = setTimeout(() => {
154
+ finish(() => reject(new Error(
155
+ "Slash subagent bridge did not start within 15s. Ensure the extension is loaded correctly.",
156
+ )));
157
+ }, startTimeoutMs);
158
+
159
+ const onStarted = (data: unknown) => {
160
+ if (done || !data || typeof data !== "object") return;
161
+ if ((data as { requestId?: unknown }).requestId !== requestId) return;
162
+ started = true;
163
+ clearTimeout(startTimeout);
164
+ if (ctx.hasUI) ctx.ui.setStatus("subagent-slash", "running...");
165
+ };
166
+
167
+ const onResponse = (data: unknown) => {
168
+ if (done || !data || typeof data !== "object") return;
169
+ const response = data as Partial<SlashSubagentResponse>;
170
+ if (response.requestId !== requestId) return;
171
+ clearTimeout(startTimeout);
172
+ finish(() => resolve(response as SlashSubagentResponse));
173
+ };
174
+
175
+ const onUpdate = (data: unknown) => {
176
+ if (done || !data || typeof data !== "object") return;
177
+ const update = data as SlashSubagentUpdate;
178
+ if (update.requestId !== requestId) return;
179
+ applySlashUpdate(requestId, update);
180
+ if (!ctx.hasUI) return;
181
+ const tool = update.currentTool ? ` ${update.currentTool}` : "";
182
+ const count = update.toolCount ?? 0;
183
+ ctx.ui.setStatus("subagent-slash", `${count} tools${tool} | Ctrl+O live detail`);
184
+ };
185
+
186
+ const onTerminalInput = ctx.hasUI
187
+ ? ctx.ui.onTerminalInput((input) => {
188
+ if (!matchesKey(input, Key.escape)) return undefined;
189
+ pi.events.emit(SLASH_SUBAGENT_CANCEL_EVENT, { requestId });
190
+ finish(() => reject(new Error("Cancelled")));
191
+ return { consume: true };
192
+ })
193
+ : undefined;
194
+
195
+ const unsubStarted = pi.events.on(SLASH_SUBAGENT_STARTED_EVENT, onStarted);
196
+ const unsubResponse = pi.events.on(SLASH_SUBAGENT_RESPONSE_EVENT, onResponse);
197
+ const unsubUpdate = pi.events.on(SLASH_SUBAGENT_UPDATE_EVENT, onUpdate);
198
+
199
+ const finish = (next: () => void) => {
200
+ if (done) return;
201
+ done = true;
202
+ clearTimeout(startTimeout);
203
+ unsubStarted();
204
+ unsubResponse();
205
+ unsubUpdate();
206
+ onTerminalInput?.();
207
+ next();
208
+ };
209
+
210
+ pi.events.emit(SLASH_SUBAGENT_REQUEST_EVENT, { requestId, params });
211
+
212
+ // Bridge emits STARTED synchronously during REQUEST emit.
213
+ // If not started, no bridge received the request.
214
+ if (!started && done) return;
215
+ if (!started) {
216
+ finish(() => reject(new Error(
217
+ "No slash subagent bridge responded. Ensure the subagent extension is loaded correctly.",
218
+ )));
219
+ }
220
+ });
221
+ }
222
+
223
+ function extractSlashMessageText(content: string | Array<{ type?: string; text?: string }>): string {
224
+ if (typeof content === "string") return content;
225
+ if (!Array.isArray(content)) return "";
226
+ return content
227
+ .filter((part): part is { type: "text"; text: string } => part?.type === "text" && typeof part.text === "string")
228
+ .map((part) => part.text)
229
+ .join("\n");
230
+ }
231
+
232
+ function formatExportPathList(paths: string[]): string {
233
+ return paths.map((file) => `- \`${file}\``).join("\n");
234
+ }
235
+
236
+ function collectResultPaths(results: SingleResult[], getPath: (result: SingleResult) => string | undefined): string[] {
237
+ return results
238
+ .map(getPath)
239
+ .filter((file): file is string => typeof file === "string" && file.length > 0);
240
+ }
241
+
242
+ function buildSlashExportText(response: SlashSubagentResponse): string {
243
+ const output = extractSlashMessageText(response.result.content) || response.errorText || "(no output)";
244
+ const results = response.result.details?.results ?? [];
245
+ const sessionFiles = collectResultPaths(results, (result) => result.sessionFile);
246
+ const savedOutputs = collectResultPaths(results, (result) => result.savedOutputPath);
247
+ const artifactOutputs = collectResultPaths(results, (result) => result.artifactPaths?.outputPath);
248
+ const sections = ["## Subagent result", output];
249
+ if (sessionFiles.length > 0) sections.push("## Child session exports", formatExportPathList(sessionFiles));
250
+ if (savedOutputs.length > 0) sections.push("## Saved outputs", formatExportPathList(savedOutputs));
251
+ if (artifactOutputs.length > 0) sections.push("## Artifact outputs", formatExportPathList(artifactOutputs));
252
+ return sections.join("\n\n");
253
+ }
254
+
255
+ function persistSlashSessionSnapshot(ctx: ExtensionContext): void {
256
+ try {
257
+ if (!ctx.sessionManager) return;
258
+ const sessionManager = ctx.sessionManager as typeof ctx.sessionManager & {
259
+ _rewriteFile?: () => void;
260
+ flushed?: boolean;
261
+ };
262
+ const sessionFile = sessionManager.getSessionFile();
263
+ if (!sessionFile || typeof sessionManager._rewriteFile !== "function") return;
264
+ fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
265
+ sessionManager._rewriteFile();
266
+ sessionManager.flushed = true;
267
+ } catch (error) {
268
+ console.error("Failed to persist slash session snapshot for export:", error);
269
+ }
270
+ }
271
+
272
+ async function runSlashSubagent(
273
+ pi: ExtensionAPI,
274
+ ctx: ExtensionContext,
275
+ params: SubagentParamsLike,
276
+ ): Promise<void> {
277
+ if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
278
+ const requestId = randomUUID();
279
+ const initialDetails = buildSlashInitialResult(requestId, params);
280
+ const initialText = extractSlashMessageText(initialDetails.result.content) || "Running subagent...";
281
+ pi.sendMessage({
282
+ customType: SLASH_RESULT_TYPE,
283
+ content: initialText,
284
+ display: true,
285
+ details: initialDetails,
286
+ });
287
+ persistSlashSessionSnapshot(ctx);
288
+
289
+ try {
290
+ const response = await requestSlashRun(pi, ctx, requestId, params);
291
+ const finalDetails = finalizeSlashResult(response);
292
+ pi.sendMessage({
293
+ customType: SLASH_RESULT_TYPE,
294
+ content: buildSlashExportText(response),
295
+ display: true,
296
+ details: finalDetails,
297
+ });
298
+ persistSlashSessionSnapshot(ctx);
299
+ if (ctx.hasUI) {
300
+ ctx.ui.setStatus("subagent-slash", undefined);
301
+ }
302
+ if (response.isError && ctx.hasUI) {
303
+ ctx.ui.notify(response.errorText || "Subagent failed", "error");
304
+ }
305
+ } catch (error) {
306
+ const message = error instanceof Error ? error.message : String(error);
307
+ const failedDetails = failSlashResult(requestId, params, message);
308
+ pi.sendMessage({
309
+ customType: SLASH_RESULT_TYPE,
310
+ content: `## Subagent result\n\n${message}`,
311
+ display: true,
312
+ details: failedDetails,
313
+ });
314
+ persistSlashSessionSnapshot(ctx);
315
+ if (ctx.hasUI) {
316
+ ctx.ui.setStatus("subagent-slash", undefined);
317
+ }
318
+ if (message === "Cancelled") {
319
+ if (ctx.hasUI) ctx.ui.notify("Cancelled", "warning");
320
+ return;
321
+ }
322
+ if (ctx.hasUI) ctx.ui.notify(message, "error");
323
+ }
324
+ }
325
+
326
+
327
+ interface ParsedStep { name: string; config: InlineConfig; task?: string }
328
+
329
+ const parseAgentArgs = (
330
+ state: SubagentState,
331
+ args: string,
332
+ command: string,
333
+ ctx: ExtensionContext,
334
+ ): { steps: ParsedStep[]; task: string } | null => {
335
+ const input = args.trim();
336
+ const usage = `Usage: /${command} agent1 "task1" -> agent2 "task2"`;
337
+ let steps: ParsedStep[];
338
+ let sharedTask: string;
339
+ let perStep = false;
340
+
341
+ if (input.includes(" -> ")) {
342
+ perStep = true;
343
+ const segments = input.split(" -> ");
344
+ steps = [];
345
+ for (const seg of segments) {
346
+ const trimmed = seg.trim();
347
+ if (!trimmed) continue;
348
+ let agentPart: string;
349
+ let task: string | undefined;
350
+ const qMatch = trimmed.match(/^(\S+(?:\[[^\]]*\])?)\s+(?:"([^"]*)"|'([^']*)')$/);
351
+ if (qMatch) {
352
+ agentPart = qMatch[1]!;
353
+ task = (qMatch[2] ?? qMatch[3]) || undefined;
354
+ } else {
355
+ const dashIdx = trimmed.indexOf(" -- ");
356
+ if (dashIdx !== -1) {
357
+ agentPart = trimmed.slice(0, dashIdx).trim();
358
+ task = trimmed.slice(dashIdx + 4).trim() || undefined;
359
+ } else {
360
+ agentPart = trimmed;
361
+ }
362
+ }
363
+ const parsed = parseAgentToken(agentPart);
364
+ steps.push({ ...parsed, task });
365
+ }
366
+ sharedTask = steps.find((s) => s.task)?.task ?? "";
367
+ } else {
368
+ const delimiterIndex = input.indexOf(" -- ");
369
+ if (delimiterIndex === -1) {
370
+ ctx.ui.notify(usage, "error");
371
+ return null;
372
+ }
373
+ const agentsPart = input.slice(0, delimiterIndex).trim();
374
+ sharedTask = input.slice(delimiterIndex + 4).trim();
375
+ if (!agentsPart || !sharedTask) {
376
+ ctx.ui.notify(usage, "error");
377
+ return null;
378
+ }
379
+ steps = agentsPart.split(/\s+/).filter(Boolean).map((t) => parseAgentToken(t));
380
+ }
381
+
382
+ if (steps.length === 0) {
383
+ ctx.ui.notify(usage, "error");
384
+ return null;
385
+ }
386
+ if (!state.baseCwd) {
387
+ ctx.ui.notify("Subagent session cwd is not initialized yet", "error");
388
+ return null;
389
+ }
390
+ const agents = discoverAgents(state.baseCwd, "both").agents;
391
+ for (const step of steps) {
392
+ if (!agents.find((a) => a.name === step.name)) {
393
+ ctx.ui.notify(`Unknown agent: ${step.name}`, "error");
394
+ return null;
395
+ }
396
+ }
397
+ if (command === "chain" && !steps[0]?.task && (perStep || !sharedTask)) {
398
+ ctx.ui.notify(`First step must have a task: /chain agent "task" -> agent2`, "error");
399
+ return null;
400
+ }
401
+ if (command === "parallel" && !steps.some((s) => s.task) && !sharedTask) {
402
+ ctx.ui.notify("At least one step must have a task", "error");
403
+ return null;
404
+ }
405
+ return { steps, task: sharedTask };
406
+ };
407
+
408
+ export function registerSlashCommands(
409
+ pi: ExtensionAPI,
410
+ state: SubagentState,
411
+ ): void {
412
+ pi.registerCommand("run", {
413
+ description: "Run a subagent directly: /run agent[output=file] [task] [--bg] [--fork]",
414
+ getArgumentCompletions: makeAgentCompletions(state, false),
415
+ handler: async (args, ctx) => {
416
+ const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
417
+ const input = cleanedArgs.trim();
418
+ const firstSpace = input.indexOf(" ");
419
+ if (!input) { ctx.ui.notify("Usage: /run <agent> [task] [--bg] [--fork]", "error"); return; }
420
+ const { name: agentName, config: inline } = parseAgentToken(firstSpace === -1 ? input : input.slice(0, firstSpace));
421
+ const task = firstSpace === -1 ? "" : input.slice(firstSpace + 1).trim();
422
+
423
+ if (!state.baseCwd) { ctx.ui.notify("Subagent session cwd is not initialized yet", "error"); return; }
424
+ const agents = discoverAgents(state.baseCwd, "both").agents;
425
+ if (!agents.find((a) => a.name === agentName)) { ctx.ui.notify(`Unknown agent: ${agentName}`, "error"); return; }
426
+
427
+ let finalTask = task;
428
+ if (inline.reads && Array.isArray(inline.reads) && inline.reads.length > 0) {
429
+ finalTask = `[Read from: ${inline.reads.join(", ")}]\n\n${finalTask}`;
430
+ }
431
+ const params: SubagentParamsLike = { agent: agentName, task: finalTask, clarify: false, agentScope: "both" };
432
+ if (inline.output !== undefined) params.output = inline.output;
433
+ if (inline.outputMode !== undefined) params.outputMode = inline.outputMode;
434
+ if (inline.skill !== undefined) params.skill = inline.skill;
435
+ if (inline.model) params.model = inline.model;
436
+ if (bg) params.async = true;
437
+ if (fork) params.context = "fork";
438
+ await runSlashSubagent(pi, ctx, params);
439
+ },
440
+ });
441
+
442
+ pi.registerCommand("chain", {
443
+ description: "Run agents in sequence: /chain scout \"task\" -> planner [--bg] [--fork]",
444
+ getArgumentCompletions: makeAgentCompletions(state, true),
445
+ handler: async (args, ctx) => {
446
+ const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
447
+ const parsed = parseAgentArgs(state, cleanedArgs, "chain", ctx);
448
+ if (!parsed) return;
449
+ const chain = parsed.steps.map(({ name, config, task: stepTask }, i) => ({
450
+ agent: name,
451
+ ...(stepTask ? { task: stepTask } : i === 0 && parsed.task ? { task: parsed.task } : {}),
452
+ ...(config.output !== undefined ? { output: config.output } : {}),
453
+ ...(config.outputMode !== undefined ? { outputMode: config.outputMode } : {}),
454
+ ...(config.reads !== undefined ? { reads: config.reads } : {}),
455
+ ...(config.model ? { model: config.model } : {}),
456
+ ...(config.skill !== undefined ? { skill: config.skill } : {}),
457
+ ...(config.progress !== undefined ? { progress: config.progress } : {}),
458
+ }));
459
+ const params: SubagentParamsLike = { chain, task: parsed.task, clarify: false, agentScope: "both" };
460
+ if (bg) params.async = true;
461
+ if (fork) params.context = "fork";
462
+ await runSlashSubagent(pi, ctx, params);
463
+ },
464
+ });
465
+
466
+ pi.registerCommand("run-chain", {
467
+ description: "Run a saved chain: /run-chain chainName -- task [--bg] [--fork]",
468
+ getArgumentCompletions: makeChainCompletions(state),
469
+ handler: async (args, ctx) => {
470
+ const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
471
+ const delimiterIndex = cleanedArgs.indexOf(" -- ");
472
+ const usage = "Usage: /run-chain <chainName> -- <task> [--bg] [--fork]";
473
+ if (delimiterIndex === -1) {
474
+ ctx.ui.notify(usage, "error");
475
+ return;
476
+ }
477
+ const chainName = cleanedArgs.slice(0, delimiterIndex).trim();
478
+ const task = cleanedArgs.slice(delimiterIndex + 4).trim();
479
+ if (!chainName || !task) {
480
+ ctx.ui.notify(usage, "error");
481
+ return;
482
+ }
483
+ if (!state.baseCwd) { ctx.ui.notify("Subagent session cwd is not initialized yet", "error"); return; }
484
+ const chain = discoverSavedChains(state.baseCwd).find((candidate) => candidate.name === chainName);
485
+ if (!chain) {
486
+ ctx.ui.notify(`Unknown chain: ${chainName}`, "error");
487
+ return;
488
+ }
489
+ const params: SubagentParamsLike = { chain: mapSavedChainSteps(chain), task, clarify: false, agentScope: "both" };
490
+ if (bg) params.async = true;
491
+ if (fork) params.context = "fork";
492
+ await runSlashSubagent(pi, ctx, params);
493
+ },
494
+ });
495
+
496
+ pi.registerCommand("parallel", {
497
+ description: "Run agents in parallel: /parallel scout \"task1\" -> reviewer \"task2\" [--bg] [--fork]",
498
+ getArgumentCompletions: makeAgentCompletions(state, true),
499
+ handler: async (args, ctx) => {
500
+ const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
501
+ const parsed = parseAgentArgs(state, cleanedArgs, "parallel", ctx);
502
+ if (!parsed) return;
503
+ const tasks = parsed.steps.map(({ name, config, task: stepTask }) => ({
504
+ agent: name,
505
+ task: stepTask ?? parsed.task,
506
+ ...(config.output !== undefined ? { output: config.output } : {}),
507
+ ...(config.outputMode !== undefined ? { outputMode: config.outputMode } : {}),
508
+ ...(config.reads !== undefined ? { reads: config.reads } : {}),
509
+ ...(config.model ? { model: config.model } : {}),
510
+ ...(config.skill !== undefined ? { skill: config.skill } : {}),
511
+ ...(config.progress !== undefined ? { progress: config.progress } : {}),
512
+ }));
513
+ const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
514
+ if (bg) params.async = true;
515
+ if (fork) params.context = "fork";
516
+ await runSlashSubagent(pi, ctx, params);
517
+ },
518
+ });
519
+
520
+
521
+ pi.registerCommand("subagents-doctor", {
522
+ description: "Show subagent diagnostics",
523
+ handler: async (_args, ctx) => {
524
+ await runSlashSubagent(pi, ctx, { action: "doctor" });
525
+ },
526
+ });
527
+
528
+ }