pi-subagents 0.36.0 → 0.37.1

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 (54) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +111 -16
  3. package/agents/delegate.md +2 -0
  4. package/agents/planner.md +2 -1
  5. package/agents/worker.md +2 -0
  6. package/package.json +6 -4
  7. package/skills/pi-subagents/SKILL.md +18 -973
  8. package/skills/pi-subagents/references/constraints-and-recipes.md +256 -0
  9. package/skills/pi-subagents/references/execution-controls.md +411 -0
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +140 -0
  11. package/skills/pi-subagents/references/prompting-and-roles.md +268 -0
  12. package/src/agents/agent-management.ts +7 -1
  13. package/src/agents/agents.ts +99 -12
  14. package/src/api/capability-ceiling.ts +17 -0
  15. package/src/api/delegation.ts +6 -1
  16. package/src/api/preflight.ts +399 -0
  17. package/src/extension/index.ts +16 -7
  18. package/src/extension/rpc.ts +29 -2
  19. package/src/extension/schemas.ts +10 -4
  20. package/src/extension/tool-description.ts +4 -2
  21. package/src/intercom/intercom-bridge.ts +5 -2
  22. package/src/runs/background/async-execution.ts +125 -14
  23. package/src/runs/background/async-job-tracker.ts +19 -12
  24. package/src/runs/background/async-resume.ts +27 -9
  25. package/src/runs/background/async-status.ts +18 -0
  26. package/src/runs/background/notify.ts +3 -0
  27. package/src/runs/background/process-terminal.ts +280 -0
  28. package/src/runs/background/result-watcher.ts +9 -3
  29. package/src/runs/background/run-status.ts +7 -1
  30. package/src/runs/background/scheduled-runs.ts +6 -1
  31. package/src/runs/background/stale-run-reconciler.ts +6 -0
  32. package/src/runs/background/subagent-runner.ts +182 -12
  33. package/src/runs/foreground/chain-execution.ts +5 -0
  34. package/src/runs/foreground/execution.ts +59 -13
  35. package/src/runs/foreground/subagent-executor.ts +142 -47
  36. package/src/runs/shared/acceptance.ts +41 -30
  37. package/src/runs/shared/capability-ceiling.ts +177 -0
  38. package/src/runs/shared/dynamic-fanout.ts +1 -1
  39. package/src/runs/shared/mcp-direct-tool-allowlist.ts +56 -17
  40. package/src/runs/shared/model-fallback.ts +8 -0
  41. package/src/runs/shared/nested-events.ts +8 -1
  42. package/src/runs/shared/parallel-utils.ts +5 -0
  43. package/src/runs/shared/pi-args.ts +141 -58
  44. package/src/runs/shared/session-lease.ts +25 -5
  45. package/src/runs/shared/subagent-prompt-runtime.ts +14 -1
  46. package/src/runs/shared/task-intent.ts +1 -1
  47. package/src/runs/shared/tool-availability.ts +18 -2
  48. package/src/shared/launch-contract.ts +123 -0
  49. package/src/shared/types.ts +118 -8
  50. package/src/shared/utils.ts +17 -42
  51. package/src/slash/delegation-adapters.ts +6 -1
  52. package/src/slash/slash-commands.ts +1 -1
  53. package/src/tui/fleet-status.ts +62 -16
  54. package/src/tui/fleet.ts +1 -1
@@ -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
+ }
@@ -26,7 +26,7 @@ import { resolveCurrentSessionId } from "../shared/session-identity.ts";
26
26
  import { cleanupOldChainDirs } from "../shared/settings.ts";
27
27
  import { clearLegacyResultAnimationTimer, renderSubagentResult } from "../tui/render.ts";
28
28
  import { openSubagentFleet } from "../tui/fleet.ts";
29
- import { SubagentFleetStatus } from "../tui/fleet-status.ts";
29
+ import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-status.ts";
30
30
  import { SubagentParams } from "./schemas.ts";
