pi-subagents 0.35.1 → 0.37.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 (89) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +224 -31
  3. package/agents/advisor.md +73 -0
  4. package/agents/delegate.md +2 -0
  5. package/agents/worker.md +2 -0
  6. package/package.json +11 -13
  7. package/skills/pi-subagents/SKILL.md +42 -13
  8. package/src/agents/agent-management.ts +7 -1
  9. package/src/agents/agents.ts +121 -17
  10. package/src/api/capability-ceiling.ts +17 -0
  11. package/src/api/delegation.ts +127 -0
  12. package/src/api/preflight.ts +399 -0
  13. package/src/extension/config.ts +7 -1
  14. package/src/extension/index.ts +52 -38
  15. package/src/extension/rpc.ts +31 -2
  16. package/src/extension/schemas.ts +29 -3
  17. package/src/extension/tool-description.ts +4 -2
  18. package/src/intercom/intercom-bridge.ts +1 -1
  19. package/src/intercom/native-supervisor-channel.ts +45 -6
  20. package/src/intercom/result-intercom.ts +7 -0
  21. package/src/runs/background/async-execution.ts +158 -17
  22. package/src/runs/background/async-job-tracker.ts +4 -0
  23. package/src/runs/background/async-resume.ts +47 -8
  24. package/src/runs/background/async-status.ts +94 -3
  25. package/src/runs/background/chain-append.ts +2 -0
  26. package/src/runs/background/completion-batcher.ts +6 -4
  27. package/src/runs/background/completion-dedupe.ts +2 -11
  28. package/src/runs/background/fleet-view.ts +9 -4
  29. package/src/runs/background/notify.ts +132 -120
  30. package/src/runs/background/process-terminal.ts +280 -0
  31. package/src/runs/background/result-watcher.ts +138 -78
  32. package/src/runs/background/run-status.ts +10 -2
  33. package/src/runs/background/scheduled-runs.ts +6 -1
  34. package/src/runs/background/stale-run-reconciler.ts +6 -0
  35. package/src/runs/background/subagent-runner.ts +406 -54
  36. package/src/runs/background/subagent-wait.ts +130 -4
  37. package/src/runs/background/wait-tool.ts +2 -2
  38. package/src/runs/foreground/chain-execution.ts +181 -111
  39. package/src/runs/foreground/execution.ts +147 -47
  40. package/src/runs/foreground/foreground-control.ts +90 -0
  41. package/src/runs/foreground/subagent-executor.ts +426 -165
  42. package/src/runs/shared/acceptance.ts +94 -41
  43. package/src/runs/shared/agent-contract.ts +38 -0
  44. package/src/runs/shared/capability-ceiling.ts +177 -0
  45. package/src/runs/shared/child-protocol.ts +1 -1
  46. package/src/runs/shared/completion-guard.ts +36 -5
  47. package/src/runs/shared/context-mode.ts +44 -0
  48. package/src/runs/shared/dynamic-fanout.ts +5 -5
  49. package/src/runs/shared/long-running-guard.ts +4 -0
  50. package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  51. package/src/runs/shared/nested-events.ts +35 -3
  52. package/src/runs/shared/parallel-handoff.ts +154 -0
  53. package/src/runs/shared/parallel-utils.ts +11 -0
  54. package/src/runs/shared/pi-args.ts +148 -56
  55. package/src/runs/shared/run-history.ts +90 -5
  56. package/src/runs/shared/session-lease.ts +25 -5
  57. package/src/runs/shared/structured-output.ts +112 -7
  58. package/src/runs/shared/subagent-control.ts +4 -0
  59. package/src/runs/shared/subagent-prompt-runtime.ts +31 -19
  60. package/src/runs/shared/task-intent.ts +10 -5
  61. package/src/runs/shared/tool-availability.ts +21 -3
  62. package/src/runs/shared/tool-budget.ts +11 -5
  63. package/src/runs/shared/turn-budget.ts +2 -1
  64. package/src/runs/shared/worktree.ts +63 -14
  65. package/src/shared/accessible-dir.ts +25 -0
  66. package/src/shared/artifacts.ts +37 -7
  67. package/src/shared/atomic-json.ts +14 -42
  68. package/src/shared/child-transcript.ts +52 -0
  69. package/src/shared/file-system-retry.ts +47 -0
  70. package/src/shared/launch-contract.ts +123 -0
  71. package/src/shared/settings.ts +9 -1
  72. package/src/shared/types.ts +314 -28
  73. package/src/shared/utils.ts +17 -42
  74. package/src/slash/delegation-adapters.ts +153 -6
  75. package/src/slash/delegation-json.ts +108 -0
  76. package/src/slash/delegation-request.ts +182 -36
  77. package/src/slash/prompt-template-bridge.ts +222 -37
  78. package/src/slash/selector.ts +147 -0
  79. package/src/slash/slash-commands.ts +15 -6
  80. package/src/slash/slash-live-state.ts +2 -2
  81. package/src/slash/subagents-admin.ts +42 -42
  82. package/src/tui/fleet-status.ts +362 -0
  83. package/src/tui/fleet-transcript.ts +472 -0
  84. package/src/tui/fleet.ts +318 -59
  85. package/src/tui/render.ts +17 -15
  86. package/src/watchdog/change-signature.ts +105 -12
  87. package/src/watchdog/review.ts +7 -2
  88. package/src/watchdog/runtime.ts +5 -3
  89. package/src/slash/subagents-editor.ts +0 -86
