@narumitw/pi-subagents 0.42.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.
package/src/runner.ts CHANGED
@@ -6,12 +6,14 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
6
6
  import type { Message } from "@earendil-works/pi-ai";
7
7
  import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
8
8
  import type { AgentConfig, AgentScope, AgentSource, SubagentThinkingLevel } from "./agents.js";
9
+ import type { TargetPolicyAudit } from "./cwd-policy.js";
9
10
  import {
10
11
  appendBounded,
11
12
  DEFAULT_MAX_CONTEXT_BYTES,
12
13
  DEFAULT_MAX_MESSAGES,
13
14
  DEFAULT_MAX_OUTPUT_BYTES,
14
15
  DEFAULT_MAX_STDERR_BYTES,
16
+ MAX_SUBAGENT_TIMEOUT_MS,
15
17
  truncateUtf8,
16
18
  } from "./limits.js";
17
19
  import { JsonLineDecoder } from "./protocol.js";
@@ -24,6 +26,11 @@ export interface UsageStats {
24
26
  cacheRead: number;
25
27
  cacheWrite: number;
26
28
  cost: number;
29
+ costInput?: number;
30
+ costOutput?: number;
31
+ costCacheRead?: number;
32
+ costCacheWrite?: number;
33
+ totalTokens?: number;
27
34
  contextTokens: number;
28
35
  turns: number;
29
36
  }
@@ -34,6 +41,21 @@ export type RecentActivityItem =
34
41
  const MAX_RECENT_ACTIVITY_ITEMS = 10;
35
42
  const MAX_RECENT_ACTIVITY_BYTES = 8 * 1024;
36
43
  const MAX_RECENT_ACTIVITY_ARGUMENT_BYTES = 1024;
44
+ const MAX_USAGE_VALUE = Number.MAX_SAFE_INTEGER;
45
+
46
+ function protocolUsageCount(value: unknown): number {
47
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
48
+ }
49
+
50
+ function protocolUsageCost(value: unknown): number {
51
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
52
+ ? Math.min(value, MAX_USAGE_VALUE)
53
+ : 0;
54
+ }
55
+
56
+ function addUsageValue(current: number, addition: number): number {
57
+ return Math.min(MAX_USAGE_VALUE, current + addition);
58
+ }
37
59
 
