@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,573 @@
1
+ /**
2
+ * Subagent Tool
3
+ *
4
+ * Full-featured subagent with sync and async modes.
5
+ * - Sync (default): Streams output, renders markdown, tracks usage
6
+ * - Async: Background execution, emits events when done
7
+ *
8
+ * Modes: single (agent + task), parallel (tasks[]), chain (chain[] with {previous})
9
+ * Toggle: async parameter (default: false, configurable via config.json)
10
+ *
11
+ * Config file: ~/.pi/agent/extensions/subagent/config.json
12
+ * { "asyncByDefault": true, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
13
+ */
14
+
15
+ import * as fs from "node:fs";
16
+ import * as os from "node:os";
17
+ import * as path from "node:path";
18
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
19
+ import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
20
+ import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
21
+ import { discoverAgents } from "../agents/agents.ts";
22
+ import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
23
+ import { resolveCurrentSessionId } from "../shared/session-identity.ts";
24
+ import { cleanupOldChainDirs } from "../shared/settings.ts";
25
+ import { clearLegacyResultAnimationTimer, renderWidget, renderSubagentResult } from "../tui/render.ts";
26
+ import { SubagentParams } from "./schemas.ts";
27
+ import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
28
+ import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
29
+ import { createResultWatcher } from "../runs/background/result-watcher.ts";
30
+ import { registerSlashCommands } from "../slash/slash-commands.ts";
31
+ import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.ts";
32
+ import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
33
+ import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
34
+ import { inspectSubagentStatus } from "../runs/background/run-status.ts";
35
+ import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
36
+ import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
37
+ import registerFanoutChildSubagentExtension from "./fanout-child.ts";
38
+ import { formatDuration, shortenPath } from "../shared/formatters.ts";
39
+ import { loadConfig } from "./config.ts";
40
+ import {
41
+ type Details,
42
+ type SubagentState,
43
+ ASYNC_DIR,
44
+ DEFAULT_ARTIFACT_CONFIG,
45
+ RESULTS_DIR,
46
+ SLASH_RESULT_TYPE,
47
+ SUBAGENT_ASYNC_COMPLETE_EVENT,
48
+ SUBAGENT_ASYNC_STARTED_EVENT,
49
+ SUBAGENT_CONTROL_EVENT,
50
+ WIDGET_KEY,
51
+ } from "../shared/types.ts";
52
+ import {
53
+ clearPendingForegroundControlNotices,
54
+ formatSubagentControlNotice,
55
+ handleSubagentControlNotice,
56
+ SUBAGENT_CONTROL_MESSAGE_TYPE,
57
+ type SubagentControlMessageDetails,
58
+ } from "./control-notices.ts";
59
+
60
+ export { loadConfig } from "./config.ts";
61
+
62
+ /**
63
+ * Derive subagent session base directory from parent session file.
64
+ * If parent session is ~/.pi/agent/sessions/abc123.jsonl,
65
+ * returns ~/.pi/agent/sessions/abc123/ as the base.
66
+ * Callers add runId to create the actual session root: abc123/{runId}/
67
+ * Falls back to a unique temp directory if no parent session.
68
+ */
69
+ function getSubagentSessionRoot(parentSessionFile: string | null): string {
70
+ if (parentSessionFile) {
71
+ const baseName = path.basename(parentSessionFile, ".jsonl");
72
+ const sessionsDir = path.dirname(parentSessionFile);
73
+ return path.join(sessionsDir, baseName);
74
+ }
75
+ return fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-session-"));
76
+ }
77
+
78
+ function expandTilde(p: string): string {
79
+ return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
80
+ }
81
+
82
+ /**
83
+ * Create a directory and verify it is actually accessible.
84
+ * On Windows with Azure AD/Entra ID, directories created shortly after
85
+ * wake-from-sleep can end up with broken NTFS ACLs (null DACL) when the
86
+ * cloud SID cannot be resolved without network connectivity. This leaves
87
+ * the directory completely inaccessible to the creating user.
88
+ */
89
+ function ensureAccessibleDir(dirPath: string): void {
90
+ fs.mkdirSync(dirPath, { recursive: true });
91
+ try {
92
+ fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
93
+ } catch {
94
+ try {
95
+ fs.rmSync(dirPath, { recursive: true, force: true });
96
+ } catch {
97
+ // Best effort: retry mkdir/access even if cleanup fails.
98
+ }
99
+ fs.mkdirSync(dirPath, { recursive: true });
100
+ fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
101
+ }
102
+ }
103
+
104
+ function isSlashResultRunning(result: { details?: Details }): boolean {
105
+ return result.details?.progress?.some((entry) => entry.status === "running")
106
+ || result.details?.results.some((entry) => entry.progress?.status === "running")
107
+ || false;
108
+ }
109
+
110
+ function isSlashResultError(result: { details?: Details }): boolean {
111
+ return result.details?.results.some((entry) => entry.exitCode !== 0 && entry.progress?.status !== "running") || false;
112
+ }
113
+
114
+ function isStaleExtensionContextError(error: unknown): boolean {
115
+ return error instanceof Error && error.message.includes("Extension context no longer active");
116
+ }
117
+
118
+ function rebuildSlashResultContainer(
119
+ container: Container,
120
+ result: AgentToolResult<Details>,
121
+ options: { expanded: boolean },
122
+ theme: ExtensionContext["ui"]["theme"],
123
+ ): void {
124
+ container.clear();
125
+ container.addChild(new Spacer(1));
126
+ const boxTheme = isSlashResultRunning(result) ? "toolPendingBg" : isSlashResultError(result) ? "toolErrorBg" : "toolSuccessBg";
127
+ const box = new Box(1, 1, (text: string) => theme.bg(boxTheme, text));
128
+ box.addChild(renderSubagentResult(result, options, theme));
129
+ container.addChild(box);
130
+ }
131
+
132
+ function createSlashResultComponent(
133
+ details: SlashMessageDetails,
134
+ options: { expanded: boolean },
135
+ theme: ExtensionContext["ui"]["theme"],
136
+ ): Container {
137
+ const container = new Container();
138
+ let lastVersion = -1;
139
+ container.render = (width: number): string[] => {
140
+ const snapshot = getSlashRenderableSnapshot(details);
141
+ if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
142
+ lastVersion = snapshot.version;
143
+ rebuildSlashResultContainer(container, snapshot.result, options, theme);
144
+ }
145
+ return Container.prototype.render.call(container, width);
146
+ };
147
+ return container;
148
+ }
149
+
150
+ function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {
151
+ const lines = content.split("\n");
152
+ const header = lines[0] ?? "";
153
+ const match = header.match(/^Background task (completed|failed|paused): \*\*(.+?)\*\*(?:\s+(\([^)]*\)))?$/);
154
+ if (!match) return undefined;
155
+ const body = lines.slice(2);
156
+ let sessionIndex = -1;
157
+ for (let i = body.length - 1; i >= 1; i--) {
158
+ if (body[i - 1]?.trim() === "" && /^(Session|Session file|Session share error):\s+/.test(body[i]!)) {
159
+ sessionIndex = i;
160
+ break;
161
+ }
162
+ }
163
+ const sessionLine = sessionIndex >= 0 ? body[sessionIndex] : undefined;
164
+ const resultLines = sessionIndex >= 0 ? body.slice(0, sessionIndex) : body;
165
+ const resultPreview = resultLines.join("\n").trim() || "(no output)";
166
+ let sessionLabel: string | undefined;
167
+ let sessionValue: string | undefined;
168
+ if (sessionLine) {
169
+ const separator = sessionLine.indexOf(":");
170
+ sessionLabel = sessionLine.slice(0, separator).toLowerCase();
171
+ sessionValue = sessionLine.slice(separator + 1).trim();
172
+ }
173
+ return {
174
+ agent: match[2]!,
175
+ status: match[1] as SubagentNotifyDetails["status"],
176
+ ...(match[3] ? { taskInfo: match[3] } : {}),
177
+ resultPreview,
178
+ ...(sessionLabel && sessionValue ? { sessionLabel, sessionValue } : {}),
179
+ };
180
+ }
181
+
182
+ class SubagentControlNoticeComponent implements Component {
183
+ constructor(
184
+ private readonly details: SubagentControlMessageDetails,
185
+ private readonly theme: ExtensionContext["ui"]["theme"],
186
+ ) {}
187
+
188
+ invalidate(): void {}
189
+
190
+ render(width: number): string[] {
191
+ const eventLabel = this.details.event.type.replaceAll("_", " ");
192
+ if (width < 3) return [truncateToWidth(`Subagent ${eventLabel}`, width)];
193
+ const bodyWidth = Math.max(1, width - 2);
194
+ const borderChar = "─";
195
+ const header = ` ⚠ Subagent ${eventLabel}: ${this.details.event.agent} `;
196
+ const headerText = truncateToWidth(header, bodyWidth, "");
197
+ const headerPadding = Math.max(0, bodyWidth - visibleWidth(headerText));
198
+ const lines = [this.theme.fg("accent", `╭${headerText}${borderChar.repeat(headerPadding)}╮`)];
199
+
200
+ for (const line of wrapTextWithAnsi(formatSubagentControlNotice(this.details), bodyWidth)) {
201
+ const text = truncateToWidth(line, bodyWidth, "");
202
+ const padding = Math.max(0, bodyWidth - visibleWidth(text));
203
+ lines.push(this.theme.fg("accent", `│${text}${" ".repeat(padding)}│`));
204
+ }
205
+ lines.push(this.theme.fg("accent", `╰${borderChar.repeat(bodyWidth)}╯`));
206
+ return lines;
207
+ }
208
+ }
209
+
210
+ export default function registerSubagentExtension(pi: ExtensionAPI): void {
211
+ if (process.env[SUBAGENT_CHILD_ENV] === "1") {
212
+ if (process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1") registerFanoutChildSubagentExtension(pi);
213
+ return;
214
+ }
215
+ const globalStore = globalThis as Record<string, unknown>;
216
+ const runtimeCleanupStoreKey = "__piSubagentRuntimeCleanup";
217
+ const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
218
+ if (typeof previousRuntimeCleanup === "function") {
219
+ try {
220
+ previousRuntimeCleanup();
221
+ } catch {
222
+ // Best effort cleanup for stale timers from an older reload.
223
+ }
224
+ }
225
+
226
+ ensureAccessibleDir(RESULTS_DIR);
227
+ ensureAccessibleDir(ASYNC_DIR);
228
+ cleanupOldChainDirs();
229
+
230
+ const config = loadConfig();
231
+ const asyncByDefault = config.asyncByDefault === true;
232
+ const tempArtifactsDir = getArtifactsDir(null);
233
+ cleanupAllArtifactDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
234
+
235
+ const state: SubagentState = {
236
+ baseCwd: "",
237
+ currentSessionId: null,
238
+ asyncJobs: new Map(),
239
+ foregroundRuns: new Map(),
240
+ foregroundControls: new Map(),
241
+ lastForegroundControlId: null,
242
+ pendingForegroundControlNotices: new Map(),
243
+ cleanupTimers: new Map(),
244
+ lastUiContext: null,
245
+ poller: null,
246
+ completionSeen: new Map(),
247
+ watcher: null,
248
+ watcherRestartTimer: null,
249
+ resultFileCoalescer: {
250
+ schedule: () => false,
251
+ clear: () => {},
252
+ },
253
+ };
254
+
255
+ const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
256
+ pi,
257
+ state,
258
+ RESULTS_DIR,
259
+ 10 * 60 * 1000,
260
+ );
261
+ startResultWatcher();
262
+ primeExistingResults();
263
+
264
+ const runtimeCleanup = () => {
265
+ stopResultWatcher();
266
+ clearPendingForegroundControlNotices(state);
267
+ if (state.poller) {
268
+ clearInterval(state.poller);
269
+ state.poller = null;
270
+ }
271
+ };
272
+ globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
273
+
274
+ const { ensurePoller, handleStarted, handleComplete, resetJobs } = createAsyncJobTracker(pi, state, ASYNC_DIR);
275
+ const executor = createSubagentExecutor({
276
+ pi,
277
+ state,
278
+ config,
279
+ asyncByDefault,
280
+ tempArtifactsDir,
281
+ getSubagentSessionRoot,
282
+ expandTilde,
283
+ discoverAgents,
284
+ });
285
+
286
+ pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
287
+ const details = resolveSlashMessageDetails(message.details);
288
+ if (!details) return undefined;
289
+ return createSlashResultComponent(details, options, theme);
290
+ });
291
+
292
+ pi.registerMessageRenderer<SubagentNotifyDetails>("subagent-notify", (message, options, theme) => {
293
+ const content = typeof message.content === "string" ? message.content : "";
294
+ const details = (message.details as SubagentNotifyDetails | undefined) ?? parseSubagentNotifyContent(content);
295
+ if (!details) return new Text(content, 0, 0);
296
+ const icon = details.status === "completed"
297
+ ? theme.fg("success", "✓")
298
+ : details.status === "paused"
299
+ ? theme.fg("warning", "■")
300
+ : theme.fg("error", "✗");
301
+ const parts: string[] = [];
302
+ if (details.taskInfo) parts.push(details.taskInfo);
303
+ if (details.durationMs !== undefined) parts.push(formatDuration(details.durationMs));
304
+ let text = `${icon} ${theme.bold(details.agent)} ${theme.fg("dim", details.status)}`;
305
+ if (parts.length > 0) text += ` ${theme.fg("dim", "·")} ${parts.map((part) => theme.fg("dim", part)).join(` ${theme.fg("dim", "·")} `)}`;
306
+ const trimmedPreview = details.resultPreview.trim();
307
+ const previewLines = options.expanded
308
+ ? trimmedPreview.split("\n").filter((line) => line.trim())
309
+ : [trimmedPreview.split("\n", 1)[0] ?? ""].filter((line) => line.trim());
310
+ for (const line of previewLines.length > 0 ? previewLines : ["(no output)"]) {
311
+ text += `\n ${theme.fg("dim", `⎿ ${line}`)}`;
312
+ }
313
+ if (!options.expanded && trimmedPreview.includes("\n")) {
314
+ text += `\n ${theme.fg("dim", "Ctrl+O full notification")}`;
315
+ }
316
+ if (details.sessionLabel && details.sessionValue) {
317
+ text += `\n ${theme.fg("muted", `${details.sessionLabel}: ${shortenPath(details.sessionValue)}`)}`;
318
+ }
319
+ return new Text(text, 0, 0);
320
+ });
321
+
322
+ pi.registerMessageRenderer<SubagentControlMessageDetails>(SUBAGENT_CONTROL_MESSAGE_TYPE, (message, _options, theme) => {
323
+ const details = message.details as SubagentControlMessageDetails | undefined;
324
+ if (!details?.event) return undefined;
325
+ const content = typeof message.content === "string" ? message.content : undefined;
326
+ return new SubagentControlNoticeComponent({ ...details, noticeText: formatSubagentControlNotice(details, content) }, theme);
327
+ });
328
+
329
+ const executeSubagentCollapsed = (id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((result: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => {
330
+ if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
331
+ return executor.execute(id, params, signal, onUpdate, ctx);
332
+ };
333
+
334
+ const slashBridge = registerSlashSubagentBridge({
335
+ events: pi.events,
336
+ getContext: () => state.lastUiContext,
337
+ execute: (id, params, signal, onUpdate, ctx) =>
338
+ executeSubagentCollapsed(id, params, signal, onUpdate, ctx),
339
+ });
340
+
341
+ const promptTemplateBridge = registerPromptTemplateDelegationBridge({
342
+ events: pi.events,
343
+ getContext: () => state.lastUiContext,
344
+ execute: async (requestId, request, signal, ctx, onUpdate) => {
345
+ if (request.tasks && request.tasks.length > 0) {
346
+ return executeSubagentCollapsed(
347
+ requestId,
348
+ {
349
+ tasks: request.tasks,
350
+ context: request.context,
351
+ cwd: request.cwd,
352
+ worktree: request.worktree,
353
+ async: false,
354
+ clarify: false,
355
+ },
356
+ signal,
357
+ onUpdate,
358
+ ctx,
359
+ );
360
+ }
361
+ return executeSubagentCollapsed(
362
+ requestId,
363
+ {
364
+ agent: request.agent,
365
+ task: request.task,
366
+ context: request.context,
367
+ cwd: request.cwd,
368
+ model: request.model,
369
+ async: false,
370
+ clarify: false,
371
+ },
372
+ signal,
373
+ onUpdate,
374
+ ctx,
375
+ );
376
+ },
377
+ });
378
+
379
+ function effectiveParallelTaskCount(tasks: Array<{ count?: unknown }> | undefined): number {
380
+ if (!tasks || tasks.length === 0) return 0;
381
+ return tasks.reduce((total, task) => {
382
+ const count = typeof task.count === "number" && Number.isInteger(task.count) && task.count >= 1 ? task.count : 1;
383
+ return total + count;
384
+ }, 0);
385
+ }
386
+
387
+ const tool: ToolDefinition<typeof SubagentParams, Details> = {
388
+ name: "subagent",
389
+ label: "Subagent",
390
+ description: `Delegate to subagents or manage agent definitions.
391
+
392
+ EXECUTION (use exactly ONE mode):
393
+ • Before executing, use { action: "list" } to inspect configured agents/chains. Only execute agents listed as executable/non-disabled.
394
+ • SINGLE: { agent, task? } - one task; omit task for self-contained agents
395
+ • CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
396
+ • PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
397
+ • Optional context: { context: "fresh" | "fork" } (default: if any requested agent has defaultContext: "fork", the whole invocation uses fork; otherwise "fresh"; inspect agent defaults via { action: "list" })
398
+
399
+ CHAIN TEMPLATE VARIABLES (use in task strings):
400
+ • {task} - The original task/request from the user
401
+ • {previous} - Text response from the previous step (empty for first step)
402
+ • {chain_dir} - Shared directory for chain files (e.g., <tmpdir>/pi-subagents-<scope>/chain-runs/abc123/)
403
+
404
+ Example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", task:"Plan based on {previous}"}] }
405
+
406
+ MANAGEMENT (use action field, omit agent/task/chain/tasks):
407
+ • { action: "list" } - discover executable agents/chains
408
+ • { action: "get", agent: "name" } - full detail; packaged agents use dotted runtime names like "package.agent"
409
+ • { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, ... } }
410
+ • { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", ... } } - merge
411
+ • { action: "delete", agent: "code-analysis.custom-agent" }
412
+ • Use chainName for chain operations; packaged chains also use dotted runtime names
413
+
414
+ CONTROL:
415
+ • { action: "status", id: "..." } - inspect an async/background run by id or prefix
416
+ • { action: "interrupt", id?: "..." } - soft-interrupt the current child turn and leave the run paused
417
+ • { action: "resume", id: "...", message: "...", index?: 0 } - follow up with a live async child or revive a completed async/foreground child from its session
418
+
419
+ DIAGNOSTICS:
420
+ • { action: "doctor" } - read-only report for runtime paths, discovery, sessions, and intercom`,
421
+ parameters: SubagentParams,
422
+
423
+ execute(id, params, signal, onUpdate, ctx) {
424
+ return executeSubagentCollapsed(id, params, signal, onUpdate, ctx);
425
+ },
426
+
427
+ renderCall(args, theme) {
428
+ if (args.action) {
429
+ const target = args.agent || args.chainName || "";
430
+ return new Text(
431
+ `${theme.fg("toolTitle", theme.bold("subagent "))}${args.action}${target ? ` ${theme.fg("accent", target)}` : ""}`,
432
+ 0, 0,
433
+ );
434
+ }
435
+ const isParallel = (args.tasks?.length ?? 0) > 0;
436
+ const parallelCount = effectiveParallelTaskCount(args.tasks as Array<{ count?: unknown }> | undefined);
437
+ const asyncLabel = args.async === true && args.clarify !== true && !isParallel ? theme.fg("warning", " [async]") : "";
438
+ if (args.chain?.length)
439
+ return new Text(
440
+ `${theme.fg("toolTitle", theme.bold("subagent "))}chain (${args.chain.length})${asyncLabel}`,
441
+ 0,
442
+ 0,
443
+ );
444
+ if (isParallel)
445
+ return new Text(
446
+ `${theme.fg("toolTitle", theme.bold("subagent "))}parallel (${parallelCount})`,
447
+ 0,
448
+ 0,
449
+ );
450
+ return new Text(
451
+ `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
452
+ 0,
453
+ 0,
454
+ );
455
+ },
456
+
457
+ renderResult(result, options, theme, context) {
458
+ clearLegacyResultAnimationTimer(context);
459
+ return renderSubagentResult(result, options, theme);
460
+ },
461
+
462
+ };
463
+
464
+ pi.registerTool(tool);
465
+ registerSlashCommands(pi, state);
466
+
467
+ const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
468
+ const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
469
+ const previousEventUnsubscribes = globalStore[eventUnsubscribeStoreKey];
470
+ if (Array.isArray(previousEventUnsubscribes)) {
471
+ for (const unsubscribe of previousEventUnsubscribes) {
472
+ if (typeof unsubscribe !== "function") continue;
473
+ try {
474
+ unsubscribe();
475
+ } catch {
476
+ // Best effort cleanup for stale handlers from an older reload.
477
+ }
478
+ }
479
+ }
480
+ registerSubagentNotify(pi);
481
+
482
+ const existingVisibleControlNotices = globalStore[controlNoticeSeenStoreKey];
483
+ const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
484
+ globalStore[controlNoticeSeenStoreKey] = visibleControlNotices;
485
+ const controlEventHandler = (payload: unknown) => {
486
+ handleSubagentControlNotice({
487
+ pi,
488
+ state,
489
+ visibleControlNotices,
490
+ details: payload as SubagentControlMessageDetails,
491
+ });
492
+ };
493
+ const eventUnsubscribes = [
494
+ pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, handleStarted),
495
+ pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete),
496
+ pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
497
+ ];
498
+ globalStore[eventUnsubscribeStoreKey] = eventUnsubscribes;
499
+
500
+ pi.on("tool_result", (event, ctx) => {
501
+ if (event.toolName !== "subagent") return;
502
+ if (!ctx.hasUI) return;
503
+ state.lastUiContext = ctx;
504
+ if (state.asyncJobs.size > 0) {
505
+ renderWidget(ctx, Array.from(state.asyncJobs.values()));
506
+ ctx.ui.requestRender?.();
507
+ ensurePoller();
508
+ }
509
+ });
510
+
511
+ const cleanupSessionArtifacts = (ctx: ExtensionContext) => {
512
+ try {
513
+ const sessionFile = ctx.sessionManager.getSessionFile();
514
+ if (sessionFile) {
515
+ cleanupOldArtifacts(getArtifactsDir(sessionFile), DEFAULT_ARTIFACT_CONFIG.cleanupDays);
516
+ }
517
+ } catch {
518
+ // Cleanup failures should not block session lifecycle events.
519
+ }
520
+ };
521
+
522
+ const resetSessionState = (ctx: ExtensionContext) => {
523
+ state.baseCwd = ctx.cwd;
524
+ state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
525
+ state.lastUiContext = ctx;
526
+ cleanupSessionArtifacts(ctx);
527
+ clearPendingForegroundControlNotices(state);
528
+ resetJobs(ctx);
529
+ restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
530
+ primeExistingResults();
531
+ };
532
+
533
+ pi.on("session_start", (_event, ctx) => {
534
+ resetSessionState(ctx);
535
+ });
536
+
537
+ pi.on("session_shutdown", () => {
538
+ for (const unsubscribe of eventUnsubscribes) {
539
+ try {
540
+ unsubscribe();
541
+ } catch {
542
+ // Best effort cleanup during shutdown.
543
+ }
544
+ }
545
+ if (globalStore[eventUnsubscribeStoreKey] === eventUnsubscribes) {
546
+ delete globalStore[eventUnsubscribeStoreKey];
547
+ }
548
+ stopResultWatcher();
549
+ if (state.poller) clearInterval(state.poller);
550
+ state.poller = null;
551
+ clearPendingForegroundControlNotices(state);
552
+ for (const timer of state.cleanupTimers.values()) {
553
+ clearTimeout(timer);
554
+ }
555
+ state.cleanupTimers.clear();
556
+ state.asyncJobs.clear();
557
+ clearSlashSnapshots();
558
+ slashBridge.cancelAll();
559
+ slashBridge.dispose();
560
+ promptTemplateBridge.cancelAll();
561
+ promptTemplateBridge.dispose();
562
+ if (globalStore[runtimeCleanupStoreKey] === runtimeCleanup) {
563
+ delete globalStore[runtimeCleanupStoreKey];
564
+ }
565
+ try {
566
+ if (state.lastUiContext?.hasUI) {
567
+ state.lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
568
+ }
569
+ } catch (error) {
570
+ if (!isStaleExtensionContextError(error)) throw error;
571
+ }
572
+ });
573
+ }