@@ -0,0 +1,399 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { discoverAgents, discoverAgentsAll, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
6
+ import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
7
+ import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
8
+ import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
9
+ import { buildModelCandidates, resolveEffectiveSubagentModel, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-fallback.ts";
10
+ import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/pi-args.ts";
11
+ import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSingleOutputPath } from "../runs/shared/single-output.ts";
12
+ import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
13
+ import { resolveEffectiveThinking } from "../shared/model-info.ts";
14
+ import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts";
15
+ import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
16
+ import { appendTurnBudgetSystemPrompt } from "../runs/shared/turn-budget.ts";
17
+ import type { ResolvedTurnBudget } from "../shared/types.ts";
18
+ import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-tool-allowlist.ts";
19
+ import { resolveStepBehavior } from "../shared/settings.ts";
20
+ import { agentDefinitionDigest, AGENT_DEFINITION_PROJECTION_VERSION, launchBindingDigest } from "../shared/launch-contract.ts";
21
+ import { ASYNC_DIR, RESULTS_DIR, TEMP_ROOT_DIR } from "../shared/types.ts";
22
+ import { processTerminalCandidatePath, processTerminalPath } from "../runs/background/process-terminal.ts";
23
+ import { nestedResultsPath } from "../runs/shared/nested-events.ts";
24
+
25
+ export const SUBAGENT_LAUNCH_CONTRACT_VERSION = 2 as const;
26
+
27
+ export type SubagentLaunchContractReasonCode =
28
+ | "missing_agent"
29
+ | "ambiguous_agent"
30
+ | "missing_skill"
31
+ | "denied_required_tool"
32
+ | "invalid_artifact_dir"
33
+ | "invalid_cwd"
34
+ | "unsupported_mode";
35
+
36
+ export interface SubagentLaunchContractDiagnostic {
37
+ code: SubagentLaunchContractReasonCode | "host_required" | "snapshot_warning";
38
+ severity: "error" | "warning" | "host-required";
39
+ message: string;
40
+ }
41
+
42
+ export interface SubagentLaunchContractInput {
43
+ agent: string;
44
+ cwd: string;
45
+ task?: string;
46
+ agentScope?: AgentScope;
47
+ context?: "fresh" | "fork";
48
+ model?: string;
49
+ thinking?: string | false;
50
+ parentModel?: ParentModel;
51
+ availableModels?: ReadonlyArray<AvailableModelInfo | { provider: string; id: string; fullId?: string; reasoning?: boolean }>;
52
+ preferredProvider?: string;
53
+ skill?: string | string[] | boolean;
54
+ output?: string | boolean;
55
+ outputMode?: OutputMode;
56
+ outputSchema?: JsonSchemaObject;
57
+ turnBudget?: ResolvedTurnBudget;
58
+ artifacts?: boolean;
59
+ artifactDir?: ArtifactDirPreference;
60
+ parentSessionFile?: string | null;
61
+ sessionRoot?: string;
62
+ sessionDir?: string;
63
+ runId?: string;
64
+ /** Root run id supplied by a host when projecting nested async lifecycle paths. */
65
+ nestedRootRunId?: string;
66
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
67
+ inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling;
68
+ }
69
+
70
+ export interface SubagentLaunchContractAgentCandidate {
71
+ name: string;
72
+ localName?: string;
73
+ packageName?: string;
74
+ source: AgentSource;
75
+ filePath: string;
76
+ disabled?: boolean;
77
+ selected: boolean;
78
+ }
79
+
80
+ export interface SubagentLaunchContractAgent {
81
+ name: string;
82
+ localName?: string;
83
+ packageName?: string;
84
+ source: AgentSource;
85
+ filePath: string;
86
+ definitionProjectionVersion: typeof AGENT_DEFINITION_PROJECTION_VERSION;
87
+ definitionDigest: string;
88
+ shadowedCandidates: SubagentLaunchContractAgentCandidate[];
89
+ }
90
+
91
+ export interface SubagentLaunchContractSkills {
92
+ requested: string[];
93
+ resolved: Array<{ name: string; path: string; source: string }>;
94
+ missing: string[];
95
+ }
96
+
97
+ export interface SubagentLaunchContractTools {
98
+ requestedBuiltin: string[];
99
+ declaredBuiltin: string[];
100
+ effectiveAllowlist: string[];
101
+ explicitAllowlist: boolean;
102
+ requiredChildTools: string[];
103
+ internalTools: string[];
104
+ mcp: ResolvedMcpDirectToolSelection[];
105
+ effectiveMcpTools: string[];
106
+ toolExtensionPaths: string[];
107
+ runtimeExtensions: string[];
108
+ configuredExtensions: string[];
109
+ extensionArgs: string[];
110
+ disableAmbientExtensions: boolean;
111
+ fanoutAuthorized: boolean;
112
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
113
+ capabilityAudit?: SubagentCapabilityAudit;
114
+ }
115
+
116
+ export interface SubagentLaunchContractRoots {
117
+ cwd: string;
118
+ sessionRoot?: string;
119
+ sessionDir?: string;
120
+ sessionFile?: string;
121
+ artifactsDir?: string;
122
+ artifactPaths?: ArtifactPaths;
123
+ outputPath?: string;
124
+ lifecycle?: {
125
+ asyncDir: string;
126
+ resultPath: string;
127
+ statusPath: string;
128
+ eventsPath: string;
129
+ processTerminalPath: string;
130
+ processTerminalCandidatePath: string;
131
+ };
132
+ }
133
+
134
+ export interface SubagentLaunchContract {
135
+ version: typeof SUBAGENT_LAUNCH_CONTRACT_VERSION;
136
+ runId: string;
137
+ agent: SubagentLaunchContractAgent;
138
+ context: "fresh" | "fork";
139
+ model?: string;
140
+ modelCandidates: string[];
141
+ thinking?: string;
142
+ systemPromptMode: AgentConfig["systemPromptMode"];
143
+ inheritProjectContext: boolean;
144
+ inheritSkills: boolean;
145
+ skills: SubagentLaunchContractSkills;
146
+ tools: SubagentLaunchContractTools;
147
+ roots: SubagentLaunchContractRoots;
148
+ protocol: {
149
+ lifecycleArtifactVersion: number;
150
+ packageVersion: string;
151
+ };
152
+ diagnostics: SubagentLaunchContractDiagnostic[];
153
+ /** Digest of the resolved child inputs, recomputed by execution paths. */
154
+ launchContractDigest: string;
155
+ digest: string;
156
+ }
157
+
158
+ export type SubagentLaunchContractResult =
159
+ | { ok: true; contract: SubagentLaunchContract }
160
+ | { ok: false; code: SubagentLaunchContractReasonCode; message: string; diagnostics: SubagentLaunchContractDiagnostic[] };
161
+
162
+ function packageVersion(): string {
163
+ const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
164
+ const parsed = JSON.parse(fs.readFileSync(packagePath, "utf-8")) as { version?: unknown };
165
+ if (typeof parsed.version !== "string" || !parsed.version.trim()) {
166
+ throw new Error(`Invalid package version in '${packagePath}'.`);
167
+ }
168
+ return parsed.version;
169
+ }
170
+
171
+ function stableJson(value: unknown): string {
172
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
173
+ if (value && typeof value === "object") {
174
+ return `{${Object.entries(value as Record<string, unknown>)
175
+ .filter(([, entry]) => entry !== undefined)
176
+ .sort(([a], [b]) => a.localeCompare(b))
177
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
178
+ .join(",")}}`;
179
+ }
180
+ return JSON.stringify(value);
181
+ }
182
+
183
+ function digestContract(contract: Omit<SubagentLaunchContract, "digest">): string {
184
+ return createHash("sha256").update(stableJson(contract)).digest("hex");
185
+ }
186
+
187
+ function normalizeAvailableModels(models: SubagentLaunchContractInput["availableModels"]): AvailableModelInfo[] {
188
+ return (models ?? []).map((model) => ({ ...model, fullId: model.fullId ?? `${model.provider}/${model.id}` }));
189
+ }
190
+
191
+ function candidateList(inputAgent: string, selected: AgentConfig | undefined, cwd: string): SubagentLaunchContractAgentCandidate[] {
192
+ const all = discoverAgentsAll(cwd);
193
+ return [...all.builtin, ...all.package, ...all.user, ...all.project]
194
+ .filter((agent) => agent.name === inputAgent || agent.localName === inputAgent)
195
+ .map((agent) => ({
196
+ name: agent.name,
197
+ ...(agent.localName ? { localName: agent.localName } : {}),
198
+ ...(agent.packageName ? { packageName: agent.packageName } : {}),
199
+ source: agent.source,
200
+ filePath: agent.filePath,
201
+ ...(agent.disabled === true ? { disabled: true } : {}),
202
+ selected: Boolean(selected && agent.filePath === selected.filePath && agent.name === selected.name),
203
+ }));
204
+ }
205
+
206
+ export async function resolveSubagentLaunchContract(input: SubagentLaunchContractInput): Promise<SubagentLaunchContractResult> {
207
+ const diagnostics: SubagentLaunchContractDiagnostic[] = [];
208
+ const effectiveCwd = path.resolve(input.cwd);
209
+ try {
210
+ if (!fs.statSync(effectiveCwd).isDirectory()) {
211
+ return { ok: false, code: "invalid_cwd", message: `cwd '${effectiveCwd}' is not a directory.`, diagnostics };
212
+ }
213
+ } catch (error) {
214
+ const detail = error instanceof Error ? ` ${error.message}` : "";
215
+ return { ok: false, code: "invalid_cwd", message: `cwd '${effectiveCwd}' is not a directory.${detail}`, diagnostics };
216
+ }
217
+ if (input.context !== undefined && input.context !== "fresh" && input.context !== "fork") {
218
+ return { ok: false, code: "unsupported_mode", message: `Unsupported context '${String(input.context)}'; expected 'fresh' or 'fork'.`, diagnostics };
219
+ }
220
+ if (input.artifactDir !== undefined && input.artifactDir !== "project" && input.artifactDir !== "session" && input.artifactDir !== "temp") {
221
+ return { ok: false, code: "invalid_artifact_dir", message: `Unsupported artifactDir '${String(input.artifactDir)}'; expected 'project', 'session', or 'temp'.`, diagnostics };
222
+ }
223
+ if (input.context === "fork") {
224
+ diagnostics.push({ code: "host_required", severity: "host-required", message: "Exact fork session branching and fork-thinking downgrade checks require Pi host session and model-registry snapshots." });
225
+ }
226
+ const scope = resolveExecutionAgentScope(input.agentScope);
227
+ const discovered = discoverAgents(effectiveCwd, scope);
228
+ const matches = discovered.agents.filter((agent) => agent.name === input.agent || agent.localName === input.agent);
229
+ if (matches.length === 0) {
230
+ return { ok: false, code: "missing_agent", message: `Unknown agent: ${input.agent}`, diagnostics };
231
+ }
232
+ if (matches.length > 1) {
233
+ return { ok: false, code: "ambiguous_agent", message: `Ambiguous agent: ${input.agent}`, diagnostics };
234
+ }
235
+ const agent = matches[0]!;
236
+ const runId = input.runId ?? "preflight";
237
+ const skillInput = normalizeSkillInput(input.skill);
238
+ const outputOverride = normalizeSingleOutputOverride(input.output, agent.output);
239
+ const behavior = resolveStepBehavior(agent, {
240
+ ...(outputOverride !== undefined ? { output: outputOverride } : {}),
241
+ ...(input.outputMode !== undefined ? { outputMode: input.outputMode } : {}),
242
+ ...(skillInput !== undefined ? { skills: skillInput } : {}),
243
+ ...(input.model !== undefined ? { model: input.model } : {}),
244
+ });
245
+ const requestedSkills = behavior.skills === false ? [] : behavior.skills;
246
+ const resolvedSkills = resolveSkillsWithFallback(
247
+ requestedSkills,
248
+ effectiveCwd,
249
+ effectiveCwd,
250
+ agent.skillPath,
251
+ agent.filePath ? path.dirname(agent.filePath) : effectiveCwd,
252
+ );
253
+ if (resolvedSkills.missing.includes("pi-subagents")) {
254
+ return { ok: false, code: "missing_skill", message: "The pi-subagents orchestration skill is not child-injectable.", diagnostics };
255
+ }
256
+ if (resolvedSkills.missing.length > 0) diagnostics.push({ code: "missing_skill", severity: "error", message: `Missing skills: ${resolvedSkills.missing.join(", ")}` });
257
+
258
+ const availableModels = normalizeAvailableModels(input.availableModels);
259
+ const preferredProvider = input.preferredProvider ?? input.parentModel?.provider;
260
+ const primaryModel = resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: discovered.modelScope });
261
+ const effectiveThinkingConfig = input.thinking !== undefined ? input.thinking : agent.thinking;
262
+ const model = applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
263
+ const modelCandidates = buildModelCandidates(primaryModel, agent.fallbackModels, availableModels, preferredProvider, { scope: discovered.modelScope })
264
+ .map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate);
265
+ let toolPlan: PiLaunchToolPlan;
266
+ try {
267
+ toolPlan = resolvePiLaunchToolPlan({
268
+ tools: agent.tools,
269
+ extensions: agent.extensions,
270
+ subagentOnlyExtensions: agent.subagentOnlyExtensions,
271
+ mcpDirectTools: agent.mcpDirectTools,
272
+ cwd: effectiveCwd,
273
+ requireReadTool: resolvedSkills.resolved.length > 0,
274
+ structuredOutput: Boolean(input.outputSchema),
275
+ capabilityCeiling: input.capabilityCeiling,
276
+ inheritedCapabilityCeiling: input.inheritedCapabilityCeiling,
277
+ });
278
+ } catch (error) {
279
+ const message = error instanceof Error ? error.message : String(error);
280
+ diagnostics.push({ code: "denied_required_tool", severity: "error", message });
281
+ return { ok: false, code: "denied_required_tool", message, diagnostics };
282
+ }
283
+ const artifactsEnabled = input.artifacts !== false;
284
+ const artifactsDir = artifactsEnabled ? getArtifactsDir(input.parentSessionFile ?? null, effectiveCwd, input.artifactDir ?? "project") : undefined;
285
+ const artifactPaths = artifactsDir ? getArtifactPaths(artifactsDir, runId, agent.name, 0) : undefined;
286
+ const outputPath = resolveSingleOutputPath(behavior.output, effectiveCwd, effectiveCwd, artifactsDir ? path.join(artifactsDir, "outputs", runId) : undefined);
287
+ const sessionRoot = input.sessionDir ? path.resolve(input.sessionDir) : input.sessionRoot ? path.join(path.resolve(input.sessionRoot), runId) : undefined;
288
+ const sessionDir = sessionRoot ? path.join(sessionRoot, "run-0") : undefined;
289
+ const lifecycleAsyncDir = input.nestedRootRunId
290
+ ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", input.nestedRootRunId, runId)
291
+ : path.join(ASYNC_DIR, runId);
292
+ const lifecycleResultPath = input.nestedRootRunId
293
+ ? nestedResultsPath(input.nestedRootRunId, runId)
294
+ : path.join(RESULTS_DIR, `${runId}.json`);
295
+ if (!sessionDir) diagnostics.push({ code: "host_required", severity: "host-required", message: "No sessionRoot/sessionDir was supplied; exact child session paths require the Pi host session-root policy." });
296
+ if (input.availableModels === undefined && (input.model || agent.model || input.parentModel)) {
297
+ diagnostics.push({ code: "host_required", severity: "host-required", message: "No availableModels snapshot was supplied; model resolution may differ from the active Pi host registry." });
298
+ }
299
+ if (resolvedSkills.missing.length > 0) {
300
+ return { ok: false, code: "missing_skill", message: `Missing skills: ${resolvedSkills.missing.join(", ")}`, diagnostics };
301
+ }
302
+ let effectiveSystemPrompt = agent.systemPrompt?.trim() ?? "";
303
+ if (resolvedSkills.resolved.length > 0) {
304
+ const skillInjection = buildSkillInjection(resolvedSkills.resolved);
305
+ effectiveSystemPrompt = effectiveSystemPrompt ? `${effectiveSystemPrompt}\n\n${skillInjection}` : skillInjection;
306
+ }
307
+ const memoryInjection = buildAgentMemoryInjection(agent, effectiveCwd);
308
+ if (memoryInjection) effectiveSystemPrompt = effectiveSystemPrompt ? `${effectiveSystemPrompt}\n\n${memoryInjection}` : memoryInjection;
309
+ effectiveSystemPrompt = injectOutputPathSystemPrompt(effectiveSystemPrompt, outputPath, agent);
310
+ const turnBudget = input.turnBudget ?? agent.defaultTurnBudget;
311
+ effectiveSystemPrompt = appendTurnBudgetSystemPrompt(effectiveSystemPrompt, turnBudget);
312
+ const candidates = candidateList(input.agent, agent, effectiveCwd);
313
+ const shadowedCandidates = candidates.filter((candidate) => !candidate.selected);
314
+ const definitionDigest = agentDefinitionDigest(agent);
315
+ const contractBase: Omit<SubagentLaunchContract, "digest"> = {
316
+ version: SUBAGENT_LAUNCH_CONTRACT_VERSION,
317
+ runId,
318
+ agent: {
319
+ name: agent.name,
320
+ ...(agent.localName ? { localName: agent.localName } : {}),
321
+ ...(agent.packageName ? { packageName: agent.packageName } : {}),
322
+ source: agent.source,
323
+ filePath: agent.filePath,
324
+ definitionProjectionVersion: AGENT_DEFINITION_PROJECTION_VERSION,
325
+ definitionDigest,
326
+ shadowedCandidates,
327
+ },
328
+ context: input.context ?? agent.defaultContext ?? "fresh",
329
+ ...(model ? { model } : {}),
330
+ modelCandidates,
331
+ ...(resolveEffectiveThinking(model, effectiveThinkingConfig) ? { thinking: resolveEffectiveThinking(model, effectiveThinkingConfig) } : {}),
332
+ systemPromptMode: agent.systemPromptMode,
333
+ inheritProjectContext: agent.inheritProjectContext,
334
+ inheritSkills: agent.inheritSkills,
335
+ skills: {
336
+ requested: requestedSkills,
337
+ resolved: resolvedSkills.resolved.map((skill) => ({ name: skill.name, path: skill.path, source: skill.source })),
338
+ missing: resolvedSkills.missing,
339
+ },
340
+ tools: {
341
+ requestedBuiltin: toolPlan.requestedBuiltinTools,
342
+ declaredBuiltin: toolPlan.declaredBuiltinTools,
343
+ effectiveAllowlist: toolPlan.effectiveToolAllowlist,
344
+ explicitAllowlist: toolPlan.explicitToolAllowlist,
345
+ requiredChildTools: toolPlan.requiredChildTools,
346
+ internalTools: toolPlan.internalTools,
347
+ mcp: toolPlan.effectiveMcpSelections,
348
+ effectiveMcpTools: toolPlan.effectiveMcpTools,
349
+ toolExtensionPaths: toolPlan.toolExtensionPaths,
350
+ runtimeExtensions: toolPlan.runtimeExtensions,
351
+ configuredExtensions: toolPlan.configuredExtensions,
352
+ extensionArgs: toolPlan.extensionArgs,
353
+ disableAmbientExtensions: toolPlan.disableAmbientExtensions,
354
+ fanoutAuthorized: toolPlan.fanoutAuthorized,
355
+ ...(toolPlan.capabilityCeiling ? { capabilityCeiling: toolPlan.capabilityCeiling } : {}),
356
+ ...(toolPlan.capabilityAudit ? { capabilityAudit: toolPlan.capabilityAudit } : {}),
357
+ },
358
+ roots: {
359
+ cwd: effectiveCwd,
360
+ ...(sessionRoot ? { sessionRoot } : {}),
361
+ ...(sessionDir ? { sessionDir, sessionFile: path.join(sessionDir, "session.jsonl") } : {}),
362
+ ...(artifactsDir ? { artifactsDir } : {}),
363
+ ...(artifactPaths ? { artifactPaths } : {}),
364
+ ...(outputPath ? { outputPath } : {}),
365
+ lifecycle: {
366
+ asyncDir: lifecycleAsyncDir,
367
+ resultPath: lifecycleResultPath,
368
+ statusPath: path.join(lifecycleAsyncDir, "status.json"),
369
+ eventsPath: path.join(lifecycleAsyncDir, "events.jsonl"),
370
+ processTerminalPath: processTerminalPath(lifecycleAsyncDir),
371
+ processTerminalCandidatePath: processTerminalCandidatePath(lifecycleAsyncDir),
372
+ },
373
+ },
374
+ protocol: {
375
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
376
+ packageVersion: packageVersion(),
377
+ },
378
+ diagnostics,
379
+ launchContractDigest: launchBindingDigest({
380
+ task: input.task ?? "",
381
+ definitionDigest,
382
+ ...(model ? { model } : {}),
383
+ modelCandidates,
384
+ ...(resolveEffectiveThinking(model, effectiveThinkingConfig) ? { thinking: resolveEffectiveThinking(model, effectiveThinkingConfig) } : {}),
385
+ systemPrompt: effectiveSystemPrompt,
386
+ systemPromptMode: agent.systemPromptMode,
387
+ inheritProjectContext: agent.inheritProjectContext,
388
+ inheritSkills: agent.inheritSkills,
389
+ skills: requestedSkills,
390
+ tools: toolPlan.effectiveToolAllowlist,
391
+ extensions: toolPlan.extensionArgs,
392
+ mcpDirectTools: toolPlan.effectiveMcpTools,
393
+ ...(outputPath ? { outputPath } : {}),
394
+ outputMode: input.outputMode ?? "inline",
395
+ ...(input.outputSchema ? { structuredOutputSchema: input.outputSchema } : {}),
396
+ }),
397
+ };
398
+ return { ok: true, contract: { ...contractBase, digest: digestContract(contractBase) } };
399
+ }
@@ -1,8 +1,10 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { ExtensionConfig } from "../shared/types.ts";
3
+ import type { ArtifactDirPreference, ExtensionConfig } from "../shared/types.ts";
4
4
  import { getAgentDir } from "../shared/utils.ts";
