pi-subagents 0.67.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 (120) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +1 -1
  3. package/docs/agents.md +37 -12
  4. package/docs/configuration.md +61 -19
  5. package/docs/extension-api.md +5 -1
  6. package/docs/missions.md +2 -2
  7. package/docs/models.md +11 -79
  8. package/docs/observability.md +18 -8
  9. package/docs/standalone-background.md +13 -3
  10. package/docs/tool-reference.md +15 -12
  11. package/docs/watchdog.md +10 -12
  12. package/docs/workflows.md +11 -1
  13. package/index.ts +5 -2
  14. package/package.json +4 -2
  15. package/runner-peer-loader.mjs +24 -0
  16. package/runner-peer-preload.mjs +25 -11
  17. package/skills/pi-subagents/SKILL.md +18 -21
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  19. package/skills/pi-subagents/references/execution-controls.md +4 -4
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  21. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  22. package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
  23. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  24. package/src/agents/agent-management.ts +57 -58
  25. package/src/agents/agent-serializer.ts +4 -3
  26. package/src/agents/agents.ts +184 -71
  27. package/src/agents/chain-serializer.ts +5 -0
  28. package/src/agents/runtime-agent-registry.ts +7 -6
  29. package/src/api/preflight.ts +20 -16
  30. package/src/api/required-child-extensions.ts +6 -0
  31. package/src/extension/config.ts +10 -37
  32. package/src/extension/fanout-child.ts +3 -0
  33. package/src/extension/herdr-pi-bridge.ts +160 -0
  34. package/src/extension/index.ts +42 -31
  35. package/src/extension/public-execution.ts +3 -3
  36. package/src/extension/schemas.ts +16 -5
  37. package/src/extension/tool-description.ts +8 -7
  38. package/src/intercom/native-supervisor-channel.ts +22 -18
  39. package/src/policy/authority.ts +4 -0
  40. package/src/profiles/profiles.ts +12 -6
  41. package/src/runs/background/active-run-index.ts +17 -1
  42. package/src/runs/background/async-execution.ts +309 -126
  43. package/src/runs/background/async-job-tracker.ts +8 -6
  44. package/src/runs/background/async-resume.ts +13 -4
  45. package/src/runs/background/async-status.ts +15 -4
  46. package/src/runs/background/auto-drain.ts +20 -10
  47. package/src/runs/background/binary-bootstrap.ts +5 -0
  48. package/src/runs/background/chain-append.ts +1 -1
  49. package/src/runs/background/chain-root-attachment.ts +14 -33
  50. package/src/runs/background/notify.ts +74 -6
  51. package/src/runs/background/result-files.ts +8 -4
  52. package/src/runs/background/result-watcher.ts +19 -2
  53. package/src/runs/background/run-child-session.ts +20 -29
  54. package/src/runs/background/runner-aliases.ts +4 -33
  55. package/src/runs/background/runner-child-launch.ts +4 -1
  56. package/src/runs/background/runner-child-sessions.ts +2 -2
  57. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  58. package/src/runs/background/scheduled-runs.ts +11 -5
  59. package/src/runs/background/stale-run-reconciler.ts +35 -11
  60. package/src/runs/background/subagent-runner.ts +396 -275
  61. package/src/runs/background/subagent-wait.ts +128 -23
  62. package/src/runs/background/wait-completions.ts +75 -27
  63. package/src/runs/background/wait-subscriptions.ts +9 -3
  64. package/src/runs/background/wait-tool.ts +4 -2
  65. package/src/runs/foreground/async-stop-action.ts +93 -3
  66. package/src/runs/foreground/execution.ts +91 -218
  67. package/src/runs/foreground/foreground-history.ts +2 -1
  68. package/src/runs/foreground/subagent-executor.ts +266 -80
  69. package/src/runs/shared/acceptance.ts +34 -10
  70. package/src/runs/shared/async-status-projection.ts +123 -33
  71. package/src/runs/shared/child-launch-plan.ts +15 -3
  72. package/src/runs/shared/child-launch.ts +19 -6
  73. package/src/runs/shared/child-runtime-config.ts +5 -0
  74. package/src/runs/shared/child-session.ts +94 -50
  75. package/src/runs/shared/child-tool-plan.ts +28 -16
  76. package/src/runs/shared/dynamic-fanout.ts +2 -2
  77. package/src/runs/shared/external-cli-contract.ts +11 -1
  78. package/src/runs/shared/external-cli-preflight.ts +6 -2
  79. package/src/runs/shared/herdr-connection.ts +134 -0
  80. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  81. package/src/runs/shared/herdr-machine.ts +279 -0
  82. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  83. package/src/runs/shared/herdr-placed-run.ts +263 -0
  84. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  85. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
  86. package/src/runs/shared/model-scope.ts +1 -1
  87. package/src/runs/shared/nested-events.ts +11 -2
  88. package/src/runs/shared/parallel-utils.ts +7 -2
  89. package/src/runs/shared/pi-spawn.ts +1 -1
  90. package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  91. package/src/runs/shared/worktree-setup-command.ts +27 -4
  92. package/src/runs/shared/worktree.ts +3 -3
  93. package/src/shared/child-cache-retention.ts +43 -0
  94. package/src/shared/launch-contract.ts +6 -9
  95. package/src/shared/pruned-fork.ts +1 -1
  96. package/src/shared/required-child-extensions.ts +81 -0
  97. package/src/shared/settings.ts +5 -2
  98. package/src/shared/shortcuts.ts +0 -4
  99. package/src/shared/types.ts +70 -29
  100. package/src/slash/slash-commands.ts +0 -6
  101. package/src/slash/subagents-admin.ts +13 -9
  102. package/src/tui/render.ts +20 -10
  103. package/src/watchdog/child-status.ts +28 -36
  104. package/src/watchdog/lsp-diagnostics.ts +1 -1
  105. package/src/watchdog/model-selection.ts +1 -1
  106. package/src/watchdog/register-child.ts +10 -3
  107. package/src/watchdog/register-main.ts +20 -20
  108. package/src/watchdog/render.ts +1 -1
  109. package/src/watchdog/review.ts +14 -30
  110. package/src/watchdog/rules.ts +1 -1
  111. package/src/watchdog/runtime.ts +23 -12
  112. package/src/watchdog/settings.ts +3 -6
  113. package/src/watchdog/types.ts +3 -5
  114. package/src/watchdog/warning-format.ts +1 -1
  115. package/src/workflows/scripted-workflow.ts +42 -3
  116. package/src/workflows/workflow-receipt.ts +21 -3
  117. package/src/workflows/workflow-resources.ts +13 -2
  118. package/src/runs/shared/model-exclusions.ts +0 -374
  119. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  120. package/src/runs/shared/readonly-session-evidence.ts +0 -307
