pi-subagents 0.35.0 → 0.36.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 (76) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +132 -24
  3. package/agents/advisor.md +73 -0
  4. package/package.json +8 -12
  5. package/skills/pi-subagents/SKILL.md +22 -9
  6. package/src/agents/agents.ts +22 -5
  7. package/src/api/delegation.ts +125 -0
  8. package/src/extension/config.ts +7 -1
  9. package/src/extension/index.ts +50 -38
  10. package/src/extension/rpc.ts +27 -2
  11. package/src/extension/schemas.ts +22 -2
  12. package/src/extension/tool-description.ts +2 -2
  13. package/src/intercom/intercom-bridge.ts +1 -1
  14. package/src/intercom/native-supervisor-channel.ts +45 -6
  15. package/src/intercom/result-intercom.ts +7 -0
  16. package/src/runs/background/async-execution.ts +34 -4
  17. package/src/runs/background/async-job-tracker.ts +4 -0
  18. package/src/runs/background/async-resume.ts +27 -5
  19. package/src/runs/background/async-status.ts +76 -3
  20. package/src/runs/background/chain-append.ts +2 -0
  21. package/src/runs/background/completion-batcher.ts +6 -4
  22. package/src/runs/background/completion-dedupe.ts +2 -11
  23. package/src/runs/background/fleet-view.ts +9 -4
  24. package/src/runs/background/notify.ts +132 -120
  25. package/src/runs/background/result-watcher.ts +138 -78
  26. package/src/runs/background/run-status.ts +3 -1
  27. package/src/runs/background/subagent-runner.ts +225 -43
  28. package/src/runs/background/subagent-wait.ts +130 -4
  29. package/src/runs/background/wait-tool.ts +2 -2
  30. package/src/runs/foreground/chain-execution.ts +176 -111
  31. package/src/runs/foreground/execution.ts +90 -36
  32. package/src/runs/foreground/foreground-control.ts +90 -0
  33. package/src/runs/foreground/subagent-executor.ts +394 -163
  34. package/src/runs/shared/acceptance.ts +55 -13
  35. package/src/runs/shared/agent-contract.ts +38 -0
  36. package/src/runs/shared/child-protocol.ts +1 -1
  37. package/src/runs/shared/completion-guard.ts +36 -5
  38. package/src/runs/shared/context-mode.ts +44 -0
  39. package/src/runs/shared/dynamic-fanout.ts +4 -4
  40. package/src/runs/shared/long-running-guard.ts +4 -0
  41. package/src/runs/shared/nested-events.ts +27 -2
  42. package/src/runs/shared/parallel-handoff.ts +154 -0
  43. package/src/runs/shared/parallel-utils.ts +6 -0
  44. package/src/runs/shared/pi-args.ts +23 -14
  45. package/src/runs/shared/run-history.ts +90 -5
  46. package/src/runs/shared/structured-output.ts +112 -7
  47. package/src/runs/shared/subagent-control.ts +4 -0
  48. package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
  49. package/src/runs/shared/task-intent.ts +10 -5
  50. package/src/runs/shared/tool-availability.ts +3 -1
  51. package/src/runs/shared/tool-budget.ts +11 -5
  52. package/src/runs/shared/turn-budget.ts +2 -1
  53. package/src/runs/shared/worktree.ts +63 -14
  54. package/src/shared/accessible-dir.ts +25 -0
  55. package/src/shared/artifacts.ts +37 -7
  56. package/src/shared/atomic-json.ts +14 -42
  57. package/src/shared/child-transcript.ts +52 -0
  58. package/src/shared/file-system-retry.ts +47 -0
  59. package/src/shared/settings.ts +9 -1
  60. package/src/shared/types.ts +211 -22
  61. package/src/slash/delegation-adapters.ts +152 -5
  62. package/src/slash/delegation-json.ts +108 -0
  63. package/src/slash/delegation-request.ts +182 -36
  64. package/src/slash/prompt-template-bridge.ts +222 -37
  65. package/src/slash/selector.ts +147 -0
  66. package/src/slash/slash-commands.ts +14 -5
  67. package/src/slash/slash-live-state.ts +2 -2
  68. package/src/slash/subagents-admin.ts +42 -42
  69. package/src/tui/fleet-status.ts +362 -0
  70. package/src/tui/fleet-transcript.ts +472 -0
  71. package/src/tui/fleet.ts +318 -59
  72. package/src/tui/render.ts +25 -15
  73. package/src/watchdog/change-signature.ts +105 -12
  74. package/src/watchdog/review.ts +7 -2
  75. package/src/watchdog/runtime.ts +5 -3
  76. package/src/slash/subagents-editor.ts +0 -86