5
5
 
6
+ const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
7
+
6
8
  export function getConfigPath(): string {
7
9
  return path.join(getAgentDir(), "extensions", "subagent", "config.json");
8
10
  }
@@ -13,6 +15,10 @@ function readConfigForUpdate(configPath = getConfigPath()): ExtensionConfig {
13
15
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
14
16
  throw new Error(`Subagent config at '${configPath}' must be a JSON object`);
15
17
  }
18
+ const config = parsed as Record<string, unknown>;
19
+ if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
20
+ throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
21
+ }
16
22
  return parsed as ExtensionConfig;
17
23
  }
18
24
 
@@ -20,10 +20,13 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
20
20
  import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
21
21
  import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
22
22
  import { discoverAgents } from "../agents/agents.ts";
23
+ import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
23
24
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
24
25
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
25
26
  import { cleanupOldChainDirs } from "../shared/settings.ts";
26
27
  import { clearLegacyResultAnimationTimer, renderSubagentResult } from "../tui/render.ts";
28
+ import { openSubagentFleet } from "../tui/fleet.ts";
29
+ import { SubagentFleetStatus } from "../tui/fleet-status.ts";
27
30
  import { SubagentParams } from "./schemas.ts";
28
31
  import { validateChainInput } from "./chain-validation.ts";
