pi-subagents 0.65.1 → 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 (148) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/researcher.md +23 -13
  5. package/agents/reviewer.md +3 -2
  6. package/docs/agents.md +20 -3
  7. package/docs/configuration.md +25 -5
  8. package/docs/extension-api.md +124 -18
  9. package/docs/missions.md +8 -0
  10. package/docs/models.md +59 -2
  11. package/docs/observability.md +46 -6
  12. package/docs/standalone-background.md +49 -0
  13. package/docs/tool-reference.md +20 -10
  14. package/docs/watchdog.md +35 -4
  15. package/docs/workflows.md +40 -19
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +2 -1
  18. package/prompts/parallel-review.md +1 -1
  19. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  20. package/skills/pi-subagents/SKILL.md +14 -0
  21. package/skills/pi-subagents/references/execution-controls.md +20 -5
  22. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  23. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  24. package/src/agents/advertised-agent-prompt.ts +94 -0
  25. package/src/agents/agent-management.ts +14 -1
  26. package/src/agents/agent-serializer.ts +2 -0
  27. package/src/agents/agents.ts +14 -0
  28. package/src/agents/builtin-names.ts +1 -0
  29. package/src/api/delegation.ts +4 -0
  30. package/src/api/preflight.ts +76 -45
  31. package/src/api/shared-types.ts +3 -1
  32. package/src/api/workflow-resources.ts +6 -0
  33. package/src/extension/fanout-child.ts +63 -4
  34. package/src/extension/index.ts +58 -8
  35. package/src/extension/public-execution.ts +4 -3
  36. package/src/extension/rpc.ts +8 -21
  37. package/src/extension/schemas.ts +71 -80
  38. package/src/extension/tool-description.ts +29 -81
  39. package/src/inspectors/actions.ts +148 -0
  40. package/src/inspectors/ghostty/actions.ts +74 -0
  41. package/src/inspectors/ghostty/plugin.ts +17 -0
  42. package/src/inspectors/herdr/actions.ts +99 -179
  43. package/src/inspectors/herdr/plugin.ts +20 -0
  44. package/src/inspectors/herdr/project-panes.ts +1 -1
  45. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  46. package/src/inspectors/plugins.ts +8 -0
  47. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  48. package/src/inspectors/types.ts +51 -0
  49. package/src/intercom/intercom-bridge.ts +50 -8
  50. package/src/intercom/native-supervisor-channel.ts +104 -67
  51. package/src/runs/background/active-async-capacity.ts +22 -18
  52. package/src/runs/background/async-execution.ts +45 -56
  53. package/src/runs/background/async-job-tracker.ts +35 -3
  54. package/src/runs/background/async-resume.ts +5 -9
  55. package/src/runs/background/async-status-snapshot.ts +10 -12
  56. package/src/runs/background/async-status.ts +17 -9
  57. package/src/runs/background/auto-drain.ts +44 -30
  58. package/src/runs/background/binary-bootstrap.ts +33 -0
  59. package/src/runs/background/chain-root-attachment.ts +8 -0
  60. package/src/runs/background/control-channel.ts +78 -44
  61. package/src/runs/background/fleet-view.ts +30 -2
  62. package/src/runs/background/notify.ts +117 -13
  63. package/src/runs/background/owned-process-tree.ts +35 -8
  64. package/src/runs/background/process-terminal.ts +23 -23
  65. package/src/runs/background/run-child-session.ts +121 -36
  66. package/src/runs/background/run-status.ts +78 -5
  67. package/src/runs/background/runner-aliases.ts +28 -9
  68. package/src/runs/background/runner-child-launch.ts +88 -0
  69. package/src/runs/background/runner-child-sessions.ts +5 -4
  70. package/src/runs/background/scheduled-runs.ts +40 -13
  71. package/src/runs/background/stale-run-reconciler.ts +3 -1
  72. package/src/runs/background/steering.ts +20 -2
  73. package/src/runs/background/subagent-runner.ts +458 -239
  74. package/src/runs/background/subagent-wait.ts +54 -8
  75. package/src/runs/background/wait-completions.ts +4 -0
  76. package/src/runs/background/wait-tool.ts +1 -1
  77. package/src/runs/foreground/async-steering-action.ts +37 -7
  78. package/src/runs/foreground/execution.ts +145 -56
  79. package/src/runs/foreground/prompt-audit.ts +3 -1
  80. package/src/runs/foreground/subagent-executor.ts +584 -297
  81. package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
  82. package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
  83. package/src/runs/shared/acceptance.ts +7 -4
  84. package/src/runs/shared/agent-contract.ts +1 -1
  85. package/src/runs/shared/async-status-projection.ts +51 -47
  86. package/src/runs/shared/capability-ceiling.ts +2 -0
  87. package/src/runs/shared/child-hooks.ts +167 -3
  88. package/src/runs/shared/child-launch.ts +28 -13
  89. package/src/runs/shared/child-lifecycle.ts +6 -3
  90. package/src/runs/shared/child-runtime-config.ts +3 -1
  91. package/src/runs/shared/child-session.ts +75 -8
  92. package/src/runs/shared/child-tool-plan.ts +124 -5
  93. package/src/runs/shared/completion-evidence.ts +2 -2
  94. package/src/runs/shared/completion-guard.ts +6 -3
  95. package/src/runs/shared/effective-system-prompt.ts +33 -0
  96. package/src/runs/shared/external-cli-runner.ts +9 -7
  97. package/src/runs/shared/host-step-status.ts +11 -11
  98. package/src/runs/shared/llm-intent-arbiter.ts +21 -11
  99. package/src/runs/shared/model-fallback.ts +12 -6
  100. package/src/runs/shared/nested-events.ts +5 -5
  101. package/src/runs/shared/orca-progress-tabs.ts +7 -1
  102. package/src/runs/shared/parallel-handoff.ts +57 -12
  103. package/src/runs/shared/parallel-utils.ts +2 -2
  104. package/src/runs/shared/pi-spawn.ts +10 -0
  105. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  106. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  107. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  108. package/src/runs/shared/run-fanout-budget.ts +8 -8
  109. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  110. package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
  111. package/src/runs/shared/task-intent.ts +46 -13
  112. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  113. package/src/runs/shared/worktree-setup-command.ts +190 -0
  114. package/src/runs/shared/worktree.ts +366 -208
  115. package/src/shared/fork-context.ts +15 -72
  116. package/src/shared/launch-contract.ts +65 -2
  117. package/src/shared/opencode-session-headers.ts +30 -0
  118. package/src/shared/types.ts +85 -61
  119. package/src/shared/utils.ts +7 -2
  120. package/src/shared/workflow-child-permit.ts +18 -13
  121. package/src/slash/delegation-adapters.ts +3 -1
  122. package/src/slash/delegation-request.ts +14 -0
  123. package/src/slash/slash-commands.ts +2 -1
  124. package/src/slash/subagents-admin.ts +11 -4
  125. package/src/tui/fleet-status.ts +164 -19
  126. package/src/tui/fleet.ts +27 -19
  127. package/src/tui/render.ts +172 -33
  128. package/src/watchdog/child-status.ts +8 -0
  129. package/src/watchdog/model-selection.ts +20 -0
  130. package/src/watchdog/permission-arbiter.ts +3 -1
  131. package/src/watchdog/register-child.ts +1 -0
  132. package/src/watchdog/register-main.ts +31 -27
  133. package/src/watchdog/review.ts +132 -67
  134. package/src/watchdog/runtime.ts +82 -20
  135. package/src/watchdog/scope.ts +1 -1
  136. package/src/watchdog/settings.ts +9 -3
  137. package/src/watchdog/tool-actions.ts +13 -12
  138. package/src/watchdog/turn-delta.ts +23 -0
  139. package/src/watchdog/types.ts +4 -0
  140. package/src/workflows/chat-progress.ts +3 -3
  141. package/src/workflows/scripted-workflow.ts +275 -17
  142. package/src/workflows/workflow-checklist.ts +13 -17
  143. package/src/workflows/workflow-child-summary.ts +57 -8
  144. package/src/workflows/workflow-preflight.ts +19 -19
  145. package/src/workflows/workflow-receipt.ts +3 -3
  146. package/src/workflows/workflow-resources.ts +96 -21
  147. package/src/workflows/workflow-settlement.ts +3 -0
  148. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -1,8 +1,15 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import registerFanoutChildSubagentExtension from "../../extension/fanout-child.ts";
