pi-subagents 0.66.0 → 0.67.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 (115) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +4 -3
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +6 -3
  6. package/docs/configuration.md +7 -5
  7. package/docs/extension-api.md +33 -18
  8. package/docs/missions.md +8 -0
  9. package/docs/models.md +1 -1
  10. package/docs/observability.md +4 -4
  11. package/docs/standalone-background.md +49 -0
  12. package/docs/tool-reference.md +18 -8
  13. package/docs/watchdog.md +35 -4
  14. package/docs/workflows.md +26 -12
  15. package/inspector-runner.mjs +2 -2
  16. package/package.json +1 -1
  17. package/prompts/parallel-review.md +1 -1
  18. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  19. package/skills/pi-subagents/SKILL.md +14 -0
  20. package/skills/pi-subagents/references/execution-controls.md +7 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  22. package/src/agents/advertised-agent-prompt.ts +34 -3
  23. package/src/agents/agents.ts +6 -0
  24. package/src/agents/builtin-names.ts +1 -0
  25. package/src/api/delegation.ts +4 -0
  26. package/src/api/preflight.ts +76 -45
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +63 -4
  29. package/src/extension/index.ts +20 -8
  30. package/src/extension/public-execution.ts +4 -2
  31. package/src/extension/rpc.ts +4 -0
  32. package/src/extension/schemas.ts +67 -78
  33. package/src/extension/tool-description.ts +29 -82
  34. package/src/inspectors/actions.ts +148 -0
  35. package/src/inspectors/ghostty/actions.ts +74 -0
  36. package/src/inspectors/ghostty/plugin.ts +17 -0
  37. package/src/inspectors/herdr/actions.ts +99 -179
  38. package/src/inspectors/herdr/plugin.ts +20 -0
  39. package/src/inspectors/herdr/project-panes.ts +1 -1
  40. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  41. package/src/inspectors/plugins.ts +8 -0
  42. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  43. package/src/inspectors/types.ts +51 -0
  44. package/src/intercom/intercom-bridge.ts +50 -8
  45. package/src/intercom/native-supervisor-channel.ts +22 -13
  46. package/src/runs/background/active-async-capacity.ts +4 -0
  47. package/src/runs/background/async-execution.ts +45 -56
  48. package/src/runs/background/async-resume.ts +5 -9
  49. package/src/runs/background/auto-drain.ts +6 -3
  50. package/src/runs/background/binary-bootstrap.ts +33 -0
  51. package/src/runs/background/fleet-view.ts +30 -2
  52. package/src/runs/background/notify.ts +31 -1
  53. package/src/runs/background/owned-process-tree.ts +29 -2
  54. package/src/runs/background/run-child-session.ts +61 -4
  55. package/src/runs/background/run-status.ts +3 -0
  56. package/src/runs/background/runner-aliases.ts +11 -3
  57. package/src/runs/background/runner-child-launch.ts +2 -0
  58. package/src/runs/background/runner-child-sessions.ts +5 -4
  59. package/src/runs/background/scheduled-runs.ts +40 -13
  60. package/src/runs/background/steering.ts +20 -2
  61. package/src/runs/background/subagent-runner.ts +47 -31
  62. package/src/runs/background/subagent-wait.ts +51 -8
  63. package/src/runs/background/wait-tool.ts +1 -1
  64. package/src/runs/foreground/async-steering-action.ts +18 -7
  65. package/src/runs/foreground/execution.ts +44 -31
  66. package/src/runs/foreground/prompt-audit.ts +3 -1
  67. package/src/runs/foreground/subagent-executor.ts +110 -100
  68. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  69. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  70. package/src/runs/shared/acceptance.ts +5 -2
  71. package/src/runs/shared/async-status-projection.ts +4 -0
  72. package/src/runs/shared/capability-ceiling.ts +2 -0
  73. package/src/runs/shared/child-hooks.ts +25 -10
  74. package/src/runs/shared/child-launch.ts +12 -2
  75. package/src/runs/shared/child-lifecycle.ts +6 -3
  76. package/src/runs/shared/child-runtime-config.ts +3 -1
  77. package/src/runs/shared/child-session.ts +33 -2
  78. package/src/runs/shared/child-tool-plan.ts +122 -3
  79. package/src/runs/shared/completion-guard.ts +5 -3
  80. package/src/runs/shared/effective-system-prompt.ts +33 -0
  81. package/src/runs/shared/external-cli-runner.ts +9 -7
  82. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  83. package/src/runs/shared/model-fallback.ts +2 -0
  84. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  85. package/src/runs/shared/pi-spawn.ts +10 -0
  86. package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
  87. package/src/runs/shared/task-intent.ts +46 -13
  88. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  89. package/src/runs/shared/worktree.ts +42 -12
  90. package/src/shared/fork-context.ts +15 -72
  91. package/src/shared/launch-contract.ts +65 -2
  92. package/src/shared/opencode-session-headers.ts +30 -0
  93. package/src/shared/types.ts +4 -1
  94. package/src/slash/delegation-adapters.ts +3 -1
  95. package/src/slash/delegation-request.ts +14 -0
  96. package/src/slash/slash-commands.ts +2 -1
  97. package/src/slash/subagents-admin.ts +11 -4
  98. package/src/tui/fleet-status.ts +164 -19
  99. package/src/tui/fleet.ts +16 -14
  100. package/src/tui/render.ts +149 -28
  101. package/src/watchdog/child-status.ts +8 -0
  102. package/src/watchdog/model-selection.ts +20 -0
  103. package/src/watchdog/permission-arbiter.ts +3 -1
  104. package/src/watchdog/register-child.ts +1 -0
  105. package/src/watchdog/register-main.ts +31 -27
  106. package/src/watchdog/review.ts +132 -67
  107. package/src/watchdog/runtime.ts +82 -20
  108. package/src/watchdog/scope.ts +1 -1
  109. package/src/watchdog/settings.ts +9 -3
  110. package/src/watchdog/tool-actions.ts +13 -12
  111. package/src/watchdog/turn-delta.ts +23 -0
  112. package/src/watchdog/types.ts +4 -0
  113. package/src/workflows/scripted-workflow.ts +237 -7
  114. package/src/workflows/workflow-checklist.ts +2 -2
  115. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -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,6 +608,7 @@ 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[];