31
31
  import { validateChainInput } from "./chain-validation.ts";
32
32
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
@@ -47,6 +47,7 @@ import { drainOutstandingWork } from "../runs/background/auto-drain.ts";
47
47
  import registerSubagentNotify, { parseSubagentNotifyContent, type SubagentNotifyDetails } from "../runs/background/notify.ts";
48
48
  import { formatSteeringNotice, handleSubagentSteeringNotice, SUBAGENT_STEERING_MESSAGE_TYPE, type SubagentSteeringMessageDetails } from "./steering-notices.ts";
49
49
  import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/pi-args.ts";
50
+ import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
50
51
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
51
52
  import { loadConfig } from "./config.ts";
52
53
  import { buildSubagentToolDescription } from "./tool-description.ts";
@@ -142,10 +143,13 @@ function createSlashResultComponent(
142
143
  }
143
144
 
144
145
  class SubagentControlNoticeComponent implements Component {
145
- constructor(
146
- private readonly details: SubagentControlMessageDetails,
147
- private readonly theme: ExtensionContext["ui"]["theme"],
148
- ) {}
146
+ private readonly details: SubagentControlMessageDetails;
147
+ private readonly theme: ExtensionContext["ui"]["theme"];
148
+
149
+ constructor(details: SubagentControlMessageDetails, theme: ExtensionContext["ui"]["theme"]) {
150
+ this.details = details;
151
+ this.theme = theme;
152
+ }
149
153
 
150
154
  invalidate(): void {}
151
155
 
@@ -192,6 +196,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
192
196
  const waitToolConfig = resolveWaitToolConfig(config.waitTool);
193
197
  const asyncByDefault = config.asyncByDefault === true;
194
198
  const fleetViewEnabled = config.fleetView !== false;
199
+ const fleetViewPlacement = resolveFleetViewPlacement(config.fleetViewPlacement);
195
200
  const asyncWidgetEnabled = config.asyncWidget === true || (!fleetViewEnabled && config.asyncWidget !== false);
196
201
  const tempArtifactsDir = getArtifactsDir(null);
197
202
  cleanupAllArtifactDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
@@ -235,14 +240,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
235
240
  const ctx = state.lastUiContext;
236
241
  if (!ctx?.hasUI) return;
237
242
  await openSubagentFleet(ctx, state, { initialKey: itemKey });
238
- })
243
+ }, { placement: fleetViewPlacement })
239
244
  : undefined;
240
245
  const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
241
246
  pi,
242
247
  state,
243
248
  RESULTS_DIR,
244
249
  10 * 60 * 1000,
245
- { notifier: completionNotifier },
250
+ {
251
+ notifier: completionNotifier,
252
+ deliverIntercomResults: config.intercomBridge?.resultDelivery !== false,
253
+ },
246
254
  );
247
255
 
248
256
  const runtimeCleanup = () => {
@@ -277,6 +285,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
277
285
  }
278
286
  return executorExecute(randomUUID(), params, signal, undefined, ctx);
279
287
  },
288
+ resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
280
289
  });