38
60
  export interface SingleResult {
39
61
  agent: string;
@@ -58,6 +80,9 @@ export interface SingleResult {
58
80
  aborted?: boolean;
59
81
  truncated?: boolean;
60
82
  malformedEvents?: number;
83
+ launchFailed?: boolean;
84
+ processStarted?: boolean;
85
+ target?: TargetPolicyAudit;
61
86
  policy?: {
62
87
  inherited: string[];
63
88
  overridden: string[];
@@ -288,20 +313,41 @@ async function writePromptToTempFile(
288
313
  return { dir: tmpDir, filePath };
289
314
  }
290
315
 
291
- export function buildPiArgs(options: {
316
+ export interface PiArgsOptions {
292
317
  model?: string;
293
318
  thinkingLevel?: SubagentThinkingLevel;
294
319
  tools?: string[];
320
+ disableExtensions?: boolean;
321
+ disableSkills?: boolean;
322
+ disablePromptTemplates?: boolean;
323
+ disableContextFiles?: boolean;
324
+ projectTrust?: boolean;
325
+ baseSystemPromptPath?: string;
326
+ appendSystemPromptPaths?: string[];
327
+ /** Existing single append prompt path retained for compatibility. */
295
328
  systemPromptPath?: string;
296
329
  task: string;
297
- }): string[] {
330
+ }
331
+
332
+ export function buildPiArgs(options: PiArgsOptions): string[] {
298
333
  const args: string[] = ["--mode", "json", "-p", "--no-session"];
299
334
  if (options.model) args.push("--model", options.model);
300
335
  if (options.thinkingLevel) args.push("--thinking", options.thinkingLevel);
336
+ if (options.disableExtensions) args.push("--no-extensions");
337
+ if (options.disableSkills) args.push("--no-skills");
338
+ if (options.disablePromptTemplates) args.push("--no-prompt-templates");
339
+ if (options.disableContextFiles) args.push("--no-context-files");
340
+ if (options.projectTrust !== undefined) {
341
+ args.push(options.projectTrust ? "--approve" : "--no-approve");
342
+ }
301
343
  if (Array.isArray(options.tools)) {
302
344
  if (options.tools.length > 0) args.push("--tools", options.tools.join(","));
303
345
  else args.push("--no-tools");
304
346
  }
347
+ if (options.baseSystemPromptPath) args.push("--system-prompt", options.baseSystemPromptPath);
348
+ for (const promptPath of options.appendSystemPromptPaths ?? []) {
349
+ args.push("--append-system-prompt", promptPath);
350
+ }
305
351
  if (options.systemPromptPath) args.push("--append-system-prompt", options.systemPromptPath);
306
352
  args.push(`Task: ${options.task}`);
307
353
  return args;
@@ -365,6 +411,17 @@ export function terminateProcess(
365
411
 
366
412
  export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
367
413
 
414
+ export interface ChildLaunchPolicy {
415
+ tools?: string[];
416
+ disableExtensions?: boolean;
417
+ disableSkills?: boolean;
418
+ disablePromptTemplates?: boolean;
419
+ disableContextFiles?: boolean;
420
+ projectTrust?: boolean;
421
+ baseSystemPrompt?: string;
422
+ appendSystemPromptPaths?: string[];
423
+ }
424
+
368
425
  export async function runSingleAgent(
369
426
  defaultCwd: string,
370
427
  agents: AgentConfig[],
@@ -378,6 +435,7 @@ export async function runSingleAgent(
378
435
  onUpdate: OnUpdateCallback | undefined,
379
436
  makeDetails: (results: SingleResult[]) => SubagentDetails,
380
437
  invocationOverride?: { command: string; argsPrefix?: string[] },
438
+ launchPolicy?: ChildLaunchPolicy,
381
439
  ): Promise<SingleResult> {
382
440
  const agent = agents.find((a) => a.name === agentName);
383
441
 
@@ -405,8 +463,9 @@ export async function runSingleAgent(
405
463
  };
406
464
  }
407
465
 
408
- let tmpPromptDir: string | null = null;
466
+ const temporaryPrompts: Array<{ dir: string; filePath: string }> = [];
409
467
  let tmpPromptPath: string | null = null;
468
+ let baseSystemPromptPath: string | null = null;
410
469
 
411
470
  let latestAssistantOutput = "";
412
471
  let terminalAssistantOutput: string | undefined;
@@ -474,17 +533,46 @@ export async function runSingleAgent(
474
533
  setErrorMessage("Subagent was aborted before start");
475
534
  return currentResult;
476
535
  }
536
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_SUBAGENT_TIMEOUT_MS) {
537
+ currentResult.exitCode = 1;
538
+ currentResult.stopReason = "error";
539
+ setErrorMessage(
540
+ `Invalid subagent timeout: expected 1-${MAX_SUBAGENT_TIMEOUT_MS}ms, received ${timeoutMs}`,
541
+ );
542
+ return currentResult;
543
+ }
477
544
 
545
+ if (launchPolicy?.baseSystemPrompt?.trim()) {
546
+ const tmp = await writePromptToTempFile(`${agent.name}-base`, launchPolicy.baseSystemPrompt);
547
+ temporaryPrompts.push(tmp);
548
+ baseSystemPromptPath = tmp.filePath;
549
+ }
478
550
  if (agent.systemPrompt.trim()) {
479
551
  const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
480
- tmpPromptDir = tmp.dir;
552
+ temporaryPrompts.push(tmp);
481
553
  tmpPromptPath = tmp.filePath;
482
554
  }
555
+ if (signal?.aborted) {
556
+ currentResult.exitCode = 130;
557
+ currentResult.aborted = true;
558
+ currentResult.stopReason = "aborted";
559
+ setErrorMessage("Subagent was aborted before launch");
560
+ return currentResult;
561
+ }
483
562
 
563
+ const effectiveTools =
564
+ launchPolicy && Object.hasOwn(launchPolicy, "tools") ? launchPolicy.tools : agent.tools;
484
565
  const args = buildPiArgs({
485
566
  model: agent.model,
486
567
  thinkingLevel,
487
- tools: agent.tools,
568
+ tools: effectiveTools,
569
+ disableExtensions: launchPolicy?.disableExtensions,
570
+ disableSkills: launchPolicy?.disableSkills,
571
+ disablePromptTemplates: launchPolicy?.disablePromptTemplates,
572
+ disableContextFiles: launchPolicy?.disableContextFiles,
573
+ projectTrust: launchPolicy?.projectTrust,
574
+ baseSystemPromptPath: baseSystemPromptPath ?? undefined,
575
+ appendSystemPromptPaths: launchPolicy?.appendSystemPromptPaths,
488
576
  systemPromptPath: tmpPromptPath ?? undefined,
489
577
  task,
490
578
  });
@@ -525,6 +613,7 @@ export async function runSingleAgent(
525
613
  },
526
614
  });
527
615
  } catch (error) {
616
+ currentResult.launchFailed = true;
528
617
  currentResult.stderr = setErrorMessage(
529
618
  error instanceof Error ? error.message : String(error),
530
619
  );
@@ -576,19 +665,62 @@ export async function runSingleAgent(
576
665
  if (msg.role === "assistant") {
577
666
  currentResult.usage.turns++;
578
667
  const usage = msg.usage;
579
- if (usage) {
580
- currentResult.usage.input += usage.input || 0;
581
- currentResult.usage.output += usage.output || 0;
582
- currentResult.usage.cacheRead += usage.cacheRead || 0;
583
- currentResult.usage.cacheWrite += usage.cacheWrite || 0;
584
- currentResult.usage.cost += usage.cost?.total || 0;
585
- currentResult.usage.contextTokens = usage.totalTokens || 0;
668
+ if (usage && typeof usage === "object") {
669
+ const input = protocolUsageCount(usage.input);
670
+ const output = protocolUsageCount(usage.output);
671
+ const cacheRead = protocolUsageCount(usage.cacheRead);
672
+ const cacheWrite = protocolUsageCount(usage.cacheWrite);
673
+ const reportedTotal = protocolUsageCount(usage.totalTokens);
674
+ const turnTotal =
675
+ reportedTotal ||
676
+ addUsageValue(addUsageValue(input, output), addUsageValue(cacheRead, cacheWrite));
677
+ const cost = usage.cost && typeof usage.cost === "object" ? usage.cost : undefined;
678
+ currentResult.usage.input = addUsageValue(currentResult.usage.input, input);
679
+ currentResult.usage.output = addUsageValue(currentResult.usage.output, output);
680
+ currentResult.usage.cacheRead = addUsageValue(
681
+ currentResult.usage.cacheRead,
682
+ cacheRead,
683
+ );
684
+ currentResult.usage.cacheWrite = addUsageValue(
685
+ currentResult.usage.cacheWrite,
686
+ cacheWrite,
687
+ );
688
+ currentResult.usage.cost = addUsageValue(
689
+ currentResult.usage.cost,
690
+ protocolUsageCost(cost?.total),
691
+ );
692
+ currentResult.usage.costInput = addUsageValue(
693
+ currentResult.usage.costInput ?? 0,
694
+ protocolUsageCost(cost?.input),
695
+ );
696
+ currentResult.usage.costOutput = addUsageValue(
697
+ currentResult.usage.costOutput ?? 0,
698
+ protocolUsageCost(cost?.output),
699
+ );
700
+ currentResult.usage.costCacheRead = addUsageValue(
701
+ currentResult.usage.costCacheRead ?? 0,
702
+ protocolUsageCost(cost?.cacheRead),
703
+ );
704
+ currentResult.usage.costCacheWrite = addUsageValue(
705
+ currentResult.usage.costCacheWrite ?? 0,
706
+ protocolUsageCost(cost?.cacheWrite),
707
+ );
708
+ currentResult.usage.totalTokens = addUsageValue(
709
+ currentResult.usage.totalTokens ?? 0,
710
+ turnTotal,
711
+ );
712
+ currentResult.usage.contextTokens = turnTotal;
586
713
  }
587
- if (msg.provider) currentResult.actualProvider = msg.provider;
588
- if (msg.responseModel ?? msg.model)
589
- currentResult.actualModel = msg.responseModel ?? msg.model;
590
- if (msg.stopReason) currentResult.stopReason = msg.stopReason;
591
- if (msg.errorMessage) setErrorMessage(msg.errorMessage);
714
+ if (typeof msg.provider === "string") currentResult.actualProvider = msg.provider;
715
+ const actualModel =
716
+ typeof msg.responseModel === "string"
717
+ ? msg.responseModel
718
+ : typeof msg.model === "string"
719
+ ? msg.model
720
+ : undefined;
721
+ if (actualModel) currentResult.actualModel = actualModel;
722
+ if (typeof msg.stopReason === "string") currentResult.stopReason = msg.stopReason;
723
+ if (typeof msg.errorMessage === "string") setErrorMessage(msg.errorMessage);
592
724
  }
593
725
  emitUpdate();
594
726
  } else if (event.type === "tool_result_end" && event.message) {
@@ -623,6 +755,9 @@ export async function runSingleAgent(
623
755
  }, timeoutMs);
624
756
  timeout.unref();
625
757
 
758
+ proc.once("spawn", () => {
759
+ currentResult.processStarted = true;
760
+ });
626
761
  proc.stdout?.on("data", (data) => decoder.push(data));
627
762
  proc.stderr?.on("data", (data) => {
628
763
  const bounded = appendBounded(
@@ -638,6 +773,7 @@ export async function runSingleAgent(
638
773
  finish(timedOut ? 124 : wasAborted ? 130 : (code ?? 0));
639
774
  });
640
775
  proc.on("error", (error) => {
776
+ currentResult.launchFailed = true;
641
777
  const message = setErrorMessage(error.message);
642
778
  const bounded = appendBounded(
643
779
  currentResult.stderr,
@@ -682,23 +818,27 @@ export async function runSingleAgent(
682
818
  "cwd",
683
819
  ...(agent.model ? ["model"] : []),
684
820
  ...(thinkingLevel ? ["thinkingLevel"] : []),
685
- ...(agent.tools ? ["tools"] : []),
821
+ ...(effectiveTools !== undefined ? ["tools"] : []),
822
+ ...(launchPolicy?.disableExtensions ? ["extensions"] : []),
823
+ ...(launchPolicy?.disableSkills ? ["skills"] : []),
824
+ ...(launchPolicy?.disablePromptTemplates ? ["promptTemplates"] : []),
825
+ ...(launchPolicy?.disableContextFiles ? ["contextFiles"] : []),
686
826
  ],
687
827
  unsupported: ["approvalPolicy", "sandboxProfile", "providerHeaders"],
688
828
  };
689
829
  return currentResult;
690
830
  } finally {
691
- if (tmpPromptPath)
831
+ for (const temporary of temporaryPrompts.reverse()) {
692
832
  try {
693
- fs.unlinkSync(tmpPromptPath);
833
+ fs.unlinkSync(temporary.filePath);
694
834
  } catch {
695
835
  /* ignore */
696
836
  }
697
- if (tmpPromptDir)
698
837
  try {
699
- fs.rmdirSync(tmpPromptDir);
838
+ fs.rmdirSync(temporary.dir);
700
839
  } catch {
701
840
  /* ignore */
702
841
  }
842
+ }
703
843
  }
704
844
  }
@@ -0,0 +1,67 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+ import { redactPrivateText } from "./context.js";
5
+ import { DEFAULT_MAX_OUTPUT_BYTES, TRUNCATION_MARKER, truncateUtf8 } from "./limits.js";
6
+
7
+ export const DEFAULT_MAX_OUTPUT_LINES = 2_000;
8
+
9
+ export function safeTerminalText(value: string): string {
10
+ return (
11
+ value
12
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls while preserving newlines.
13
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu, "?")
14
+ .replace(/\r/gu, "")
15
+ );
16
+ }
17
+
18
+ export function safeTerminalLine(value: string, maxBytes = 2 * 1024): string {
19
+ const singleLine = safeTerminalText(redactPrivateText(value)).replace(/\s+/gu, " ").trim();
20
+ return truncateUtf8(singleLine, maxBytes).text.replace(/\s+/gu, " ").trim();
21
+ }
22
+
23
+ export function boundText(
24
+ value: string,
25
+ maxBytes = DEFAULT_MAX_OUTPUT_BYTES,
26
+ maxLines = DEFAULT_MAX_OUTPUT_LINES,
27
+ ): { text: string; truncated: boolean } {
28
+ const safe = safeTerminalText(value);
29
+ const lines = safe.split("\n");
30
+ const lineBounded =
31
+ lines.length > maxLines
32
+ ? `${lines.slice(0, Math.max(0, maxLines - 1)).join("\n")}${TRUNCATION_MARKER}`
33
+ : safe;
34
+ const bounded = truncateUtf8(lineBounded, maxBytes);
35
+ return { text: bounded.text, truncated: lines.length > maxLines || bounded.truncated };
36
+ }
37
+
38
+ export function boundedPrivateText(value: string, maxBytes: number): string {
39
+ return boundText(redactPrivateText(value), maxBytes).text;
40
+ }
41
+
42
+ export function safeDisplayPath(value: string, workspace: string): string {
43
+ if (value.startsWith("built-in:")) return safeTerminalLine(value);
44
+ const resolved = path.resolve(value);
45
+ const agentDir = path.resolve(getAgentDir());
46
+ const relativeAgent = path.relative(agentDir, resolved);
47
+ if (
48
+ relativeAgent === "" ||
49
+ (!relativeAgent.startsWith("..") && !path.isAbsolute(relativeAgent))
50
+ ) {
51
+ return relativeAgent ? `~/${safeTerminalLine(relativeAgent)}` : "~";
52
+ }
53
+ const resolvedWorkspace = path.resolve(workspace);
54
+ const relativeWorkspace = path.relative(resolvedWorkspace, resolved);
55
+ if (
56
+ relativeWorkspace === "" ||
57
+ (!relativeWorkspace.startsWith("..") && !path.isAbsolute(relativeWorkspace))
58
+ ) {
59
+ return relativeWorkspace ? safeTerminalLine(relativeWorkspace) : ".";
60
+ }
61
+ const home = path.resolve(os.homedir());
62
+ const relativeHome = path.relative(home, resolved);
63
+ if (relativeHome === "" || (!relativeHome.startsWith("..") && !path.isAbsolute(relativeHome))) {
64
+ return relativeHome ? `~/${safeTerminalLine(relativeHome)}` : "~";
65
+ }
66
+ return safeTerminalLine(resolved);
67
+ }
package/src/settings.ts CHANGED
@@ -5,12 +5,19 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
5
  import lockfile from "proper-lockfile";
6
6
  import {
7
7
  type AgentConfig,
8
+ CONSULT_RESOURCE_POLICIES,
9
+ CONSULTATION_CWD_POLICIES,
8
10
  type CompletionDelivery,
11
+ type ConsultationCwdPolicy,
12
+ type ConsultResourcePolicy,
13
+ DELEGATION_CWD_POLICIES,
14
+ type DelegationCwdPolicy,
9
15
  isThinkingLevel,
10
16
  type SubagentAgentConfig,
11
17
  type SubagentSettings,
12
18
  type SubagentThinkingLevel,
13
19
  } from "./agents.js";
20
+ import { MAX_SUBAGENT_TIMEOUT_MS } from "./limits.js";
14
21
 
15
22
  export function hasOwn(obj: object, key: PropertyKey): boolean {
16
23
  return Object.hasOwn(obj, key);
@@ -61,7 +68,12 @@ export function normalizeAgentSettings(value: unknown): SubagentAgentConfig | un
61
68
  }
62
69
 
63
70
  if (hasOwn(value, "timeoutMs")) {
64
- if (value.timeoutMs !== null && !isPositiveNumber(value.timeoutMs)) return undefined;
71
+ if (
72
+ value.timeoutMs !== null &&
73
+ (!isPositiveNumber(value.timeoutMs) || value.timeoutMs > MAX_SUBAGENT_TIMEOUT_MS)
74
+ ) {
75
+ return undefined;
76
+ }
65
77
  config.timeoutMs = value.timeoutMs;
66
78
  hasKnownField = true;
67
79
  }
@@ -136,12 +148,52 @@ export function normalizeSubagentSettings(value: unknown): SubagentSettings | un
136
148
  }
137
149
  settings.stateful = runtime;
138
150
  }
151
+ if (hasOwn(value, "consult")) {
152
+ if (!isPlainObject(value.consult)) return undefined;
153
+ const consult: NonNullable<SubagentSettings["consult"]> = {};
154
+ if (hasOwn(value.consult, "resources")) {
155
+ if (
156
+ typeof value.consult.resources !== "string" ||
157
+ !CONSULT_RESOURCE_POLICIES.includes(value.consult.resources as ConsultResourcePolicy)
158
+ ) {
159
+ return undefined;
160
+ }
161
+ consult.resources = value.consult.resources as ConsultResourcePolicy;
162
+ }
163
+ settings.consult = consult;
164
+ }
165
+ if (hasOwn(value, "cwdPolicy")) {
166
+ if (!isPlainObject(value.cwdPolicy)) return undefined;
167
+ const cwdPolicy: NonNullable<SubagentSettings["cwdPolicy"]> = {};
168
+ if (hasOwn(value.cwdPolicy, "consultation")) {
169
+ if (
170
+ typeof value.cwdPolicy.consultation !== "string" ||
171
+ !CONSULTATION_CWD_POLICIES.includes(value.cwdPolicy.consultation as ConsultationCwdPolicy)
172
+ ) {
173
+ return undefined;
174
+ }
175
+ cwdPolicy.consultation = value.cwdPolicy.consultation as ConsultationCwdPolicy;
176
+ }
177
+ if (hasOwn(value.cwdPolicy, "delegation")) {
178
+ if (
179
+ typeof value.cwdPolicy.delegation !== "string" ||
180
+ !DELEGATION_CWD_POLICIES.includes(value.cwdPolicy.delegation as DelegationCwdPolicy)
181
+ ) {
182
+ return undefined;
183
+ }
184
+ cwdPolicy.delegation = value.cwdPolicy.delegation as DelegationCwdPolicy;
185
+ }
186
+ settings.cwdPolicy = cwdPolicy;
187
+ }
139
188
  return settings;
140
189
  }
141
190
 
142
191
  const SETTINGS_FILE = "pi-subagents.json";
143
192
  const LEGACY_SETTINGS_FILE = "pi-subagents-config.json";
144
193
  const DEFAULT_COMPLETION_DELIVERY: CompletionDelivery = "next-turn";
194
+ export const DEFAULT_CONSULT_RESOURCE_POLICY: ConsultResourcePolicy = "project-context";
195
+ export const DEFAULT_CONSULTATION_CWD_POLICY: ConsultationCwdPolicy = "anywhere";
196
+ export const DEFAULT_DELEGATION_CWD_POLICY: DelegationCwdPolicy = "trusted-targets";
145
197
  const SETTINGS_LOCK_FS_ADAPTER = {
146
198
  mkdir: fs.mkdir,
147
199
  mkdirSync: fs.mkdirSync,
@@ -231,6 +283,32 @@ export interface CompletionDeliverySettingsSnapshot {
231
283
  error?: string;
232
284
  }
233
285
 
286
+ export interface ConsultResourceSettingsSnapshot {
287
+ path: string;
288
+ value: ConsultResourcePolicy;
289
+ source: "default" | "user settings";
290
+ error?: string;
291
+ }
292
+
293
+ export interface CwdPolicyFieldSnapshot<T> {
294
+ value: T;
295
+ source: "default" | "user settings";
296
+ }
297
+
298
+ export interface CwdPolicySettingsSnapshot {
299
+ path: string;
300
+ consultation: CwdPolicyFieldSnapshot<ConsultationCwdPolicy>;
301
+ delegation: CwdPolicyFieldSnapshot<DelegationCwdPolicy>;
302
+ error?: string;
303
+ }
304
+
305
+ export interface SubagentSettingsSnapshot {
306
+ path: string;
307
+ settings?: SubagentSettings;
308
+ source: "default" | "user settings";
309
+ error?: string;
310
+ }
311
+
234
312
  export function subagentSettingsFilePath(): string {
235
313
  return path.join(getAgentDir(), SETTINGS_FILE);
236
314
  }
@@ -265,16 +343,81 @@ function inspectSubagentSettingsPath(configPath: string): {
265
343
  settings?: SubagentSettings;
266
344
  error?: string;
267
345
  } {
346
+ const fileName = path.basename(configPath);
347
+ let contents: string;
268
348
  try {
269
- const raw: unknown = JSON.parse(fs.readFileSync(configPath, "utf8"));
270
- const settings = normalizeSubagentSettings(raw);
271
- if (!isPlainObject(raw) || !settings) {
272
- throw new Error(`${path.basename(configPath)} is not a valid settings object`);
273
- }
274
- return { path: configPath, raw, settings };
349
+ contents = fs.readFileSync(configPath, "utf8");
275
350
  } catch (error) {
276
- return { path: configPath, error: formatError(error) };
351
+ const code = (error as NodeJS.ErrnoException).code;
352
+ return {
353
+ path: configPath,
354
+ error: `${fileName} could not be read${code ? ` (${safeErrorCode(code)})` : ""}`,
355
+ };
356
+ }
357
+ let raw: unknown;
358
+ try {
359
+ raw = JSON.parse(contents);
360
+ } catch {
361
+ return { path: configPath, error: `${fileName} contains malformed JSON` };
362
+ }
363
+ const settings = normalizeSubagentSettings(raw);
364
+ if (!isPlainObject(raw) || !settings) {
365
+ return { path: configPath, error: `${fileName} is not a valid settings object` };
366
+ }
367
+ return { path: configPath, raw, settings };
368
+ }
369
+
370
+ export function inspectSubagentSettings(): SubagentSettingsSnapshot {
371
+ const inspected = inspectSubagentSettingsDocument();
372
+ return {
373
+ path: inspected.path,
374
+ settings: inspected.settings,
375
+ source: inspected.settings ? "user settings" : "default",
376
+ ...(inspected.error ? { error: inspected.error } : {}),
377
+ };
378
+ }
379
+
380
+ export function inspectConsultResourceSettings(): ConsultResourceSettingsSnapshot {
381
+ const inspected = inspectSubagentSettingsDocument();
382
+ if (!inspected.raw || !inspected.settings) {
383
+ return {
384
+ path: inspected.path,
385
+ value: DEFAULT_CONSULT_RESOURCE_POLICY,
386
+ source: "default",
387
+ ...(inspected.error ? { error: inspected.error } : {}),
388
+ };
277
389
  }
390
+ const explicit =
391
+ isPlainObject(inspected.raw.consult) && hasOwn(inspected.raw.consult, "resources");
392
+ return {
393
+ path: inspected.path,
394
+ value: inspected.settings.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY,
395
+ source: explicit ? "user settings" : "default",
396
+ };
397
+ }
398
+
399
+ export function inspectCwdPolicySettings(): CwdPolicySettingsSnapshot {
400
+ const inspected = inspectSubagentSettingsDocument();
401
+ if (!inspected.raw || !inspected.settings) {
402
+ return {
403
+ path: inspected.path,
404
+ consultation: { value: DEFAULT_CONSULTATION_CWD_POLICY, source: "default" },
405
+ delegation: { value: DEFAULT_DELEGATION_CWD_POLICY, source: "default" },
406
+ ...(inspected.error ? { error: inspected.error } : {}),
407
+ };
408
+ }
409
+ const rawPolicy = isPlainObject(inspected.raw.cwdPolicy) ? inspected.raw.cwdPolicy : undefined;
410
+ return {
411
+ path: inspected.path,
412
+ consultation: {
413
+ value: inspected.settings.cwdPolicy?.consultation ?? DEFAULT_CONSULTATION_CWD_POLICY,
414
+ source: rawPolicy && hasOwn(rawPolicy, "consultation") ? "user settings" : "default",
415
+ },
416
+ delegation: {
417
+ value: inspected.settings.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY,
418
+ source: rawPolicy && hasOwn(rawPolicy, "delegation") ? "user settings" : "default",
419
+ },
420
+ };
278
421
  }
279
422
 
280
423
  export function inspectDelegationWorkflowSettings(): DelegationWorkflowSettingsSnapshot {
@@ -371,6 +514,50 @@ export function updateCompletionDeliverySetting(value: CompletionDelivery): void
371
514
  });
372
515
  }
373
516
 
517
+ export function updateConsultResourceSetting(value: ConsultResourcePolicy): void {
518
+ withSettingsMutationLock(() => {
519
+ const update = readSettingsObjectForUpdate();
520
+ const raw = update.document;
521
+ const consult = raw.consult;
522
+ if (consult !== undefined && !isPlainObject(consult)) {
523
+ throw new Error(`Cannot update invalid ${SETTINGS_FILE} consult settings`);
524
+ }
525
+ writeSettingsObjectUnlocked(
526
+ {
527
+ ...raw,
528
+ consult: {
529
+ ...(consult ?? {}),
530
+ resources: value,
531
+ },
532
+ },
533
+ update.replaceCanonical,
534
+ );
535
+ });
536
+ }
537
+
538
+ export function updateCwdPolicySetting(field: "consultation", value: ConsultationCwdPolicy): void;
539
+ export function updateCwdPolicySetting(field: "delegation", value: DelegationCwdPolicy): void;
540
+ export function updateCwdPolicySetting(
541
+ field: "consultation" | "delegation",
542
+ value: ConsultationCwdPolicy | DelegationCwdPolicy,
543
+ ): void {
544
+ withSettingsMutationLock(() => {
545
+ const update = readSettingsObjectForUpdate();
546
+ const raw = update.document;
547
+ const cwdPolicy = raw.cwdPolicy;
548
+ if (cwdPolicy !== undefined && !isPlainObject(cwdPolicy)) {
549
+ throw new Error(`Cannot update invalid ${SETTINGS_FILE} cwdPolicy settings`);
550
+ }
551
+ writeSettingsObjectUnlocked(
552
+ {
553
+ ...raw,
554
+ cwdPolicy: { ...(cwdPolicy ?? {}), [field]: value },
555
+ },
556
+ update.replaceCanonical,
557
+ );
558
+ });
559
+ }
560
+
374
561
  export function updateAgentToolsSetting(name: string, tools: string[] | undefined): void {
375
562
  withSettingsMutationLock(() => {
376
563
  const update = readSettingsObjectForUpdate();
@@ -417,8 +604,8 @@ function readSettingsObjectForUpdate(): SettingsObjectForUpdate {
417
604
  let parsed: unknown;
418
605
  try {
419
606
  parsed = JSON.parse(fs.readFileSync(activePath, "utf8"));
420
- } catch (error) {
421
- throw new Error(`Cannot update malformed ${activeFile}: ${formatError(error)}`);
607
+ } catch {
608
+ throw new Error(`Cannot update malformed ${activeFile}`);
422
609
  }
423
610
  if (!isPlainObject(parsed) || !normalizeSubagentSettings(parsed)) {
424
611
  throw new Error(`Cannot update invalid ${activeFile}`);
@@ -498,8 +685,8 @@ function readSettingsSnapshot(configPath: string): {
498
685
  }
499
686
  }
500
687
 
501
- function formatError(error: unknown) {
502
- return error instanceof Error ? error.message : String(error);
688
+ function safeErrorCode(value: string): string {
689
+ return value.replace(/[^A-Z0-9_-]/giu, "?").slice(0, 64);
503
690
  }
504
691
 
505
692
  export function uniqueToolNames(tools: string[]): string[] {