29
32
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
@@ -44,6 +47,7 @@ import { drainOutstandingWork } from "../runs/background/auto-drain.ts";
44
47
  import registerSubagentNotify, { parseSubagentNotifyContent, type SubagentNotifyDetails } from "../runs/background/notify.ts";
45
48
  import { formatSteeringNotice, handleSubagentSteeringNotice, SUBAGENT_STEERING_MESSAGE_TYPE, type SubagentSteeringMessageDetails } from "./steering-notices.ts";
46
49
  import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/pi-args.ts";
50
+ import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
47
51
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
48
52
  import { loadConfig } from "./config.ts";
49
53
  import { buildSubagentToolDescription } from "./tool-description.ts";
@@ -92,28 +96,6 @@ function expandTilde(p: string): string {
92
96
  return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
93
97
  }
94
98
 
95
- /**
96
- * Create a directory and verify it is actually accessible.
97
- * On Windows with Azure AD/Entra ID, directories created shortly after
98
- * wake-from-sleep can end up with broken NTFS ACLs (null DACL) when the
99
- * cloud SID cannot be resolved without network connectivity. This leaves
100
- * the directory completely inaccessible to the creating user.
101
- */
102
- function ensureAccessibleDir(dirPath: string): void {
103
- fs.mkdirSync(dirPath, { recursive: true });
104
- try {
105
- fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
106
- } catch {
107
- try {
108
- fs.rmSync(dirPath, { recursive: true, force: true });
109
- } catch {
110
- // Best effort: retry mkdir/access even if cleanup fails.
111
- }
112
- fs.mkdirSync(dirPath, { recursive: true });
113
- fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
114
- }
115
- }
116
-
117
99
  function isSlashResultRunning(result: { details?: Details }): boolean {
118
100
  return result.details?.progress?.some((entry) => entry.status === "running")
119
101
  || result.details?.results.some((entry) => entry.progress?.status === "running")
@@ -210,12 +192,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
210
192
  const config = loadConfig();
211
193
  const waitToolConfig = resolveWaitToolConfig(config.waitTool);
212
194
  const asyncByDefault = config.asyncByDefault === true;
195
+ const fleetViewEnabled = config.fleetView !== false;
196
+ const asyncWidgetEnabled = config.asyncWidget === true || (!fleetViewEnabled && config.asyncWidget !== false);
213
197
  const tempArtifactsDir = getArtifactsDir(null);
214
198
  cleanupAllArtifactDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
215
199
 
216
200
  const state: SubagentState = {
217
201
  baseCwd: "",
218
202
  currentSessionId: null,
203
+ artifactDirPreference: config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
204
+ parentSessionFile: null,
219
205
  subagentInProgress: false,
220
206
  subagentSpawns: {
221
207
  sessionId: null,
@@ -244,22 +230,30 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
244
230
 
245
231
  const supervisorChannel = createNativeSupervisorChannel(pi, state);
246
232
  const mainWatchdog = registerMainWatchdog(pi);
247
- let disposeSubagentNotify = () => {};
233
+ const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch });
234
+ const fleetStatus = fleetViewEnabled
235
+ ? new SubagentFleetStatus(state, async (itemKey) => {
236
+ const ctx = state.lastUiContext;
237
+ if (!ctx?.hasUI) return;
238
+ await openSubagentFleet(ctx, state, { initialKey: itemKey });
239
+ })
240
+ : undefined;
248
241
  const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
249
242
  pi,
250
243
  state,
251
244
  RESULTS_DIR,
252
245
  10 * 60 * 1000,
246
+ { notifier: completionNotifier },
253
247
  );
