@narumitw/pi-subagents 0.43.0 → 0.46.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.
@@ -0,0 +1,234 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { Theme, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import type { SubagentInspectParams } from "./inspect.js";
5
+ import {
6
+ booleanValue,
7
+ COLLAPSED_LIST_LIMIT,
8
+ expansionHint,
9
+ numberValue,
10
+ recordList,
11
+ recordValue,
12
+ renderFallbackResult,
13
+ safeBlock,
14
+ safeLine,
15
+ statusBadge,
16
+ stringValue,
17
+ type ToolRendererContext,
18
+ toolHeader,
19
+ } from "./render-common.js";
20
+
21
+ export function renderInspectCall(args: Partial<SubagentInspectParams>, theme: Theme) {
22
+ const action = safeLine(args.action, "...", 128);
23
+ const metadata: string[] = [];
24
+ if (args.agentScope) metadata.push(`[${args.agentScope}]`);
25
+ if (args.agent) metadata.push(`agent:${safeLine(args.agent, "", 256)}`);
26
+ if (args.agentId) metadata.push(`id:${safeLine(args.agentId, "", 256)}`);
27
+ if (args.includeClosed) metadata.push("include closed");
28
+ return new Text(toolHeader(theme, "subagent_inspect", action, metadata), 0, 0);
29
+ }
30
+
31
+ export function renderInspectResult(
32
+ result: AgentToolResult<Record<string, unknown>>,
33
+ options: ToolRenderResultOptions,
34
+ theme: Theme,
35
+ context: ToolRendererContext<SubagentInspectParams>,
36
+ ) {
37
+ const details = recordValue(result.details);
38
+ const action = stringValue(details?.action);
39
+ if (!details || !action) return renderFallbackResult(result, options, theme, context.isError);
40
+
41
+ const rendered = renderAction(action, details, options.expanded, theme);
42
+ if (!rendered) return renderFallbackResult(result, options, theme, context.isError);
43
+ return new Text(rendered, 0, 0);
44
+ }
45
+
46
+ function renderAction(
47
+ action: string,
48
+ details: Record<string, unknown>,
49
+ expanded: boolean,
50
+ theme: Theme,
51
+ ): string | undefined {
52
+ switch (action) {
53
+ case "list_agents":
54
+ return renderList("agent", recordList(details.agents), details, expanded, theme, formatAgent);
55
+ case "get_agent": {
56
+ const agent = recordValue(details.agent);
57
+ if (!agent) return undefined;
58
+ const lines = [
59
+ `${statusBadge(theme, "completed")} · ${theme.fg("toolTitle", theme.bold(safeLine(agent.name, "agent", 256)))}${theme.fg("muted", ` (${safeLine(agent.source, "unknown", 128)})`)}`,
60
+ formatAgent(agent, theme, true),
61
+ ];
62
+ if (!expanded) lines.push(expansionHint());
63
+ return lines.join("\n");
64
+ }
65
+ case "list_runs":
66
+ return renderList("run", recordList(details.runs), details, expanded, theme, formatRun);
67
+ case "get_run": {
68
+ const run = recordValue(details.run);
69
+ if (!run) return undefined;
70
+ const lines = [
71
+ `${statusBadge(theme, "completed")} · run ${theme.fg("accent", safeLine(run.id, "run", 256))}`,
72
+ formatRun(run, theme, true),
73
+ ];
74
+ if (!expanded) lines.push(expansionHint());
75
+ return lines.join("\n");
76
+ }
77
+ case "list_models":
78
+ return renderList(
79
+ "model",
80
+ recordList(details.models),
81
+ details,
82
+ expanded,
83
+ theme,
84
+ formatModel,
85
+ stringValue(details.source),
86
+ );
87
+ case "status":
88
+ return renderStatus(details, expanded, theme);
89
+ case "diagnose":
90
+ return renderDiagnose(details, expanded, theme);
91
+ default:
92
+ return undefined;
93
+ }
94
+ }
95
+
96
+ function renderList(
97
+ label: string,
98
+ items: Record<string, unknown>[],
99
+ details: Record<string, unknown>,
100
+ expanded: boolean,
101
+ theme: Theme,
102
+ format: (item: Record<string, unknown>, theme: Theme, expanded: boolean) => string,
103
+ source = "",
104
+ ): string {
105
+ const returned = numberValue(details.returned, items.length);
106
+ const omitted = numberValue(details.omitted);
107
+ const noun = `${label}${returned === 1 ? "" : "s"}`;
108
+ const lines = [
109
+ `${statusBadge(theme, "completed")} · ${returned} ${noun}${source ? theme.fg("muted", ` · ${safeLine(source, "", 256)}`) : ""}`,
110
+ ];
111
+ const selected = expanded ? items : items.slice(0, COLLAPSED_LIST_LIMIT);
112
+ for (const item of selected) lines.push(format(item, theme, expanded));
113
+ const hidden = Math.max(0, items.length - selected.length) + omitted;
114
+ if (hidden > 0) lines.push(theme.fg("muted", `… ${hidden} omitted`));
115
+ if (items.length === 0) lines.push(theme.fg("muted", `(no ${label}s)`));
116
+ if (!expanded && (items.length > 0 || omitted > 0)) lines.push(expansionHint());
117
+ return lines.join("\n");
118
+ }
119
+
120
+ function formatAgent(agent: Record<string, unknown>, theme: Theme, expanded: boolean): string {
121
+ const name = safeLine(agent.name, "agent", 256);
122
+ const source = safeLine(agent.source, "unknown", 128);
123
+ const model = stringValue(agent.model);
124
+ const toolCount = typeof agent.toolCount === "number" ? agent.toolCount : undefined;
125
+ const description = safeBlock(agent.description, "", 512).trim();
126
+ const lines = [
127
+ `${theme.fg("muted", "• ")}${theme.fg("accent", name)} ${theme.fg("muted", source)}${model ? theme.fg("dim", ` · ${safeLine(model, "", 256)}`) : ""}${toolCount !== undefined ? theme.fg("dim", ` · ${toolCount} tools`) : ""}`,
128
+ ];
129
+ if (description) lines.push(` ${theme.fg("toolOutput", description)}`);
130
+ if (expanded) {
131
+ const path = stringValue(agent.path);
132
+ const consultTools = stringList(agent.consultTools);
133
+ if (path) lines.push(` ${theme.fg("dim", `path: ${safeLine(path, "", 2 * 1024)}`)}`);
134
+ lines.push(` ${theme.fg("dim", `consult tools: ${consultTools.join(", ") || "none"}`)}`);
135
+ }
136
+ return lines.join("\n");
137
+ }
138
+
139
+ function formatRun(run: Record<string, unknown>, theme: Theme, expanded: boolean): string {
140
+ const id = safeLine(run.id, "run", 256);
141
+ const agent = safeLine(run.agent, "agent", 256);
142
+ const state = safeLine(run.state, "unknown", 128);
143
+ const unread = numberValue(run.unreadMessages);
144
+ const lines = [
145
+ `${theme.fg("muted", "• ")}${theme.fg("accent", id)} ${theme.fg("toolOutput", agent)} ${theme.fg("muted", state)}${unread > 0 ? theme.fg("warning", ` · unread:${unread}`) : ""}`,
146
+ ];
147
+ if (expanded) {
148
+ const thinking = stringValue(run.thinkingLevel);
149
+ const task = safeBlock(run.currentTask, "", 2 * 1024).trim();
150
+ const error = safeBlock(run.error, "", 2 * 1024).trim();
151
+ lines.push(
152
+ ` ${theme.fg("dim", `${numberValue(run.historyCount)} history · ${thinking ? `thinking:${safeLine(thinking, "", 128)} · ` : ""}${numberValue(run.children)} children`)}`,
153
+ );
154
+ if (task) lines.push(` ${theme.fg("dim", `task: ${task}`)}`);
155
+ if (error) lines.push(` ${theme.fg("error", `error: ${error}`)}`);
156
+ }
157
+ return lines.join("\n");
158
+ }
159
+
160
+ function formatModel(model: Record<string, unknown>, theme: Theme, expanded: boolean): string {
161
+ const identity = `${safeLine(model.provider, "provider", 256)}/${safeLine(model.id, "model", 256)}`;
162
+ const current = booleanValue(model.current) ? theme.fg("success", " · current") : "";
163
+ const reasoning = booleanValue(model.reasoning) ? theme.fg("dim", " · reasoning") : "";
164
+ const lines = [`${theme.fg("muted", "• ")}${theme.fg("accent", identity)}${current}${reasoning}`];
165
+ if (expanded) {
166
+ const name = stringValue(model.name);
167
+ const thinking = stringValue(model.thinkingLevel);
168
+ if (name) lines.push(` ${theme.fg("toolOutput", safeLine(name, "", 512))}`);
169
+ lines.push(
170
+ ` ${theme.fg("dim", `context:${numberValue(model.contextWindow)} · max:${numberValue(model.maxTokens)}${thinking ? ` · thinking:${safeLine(thinking, "", 128)}` : ""}`)}`,
171
+ );
172
+ }
173
+ return lines.join("\n");
174
+ }
175
+
176
+ function renderStatus(
177
+ details: Record<string, unknown>,
178
+ expanded: boolean,
179
+ theme: Theme,
180
+ ): string | undefined {
181
+ const status = recordValue(details.status);
182
+ const stateful = recordValue(status?.stateful);
183
+ if (!status || !stateful) return undefined;
184
+ const lines = [
185
+ `${statusBadge(theme, "completed")} · runtime status`,
186
+ `${theme.fg("muted", "workflow: ")}${theme.fg("accent", safeLine(status.workflow, "unknown", 128))} · ${numberValue(stateful.activeAgents)} active · ${numberValue(stateful.retainedAgents)} retained`,
187
+ `${theme.fg("muted", "stateful: ")}${stateful.initialized === true ? "initialized" : "not initialized"} · resources: ${safeLine(status.consultResources, "unknown", 128)}`,
188
+ ];
189
+ if (expanded) {
190
+ const delivery = stringValue(stateful.completionDelivery);
191
+ const transport = stringValue(stateful.transport);
192
+ if (transport || delivery) {
193
+ lines.push(
194
+ theme.fg(
195
+ "dim",
196
+ [
197
+ transport && `transport:${safeLine(transport)}`,
198
+ delivery && `delivery:${safeLine(delivery)}`,
199
+ ]
200
+ .filter(Boolean)
201
+ .join(" · "),
202
+ ),
203
+ );
204
+ }
205
+ } else lines.push(expansionHint());
206
+ return lines.join("\n");
207
+ }
208
+
209
+ function renderDiagnose(details: Record<string, unknown>, expanded: boolean, theme: Theme): string {
210
+ const checks = recordList(details.checks);
211
+ const hasFail = checks.some((check) => check.status === "fail");
212
+ const hasWarning = checks.some((check) => check.status === "warning");
213
+ const overall = hasFail ? "failed" : hasWarning ? "warnings" : "passed";
214
+ const status = hasFail ? "failed" : hasWarning ? "warning" : "completed";
215
+ const lines = [`${statusBadge(theme, status)} · Diagnostics ${overall}`];
216
+ const selected = expanded ? checks : checks.slice(0, COLLAPSED_LIST_LIMIT);
217
+ for (const check of selected) {
218
+ const checkStatus = safeLine(check.status, "unknown", 64);
219
+ const color =
220
+ checkStatus === "fail" ? "error" : checkStatus === "warning" ? "warning" : "success";
221
+ lines.push(
222
+ `${theme.fg(color, checkStatus.toUpperCase())} ${theme.fg("accent", safeLine(check.name, "check", 256))} · ${theme.fg("toolOutput", safeBlock(check.message, "", 2 * 1024))}`,
223
+ );
224
+ }
225
+ if (checks.length === 0) lines.push(theme.fg("muted", "(no diagnostic checks)"));
226
+ if (!expanded && checks.length > 0) lines.push(expansionHint());
227
+ return lines.join("\n");
228
+ }
229
+
230
+ function stringList(value: unknown): string[] {
231
+ return Array.isArray(value)
232
+ ? value.flatMap((item) => (typeof item === "string" ? [safeLine(item, "", 256)] : []))
233
+ : [];
234
+ }
package/src/inspect.ts CHANGED
@@ -2,12 +2,21 @@ import * as path from "node:path";
2
2
  import { StringEnum } from "@earendil-works/pi-ai";
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import { type Static, Type } from "typebox";
5
- import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
5
+ import {
6
+ type AgentConfig,
7
+ type AgentScope,
8
+ type ConsultationCwdPolicy,
9
+ type DelegationCwdPolicy,
10
+ discoverAgents,
11
+ } from "./agents.js";
6
12
  import { resolveConsultTools } from "./consult-policy.js";
13
+ import { renderInspectCall, renderInspectResult } from "./inspect-render.js";
7
14
  import type { AgentRunInspectionDetail, AgentRunInspectionSummary } from "./registry.js";
8
15
  import { boundedPrivateText, boundText, safeDisplayPath, safeTerminalLine } from "./safe-text.js";
9
16
  import {
17
+ inspectCompletionDeliverySettings,
10
18
  inspectConsultResourceSettings,
19
+ inspectCwdPolicySettings,
11
20
  inspectDelegationWorkflowSettings,
12
21
  inspectSubagentSettings,
13
22
  resolveDelegationWorkflow,
@@ -49,6 +58,8 @@ export type SubagentInspectParams = Static<typeof SubagentInspectParams>;
49
58
  export interface SubagentInspectRuntime {
50
59
  getBlockingEnabled(): boolean;
51
60
  getConsultResourcePolicy(): "project-context" | "none" | "all";
61
+ getConsultationCwdPolicy(): ConsultationCwdPolicy;
62
+ getDelegationCwdPolicy(): DelegationCwdPolicy;
52
63
  getRuntimeStatus(): StatefulSubagentRuntimeStatus;
53
64
  listRunInspection(includeClosed?: boolean): AgentRunInspectionSummary[];
54
65
  getRunInspection(agentId: string): AgentRunInspectionDetail | undefined;
@@ -79,6 +90,12 @@ export function registerSubagentInspect(pi: ExtensionAPI, runtime: SubagentInspe
79
90
  async execute(_toolCallId, params, _signal, _onUpdate, ctx): Promise<InspectToolResult> {
80
91
  return executeSubagentInspect(validateInspectParams(params), ctx, runtime);
81
92
  },
93
+ renderCall(args, theme) {
94
+ return renderInspectCall(args, theme);
95
+ },
96
+ renderResult(result, options, theme, context) {
97
+ return renderInspectResult(result, options, theme, context);
98
+ },
82
99
  });
83
100
  }
84
101
 
@@ -280,9 +297,26 @@ function projectRun(run: AgentRunInspectionDetail, ctx: ExtensionContext): Recor
280
297
  return {
281
298
  ...projectRunSummary(run),
282
299
  cwd: safeDisplayPath(run.cwd, ctx.cwd),
300
+ workspaceMode: run.workspaceMode ?? "shared",
283
301
  thinkingLevel: run.thinkingLevel,
284
302
  currentTask: run.currentTask ? boundedPrivateText(run.currentTask, 2 * 1024) : undefined,
285
303
  error: run.error ? boundedPrivateText(run.error, 2 * 1024) : undefined,
304
+ target: run.target
305
+ ? {
306
+ cwd: safeDisplayPath(run.target.cwd, ctx.cwd),
307
+ boundary: run.target.boundary,
308
+ trust: {
309
+ kind: run.target.trust.kind,
310
+ projectTrusted: run.target.trust.projectTrusted,
311
+ sourcePath: run.target.trust.sourcePath
312
+ ? safeDisplayPath(run.target.trust.sourcePath, ctx.cwd)
313
+ : undefined,
314
+ warning: run.target.trust.warning
315
+ ? boundedPrivateText(run.target.trust.warning, 512)
316
+ : undefined,
317
+ },
318
+ }
319
+ : undefined,
286
320
  policy: run.policy
287
321
  ? {
288
322
  inherited: projectToolNames(run.policy.inherited),
@@ -323,17 +357,31 @@ function projectStatus(runtime: SubagentInspectRuntime): Record<string, unknown>
323
357
  const workflow = resolveDelegationWorkflow(runtime.getBlockingEnabled(), stateful.enabled);
324
358
  const configured = inspectDelegationWorkflowSettings();
325
359
  const resources = inspectConsultResourceSettings();
360
+ const cwdPolicy = inspectCwdPolicySettings();
361
+ const completion = inspectCompletionDeliverySettings();
326
362
  return {
327
363
  workflow,
328
364
  configuredWorkflow: configured.value,
365
+ configuredWorkflowSource: configured.source,
329
366
  stateful,
367
+ configuredCompletionDelivery: completion.value,
368
+ configuredCompletionDeliverySource: completion.source,
330
369
  consultResources: runtime.getConsultResourcePolicy(),
370
+ consultationCwdPolicy: runtime.getConsultationCwdPolicy(),
371
+ configuredConsultationCwdPolicy: cwdPolicy.consultation.value,
372
+ consultationCwdPolicySource: cwdPolicy.consultation.source,
373
+ delegationCwdPolicy: runtime.getDelegationCwdPolicy(),
374
+ configuredDelegationCwdPolicy: cwdPolicy.delegation.value,
375
+ delegationCwdPolicySource: cwdPolicy.delegation.source,
331
376
  configuredConsultResources: resources.value,
332
377
  consultResourcesSource: resources.source,
333
378
  settingsPath: safeDisplayPath(resources.path, process.cwd()),
334
379
  settingsError:
335
- configured.error || resources.error
336
- ? boundedPrivateText(configured.error ?? resources.error ?? "", 2 * 1024)
380
+ configured.error || resources.error || cwdPolicy.error || completion.error
381
+ ? boundedPrivateText(
382
+ configured.error ?? resources.error ?? cwdPolicy.error ?? completion.error ?? "",
383
+ 2 * 1024,
384
+ )
337
385
  : undefined,
338
386
  };
339
387
  }
@@ -167,6 +167,8 @@ function isStoredState(value: unknown): value is StoredState {
167
167
  Number.isFinite(record.updatedAt) &&
168
168
  (record.parentId === undefined || typeof record.parentId === "string") &&
169
169
  (record.thinkingLevel === undefined || isThinkingLevel(record.thinkingLevel)) &&
170
+ (record.workspaceMode === undefined || record.workspaceMode === "worktree") &&
171
+ (record.target === undefined || isTargetPolicyAudit(record.target)) &&
170
172
  (record.children === undefined ||
171
173
  (Array.isArray(record.children) &&
172
174
  record.children.every((id) => typeof id === "string"))) &&
@@ -178,6 +180,33 @@ function isStoredState(value: unknown): value is StoredState {
178
180
  });
179
181
  }
180
182
 
183
+ function isTargetPolicyAudit(value: unknown): boolean {
184
+ if (!value || typeof value !== "object") return false;
185
+ const target = value as Record<string, unknown>;
186
+ if (
187
+ typeof target.cwd !== "string" ||
188
+ (target.boundary !== "current-workspace" && target.boundary !== "external") ||
189
+ !target.trust ||
190
+ typeof target.trust !== "object"
191
+ ) {
192
+ return false;
193
+ }
194
+ const trust = target.trust as Record<string, unknown>;
195
+ return (
196
+ [
197
+ "session-trusted",
198
+ "session-untrusted",
199
+ "saved-trusted",
200
+ "saved-denied",
201
+ "unsaved",
202
+ "trust-error",
203
+ ].includes(String(trust.kind)) &&
204
+ typeof trust.projectTrusted === "boolean" &&
205
+ (trust.sourcePath === undefined || typeof trust.sourcePath === "string") &&
206
+ (trust.warning === undefined || typeof trust.warning === "string")
207
+ );
208
+ }
209
+
181
210
  function isAgentTurn(value: unknown): boolean {
182
211
  if (!value || typeof value !== "object") return false;
183
212
  const turn = value as Record<string, unknown>;
@@ -0,0 +1,168 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { getPackageDir } from "@earendil-works/pi-coding-agent";
4
+
5
+ const CORE_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
6
+ const MAX_DISPLAY_PATH_LENGTH = 500;
7
+
8
+ export interface PiInvocation {
9
+ command: string;
10
+ args: string[];
11
+ }
12
+
13
+ export interface PiInvocationRuntime {
14
+ execPath: string;
15
+ packageDir: string;
16
+ runtimeKind: "node" | "bun" | "unsupported";
17
+ }
18
+
19
+ export class PiInvocationError extends Error {
20
+ constructor(message: string) {
21
+ super(message);
22
+ this.name = "PiInvocationError";
23
+ }
24
+ }
25
+
26
+ function displayPath(value: string): string {
27
+ const suffix = value.length > MAX_DISPLAY_PATH_LENGTH ? "…" : "";
28
+ return JSON.stringify(`${value.slice(0, MAX_DISPLAY_PATH_LENGTH)}${suffix}`);
29
+ }
30
+
31
+ function resolutionError(packageDir: string, reason: string): PiInvocationError {
32
+ return new PiInvocationError(
33
+ `Unable to resolve the Pi CLI from the loaded ${CORE_PACKAGE_NAME} package at ${displayPath(packageDir)}: ${reason}. Reinstall the matching Pi core package before using the subprocess transport.`,
34
+ );
35
+ }
36
+
37
+ function currentRuntime(): PiInvocationRuntime {
38
+ let packageDir: string;
39
+ try {
40
+ packageDir = getPackageDir();
41
+ } catch {
42
+ throw resolutionError("<unavailable>", "Pi core did not provide its package directory");
43
+ }
44
+ const runtimeKind = process.versions.bun
45
+ ? "bun"
46
+ : process.release.name === "node" && !process.versions.electron
47
+ ? "node"
48
+ : "unsupported";
49
+ return {
50
+ execPath: process.execPath,
51
+ packageDir,
52
+ runtimeKind,
53
+ };
54
+ }
55
+
56
+ function isRecord(value: unknown): value is Record<string, unknown> {
57
+ return typeof value === "object" && value !== null && !Array.isArray(value);
58
+ }
59
+
60
+ function isWithinDirectory(parent: string, candidate: string): boolean {
61
+ const relative = path.relative(parent, candidate);
62
+ return (
63
+ relative === "" ||
64
+ (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))
65
+ );
66
+ }
67
+
68
+ function readCoreManifest(packageDir: string): Record<string, unknown> {
69
+ const manifestPath = path.join(packageDir, "package.json");
70
+ let source: string;
71
+ try {
72
+ source = fs.readFileSync(manifestPath, "utf8");
73
+ } catch {
74
+ throw resolutionError(packageDir, "the package manifest is unavailable");
75
+ }
76
+ let manifest: unknown;
77
+ try {
78
+ manifest = JSON.parse(source);
79
+ } catch {
80
+ throw resolutionError(packageDir, "the package manifest is invalid JSON");
81
+ }
82
+ if (!isRecord(manifest)) {
83
+ throw resolutionError(packageDir, "the package manifest is invalid");
84
+ }
85
+ if (manifest.name !== CORE_PACKAGE_NAME) {
86
+ throw resolutionError(packageDir, "the package manifest has an unexpected package name");
87
+ }
88
+ return manifest;
89
+ }
90
+
91
+ function resolveDeclaredBin(packageDir: string, manifest: Record<string, unknown>): string {
92
+ const bin = manifest.bin;
93
+ const piBin = isRecord(bin) ? bin.pi : undefined;
94
+ if (typeof piBin !== "string" || !piBin.trim()) {
95
+ throw resolutionError(packageDir, "package.json bin.pi must be a non-empty string");
96
+ }
97
+ const candidate = path.resolve(packageDir, piBin);
98
+ if (path.isAbsolute(piBin) || !isWithinDirectory(packageDir, candidate)) {
99
+ throw resolutionError(packageDir, "the declared bin.pi target escapes the package directory");
100
+ }
101
+ return candidate;
102
+ }
103
+
104
+ function resolveExistingFile(packageDir: string, candidate: string, reason: string): string {
105
+ let resolved: string;
106
+ try {
107
+ resolved = fs.realpathSync(candidate);
108
+ if (!fs.statSync(resolved).isFile()) throw new Error("not a file");
109
+ } catch {
110
+ throw resolutionError(packageDir, reason);
111
+ }
112
+ if (!isWithinDirectory(packageDir, resolved)) {
113
+ throw resolutionError(packageDir, "the declared bin.pi target escapes the package directory");
114
+ }
115
+ return resolved;
116
+ }
117
+
118
+ function resolveStandaloneExecutable(
119
+ packageDir: string,
120
+ runtime: PiInvocationRuntime,
121
+ ): string | undefined {
122
+ if (runtime.runtimeKind !== "bun") return undefined;
123
+ const { execPath } = runtime;
124
+ if (!/^pi(?:\.exe)?$/i.test(path.basename(execPath))) return undefined;
125
+ let resolved: string;
126
+ let mode: number;
127
+ try {
128
+ resolved = fs.realpathSync(execPath);
129
+ const stat = fs.statSync(resolved);
130
+ if (!stat.isFile()) throw new Error("not a file");
131
+ mode = stat.mode;
132
+ } catch {
133
+ throw resolutionError(packageDir, "the standalone Pi executable is unavailable");
134
+ }
135
+ if (path.dirname(resolved) !== packageDir) return undefined;
136
+ if (process.platform !== "win32" && (mode & 0o111) === 0) {
137
+ throw resolutionError(packageDir, "the standalone Pi executable is not executable");
138
+ }
139
+ return resolved;
140
+ }
141
+
142
+ export function resolvePiInvocation(
143
+ args: string[],
144
+ runtime: PiInvocationRuntime = currentRuntime(),
145
+ ): PiInvocation {
146
+ let packageDir: string;
147
+ try {
148
+ packageDir = fs.realpathSync(runtime.packageDir);
149
+ if (!fs.statSync(packageDir).isDirectory()) throw new Error("not a directory");
150
+ } catch {
151
+ throw resolutionError(runtime.packageDir, "the package directory is unavailable");
152
+ }
153
+
154
+ const manifest = readCoreManifest(packageDir);
155
+ const declaredBin = resolveDeclaredBin(packageDir, manifest);
156
+ const standalone = resolveStandaloneExecutable(packageDir, runtime);
157
+ if (standalone) return { command: standalone, args: [...args] };
158
+
159
+ if (runtime.runtimeKind !== "node" && runtime.runtimeKind !== "bun") {
160
+ throw resolutionError(packageDir, "the host does not provide a supported Node or Bun runtime");
161
+ }
162
+ const cliPath = resolveExistingFile(
163
+ packageDir,
164
+ declaredBin,
165
+ "the declared bin.pi target is unavailable",
166
+ );
167
+ return { command: runtime.execPath, args: [cliPath, ...args] };
168
+ }
package/src/registry.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { SubagentThinkingLevel } from "./agents.js";
3
+ import type { TargetPolicyAudit } from "./cwd-policy.js";
3
4
  import { DEFAULT_MAX_CONTEXT_BYTES, DEFAULT_MAX_OUTPUT_BYTES, truncateUtf8 } from "./limits.js";
4
5
  import { type AgentTurnRunner, normalizeTransport, type SubagentTransport } from "./transport.js";
5
6
 
@@ -50,6 +51,8 @@ export interface ManagedAgent {
50
51
  context?: string;
51
52
  contextSourceIds?: string[];
52
53
  contextTruncated?: boolean;
54
+ workspaceMode?: "worktree";
55
+ target?: TargetPolicyAudit;
53
56
  policy?: { inherited: string[]; overridden: string[]; unsupported: string[] };
54
57
  mailbox: AgentMailboxMessage[];
55
58
  currentMailboxMessageIds?: string[];
@@ -70,6 +73,8 @@ export interface AgentRunInspectionDetail extends AgentRunInspectionSummary {
70
73
  thinkingLevel?: SubagentThinkingLevel;
71
74
  currentTask?: string;
72
75
  error?: string;
76
+ workspaceMode?: "worktree";
77
+ target?: TargetPolicyAudit;
73
78
  policy?: { inherited: string[]; overridden: string[]; unsupported: string[] };
74
79
  }
75
80
 
@@ -243,6 +248,8 @@ export class AgentRegistry {
243
248
  context?: string;
244
249
  contextSourceIds?: string[];
245
250
  contextTruncated?: boolean;
251
+ workspaceMode?: "worktree";
252
+ target?: TargetPolicyAudit;
246
253
  }): Promise<ManagedAgent> {
247
254
  if (!input.task.trim()) throw new Error("Subagent tasks cannot be empty");
248
255
  const task = truncateUtf8(input.task, this.maxTaskBytes).text;
@@ -286,6 +293,8 @@ export class AgentRegistry {
286
293
  context: input.context,
287
294
  contextSourceIds: input.contextSourceIds,
288
295
  contextTruncated: input.contextTruncated,
296
+ workspaceMode: input.workspaceMode,
297
+ target: input.target,
289
298
  };
290
299
  this.agents.set(record.id, record);
291
300
  if (parent) {
@@ -549,6 +558,8 @@ export class AgentRegistry {
549
558
  thinkingLevel: agent.thinkingLevel,
550
559
  currentTask: agent.currentTask,
551
560
  error: agent.error,
561
+ workspaceMode: agent.workspaceMode,
562
+ target: agent.target ? { ...agent.target, trust: { ...agent.target.trust } } : undefined,
552
563
  policy: agent.policy
553
564
  ? {
554
565
  inherited: [...agent.policy.inherited],
@@ -840,6 +851,7 @@ export class AgentRegistry {
840
851
  : undefined,
841
852
  history: agent.history.map((turn) => ({ ...turn })),
842
853
  mailbox: agent.mailbox.map((message) => ({ ...message })),
854
+ target: agent.target ? { ...agent.target, trust: { ...agent.target.trust } } : undefined,
843
855
  policy: agent.policy
844
856
  ? {
845
857
  inherited: [...agent.policy.inherited],