@@ -681,10 +684,11 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
681
684
  let started = false;
682
685
  let lastStaleCleanupAt = 0;
683
686
  const platform = deps.platform ?? process.platform;
684
- const useNativeWatcher = () => shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
687
+ const useNativeWatcher = () => !deps.getChannelDirs && shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
685
688
  const hasTransportDemand = () => {
686
689
  if (pending.size > 0) return true;
687
690
  if (state.foregroundControls.size > 0) return true;
691
+ if (deps.getChannelDirs?.().dirs.length) return true;
688
692
  if ([...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running")) return true;
689
693
  for (const ownerState of deps.getCurrentOwnerStates?.() ?? []) {
690
694
  for (const job of ownerState.asyncJobs.values()) {
@@ -699,6 +703,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
699
703
  };
700
704
 
701
705
  const cleanupStaleChannelsIfDue = (): void => {
706
+ if (deps.getChannelDirs) return; // The root owns global retention cleanup.
702
707
  const nowMs = Date.now();
703
708
  if (nowMs - lastStaleCleanupAt < STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS) return;
704
709
  lastStaleCleanupAt = nowMs;
@@ -714,7 +719,8 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
714
719
  // Only display notifications require a live UI context, not request registration.
715
720
  refreshPendingRequests(pending, state, observeRequestLifecycle, runState);
716
721
  const now = Date.now();
717
- for (const { channelDir, file } of listRequestFiles()) {
722
+ const channels = deps.getChannelDirs?.();
723
+ for (const { channelDir, file } of listRequestFiles(channels?.dirs)) {
718
724
  if (seenFiles.has(file)) continue;
719
725
  const request = parseRequestFile(file, channelDir);
720
726
  if (!request || !requestMatchesOwner(request, state)) continue;
@@ -769,13 +775,14 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
769
775
  if (pending.has(request.id)) markForegroundSupervisorAttention(request, state);
770
776
  }
771
777
  }
778
+ channels?.retire?.();
772
779
  };
773
780
 
774
781
  const startPolling = (): void => {
775
782
  if (poller) return;
776
783
  poller = timers.setInterval(() => {
777
784
  poll();
778
- if (!useNativeWatcher() && platform === "darwin" && !hasTransportDemand()) {
785
+ if (!useNativeWatcher() && (platform === "darwin" || deps.getChannelDirs) && !hasTransportDemand()) {
779
786
  if (poller) timers.clearInterval(poller);
780
787
  poller = undefined;
781
788
  }
@@ -830,6 +837,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
830
837
  };
831
838
 
832
839
  return {
840
+ registerTools: registerParentTools,
833
841
  activateTransport: () => {
834
842
  if (!started) return;
835
843
  poll();
@@ -857,6 +865,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
857
865
  started = true;
858
866
  registerParentTools();
859
867
  poll();
868
+ if (deps.getChannelDirs) return; // Child polling starts only after a descendant launch.
860
869
  try {
861
870
  fs.mkdirSync(SUPERVISOR_CHANNEL_ROOT, { recursive: true });
862
871
  if (!useNativeWatcher()) {
@@ -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,
@@ -10,23 +10,22 @@ import { fileURLToPath } from "node:url";
10
10
  import { createRequire } from "node:module";
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
13
- import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
14
13
  import { createAtomicJsonWriter, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
14
+ import { buildEffectiveSystemPrompt } from "../shared/effective-system-prompt.ts";
15
15
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
16
16
  import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
17
- import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
18
- import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
17
+ import { applyThinkingSuffix, getHostBuiltinToolNames, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
18
+ import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
19
19
  import { applyWatchdogLaunchRules, sendRuleViolationWarning } from "../../watchdog/rules.ts";
20
20
  import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadInstructionPaths, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
21
21
  import type { RunnerStep } from "../shared/parallel-utils.ts";
22
22
  import type { ContextMode } from "../shared/context-mode.ts";
23
- import { PI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
23
+ import { PI_CODING_AGENT_PACKAGE, resolveBunPiExecutable, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
24
24
  import { JITI_ALIAS_ENV, resolveHostPeerAliases } from "./runner-aliases.ts";
25
25
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
26
26
  import { resolveNodeExecutable } from "../../shared/node-executable.ts";
27
27
  import { backgroundProcessOptions } from "../shared/background-process-options.ts";
28
- import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
29
- import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
28
+ import { normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
30
29
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
31
30
  import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelOrigin, resolveSubagentModelOverride, type AvailableModelInfo, type ModelOrigin, type ParentModel } from "../shared/model-fallback.ts";
32
31
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
@@ -80,7 +79,7 @@ import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
80
79
  import { statusStepDescription } from "./chain-append.ts";
81
80
  import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
82
81
  import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
83
- import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
82
+ import { resolveLaunchBinding } from "../../shared/launch-contract.ts";
84
83
  import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
85
84
  import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
86
85
  import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
@@ -542,15 +541,20 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
542
541
  const cwdError = preflightLaunchCwd(requestedCwd, cwd);
543
542
  if (cwdError) return { error: cwdError };
544
543
 
545
- if (!jitiCliPath) {
544
+ // The compiled host exposes its SDK only through Pi's extension loader.
545
+ const binaryHost = resolveBunPiExecutable();
546
+ const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
547
+ const bootstrap = path.join(path.dirname(runner), "binary-bootstrap.ts");
548
+ if (binaryHost && !fs.existsSync(bootstrap)) return { error: `Background runner bootstrap not found: ${bootstrap}` };
549
+ if (!binaryHost && !jitiCliPath) {
546
550
  return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
547
551
  }
548
- if (!piPackageRoot) {
549
- return { error: `Background children require pi installed as the npm package (${PI_CODING_AGENT_PACKAGE}); a standalone pi binary has no package directory, so the async runner cannot create child sessions. Run this child in the foreground (async: false) or install pi from npm.` };
552
+ if (!binaryHost && !piPackageRoot) {
553
+ return { error: `Background children require a supported standalone Pi host or the installed npm package (${PI_CODING_AGENT_PACKAGE}); neither is available.` };
550
554
  }
551
- const hostPeerAliases = resolveHostPeerAliases(piPackageRoot);
555
+ const hostPeerAliases = !binaryHost && piPackageRoot ? resolveHostPeerAliases(piPackageRoot) : { aliases: {}, missing: [] };
552
556
  if (hostPeerAliases.missing.length > 0) {
553
- return { error: `Background children require pi installed as the npm package (${PI_CODING_AGENT_PACKAGE}) with its dependencies; ${piPackageRoot} does not provide ${hostPeerAliases.missing.join(", ")}, so the async runner cannot create child sessions. A standalone pi binary cannot run background children.` };
557
+ return { error: `Background children require the host npm package (${PI_CODING_AGENT_PACKAGE}) with its dependencies; ${piPackageRoot} does not provide ${hostPeerAliases.missing.join(", ")}.` };
554
558
  }
555
559
 
556
560
  fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
@@ -560,8 +564,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
560
564
  const launchBarrierToken = hasRevivalLease ? undefined : runnerProcessInstanceId;
561
565
  const launchConfig = { ...cfg, runnerProcessInstanceId, ...(launchBarrierToken ? { launchBarrierToken } : {}) };
562
566
  writePrivateAtomicJson(cfgPath, launchConfig);
563
- const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
564
- const nodeCommand = resolveNodeExecutable();
567
+ const command = binaryHost ?? resolveNodeExecutable();
565
568
  const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; completionOwnerId?: unknown; revivalLease?: unknown };
566
569
  const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
567
570
  const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
@@ -587,17 +590,23 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
587
590
  stdoutFd = fs.openSync(logPaths.stdoutPath, "a");
588
591
  stderrFd = fs.openSync(logPaths.stderrPath, "a");
589
592
  }
590
- const preload = hostPeerAliases.supplemental.length > 0
591
- ? ["--import", new URL("../../../runner-server-preload.mjs", import.meta.url).href]
593
+ const preload = Object.keys(hostPeerAliases.aliases).length > 0
594
+ ? ["--import", new URL("../../../runner-peer-preload.mjs", import.meta.url).href]
592
595
  : [];
593
- const proc = spawn(nodeCommand, [...preload, jitiCliPath, runner, cfgPath], {
596
+ const args = binaryHost
597
+ ? ["--no-extensions", "--no-skills", "--no-prompt-templates", "--no-session", "--mode", "rpc", "--extension", bootstrap]
598
+ : [...preload, jitiCliPath!, runner, cfgPath];
599
+ const proc = spawn(command, args, {
594
600
  cwd,
595
601
  ...backgroundProcessOptions(),
596
602
  stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
597
603
  env: {
598
604
  ...omitExtensionBindingsEnv(process.env),
599
- [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot,
600
- [JITI_ALIAS_ENV]: JSON.stringify(hostPeerAliases.aliases),
605
+ [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: binaryHost ? undefined : piPackageRoot,
606
+ // npm must override inherited bundled layouts (#2071); binaries retain release assets.
607
+ PI_PACKAGE_DIR: binaryHost ? process.env.PI_PACKAGE_DIR : piPackageRoot,
608
+ [JITI_ALIAS_ENV]: binaryHost ? undefined : JSON.stringify(hostPeerAliases.aliases),
609
+ PI_SUBAGENT_RUNNER_CONFIG: binaryHost ? cfgPath : undefined,
601
610
  },
602
611
  });
603
612
  closeFd(stdoutFd);
@@ -891,22 +900,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
891
900
  );
892
901
  if (missingSkills.includes("pi-subagents")) throw new UnavailableSubagentSkillError(UNAVAILABLE_SUBAGENT_SKILL_ERROR);
893
902
 
894
- let systemPrompt = a.systemPrompt?.trim() ?? "";
895
- if (resolvedSkills.length > 0) {
896
- const injection = buildSkillInjection(resolvedSkills);
897
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
898
- }
899
- const memoryInjection = buildAgentMemoryInjection(a, stepCwd);
900
- if (memoryInjection) {
901
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
902
- }
903
- systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: stepCwd, agentName: a.name });
903
+ // A namespaced parallel output is injected by the runner, not the prompt.
904
+ const systemPrompt = buildEffectiveSystemPrompt({ agent: a, resolvedSkills, cwd: stepCwd, ...(!namespaceOutputPath && outputPath ? { outputPath } : {}) });
904
905
 
905
906
  const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false, undefined, readExistenceCwd);
906
907
  const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
907
908
  if (behavior.progress) progressInstructionCreated = true;
908
909
  const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
909
- if (!namespaceOutputPath) systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, a);
910
910
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
911
911
  if (validationError) throw new AsyncStartValidationError(validationError);
912
912
  let taskTemplate = s.task ?? "{previous}";
@@ -963,6 +963,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
963
963
  const launchRuleError = applyWatchdogLaunchRules({ cwd: stepCwd, agent: a.name, model: modelCandidates[0] ?? model, warn: (violation) => sendRuleViolationWarning(ctx.pi, violation) });
964
964
  if (launchRuleError) throw new AsyncStartValidationError(launchRuleError);
965
965
  const fast = s.fast ?? params.fast ?? a.fast;
966
+ const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
966
967
  const toolPlan = resolvePiLaunchToolPlan({
967
968
  tools: a.tools,
968
969
  excludeTools: a.excludeTools,
@@ -981,6 +982,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
981
982
  agentName: a.name,
982
983
  permissionRules,
983
984
  runtimeSnapshotHost: ctx.pi,
985
+ hostAvailableBuiltins,
984
986
  });
985
987
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
986
988
  if (externalRunner && permissionRules) {
@@ -1377,6 +1379,7 @@ export function executeAsyncChain(
1377
1379
  deadlineAt,
1378
1380
  globalConcurrencyLimit: params.globalConcurrencyLimit,
1379
1381
  runFanoutBudget,
1382
+ hostAvailableBuiltins: getHostBuiltinToolNames(ctx.pi),
1380
1383
  workflowGraph,
1381
1384
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1382
1385
  ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
@@ -1636,16 +1639,6 @@ export function executeAsyncSingle(
1636
1639
  agentConfig.filePath ? path.dirname(agentConfig.filePath) : runnerCwd,
1637
1640
  );
1638
1641
  if (missingSkills.includes("pi-subagents")) return formatAsyncStartError("single", UNAVAILABLE_SUBAGENT_SKILL_ERROR);
1639
- let systemPrompt = agentConfig.systemPrompt?.trim() ?? "";
1640
- if (resolvedSkills.length > 0) {
1641
- const injection = buildSkillInjection(resolvedSkills);
1642
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
1643
- }
1644
- const memoryInjection = buildAgentMemoryInjection(agentConfig, runnerCwd);
1645
- if (memoryInjection) {
1646
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
1647
- }
1648
- systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: runnerCwd, agentName: agentConfig.name });
1649
1642
 
1650
1643
  const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
1651
1644
  const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
@@ -1668,7 +1661,7 @@ export function executeAsyncSingle(
1668
1661
 
1669
1662
  const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
1670
1663
  const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, instructionCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
1671
- systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, agentConfig);
1664
+ const systemPrompt = buildEffectiveSystemPrompt({ agent: agentConfig, resolvedSkills, cwd: runnerCwd, ...(outputPath ? { outputPath } : {}) });
1672
1665
  const outputMode = params.outputMode ?? agentConfig.outputMode ?? "inline";
1673
1666
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
1674
1667
  if (validationError) return formatAsyncStartError("single", validationError);
@@ -1759,6 +1752,7 @@ export function executeAsyncSingle(
1759
1752
  return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1760
1753
  }
1761
1754
  }
1755
+ const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
1762
1756
  const toolPlan = resolvePiLaunchToolPlan({
1763
1757
  tools: agentConfig.tools,
1764
1758
  excludeTools: agentConfig.excludeTools,
@@ -1777,6 +1771,7 @@ export function executeAsyncSingle(
1777
1771
  agentName: agentConfig.name,
1778
1772
  permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
1779
1773
  runtimeSnapshotHost: ctx.pi,
1774
+ hostAvailableBuiltins,
1780
1775
  });
1781
1776
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1782
1777
  if (!externalRunner) {
@@ -1793,24 +1788,17 @@ export function executeAsyncSingle(
1793
1788
  });
1794
1789
  if (contractError) return formatAsyncStartError("single", contractError);
1795
1790
  }
1796
- const launchContractDigest = launchBindingDigest({
1797
- definitionDigest: agentDefinitionDigest(agentConfig),
1791
+ const fast = params.fast ?? agentConfig.fast;
1792
+ const launchThinking = resolveEffectiveThinking(model, effectiveThinking);
1793
+ const { definitionDigest, launchContractDigest } = resolveLaunchBinding({
1794
+ agent: agentConfig,
1798
1795
  task,
1799
- ...(model ? { model } : {}),
1800
1796
  modelCandidates,
1801
- ...((params.fast ?? agentConfig.fast) !== undefined ? { fast: params.fast ?? agentConfig.fast } : {}),
1802
- ...(resolveEffectiveThinking(model, effectiveThinking) ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1803
- ...(thinkingCeiling ? { thinkingCeiling } : {}),
1797
+ ...(fast !== undefined ? { fast } : {}),
1798
+ ...(launchThinking ? { thinking: launchThinking } : {}),
1804
1799
  systemPrompt,
1805
- systemPromptMode: agentConfig.systemPromptMode,
1806
- inheritProjectContext: agentConfig.inheritProjectContext,
1807
- inheritGlobalContext: agentConfig.inheritGlobalContext,
1808
- inheritSkills: agentConfig.inheritSkills,
1809
1800
  skills: resolvedSkills.map((skill) => skill.name),
1810
- tools: toolPlan.effectiveToolAllowlist,
1811
- ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
1812
- extensions: toolPlan.extensionArgs,
1813
- mcpDirectTools: toolPlan.effectiveMcpTools,
1801
+ toolPlan,
1814
1802
  ...(outputPath ? { outputPath } : {}),
1815
1803
  outputMode,
1816
1804
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
@@ -1939,7 +1927,7 @@ export function executeAsyncSingle(
1939
1927
  waitToolEnabled: params.waitToolEnabled,
1940
1928
  waitToolDefaultTimeoutMs: params.waitToolDefaultTimeoutMs,
1941
1929
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
1942
- definitionDigest: agentDefinitionDigest(agentConfig),
1930
+ definitionDigest,
1943
1931
  launchBindingTask: task,
1944
1932
  launchContractDigest,
1945
1933
  ...(extensionBindings ? { extensionBindings } : {}),
@@ -1988,6 +1976,7 @@ export function executeAsyncSingle(
1988
1976
  launchContractDigest,
1989
1977
  launchResolvedExtensions,
1990
1978
  runFanoutBudget,
1979
+ hostAvailableBuiltins,
1991
1980
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1992
1981
  ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1993
1982
  ...(lane ? { lane } : {}),
@@ -15,6 +15,7 @@ import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/paral
15
15
  import { normalizeWorktreeBaseRef } from "../shared/worktree.ts";
16
16
  import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
17
17
  import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
18
+ import { validateIntercomBridgeConfig } from "../../intercom/intercom-bridge.ts";
18
19
  import { validateModelResponseAliases } from "../../shared/model-response-aliases.ts";
19
20
 
20
21
  export interface AsyncResumeParams {
@@ -318,7 +319,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
318
319
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
319
320
  const parsed = value as Record<string, unknown>;
320
321
  const allowedFields = new Set([
321
- "modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
322
+ "modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "fast", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
322
323
  "subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
323
324
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
324
325
  "artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
@@ -352,6 +353,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
352
353
  if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
353
354
  if (parsed.context !== undefined && parsed.context !== "fresh" && parsed.context !== "fork") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': context is invalid.`);
354
355
  if (parsed.modelOverrideFromParent !== undefined && typeof parsed.modelOverrideFromParent !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOverrideFromParent must be a boolean.`);
356
+ if (parsed.fast !== undefined && typeof parsed.fast !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': fast must be a boolean.`);
355
357
  if (parsed.modelOrigin !== undefined && parsed.modelOrigin !== "explicit" && parsed.modelOrigin !== "inherited" && parsed.modelOrigin !== "configured") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOrigin must be 'explicit', 'inherited', or 'configured'.`);
356
358
  if (parsed.modelOrigin === undefined && parsed.model !== undefined) parsed.modelOrigin = parsed.modelOverrideFromParent ? "inherited" : "configured";
357
359
  for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
@@ -411,14 +413,8 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
411
413
  if (!Number.isInteger(artifact.cleanupDays) || (artifact.cleanupDays as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.cleanupDays must be a non-negative integer.`);
412
414
  }
413
415
  if (parsed.intercomBridge !== undefined) {
414
- if (!parsed.intercomBridge || typeof parsed.intercomBridge !== "object" || Array.isArray(parsed.intercomBridge)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge must be an object.`);
415
- const bridge = parsed.intercomBridge as Record<string, unknown>;
416
- for (const field of Object.keys(bridge)) {
417
- if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.${field} is not supported.`);
418
- }
419
- if (bridge.mode !== undefined && bridge.mode !== "off" && bridge.mode !== "fork-only" && bridge.mode !== "always") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.mode is invalid.`);
420
- if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.instructionFile must be a string.`);
421
- if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.resultDelivery must be a boolean.`);
416
+ const bridge = validateIntercomBridgeConfig({ value: parsed.intercomBridge, label: "intercomBridge" });
417
+ if (!bridge.ok) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${bridge.error}`);
422
418
  }
423
419
  if (parsed.controlConfig !== undefined) {
424
420
  if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
@@ -24,14 +24,17 @@ function resultText(value: AgentToolResult<Details>): string {
24
24
  return value.content.map((part) => part.type === "text" ? part.text : "").join(" ").trim();
25
25
  }
26
26
 
27
- function hasOutstandingWork(sessionId: string, nowMs: number, observation?: ReadonlyDrainObservation): boolean {
27
+ function hasOutstandingWork(state: SubagentState, sessionId: string, nowMs: number, observation?: ReadonlyDrainObservation): boolean {
28
28
  const asyncRuns = listAsyncRuns(DIRS.async, {
29
29
  states: ["queued", "running"],
30
30
  sessionId,
31
31
  resultsDir: DIRS.results,
32
32
  now: () => nowMs,
33
33
  }, observation?.status);
34
- return asyncRuns.length > 0 || snapshotBackgroundWork(sessionId, nowMs).items.length > 0;
34
+ const detachedForeground = [...(state.foregroundRuns?.values() ?? [])].some((run) =>
35
+ run.sessionId === sessionId && run.children.some((child) => child.status === "detached")
36
+ );
37
+ return asyncRuns.length > 0 || snapshotBackgroundWork(sessionId, nowMs).items.length > 0 || detachedForeground;
35
38
  }
36
39
 
37
40
  /** Drain all work owned by the current headless session, including work added while draining. */
@@ -44,7 +47,7 @@ export async function drainOutstandingWork(deps: AutoDrainDeps, observation?: Re
44
47
  const timeoutMs = deps.timeoutMs ?? DEFAULT_AUTO_DRAIN_TIMEOUT_MS;
45
48
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Auto-drain timeoutMs must be a positive finite number.");
46
49
  const deadlineAt = now() + timeoutMs;
47
- const hasWork = deps.hasWork ?? (observation ? (id: string, time: number) => hasOutstandingWork(id, time, observation) : hasOutstandingWork);
50
+ const hasWork = deps.hasWork ?? ((id: string, time: number) => hasOutstandingWork(deps.state, id, time, observation));
48
51
  const wait = deps.wait ?? waitForSubagents;
49
52
 
50
53
  while (true) {
@@ -0,0 +1,33 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as sdk from "@earendil-works/pi-coding-agent";
4
+ import { runConfiguredSubagent, type SubagentRunConfig } from "./subagent-runner.ts";
5
+
6
+ /**
7
+ * Pi's extension loader supplies the embedded SDK. Bare Bun/Node cannot replace
8
+ * it: an apparent import success can be an auto-installed, different SDK.
9
+ * Await the configured runner inside extension initialization so the outer Pi
10
+ * session/RPC loop never starts. Exit only after shared disposal/lease release.
11
+ * Derived from xz-dev's PR #2049, commit 910807bfefcf9ee41d73fa25ec86dcd75ab8f4b2.
12
+ */
13
+ export default async function runBinaryBootstrap(): Promise<never> {
14
+ const configPath = process.env.PI_SUBAGENT_RUNNER_CONFIG;
15
+ delete process.env.PI_SUBAGENT_RUNNER_CONFIG;
16
+ try {
17
+ if (!configPath || !path.isAbsolute(configPath)) throw new Error("Missing absolute PI_SUBAGENT_RUNNER_CONFIG path");
18
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as SubagentRunConfig;
19
+ if (!config || typeof config.id !== "string" || typeof config.asyncDir !== "string" || !Array.isArray(config.steps)) {
20
+ throw new Error("Invalid binary runner configuration");
21
+ }
22
+ try {
23
+ fs.unlinkSync(configPath);
24
+ } catch {
25
+ // Temp-config cleanup is best effort, as in the Node entrypoint.
26
+ }
27
+ await runConfiguredSubagent(config, { loadPiCodingAgent: async () => sdk });
28
+ process.exit(0);
29
+ } catch (error) {
30
+ console.error("Subagent binary runner error:", error);
31
+ process.exit(1);
32
+ }
33
+ }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
- import { safeTerminalText } from "../../shared/display-text.ts";
4
+ import { safeTerminalText, truncateDisplayText } from "../../shared/display-text.ts";
5
5
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
6
6
  import { formatActivityLabel } from "../../shared/status-format.ts";
7
7
  import {
@@ -23,6 +23,10 @@ import { isTrustedRecordedSessionFile } from "../../shared/session-file-trust.ts
23
23
  const DEFAULT_TRANSCRIPT_LINES = 80;
24
24
  const MAX_TRANSCRIPT_LINES = 500;
25
25
  const TRANSCRIPT_TAIL_BYTES = 256 * 1024;
26
+ const MAX_TRANSCRIPT_LINE_CHARS = 2000;
27
+ const MAX_TRANSCRIPT_BODY_BYTES = 32 * 1024;
28
+ const TRANSCRIPT_LINE_OMISSION = "… [content omitted]";
29
+ const TRANSCRIPT_BODY_OMISSION = " … [earlier lines omitted]";
26
30
 
27
31
  type ForegroundControl = SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
28
32
  type ForegroundRun = NonNullable<SubagentState["foregroundRuns"]> extends Map<string, infer T> ? T : never;
@@ -494,7 +498,31 @@ function appendTranscriptBody(lines: string[], sourceLabel: string, sourceLines:
494
498
  lines.push(" (no transcript lines available yet)");
495
499
  return;
496
500
  }
497
- for (const line of sourceLines) lines.push(` ${line}`);
501
+ // Bound the rendered body, not the raw input: terminal escaping can expand it.
502
+ // Keep line prefixes (roles/tool names), then prefer whole recent lines. The
503
+ // byte budget includes indentation, separators and omission markers, but not
504
+ // the source label, run metadata, warnings or artifact paths above the body.
505
+ const body: string[] = [];
506
+ let bytes = 0;
507
+ for (let index = sourceLines.length - 1; index >= 0; index--) {
508
+ // Preserve assembled-line binary detection (including indentation). A
509
+ // binary placeholder replaces the whole line and must not be reindented.
510
+ const safe = safeTerminalText(` ${sourceLines[index]!}`);
511
+ const lineLimit = MAX_TRANSCRIPT_LINE_CHARS + 2;
512
+ const line = safe.length <= lineLimit
513
+ ? safe
514
+ : truncateDisplayText(safe, lineLimit - TRANSCRIPT_LINE_OMISSION.length) + TRANSCRIPT_LINE_OMISSION;
515
+ const size = Buffer.byteLength(line) + 1;
516
+ if (bytes + size > MAX_TRANSCRIPT_BODY_BYTES) {
517
+ const markerBytes = Buffer.byteLength(TRANSCRIPT_BODY_OMISSION) + 1;
518
+ while (bytes + markerBytes > MAX_TRANSCRIPT_BODY_BYTES) bytes -= Buffer.byteLength(body.pop()!) + 1;
519
+ body.push(TRANSCRIPT_BODY_OMISSION);
520
+ break;
521
+ }
522
+ body.push(line);
523
+ bytes += size;
524
+ }
525
+ lines.push(...body.reverse());
498
526
  }
499
527
 
500
528
  export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string, options: TranscriptOptions = {}): string {