254
- startResultWatcher();
255
- primeExistingResults();
256
248
 
257
249
  const runtimeCleanup = () => {
258
- disposeSubagentNotify();
259
- mainWatchdog.dispose();
260
250
  stopResultWatcher();
251
+ state.currentSessionId = null;
252
+ completionNotifier.dispose();
253
+ mainWatchdog.dispose();
261
254
  scheduledRunManager.stop();
262
255
  supervisorChannel.dispose();
256
+ fleetStatus?.dispose();
263
257
  clearPendingForegroundControlNotices(state);
264
258
  if (state.poller) {
265
259
  clearInterval(state.poller);
@@ -269,7 +263,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
269
263
  globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
270
264
 
271
265
  const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs } = createAsyncJobTracker(pi, state, ASYNC_DIR, {
272
- widgetEnabled: config.asyncWidget !== false,
266
+ widgetEnabled: asyncWidgetEnabled,
273
267
  });
274
268
  let executorExecute: ((id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
275
269
  const scheduledRunManager = createScheduledRunManager({
@@ -284,6 +278,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
284
278
  }
285
279
  return executorExecute(randomUUID(), params, signal, undefined, ctx);
286
280
  },
281
+ resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
287
282
  });
288
283
  const executor = createSubagentExecutor({
289
284
  pi,
@@ -377,6 +372,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
377
372
  getContext: () => state.lastUiContext,
378
373
  execute: (requestId, params, signal, ctx, onUpdate) =>
379
374
  executeSubagentCollapsed(requestId, params, signal, onUpdate, ctx),
375
+ executeVersioned: (requestId, params, signal, ctx, onUpdate) => {
376
+ if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
377
+ return executor.executeDelegated(requestId, params, signal, onUpdate, ctx);
378
+ },
380
379
  });
381
380
 
382
381
  const rpcBridge = registerSubagentRpcBridge({
@@ -472,8 +471,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
472
471
  }
473
472
  }
474
473
  }
