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
@@ -7,6 +7,7 @@
7
7
  * - `classifyTaskMutationIntent` / `expectsImplementationMutation`: does the
8
8
  * task REQUIRE file changes? Consumed by the completion mutation guard,
9
9
  * which blocks completion, so its vocabulary is deliberately narrow.
10
+ * Verbs inside filenames and path-like tokens do not count.
10
11
  * - `taskMayMutate`: COULD the task plausibly change files? Consumed by
11
12
  * acceptance level inference, which only raises evidence gates, so its
12
13
  * vocabulary is deliberately broad (any bare write verb).
@@ -18,16 +19,20 @@
18
19
  */
19
20
 
20
21
  const REVIEW_ONLY_PATTERNS = [
21
- /\breview only\b/i,
22
+ /\b(?:review|read)[- ]only\b/i,
23
+ /\bno\s+(?:source\s+)?edits?\b/i,
24
+ /\bwithout\s+edits?\b/i,
22
25
  /\bsuggest fixes only\b/i,
23
26
  /\bonly return findings\b/i,
24
27
  /\breturn findings only\b/i,
25
28
  ];
26
29
 
30
+ const NO_EDIT_BOUNDARY_ASSERTION_PATTERN = /\bno\s+(?:source\s+)?edits?\s*,\s*(?:commits?|pushes?|merges?|installs?|changes?|writes?)\b/i;
31
+
27
32
  const REVIEWER_REQUIRED_EDIT_PATTERNS = [
28
33
  /\bmust\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
29
34
  /\brequired\s+to\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
30
- /(?:^|[.!?\n]\s*)implement\s+(?:the\s+)?(?:approved|requested|specified|file|code|source|fix(?:es)?|changes?)\b/i,
35
+ /(?:^|[.!?:;,\n]\s*)implement\s+(?:the\s+)?(?:approved|requested|specified|file|code|source|fix(?:es)?|changes?)\b/i,
31
36
  /\bregardless\s+of\s+findings\b/i,
32
37
  /\balways\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
33
38
  /\bapply\s+(?:the\s+)?fix(?:es)?\s+directly\b/i,
@@ -40,7 +45,7 @@ const REVIEWER_REQUIRED_EDIT_PATTERNS = [
40
45
  // being swallowed as the object.
41
46
  // Accept serialized line separators too: workflow prompts can carry literal
42
47
  // `\\n`/`\\r\\n` between clauses instead of decoded newlines.
43
- const NO_EDIT_PROHIBITION_PATTERN = /(?:\b|\\(?:r\\n|n))(?:do not|don't|must not)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b((?:(?!\b(?:but|and|then)\b|\\(?:r\\n|n))[^.;,:!?\n–—-])*)/gi;
48
+ const NO_EDIT_PROHIBITION_PATTERN = /(?:\b|\\(?:r\\n|n))(?:do not|don't|must not)\s+(?:edit|modify|write(?:\s+to)?|touch|change|implement)\b((?:(?!\b(?:but|and|then)\b|\\(?:r\\n|n))[^.;,:!?\n–—-])*)/gi;
44
49
  const COORDINATED_NO_EDIT_PROHIBITION_PATTERN = /(?:\b|\\(?:r\\n|n))(?:do not|don't|must not)\s+((?=(?:(?!\\(?:r\\n|n))[^.;:!?\n–—-])*\b(?:and|or)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b)(?:(?!\\(?:r\\n|n))[^.;:!?\n–—-])*?\b(?:and|or)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b(?:(?!\b(?:but|and|then)\b|\\(?:r\\n|n))[^.;,:!?\n–—-])*)/gi;
45
50
 
46
51
  /** Objects of a no-edit prohibition that mean "the codebase in general" rather than a named scope. */
@@ -85,13 +90,25 @@ const RESEARCH_AGENT_PATTERNS = [
85
90
  // CLI flags like "--fix" and genuine clause-level dashes like "branch—fix it"),
86
91
  // strip the known severity compounds (must|should|needs + dash + verb) from the
87
92
  // task text before matching. Dash coverage: ASCII hyphen + U+2010..U+2015.
88
- const SEVERITY_COMPOUND_PATTERN = /\b(?:must|should|needs)[\-\u2010-\u2015](?:fix|edit|update|add|remove|replace|create|apply|make|do|implement|modify|delete|patch)\b/gi;
93
+ const SEVERITY_COMPOUND_PATTERN = /\b(?:must|should|needs)[-\u2010-\u2015](?:fix|edit|update|add|remove|replace|create|apply|make|do|implement|modify|delete|patch)\b/gi;
89
94
 
90
95
  function stripSeverityCompounds(task: string): string {
91
96
  return task.replace(SEVERITY_COMPOUND_PATTERN, " ");
92
97
  }
93
98
  export { stripSeverityCompounds };
94
99
 
100
+ // Quoted categories in a finding-classification request describe fixes, not
101
+ // instructions to perform them. Strip only that request, leaving sibling
102
+ // imperatives (including another "must fix") subject to the normal guards.
103
+ const FINDING_CLASSIFICATION_PATTERN = /\b(?:classify|categorize|weigh)\s+(?:(?:the|remaining)\s+)*(?:findings?|items?|issues?)\s+as\s+["“]must\s+fix\s+before\s+[^"”\n]+["”]\s+(?:vs\.?|versus)\s+["“]must\s+fix\s+before\s+[^"”\n]+["”]/gi;
104
+
105
+ const PATH_LIKE_TOKEN_PATTERN = /[^\s]+[/\\][^\s]+|[^\s/\\]+\.[A-Za-z][A-Za-z0-9]{0,9}\b/g;
106
+ const PATH_INTERNAL_IMPLEMENTATION_VERB = /\b(?:implement|edit|modify|refactor|delete|update|add|remove|replace|create)\b/i;
107
+
108
+ function stripPathLikeTokens(task: string): string {
109
+ return task.replace(PATH_LIKE_TOKEN_PATTERN, (token) => (PATH_INTERNAL_IMPLEMENTATION_VERB.test(token) ? " " : token));
110
+ }
111
+
95
112
  const FIX_OR_PATCH_IMPLEMENTATION_PATTERN = /\b(?:fix|patch)\s+(?:(?:it|this|that|them|each|any|all|these|those)\b|(?:(?:a|an|the|any|all)\s+)?(?:(?:failing|failed|broken|flaky|red|cold|start|current|existing|reported|approved|known|regression|unit|integration|e2e|source|typescript|type-?script|ts|type-?check|compiler)\s+)*(?:bug|defect|issues?|problems?|failures?|regressions?|tests?|errors?|items?|typos?|code|source|implementation|component|function|module|class|method|logic|file|files|readme|docs?|changelog|package\.json|config|manifest|extension|prompt|command|lint(?:ing)?|build|ci|type-?check|type\s+checking)\b)/i;
96
113
 
97
114
  const WORKER_IMPLEMENTATION_PATTERNS = [
@@ -103,12 +120,18 @@ const WORKER_IMPLEMENTATION_PATTERNS = [
103
120
  /\bdo those fixes\b/i,
104
121
  ];
105
122
 
106
- const GENERAL_IMPLEMENTATION_PATTERNS = [
123
+ const FOLLOW_ON_IMPLEMENTATION_PATTERN = /(?:^|[.!?:;,\n])\s*(?:fix|patch|update|add|remove|replace|create|delete)\s+(?:(?:the|a|an|this|that|these|those|requested|specified|current|existing|approved|your|our)\s+)(?!(?:report|summary|findings?|analysis|recommendations?|answer|response|proposal|plan|issue|bug report)\b)[a-z][\w./-]*/i;
124
+ const ADVISORY_INFINITIVE_PATTERN = /\b(?:explain|recommend|describe)\s+how\s+to\s+(?:fix|patch|update|add|remove|replace|create|delete|implement|edit|modify|refactor)\b/i;
125
+ const EXPLICIT_IMPLEMENTATION_PATTERNS = [
107
126
  /\b(?:implement|edit|modify|refactor)\b/i,
108
- FIX_OR_PATCH_IMPLEMENTATION_PATTERN,
109
127
  /\bapply\s+(?:the\s+)?(?:(?:suggested|proposed|recommended)\s+)?(?:changes?|fix(?:es)?|patch)\b/i,
110
128
  /\bmake\s+(?:the\s+)?changes\b/i,
111
129
  /\bdo those fixes\b/i,
130
+ ];
131
+
132
+ const GENERAL_IMPLEMENTATION_PATTERNS = [
133
+ ...EXPLICIT_IMPLEMENTATION_PATTERNS,
134
+ FIX_OR_PATCH_IMPLEMENTATION_PATTERN,
112
135
  /\b(?:update|add|remove|replace|delete|create)\s+(?:the\s+)?(?:file|files|code|source|implementation|test|tests|component|function|module|class|method|logic|import|imports|readme|docs?|changelog|package\.json|config|manifest|extension|prompt|command)\b/i,
113
136
  ];
114
137
 
@@ -141,10 +164,13 @@ interface NoEditProhibitionAnalysis {
141
164
  }
142
165
 
143
166
  function analyzeNoEditProhibitions(taskText: string): NoEditProhibitionAnalysis {
167
+ const readOnlyBoundary = NO_EDIT_BOUNDARY_ASSERTION_PATTERN.test(taskText);
144
168
  let present = REVIEW_ONLY_PATTERNS.some((pattern) => pattern.test(taskText))
145
- || NO_TOOL_INTENT_PATTERNS.some((pattern) => pattern.test(taskText));
146
- let blanket = present;
147
- let strippedText = stripPatterns(taskText, [...REVIEW_ONLY_PATTERNS, ...NO_TOOL_INTENT_PATTERNS]);
169
+ || NO_TOOL_INTENT_PATTERNS.some((pattern) => pattern.test(taskText))
170
+ || readOnlyBoundary;
171
+ let blanket = false;
172
+ let strippedText = stripPatterns(taskText, [...REVIEW_ONLY_PATTERNS, ...NO_TOOL_INTENT_PATTERNS, ADVISORY_INFINITIVE_PATTERN]);
173
+ if (readOnlyBoundary) strippedText = stripPatterns(strippedText, [NO_EDIT_BOUNDARY_ASSERTION_PATTERN]);
148
174
  const stripNoEditProhibition = (match: string, object: string, offset: number, source: string): string => {
149
175
  present = true;
150
176
  if (GENERIC_PROHIBITION_OBJECT.test(object) && !hasScopedProhibitionContinuation(source.slice(offset + match.length))) blanket = true;
@@ -172,16 +198,23 @@ function hasImplementationIntent(agent: string, taskText: string): boolean {
172
198
  }
173
199
 
174
200
  export function classifyTaskMutationIntent(agent: string, task: string): TaskMutationIntent {
175
- const taskText = stripSeverityCompounds(stripFrameworkInstructions(task));
201
+ const taskText = stripPatterns(stripSeverityCompounds(stripFrameworkInstructions(task)), [FINDING_CLASSIFICATION_PATTERN]);
176
202
  const taskTextWithoutScopedConstraints = stripPatterns(taskText, SCOPED_NO_EDIT_CONSTRAINT_PATTERNS);
177
203
  const prohibitions = analyzeNoEditProhibitions(taskTextWithoutScopedConstraints);
178
204
  if (prohibitions.present) {
179
205
  if (prohibitions.blanket) return { kind: "read-only" };
180
- return hasImplementationIntent(agent, prohibitions.strippedText) ? { kind: "implementation" } : { kind: "read-only" };
206
+ const remaining = stripPathLikeTokens(prohibitions.strippedText);
207
+ if (isReviewerStyleAgent(agent)) {
208
+ return hasImplementationIntent(agent, remaining) ? { kind: "implementation" } : { kind: "read-only" };
209
+ }
210
+ return EXPLICIT_IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(remaining))
211
+ || FOLLOW_ON_IMPLEMENTATION_PATTERN.test(remaining)
212
+ ? { kind: "implementation" }
213
+ : { kind: "read-only" };
181
214
  }
182
215
 
183
216
  if (RESEARCH_AGENT_PATTERNS.some((pattern) => pattern.test(agent))) return { kind: "read-only" };
184
- if (hasImplementationIntent(agent, taskText)) return { kind: "implementation" };
217
+ if (hasImplementationIntent(agent, stripPathLikeTokens(taskText))) return { kind: "implementation" };
185
218
  if (isReviewerStyleAgent(agent)) return { kind: "read-only" };
186
219
  return taskHasReadOnlyDeliverable(taskTextWithoutScopedConstraints) ? { kind: "read-only" } : { kind: "unknown" };
187
220
  }
@@ -204,7 +237,7 @@ const MAY_MUTATE_VERB_PATTERN = /\b(?:fix|implement|update|write|edit|modify|mig
204
237
  * does.
205
238
  */
206
239
  export function taskMayMutate(task: string): boolean {
207
- const taskText = stripPatterns(stripSeverityCompounds(stripFrameworkInstructions(task)), SCOPED_NO_EDIT_CONSTRAINT_PATTERNS);
240
+ const taskText = stripPatterns(stripSeverityCompounds(stripFrameworkInstructions(task)), [FINDING_CLASSIFICATION_PATTERN, ...SCOPED_NO_EDIT_CONSTRAINT_PATTERNS]);
208
241
  const prohibitions = analyzeNoEditProhibitions(taskText);
209
242
  if (prohibitions.blanket) return false;
210
243
  return MAY_MUTATE_VERB_PATTERN.test(stripPatterns(prohibitions.strippedText, READ_ONLY_DELIVERABLE_PATTERNS));
@@ -0,0 +1,18 @@
1
+ import type { AsyncStatus, SubagentState } from "../../shared/types.ts";
2
+
3
+ /** Projection-only guidance, not a control route or proof of current child liveness. */
4
+ export function workflowAsyncChildSteeringGuidance(status: AsyncStatus, state?: SubagentState): string[] {
5
+ if (status.mode !== "workflow" || (status.state !== "running" && status.state !== "queued")
6
+ || !state?.currentSessionId || status.sessionId !== state.currentSessionId
7
+ || !state.workflowControllers?.has(status.runId)) return [];
8
+ const ids = new Set((status.steps ?? []).flatMap((step) =>
9
+ step.async === true && step.status === "running"
10
+ && step.runner?.type !== "external-cli" && step.runner?.type !== "external-job"
11
+ && typeof step.runId === "string" && /^[A-Za-z0-9_-]+$/.test(step.runId) && step.runId !== status.runId
12
+ ? [step.runId] : []));
13
+ if (ids.size === 0) return [];
14
+ return [
15
+ "Async children recorded as running; direct steering rechecks controls. Choose an exact child ID (queued does not mean consumed):",
16
+ ...[...ids].map((id) => ` subagent({ action: "steer", id: "${id}", mode: "follow_up", message: "..." })`),
17
+ ];
18
+ }
@@ -18,6 +18,8 @@ const WORKTREE_NAMING_COMPONENT_MAX_BYTES = 96;
18
18
  const WORKTREE_NAMING_LABEL_MAX_BYTES = 256;
19
19
  const WORKTREE_NAMING_BRANCH_MAX_BYTES = 256;
20
20
  const WORKTREE_COMMAND_OUTPUT_MAX_BYTES = 128 * 1024;
21
+ const WORKTRUNK_COMMAND = process.platform === "win32" ? "git" : "wt";
22
+ const WORKTRUNK_ARG_PREFIX = process.platform === "win32" ? ["wt"] : [];
21
23
  export const MACHINE_DIFF_OPTIONS = ["--no-color", "--no-ext-diff", "--no-textconv", "--default-prefix", "--line-prefix=", "--no-relative"] as const;
22
24
  const MACHINE_PATCH_OPTIONS = [...MACHINE_DIFF_OPTIONS, "--binary"] as const;
23
25
  const PATCH_VALIDATION_OPTIONS = ["apply", "--check", "--cached", "--reverse", "--binary", "--whitespace=nowarn"] as const;
@@ -339,11 +341,9 @@ export function validateWorktreePatchRepresentsCurrentWorktree(worktreePath: str
339
341
  return undefined;
340
342
  }
341
343
 
342
- async function resolveRepoState(tx: SetupTransaction, cwd: string, requestedBaseRef: string | undefined): Promise<RepoState> {
344
+ async function probeWorktreeSource(tx: Pick<SetupTransaction, "git" | "gitChecked">, cwd: string): Promise<string> {
343
345
  const repoCheck = await tx.git(cwd, ["rev-parse", "--is-inside-work-tree"], [0, 128]);
344
346
  if (repoCheck.status !== 0 || repoCheck.stdout.trim() !== "true") throw new Error("worktree isolation requires a git repository");
345
- const rawPrefix = (await tx.gitChecked(cwd, ["rev-parse", "--show-prefix"])).trim();
346
- const cwdRelative = rawPrefix ? path.normalize(rawPrefix.replace(/[\\/]+$/, "")) : "";
347
347
  const toplevel = (await tx.gitChecked(cwd, ["rev-parse", "--show-toplevel"])).trim();
348
348
 
349
349
  // pi-subagents writes durable runtime state under .pi/subagents/ by default;
@@ -352,6 +352,26 @@ async function resolveRepoState(tx: SetupTransaction, cwd: string, requestedBase
352
352
  if (status.trim().length > 0) {
353
353
  throw new Error("worktree isolation requires a clean git working tree. Commit or stash changes first.");
354
354
  }
355
+ return toplevel;
356
+ }
357
+
358
+ /** Read-only admission check; allocation repeats it because source state can change. */
359
+ export async function preflightWorktreeSource(cwd: string, options: Pick<SetupCommandOptions, "signal" | "deadlineAt"> = {}): Promise<void> {
360
+ const git = async (cwd: string, args: string[], acceptedExitCodes: readonly number[] = [0]): Promise<SetupCommandResult> => {
361
+ const result = await runSetupCommand("git", ["-C", cwd, ...args], { ...options, acceptedExitCodes });
362
+ if (result.error) throw result.error;
363
+ if (result.outputIncomplete || result.status === null || !acceptedExitCodes.includes(result.status)) {
364
+ throw new Error(result.stderr.trim().slice(0, 2000) || "Worktree source probe failed");
365
+ }
366
+ return result;
367
+ };
368
+ await probeWorktreeSource({ git, gitChecked: async (cwd, args) => (await git(cwd, args)).stdout }, cwd);
369
+ }
370
+
371
+ async function resolveRepoState(tx: SetupTransaction, cwd: string, requestedBaseRef: string | undefined): Promise<RepoState> {
372
+ const toplevel = await probeWorktreeSource(tx, cwd);
373
+ const rawPrefix = (await tx.gitChecked(cwd, ["rev-parse", "--show-prefix"])).trim();
374
+ const cwdRelative = rawPrefix ? path.normalize(rawPrefix.replace(/[\\/]+$/, "")) : "";
355
375
 
356
376
  const baseRef = normalizeWorktreeBaseRef(requestedBaseRef) ?? DEFAULT_WORKTREE_BASE_REF;
357
377
  let baseCommit: string;
@@ -526,6 +546,11 @@ function hasConfiguredWorktreeBaseDir(baseDir: string | undefined): boolean {
526
546
  : (process.env.PI_SUBAGENTS_WORKTREE_DIR?.trim().length ?? 0) > 0;
527
547
  }
528
548
 
549
+ function isInsidePiExtensionsDirectory(targetPath: string): boolean {
550
+ const extensionsDir = normalizeComparableCwd(path.join(getAgentDir(), "extensions"));
551
+ return isPathInside(extensionsDir, normalizeComparableCwd(targetPath));
552
+ }
553
+
529
554
  interface WorktrunkCapability {
530
555
  available: boolean;
531
556
  reason?: string;
@@ -533,7 +558,7 @@ interface WorktrunkCapability {
533
558
 
534
559
  function runWorktrunk(args: string[], cwd?: string): WorktreeCommandResult {
535
560
  try {
536
- const result = spawnSync("wt", args, {
561
+ const result = spawnSync(WORKTRUNK_COMMAND, [...WORKTRUNK_ARG_PREFIX, ...args], {
537
562
  cwd,
538
563
  encoding: "utf-8",
539
564
  windowsHide: true,
@@ -578,7 +603,7 @@ export function resolveWorktreeProvider(requested: WorktreeProvider | undefined,
578
603
  return "native";
579
604
  }
580
605
 
581
- async function resolveSetupProvider(tx: SetupTransaction, requested: WorktreeProvider | undefined, baseDir?: string): Promise<ManagedWorktreeProvider> {
606
+ async function resolveSetupProvider(tx: SetupTransaction, requested: WorktreeProvider | undefined, baseDir: string | undefined, repoRoot: string): Promise<ManagedWorktreeProvider> {
582
607
  const selection = requested ?? DEFAULT_WORKTREE_PROVIDER;
583
608
  if (selection !== "auto" && selection !== "native" && selection !== "worktrunk") throw new Error('worktree provider must be "auto", "native", or "worktrunk"');
584
609
  if (selection === "native") return "native";
@@ -586,13 +611,14 @@ async function resolveSetupProvider(tx: SetupTransaction, requested: WorktreePro
586
611
  if (selection === "worktrunk") throw new Error("worktreeProvider='worktrunk' cannot be combined with worktreeBaseDir or PI_SUBAGENTS_WORKTREE_DIR");
587
612
  return "native";
588
613
  }
614
+ if (selection === "auto" && isInsidePiExtensionsDirectory(repoRoot)) return "native";
589
615
  let reason: string | undefined;
590
616
  try {
591
617
  const probeExitCodes = Array.from({ length: 256 }, (_, code) => code);
592
- const version = await tx.command("wt", ["--version"], { maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES, acceptedExitCodes: probeExitCodes });
618
+ const version = await tx.command(WORKTRUNK_COMMAND, [...WORKTRUNK_ARG_PREFIX, "--version"], { maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES, acceptedExitCodes: probeExitCodes });
593
619
  if (version.status !== 0 || !/\b(?:wt\s+)?v?(\d+\.\d+(?:\.\d+)?)\b/i.test(version.stdout.trim())) reason = "Worktrunk is unavailable or returned an invalid version";
594
620
  else {
595
- const help = await tx.command("wt", ["switch", "--help"], { maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES, acceptedExitCodes: probeExitCodes });
621
+ const help = await tx.command(WORKTRUNK_COMMAND, [...WORKTRUNK_ARG_PREFIX, "switch", "--help"], { maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES, acceptedExitCodes: probeExitCodes });
596
622
  const missing = ["--create", "--base", "--no-cd", "--no-hooks", "--format"].filter((flag) => !`${help.stdout}\n${help.stderr}`.includes(flag));
597
623
  if (help.status !== 0 || missing.length) reason = `Worktrunk switch capability unavailable: ${missing.join(", ")}`;
598
624
  }
@@ -616,11 +642,13 @@ export function shouldDeferWorktreeCwd(requested: WorktreeProvider | undefined,
616
642
  * to the repository. Managed leaves always nest one level deeper under the
617
643
  * project folder (`basename(repoRoot)`).
618
644
  */
619
- function resolveWorktreeDedicatedRoot(configuredBaseDir: string | undefined, repoRoot: string): string {
645
+ function resolveWorktreeDedicatedRoot(configuredBaseDir: string | undefined, repoRoot: string, relocateExtensionRepo = true): string {
620
646
  const rawBaseDir = configuredBaseDir ?? process.env.PI_SUBAGENTS_WORKTREE_DIR;
621
647
  let expanded: string;
622
648
  if (rawBaseDir === undefined || (configuredBaseDir === undefined && !rawBaseDir.trim())) {
623
- expanded = path.join(path.dirname(repoRoot), "worktrees");
649
+ expanded = relocateExtensionRepo && isInsidePiExtensionsDirectory(repoRoot)
650
+ ? path.join(getAgentDir(), "worktrees")
651
+ : path.join(path.dirname(repoRoot), "worktrees");
624
652
  } else {
625
653
  const trimmed = rawBaseDir.trim();
626
654
  if (!trimmed) throw new Error("worktree base directory cannot be empty");
@@ -942,7 +970,7 @@ async function createWorktrunkWorktree(
942
970
  const naming = buildWorktreeNaming({ runId, index, agent: agents?.[index], label: labels?.[index], task: tasks?.[index], branchPrefix });
943
971
  const args = ["-C", toplevel, "switch", "--create", naming.requestedBranch, "--base", baseCommit, "--no-cd", "--no-hooks", "--format", "json"];
944
972
  tx.attempt(index, naming.requestedBranch);
945
- const result = await tx.command("wt", args, { cwd: toplevel, maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES });
973
+ const result = await tx.command(WORKTRUNK_COMMAND, [...WORKTRUNK_ARG_PREFIX, ...args], { cwd: toplevel, maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES });
946
974
  tx.progress.phase = "validation";
947
975
  try {
948
976
  const output = parseWorktrunkSwitchOutput(result.stdout);
@@ -1331,9 +1359,11 @@ async function allocateWorktrees(tx: SetupTransaction, cwd: string, runId: strin
1331
1359
  tx.progress.setup.cwd = repo.toplevel;
1332
1360
  tx.progress.setup.baseCommit = repo.baseCommit;
1333
1361
  const setupHook = resolveWorktreeSetupHook(repo.toplevel, options?.setupHook);
1334
- const provider = await resolveSetupProvider(tx, options?.provider, options?.baseDir);
1362
+ const provider = await resolveSetupProvider(tx, options?.provider, options?.baseDir, repo.toplevel);
1335
1363
  const branchPrefix = normalizeWorktreeBranchPrefix(options?.branchPrefix);
1336
- const dedicatedRoot = provider === "native" ? resolveWorktreeDedicatedRoot(options?.baseDir, repo.toplevel) : undefined;
1364
+ const dedicatedRoot = provider === "native"
1365
+ ? resolveWorktreeDedicatedRoot(options?.baseDir, repo.toplevel, (options?.provider ?? DEFAULT_WORKTREE_PROVIDER) === "auto")
1366
+ : undefined;
1337
1367
  const worktrees = tx.progress.setup.worktrees;
1338
1368
 
1339
1369
  try {
@@ -1,8 +1,12 @@
1
- import { randomUUID } from "node:crypto";
1
+ // Forking a parent transcript into a child must drop Anthropic's signed and redacted
2
+ // thinking blocks: a thinking signature is bound to the session that produced it and
3
+ // cannot be replayed into a branch. Stripping them is required; disabling the child's
4
+ // thinking is not. Pi >= 0.85.0 recovers from signed-thinking mismatches on the
5
+ // transport, so a sanitized transcript is safe to resume with thinking enabled and the
6
+ // child keeps the level it asked for, reasoning fresh from its first turn.
2
7
  import * as fs from "node:fs";
3
8
  import * as path from "node:path";
4
9
  import { SessionManager } from "@earendil-works/pi-coding-agent";
5
- import { findModelInfo, type ModelInfo } from "./model-info.ts";
6
10
 
7
11
  type SubagentExecutionContext = "fresh" | "fork";
8
12
 
@@ -19,7 +23,6 @@ interface BranchSessionEntry {
19
23
  api?: string;
20
24
  model?: string;
21
25
  };
22
- thinkingLevel?: string;
23
26
  }
24
27
 
25
28
  interface BranchSessionManager {
@@ -39,20 +42,11 @@ interface ForkContextResolverOptions {
39
42
  openSession?: (path: string, sessionDir?: string) => BranchSessionManager;
40
43
  /** Rewrite a created fork before its path can be used to spawn a child. */
41
44
  pruneSession?: (sessionFile: string) => Promise<void>;
42
- /** Decide per child index whether a sanitized transcript must also disable the child's
43
- * thinking. Defaults to true (the pre-existing conservative behavior) when omitted. */
44
- forceThinkingOffForIndex?: (index: number) => boolean;
45
- }
46
-
47
- interface ForkContextResolution {
48
- sessionFile: string;
49
- thinkingOverride?: "off";
50
45
  }
51
46
 
52
47
  interface ForkContextResolver {
53
48
  prepareSessionForIndex(index?: number): Promise<void>;
54
49
  sessionFileForIndex(index?: number): string | undefined;
55
- thinkingOverrideForIndex(index?: number): "off" | undefined;
56
50
  }
57
51
 
58
52
  export function resolveSubagentContext(value: unknown): SubagentExecutionContext {
@@ -101,20 +95,6 @@ export function canPreferForkFromSnapshot(input: PreferredForkSnapshot): boolean
101
95
  }
102
96
  }
103
97
 
104
- /** Decide whether a resolved child model uses Anthropic's provider or message API, which
105
- * requires the sanitized fork to disable thinking. Unknown models stay conservative. */
106
- export function forkedChildRequiresThinkingOff(
107
- model: string | undefined,
108
- availableModels?: ModelInfo[],
109
- preferredProvider?: string,
110
- ): boolean {
111
- if (!model) return true;
112
- const info = findModelInfo(model, availableModels, preferredProvider);
113
- if (!info) return true;
114
- return info.provider.toLowerCase() === "anthropic"
115
- || info.api?.toLowerCase() === "anthropic-messages";
116
- }
117
-
118
98
  function isUnsafeAnthropicThinkingBlock(message: BranchSessionEntry["message"], block: unknown): boolean {
119
99
  if (!message || !block || typeof block !== "object" || !("type" in block)) return false;
120
100
  const provider = typeof message.provider === "string" ? message.provider.toLowerCase() : "";
@@ -128,28 +108,6 @@ function isUnsafeAnthropicThinkingBlock(message: BranchSessionEntry["message"],
128
108
  return record.redacted === true || (typeof signature === "string" && signature.length > 0);
129
109
  }
130
110
 
131
- function createEntryId(entries: BranchSessionEntry[]): string {
132
- const ids = new Set(entries.map((entry) => entry.id).filter((id): id is string => typeof id === "string"));
133
- for (let attempt = 0; attempt < 100; attempt++) {
134
- const id = randomUUID().slice(0, 8);
135
- if (!ids.has(id)) return id;
136
- }
137
- return randomUUID();
138
- }
139
-
140
- function appendThinkingOffEntry(entries: BranchSessionEntry[]): void {
141
- const last = entries[entries.length - 1];
142
- if (last?.type === "thinking_level_change" && last.thinkingLevel === "off") return;
143
- const parent = [...entries].reverse().find((entry) => typeof entry.id === "string");
144
- entries.push({
145
- type: "thinking_level_change",
146
- id: createEntryId(entries),
147
- parentId: parent?.id ?? null,
148
- timestamp: new Date().toISOString(),
149
- thinkingLevel: "off",
150
- });
151
- }
152
-
153
111
  function sanitizeUnsafeThinkingBlocks(entries: BranchSessionEntry[]): boolean {
154
112
  let sanitized = false;
155
113
  for (const entry of entries) {
@@ -183,7 +141,6 @@ export function createForkContextResolver(
183
141
  return {
184
142
  prepareSessionForIndex: async () => {},
185
143
  sessionFileForIndex: () => undefined,
186
- thinkingOverrideForIndex: () => undefined,
187
144
  };
188
145
  }
189
146
 
@@ -217,12 +174,12 @@ export function createForkContextResolver(
217
174
  path.basename(parentSessionFile, ".jsonl"),
218
175
  "forks",
219
176
  );
220
- const cachedResolutions = new Map<number, ForkContextResolution>();
177
+ const cachedSessionFiles = new Map<number, string>();
221
178
  const preparedIndexes = new Set<number>();
222
179
  const preparationPromises = new Map<number, Promise<void>>();
223
180
 
224
- const resolveFork = (index = 0): ForkContextResolution => {
225
- const cached = cachedResolutions.get(index);
181
+ const resolveFork = (index = 0): string => {
182
+ const cached = cachedSessionFiles.get(index);
226
183
  if (cached) return cached;
227
184
  try {
228
185
  if (!fs.existsSync(parentSessionFile)) {
@@ -233,34 +190,23 @@ export function createForkContextResolver(
233
190
  if (!sessionFile) {
234
191
  throw new Error("Session manager did not return a forked session file.");
235
192
  }
236
- const forceThinkingOff = (sanitized: boolean): boolean =>
237
- sanitized && (options.forceThinkingOffForIndex?.(index) ?? true);
238
- let thinkingOverride: "off" | undefined;
239
193
  if (!fs.existsSync(sessionFile)) {
240
194
  const header = sourceManager.getHeader?.();
241
195
  const entries = sourceManager.getEntries?.();
242
196
  if (!header || !entries) {
243
197
  throw new Error(`Session manager returned a forked session file that does not exist and cannot be persisted by fallback: ${sessionFile}`);
244
198
  }
245
- if (forceThinkingOff(sanitizeUnsafeThinkingBlocks(entries))) {
246
- appendThinkingOffEntry(entries);
247
- thinkingOverride = "off";
248
- }
199
+ sanitizeUnsafeThinkingBlocks(entries);
249
200
  fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
250
201
  fs.writeFileSync(sessionFile, `${[header, ...entries].map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
251
202
  } else {
252
203
  const entries = readSessionEntries(sessionFile);
253
204
  if (sanitizeUnsafeThinkingBlocks(entries)) {
254
- if (forceThinkingOff(true)) {
255
- appendThinkingOffEntry(entries);
256
- thinkingOverride = "off";
257
- }
258
205
  fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
259
206
  }
260
207
  }
261
- const resolution = { sessionFile, ...(thinkingOverride ? { thinkingOverride } : {}) };
262
- cachedResolutions.set(index, resolution);
263
- return resolution;
208
+ cachedSessionFiles.set(index, sessionFile);
209
+ return sessionFile;
264
210
  } catch (error) {
265
211
  const cause = error instanceof Error ? error : new Error(String(error));
266
212
  throw new Error(`Failed to create forked subagent session: ${cause.message}`, { cause });
@@ -269,11 +215,11 @@ export function createForkContextResolver(
269
215
 
270
216
  return {
271
217
  async prepareSessionForIndex(index = 0): Promise<void> {
272
- const resolution = resolveFork(index);
218
+ const sessionFile = resolveFork(index);
273
219
  if (!options.pruneSession || preparedIndexes.has(index)) return;
274
220
  let preparation = preparationPromises.get(index);
275
221
  if (!preparation) {
276
- preparation = options.pruneSession(resolution.sessionFile).then(() => {
222
+ preparation = options.pruneSession(sessionFile).then(() => {
277
223
  preparedIndexes.add(index);
278
224
  });
279
225
  preparationPromises.set(index, preparation);
@@ -284,10 +230,7 @@ export function createForkContextResolver(
284
230
  if (options.pruneSession && !preparedIndexes.has(index)) {
285
231
  throw new Error(`Pruned fork session ${index} was used before pruning completed.`);
286
232
  }
287
- return resolveFork(index).sessionFile;
288
- },
289
- thinkingOverrideForIndex(index = 0): "off" | undefined {
290
- return resolveFork(index).thinkingOverride;
233
+ return resolveFork(index);
291
234
  },
292
235
  };
293
236
  }
@@ -1,10 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import type { AgentConfig } from "../agents/agents.ts";
4
+ import type { PiLaunchToolPlan } from "../runs/shared/child-tool-plan.ts";
4
5
  import type { ExtensionBindings } from "../runs/shared/extension-bindings.ts";
5
6
 
6
7
  export const AGENT_DEFINITION_PROJECTION_VERSION = 1 as const;
7
- export const LAUNCH_BINDING_PROJECTION_VERSION = 1 as const;
8
+ // v2: the Intercom bridge prompt and tools are part of the binding on every
9
+ // path, and the bridge text no longer names the parent session.
10
+ export const LAUNCH_BINDING_PROJECTION_VERSION = 2 as const;
8
11
 
9
12
  function stableJson(value: unknown): string {
10
13
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -75,8 +78,9 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
75
78
  };
76
79
  }
77
80
 
81
+ /** Digest of the parsed definition; a runtime overlay that already captured it wins over re-hashing the overlaid copy. */
78
82
  export function agentDefinitionDigest(agent: AgentConfig): string {
79
- return stableJsonDigest(projectAgentDefinition(agent));
83
+ return agent.definitionDigest ?? stableJsonDigest(projectAgentDefinition(agent));
80
84
  }
81
85
 
82
86
  export interface LaunchBindingInput {
@@ -136,3 +140,62 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
136
140
  export function launchBindingDigest(input: LaunchBindingInput): string {
137
141
  return stableJsonDigest(projectLaunchBinding(input));
138
142
  }
143
+
144
+ type LaunchBindingPromptMode = Pick<LaunchBindingInput, "systemPromptMode" | "inheritProjectContext" | "inheritGlobalContext" | "inheritSkills">;
145
+
146
+ /**
147
+ * Who the child is: the launched agent, or the identity a persisted step
148
+ * already captured when a runner recomputes the binding per model attempt.
149
+ */
150
+ export type LaunchBindingIdentity =
151
+ | { agent: AgentConfig }
152
+ | ({ definitionDigest: string } & LaunchBindingPromptMode);
153
+
154
+ export type LaunchBindingSource = LaunchBindingIdentity
155
+ & Pick<LaunchBindingInput, "fast" | "thinking" | "skills" | "outputPath" | "outputMode" | "structuredOutputSchema" | "extensionBindings">
156
+ & {
157
+ task: string;
158
+ modelCandidates: string[];
159
+ /** Effective child system prompt before runtime acceptance prose. */
160
+ systemPrompt: string;
161
+ toolPlan: Pick<PiLaunchToolPlan, "effectiveToolAllowlist" | "excludeTools" | "extensionArgs" | "effectiveMcpTools">;
162
+ };
163
+
164
+ export interface LaunchBinding {
165
+ definitionDigest: string;
166
+ launchContractDigest: string;
167
+ }
168
+
169
+ /**
170
+ * Assemble launch identity from resolved preflight or execution inputs.
171
+ * The stable projection omits undefined optional fields.
172
+ */
173
+ export function resolveLaunchBinding(source: LaunchBindingSource): LaunchBinding {
174
+ const identity = "agent" in source ? {
175
+ definitionDigest: agentDefinitionDigest(source.agent),
176
+ systemPromptMode: source.agent.systemPromptMode,
177
+ inheritProjectContext: source.agent.inheritProjectContext,
178
+ inheritGlobalContext: source.agent.inheritGlobalContext,
179
+ inheritSkills: source.agent.inheritSkills,
180
+ } : source;
181
+ return {
182
+ definitionDigest: identity.definitionDigest,
183
+ launchContractDigest: launchBindingDigest({
184
+ ...identity,
185
+ task: source.task,
186
+ modelCandidates: source.modelCandidates,
187
+ fast: source.fast,
188
+ thinking: source.thinking || undefined,
189
+ systemPrompt: source.systemPrompt,
190
+ skills: source.skills,
191
+ tools: source.toolPlan.effectiveToolAllowlist,
192
+ excludeTools: source.toolPlan.excludeTools.length > 0 ? source.toolPlan.excludeTools : undefined,
193
+ extensions: source.toolPlan.extensionArgs,
194
+ mcpDirectTools: source.toolPlan.effectiveMcpTools,
195
+ outputPath: source.outputPath || undefined,
196
+ outputMode: source.outputMode,
197
+ structuredOutputSchema: source.structuredOutputSchema || undefined,
198
+ extensionBindings: source.extensionBindings || undefined,
199
+ }),
200
+ };
201
+ }
@@ -0,0 +1,30 @@
1
+ import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
2
+
3
+ const OPENCODE_HOST = "opencode.ai";
4
+
5
+ function matchesOpenCodeHost(baseUrl: string): boolean {
6
+ try {
7
+ return new URL(baseUrl).hostname === OPENCODE_HOST;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ /**
14
+ * OpenCode session-routing headers for internal subagent model calls.
15
+ *
16
+ * Pi's own session path emits these from coding-agent's provider-attribution
17
+ * merge, but subagent-internal calls (watchdog review, permission arbiter,
18
+ * task-mutation arbiter, prompt audit) stream through bare Agents that bypass
19
+ * that path. Without them OpenCode falls back to client-IP affinity and loses
20
+ * prompt-cache routing (see pi issue #4847). Returns undefined for every other
21
+ * provider so non-OpenCode requests stay byte-identical.
22
+ */
23
+ export function opencodeSessionHeaders(
24
+ model: Pick<Model<Api>, "provider" | "baseUrl">,
25
+ sessionId: string | undefined,
26
+ ): ProviderHeaders | undefined {
27
+ if (!sessionId) return undefined;
28
+ if (model.provider !== "opencode" && model.provider !== "opencode-go" && !matchesOpenCodeHost(model.baseUrl)) return undefined;
29
+ return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
30
+ }
@@ -708,6 +708,7 @@ export type ProcessTerminal =
708
708
  export interface ScheduleOrigin {
709
709
  id: string;
710
710
  name?: string;
711
+ quiet?: boolean;
711
712
  }
712
713
 
713
714
  export type SteeringActionState = "delivered" | "scheduled" | "pending" | "partial" | "recovered" | "failed";
@@ -2440,6 +2441,8 @@ export interface RunSyncOptions {
2440
2441
  preferredModelProvider?: string;
2441
2442
  /** Parent Pi event host used to snapshot runtime-registered MCP servers before child launch. */
2442
2443
  runtimeSnapshotHost?: import("../runs/shared/mcp-direct-tool-allowlist.ts").McpRuntimeSnapshotHost;
2444
+ /** Builtin tool names the host runtime provides; used to intersect agent-declared tools. */
2445
+ hostAvailableBuiltins?: readonly string[];
2443
2446
  /** Optional subagent model-scope enforcement for fallback candidates */
2444
2447
  modelScope?: ModelScopeRule | ModelScopeRule[];
2445
2448
  /** Skills to make available (overrides agent default if provided) */
@@ -2754,7 +2757,7 @@ export const POLL_INTERVAL_MS = 250;
2754
2757
  export const WIDGET_ANIMATION_INTERVAL_MS = 1000;
2755
2758
  export const MAX_WIDGET_JOBS = 4;
2756
2759
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
2757
- export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "validate", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "worktree.cleanup", "lane.status", "lane.recordMerge", "lane.recordSupersession", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "debug.run", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
2760
+ export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "validate", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "worktree.cleanup", "lane.status", "lane.recordMerge", "lane.recordSupersession", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.command", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "debug.run", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
2758
2761
 
2759
2762
  export const DEFAULT_FORK_PREAMBLE =
2760
2763
  "You are a delegated subagent running from a fork of the parent session. " +
@@ -6,7 +6,7 @@ import {
6
6
  type SubagentDelegationUpdate,
7
7
  type SubagentDelegationValue,
8
8
  } from "../api/delegation.ts";
9
- import type { AcceptanceInput, AgentContract, EffectsProjection, ExecutionProjection, JsonSchemaObject, ReviewProjection, ToolBudgetConfig, Usage } from "../shared/types.ts";
9
+ import type { AcceptanceInput, AgentContract, EffectsProjection, ExecutionProjection, IntercomBridgeConfig, JsonSchemaObject, ReviewProjection, ToolBudgetConfig, Usage } from "../shared/types.ts";
10
10
  import { cloneJsonWithinByteLimit } from "./delegation-json.ts";
11
11
 
12
12
  export interface PromptTemplateDelegationRequest {
@@ -127,6 +127,7 @@ export interface DelegatedSubagentExecutionParams {
127
127
  agentContract?: AgentContract;
128
128
  acceptance?: AcceptanceInput;
129
129
  artifacts?: boolean;
130
+ intercomBridge?: IntercomBridgeConfig;
130
131
  /** Internal-only thinking override accepted by executeDelegated. */
131
132
  delegatedThinkingOverride?: SubagentDelegationThinking;
132
133
  /** Internal-only capability accepted and stripped by executeDelegated. */
@@ -295,6 +296,7 @@ export function toSubagentDelegationExecutionParams(request: SubagentDelegationR
295
296
  ...(request.result.kind === "structured" ? { outputSchema: request.result.schema } : {}),
296
297
  acceptance: false,
297
298
  artifacts: request.artifacts,
299
+ ...(request.intercomBridge !== undefined ? { intercomBridge: request.intercomBridge } : {}),
298
300
  delegatedThinkingOverride: request.thinking,
299
301
  delegatedAllowZeroToolBudget: true,
300
302
  async: false,