pi-subagents 0.66.0 → 0.68.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 (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +43 -15
  6. package/docs/configuration.md +67 -23
  7. package/docs/extension-api.md +38 -19
  8. package/docs/missions.md +10 -2
  9. package/docs/models.md +11 -79
  10. package/docs/observability.md +22 -12
  11. package/docs/standalone-background.md +59 -0
  12. package/docs/tool-reference.md +33 -20
  13. package/docs/watchdog.md +39 -10
  14. package/docs/workflows.md +37 -13
  15. package/index.ts +5 -2
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +4 -2
  18. package/prompts/parallel-review.md +1 -1
  19. package/runner-peer-loader.mjs +24 -0
  20. package/runner-peer-preload.mjs +32 -0
  21. package/skills/pi-subagents/SKILL.md +32 -21
  22. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  23. package/skills/pi-subagents/references/execution-controls.md +11 -9
  24. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  25. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  26. package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
  27. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  28. package/src/agents/advertised-agent-prompt.ts +34 -3
  29. package/src/agents/agent-management.ts +57 -58
  30. package/src/agents/agent-serializer.ts +4 -3
  31. package/src/agents/agents.ts +190 -71
  32. package/src/agents/builtin-names.ts +1 -0
  33. package/src/agents/chain-serializer.ts +5 -0
  34. package/src/agents/runtime-agent-registry.ts +7 -6
  35. package/src/api/delegation.ts +4 -0
  36. package/src/api/preflight.ts +94 -59
  37. package/src/api/required-child-extensions.ts +6 -0
  38. package/src/api/shared-types.ts +2 -0
  39. package/src/extension/config.ts +10 -37
  40. package/src/extension/fanout-child.ts +66 -4
  41. package/src/extension/herdr-pi-bridge.ts +160 -0
  42. package/src/extension/index.ts +62 -39
  43. package/src/extension/public-execution.ts +7 -5
  44. package/src/extension/rpc.ts +4 -0
  45. package/src/extension/schemas.ts +81 -81
  46. package/src/extension/tool-description.ts +30 -82
  47. package/src/inspectors/actions.ts +148 -0
  48. package/src/inspectors/ghostty/actions.ts +74 -0
  49. package/src/inspectors/ghostty/plugin.ts +17 -0
  50. package/src/inspectors/herdr/actions.ts +99 -179
  51. package/src/inspectors/herdr/plugin.ts +20 -0
  52. package/src/inspectors/herdr/project-panes.ts +1 -1
  53. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  54. package/src/inspectors/plugins.ts +8 -0
  55. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  56. package/src/inspectors/types.ts +51 -0
  57. package/src/intercom/intercom-bridge.ts +50 -8
  58. package/src/intercom/native-supervisor-channel.ts +44 -31
  59. package/src/policy/authority.ts +4 -0
  60. package/src/profiles/profiles.ts +12 -6
  61. package/src/runs/background/active-async-capacity.ts +4 -0
  62. package/src/runs/background/active-run-index.ts +17 -1
  63. package/src/runs/background/async-execution.ts +348 -176
  64. package/src/runs/background/async-job-tracker.ts +8 -6
  65. package/src/runs/background/async-resume.ts +17 -12
  66. package/src/runs/background/async-status.ts +15 -4
  67. package/src/runs/background/auto-drain.ts +23 -10
  68. package/src/runs/background/binary-bootstrap.ts +38 -0
  69. package/src/runs/background/chain-append.ts +1 -1
  70. package/src/runs/background/chain-root-attachment.ts +14 -33
  71. package/src/runs/background/fleet-view.ts +30 -2
  72. package/src/runs/background/notify.ts +105 -7
  73. package/src/runs/background/owned-process-tree.ts +29 -2
  74. package/src/runs/background/result-files.ts +8 -4
  75. package/src/runs/background/result-watcher.ts +19 -2
  76. package/src/runs/background/run-child-session.ts +81 -33
  77. package/src/runs/background/run-status.ts +3 -0
  78. package/src/runs/background/runner-aliases.ts +12 -33
  79. package/src/runs/background/runner-child-launch.ts +6 -1
  80. package/src/runs/background/runner-child-sessions.ts +5 -4
  81. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  82. package/src/runs/background/scheduled-runs.ts +51 -18
  83. package/src/runs/background/stale-run-reconciler.ts +35 -11
  84. package/src/runs/background/steering.ts +20 -2
  85. package/src/runs/background/subagent-runner.ts +441 -304
  86. package/src/runs/background/subagent-wait.ts +176 -28
  87. package/src/runs/background/wait-completions.ts +75 -27
  88. package/src/runs/background/wait-subscriptions.ts +9 -3
  89. package/src/runs/background/wait-tool.ts +5 -3
  90. package/src/runs/foreground/async-steering-action.ts +18 -7
  91. package/src/runs/foreground/async-stop-action.ts +93 -3
  92. package/src/runs/foreground/execution.ts +134 -248
  93. package/src/runs/foreground/foreground-history.ts +2 -1
  94. package/src/runs/foreground/prompt-audit.ts +3 -1
  95. package/src/runs/foreground/subagent-executor.ts +374 -178
  96. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  97. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  98. package/src/runs/shared/acceptance.ts +38 -11
  99. package/src/runs/shared/async-status-projection.ts +127 -33
  100. package/src/runs/shared/capability-ceiling.ts +2 -0
  101. package/src/runs/shared/child-hooks.ts +25 -10
  102. package/src/runs/shared/child-launch-plan.ts +15 -3
  103. package/src/runs/shared/child-launch.ts +28 -5
  104. package/src/runs/shared/child-lifecycle.ts +6 -3
  105. package/src/runs/shared/child-runtime-config.ts +8 -1
  106. package/src/runs/shared/child-session.ts +127 -52
  107. package/src/runs/shared/child-tool-plan.ts +142 -11
  108. package/src/runs/shared/completion-guard.ts +5 -3
  109. package/src/runs/shared/dynamic-fanout.ts +2 -2
  110. package/src/runs/shared/effective-system-prompt.ts +33 -0
  111. package/src/runs/shared/external-cli-contract.ts +11 -1
  112. package/src/runs/shared/external-cli-preflight.ts +6 -2
  113. package/src/runs/shared/external-cli-runner.ts +9 -7
  114. package/src/runs/shared/herdr-connection.ts +134 -0
  115. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  116. package/src/runs/shared/herdr-machine.ts +279 -0
  117. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  118. package/src/runs/shared/herdr-placed-run.ts +263 -0
  119. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  120. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  121. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
  122. package/src/runs/shared/model-scope.ts +1 -1
  123. package/src/runs/shared/nested-events.ts +11 -2
  124. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  125. package/src/runs/shared/parallel-utils.ts +7 -2
  126. package/src/runs/shared/pi-spawn.ts +10 -0
  127. package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
  128. package/src/runs/shared/task-intent.ts +46 -13
  129. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  130. package/src/runs/shared/worktree-setup-command.ts +27 -4
  131. package/src/runs/shared/worktree.ts +45 -15
  132. package/src/shared/child-cache-retention.ts +43 -0
  133. package/src/shared/fork-context.ts +15 -72
  134. package/src/shared/launch-contract.ts +68 -8
  135. package/src/shared/opencode-session-headers.ts +30 -0
  136. package/src/shared/pruned-fork.ts +1 -1
  137. package/src/shared/required-child-extensions.ts +81 -0
  138. package/src/shared/settings.ts +5 -2
  139. package/src/shared/shortcuts.ts +0 -4
  140. package/src/shared/types.ts +74 -30
  141. package/src/slash/delegation-adapters.ts +3 -1
  142. package/src/slash/delegation-request.ts +14 -0
  143. package/src/slash/slash-commands.ts +2 -7
  144. package/src/slash/subagents-admin.ts +24 -13
  145. package/src/tui/fleet-status.ts +164 -19
  146. package/src/tui/fleet.ts +16 -14
  147. package/src/tui/render.ts +168 -37
  148. package/src/watchdog/child-status.ts +28 -28
  149. package/src/watchdog/lsp-diagnostics.ts +1 -1
  150. package/src/watchdog/model-selection.ts +21 -1
  151. package/src/watchdog/permission-arbiter.ts +3 -1
  152. package/src/watchdog/register-child.ts +10 -2
  153. package/src/watchdog/register-main.ts +39 -35
  154. package/src/watchdog/render.ts +1 -1
  155. package/src/watchdog/review.ts +123 -74
  156. package/src/watchdog/rules.ts +1 -1
  157. package/src/watchdog/runtime.ts +100 -27
  158. package/src/watchdog/scope.ts +1 -1
  159. package/src/watchdog/settings.ts +3 -0
  160. package/src/watchdog/tool-actions.ts +13 -12
  161. package/src/watchdog/turn-delta.ts +23 -0
  162. package/src/watchdog/types.ts +5 -3
  163. package/src/watchdog/warning-format.ts +1 -1
  164. package/src/workflows/scripted-workflow.ts +279 -10
  165. package/src/workflows/workflow-checklist.ts +2 -2
  166. package/src/workflows/workflow-receipt.ts +21 -3
  167. package/src/workflows/workflow-resources.ts +13 -2
  168. package/runner-server-preload.mjs +0 -13
  169. package/src/runs/shared/model-exclusions.ts +0 -374
  170. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  171. package/src/runs/shared/readonly-session-evidence.ts +0 -307
  172. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -2,13 +2,13 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import * as readline from "node:readline";
4
4
  import { fileURLToPath } from "node:url";
5
- import { parseMissionRecord } from "../../missions/store.ts";
6
- import type { MissionRecord } from "../../missions/types.ts";
7
- import { requestAsyncSteer, requestAsyncStop } from "../../runs/background/control-channel.ts";
8
- import { formatAsyncRunTranscript } from "../../runs/background/fleet-view.ts";
9
- import { steeringReceipt } from "../../runs/background/steering.ts";
10
- import type { AsyncStatus } from "../../shared/types.ts";
11
- import { readStatus } from "../../shared/utils.ts";
5
+ import { parseMissionRecord } from "../missions/store.ts";
6
+ import type { MissionRecord } from "../missions/types.ts";
7
+ import { requestAsyncSteer, requestAsyncStop } from "../runs/background/control-channel.ts";
8
+ import { formatAsyncRunTranscript } from "../runs/background/fleet-view.ts";
9
+ import { steeringReceipt } from "../runs/background/steering.ts";
10
+ import type { AsyncStatus } from "../shared/types.ts";
11
+ import { readStatus } from "../shared/utils.ts";
12
12
  import { decodeSessionRoots } from "./session-roots-codec.ts";
13
13
 
14
14
  export interface RunnerOptions {
@@ -31,7 +31,7 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
31
31
  const { status, asyncDir, mission } = input;
32
32
  const lines = [
33
33
  `pi-subagents inspector for ${status.runId}`,
34
- "This pane mirrors lifecycle artifacts; closing it does not stop the run.",
34
+ "This inspector mirrors lifecycle artifacts; closing it does not stop the run.",
35
35
  "",
36
36
  ];
37
37
  if (mission) {
@@ -43,7 +43,7 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
43
43
  lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60, sessionRoots: input.sessionRoots }));