@@ -35,6 +35,7 @@ export interface AgentMemoryConfig {
35
35
  }
36
36
 
37
37
  export const BUILTIN_AGENT_NAMES = [
38
+ "advisor",
38
39
  "context-builder",
39
40
  "delegate",
40
41
  "oracle",
@@ -476,7 +477,7 @@ function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpD
476
477
  }
477
478
  }
478
479
  return {
479
- ...(tools.length > 0 ? { tools } : {}),
480
+ ...(rawTools !== undefined ? { tools } : {}),
480
481
  ...(mcpDirectTools.length > 0 ? { mcpDirectTools } : {}),
481
482
  };
482
483
  }
@@ -1159,7 +1160,19 @@ export function removeBuiltinAgentOverrideFields(
1159
1160
  return { path: filePath, removed: true };
1160
1161
  }
1161
1162
 
1162
- function listFilesRecursive(dir: string, predicate: (fileName: string) => boolean): string[] {
1163
+ const DISCOVERY_PRUNED_DIR_NAMES = new Set([".git", "node_modules"]);
1164
+
1165
+ function isDiscoveryNestedProjectRoot(dir: string): boolean {
1166
+ return isDirectory(getProjectConfigDir(dir)) || isDirectory(path.join(dir, ".agents"));
1167
+ }
1168
+
1169
+ function shouldPruneDiscoveryDir(rootDir: string, dir: string, dirName: string): boolean {
1170
+ if (DISCOVERY_PRUNED_DIR_NAMES.has(dirName)) return true;
1171
+ if (fs.existsSync(path.join(dir, ".git"))) return true;
1172
+ return path.resolve(dir) !== path.resolve(rootDir) && isDiscoveryNestedProjectRoot(dir);
1173
+ }
1174
+
1175
+ function listFilesRecursive(dir: string, predicate: (fileName: string) => boolean, rootDir = dir): string[] {
1163
1176
  const files: string[] = [];
1164
1177
  if (!fs.existsSync(dir)) return files;
1165
1178
 
@@ -1173,7 +1186,9 @@ function listFilesRecursive(dir: string, predicate: (fileName: string) => boolea
1173
1186
  for (const entry of entries) {
1174
1187
  const filePath = path.join(dir, entry.name);
1175
1188
  if (entry.isDirectory()) {
1176
- files.push(...listFilesRecursive(filePath, predicate));
1189
+ if (!shouldPruneDiscoveryDir(rootDir, filePath, entry.name)) {
1190
+ files.push(...listFilesRecursive(filePath, predicate, rootDir));
1191
+ }
1177
1192
  continue;
1178
1193
  }
1179
1194
  if (!entry.isFile() && !entry.isSymbolicLink()) continue;
@@ -1233,7 +1248,9 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1233
1248
  const runtimeName = buildRuntimeName(localName, packageName);
1234
1249
 
1235
1250
  const rawTools = parseFrontmatterList(frontmatter.tools);
1236
- const { tools = [], mcpDirectTools = [] } = splitToolList(rawTools);
1251
+ const parsedTools = splitToolList(rawTools);
1252
+ const tools = parsedTools.tools ?? [];
1253
+ const mcpDirectTools = parsedTools.mcpDirectTools ?? [];
1237
1254
  const defaultReads = parseFrontmatterList(frontmatter.defaultReads);
1238
1255
  const skillStr = frontmatter.skill || frontmatter.skills;
1239
1256
  const skills = parseFrontmatterList(skillStr);
@@ -1315,7 +1332,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1315
1332
  localName,
1316
1333
  packageName,
1317
1334
  description: frontmatter.description,
1318
- tools: tools.length > 0 ? tools : undefined,
1335
+ tools: rawTools !== undefined ? tools : undefined,
1319
1336
  mcpDirectTools: mcpDirectTools.length > 0 ? mcpDirectTools : undefined,
1320
1337
  model: frontmatter.model,
1321
1338
  fallbackModels: fallbackModels && fallbackModels.length > 0 ? fallbackModels : undefined,
@@ -1,4 +1,5 @@
1
1
  export const SUBAGENT_DELEGATION_PROTOCOL_VERSION = 1 as const;
2
+ export const SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION = 2 as const;
2
3
 
3
4
  // This is the established extension-to-extension transport. The public API
4
5
  // intentionally reuses it instead of adding a second event protocol.
@@ -19,6 +20,37 @@ export interface SubagentDelegationToolBudget {
19
20
  block?: string[] | "*";
20
21
  }
21
22
 
23
+ export interface SubagentDelegationAgentContract {
24
+ version: 1;
25
+ }
26
+
27
+ export type SubagentDelegationJsonSchemaObject = Record<string, unknown>;
28
+
29
+ export interface SubagentDelegationExecutionResult {
30
+ status: "completed" | "failed" | "paused" | "stopped" | "detached";
31
+ success: boolean;
32
+ exitCode: number;
33
+ error?: string;
34
+ interrupted?: boolean;
35
+ timedOut?: boolean;
36
+ stopped?: boolean;
37
+ detached?: boolean;
38
+ }
39
+
40
+ export interface SubagentDelegationReviewResult {
41
+ status: "not-requested" | "no-blockers" | "blockers" | "needs-parent-decision";
42
+ findings?: Array<{ severity: "blocker" | "non-blocking"; file?: string; issue: string; rationale: string }>;
43
+ }
44
+
45
+ export interface SubagentDelegationEffectsResult {
46
+ fileMutation?: {
47
+ status: "not-requested" | "not-applicable" | "observed" | "missing";
48
+ expected: boolean;
49
+ attempted: boolean;
50
+ message?: string;
51
+ };
52
+ }
53
+
22
54
  export type SubagentDelegationAcceptanceEvidence =
23
55
  | "changed-files"
24
56
  | "tests-added"
@@ -87,6 +119,8 @@ export interface SubagentDelegationRequest {
87
119
  skill?: string | string[] | boolean;
88
120
  output?: string | boolean;
89
121
  outputMode?: "inline" | "file-only";
122
+ outputSchema?: SubagentDelegationJsonSchemaObject;
123
+ agentContract?: SubagentDelegationAgentContract;
90
124
  acceptance?: SubagentDelegationAcceptance;
91
125
  artifacts?: boolean;
92
126
  }
@@ -116,6 +150,7 @@ export type SubagentDelegationStatus =
116
150
  | "interrupted"
117
151
  | "turn_budget_exhausted"
118
152
  | "tool_budget_exhausted"
153
+ | "structured_output_failed"
119
154
  | "acceptance_failed"
120
155
  | "invalid_request"
121
156
  | "unavailable_context";
@@ -144,10 +179,13 @@ export interface SubagentDelegationResponse extends SubagentDelegationStarted {
144
179
  agent?: string;
145
180
  model?: string;
146
181
  exitCode?: number;
182
+ execution?: SubagentDelegationExecutionResult;
147
183
  output?: string;
148
184
  outputPath?: string;
149
185
  sessionFile?: string;
150
186
  acceptance?: SubagentDelegationAcceptanceResult;
187
+ review?: SubagentDelegationReviewResult;
188
+ effects?: SubagentDelegationEffectsResult;
151
189
  turns?: number;
152
190
  toolCount?: number;
153
191
  durationMs?: number;
@@ -156,3 +194,90 @@ export interface SubagentDelegationResponse extends SubagentDelegationStarted {
156
194
  }
157
195
 
158
196
  export interface SubagentDelegationCancel extends SubagentDelegationStarted {}
197
+
198
+ export type SubagentDelegationV2Thinking = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
199
+
200
+ export type SubagentDelegationV2ResultRequest =
201
+ | { kind: "text" }
202
+ | { kind: "structured"; schema: SubagentDelegationJsonSchemaObject };
203
+
204
+ export interface SubagentDelegationV2Request {
205
+ version: typeof SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION;
206
+ requestId: string;
207
+ ownerRunId: string;
208
+ nodeId: string;
209
+ agent: string;
210
+ task: string;
211
+ context: "fresh" | "fork";
212
+ cwd: string;
213
+ model?: string;
214
+ thinking?: SubagentDelegationV2Thinking;
215
+ timeoutMs?: number;
216
+ turnBudget?: SubagentDelegationTurnBudget;
217
+ toolBudget?: SubagentDelegationToolBudget;
218
+ skill?: string | string[] | boolean;
219
+ artifacts?: boolean;
220
+ result: SubagentDelegationV2ResultRequest;
221
+ }
222
+
223
+ export interface SubagentDelegationV2Started {
224
+ version: typeof SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION;
225
+ requestId: string;
226
+ ownerRunId: string;
227
+ nodeId: string;
228
+ }
229
+
230
+ export interface SubagentDelegationV2Update extends SubagentDelegationV2Started {
231
+ currentTool?: string;
232
+ currentToolArgs?: string;
233
+ recentOutput?: string;
234
+ recentOutputLines?: string[];
235
+ recentTools?: Array<{ tool: string; args: string }>;
236
+ model?: string;
237
+ toolCount?: number;
238
+ durationMs?: number;
239
+ tokens?: number;
240
+ }
241
+
242
+ export type SubagentDelegationV2Status = SubagentDelegationStatus | "duplicate_node";
243
+
244
+ export type SubagentDelegationV2Value =
245
+ | { kind: "text"; text: string }
246
+ | { kind: "structured"; value: unknown };
247
+
248
+ export interface SubagentDelegationV2Usage {
249
+ input: number;
250
+ output: number;
251
+ cacheRead: number;
252
+ cacheWrite: number;
253
+ cost: number;
254
+ turns: number;
255
+ toolCalls: number;
256
+ durationMs: number;
257
+ }
258
+
259
+ export interface SubagentDelegationV2TerminalResponse extends SubagentDelegationV2Started {
260
+ status: Exclude<SubagentDelegationV2Status, "invalid_request">;
261
+ error?: string;
262
+ runId?: string;
263
+ agent?: string;
264
+ model?: string;
265
+ thinking?: string;
266
+ exitCode?: number;
267
+ result?: SubagentDelegationV2Value;
268
+ usage?: SubagentDelegationV2Usage;
269
+ }
270
+
271
+ /** A malformed V2 request can only be correlated by the valid identity fields it supplied. */
272
+ export interface SubagentDelegationV2InvalidResponse {
273
+ version: typeof SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION;
274
+ requestId: string;
275
+ ownerRunId?: string;
276
+ nodeId?: string;
277
+ status: "invalid_request";
278
+ error?: string;
279
+ }
280
+
281
+ export type SubagentDelegationV2Response = SubagentDelegationV2TerminalResponse | SubagentDelegationV2InvalidResponse;
282
+
283
+ export interface SubagentDelegationV2Cancel extends SubagentDelegationV2Started {}
@@ -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";
@@ -92,28 +95,6 @@ function expandTilde(p: string): string {
92
95
  return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
93
96
  }
94
97
 
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
98
  function isSlashResultRunning(result: { details?: Details }): boolean {
118
99
  return result.details?.progress?.some((entry) => entry.status === "running")
119
100
  || result.details?.results.some((entry) => entry.progress?.status === "running")
@@ -210,12 +191,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
210
191
  const config = loadConfig();
211
192
  const waitToolConfig = resolveWaitToolConfig(config.waitTool);
212
193
  const asyncByDefault = config.asyncByDefault === true;
194
+ const fleetViewEnabled = config.fleetView !== false;
195
+ const asyncWidgetEnabled = config.asyncWidget === true || (!fleetViewEnabled && config.asyncWidget !== false);
213
196
  const tempArtifactsDir = getArtifactsDir(null);
214
197
  cleanupAllArtifactDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
215
198
 
216
199
  const state: SubagentState = {
217
200
  baseCwd: "",
218
201
  currentSessionId: null,
202
+ artifactDirPreference: config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
203
+ parentSessionFile: null,
219
204
  subagentInProgress: false,
220
205
  subagentSpawns: {
221
206
  sessionId: null,
@@ -244,22 +229,30 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
244
229
 
245
230
  const supervisorChannel = createNativeSupervisorChannel(pi, state);
246
231
  const mainWatchdog = registerMainWatchdog(pi);
247
- let disposeSubagentNotify = () => {};
232
+ const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch });
233
+ const fleetStatus = fleetViewEnabled
234
+ ? new SubagentFleetStatus(state, async (itemKey) => {
235
+ const ctx = state.lastUiContext;
236
+ if (!ctx?.hasUI) return;
237
+ await openSubagentFleet(ctx, state, { initialKey: itemKey });
238
+ })
239
+ : undefined;
248
240
  const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
249
241
  pi,
250
242
  state,
251
243
  RESULTS_DIR,
252
244
  10 * 60 * 1000,
245
+ { notifier: completionNotifier },
253
246
  );
254
- startResultWatcher();
255
- primeExistingResults();
256
247
 
257
248
  const runtimeCleanup = () => {
258
- disposeSubagentNotify();
259
- mainWatchdog.dispose();
260
249
  stopResultWatcher();
250
+ state.currentSessionId = null;
251
+ completionNotifier.dispose();
252
+ mainWatchdog.dispose();
261
253
  scheduledRunManager.stop();
262
254
  supervisorChannel.dispose();
255
+ fleetStatus?.dispose();
263
256
  clearPendingForegroundControlNotices(state);
264
257
  if (state.poller) {
265
258
  clearInterval(state.poller);
@@ -269,7 +262,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
269
262
  globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
270
263
 
271
264
  const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs } = createAsyncJobTracker(pi, state, ASYNC_DIR, {
272
- widgetEnabled: config.asyncWidget !== false,
265
+ widgetEnabled: asyncWidgetEnabled,
273
266
  });
274
267
  let executorExecute: ((id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
275
268
  const scheduledRunManager = createScheduledRunManager({
@@ -377,6 +370,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
377
370
  getContext: () => state.lastUiContext,
378
371
  execute: (requestId, params, signal, ctx, onUpdate) =>
379
372
  executeSubagentCollapsed(requestId, params, signal, onUpdate, ctx),
373
+ executeVersioned: (requestId, params, signal, ctx, onUpdate) => {
374
+ if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
375
+ return executor.executeDelegated(requestId, params, signal, onUpdate, ctx);
376
+ },
380
377
  });
381
378
 
382
379
  const rpcBridge = registerSubagentRpcBridge({
@@ -472,8 +469,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
472
469
  }
473
470
  }
474
471
  }
475
- disposeSubagentNotify = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch });
476
-
477
472
  const existingVisibleControlNotices = globalStore[controlNoticeSeenStoreKey];
478
473
  const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
479
474
  globalStore[controlNoticeSeenStoreKey] = visibleControlNotices;
@@ -488,9 +483,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
488
483
  const steeringNoticeHandler = (payload: unknown) => {
489
484
  handleSubagentSteeringNotice({ pi, state, details: payload as SubagentSteeringMessageDetails });
490
485
  };
486
+ const asyncStartedHandler = (payload: unknown) => {
487
+ handleStarted(payload);
488
+ fleetStatus?.refresh();
489
+ };
490
+ const asyncCompleteHandler = (payload: unknown) => {
491
+ handleComplete(payload);
492
+ fleetStatus?.refresh();
493
+ };
491
494
  const eventUnsubscribes = [
492
- pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, handleStarted),
493
- pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete),
495
+ pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, asyncStartedHandler),
496
+ pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
494
497
  pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
495
498
  pi.events.on(SUBAGENT_STEERING_NOTICE_EVENT, steeringNoticeHandler),
496
499
  rpcBridge.dispose,
@@ -501,6 +504,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
501
504
  if (event.toolName !== "subagent") return;
502
505
  if (!ctx.hasUI) return;
503
506
  state.lastUiContext = ctx;
507
+ fleetStatus?.setContext(ctx);
508
+ fleetStatus?.refresh();
504
509
  if (state.asyncJobs.size > 0) {
505
510
  refreshWidget(ctx);
506
511
  ensurePoller();
@@ -518,9 +523,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
518
523
  }
519
524
  };
520
525
 
521
- const resetSessionState = (ctx: ExtensionContext) => {
526
+ const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
522
527
  state.baseCwd = ctx.cwd;
523
528
  state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
529
+ state.parentSessionFile = ctx.sessionManager.getSessionFile();
524
530
  state.subagentSpawns = {
525
531
  sessionId: state.currentSessionId,
526
532
  count: 0,
@@ -546,17 +552,23 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
546
552
  restoreActiveJobs(ctx);
547
553
  scheduledRunManager.bindSession(ctx);
548
554
  restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
549
- primeExistingResults();
555
+ startResultWatcher();
556
+ primeExistingResults({ triggerTurn: !recovering });
557
+ fleetStatus?.setContext(ctx);
550
558
  };
551
559
 
552
- pi.on("session_start", (_event, ctx) => {
553
- resetSessionState(ctx);
560
+ pi.on("session_start", (event, ctx) => {
561
+ const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
562
+ resetSessionState(ctx, recovering);
554
563
  rpcBridge.emitReady(ctx);
555
564
  supervisorChannel.start();
556
565
  });
557
566
 
558
567
  pi.on("session_shutdown", () => {
559
- disposeSubagentNotify();
568
+ stopResultWatcher();
569
+ state.currentSessionId = null;
570
+ state.parentSessionFile = null;
571
+ completionNotifier.dispose();
560
572
  delete process.env[SUBAGENT_PARENT_SESSION_ENV];
561
573
  for (const unsubscribe of eventUnsubscribes) {
562
574
  try {
@@ -568,7 +580,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
568
580
  if (globalStore[eventUnsubscribeStoreKey] === eventUnsubscribes) {
569
581
  delete globalStore[eventUnsubscribeStoreKey];
570
582
  }
571
- stopResultWatcher();
572
583
  scheduledRunManager.stop();
573
584
  if (state.poller) clearInterval(state.poller);
574
585
  state.poller = null;
@@ -584,6 +595,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
584
595
  promptTemplateBridge.cancelAll();
585
596
  promptTemplateBridge.dispose();
586
597
  supervisorChannel.dispose();
598
+ fleetStatus?.dispose();
587
599
  if (globalStore[runtimeCleanupStoreKey] === runtimeCleanup) {
588
600
  delete globalStore[runtimeCleanupStoreKey];
589
601
  }
@@ -6,7 +6,12 @@ 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 { type Details, ASYNC_DIR, RESULTS_DIR } from "../shared/types.ts";
9
+ import {
10
+ type Details,
11
+ ASYNC_DIR,
12
+ RESULTS_DIR,
13
+ SUBAGENT_ASYNC_COMPLETE_EVENT,
14
+ } from "../shared/types.ts";
10
15
  import { readStatus } from "../shared/utils.ts";
11
16
  import { SubagentParams } from "./schemas.ts";
12
17
  import { validateChainInput } from "./chain-validation.ts";
@@ -16,7 +21,7 @@ export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
16
21
  export const SUBAGENT_RPC_READY_EVENT = "subagents:rpc:v1:ready";
17
22
  export const SUBAGENT_RPC_REPLY_EVENT_PREFIX = "subagents:rpc:v1:reply:";
18
23
 
19
- export const SUBAGENT_RPC_METHODS = ["ping", "status", "spawn", "interrupt", "stop"] as const;
24
+ export const SUBAGENT_RPC_METHODS = ["ping", "status", "spawn", "steer", "interrupt", "stop"] as const;
20
25
  export type SubagentRpcMethod = typeof SUBAGENT_RPC_METHODS[number];
21
26
 
22
27
  export interface SubagentRpcRequestEnvelope {
@@ -172,6 +177,8 @@ function pingData(ctx: ExtensionContext | null) {
172
177
  capabilities: {
173
178
  status: true,
174
179
  asyncSpawn: true,
180
+ steer: true,
181
+ nonRecoveringSteer: true,
175
182
  interrupt: true,
176
183
  stop: true,
177
184
  },
@@ -179,6 +186,7 @@ function pingData(ctx: ExtensionContext | null) {
179
186
  ready: SUBAGENT_RPC_READY_EVENT,
180
187
  request: SUBAGENT_RPC_REQUEST_EVENT,
181
188
  replyPrefix: SUBAGENT_RPC_REPLY_EVENT_PREFIX,
189
+ asyncComplete: SUBAGENT_ASYNC_COMPLETE_EVENT,
182
190
  },
183
191
  session: sessionData(ctx),
184
192
  };
@@ -212,6 +220,20 @@ function spawnParams(params: unknown): SubagentParamsLike {
212
220
  return { ...(input as SubagentParamsLike), async: true, clarify: false };
213
221
  }
214
222
 
223
+ function steerParams(params: unknown): SubagentParamsLike {
224
+ const input = assertRecordParams(params, "steer");
225
+ if (typeof input.message !== "string" || !input.message.trim())
226
+ throw new SubagentRpcError("invalid_params", "RPC steer requires a non-empty message.");
227
+ const target = normalizeTargetParams(input, "steer");
228
+ if (!target.id && !target.runId && !target.dir) throw new SubagentRpcError("invalid_params", "RPC steer requires id, runId, or dir.");
229
+ return {
230
+ action: "steer",
231
+ ...target,
232
+ message: input.message.trim(),
233
+ steeringRecovery: false,
234
+ };
235
+ }
236
+
215
237
  function stopAsyncRun(
216
238
  params: unknown,
217
239
  options: RegisterSubagentRpcBridgeOptions,
@@ -289,6 +311,9 @@ async function handleRequest(
289
311
  if (request.method === "status") {
290
312
  return executeChecked(options, ctx, request.requestId, request.method, { action: "status", ...normalizeTargetParams(request.params, "status") });
291
313
  }
314
+ if (request.method === "steer") {
315
+ return executeChecked(options, ctx, request.requestId, request.method, steerParams(request.params));
316
+ }
292
317
  if (request.method === "interrupt") {
293
318
  return executeChecked(options, ctx, request.requestId, request.method, { action: "interrupt", ...normalizeTargetParams(request.params, "interrupt") });
294
319
  }
@@ -71,7 +71,16 @@ const AcceptanceOverride = Type.Unsafe({
71
71
  { type: "boolean", enum: [false] },
72
72
  { type: "object", additionalProperties: true },
73
73
  ],
74
- description: "Optional acceptance policy. Omitted means auto-inferred; verified requires configured runtime commands. Reviewed is inferred-only because explicit runs cannot supply an independent reviewer result. Bare \"none\" requires { level: \"none\", reason: \"...\" }, while false is deprecated.",
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.",
75
+ });
76
+
77
+ const AgentContractOverride = Type.Object({
78
+ version: Type.Integer({ enum: [1], description: "Opt into generic agent contract v1 for this run/child." }),
79
+ }, { additionalProperties: false, description: "Opt-in compatibility contract. Omit to use current default behavior." });
80
+
81
+ const ChainGateOverride = Type.String({
82
+ enum: ["execution", "acceptance"],
83
+ description: "For agentContract.version=1 chain steps, choose whether the chain advances on execution success or acceptance success. Defaults to execution.",
75
84
  });
76
85
 
77
86
  const TurnBudgetOverride = Type.Object({
@@ -104,7 +113,9 @@ const TaskItem = Type.Object({
104
113
  model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
105
114
  skill: Type.Optional(SkillOverride),
106
115
  toolBudget: Type.Optional(ToolBudgetOverride),
116
+ outputSchema: Type.Optional(JsonSchemaObject),
107
117
  acceptance: Type.Optional(AcceptanceOverride),
118
+ agentContract: Type.Optional(AgentContractOverride),
108
119
  });
109
120
 
110
121
  // Parallel task item (within a parallel step)
@@ -125,6 +136,8 @@ export const ParallelTaskSchema = Type.Object({
125
136
  model: Type.Optional(Type.String({ description: "Override model for this task" })),
126
137
  toolBudget: Type.Optional(ToolBudgetOverride),
127
138
  acceptance: Type.Optional(AcceptanceOverride),
139
+ agentContract: Type.Optional(AgentContractOverride),
140
+ gateOn: Type.Optional(ChainGateOverride),
128
141
  });
129
142
 
130
143
  export const DynamicExpandSchema = Type.Object({
@@ -153,6 +166,8 @@ export const DynamicParallelTemplateSchema = Type.Object({
153
166
  model: Type.Optional(Type.String({ description: "Override model for this task" })),
154
167
  toolBudget: Type.Optional(ToolBudgetOverride),
155
168
  acceptance: Type.Optional(AcceptanceOverride),
169
+ agentContract: Type.Optional(AgentContractOverride),
170
+ gateOn: Type.Optional(ChainGateOverride),
156
171
  }, { additionalProperties: false });
157
172
 
158
173
  export const DynamicCollectSchema = Type.Object({
@@ -179,6 +194,8 @@ export const ChainItem = Type.Object({
179
194
  model: Type.Optional(Type.String({ description: "Override model for this step" })),
180
195
  toolBudget: Type.Optional(ToolBudgetOverride),
181
196
  acceptance: Type.Optional(AcceptanceOverride),
197
+ agentContract: Type.Optional(AgentContractOverride),
198
+ gateOn: Type.Optional(ChainGateOverride),
182
199
  parallel: Type.Optional(Type.Unsafe({
183
200
  anyOf: [
184
201
  Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
@@ -218,7 +235,7 @@ const SubagentParamsSchema = Type.Object({
218
235
  task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
219
236
  // Management action (when present, tool operates in management mode)
220
237
  action: Type.Optional(Type.String({
221
- description: "Management/control action only. Must be omitted for execution mode (single, parallel, or chain)."
238
+ description: "Optional management/control action. Omit this field entirely for execution/delegation ({agent, task}, {tasks}, or {chain}); use it only for management/control actions."
222
239
  })),
223
240
  id: Type.Optional(Type.String({
224
241
  description: "Run id or prefix for action='status', action='interrupt', action='stop', action='resume', action='steer', or action='append-step'."
@@ -236,6 +253,7 @@ const SubagentParamsSchema = Type.Object({
236
253
  })),
237
254
  lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum transcript lines for action='status', view='transcript'. Defaults to 80." })),
238
255
  message: Type.Optional(Type.String({ description: "Follow-up message for action='resume' (revive paused, completed, or failed children, or reach a routed nested run) or live async guidance for action='steer'. Stopped runs are non-resumable. Use index to choose a child from multi-child runs." })),
256
+ steeringRecovery: Type.Optional(Type.Boolean({ description: "For action='steer', allow pause-and-revive recovery after a missed acknowledgment. Defaults true for direct tool calls; extension RPC steering forces false so callers retain exact child ownership." })),
239
257
  additional: Type.Optional(Type.Integer({ minimum: 1, description: "Positive launches to add with action='grant-spawn-budget'. Root interactive parent with native user confirmation only; total grants cannot exceed the original configured cap." })),
240
258
  scope: Type.Optional(Type.String({ enum: ["session", "user", "project"], description: "Scope for action='watchdog.configure'. Defaults to session to avoid persistent settings writes unless user/project is explicit." })),
241
259
  target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for action='watchdog.configure'. Defaults to main. Use target='child' with agent for a per-agent child watchdog override." })),
@@ -292,6 +310,8 @@ const SubagentParamsSchema = Type.Object({
292
310
  outputMode: Type.Optional(OutputModeOverride),
293
311
  skill: Type.Optional(SkillOverride),
294
312
  model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
313
+ outputSchema: Type.Optional(JsonSchemaObject),
314
+ agentContract: Type.Optional(AgentContractOverride),
295
315
  acceptance: Type.Optional(AcceptanceOverride),
296
316
  });
297
317
 
@@ -14,7 +14,7 @@ export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
14
14
  • Writing/review safety: keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers/validators for independent review, then have the parent synthesize and apply fixes as the sole writer unless an isolated worktree was intentionally requested.
15
15
  • Artifacts/status essentials: chain outputs live under {chain_dir}; async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, and status via { action: "status", id }. Include output paths and residual risks when reporting results.`;
16
16
 
17
- export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Delegate to subagents or manage agent definitions.
17
+ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `To delegate work, call with { agent, task }, { tasks }, or { chain }; omit action. Use action only for management/control actions listed below.
18
18
 
19
19
  EXECUTION (use exactly ONE mode):
20
20
  • Before executing, use { action: "list" } to inspect configured agents/chains. Only execute agents listed as executable/non-disabled.
@@ -74,7 +74,7 @@ DIAGNOSTICS:
74
74
 
75
75
  ${SUBAGENT_SAFETY_GUIDANCE}`;
76
76
 
77
- export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to subagents or manage definitions. Use exactly one mode per call.
77
+ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `To delegate work, call with { agent, task }, { tasks }, or { chain }; omit action. Use action only for management/control actions listed below. Use exactly one mode per call.
78
78
 
79
79
  EXECUTE:
80
80
  • Before execution, call { action: "list" }; run only executable/non-disabled configured agents/chains.
@@ -161,7 +161,7 @@ export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomB
161
161
  if (!bridge.active || !bridge.orchestratorTarget) return agent;
162
162
 
163
163
  const bridgeTools = ["intercom", "contact_supervisor"];
164
- const tools = agent.tools
164
+ const tools = agent.tools && agent.tools.length > 0
165
165
  ? [...agent.tools, ...bridgeTools.filter((tool) => !agent.tools?.includes(tool))]
166
166
  : agent.tools;
167
167
  const instruction = bridge.instruction;