475
- disposeSubagentNotify = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch });
476
-
477
474
  const existingVisibleControlNotices = globalStore[controlNoticeSeenStoreKey];
478
475
  const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
479
476
  globalStore[controlNoticeSeenStoreKey] = visibleControlNotices;
@@ -488,9 +485,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
488
485
  const steeringNoticeHandler = (payload: unknown) => {
489
486
  handleSubagentSteeringNotice({ pi, state, details: payload as SubagentSteeringMessageDetails });
490
487
  };
488
+ const asyncStartedHandler = (payload: unknown) => {
489
+ handleStarted(payload);
490
+ fleetStatus?.refresh();
491
+ };
492
+ const asyncCompleteHandler = (payload: unknown) => {
493
+ handleComplete(payload);
494
+ fleetStatus?.refresh();
495
+ };
491
496
  const eventUnsubscribes = [
492
- pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, handleStarted),
493
- pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete),
497
+ pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, asyncStartedHandler),
498
+ pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
494
499
  pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
495
500
  pi.events.on(SUBAGENT_STEERING_NOTICE_EVENT, steeringNoticeHandler),
496
501
  rpcBridge.dispose,
@@ -501,6 +506,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
501
506
  if (event.toolName !== "subagent") return;
502
507
  if (!ctx.hasUI) return;