@@ -22,6 +22,10 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
22
22
  const key = keyValue.trim().toLowerCase();
23
23
  const rawValue = rawValueValue.trim();
24
24
 
25
+ if (key === "machine") {
26
+ if (rawValue) step.machine = rawValue;
27
+ continue;
28
+ }
25
29
  if (key === "output") {
26
30
  if (rawValue === "false") step.output = false;
27
31
  else if (rawValue) step.output = rawValue;
@@ -257,6 +261,7 @@ export function serializeChain(config: ChainConfig): string {
257
261
  for (let i = 0; i < config.steps.length; i++) {
258
262
  const step = config.steps[i]!;
259
263
  lines.push(`## ${step.agent}`);
264
+ if (step.machine) lines.push(`machine: ${step.machine}`);
260
265
  if (step.output === false) lines.push("output: false");
261
266
  else if (step.output) lines.push(`output: ${step.output}`);
262
267
  if (step.phase) lines.push(`phase: ${step.phase}`);
@@ -24,7 +24,6 @@ export interface RuntimeAgentDefinition {
24
24
  allowNestedSubagents?: boolean;
25
25
  mcpDirectTools?: readonly string[];
26
26
  model?: string;
27
- fallbackModels?: readonly string[];
28
27
  thinking?: string | false;
29
28
  systemPromptMode?: "append" | "replace";
30
29
  inheritProjectContext?: boolean;
@@ -37,6 +36,7 @@ export interface RuntimeAgentDefinition {
37
36
  defaultAcceptance?: AcceptanceInput;
38
37
  acceptanceRole?: AcceptanceRole;
39
38
  runner?: AgentRunnerConfig;
39
+ machine?: string;
40
40
  skills?: readonly string[];
41
41
  skillPath?: readonly string[];
42
42
  extensions?: readonly string[];
@@ -199,9 +199,9 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
199
199
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Runtime agent definition must be an object.");
200
200
  const definition = value as Record<string, unknown>;
201
201
  const supported = new Set([
202
- "description", "systemPrompt", "aliases", "tools", "excludeTools", "allowNestedSubagents", "mcpDirectTools", "model", "fallbackModels", "thinking",
202
+ "description", "systemPrompt", "aliases", "tools", "excludeTools", "allowNestedSubagents", "mcpDirectTools", "model", "thinking",
203
203
  "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "defaultContext", "defaultAsync", "defaultTimeoutMs",
204
- "defaultToolTimeoutMs", "defaultAcceptance", "acceptanceRole", "runner", "skills", "skillPath",
204
+ "defaultToolTimeoutMs", "defaultAcceptance", "acceptanceRole", "runner", "machine", "skills", "skillPath",
205
205
  "extensions", "subagentOnlyExtensions", "mutationTools", "output", "outputMode", "defaultReads", "defaultProgress", "interactive",
206
206
  "maxSubagentDepth", "completionGuard", "toolBudget", "permissions",
207
207
  ]);
@@ -223,7 +223,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
223
223
  const allowNestedSubagents = validateBoolean(definition.allowNestedSubagents, "Runtime agent definition allowNestedSubagents");
224
224
  const mcpDirectTools = validateStringList(definition.mcpDirectTools, "Runtime agent definition mcpDirectTools");
225
225
  const model = validateOptionalString(definition.model, "Runtime agent definition model");
226
- const fallbackModels = validateStringList(definition.fallbackModels, "Runtime agent definition fallbackModels");
226
+ if ((definition as Record<string, unknown>).fallbackModels !== undefined) throw new Error("Runtime agent definition fallbackModels was removed; configure one model instead.");
227
227
  const inheritProjectContext = validateBoolean(definition.inheritProjectContext, "Runtime agent definition inheritProjectContext");
228
228
  const inheritGlobalContext = validateBoolean(definition.inheritGlobalContext, "Runtime agent definition inheritGlobalContext");
229
229
  const inheritSkills = validateBoolean(definition.inheritSkills, "Runtime agent definition inheritSkills");
@@ -237,6 +237,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
237
237
  const extensions = validateStringList(definition.extensions, "Runtime agent definition extensions");
238
238
  const subagentOnlyExtensions = validateStringList(definition.subagentOnlyExtensions, "Runtime agent definition subagentOnlyExtensions");
239
239
  const mutationTools = validateStringList(definition.mutationTools, "Runtime agent definition mutationTools");
240
+ const machine = validateOptionalString(definition.machine, "Runtime agent definition machine");
240
241
  const output = validateOptionalString(definition.output, "Runtime agent definition output");
241
242
  const defaultReads = validateStringList(definition.defaultReads, "Runtime agent definition defaultReads");
242
243
  const defaultProgress = validateBoolean(definition.defaultProgress, "Runtime agent definition defaultProgress");
@@ -254,7 +255,6 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
254
255
  ...(allowNestedSubagents !== undefined ? { allowNestedSubagents } : {}),
255
256
  ...(mcpDirectTools ? { mcpDirectTools } : {}),
256
257
  ...(model ? { model } : {}),
257
- ...(fallbackModels ? { fallbackModels } : {}),
258
258
  ...(thinking !== undefined ? { thinking: thinking as string | false } : {}),
259
259
  ...(systemPromptMode !== undefined ? { systemPromptMode: systemPromptMode as "append" | "replace" } : {}),
260
260
  ...(inheritProjectContext !== undefined ? { inheritProjectContext } : {}),
@@ -272,6 +272,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
272
272
  ...(extensions ? { extensions } : {}),
273
273
  ...(subagentOnlyExtensions ? { subagentOnlyExtensions } : {}),
274
274
  ...(mutationTools ? { mutationTools } : {}),
275
+ ...(machine ? { machine } : {}),
275
276
  ...(output ? { output } : {}),
276
277
  ...(outputMode !== undefined ? { outputMode: outputMode as OutputMode } : {}),
277
278
  ...(defaultReads ? { defaultReads } : {}),
@@ -334,7 +335,6 @@ function toAgentConfig(name: string, definition: RuntimeAgentDefinition): AgentC
334
335
  ...(definition.allowNestedSubagents !== undefined ? { allowNestedSubagents: definition.allowNestedSubagents } : {}),
335
336
  ...(definition.mcpDirectTools !== undefined ? { mcpDirectTools: [...definition.mcpDirectTools] } : {}),
336
337
  ...(definition.model !== undefined ? { model: definition.model } : {}),
337
- ...(definition.fallbackModels !== undefined ? { fallbackModels: [...definition.fallbackModels] } : {}),
338
338
  ...(definition.thinking !== undefined ? { thinking: definition.thinking } : {}),
339
339
  systemPromptMode: definition.systemPromptMode ?? defaultSystemPromptMode(name),
340
340
  inheritProjectContext: definition.inheritProjectContext ?? defaultInheritProjectContext(name),
@@ -354,6 +354,7 @@ function toAgentConfig(name: string, definition: RuntimeAgentDefinition): AgentC
354
354
  ...(definition.extensions !== undefined ? { extensions: [...definition.extensions] } : {}),
355
355
  ...(definition.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...definition.subagentOnlyExtensions] } : {}),
356
356
  ...(definition.mutationTools !== undefined ? { mutationTools: [...definition.mutationTools] } : {}),
357
+ ...(definition.machine !== undefined ? { machine: definition.machine } : {}),
357
358
  ...(definition.output !== undefined ? { output: definition.output } : {}),
358
359
  ...(definition.outputMode !== undefined ? { outputMode: definition.outputMode } : {}),
359
360
  ...(definition.defaultReads !== undefined ? { defaultReads: [...definition.defaultReads] } : {}),
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { discoverAgentSnapshot, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentDiscoveryAllResult, type AgentScope, type AgentSource } from "../agents/agents.ts";
5
5
  import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
6
6
  import { normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
7
- import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelOrigin, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-fallback.ts";
7
+ import { inheritsParentModel, resolveEffectiveSubagentModel, resolveModelOrigin, resolveModelSelection, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-resolution.ts";
8
8
  import { resolveModelScopesForAgent } from "../runs/shared/model-scope.ts";
9
9
  import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/child-tool-plan.ts";
10
10
  import { buildEffectiveSystemPrompt } from "../runs/shared/effective-system-prompt.ts";
@@ -26,6 +26,7 @@ import { processTerminalCandidatePath, processTerminalPath } from "../runs/backg
26
26
  import { resultFilePath } from "../runs/background/result-files.ts";
27
27
  import { nestedResultsPath } from "../runs/shared/nested-events.ts";
28
28
  import { normalizeExtensionBindings, type ExtensionBindings } from "../runs/shared/extension-bindings.ts";
29
+ import { resolveRequiredChildExtensions } from "../shared/required-child-extensions.ts";
29
30
 
30
31
  // v3: the contract reports the resolved Intercom bridge state and binds its
31
32
  // prompt and tools into launchContractDigest, matching execution (#2127).
@@ -72,11 +73,13 @@ export interface SubagentLaunchContractInput {
72
73
  skill?: string | string[] | boolean;
73
74
  output?: string | boolean;
74
75
  outputMode?: OutputMode;
75
- outputSchema?: JsonSchemaObject;
76
+ outputSchema?: JsonSchemaObject | false;
76
77
  extensionBindings?: ExtensionBindings;
77
78
  artifacts?: boolean;
78
79
  artifactDir?: ArtifactDirPreference;
79
80
  parentSessionFile?: string | null;
81
+ /** Parent session whose host-required child extension snapshot is preflighted. */
82
+ parentSessionId?: string;
80
83
  /** Current parent leaf required before an implicit `defaultContext: fork` stays `fork`. */
81
84
  parentLeafId?: string | null;
82
85
  sessionRoot?: string;
@@ -144,6 +147,7 @@ export interface SubagentLaunchContractTools {
144
147
  toolExtensionPaths: string[];
145
148
  runtimeExtensions: string[];
146
149
  configuredExtensions: string[];
150
+ requiredExtensionIds: string[];
147
151
  extensionArgs: string[];
148
152
  disableAmbientExtensions: boolean;
149
153
  fanoutAuthorized: boolean;
@@ -175,7 +179,6 @@ export interface SubagentLaunchContract {
175
179
  agent: SubagentLaunchContractAgent;
176
180
  context: "fresh" | "fork";
177
181
  model?: string;
178
- modelCandidates: string[];
179
182
  thinking?: string;
180
183
  thinkingCeiling?: ThinkingLevel;
181
184
  systemPromptMode: AgentConfig["systemPromptMode"];
@@ -345,6 +348,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
345
348
  ...(input.outputMode !== undefined ? { outputMode: input.outputMode } : {}),
346
349
  ...(skillInput !== undefined ? { skills: skillInput } : {}),
347
350
  ...(input.model !== undefined ? { model: input.model } : {}),
351
+ ...(input.outputSchema !== undefined ? { outputSchema: input.outputSchema } : {}),
348
352
  });
349
353
  const requestedSkills = behavior.skills === false ? [] : behavior.skills;
350
354
  const resolvedSkills = resolveSkillsWithFallback(
@@ -360,6 +364,9 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
360
364
  if (resolvedSkills.missing.length > 0) diagnostics.push({ code: "missing_skill", severity: "error", message: `Missing skills: ${resolvedSkills.missing.join(", ")}` });
361
365
 
362
366
  const externalRunner = agent.runner?.type === "external-cli" || agent.runner?.type === "external-job";
367
+ if (externalRunner && behavior.outputSchema) {
368
+ return { ok: false, code: "unsupported_mode", message: `Agent '${agent.name}' uses runner.type='${agent.runner?.type}' and does not support: structured output.`, diagnostics };
369
+ }
363
370
  const availableModels = normalizeAvailableModels(input.availableModels);
364
371
  const preferredProvider = agent.modelProvider ?? input.preferredProvider ?? input.parentModel?.provider;
365
372
  const modelScopes = resolveModelScopesForAgent(discovered.modelScope, agent.name, input.parentModel);
@@ -376,19 +383,14 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
376
383
  input.thinkingCeiling,
377
384
  input.inheritedThinkingCeiling,
378
385
  );
379
- const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
380
- const modelCandidates = externalRunner
381
- ? []
382
- : buildModelCandidates(primaryModel, agent.fallbackModels, availableModels, preferredProvider, {
386
+ const model = externalRunner ? undefined : applyThinkingSuffix(resolveModelSelection(primaryModel, availableModels, preferredProvider, {
383
387
  scope: modelScopes,
384
388
  primaryModelFromParent: modelOrigin === "inherited" || inheritsParentModel(input.model, agent.model, input.parentModel),
385
389
  origin: modelOrigin,
386
- })
387
- .map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate);
390
+ }).model, effectiveThinkingConfig, input.thinking !== undefined);
388
391
  if (!externalRunner) {
389
392
  try {
390
393
  assertThinkingWithinCeiling({ model, configThinking: effectiveThinkingConfig, ceiling: thinkingCeiling, agent: agent.name, runId });
391
- for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinkingConfig, ceiling: thinkingCeiling, agent: agent.name, runId });
392
394
  } catch (error) {
393
395
  const message = error instanceof Error ? error.message : String(error);
394
396
  diagnostics.push({ code: "thinking_ceiling", severity: "error", message });
@@ -398,6 +400,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
398
400
  let toolPlan: PiLaunchToolPlan;
399
401
  const permissionRules = resolvePermissionRules(loadConfig().permissions, agent.permissions);
400
402
  const fast = input.fast ?? agent.fast;
403
+ const requiredExtensions = externalRunner ? [] : resolveRequiredChildExtensions(input.parentSessionId);
401
404
  try {
402
405
  toolPlan = resolvePiLaunchToolPlan({
403
406
  tools: agent.tools,
@@ -405,13 +408,13 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
405
408
  allowNestedSubagents: agent.allowNestedSubagents,
406
409
  extensions: agent.extensions,
407
410
  subagentOnlyExtensions: agent.subagentOnlyExtensions,
411
+ requiredExtensions,
408
412
  mcpDirectTools: agent.mcpDirectTools,
409
413
  cwd: effectiveCwd,
410
414
  requireReadTool: resolvedSkills.resolved.length > 0,
411
- structuredOutput: Boolean(input.outputSchema),
415
+ structuredOutput: Boolean(behavior.outputSchema),
412
416
  fast,
413
417
  model,
414
- modelCandidates,
415
418
  capabilityCeiling: effectiveCapabilityCeiling,
416
419
  agentName: agent.name,
417
420
  permissionRules,
@@ -448,7 +451,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
448
451
  const binding = resolveLaunchBinding({
449
452
  agent,
450
453
  task: input.task ?? "",
451
- modelCandidates,
454
+ model,
452
455
  ...(fast !== undefined ? { fast } : {}),
453
456
  ...(effectiveThinking ? { thinking: effectiveThinking } : {}),
454
457
  systemPrompt: buildEffectiveSystemPrompt({ agent, resolvedSkills: resolvedSkills.resolved, cwd: effectiveCwd, ...(outputPath ? { outputPath } : {}) }),
@@ -456,7 +459,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
456
459
  toolPlan,
457
460
  ...(outputPath ? { outputPath } : {}),
458
461
  outputMode: behavior.outputMode,
459
- ...(input.outputSchema ? { structuredOutputSchema: input.outputSchema } : {}),
462
+ ...(behavior.outputSchema ? { structuredOutputSchema: behavior.outputSchema } : {}),
460
463
  ...(extensionBindings ? { extensionBindings } : {}),
461
464
  });
462
465
  const candidates = candidateList(input.agent, agent, discovery.all);
@@ -476,7 +479,6 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
476
479
  },
477
480
  context,
478
481
  ...(model ? { model } : {}),
479
- modelCandidates,
480
482
  ...(effectiveThinking ? { thinking: effectiveThinking } : {}),
481
483
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
482
484
  systemPromptMode: agent.systemPromptMode,
@@ -501,7 +503,9 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
501
503
  toolExtensionPaths: toolPlan.toolExtensionPaths,
502
504
  runtimeExtensions: toolPlan.runtimeExtensions,
503
505
  configuredExtensions: toolPlan.configuredExtensions,
504
- extensionArgs: toolPlan.extensionArgs,
506
+ requiredExtensionIds: toolPlan.requiredExtensions.map(({ id }) => id),
507
+ // Required paths are private launch authority; preflight exposes their safe IDs above.
508
+ extensionArgs: toolPlan.extensionArgs.filter((extensionPath) => !requiredExtensions.some(({ path }) => path === extensionPath)),
505
509
  disableAmbientExtensions: toolPlan.disableAmbientExtensions,
506
510
  fanoutAuthorized: toolPlan.fanoutAuthorized,
507
511
  ...(toolPlan.capabilityCeiling ? { capabilityCeiling: toolPlan.capabilityCeiling } : {}),
@@ -0,0 +1,6 @@
1
+ export {
2
+ registerRequiredChildExtensions,
3
+ type RegisterRequiredChildExtensionsInput,
4
+ type RequiredChildExtension,
5
+ type RequiredChildExtensionRegistration,
6
+ } from "../shared/required-child-extensions.ts";
@@ -6,7 +6,6 @@ import { FLEET_KEYBINDING_ACTIONS, type ArtifactDirPreference, type ExtensionCon
6
6
  import { validateMissionStoreConfig } from "../missions/store.ts";
7
7
  import { validateAuthorityPolicy } from "../policy/authority.ts";
8
8
  import { getAgentDir } from "../shared/utils.ts";
9
- import { DEFAULT_MODEL_EXCLUSION_TTL_MS, MAX_MODEL_EXCLUSION_TTL_MS, setDefaultTTL } from "../runs/shared/model-exclusions.ts";
10
9
  import { validatePermissionConfig } from "../runs/shared/permissions.ts";
11
10
  import { MAX_ABANDONED_SLOT_RELEASE_AFTER_MS, MIN_ABANDONED_SLOT_RELEASE_AFTER_MS } from "../runs/background/active-async-capacity.ts";
12
11
  import { normalizeWorktreeBranchPrefix } from "../runs/shared/worktree.ts";
@@ -97,17 +96,6 @@ function validateCapacityConfig(value: unknown): void {
97
96
  }
98
97
  }
99
98
 
100
- /** Validate the user-controlled TTL policy before it reaches the exclusion store. */
101
- // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
102
- function validateModelExclusionsConfig(value: unknown): void {
103
- if (value === undefined) return;
104
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.modelExclusions must be a JSON object");
105
- const defaultTtlMs = (value as Record<string, unknown>).defaultTtlMs;
106
- if (defaultTtlMs !== undefined && (typeof defaultTtlMs !== "number" || !Number.isFinite(defaultTtlMs) || defaultTtlMs <= 0 || defaultTtlMs > MAX_MODEL_EXCLUSION_TTL_MS)) {
107
- throw new Error(`config.modelExclusions.defaultTtlMs must be a finite positive number no greater than ${MAX_MODEL_EXCLUSION_TTL_MS}`);
108
- }
109
- }
110
-
111
99
  function validateOrcaProgressTabsConfig(value: unknown): void {
112
100
  if (value === undefined) return;
113
101
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.orcaProgressTabs must be a JSON object");
@@ -154,6 +142,13 @@ function validateConfig(config: Record<string, unknown>): void {
154
142
  throw new Error('config.defaultSubagentContext must be "fresh" or "fork"');
155
143
  }
156
144
  validateForkContextConfig(config.forkContext);
145
+ if (config.checkpointBeforeDeadlineMs !== undefined
146
+ && (typeof config.checkpointBeforeDeadlineMs !== "number"
147
+ || !Number.isInteger(config.checkpointBeforeDeadlineMs)
148
+ || config.checkpointBeforeDeadlineMs <= 0
149
+ || config.checkpointBeforeDeadlineMs > 2_147_483_647)) {
150
+ throw new Error("config.checkpointBeforeDeadlineMs must be a positive integer no larger than 2147483647");
151
+ }
157
152
  if (config.foregroundDetachShortcut !== undefined
158
153
  && (typeof config.foregroundDetachShortcut !== "string" || !isValidKeyId(config.foregroundDetachShortcut))) {
159
154
  throw new Error("config.foregroundDetachShortcut must be a valid keybinding string such as \"ctrl+b\"");
@@ -177,7 +172,7 @@ function validateConfig(config: Record<string, unknown>): void {
177
172
  validateFleetKeybindingsConfig(config.fleetKeybindings);
178
173
  validateArtifactConfig(config.artifactConfig);
179
174
  validateCapacityConfig(config.capacity);
180
- validateModelExclusionsConfig(config.modelExclusions);
175
+ if (config.modelExclusions !== undefined) throw new Error("config.modelExclusions was removed; model failures are no longer persisted or used for automatic switching");
181
176
  validateModelResponseAliases(config.modelResponseAliases);
182
177
  validateMainWindowRendererConfig(config.mainWindowRenderer);
183
178
  validateOrcaProgressTabsConfig(config.orcaProgressTabs);
@@ -210,28 +205,6 @@ export function updateConfig(updater: (config: ExtensionConfig) => ExtensionConf
210
205
  return next;
211
206
  }
212
207
 
213
- /**
214
- * Resolve the default TTL that the process-wide exclusion store should use.
215
- *
216
- * @param config Extension configuration after validation.
217
- * @returns The configured TTL in milliseconds, or the built-in 24-hour default.
218
- */
219
- // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
220
- export function resolveModelExclusionTTL(config: Pick<ExtensionConfig, "modelExclusions">): number {
221
- return config.modelExclusions?.defaultTtlMs ?? DEFAULT_MODEL_EXCLUSION_TTL_MS;
222
- }
223
-
224
- /**
225
- * Apply the configured exclusion policy to the process-wide model store.
226
- *
227
- * @param config Extension configuration after validation.
228
- * @returns Nothing.
229
- */
230
- // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
231
- export function applyModelExclusionsConfig(config: Pick<ExtensionConfig, "modelExclusions">): void {
232
- setDefaultTTL(resolveModelExclusionTTL(config), { shortenExisting: config.modelExclusions?.defaultTtlMs !== undefined });
233
- }
234
-
235
208
  export function resolveAsyncByDefault(config: Pick<ExtensionConfig, "asyncByDefault">): boolean {
236
209
  return config.asyncByDefault !== false;
237
210
  }
@@ -242,12 +215,12 @@ export function loadConfig(): ExtensionConfig {
242
215
  return readConfigForUpdate(configPath);
243
216
  } catch (error) {
244
217
  if (error instanceof PrunedForkConfigError) throw error;
245
- // Explicit route identity and worktree policies must not be silently
218
+ // Explicit route identity, worktree, and checkpoint policies must not be silently
246
219
  // discarded and replaced by the built-in defaults after validation fails.
247
220
  try {
248
221
  const raw = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
249
222
  if (raw && typeof raw === "object" && !Array.isArray(raw)
250
- && (Object.hasOwn(raw, "worktreeProvider") || Object.hasOwn(raw, "worktreeBranchPrefix") || Object.hasOwn(raw, "modelResponseAliases"))) throw error;
223
+ && (Object.hasOwn(raw, "worktreeProvider") || Object.hasOwn(raw, "worktreeBranchPrefix") || Object.hasOwn(raw, "modelResponseAliases") || Object.hasOwn(raw, "modelExclusions") || Object.hasOwn(raw, "checkpointBeforeDeadlineMs"))) throw error;
251
224
  } catch (readError) {
252
225
  if (readError === error) throw error;
253
226
  }
@@ -190,6 +190,8 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, c
190
190
  };
191
191
  },
192
192
  });
193
+ const hasPendingSupervisorRequest = supervisorChannel.hasPendingRequests;
194
+ childConfig.hasPendingSupervisorRequest = hasPendingSupervisorRequest;
193
195
  const executor = createSubagentExecutor({
194
196
  pi,
195
197
  state,
@@ -245,6 +247,7 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, c
245
247
  asyncChildren.clear();
246
248
  foregroundChannels.clear();
247
249
  supervisorChannel.dispose();
250
+ if (childConfig.hasPendingSupervisorRequest === hasPendingSupervisorRequest) childConfig.hasPendingSupervisorRequest = undefined;
248
251
  state.supervisorOwnerSessionId = null;
249
252
  });
250
253
  const route = resolveNestedControlRoute(childConfig);
@@ -0,0 +1,160 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as net from "node:net";
4
+ import * as path from "node:path";
5
+ import { Type } from "typebox";
6
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import packageJson from "../../package.json" with { type: "json" };
9
+ import { encodeHerdrPiFrame, HERDR_PI_MAX_FRAME_BYTES, HERDR_PI_PROTOCOL, HERDR_PI_RUN_ENV, HERDR_PI_RUNTIME_DIR_ENV, HerdrPiFrameDecoder, validateHerdrPiRunId, type HerdrPiFrame } from "../runs/shared/herdr-pi-protocol.ts";
10
+ import { discoverAgents } from "../agents/agents.ts";
11
+ import { buildSkillInjection, resolveSkills } from "../agents/skills.ts";
12
+ import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
13
+ import { appendAgentRefinementOverlay } from "../agents/agent-refinements.ts";
14
+ import { rewriteSubagentPrompt } from "../runs/shared/subagent-prompt-runtime.ts";
15
+ import { resolveExistingReadPaths } from "../shared/settings.ts";
16
+
17
+ interface PendingRequest { operation: "prompt" | "steer" | "follow-up" | "abort" | "supervisor-reply"; text?: string; supervisorId?: string }
18
+ interface BridgeContext {
19
+ isIdle(): boolean;
20
+ hasPendingMessages(): boolean;
21
+ getActiveTools?(): string[];
22
+ sessionManager?: { getSessionId?(): string; getSessionFile?(): string | undefined };
23
+ model?: { provider?: string; id?: string };
24
+ }
25
+
26
+ function gitEvidence(cwd: string): Record<string, unknown> | undefined {
27
+ const run = (args: string[]) => spawnSync("git", args, { cwd, encoding: "utf8", timeout: 5_000, maxBuffer: 128 * 1024 });
28
+ const inside = run(["rev-parse", "--is-inside-work-tree"]);
29
+ if (inside.status !== 0) return undefined;
30
+ const head = run(["rev-parse", "HEAD"]); const branch = run(["symbolic-ref", "--quiet", "--short", "HEAD"]); const dirty = run(["status", "--porcelain"]);
31
+ return { ...(head.status === 0 ? { head: head.stdout.trim() } : {}), ...(branch.status === 0 ? { branch: branch.stdout.trim() } : {}), dirty: Boolean(dirty.stdout.trim()) };
32
+ }
33
+
34
+ export function resolveRemoteHerdrResources(cwd: string, resources: { agent: string; skills?: string[]; toolCeiling?: string[]; reads?: string[] | false }, remoteDefaultTools: string[] = []): { agent: string; skills: string[]; tools: string[]; systemPrompt: string; inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean } {
35
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(resources.agent) || resources.skills?.some((name) => !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(name)) || resources.toolCeiling?.some((name) => !/^[A-Za-z0-9_-]{1,64}$/u.test(name)) || (resources.reads !== undefined && resources.reads !== false && (!Array.isArray(resources.reads) || resources.reads.some((name) => typeof name !== "string")))) throw new Error("Remote logical resource names are invalid.");
36
+ const discovered = discoverAgents(cwd, "both").agents.filter((agent) => agent.name === resources.agent || agent.aliases?.includes(resources.agent));
37
+ if (discovered.length !== 1) throw new Error(`Remote agent '${resources.agent}' was not found unambiguously in ${cwd}.`);
38
+ const agent = discovered[0]!; const names = resources.skills ?? agent.skills ?? [];
39
+ const skills = resolveSkills(names, cwd); if (skills.missing.length) throw new Error(`Remote Pi skills not found: ${skills.missing.join(", ")}. Configure them on the saved machine.`);
40
+ const skillPrompt = buildSkillInjection(skills.resolved);
41
+ const reads = resources.reads === undefined ? agent.defaultReads ?? false : resources.reads; const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, cwd) : [];
42
+ const systemPrompt = appendAgentRefinementOverlay(`${agent.systemPrompt}${buildAgentMemoryInjection(agent, cwd)}${skillPrompt ? `\n\n${skillPrompt}` : ""}${readPaths.length ? `\n\n[Read from: ${readPaths.join(", ")}]` : ""}`, { cwd, agentName: agent.name });
43
+ const denied = new Set(agent.excludeTools ?? []); const tools = (agent.tools === undefined ? remoteDefaultTools : agent.tools).filter((tool) => !denied.has(tool) && (!resources.toolCeiling || resources.toolCeiling.includes(tool))); if (agent.tools === undefined && !tools.length) throw new Error(`Remote agent '${agent.name}' resolved no default active tools within the inherited ceiling.`);
44
+ return { agent: agent.name, skills: names, tools, systemPrompt, inheritProjectContext: agent.inheritProjectContext, inheritGlobalContext: agent.inheritGlobalContext, inheritSkills: agent.inheritSkills };
45
+ }
46
+
47
+ export default function registerHerdrPiBridge(pi: ExtensionAPI): void {
48
+ const runId = validateHerdrPiRunId(process.env[HERDR_PI_RUN_ENV]);
49
+ const runDir = process.env[HERDR_PI_RUNTIME_DIR_ENV];
50
+ if (!runDir || !path.isAbsolute(runDir) || fs.lstatSync(runDir).isSymbolicLink()) throw new Error("Herdr bridge runtime directory was not authoritatively provisioned.");
51
+ fs.chmodSync(runDir, 0o700);
52
+ const socketPath = path.join(runDir, "bridge.sock");
53
+ const manifestPath = path.join(runDir, "manifest.json");
54
+ for (const candidate of [socketPath, manifestPath]) {
55
+ try { const stat = fs.lstatSync(candidate); if (stat.isSymbolicLink()) throw new Error(`Refusing symlink at '${candidate}'.`); fs.rmSync(candidate, { force: true }); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
56
+ }
57
+ let client: net.Socket | undefined;
58
+ let context: BridgeContext | undefined;
59
+ let nativeSessionId: string | undefined;
60
+ let finalAssistant: AgentMessage | undefined;
61
+ let activeRequestId: string | undefined;
62
+ let resolvedSystemPrompt: string | undefined;
63
+ let resolvedContextPolicy: { agent: string; inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean } | undefined;
64
+ let configured = false;
65
+ const pending = new Map<string, PendingRequest>();
66
+ const supervisor = new Map<string, { reason: "need_decision" | "interview_request" | "progress_update"; resolve(answer: string): void }>();
67
+ let evidenceCursor = 0; let journalBytes = 0; const journal: HerdrPiFrame[] = [];
68
+ const send = (frame: { type: string; [key: string]: unknown }) => {
69
+ const record = { protocol: HERDR_PI_PROTOCOL, runId, ...frame, cursor: ++evidenceCursor } as HerdrPiFrame; const size = Buffer.byteLength(JSON.stringify(record)); journal.push(record); journalBytes += size; while (journal.length > 512 || journalBytes > 8 * 1024 * 1024) { const removed = journal.shift(); if (removed) journalBytes -= Buffer.byteLength(JSON.stringify(removed)); }
70
+ if (!client || client.destroyed) return;
71
+ client.write(encodeHerdrPiFrame(record));
72
+ };
73
+ const sessionId = (ctx: BridgeContext) => ctx.sessionManager?.getSessionId?.();
74
+ const activeTools = (ctx: BridgeContext) => ctx.getActiveTools?.() ?? [];
75
+
76
+ pi.registerTool({
77
+ name: "contact_supervisor",
78
+ label: "Contact supervisor",
79
+ description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
80
+ parameters: Type.Object({ reason: Type.Union([Type.Literal("need_decision"), Type.Literal("interview_request"), Type.Literal("progress_update")]), message: Type.Optional(Type.String({ maxLength: 65_536 })), interview: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
81
+ execute: async (_toolCallId, input: { reason: "need_decision" | "interview_request" | "progress_update"; message?: string; interview?: unknown }) => {
82
+ if (input.reason !== "interview_request" && !input.message?.trim()) throw new Error("message is required for supervisor decisions and progress updates.");
83
+ const id = `sup_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
84
+ send({ type: "supervisor-request", requestId: id, reason: input.reason, message: input.message?.slice(0, 65_536) ?? "", ...(input.interview !== undefined ? { interview: input.interview } : {}), expectsReply: input.reason !== "progress_update", nativeSessionId });
85
+ if (input.reason === "progress_update") { await new Promise<string>((resolve) => supervisor.set(id, { reason: input.reason, resolve })); return { content: [{ type: "text", text: "Supervisor progress update queued." }], details: { delivered: true, requestId: id, reason: input.reason } }; }
86
+ const reason = input.reason as "need_decision" | "interview_request"; const answer = await new Promise<string>((resolve) => supervisor.set(id, { reason, resolve })); const details: Record<string, unknown> = { requestId: id, reason }; if (reason === "interview_request") try { details.structuredReply = JSON.parse(answer.replace(/^```(?:json)?\s*|\s*```$/giu, "")); } catch { details.structuredReplyParseError = "Supervisor interview reply was not valid JSON."; }
87
+ return { content: [{ type: "text", text: `**Reply from supervisor:**\n${answer}` }], details };
88
+ },
89
+ });
90
+
91
+ const execute = async (requestId: string, ctx: BridgeContext) => {
92
+ const request = pending.get(requestId);
93
+ if (!request) throw new Error(`Unknown or already consumed bridge request '${requestId}'.`);
94
+ pending.delete(requestId);
95
+ if (sessionId(ctx) !== nativeSessionId) throw new Error("Native Pi session identity changed.");
96
+ send({ type: "accepted", requestId, operation: request.operation, nativeSessionId });
97
+ const api = pi as unknown as { sendUserMessage(text: string, options?: { deliverAs?: "steer" | "followUp" }): void; abort(): void };
98
+ if (request.operation === "prompt") { activeRequestId = requestId; finalAssistant = undefined; api.sendUserMessage(request.text ?? ""); }
99
+ else if (request.operation === "steer") api.sendUserMessage(request.text ?? "", { deliverAs: "steer" });
100
+ else if (request.operation === "follow-up") api.sendUserMessage(request.text ?? "", { deliverAs: "followUp" });
101
+ else if (request.operation === "abort") api.abort();
102
+ else {
103
+ const entry = request.supervisorId ? supervisor.get(request.supervisorId) : undefined;
104
+ if (!entry) throw new Error("Unknown supervisor request.");
105
+ supervisor.delete(request.supervisorId!); entry.resolve(request.text ?? "");
106
+ }
107
+ send({ type: "control-ack", requestId, operation: request.operation, nativeSessionId });
108
+ };
109
+ pi.registerCommand("pi-subagents-bridge", { description: "Internal pane-native request dispatch", handler: (args, ctx) => execute(args.trim(), ctx as unknown as BridgeContext) });
110
+
111
+ const on = pi.on as unknown as (name: string, handler: (event: Record<string, unknown>, ctx: BridgeContext) => unknown) => void;
112
+ on("before_agent_start", (event) => resolvedSystemPrompt && resolvedContextPolicy ? { systemPrompt: `${rewriteSubagentPrompt(typeof event.systemPrompt === "string" ? event.systemPrompt : "", { inheritProjectContext: resolvedContextPolicy.inheritProjectContext, inheritGlobalContext: resolvedContextPolicy.inheritGlobalContext, inheritSkills: resolvedContextPolicy.inheritSkills })}\n\n<active_agent name=${JSON.stringify(resolvedContextPolicy.agent)}/>\n\n${resolvedSystemPrompt}` } : undefined);
113
+ on("session_start", (_event, ctx) => {
114
+ context = ctx; nativeSessionId = sessionId(ctx);
115
+ if (!nativeSessionId) throw new Error("Pane-native Pi bridge requires a persisted native session identity.");
116
+ fs.writeFileSync(manifestPath, JSON.stringify({ protocol: HERDR_PI_PROTOCOL, packageVersion: packageJson.version, runId, socketPath, nativeSessionId, pid: process.pid }), { mode: 0o600, flag: "wx" });
117
+ send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: ctx.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: activeTools(ctx), model: ctx.model?.provider && ctx.model.id ? `${ctx.model.provider}/${ctx.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
118
+ });
119
+ on("message_end", (event) => {
120
+ const message = event.message as AgentMessage | undefined;
121
+ if (message?.role === "assistant") finalAssistant = message;
122
+ send({ type: "event", requestId: activeRequestId, event: { ...event, message }, nativeSessionId });
123
+ });
124
+ on("agent_settled", (_event, ctx) => { const requestId = activeRequestId; send({ type: "settled", requestId, nativeSessionId, idle: ctx.isIdle(), pending: ctx.hasPendingMessages(), finalAssistant, finalGit: gitEvidence(process.cwd()) }); if (ctx.isIdle() && !ctx.hasPendingMessages()) activeRequestId = undefined; });
125
+ on("session_shutdown", () => { server.close(); client?.destroy(); try { fs.rmSync(runDir, { recursive: true, force: true }); } catch {} });
126
+
127
+ const server = net.createServer((socket) => {
128
+ if (client && !client.destroyed) { socket.destroy(); return; }
129
+ client = socket; const decoder = new HerdrPiFrameDecoder(); let total = 0;
130
+ for (const record of journal) socket.write(encodeHerdrPiFrame(record));
131
+ socket.on("data", (chunk: Buffer) => {
132
+ total += chunk.byteLength; if (total > HERDR_PI_MAX_FRAME_BYTES * 32) { socket.destroy(new Error("Bridge byte budget exceeded.")); return; }
133
+ try {
134
+ for (const frame of decoder.push(chunk)) {
135
+ if (frame.runId !== runId) throw new Error("Bridge run identity mismatch.");
136
+ if (frame.type === "supervisor-delivered" && typeof frame.requestId === "string") { const entry = supervisor.get(frame.requestId); if (!entry || entry.reason !== "progress_update") throw new Error("Unknown supervisor delivery acknowledgement."); supervisor.delete(frame.requestId); entry.resolve("delivered"); continue; }
137
+ if (frame.type === "configure") {
138
+ if (configured || typeof frame.requestId !== "string" || !frame.resources || typeof frame.resources !== "object") throw new Error("Invalid or duplicate bridge configuration.");
139
+ const resources = frame.resources as { agent?: unknown; skills?: unknown; toolCeiling?: unknown; reads?: unknown };
140
+ const invalidSkills = resources.skills !== undefined && (!Array.isArray(resources.skills) || resources.skills.some((name) => typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(name)));
141
+ const invalidReads = resources.reads !== undefined && resources.reads !== false && (!Array.isArray(resources.reads) || resources.reads.some((name) => typeof name !== "string"));
142
+ if (typeof resources.agent !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(resources.agent) || invalidSkills || invalidReads) throw new Error("Remote logical resource names are invalid.");
143
+ const resolved = resolveRemoteHerdrResources(process.cwd(), { agent: resources.agent, ...(resources.skills ? { skills: resources.skills as string[] } : {}), ...(resources.toolCeiling ? { toolCeiling: resources.toolCeiling as string[] } : {}), ...(resources.reads !== undefined ? { reads: resources.reads as string[] | false } : {}) }, context ? activeTools(context) : []); resolvedSystemPrompt = resolved.systemPrompt; resolvedContextPolicy = resolved;
144
+ (pi as unknown as { setActiveTools?(tools: string[]): void }).setActiveTools?.(resolved.tools); const acknowledged = context ? activeTools(context) : []; if (acknowledged.some((tool) => !resolved.tools.includes(tool)) || resolved.tools.some((tool) => !acknowledged.includes(tool))) throw new Error("Remote Pi did not apply the resolved active-tool set exactly.");
145
+ configured = true; send({ type: "configured", requestId: frame.requestId, nativeSessionId, agent: resolved.agent, skills: resolved.skills, model: context?.model?.provider && context.model.id ? `${context.model.provider}/${context.model.id}` : undefined, tools: acknowledged }); continue;
146
+ }
147
+ if (frame.type !== "prepare" || typeof frame.requestId !== "string" || pending.has(frame.requestId) || !configured) throw new Error("Invalid, duplicate, or unconfigured bridge request.");
148
+ const operation = frame.operation;
149
+ if (!(["prompt", "steer", "follow-up", "abort", "supervisor-reply"] as unknown[]).includes(operation)) throw new Error("Unsupported bridge operation.");
150
+ const text = frame.text; if (text !== undefined && (typeof text !== "string" || Buffer.byteLength(text) > 256 * 1024)) throw new Error("Bridge request text is invalid or too large.");
151
+ pending.set(frame.requestId, { operation: operation as PendingRequest["operation"], ...(text !== undefined ? { text } : {}), ...(typeof frame.supervisorId === "string" ? { supervisorId: frame.supervisorId } : {}) });
152
+ send({ type: "prepared", requestId: frame.requestId, nativeSessionId });
153
+ }
154
+ } catch (error) { socket.destroy(error as Error); }
155
+ });
156
+ socket.on("close", () => { if (client === socket) client = undefined; });
157
+ if (context && nativeSessionId) send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: context.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: activeTools(context), model: context.model?.provider && context.model.id ? `${context.model.provider}/${context.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
158
+ });
159
+ server.listen(socketPath, () => { fs.chmodSync(socketPath, 0o600); });
160
+ }