44
44
  const acceptsPlainGuidance = input.index !== undefined || status.mode === "single";
45
45
  const controls = [input.allowSteer === false || !acceptsPlainGuidance ? undefined : "type guidance", input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
46
- lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom).");
46
+ lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom); this inspector is read-only.");
47
47
  return lines.join("\n");
48
48
  }
49
49
 
@@ -91,7 +91,7 @@ function queueInspectorSteer(options: RunnerOptions, status: AsyncStatus, messag
91
91
  requestAsyncSteer(options.asyncDir, {
92
92
  message,
93
93
  ...(targetIndex !== undefined ? { targetIndex } : { targetIndexes: runningIndexes }),
94
- source: "herdr-inspector",
94
+ source: "inspector-runner",
95
95
  });
96
96
  return steeringReceipt(message, `Steering queued for run ${options.runId}.`);
97
97
  }
@@ -104,7 +104,7 @@ export function submitInspectorControl(options: RunnerOptions, line: string): st
104
104
  if (command === "stop") {
105
105
  if (options.allowStop === false) throw new Error("Authority policy does not allow stop from this inspector.");
106
106
  if (isTerminal(status)) throw new Error(`Run '${options.runId}' is ${status.state} and cannot be stopped.`);
107
- requestAsyncStop(options.asyncDir, { source: "herdr-inspector" });
107
+ requestAsyncStop(options.asyncDir, { source: "inspector-runner" });
108
108
  return `Stop requested for run ${options.runId}.`;
109
109
  }
