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.
- package/CHANGELOG.md +68 -0
- package/README.md +4 -3
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +6 -3
- package/docs/configuration.md +7 -5
- package/docs/extension-api.md +33 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +1 -1
- package/docs/observability.md +4 -4
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +18 -8
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +26 -12
- package/inspector-runner.mjs +2 -2
- package/package.json +1 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +7 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agents.ts +6 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +2 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +20 -8
- package/src/extension/public-execution.ts +4 -2
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +67 -78
- package/src/extension/tool-description.ts +29 -82
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +22 -13
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/auto-drain.ts +6 -3
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +31 -1
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/run-child-session.ts +61 -4
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +11 -3
- package/src/runs/background/runner-child-launch.ts +2 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +47 -31
- package/src/runs/background/subagent-wait.ts +51 -8
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/execution.ts +44 -31
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +110 -100
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +5 -2
- package/src/runs/shared/async-status-projection.ts +4 -0
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch.ts +12 -2
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +33 -2
- package/src/runs/shared/child-tool-plan.ts +122 -3
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-fallback.ts +2 -0
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree.ts +42 -12
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +4 -1
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +149 -28
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/scripted-workflow.ts +237 -7
- package/src/workflows/workflow-checklist.ts +2 -2
- /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
|
-
/\
|
|
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
|
-
/(?:^|[
|
|
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)[
|
|
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
|
|
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
|
-
|
|
147
|
-
let
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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"
|
|
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
|
-
|
|
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
|
|
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):
|
|
225
|
-
const cached =
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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
|
|
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(
|
|
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)
|
|
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
|
-
|
|
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
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -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,
|