@narumitw/pi-subagents 0.43.0 → 0.43.1

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,194 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import {
3
+ getMarkdownTheme,
4
+ type Theme,
5
+ type ToolRenderResultOptions,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
8
+ import type { ConsultDetails, SubagentConsultParams } from "./consult.js";
9
+ import { formatUsageStats } from "./render.js";
10
+ import {
11
+ COLLAPSED_ANSWER_LINES,
12
+ COLLAPSED_LIST_LIMIT,
13
+ expansionHint,
14
+ previewLines,
15
+ projectRenderActivity,
16
+ type RenderStatus,
17
+ recordValue,
18
+ renderActivityLines,
19
+ renderFallbackResult,
20
+ safeBlock,
21
+ safeLine,
22
+ statusBadge,
23
+ stringValue,
24
+ type ToolRendererContext,
25
+ textResult,
26
+ toolHeader,
27
+ } from "./render-common.js";
28
+
29
+ export function renderConsultCall(args: Partial<SubagentConsultParams>, theme: Theme) {
30
+ const scope = args.agentScope ?? "user";
31
+ const text = [
32
+ toolHeader(theme, "subagent_consult", args.agent, [`[${scope}]`, "read-only"]),
33
+ ` ${theme.fg("dim", safeLine(args.task, "...", 2 * 1024))}`,
34
+ ].join("\n");
35
+ return new Text(text, 0, 0);
36
+ }
37
+
38
+ export function renderConsultResult(
39
+ result: AgentToolResult<ConsultDetails>,
40
+ options: ToolRenderResultOptions,
41
+ theme: Theme,
42
+ context: ToolRendererContext<SubagentConsultParams>,
43
+ ) {
44
+ const details = recordValue(result.details);
45
+ if (!details || !recordValue(details.policy)) {
46
+ return renderFallbackResult(result, options, theme, context.isError);
47
+ }
48
+
49
+ const progress = recordValue(details.progress);
50
+ const child = recordValue(details.child);
51
+ const status = consultStatus(details, progress, options, context.isError);
52
+ const agent = safeLine(details.agent, safeLine(context.args?.agent, "subagent"), 256);
53
+ const source = safeLine(details.agentSource, "unknown", 128);
54
+ const usage = recordValue(progress?.usage ?? child?.usage);
55
+ const usageText = formatUnknownUsage(
56
+ usage,
57
+ stringValue(details.model) || undefined,
58
+ stringValue(details.thinkingLevel) || undefined,
59
+ stringValue(progress?.actualProvider ?? child?.actualProvider) || undefined,
60
+ stringValue(progress?.actualModel ?? child?.actualModel) || undefined,
61
+ );
62
+ const effectiveTools = stringList(recordValue(details.policy)?.effectiveTools);
63
+ const activity = projectRenderActivity(progress?.recentActivity);
64
+ const activityTotal = Math.max(
65
+ activity.length,
66
+ typeof progress?.recentActivityTotal === "number" ? progress.recentActivityTotal : 0,
67
+ );
68
+ const answer = safeBlock(textResult(result), "", 50 * 1024).trim();
69
+
70
+ if (options.expanded) {
71
+ const container = new Container();
72
+ container.addChild(
73
+ new Text(
74
+ `${statusBadge(theme, status)} · ${theme.fg("toolTitle", theme.bold(agent))}${theme.fg("muted", ` (${source})`)}`,
75
+ 0,
76
+ 0,
77
+ ),
78
+ );
79
+ if (usageText) container.addChild(new Text(theme.fg("dim", usageText), 0, 0));
80
+ container.addChild(new Spacer(1));
81
+ container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
82
+ container.addChild(
83
+ new Text(theme.fg("dim", safeBlock(context.args?.task, "(unavailable)", 50 * 1024)), 0, 0),
84
+ );
85
+ container.addChild(new Spacer(1));
86
+ container.addChild(new Text(theme.fg("muted", "─── Policy ───"), 0, 0));
87
+ container.addChild(new Text(theme.fg("dim", policySummary(details.policy)), 0, 0));
88
+ if (activity.length > 0) {
89
+ container.addChild(new Spacer(1));
90
+ container.addChild(new Text(theme.fg("muted", "─── Activity ───"), 0, 0));
91
+ container.addChild(
92
+ new Text(renderActivityLines(activity, theme, undefined, activityTotal), 0, 0),
93
+ );
94
+ }
95
+ container.addChild(new Spacer(1));
96
+ container.addChild(new Text(theme.fg("muted", "─── Answer ───"), 0, 0));
97
+ if (answer) container.addChild(new Markdown(answer, 0, 0, getMarkdownTheme()));
98
+ else
99
+ container.addChild(
100
+ new Text(
101
+ theme.fg("muted", options.isPartial ? "(waiting for output)" : "(no output)"),
102
+ 0,
103
+ 0,
104
+ ),
105
+ );
106
+ return container;
107
+ }
108
+
109
+ const lines = [
110
+ `${statusBadge(theme, status)} · ${theme.fg("toolTitle", theme.bold(agent))}${theme.fg("muted", ` (${source})`)}`,
111
+ ];
112
+ if (usageText) lines.push(theme.fg("dim", usageText));
113
+ if (effectiveTools.length > 0 && options.isPartial) {
114
+ lines.push(theme.fg("dim", `tools: ${effectiveTools.join(", ")}`));
115
+ }
116
+ if (activity.length > 0) {
117
+ lines.push(renderActivityLines(activity, theme, COLLAPSED_LIST_LIMIT, activityTotal));
118
+ } else if (options.isPartial) {
119
+ lines.push(
120
+ theme.fg("muted", status === "starting" ? "(starting child)" : "(waiting for activity)"),
121
+ );
122
+ } else if (answer) {
123
+ lines.push(theme.fg("toolOutput", previewLines(answer, COLLAPSED_ANSWER_LINES)));
124
+ } else {
125
+ const error = safeBlock(child?.error, "", 2 * 1024).trim();
126
+ lines.push(theme.fg(status === "failed" ? "error" : "muted", error || "(no output)"));
127
+ }
128
+ if (!options.isPartial) lines.push(expansionHint());
129
+ return new Text(lines.filter(Boolean).join("\n"), 0, 0);
130
+ }
131
+
132
+ function consultStatus(
133
+ details: Record<string, unknown>,
134
+ progress: Record<string, unknown> | undefined,
135
+ options: ToolRenderResultOptions,
136
+ isError: boolean,
137
+ ): RenderStatus {
138
+ if (details.cancelled === true) return "cancelled";
139
+ if (isError || details.isError === true) {
140
+ const child = recordValue(details.child);
141
+ return child?.aborted === true && child?.timedOut !== true ? "cancelled" : "failed";
142
+ }
143
+ if (options.isPartial) return progress?.phase === "starting" ? "starting" : "running";
144
+ return "completed";
145
+ }
146
+
147
+ function stringList(value: unknown): string[] {
148
+ return Array.isArray(value)
149
+ ? value.flatMap((item) => (typeof item === "string" ? [safeLine(item, "", 256)] : []))
150
+ : [];
151
+ }
152
+
153
+ function policySummary(value: unknown): string {
154
+ const policy = recordValue(value);
155
+ if (!policy) return "(unavailable)";
156
+ const resources = recordValue(policy.effectiveResources);
157
+ const tools = stringList(policy.effectiveTools);
158
+ return [
159
+ `tools: ${tools.length > 0 ? tools.join(", ") : "none"}`,
160
+ `resources: ${safeLine(resources?.policy ?? policy.requestedResources, "unknown", 256)}`,
161
+ `extensions: ${safeLine(policy.extensions, "unknown", 128)}`,
162
+ `session persistence: ${safeLine(policy.sessionPersistence, "unknown", 128)}`,
163
+ `retained agent: ${policy.retainedAgent === false ? "no" : "unknown"}`,
164
+ ].join("\n");
165
+ }
166
+
167
+ function formatUnknownUsage(
168
+ value: Record<string, unknown> | undefined,
169
+ model?: string,
170
+ thinkingLevel?: string,
171
+ actualProvider?: string,
172
+ actualModel?: string,
173
+ ): string {
174
+ const usage = value ?? {};
175
+ return formatUsageStats(
176
+ {
177
+ input: safeNumber(usage.input),
178
+ output: safeNumber(usage.output),
179
+ cacheRead: safeNumber(usage.cacheRead),
180
+ cacheWrite: safeNumber(usage.cacheWrite),
181
+ cost: safeNumber(usage.cost),
182
+ contextTokens: safeNumber(usage.contextTokens),
183
+ turns: safeNumber(usage.turns),
184
+ },
185
+ model,
186
+ thinkingLevel as never,
187
+ actualProvider,
188
+ actualModel,
189
+ );
190
+ }
191
+
192
+ function safeNumber(value: unknown): number {
193
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
194
+ }
package/src/consult.ts CHANGED
@@ -21,6 +21,12 @@ import {
21
21
  THINKING_LEVELS,
22
22
  } from "./agents.js";
23
23
  import { resolveConsultTools } from "./consult-policy.js";
24
+ import { renderConsultCall, renderConsultResult } from "./consult-render.js";
25
+ import {
26
+ assertConsultationTargetAllowed,
27
+ type ResolvedSubagentTarget,
28
+ resolveSubagentTarget,
29
+ } from "./cwd-policy.js";
24
30
  import { assertSubagentDepthAllowed, resolveDefaultSubagentTimeoutMs } from "./execution.js";
25
31
  import {
26
32
  DEFAULT_MAX_CONTEXT_BYTES,
@@ -37,7 +43,11 @@ import {
37
43
  type SubagentDetails,
38
44
  } from "./runner.js";
39
45
  import { boundedPrivateText, boundText, safeDisplayPath, safeTerminalLine } from "./safe-text.js";
40
- import { DEFAULT_CONSULT_RESOURCE_POLICY, resolveSubagentThinkingLevel } from "./settings.js";
46
+ import {
47
+ DEFAULT_CONSULT_RESOURCE_POLICY,
48
+ DEFAULT_CONSULTATION_CWD_POLICY,
49
+ resolveSubagentThinkingLevel,
50
+ } from "./settings.js";
41
51
 
42
52
  const ConsultScopeSchema = StringEnum(["user", "project", "both"] as const, {
43
53
  default: "user",
@@ -80,6 +90,30 @@ export interface RegisterSubagentConsultOptions {
80
90
  invocationOverride?: { command: string; argsPrefix?: string[] };
81
91
  }
82
92
 
93
+ export interface ConsultProgressActivity {
94
+ type: "text" | "toolCall";
95
+ text?: string;
96
+ name?: "read" | "grep" | "find" | "ls";
97
+ args?: Record<string, string | number | boolean>;
98
+ }
99
+
100
+ export interface ConsultProgress {
101
+ phase: "starting" | "running";
102
+ recentActivity: ConsultProgressActivity[];
103
+ recentActivityTotal: number;
104
+ actualProvider?: string;
105
+ actualModel?: string;
106
+ usage: {
107
+ input: number;
108
+ output: number;
109
+ cacheRead: number;
110
+ cacheWrite: number;
111
+ cost: number;
112
+ contextTokens: number;
113
+ turns: number;
114
+ };
115
+ }
116
+
83
117
  export interface ConsultDetails {
84
118
  agent: string;
85
119
  agentSource: string;
@@ -91,6 +125,13 @@ export interface ConsultDetails {
91
125
  policy: {
92
126
  requestedTools: string[] | null;
93
127
  effectiveTools: string[];
128
+ cwdBoundary: "current-workspace" | "external";
129
+ targetTrust: {
130
+ kind: string;
131
+ projectTrusted: boolean;
132
+ sourcePath?: string;
133
+ warning?: string;
134
+ };
94
135
  requestedResources: ConsultResourcePolicy;
95
136
  effectiveResources: {
96
137
  policy: ConsultResourcePolicy;
@@ -99,11 +140,13 @@ export interface ConsultDetails {
99
140
  skills: boolean;
100
141
  promptTemplates: boolean;
101
142
  };
143
+ resourceDowngradeReason?: string;
102
144
  extensions: "disabled";
103
145
  sessionPersistence: "disabled";
104
146
  retainedAgent: false;
105
147
  };
106
148
  child?: Record<string, unknown>;
149
+ progress?: ConsultProgress;
107
150
  cancelled?: boolean;
108
151
  isError?: boolean;
109
152
  truncated?: boolean;
@@ -143,12 +186,12 @@ export function registerSubagentConsult(
143
186
  cancelAndWaitForChildren("Subagent consultation session shut down"),
144
187
  );
145
188
 
146
- const baseDescription =
147
- "Run one ephemeral subagent synchronously under enforced read-only tool and resource policies and return its answer. The child can use only the effective subset of Pi's built-in read, grep, find, and ls tools. Shell commands, file writes, extension tools, detached lifecycle operations, and persistent agent state are disabled.";
189
+ const baseDescription = () =>
190
+ `Run one ephemeral subagent synchronously under enforced read-only tool and resource policies and return its answer. The child can use only the effective subset of Pi's built-in read, grep, find, and ls tools. Shell commands, file writes, extension tools, detached lifecycle operations, and persistent agent state are disabled. Working-directory target policy: ${options.getSettings()?.cwdPolicy?.consultation ?? DEFAULT_CONSULTATION_CWD_POLICY}; configured trusted-target resources: ${options.getSettings()?.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY}; allowed targets without effective trust inherit no target/project resources. This is not a filesystem sandbox.`;
148
191
  const definition: ToolDefinition<typeof SubagentConsultParams, ConsultDetails> = {
149
192
  name: "subagent_consult",
150
193
  label: "Consult Read-only Subagent",
151
- description: baseDescription,
194
+ description: baseDescription(),
152
195
  promptSnippet: "Consult one constrained read-only subagent and wait for its answer",
153
196
  promptGuidelines: [
154
197
  "Use subagent_consult for bounded reconnaissance, planning, or review whose result is required in the current turn.",
@@ -187,6 +230,12 @@ export function registerSubagentConsult(
187
230
  active.delete(ownedController);
188
231
  }
189
232
  },
233
+ renderCall(args, theme) {
234
+ return renderConsultCall(args, theme);
235
+ },
236
+ renderResult(result, renderOptions, theme, context) {
237
+ return renderConsultResult(result, renderOptions, theme, context);
238
+ },
190
239
  };
191
240
  pi.registerTool<typeof SubagentConsultParams, ConsultDetails>(definition);
192
241
  pi.on("tool_result", (event) => {
@@ -194,7 +243,7 @@ export function registerSubagentConsult(
194
243
  if ((event.details as ConsultDetails | undefined)?.isError) return { isError: true };
195
244
  });
196
245
  return (catalog: string) => {
197
- definition.description = catalog ? `${baseDescription}\n\n${catalog}` : baseDescription;
246
+ definition.description = catalog ? `${baseDescription()}\n\n${catalog}` : baseDescription();
198
247
  pi.registerTool<typeof SubagentConsultParams, ConsultDetails>(definition);
199
248
  };
200
249
  }
@@ -294,6 +343,15 @@ async function executeConsult(
294
343
  throw new Error("Project-local subagent definitions require a trusted project");
295
344
  }
296
345
  const settings = options.getSettings();
346
+ const target = resolveSubagentTarget({
347
+ workspace: ctx.cwd,
348
+ requestedCwd: operation.cwd,
349
+ currentProjectTrusted: ctx.isProjectTrusted(),
350
+ });
351
+ assertConsultationTargetAllowed(
352
+ target,
353
+ settings?.cwdPolicy?.consultation ?? DEFAULT_CONSULTATION_CWD_POLICY,
354
+ );
297
355
  const discovery = discoverAgents(ctx.cwd, operation.agentScope, settings);
298
356
  const agent = discovery.agents.find((candidate) => candidate.name === operation.agent);
299
357
  if (!agent) {
@@ -302,7 +360,7 @@ async function executeConsult(
302
360
  `Available agents for agentScope "${operation.agentScope}": ${formatAvailableConsultAgents(discovery)}`,
303
361
  );
304
362
  }
305
- const setup = resolveConsultSetup(operation, agent, settings, ctx);
363
+ const setup = resolveConsultSetup(operation, agent, settings, target);
306
364
 
307
365
  if (agent.source === "project" && operation.confirmProjectAgents) {
308
366
  if (!ctx.hasUI) {
@@ -323,6 +381,8 @@ async function executeConsult(
323
381
  }
324
382
  }
325
383
  assertCurrentRequest(signal, isCurrent);
384
+ emitUpdate(consultStartingUpdate(setup.details));
385
+ assertCurrentRequest(signal, isCurrent);
326
386
  const runChild = options.runChild ?? ((request) => runConsultChild(request, options));
327
387
  const child = runChild({
328
388
  agent: setup.agent,
@@ -376,19 +436,13 @@ function resolveConsultSetup(
376
436
  operation: ReturnType<typeof validateConsultParams>,
377
437
  agent: AgentConfig,
378
438
  settings: SubagentSettings | undefined,
379
- ctx: ExtensionContext,
439
+ target: ResolvedSubagentTarget,
380
440
  ) {
381
- const cwd = canonicalDirectory(operation.cwd ?? ctx.cwd, "subagent consultation cwd");
382
- const workspace = canonicalDirectory(ctx.cwd, "current workspace");
383
- const resourcePolicy = settings?.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY;
384
- if (!isWithin(cwd, workspace) && resourcePolicy !== "none") {
385
- throw new Error(
386
- `Subagent consultation cwd is outside the current workspace; consult.resources must be "none": ${safeTerminalLine(cwd)}`,
387
- );
388
- }
441
+ const requestedResourcePolicy = settings?.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY;
442
+ const resourcePolicy = target.trust.projectTrusted ? requestedResourcePolicy : "none";
389
443
  const effectiveTools = resolveConsultTools(agent.tools);
390
- const projectTrusted = ctx.isProjectTrusted();
391
- const launchPolicy = resourceLaunchPolicy(resourcePolicy, projectTrusted, workspace);
444
+ const projectTrusted = target.trust.projectTrusted;
445
+ const launchPolicy = resourceLaunchPolicy(resourcePolicy, projectTrusted, target.cwd);
392
446
  launchPolicy.tools = effectiveTools;
393
447
  const thinkingLevel = resolveSubagentThinkingLevel([agent], agent.name, operation.thinkingLevel);
394
448
  const timeoutMs = operation.timeoutMs ?? agent.timeoutMs ?? resolveDefaultSubagentTimeoutMs();
@@ -413,7 +467,7 @@ function resolveConsultSetup(
413
467
  agent: boundedPrivateText(agent.name, 256),
414
468
  agentSource: agent.source,
415
469
  agentScope: operation.agentScope,
416
- cwd: safeDisplayPath(cwd, workspace),
470
+ cwd: safeDisplayPath(target.cwd, target.workspace),
417
471
  model: agent.model ? boundedPrivateText(agent.model, 256) : undefined,
418
472
  thinkingLevel,
419
473
  timeoutMs,
@@ -423,8 +477,20 @@ function resolveConsultSetup(
423
477
  ? null
424
478
  : agent.tools.slice(0, 100).map((tool) => boundedPrivateText(tool, 256)),
425
479
  effectiveTools,
426
- requestedResources: resourcePolicy,
480
+ cwdBoundary: target.boundary,
481
+ targetTrust: {
482
+ kind: target.trust.kind,
483
+ projectTrusted,
484
+ sourcePath: target.trust.sourcePath
485
+ ? safeDisplayPath(target.trust.sourcePath, target.workspace)
486
+ : undefined,
487
+ warning: target.trust.warning,
488
+ },
489
+ requestedResources: requestedResourcePolicy,
427
490
  effectiveResources,
491
+ ...(resourcePolicy !== requestedResourcePolicy
492
+ ? { resourceDowngradeReason: `Target trust is ${target.trust.kind}` }
493
+ : {}),
428
494
  extensions: "disabled",
429
495
  sessionPersistence: "disabled",
430
496
  retainedAgent: false,
@@ -432,7 +498,7 @@ function resolveConsultSetup(
432
498
  };
433
499
  return {
434
500
  agent: childAgent,
435
- cwd,
501
+ cwd: target.cwd,
436
502
  resourcePolicy,
437
503
  effectiveTools,
438
504
  thinkingLevel,
@@ -471,6 +537,21 @@ async function runConsultChild(
471
537
  );
472
538
  }
473
539
 
540
+ function consultStartingUpdate(details: ConsultDetails): AgentToolResult<ConsultDetails> {
541
+ return {
542
+ content: [{ type: "text", text: "Read-only subagent consultation starting." }],
543
+ details: {
544
+ ...details,
545
+ progress: {
546
+ phase: "starting",
547
+ recentActivity: [],
548
+ recentActivityTotal: 0,
549
+ usage: emptyProgressUsage(),
550
+ },
551
+ },
552
+ };
553
+ }
554
+
474
555
  function consultUpdate(
475
556
  result: SingleResult,
476
557
  details: ConsultDetails,
@@ -478,7 +559,69 @@ function consultUpdate(
478
559
  const output = boundText(getResultFinalOutput(result) || "(running...)");
479
560
  return {
480
561
  content: [{ type: "text", text: output.text }],
481
- details: { ...details, child: projectChildResult(result) },
562
+ details: {
563
+ ...details,
564
+ child: projectChildResult(result),
565
+ progress: projectConsultProgress(result),
566
+ },
567
+ };
568
+ }
569
+
570
+ const CONSULT_ACTIVITY_ARGUMENTS: Record<"read" | "grep" | "find" | "ls", readonly string[]> = {
571
+ read: ["path", "file_path", "offset", "limit"],
572
+ grep: ["pattern", "path", "glob", "limit"],
573
+ find: ["pattern", "path", "limit"],
574
+ ls: ["path", "limit"],
575
+ };
576
+
577
+ function projectConsultProgress(result: SingleResult): ConsultProgress {
578
+ const recentActivity: ConsultProgressActivity[] = [];
579
+ for (const item of result.recentActivity ?? []) {
580
+ if (item.type === "text") {
581
+ const text = boundedPrivateText(item.text, 1024).trim();
582
+ if (text) recentActivity.push({ type: "text", text });
583
+ continue;
584
+ }
585
+ if (!Object.hasOwn(CONSULT_ACTIVITY_ARGUMENTS, item.name)) continue;
586
+ const name = item.name as keyof typeof CONSULT_ACTIVITY_ARGUMENTS;
587
+ const args: Record<string, string | number | boolean> = {};
588
+ for (const key of CONSULT_ACTIVITY_ARGUMENTS[name]) {
589
+ const value = item.args[key];
590
+ if (typeof value === "string") args[key] = safeTerminalLine(value, 512);
591
+ else if (typeof value === "number" && Number.isFinite(value)) args[key] = value;
592
+ else if (typeof value === "boolean") args[key] = value;
593
+ }
594
+ recentActivity.push({ type: "toolCall", name, args });
595
+ }
596
+ return {
597
+ phase: "running",
598
+ recentActivity,
599
+ recentActivityTotal: Math.max(recentActivity.length, result.recentActivityTotal ?? 0),
600
+ actualProvider: result.actualProvider
601
+ ? boundedPrivateText(result.actualProvider, 256)
602
+ : undefined,
603
+ actualModel: result.actualModel ? boundedPrivateText(result.actualModel, 256) : undefined,
604
+ usage: {
605
+ input: result.usage.input,
606
+ output: result.usage.output,
607
+ cacheRead: result.usage.cacheRead,
608
+ cacheWrite: result.usage.cacheWrite,
609
+ cost: result.usage.cost,
610
+ contextTokens: result.usage.contextTokens,
611
+ turns: result.usage.turns,
612
+ },
613
+ };
614
+ }
615
+
616
+ function emptyProgressUsage(): ConsultProgress["usage"] {
617
+ return {
618
+ input: 0,
619
+ output: 0,
620
+ cacheRead: 0,
621
+ cacheWrite: 0,
622
+ cost: 0,
623
+ contextTokens: 0,
624
+ turns: 0,
482
625
  };
483
626
  }
484
627
 
@@ -623,22 +766,6 @@ function usageFromResult(result: SingleResult): Usage {
623
766
  };
624
767
  }
625
768
 
626
- function canonicalDirectory(value: string, label: string): string {
627
- try {
628
- const resolved = fs.realpathSync(value);
629
- if (!fs.statSync(resolved).isDirectory()) throw new Error("not a directory");
630
- return resolved;
631
- } catch (error) {
632
- const reason = error instanceof Error ? error.message : String(error);
633
- throw new Error(`Invalid ${label}: ${safeTerminalLine(value)} (${safeTerminalLine(reason)})`);
634
- }
635
- }
636
-
637
- function isWithin(candidate: string, root: string): boolean {
638
- const relative = path.relative(root, candidate);
639
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
640
- }
641
-
642
769
  function assertCurrentRequest(signal: AbortSignal, isCurrent: () => boolean): void {
643
770
  if (signal.aborted || !isCurrent()) throw abortError("Subagent consultation owner was replaced");
644
771
  }