110
110
  if (command.startsWith("steer ")) {
@@ -147,7 +147,7 @@ export function runInspector(argv = process.argv.slice(2)): void {
147
147
 
148
148
  if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
149
149
  try { runInspector(); } catch (cause) {
150
- process.stderr.write(`Herdr inspector failed: ${cause instanceof Error ? cause.message : String(cause)}\n`);
150
+ process.stderr.write(`Inspector failed: ${cause instanceof Error ? cause.message : String(cause)}\n`);
151
151
  process.exitCode = 1;
152
152
  }
153
153
  }
@@ -0,0 +1,8 @@
1
+ import { createHerdrInspectorPlugin } from "./herdr/plugin.ts";
2
+ import { createGhosttyInspectorPlugin } from "./ghostty/plugin.ts";
3
+ import type { InspectorPlugin } from "./types.ts";
4
+
5
+ /** Built-in inspector plugins, ordered by host preference. */
6
+ export function createBuiltinInspectorPlugins(): readonly InspectorPlugin[] {
7
+ return [createHerdrInspectorPlugin(), createGhosttyInspectorPlugin()];
8
+ }
@@ -19,24 +19,13 @@ function parseStringArray(value: unknown): string[] | undefined {
19
19
  return value;
20
20
  }
21
21
 
22
- /**
23
- * Decodes a `--session-roots` argument produced by {@link encodeSessionRoots}.
24
- * Falls back to parsing the value as raw JSON so any externally-launched
25
- * inspector runner (a cached copy, or a manual invocation) that still passes
26
- * the legacy unencoded form keeps working.
27
- */
22
+ /** Decodes a `--session-roots` argument produced by {@link encodeSessionRoots}. */
28
23
  export function decodeSessionRoots(raw: string): string[] {
29
24
  try {
30
25
  const decoded = parseStringArray(JSON.parse(Buffer.from(raw, "base64").toString("utf-8")));
31
26
  if (decoded) return decoded;
32
27
  } catch {
33
- // fall through to legacy raw-JSON parsing below
34
- }
35
- try {
36
- const parsed = parseStringArray(JSON.parse(raw));
37
- if (parsed) return parsed;
38
- } catch {
39
- // fall through to the shared error below
28
+ // Report one stable validation error below.
40
29
  }
41
- throw new Error("--session-roots must be a base64-encoded or raw JSON array of strings.");
30
+ throw new Error("--session-roots must be a base64-encoded JSON array of strings.");
42
31
  }
@@ -0,0 +1,51 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { Details } from "../shared/types.ts";
3
+
4
+ export const INSPECTOR_ACTIONS = ["inspector.open", "inspector.command", "inspector.status", "inspector.close"] as const;
5
+ export type InspectorAction = typeof INSPECTOR_ACTIONS[number];
6
+
7
+ export interface InspectorParams {
8
+ id?: string;
9
+ runId?: string;
10
+ dir?: string;
11
+ index?: number;
12
+ focus?: boolean;
13
+ }
14
+
15
+ export interface InspectorTarget {
16
+ runId: string;
17
+ asyncDir: string;
18
+ index?: number;
19
+ status: {
20
+ cwd?: string;
21
+ state: string;
22
+ steps?: unknown[];
23
+ };
24
+ }
25
+
26
+ export interface InspectorContext {
27
+ cwd: string;
28
+ signal?: AbortSignal;
29
+ env: NodeJS.ProcessEnv;
30
+ now?: () => Date;
31
+ target: InspectorTarget;
32
+ }
33
+
34
+ export interface InspectorLaunch {
35
+ executable: string;
36
+ argv: string[];
37
+ displayCommand: string;
38
+ mission?: { id: string; path: string };
39
+ allowSteer: boolean;
40
+ allowStop: boolean;
41
+ sessionRoots: string[];
42
+ }
43
+
44
+ export interface InspectorPlugin {
45
+ readonly name: string;
46
+ available(context: InspectorContext): Promise<boolean> | boolean;
47
+ owns(context: InspectorContext): boolean;
48
+ open(context: InspectorContext, launch: InspectorLaunch, params: InspectorParams): Promise<AgentToolResult<Details>>;
49
+ status?(context: InspectorContext): Promise<AgentToolResult<Details>>;
50
+ close?(context: InspectorContext): Promise<AgentToolResult<Details>>;
51
+ }
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import type { AgentConfig } from "../agents/agents.ts";
5
+ import { agentDefinitionDigest } from "../shared/launch-contract.ts";
5
6
  import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "../shared/types.ts";
6
7
  import { getAgentDir } from "../shared/utils.ts";
7
8
 
@@ -18,9 +19,13 @@ function defaultSubagentConfigDir(agentDir = defaultAgentDir()): string {
18
19
  const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
19
20
  export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
20
21
  export const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
22
+ const ORCHESTRATOR_TARGET_PLACEHOLDER = "{orchestratorTarget}";
23
+ // The default template must stay session-independent: the child reads the
24
+ // supervisor target from its runtime config, and a prompt that names the
25
+ // parent session would make the launch digest vary per session (#2127).
21
26
  const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the supervisor in normal assistant text.
22
27
 
23
- Use contact_supervisor first. It resolves the supervisor session "{orchestratorTarget}" and run metadata automatically.
28
+ Use contact_supervisor first. It resolves the supervisor session and run metadata automatically.
24
29
  - Need a decision, blocked, approval, or product/API/scope ambiguity: contact_supervisor({ reason: "need_decision", message: "<question>" })
25
30
  - Need structured supervisor input rather than a freeform reply: contact_supervisor({ reason: "interview_request", message: "<what input is needed>", interview: { title: "...", questions: [] } })
26
31
  - After contact_supervisor with reason "need_decision" or "interview_request", stay alive and continue only after the reply arrives. Do not finish your final response with a choose-one question.
@@ -36,6 +41,32 @@ export interface IntercomBridgeState {
36
41
  orchestratorTarget?: string;
37
42
  extensionDir: string;
38
43
  instruction: string;
44
+ /** True when the instruction template names the supervisor session, which ties the child prompt to the parent session. */
45
+ interpolatesOrchestratorTarget: boolean;
46
+ }
47
+
48
+ export type IntercomBridgeConfigValidation =
49
+ | { ok: true; value: IntercomBridgeConfig }
50
+ | { ok: false; error: string };
51
+
52
+ /** Validates untrusted bridge config from descriptors or delegation requests; `label` prefixes each error. */
53
+ export function validateIntercomBridgeConfig({ value, label }: { value: unknown; label: string }): IntercomBridgeConfigValidation {
54
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { ok: false, error: `${label} must be an object.` };
55
+ const bridge = value as Record<string, unknown>;
56
+ for (const field of Object.keys(bridge)) {
57
+ if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") return { ok: false, error: `${label}.${field} is not supported.` };
58
+ }
59
+ if (bridge.mode !== undefined && bridge.mode !== "off" && bridge.mode !== "fork-only" && bridge.mode !== "always") return { ok: false, error: `${label}.mode is invalid.` };
60
+ if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") return { ok: false, error: `${label}.instructionFile must be a string.` };
61
+ if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") return { ok: false, error: `${label}.resultDelivery must be a boolean.` };
62
+ return {
63
+ ok: true,
64
+ value: {
65
+ ...(bridge.mode !== undefined ? { mode: bridge.mode as IntercomBridgeMode } : {}),
66
+ ...(bridge.instructionFile !== undefined ? { instructionFile: bridge.instructionFile as string } : {}),
67
+ ...(bridge.resultDelivery !== undefined ? { resultDelivery: bridge.resultDelivery as boolean } : {}),
68
+ },
69
+ };
39
70
  }
40
71
 
41
72
  export interface IntercomBridgeDiagnostic {
@@ -114,7 +145,7 @@ function resolveInstructionTemplate(instructionFile: string, settingsDir: string
114
145
  }
115
146
 
116
147
  function buildIntercomBridgeInstruction(orchestratorTarget: string, template: string): string {
117
- const instruction = template.replaceAll("{orchestratorTarget}", orchestratorTarget).trim();
148
+ const instruction = template.replaceAll(ORCHESTRATOR_TARGET_PLACEHOLDER, orchestratorTarget).trim();
118
149
  if (instruction.startsWith(INTERCOM_BRIDGE_MARKER)) return instruction;
119
150
  return `${INTERCOM_BRIDGE_MARKER}\n${instruction}`;
120
151
  }
@@ -149,24 +180,34 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
149
180
  const orchestratorTarget = input.orchestratorTarget?.trim();
150
181
  const agentDir = path.resolve(input.agentDir ?? defaultAgentDir());
151
182
  const settingsDir = path.resolve(input.settingsDir ?? defaultSubagentConfigDir(agentDir));
152
- const defaultInstruction = buildIntercomBridgeInstruction(
153
- orchestratorTarget || "{orchestratorTarget}",
154
- DEFAULT_INTERCOM_BRIDGE_TEMPLATE,
155
- );
156
183
  const reason = inactiveReason(mode, input.context, orchestratorTarget);
157
184
  if (reason || !orchestratorTarget) {
158
- return { active: false, mode, resultDelivery: config.resultDelivery, extensionDir: NATIVE_INTERCOM_EXTENSION_DIR, instruction: defaultInstruction };
185
+ return {
186
+ active: false,
187
+ mode,
188
+ resultDelivery: config.resultDelivery,
189
+ extensionDir: NATIVE_INTERCOM_EXTENSION_DIR,
190
+ instruction: buildIntercomBridgeInstruction(ORCHESTRATOR_TARGET_PLACEHOLDER, DEFAULT_INTERCOM_BRIDGE_TEMPLATE),
191
+ interpolatesOrchestratorTarget: false,
192
+ };
159
193
  }
194
+ const template = resolveInstructionTemplate(config.instructionFile, settingsDir);
160
195
  return {
161
196
  active: true,
162
197
  mode,
163
198
  resultDelivery: config.resultDelivery,
164
199
  orchestratorTarget,
165
200
  extensionDir: NATIVE_INTERCOM_EXTENSION_DIR,
166
- instruction: buildIntercomBridgeInstruction(orchestratorTarget, resolveInstructionTemplate(config.instructionFile, settingsDir)),
201
+ instruction: buildIntercomBridgeInstruction(orchestratorTarget, template),
202
+ interpolatesOrchestratorTarget: template.includes(ORCHESTRATOR_TARGET_PLACEHOLDER),
167
203
  };
168
204
  }
169
205
 
206
+ /**
207
+ * Rewrites the launch prompt and tools for an active bridge. The parsed
208
+ * definition digest is captured first so launch identity keeps describing the
209
+ * agent file rather than this runtime overlay.
210
+ */
170
211
  export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomBridgeState): AgentConfig {
171
212
  if (!bridge.active || !bridge.orchestratorTarget) return agent;
172
213
 
@@ -185,6 +226,7 @@ export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomB
185
226
  if (tools === agent.tools && systemPrompt === agent.systemPrompt) return agent;
186
227
  return {
187
228
  ...agent,
229
+ definitionDigest: agentDefinitionDigest(agent),
188
230
  tools,
189
231
  systemPrompt,
190
232
  };
@@ -83,6 +83,8 @@ interface IntercomParams {
83
83
  type SupervisorWatch = (filename: fs.PathLike, listener: fs.WatchListener<string>) => fs.FSWatcher;
84
84
 
85
85
  interface NativeSupervisorChannelDeps {
86
+ /** Owned live/final-drain mailboxes. Only a completed poll retires the snapshot, never a demand probe. */
87
+ getChannelDirs?: () => { dirs: string[]; retire?: () => void };
86
88
  /** Retained scheduled states for the current runtime owner, never foreign owners. */
87
89
  getCurrentOwnerStates?: () => Iterable<SubagentState>;
88
90
  platform?: NodeJS.Platform;
@@ -286,18 +288,18 @@ function parseRequestFile(file: string, channelDir: string): PendingSupervisorRe
286
288
  }
287
289
  }
288
290
 
289
- function listRequestFiles(): Array<{ channelDir: string; file: string }> {
290
- let channelEntries: fs.Dirent[];
291
- try {
292
- channelEntries = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true });
293
- } catch (error) {
294
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
295
- throw error;
291
+ function listRequestFiles(channelDirs?: string[]): Array<{ channelDir: string; file: string }> {
292
+ if (!channelDirs) {
293
+ try {
294
+ channelDirs = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true })
295
+ .filter(entry => entry.isDirectory()).map(entry => path.join(SUPERVISOR_CHANNEL_ROOT, entry.name));
296
+ } catch (error) {
297
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
298
+ throw error;
299
+ }
296
300
  }
297
301
  const files: Array<{ channelDir: string; file: string }> = [];
298
- for (const entry of channelEntries) {
299
- if (!entry.isDirectory()) continue;
300
- const channelDir = path.join(SUPERVISOR_CHANNEL_ROOT, entry.name);
302
+ for (const channelDir of channelDirs) {
301
303
  const requestsDir = path.join(channelDir, REQUESTS_DIR);
302
304
  let requestEntries: fs.Dirent[];
303
305
  try {
@@ -606,9 +608,11 @@ function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, Pendin
606
608
  }
607
609
 
608
610
  export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): {
611
+ registerTools: () => void;
609
612
  start: () => void;
610
613
  activateTransport: () => void;
611
614
  findPendingAsks: (target: { runId: string; agent: string; childIndex: number }) => string[];
615
+ hasPendingRequests: () => boolean;
612
616
  dispose: () => void;
613
617
  pending: Map<string, PendingSupervisorRequest>;
614
618
  getSupervisorRequestState: (event: ControlEvent) => SupervisorRequestState;
@@ -681,10 +685,11 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
681
685
  let started = false;
682
686
  let lastStaleCleanupAt = 0;
683
687
  const platform = deps.platform ?? process.platform;
684
- const useNativeWatcher = () => shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
688
+ const useNativeWatcher = () => !deps.getChannelDirs && shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
685
689
  const hasTransportDemand = () => {
686
690
  if (pending.size > 0) return true;
687
691
  if (state.foregroundControls.size > 0) return true;
692
+ if (deps.getChannelDirs?.().dirs.length) return true;
688
693
  if ([...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running")) return true;
689
694
  for (const ownerState of deps.getCurrentOwnerStates?.() ?? []) {
690
695
  for (const job of ownerState.asyncJobs.values()) {
@@ -699,6 +704,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
699
704
  };
700
705
 
701
706
  const cleanupStaleChannelsIfDue = (): void => {
707
+ if (deps.getChannelDirs) return; // The root owns global retention cleanup.
702
708
  const nowMs = Date.now();
703
709
  if (nowMs - lastStaleCleanupAt < STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS) return;
704
710
  lastStaleCleanupAt = nowMs;
@@ -714,7 +720,8 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
714
720
  // Only display notifications require a live UI context, not request registration.
715
721
  refreshPendingRequests(pending, state, observeRequestLifecycle, runState);
716
722
  const now = Date.now();
717
- for (const { channelDir, file } of listRequestFiles()) {
723
+ const channels = deps.getChannelDirs?.();
724
+ for (const { channelDir, file } of listRequestFiles(channels?.dirs)) {
718
725
  if (seenFiles.has(file)) continue;
719
726
  const request = parseRequestFile(file, channelDir);
720
727
  if (!request || !requestMatchesOwner(request, state)) continue;
@@ -726,11 +733,15 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
726
733
  continue;
727
734
  }
728
735
  seenFiles.add(file);
729
- if (request.expectsReply) {
730
- rememberPendingRequest(request);
731
- pending.set(request.id, request);
732
- markForegroundSupervisorAttention(request, state);
736
+ if (!request.expectsReply) {
737
+ // Progress is already visible through child activity; do not inject a
738
+ // parent message or trigger a parent model turn.
739
+ removeRequestFile(request.requestFile);
740
+ continue;
733
741
  }
742
+ rememberPendingRequest(request);
743
+ pending.set(request.id, request);
744
+ markForegroundSupervisorAttention(request, state);
734
745
  // The ask is already queued above. A sendMessage failure (no UI, stale context) must not
735
746
  // lose it, and must not abort the loop before the remaining asks register.
736
747
  try {
@@ -749,33 +760,28 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
749
760
  ...(request.childTarget ? { childTarget: request.childTarget } : {}),
750
761
  ...(request.interview !== undefined ? { interview: request.interview } : {}),
751
762
  requestBody: request.message,
752
- ...(request.expectsReply ? { replyHint: supervisorReplyHint(request.id) } : {}),
763
+ replyHint: supervisorReplyHint(request.id),
753
764
  },
754
765
  }, { triggerTurn: true });
755
- // sendMessage accepts synchronously; one-way updates stay on disk until it returns.
756
- if (!request.expectsReply) removeRequestFile(request.requestFile);
757
766
  } catch (error) {
758
- // Allow an existing later scan to retry an unaccepted one-way update.
759
- if (!request.expectsReply) seenFiles.delete(file);
760
767
  console.error(`Failed to surface supervisor request ${request.id} as a user turn:`, error);
761
768
  }
762
- if (request.expectsReply) {
763
- (pi as { events?: IntercomEventBus }).events?.emit(INTERCOM_DETACH_REQUEST_EVENT, {
764
- requestId: request.id,
765
- runId: request.runId,
766
- agent: request.agent,
767
- childIndex: request.childIndex,
768
- });
769
- if (pending.has(request.id)) markForegroundSupervisorAttention(request, state);
770
- }
769
+ (pi as { events?: IntercomEventBus }).events?.emit(INTERCOM_DETACH_REQUEST_EVENT, {
770
+ requestId: request.id,
771
+ runId: request.runId,
772
+ agent: request.agent,
773
+ childIndex: request.childIndex,
774
+ });
775
+ if (pending.has(request.id)) markForegroundSupervisorAttention(request, state);
771
776
  }
777
+ channels?.retire?.();
772
778
  };
773
779
 
774
780
  const startPolling = (): void => {
775
781
  if (poller) return;
776
782
  poller = timers.setInterval(() => {
777
783
  poll();
778
- if (!useNativeWatcher() && platform === "darwin" && !hasTransportDemand()) {
784
+ if (!useNativeWatcher() && (platform === "darwin" || deps.getChannelDirs) && !hasTransportDemand()) {
779
785
  if (poller) timers.clearInterval(poller);
780
786
  poller = undefined;
781
787
  }
@@ -830,6 +836,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
830
836
  };
831
837
 
832
838
  return {
839
+ registerTools: registerParentTools,
833
840
  activateTransport: () => {
834
841
  if (!started) return;
835
842
  poll();
@@ -852,11 +859,17 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
852
859
  && requestLifecycle(request, state, now, runState(request)) === "pending" ? [request.id] : [];
853
860
  }).sort();
854
861
  },
862
+ hasPendingRequests: () => {
863
+ if (!started) return false;
864
+ poll();
865
+ return pending.size > 0;
866
+ },
855
867
  start: () => {
856
868
  if (started) return;
857
869
  started = true;
858
870
  registerParentTools();
859
871
  poll();
872
+ if (deps.getChannelDirs) return; // Child polling starts only after a descendant launch.
860
873
  try {
861
874
  fs.mkdirSync(SUPERVISOR_CHANNEL_ROOT, { recursive: true });
862
875
  if (!useNativeWatcher()) {
@@ -5,6 +5,8 @@ export const AUTHORITY_ACTIONS = [
5
5
  "scheduleCreate",
6
6
  "stopRun",
7
7
  "steerRun",
8
+ "inspectorOpen",
9
+ "projectOpen",
8
10
  ] as const;
9
11
 
10
12
  export type AuthorityAction = typeof AUTHORITY_ACTIONS[number];
@@ -18,6 +20,8 @@ const DEFAULT_AUTHORITY_POLICY: Record<AuthorityAction, AuthorityDecision> = {
18
20
  scheduleCreate: "auto",
19
21
  stopRun: "auto",
20
22
  steerRun: "auto",
23
+ inspectorOpen: "auto",
24
+ projectOpen: "confirm",
21
25
  };
22
26
 
23
27
  export function resolveAuthorityDecision(input: {
@@ -20,7 +20,7 @@ export type RecommendedRoleTier = "cheap" | "medium" | "strong";
20
20
  interface ProfileAgentOverride {
21
21
  model?: string;
22
22
  thinking?: string | false;
23
- fallbackModels?: string[] | false;
23
+ machine?: string;
24
24
  }
25
25
 
26
26
  export interface SubagentProfileFile {
@@ -144,10 +144,7 @@ function validateSubagentProfile(filePath: string, parsed: Record<string, unknow
144
144
  if (thinking !== undefined && thinking !== false && typeof thinking !== "string") {
145
145
  throw new Error(`Profile '${filePath}' has invalid thinking for '${name}'; expected a string or false.`);
146
146
  }
147
- const fallbackModels = override.fallbackModels;
148
- if (fallbackModels !== undefined && fallbackModels !== false && (!Array.isArray(fallbackModels) || fallbackModels.some((item) => typeof item !== "string"))) {
149
- throw new Error(`Profile '${filePath}' has invalid fallbackModels for '${name}'; expected an array of strings or false.`);
150
- }
147
+ if ((override as Record<string, unknown>).fallbackModels !== undefined) throw new Error(`Profile '${filePath}' uses removed field fallbackModels for '${name}'; configure one model instead.`);
151
148
  }
152
149
  const disableBuiltins = (subagents as Record<string, unknown>).disableBuiltins;
153
150
  if (disableBuiltins !== undefined && typeof disableBuiltins !== "boolean") {
@@ -489,10 +486,19 @@ export function applySubagentProfile(name: string): { filePath: string; settings
489
486
  : {};
490
487
  // A profile owns the complete agent mapping, but unrelated subagent settings
491
488
  // (notably disableBuiltins, modelScope, watchdog, etc.) survive profile switches.
489
+ // Machine placement is not a model choice, so an existing pin survives a profile switch too.
490
+ const agentOverrides: Record<string, ProfileAgentOverride> = { ...profile.subagents.agentOverrides };
491
+ const existingOverrides = existing.agentOverrides && typeof existing.agentOverrides === "object" && !Array.isArray(existing.agentOverrides)
492
+ ? existing.agentOverrides as Record<string, unknown>
493
+ : {};
494
+ for (const [name, value] of Object.entries(existingOverrides)) {
495
+ const machine = value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>).machine : undefined;
496
+ if (typeof machine === "string" && agentOverrides[name]?.machine === undefined) agentOverrides[name] = { ...agentOverrides[name], machine };
497
+ }
492
498
  settings.subagents = {
493
499
  ...existing,
494
500
  ...profile.subagents,
495
- agentOverrides: profile.subagents.agentOverrides,
501
+ agentOverrides,
496
502
  };
497
503
  writeJsonFile(settingsPath, settings);
498
504
  return { filePath, settingsPath };
@@ -280,6 +280,10 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncSt
280
280
  if (!childStatus) return { state: "retained", reason: `async workflow child ${label} status is missing or unreadable` };
281
281
  if (!terminalState(childStatus.state)) return { state: "retained", reason: `async workflow child ${label} is still ${childStatus.state}` };
282
282
  if (!childStatus.processTerminal?.runnerProcessInstanceId) return { state: "retained", reason: `async workflow child ${label} has no runner process identity` };
283
+ if (childStatus.processTerminal.state === "not-started"
284
+ && childStatus.processTerminal.runId === step.runId
285
+ && typeof childStatus.error === "string"
286
+ && childStatus.error) continue;
283
287
  const proof = readProcessTerminal(childDir, {
284
288
  runId: step.runId,
285
289
  runnerProcessInstanceId: childStatus.processTerminal.runnerProcessInstanceId,
@@ -76,7 +76,7 @@ export function releaseActiveRunIndex(asyncDir: string): void {
76
76
  releaseToolCallAliases(asyncDir);
77
77
  }
78
78
 
79
- export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"], toolCallId?: string, options: { retryCapacityErrors?: boolean } = {}): void {
79
+ export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"], toolCallId?: string, options: { retryCapacityErrors?: boolean; terminalIndexBeforeRelease?: boolean } = {}): void {
80
80
  const marker = markerPath(asyncDir);
81
81
  if (isActiveAsyncState(state)) {
82
82
  fs.mkdirSync(path.dirname(marker), { recursive: true });
@@ -94,6 +94,22 @@ export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state
94
94
  }
95
95
  return;
96
96
  }
97
+ if (options.terminalIndexBeforeRelease) {
98
+ fs.mkdirSync(path.dirname(marker), { recursive: true });
99
+ fs.writeFileSync(marker, "", { flag: "a" });
100
+ const status = readStatus(asyncDir);
101
+ if (status?.state === state) {
102
+ try {
103
+ updateTerminalRunIndex(asyncDir, status);
104
+ } catch (error) {
105
+ if (options.retryCapacityErrors) throw error;
106
+ console.error(`Failed to write async terminal-run index for '${asyncDir}':`, error);
107
+ return;
108
+ }
109
+ }
110
+ releaseActiveRunIndex(asyncDir);
111
+ return;
112
+ }
97
113
  releaseActiveRunIndex(asyncDir);
98
114
  const status = readStatus(asyncDir);
99
115
  if (status && status.state === state) {