281
290
  const executor = createSubagentExecutor({
282
291
  pi,
@@ -6,11 +6,14 @@ import { resolveAsyncRunLocation } from "../runs/background/async-resume.ts";
6
6
  import { deliverStopRequest } from "../runs/background/control-channel.ts";
7
7
  import { reconcileAsyncRun } from "../runs/background/stale-run-reconciler.ts";
8
8
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
9
+ import { resolveCurrentSessionId } from "../shared/session-identity.ts";
9
10
  import {
10
11
  type Details,
11
12
  ASYNC_DIR,
12
13
  RESULTS_DIR,
13
14
  SUBAGENT_ASYNC_COMPLETE_EVENT,
15
+ SUBAGENT_PROCESS_TERMINAL_EVENT,
16
+ SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
14
17
  } from "../shared/types.ts";
15
18
  import { readStatus } from "../shared/utils.ts";
16
19
  import { SubagentParams } from "./schemas.ts";
@@ -21,7 +24,7 @@ export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
21
24
  export const SUBAGENT_RPC_READY_EVENT = "subagents:rpc:v1:ready";
22
25
  export const SUBAGENT_RPC_REPLY_EVENT_PREFIX = "subagents:rpc:v1:reply:";
23
26
 
24
- export const SUBAGENT_RPC_METHODS = ["ping", "status", "spawn", "steer", "interrupt", "stop"] as const;
27
+ export const SUBAGENT_RPC_METHODS = ["ping", "status", "spawn", "steer", "interrupt", "stop", "resume"] as const;
25
28
  export type SubagentRpcMethod = typeof SUBAGENT_RPC_METHODS[number];
26
29
 
27
30
  export interface SubagentRpcRequestEnvelope {
@@ -181,12 +184,15 @@ function pingData(ctx: ExtensionContext | null) {
181
184
  nonRecoveringSteer: true,
182
185
  interrupt: true,
183
186
  stop: true,
187
+ resume: true,
188
+ processTerminalProof: { version: 1, lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION },
184
189
  },
185
190
  events: {
186
191
  ready: SUBAGENT_RPC_READY_EVENT,
187
192
  request: SUBAGENT_RPC_REQUEST_EVENT,
188
193
  replyPrefix: SUBAGENT_RPC_REPLY_EVENT_PREFIX,
189
194
  asyncComplete: SUBAGENT_ASYNC_COMPLETE_EVENT,
195
+ processTerminal: SUBAGENT_PROCESS_TERMINAL_EVENT,
190
196
  },
191
197
  session: sessionData(ctx),
192
198
  };
@@ -234,6 +240,24 @@ function steerParams(params: unknown): SubagentParamsLike {
234
240
  };
235
241
  }
236
242
 
243
+ function resumeParams(params: unknown): SubagentParamsLike {
244
+ const input = assertRecordParams(params, "resume");
245
+ if (typeof input.message !== "string" || !input.message.trim())
246
+ throw new SubagentRpcError("invalid_params", "RPC resume requires a non-empty message.");
247
+ const target = normalizeTargetParams(input, "resume");
248
+ if (!target.id && !target.runId && !target.dir) throw new SubagentRpcError("invalid_params", "RPC resume requires id, runId, or dir.");
249
+ if (input.output !== undefined && (typeof input.output !== "string" || !input.output.trim()))
250
+ throw new SubagentRpcError("invalid_params", "RPC resume output must be a non-empty path.");
251
+ if (input.outputMode !== undefined && input.outputMode !== "file-only")
252
+ throw new SubagentRpcError("invalid_params", "RPC resume supports only file-only output mode.");
253
+ return {
254
+ action: "resume",
255
+ ...target,
256
+ message: input.message.trim(),
257
+ ...(typeof input.output === "string" ? { output: input.output.trim(), outputMode: "file-only" } : {}),
258
+ };
259
+ }
260
+
237
261
  function stopAsyncRun(
238
262
  params: unknown,
239
263
  options: RegisterSubagentRpcBridgeOptions,
@@ -253,7 +277,7 @@ function stopAsyncRun(
253
277
  throw new SubagentRpcError("not_found", "Async run not found or already completed; stop requires a live async run directory.");
254
278
  }
255
279
 
256
- const currentSessionId = ctx.sessionManager.getSessionId();
280
+ const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
257
281
  const initialStatus = readStatus(location.asyncDir);
258
282
  const initialRunId = initialStatus?.runId ?? location.resolvedId ?? path.basename(location.asyncDir);
259
283
  if (!initialStatus) throw new SubagentRpcError("not_found", `Status file not found for async run '${initialRunId}'.`);
@@ -320,6 +344,9 @@ async function handleRequest(
320
344
  if (request.method === "stop") {
321
345
  return stopAsyncRun(request.params, options, ctx);
322
346
  }
347
+ if (request.method === "resume") {
348
+ return executeChecked(options, ctx, request.requestId, request.method, resumeParams(request.params));
349
+ }
323
350
  throw new SubagentRpcError("unsupported_method", `Unsupported subagent RPC method: ${String(request.method)}`);
324
351
  }
325
352
 
@@ -67,11 +67,17 @@ const JsonSchemaObject = Type.Unsafe({
67
67
 
68
68
  const AcceptanceOverride = Type.Unsafe({
69
69
  anyOf: [
70
- { type: "string", enum: ["auto", "attested", "checked", "verified", "reviewed"] },
70
+ { type: "string", enum: ["auto", "attested", "checked", "verified"] },
71
+ {
72
+ type: "string",
73
+ enum: ["reviewed"],
74
+ deprecated: true,
75
+ description: "Invalid as an explicit policy. Recognized only so preflight can explain that reviewed is an achieved status.",
76
+ },
71
77
  { type: "boolean", enum: [false] },
72
78
  { type: "object", additionalProperties: true },
73
79
  ],
74
- description: "Optional acceptance policy. In the current/default contract, omitted means auto-inferred; verified requires configured runtime commands. With agentContract.version=1, omitted means not requested and acceptance failures are reported separately from execution.",
80
+ description: "Optional acceptance policy. For reviewer/read-only calls, omit acceptance. Evidence levels end at verified; require independent review with acceptance.review.required. The value reviewed is an achieved status and is accepted by the schema only for actionable preflight recovery. In the current/default contract, omitted means auto-inferred. With agentContract.version=1, omitted means not requested and acceptance failures are reported separately from execution.",
75
81
  });
76
82
 
77
83
  const AgentContractOverride = Type.Object({
@@ -284,8 +290,8 @@ const SubagentParamsSchema = Type.Object({
284
290
  })),
285
291
  chainDir: Type.Optional(Type.String({ description: "Persistent chain artifact directory; defaults to user-scoped temp storage." })),
286
292
  async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
287
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Optional run-level timeout in ms for foreground and async/background runs. Alias of maxRuntimeMs." })),
288
- maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias of timeoutMs for optional run-level timeout in foreground and async/background runs." })),
293
+ timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout for foreground and async/background runs; foreground defaults to 30m absent call/agent. Alias maxRuntimeMs." })),
294
+ maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs for foreground and async/background runs; foreground defaults to 30m absent call/agent." })),
289
295
  turnBudget: Type.Optional(TurnBudgetOverride),
290
296
  toolBudget: Type.Optional(ToolBudgetOverride),
291
297
  agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
@@ -23,7 +23,8 @@ EXECUTION (use exactly ONE mode):
23
23
  • PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
24
24
  • Optional context: { context: "fresh" | "fork" } (explicit value overrides every child; when omitted, each requested agent uses its own defaultContext, otherwise "fresh"; inspect agent defaults via { action: "list" })
25
25
  • Fork thinking: model strings accept a thinking suffix (provider/model:off|minimal|low|medium|high|xhigh|max). Forking over a parent transcript that carries signed Anthropic thinking blocks forces thinking off only when a child's effective primary or fallback model resolves to the Anthropic provider or anthropic-messages API; unresolved models are treated conservatively. The result notes affected children, including on failures. Use fresh context when an Anthropic child needs thinking.
26
- • Optional timeout: { timeoutMs } or { maxRuntimeMs } sets a run-level max runtime for foreground and async/background runs
26
+ • Optional timeout: { timeoutMs } or { maxRuntimeMs } sets a run-level max runtime for foreground and async/background runs; foreground defaults to 30 minutes only when neither value nor an agent timeout is provided
27
+ • Acceptance: omit acceptance for reviewer/read-only calls. Evidence levels end at verified. Use acceptance.review.required to require independent review of a writer result. Never request acceptance:"reviewed"; reviewed is achieved only after an independent reviewer result.
27
28
  • If { action: "list" } shows proactive skill subagent suggestions, consider a small fresh-context fanout for broad tasks where one of those skills would materially help
28
29
 
29
30
  CHAIN TEMPLATE VARIABLES (use in task strings):
@@ -79,7 +80,8 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `To delegate work, call with {
79
80
  EXECUTE:
80
81
  • Before execution, call { action: "list" }; run only executable/non-disabled configured agents/chains.
81
82
  • SINGLE {agent, task?}; PARALLEL {tasks:[{agent,task,count?,output?,reads?,progress?}], concurrency?, worktree?}; CHAIN {chain:[{agent,task?},{parallel:[...]}]}.
82
- • context can be "fresh" or "fork"; omitted uses each agent defaultContext, otherwise fresh. timeoutMs/maxRuntimeMs apply to foreground and async/background runs.
83
+ • context can be "fresh" or "fork"; omitted uses each agent defaultContext, otherwise fresh. timeoutMs/maxRuntimeMs apply to foreground and async/background runs; foreground defaults to 30 minutes only when neither value nor an agent timeout is provided.
84
+ • Omit acceptance for reviewer/read-only calls. Evidence levels end at verified; use acceptance.review.required for independent writer review. reviewed is an achieved status, never an explicit input.
83
85
  • Chain templates may use {task}, {previous}, {chain_dir}, and named outputs. Parallel worktree isolation requires a clean git repo.
84
86
  • Chain example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {parallel: [{agent:"agent-b", task:"Check {previous}", count: 3}]}] }
85
87
  • If list shows proactive skill subagent suggestions, use a small fresh-context fanout only when the task is broad enough.
@@ -32,6 +32,7 @@ Do not use contact_supervisor or intercom for routine completion handoffs. If no
32
32
  export interface IntercomBridgeState {
33
33
  active: boolean;
34
34
  mode: IntercomBridgeMode;
35
+ resultDelivery: boolean;
35
36
  orchestratorTarget?: string;
36
37
  extensionDir: string;
37
38
  instruction: string;
@@ -78,11 +79,12 @@ export function resolveIntercomBridgeMode(value: unknown): IntercomBridgeMode {
78
79
 
79
80
  function resolveIntercomBridgeConfig(value: ExtensionConfig["intercomBridge"]): Required<IntercomBridgeConfig> {
80
81
  if (!value || typeof value !== "object" || Array.isArray(value)) {
81
- return { mode: "always", instructionFile: "" };
82
+ return { mode: "always", instructionFile: "", resultDelivery: true };
82
83
  }
83
84
  return {
84
85
  mode: resolveIntercomBridgeMode(value.mode),
85
86
  instructionFile: typeof value.instructionFile === "string" ? value.instructionFile : "",
87
+ resultDelivery: value.resultDelivery !== false,
86
88
  };
87
89
  }
88
90
 
@@ -146,11 +148,12 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
146
148
  );
147
149
  const reason = inactiveReason(mode, input.context, orchestratorTarget);
148
150
  if (reason || !orchestratorTarget) {
149
- return { active: false, mode, extensionDir: NATIVE_INTERCOM_EXTENSION_DIR, instruction: defaultInstruction };
151
+ return { active: false, mode, resultDelivery: config.resultDelivery, extensionDir: NATIVE_INTERCOM_EXTENSION_DIR, instruction: defaultInstruction };
150
152
  }
151
153
  return {
152
154
  active: true,
153
155
  mode,
156
+ resultDelivery: config.resultDelivery,
154
157
  orchestratorTarget,
155
158
  extensionDir: NATIVE_INTERCOM_EXTENSION_DIR,
156
159
  instruction: buildIntercomBridgeInstruction(orchestratorTarget, resolveInstructionTemplate(config.instructionFile, settingsDir)),