503
508
  state.lastUiContext = ctx;
509
+ fleetStatus?.setContext(ctx);
510
+ fleetStatus?.refresh();
504
511
  if (state.asyncJobs.size > 0) {
505
512
  refreshWidget(ctx);
506
513
  ensurePoller();
@@ -518,9 +525,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
518
525
  }
519
526
  };
520
527
 
521
- const resetSessionState = (ctx: ExtensionContext) => {
528
+ const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
522
529
  state.baseCwd = ctx.cwd;
523
530
  state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
531
+ state.parentSessionFile = ctx.sessionManager.getSessionFile();
524
532
  state.subagentSpawns = {
525
533
  sessionId: state.currentSessionId,
526
534
  count: 0,
@@ -546,17 +554,23 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
546
554
  restoreActiveJobs(ctx);
547
555
  scheduledRunManager.bindSession(ctx);
548
556
  restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
549
- primeExistingResults();
557
+ startResultWatcher();
558
+ primeExistingResults({ triggerTurn: !recovering });
559
+ fleetStatus?.setContext(ctx);
550
560
  };
551
561
 
552
- pi.on("session_start", (_event, ctx) => {
553
- resetSessionState(ctx);
562
+ pi.on("session_start", (event, ctx) => {
563
+ const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
564
+ resetSessionState(ctx, recovering);
554
565
  rpcBridge.emitReady(ctx);
555
566
  supervisorChannel.start();
556
567
  });
557
568
 
558
569
  pi.on("session_shutdown", () => {
559
- disposeSubagentNotify();
570
+ stopResultWatcher();
571
+ state.currentSessionId = null;
572
+ state.parentSessionFile = null;
573
+ completionNotifier.dispose();
560
574
  delete process.env[SUBAGENT_PARENT_SESSION_ENV];
561
575
  for (const unsubscribe of eventUnsubscribes) {
562
576
  try {
@@ -568,7 +582,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
568
582
  if (globalStore[eventUnsubscribeStoreKey] === eventUnsubscribes) {
569
583
  delete globalStore[eventUnsubscribeStoreKey];
570
584
  }
571
- stopResultWatcher();
572
585
  scheduledRunManager.stop();
573
586
  if (state.poller) clearInterval(state.poller);
574
587
  state.poller = null;
@@ -584,6 +597,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
584
597
  promptTemplateBridge.cancelAll();
585
598
  promptTemplateBridge.dispose();
586
599
  supervisorChannel.dispose();
600
+ fleetStatus?.dispose();
587
601
  if (globalStore[runtimeCleanupStoreKey] === runtimeCleanup) {
588
602
  delete globalStore[runtimeCleanupStoreKey];
589
603
  }