2
+ import { listBackgroundWorkProviders } from "../../api/background-work.ts";
3
+ import { ReadonlyDrainObservation } from "./readonly-drain-observation.ts";
4
+ import registerFanoutChildSubagentExtension, { createChildSafeState } from "../../extension/fanout-child.ts";
3
5
  import registerSubagentFastModeExtension from "./fast-mode-extension.ts";
4
6
  import registerSubagentPromptRuntime from "./subagent-prompt-runtime.ts";
5
7
  import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
8
+ import type { ChildToolDiagnostic } from "./tool-availability.ts";
9
+ import type { ChildSessionLaunch } from "./child-session.ts";
10
+ import type { ArbiterModelContext } from "./llm-intent-arbiter.ts";
11
+ import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
12
+ import { projectRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
6
13
 
7
14
  /** Inline extension shape accepted by pi's resource loader (`extensionFactories`). */
8
15
  export interface ChildHookExtension {
@@ -10,16 +17,173 @@ export interface ChildHookExtension {
10
17
  factory: (pi: ExtensionAPI) => void;
11
18
  }
12
19
 
20
+ type OwnedCapture = Required<Pick<ChildRuntimeConfig, "toolDiagnostic" | "runtimeAcknowledgements">>;
21
+ type PromptProof = { config: ChildRuntimeConfig; snapshot: string; factories?: ChildHookExtension["factory"][]; observeNext?: ReadonlyDrainObservation; observation?: ReadonlyDrainObservation; capture?: OwnedCapture; reporting?: { launch: ChildSessionLaunch; callback: ChildSessionLaunch["onExtensionError"] } };
22
+ const promptProofs = new WeakMap<ChildHookExtension["factory"], PromptProof>();
23
+
24
+ function ownedProof(hooks: ChildHookExtension[]): PromptProof | undefined {
25
+ const proof = hooks[0] && promptProofs.get(hooks[0].factory);
26
+ return proof?.factories?.length === hooks.length && proof.factories.every((factory, index) => hooks[index]?.factory === factory) ? proof : undefined;
27
+ }
28
+
29
+ /** The host's mandatory transcript reporting, owned by the existing hook certificate. */
30
+ export function withChildSessionErrorReporting(session: Omit<ChildSessionLaunch, "onExtensionError">, transcriptWriter: ChildTranscriptWriter | undefined): ChildSessionLaunch {
31
+ const launch: ChildSessionLaunch = { ...session, onExtensionError: (error) => {
32
+ transcriptWriter?.writeStderrLine(`Extension error (${error.extensionPath}, ${error.event}): ${error.error instanceof Error ? error.error.message : String(error.error)}`);
33
+ } };
34
+ const proof = ownedProof(launch.hooks);
35
+ if (proof?.capture && proof.config === launch.runtime) proof.reporting = { launch, callback: launch.onExtensionError };
36
+ return launch;
37
+ }
38
+
39
+ export function isReadonlyChildSessionReporting(launch: ChildSessionLaunch): boolean {
40
+ const proof = ownedProof(launch.hooks);
41
+ const descriptor = Object.getOwnPropertyDescriptor(launch, "onExtensionError");
42
+ if (descriptor && (!descriptor.enumerable || !("value" in descriptor))) return false;
43
+ return proof?.reporting ? proof.reporting.launch === launch && proof.reporting.callback === descriptor?.value : !descriptor && !("onExtensionError" in launch);
44
+ }
45
+
46
+ /** Admit only own enumerable data: inspecting descriptors must not invoke getters. */
47
+ function dataKeys(value: unknown): string[] | undefined {
48
+ if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) return undefined;
49
+ const keys = Reflect.ownKeys(value);
50
+ for (const key of keys) {
51
+ const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
52
+ if (typeof key !== "string" || !descriptor.enumerable || !("value" in descriptor)) return undefined;
53
+ }
54
+ return keys as string[];
55
+ }
56
+
57
+ /** Only reviewed data and the capture callbacks constructed below; no caller callback opt-in. */
58
+ function readonlyConfig(config: ChildRuntimeConfig, capture?: OwnedCapture): string | undefined {
59
+ const strings = ["runId", "agent", "sessionName", "forkCacheKey", "parentSessionId", "orchestratorSessionId"];
60
+ const numbers = ["childIndex", "depth", "maxDepth"];
61
+ const booleans = ["inheritProjectContext", "inheritGlobalContext", "inheritSkills"];
62
+ const keys = dataKeys(config);
63
+ if (!keys) return undefined;
64
+ if (capture && (config.toolDiagnostic !== capture.toolDiagnostic || config.runtimeAcknowledgements !== capture.runtimeAcknowledgements)) return undefined;
65
+ const waitKeys = dataKeys(config.waitTool);
66
+ if (!waitKeys || waitKeys.some((key) => key !== "enabled")) return undefined;
67
+ if (config.fast !== false || config.fanoutChild !== false || config.waitTool.enabled !== false) return undefined;
68
+ for (const key of keys) {
69
+ const value = Object.getOwnPropertyDescriptor(config, key)!.value;
70
+ if (["fast", "fanoutChild", "waitTool"].includes(key)) continue;
71
+ if (capture && (key === "toolDiagnostic" || key === "runtimeAcknowledgements")) continue;
72
+ if (capture && key === "requiredTools" && Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype) {
73
+ const descriptors = Object.getOwnPropertyDescriptors(value);
74
+ if (Reflect.ownKeys(descriptors).length !== value.length + 1) return undefined;
75
+ for (let i = 0; i < value.length; i++) {
76
+ const descriptor = descriptors[String(i)];
77
+ if (!descriptor?.enumerable || !("value" in descriptor) || !["read", "ls"].includes(descriptor.value)) return undefined;
78
+ }
79
+ continue;
80
+ }
81
+ if (strings.includes(key) && (value === undefined || typeof value === "string")) continue;
82
+ if (numbers.includes(key) && (value === undefined || (typeof value === "number" && Number.isInteger(value) && value >= 0))) continue;
83
+ if (booleans.includes(key) && (value === undefined || typeof value === "boolean")) continue;
84
+ return undefined;
85
+ }
86
+ return JSON.stringify(config);
87
+ }
88
+
89
+ function noBackgroundProviders(): boolean {
90
+ try {
91
+ return listBackgroundWorkProviders().length === 0;
92
+ } catch { return false; }
93
+ }
94
+
95
+ /** Internal proof of the captured closure/config, not its caller-controlled display name. */
96
+ export function isReadonlyChildHookProfile(hooks: ChildHookExtension[], config: ChildRuntimeConfig): boolean {
97
+ const proof = ownedProof(hooks);
98
+ if (!proof) return false;
99
+ const valid = proof.config === config && readonlyConfig(config, proof.capture) === proof.snapshot && noBackgroundProviders();
100
+ if (!valid) proof.observation?.deny();
101
+ return valid && (proof.observation?.check() ?? true);
102
+ }
103
+
104
+ /** Arm just the next installation; ordinary installations retain the original API/handlers. */
105
+ export function observeReadonlyChildHookDrain(hooks: ChildHookExtension[], enabled: boolean, sessionFile: string): void {
106
+ const proof = ownedProof(hooks);
107
+ if (!proof) return;
108
+ if (enabled) {
109
+ proof.observation?.deny();
110
+ proof.observation = undefined;
111
+ }
112
+ proof.observeNext = enabled ? new ReadonlyDrainObservation(sessionFile, () => readonlyConfig(proof.config, proof.capture) === proof.snapshot && noBackgroundProviders()) : undefined;
113
+ }
114
+
115
+ export function captureReadonlyChildDrain(hooks: ChildHookExtension[]): (() => boolean) {
116
+ const proof = ownedProof(hooks);
117
+ const observation = proof?.observation;
118
+ return () => !!observation && proof?.observation === observation && observation.settled();
119
+ }
120
+
13
121
  /**
14
122
  * The child-side hooks pi-subagents installs in every child, keyed off the
15
123
  * launch config. The registrations live in `subagent-prompt-runtime.ts`,
16
124
  * `fast-mode-extension.ts`, and `fanout-child.ts`.
17
125
  */
18
126
  export function createChildHooks(config: ChildRuntimeConfig): ChildHookExtension[] {
127
+ return childHooks(config);
128
+ }
129
+
130
+ /** Launch-owned bookkeeping, paired with the same private hook certificate (no callback registration API). */
131
+ export function createCapturedChildHooks(config: ChildRuntimeConfig, runner = false) {
132
+ let diagnostic: ChildToolDiagnostic | undefined;
133
+ let acknowledgedIds: string[] | undefined;
134
+ let completionIntentContext: ArbiterModelContext | undefined;
135
+ let finalDrainHeld = false;
136
+ const capture: OwnedCapture = {
137
+ toolDiagnostic: (value) => { diagnostic = value; },
138
+ runtimeAcknowledgements: (ids) => { acknowledgedIds = ids; },
139
+ };
140
+ Object.assign(config, capture);
141
+ const hooks = childHooks(config, capture, (held) => { finalDrainHeld = held; });
142
+ if (runner) {
143
+ hooks.push({ name: "pi-subagents:completion-intent", factory: (pi) => pi.on("session_start", (_event, childCtx) => {
144
+ // Retain only attempt model services and the session id string, not the live child session.
145
+ completionIntentContext = {
146
+ model: childCtx.model,
147
+ modelRegistry: childCtx.modelRegistry,
148
+ sessionId: childCtx.sessionManager.getSessionId(),
149
+ };
150
+ }) });
151
+ const proof = promptProofs.get(hooks[0]!.factory);
152
+ if (proof) proof.factories = hooks.map((hook) => hook.factory);
153
+ }
154
+ return {
155
+ hooks,
156
+ completionIntentContext: () => completionIntentContext,
157
+ toolDiagnostic: () => diagnostic,
158
+ runtimeAcknowledgedExtensions: () => acknowledgedIds ? projectRuntimeAcknowledgedExtensions(acknowledgedIds) : undefined,
159
+ finalDrainHeld: () => finalDrainHeld,
160
+ };
161
+ }
162
+
163
+ function childHooks(config: ChildRuntimeConfig, capture?: OwnedCapture, holdFinalDrain?: (held: boolean) => void): ChildHookExtension[] {
164
+ const snapshot = readonlyConfig(config, capture);
165
+ const proof: PromptProof | undefined = snapshot === undefined ? undefined : { config, snapshot, capture };
166
+ const runtime = Object.create(config) as ChildRuntimeConfig;
167
+ const ownedState = Object.getOwnPropertyDescriptor(config, "runtimeState");
168
+ if (!ownedState || !("value" in ownedState) || ownedState.value == null) {
169
+ Object.defineProperty(runtime, "runtimeState", { configurable: true, enumerable: true, writable: true, value: createChildSafeState() });
170
+ }
171
+ if (holdFinalDrain) {
172
+ Object.defineProperty(runtime, "holdFinalDrain", { configurable: true, enumerable: true, writable: true, value: holdFinalDrain });
173
+ }
19
174
  const hooks: ChildHookExtension[] = [
20
- { name: "pi-subagents:prompt-runtime", factory: (pi) => registerSubagentPromptRuntime(pi, config) },
175
+ { name: "pi-subagents:prompt-runtime", factory: function promptRuntime(pi) {
176
+ if (!proof?.observeNext) return registerSubagentPromptRuntime(pi, runtime);
177
+ proof.observation = proof.observeNext;
178
+ proof.observeNext = undefined;
179
+ registerSubagentPromptRuntime(pi, runtime, proof.observation);
180
+ } },
21
181
  ];
182
+ if (proof) {
183
+ proof.factories = hooks.map((hook) => hook.factory);
184
+ promptProofs.set(hooks[0]!.factory, proof);
185
+ }
22
186
  if (config.fast) hooks.push({ name: "pi-subagents:fast-mode", factory: (pi) => registerSubagentFastModeExtension(pi) });
23
- if (config.fanoutChild) hooks.push({ name: "pi-subagents:fanout-child", factory: (pi) => registerFanoutChildSubagentExtension(pi, config) });
187
+ if (config.fanoutChild) hooks.push({ name: "pi-subagents:fanout-child", factory: (pi) => registerFanoutChildSubagentExtension(pi, runtime) });
24
188
  return hooks;
25
189
  }
@@ -11,7 +11,7 @@ import type { ThinkingLevel } from "../../shared/model-info.ts";
11
11
  import { intersectThinkingCeilings } from "../../shared/thinking-ceiling.ts";
12
12
  import {
13
13
  resolveChildDepth,
14
- type LaunchResolvedChildExtensionsV1,
14
+ type LaunchResolvedChildExtensions,
15
15
  type ResolvedToolBudget,
16
16
  type RunFanoutBudgetDescriptor,
17
17
  } from "../../shared/types.ts";
@@ -20,8 +20,7 @@ import type { McpRuntimeSnapshotHost } from "./mcp-direct-tool-allowlist.ts";
20
20
  import type { PermissionRules } from "./permissions.ts";
21
21
  import type { StructuredOutputRuntime } from "./structured-output.ts";
22
22
  import type { ChildToolDiagnostic } from "./tool-availability.ts";
23
- import type { RuntimeAcknowledgedChildExtensionsV1 } from "../../shared/types.ts";
24
- import { projectRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
23
+ import type { RuntimeAcknowledgedChildExtensions } from "../../shared/types.ts";
25
24
  import { encodeExtensionBindings, PI_SUBAGENT_EXTENSION_BINDINGS_ENV, type ExtensionBindings } from "./extension-bindings.ts";
26
25
  import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "./capability-ceiling.ts";
27
26
  import {
@@ -32,8 +31,10 @@ import {
32
31
  type PiLaunchToolPlan,
33
32
  } from "./child-tool-plan.ts";
34
33
  import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
35
- import { createChildHooks } from "./child-hooks.ts";
34
+ import { createCapturedChildHooks, withChildSessionErrorReporting } from "./child-hooks.ts";
35
+ import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
36
36
  import type { ChildSessionLaunch, ChildSessionStorage } from "./child-session.ts";
37
+ import type { ArbiterModelContext } from "./llm-intent-arbiter.ts";
37
38
 
38
39
  /** Environment variable pi-mcp-adapter reads for the tools a child may expose. */
39
40
  export const MCP_DIRECT_TOOLS_ENV = "MCP_DIRECT_TOOLS";
@@ -111,12 +112,21 @@ export interface BuildInProcessChildLaunchInput {
111
112
  * them and exposes the child environment external extensions read.
112
113
  */
113
114
  host: "parent" | "runner";
115
+ /**
116
+ * Builtin tool names the host runtime provides. When set, child tool plans
117
+ * intersect declared agent tools with this set, omitting tools the host
118
+ * cannot provide. Review/scout lanes fail closed when a requested,
119
+ * still-permitted repository inspection tool is missing from that set.
120
+ */
121
+ hostAvailableBuiltins?: readonly string[];
114
122
  }
115
123
 
116
124
  export interface InProcessChildCapture {
125
+ completionIntentContext?(): ArbiterModelContext | undefined;
117
126
  structuredOutput(): { called: boolean; value?: unknown; acceptanceReport?: unknown; acceptanceReportProvided: boolean };
118
127
  toolDiagnostic(): ChildToolDiagnostic | undefined;
119
- runtimeAcknowledgedExtensions(): RuntimeAcknowledgedChildExtensionsV1 | undefined;
128
+ runtimeAcknowledgedExtensions(): RuntimeAcknowledgedChildExtensions | undefined;
129
+ finalDrainHeld(): boolean;
120
130
  }
121
131
 
122
132
  export interface InProcessChildLaunch {
@@ -124,11 +134,16 @@ export interface InProcessChildLaunch {
124
134
  config: ChildRuntimeConfig;
125
135
  session: Omit<ChildSessionLaunch, "onExtensionError">;
126
136
  capture: InProcessChildCapture;
127
- launchResolvedExtensions: LaunchResolvedChildExtensionsV1;
137
+ launchResolvedExtensions: LaunchResolvedChildExtensions;
128
138
  warnings: string[];
129
139
  capabilityAudit?: SubagentCapabilityAudit;
130
140
  }
131
141
 
142
+ /** Actual host create-input boundary. Evidence remains explicitly opt-in and dormant in production. */
143
+ export function createReportedChildSessionInput(launch: InProcessChildLaunch, transcriptWriter?: ChildTranscriptWriter): ChildSessionLaunch {
144
+ return withChildSessionErrorReporting(launch.session, transcriptWriter);
145
+ }
146
+
132
147
  /** Escape XML-significant characters in a string for safe attribute interpolation. */
133
148
  function escapeXmlAttr(value: string): string {
134
149
  return value
@@ -185,6 +200,7 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
185
200
  agentName: input.childAgentName,
186
201
  permissionRules: input.permissionRules,
187
202
  runtimeSnapshotHost: input.runtimeSnapshotHost,
203
+ hostAvailableBuiltins: input.hostAvailableBuiltins,
188
204
  });
189
205
 
190
206
  const inherited = input.inherited;
@@ -214,8 +230,6 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
214
230
  let structuredAcceptanceReport: unknown;
215
231
  let structuredCalled = false;
216
232
  let structuredAcceptanceProvided = false;
217
- let toolDiagnostic: ChildToolDiagnostic | undefined;
218
- let acknowledgedIds: string[] | undefined;
219
233
 
220
234
  const config: ChildRuntimeConfig = {
221
235
  ...(input.runId ? { runId: input.runId } : {}),
@@ -264,10 +278,9 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
264
278
  : {}),
265
279
  ...(toolPlan.requiredChildTools.length > 0 ? { requiredTools: toolPlan.requiredChildTools } : {}),
266
280
  ...(toolPlan.effectiveMcpTools.length > 0 ? { mcpDirectTools: toolPlan.effectiveMcpTools } : {}),
267
- toolDiagnostic: (diagnostic) => { toolDiagnostic = diagnostic; },
268
- runtimeAcknowledgements: (ids) => { acknowledgedIds = ids; },
269
281
  fast: input.fast === true,
270
282
  };
283
+ const capturedHooks = createCapturedChildHooks(config, input.host === "runner");
271
284
 
272
285
  const extensionPaths = toolPlan.extensionArgs.filter((extensionPath) => !isSubagentRuntimeExtensionPath(extensionPath));
273
286
  const ambientExtensions = input.host === "runner" && !toolPlan.disableAmbientExtensions;
@@ -288,7 +301,7 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
288
301
  ...(!toolPlan.explicitToolAllowlist && toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
289
302
  extensionPaths,
290
303
  ambientExtensions,
291
- hooks: createChildHooks(config),
304
+ hooks: capturedHooks.hooks,
292
305
  ...(input.host === "runner" ? { processEnv: childProcessEnv(input, toolPlan) } : {}),
293
306
  runtime: config,
294
307
  noSkills: !input.inheritSkills,
@@ -303,9 +316,11 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
303
316
  config,
304
317
  session,
305
318
  capture: {
319
+ completionIntentContext: capturedHooks.completionIntentContext,
306
320
  structuredOutput: () => ({ called: structuredCalled, value: structuredValue, acceptanceReport: structuredAcceptanceReport, acceptanceReportProvided: structuredAcceptanceProvided }),
307
- toolDiagnostic: () => toolDiagnostic,
308
- runtimeAcknowledgedExtensions: () => (acknowledgedIds ? projectRuntimeAcknowledgedExtensions(acknowledgedIds) : undefined),
321
+ toolDiagnostic: capturedHooks.toolDiagnostic,
322
+ runtimeAcknowledgedExtensions: capturedHooks.runtimeAcknowledgedExtensions,
323
+ finalDrainHeld: capturedHooks.finalDrainHeld,
309
324
  },
310
325
  launchResolvedExtensions,
311
326
  warnings: toolPlan.warnings,
@@ -14,11 +14,14 @@ export function projectChildLifecycle(event: { type?: string; willRetry?: unknow
14
14
  if (state) state.compactionRetryActive = event.willRetry === true;
15
15
  return event.willRetry === true ? "cancel-drain" : "none";
16
16
  }
17
- if (event.type === "agent_start" || event.type === "auto_retry_start") {
17
+ if (event.type === "agent_start" || event.type === "auto_retry_start" || event.type === "turn_start") {
18
18
  if (state) state.compactionRetryActive = false;
19
+ return "cancel-drain";
20
+ }
21
+ if (event.type === "agent_end") {
22
+ if (event.willRetry !== true && state) state.compactionRetryActive = false;
23
+ return "cancel-drain";
19
24
  }
20
- if (event.type === "agent_end" && event.willRetry === true) return "cancel-drain";
21
- if (event.type === "agent_end" && state) state.compactionRetryActive = false;
22
25
  if (event.type === "agent_settled") return state?.compactionRetryActive ? "none" : "start-drain";
23
26
  if (terminalAssistantStop) return "start-drain";
24
27
  return "none";
@@ -1,4 +1,4 @@
1
- import type { JsonSchemaObject, ResolvedToolBudget, RunFanoutBudgetDescriptor } from "../../shared/types.ts";
1
+ import type { JsonSchemaObject, ResolvedToolBudget, RunFanoutBudgetDescriptor, SubagentState } from "../../shared/types.ts";
2
2
  import type { ThinkingLevel } from "../../shared/model-info.ts";
3
3
  import type { NestedPathEntry } from "./nested-path.ts";
4
4
  import type { PermissionRules } from "./permissions.ts";
@@ -86,6 +86,8 @@ export interface ChildRuntimeConfig {
86
86
  /** Receives child watchdog status events. */
87
87
  watchdogStatus?: (event: ChildWatchdogStatusEvent) => void;
88
88
  waitTool: ResolvedWaitToolConfig;
89
+ runtimeState?: SubagentState;
90
+ holdFinalDrain?: (held: boolean) => void;
89
91
  structuredOutput?: ChildStructuredOutput;
90
92
  requiredTools?: string[];
91
93
  mcpDirectTools?: string[];
@@ -12,6 +12,14 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core";
12
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
  import { getAgentDir } from "../../shared/utils.ts";
14
14
  import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
15
+ import { prepareReadonlySessionEvidence } from "./readonly-session-evidence.ts";
16
+ import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
17
+
18
+ // Private runtime authority for host continuation planning; injected factories have none.
19
+ const readonlyModels = new WeakMap<ChildSession, { current: ModelInfo; resolve(reference: string): ModelInfo | undefined; requestBytes: number }>();
20
+ export function getReadonlyChildModels(child: ChildSession) {
21
+ return readonlyModels.get(child);
22
+ }
15
23
 
16
24
  export interface ChildSessionEvent {
17
25
  type: string;
@@ -85,6 +93,8 @@ export interface ChildSession {
85
93
  abort(): Promise<void>;
86
94
  /** Emits `session_shutdown` to the child's extensions and disposes the session; resolves once that shutdown work is done. */
87
95
  dispose(): Promise<void>;
96
+ /** True while Pi still has steering or follow-up input that has not started a turn. */
97
+ hasQueuedMessages?(): boolean;
88
98
  readonly messages: readonly AgentMessage[];
89
99
  readonly sessionFile: string | undefined;
90
100
  readonly sessionId: string;
@@ -95,6 +105,14 @@ export interface ChildSession {
95
105
  shutDown?: boolean;
96
106
  }
97
107
 
108
+ export function childSessionHasQueuedMessages(session: ChildSession | undefined): boolean {
109
+ try {
110
+ return session?.hasQueuedMessages?.() === true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
98
116
  export interface ChildSessionFactory {
99
117
  create(launch: ChildSessionLaunch): Promise<ChildSession>;
100
118
  /** Abort and dispose every live attached child; detached children keep running and hold the shared runtime. */
@@ -116,6 +134,21 @@ export interface DefaultChildSessionFactoryOptions {
116
134
 
117
135
  type ModelRuntimeInstance = Awaited<ReturnType<PiCodingAgentModule["ModelRuntime"]["create"]>>;
118
136
 
137
+ const CHILD_PROMPT_RUNTIME_EXTENSION_PATH = "<inline:pi-subagents:prompt-runtime>";
138
+
139
+ /** The prompt runtime filters parent-only context before ambient extensions inspect
140
+ * the child prompt. Other inline hooks keep their normal position after ambient
141
+ * extensions, and ambient extension order stays unchanged. */
142
+ function prioritizeChildPromptRuntime<T extends { extensions: Array<{ path: string }> }>(result: T): T {
143
+ const index = result.extensions.findIndex(({ path }) => path === CHILD_PROMPT_RUNTIME_EXTENSION_PATH);
144
+ if (index <= 0) return result;
145
+ const extensions = [...result.extensions];
146
+ const [promptRuntime] = extensions.splice(index, 1);
147
+ if (!promptRuntime) return result;
148
+ extensions.unshift(promptRuntime);
149
+ return { ...result, extensions };
150
+ }
151
+
119
152
  /** One launch at a time from env application through `session_start`, so parallel launches never observe each other's `processEnv` while their extensions load and start. */
120
153
  let loading: Promise<unknown> = Promise.resolve();
121
154
 
@@ -185,12 +218,17 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
185
218
  };
186
219
  return {
187
220
  async create(launch) {
221
+ const observeReadonly = prepareReadonlySessionEvidence(launch);
188
222
  const pi = await loadPiCodingAgent();
189
223
  const modelRuntime = await sharedRuntime(pi);
190
224
  const agentDir = getAgentDir();
191
225
  const settingsManager = pi.SettingsManager.create(launch.cwd, agentDir);
192
- // Headless sessions skip Pi's CLI theme setup; extensions still need ctx.ui.theme.
193
- if (typeof pi.initTheme === "function") pi.initTheme(settingsManager.getTheme());
226
+ // Foreground children share Pi's global theme with the parent, so reinitializing it
227
+ // would overwrite the parent's active light/dark appearance. Detached runners have
228
+ // no initialized theme and must initialize one for headless extension renderers.
229
+ const themeKey = Symbol.for("@earendil-works/pi-coding-agent:theme");
230
+ const themeInitialized = Boolean((globalThis as Record<symbol, unknown>)[themeKey]);
231
+ if (!themeInitialized && typeof pi.initTheme === "function") pi.initTheme(settingsManager.getTheme());
194
232
  const loader = new pi.DefaultResourceLoader({
195
233
  cwd: launch.cwd,
196
234
  agentDir,
@@ -202,14 +240,18 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
202
240
  noContextFiles: launch.noContextFiles,
203
241
  additionalExtensionPaths: launch.extensionPaths,
204
242
  extensionFactories: launch.hooks,
243
+ extensionsOverride: prioritizeChildPromptRuntime,
205
244
  ...(launch.systemPrompt !== undefined ? { systemPrompt: launch.systemPrompt } : {}),
206
245
  ...(launch.appendSystemPrompt !== undefined ? { appendSystemPrompt: [launch.appendSystemPrompt] } : {}),
207
246
  });
208
247
  const open = async () => {
209
248
  applyProcessEnv(launch.processEnv);
210
249
  if (!resetExtensionCacheOnReload(loader) && (launch.ambientExtensions || launch.extensionPaths.length)) launch.onExtensionError?.({ extensionPath: "<loader>", event: "load", error: new Error("pi's extension cache reset is unavailable; extensions loaded into this child share module state with other sessions in this process.") });
211
- await loader.reload();
250
+ observeReadonly?.loadingHooks(true);
251
+ try { await loader.reload(); } finally { observeReadonly?.loadingHooks(false); }
212
252
  await flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError);
253
+ // No await between receipt validation and the SDK's permissive file open.
254
+ observeReadonly?.beforeOpen();
213
255
  const sessionManager = launch.storage.kind === "file"
214
256
  ? pi.SessionManager.open(launch.storage.sessionFile, undefined, launch.cwd)
215
257
  : launch.storage.kind === "dir"
@@ -217,6 +259,7 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
217
259
  : launch.storage.kind === "memory"
218
260
  ? pi.SessionManager.inMemory(launch.cwd)
219
261
  : pi.SessionManager.create(launch.cwd);
262
+ observeReadonly?.opened(sessionManager);
220
263
  const resolvedModel = launch.model
221
264
  ? pi.resolveCliModel({ cliModel: launch.model, modelRuntime })
222
265
  : undefined;
@@ -248,6 +291,9 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
248
291
  const opened = loading.catch(() => {}).then(open);
249
292
  loading = opened;
250
293
  const session = await opened;
294
+ let evidence: ReturnType<NonNullable<typeof observeReadonly>["observe"]>;
295
+ try { evidence = observeReadonly?.observe(pi, modelRuntime, session); }
296
+ catch (error) { session.dispose(); throw error; }
251
297
  let pending: Promise<void> | undefined;
252
298
  // pi's own hosts emit `session_shutdown` before disposing a session so the
253
299
  // extensions loaded into it (ambient extensions included) release their
@@ -255,19 +301,30 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
255
301
  const shutdown = async (): Promise<void> => {
256
302
  try {
257
303
  const runner = session.extensionRunner;
258
- if (runner.hasHandlers("session_shutdown")) await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }), new Promise((resolve) => setTimeout(resolve, shutdownTimeoutMs).unref?.())]);
304
+ if (runner.hasHandlers("session_shutdown")) {
305
+ evidence?.beforeShutdown();
306
+ const settled = await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }).then(() => true), new Promise<false>((resolve) => setTimeout(() => resolve(false), shutdownTimeoutMs).unref?.())]);
307
+ if (!settled) evidence?.invalidate();
308
+ }
259
309
  } catch (error) {
310
+ evidence?.invalidate();
260
311
  launch.onExtensionError?.({ extensionPath: "<session>", event: "session_shutdown", error });
261
312
  } finally {
262
313
  session.dispose();
314
+ evidence?.finish(child);
263
315
  }
264
316
  };
265
317
  const child: ChildSession = {
266
318
  subscribe: (listener) => session.subscribe((event) => listener(event as unknown as ChildSessionEvent)),
267
- prompt: (text) => session.prompt(text),
268
- steer: (text) => session.steer(text),
269
- followUp: (text) => session.followUp(text),
270
- abort: () => session.abort(),
319
+ prompt: (text) => {
320
+ if (!evidence) return session.prompt(text);
321
+ try { evidence.start(); } catch (error) { return Promise.reject(error); }
322
+ return session.prompt(text).then(() => evidence?.settled(), (error) => { evidence?.invalidate(); throw error; });
323
+ },
324
+ steer: (text) => { evidence?.invalidate(); return session.steer(text); },
325
+ followUp: (text) => { evidence?.invalidate(); return session.followUp(text); },
326
+ abort: () => { evidence?.invalidate(); return session.abort(); },
327
+ hasQueuedMessages: () => session.agent?.hasQueuedMessages?.() === true,
271
328
  dispose: () => {
272
329
  if (!pending) {
273
330
  live.delete(child);
@@ -283,6 +340,16 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
283
340
  get sessionId() { return session.sessionId; },
284
341
  get modelId() { return session.model ? `${session.model.provider}/${session.model.id}` : undefined; },
285
342
  };
343
+ if (evidence && session.model) readonlyModels.set(child, {
344
+ current: toModelInfo(session.model),
345
+ requestBytes: Buffer.byteLength(session.systemPrompt) + Buffer.byteLength(JSON.stringify(session.agent.state.tools)),
346
+ resolve(reference) {
347
+ try {
348
+ const resolved = pi.resolveCliModel({ cliModel: reference, modelRuntime });
349
+ return !resolved.error && resolved.model ? toModelInfo(resolved.model) : undefined;
350
+ } catch { return undefined; }
351
+ },
352
+ });
286
353
  live.add(child);
287
354
  return child;
288
355
  },