@mrclrchtr/supi-consulting 11.1.0 → 11.3.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.
@@ -3,7 +3,8 @@ const FAILURE_MESSAGES = {
3
3
  timeout: "The Consultation timed out.",
4
4
  cancelled: "The Consultation was canceled.",
5
5
  "invalid-response": "The Consulting Agent returned an invalid response.",
6
- "invalid-continuation": "Invalid Conversation Handle continuation state.",
6
+ "invalid-continuation":
7
+ "The Conversation Handle has invalid or incompatible continuation state. Start a new Consultation.",
7
8
  "conversation-mismatch":
8
9
  "The Consulting Agent returned a different conversation for this handle.",
9
10
  execution: "Consultation execution failed.",
@@ -15,6 +16,8 @@ const FAILURE_REASON_MESSAGES = {
15
16
  "missing-answer": "The Consulting Agent did not return a final structured answer.",
16
17
  "invalid-answer":
17
18
  "The Consulting Agent returned an answer that does not match the required schema.",
19
+ "unbound-answer":
20
+ "The Consulting Agent's structured answer could not be verified for the current request. Start a new Consultation.",
18
21
  "missing-conversation": "The Consulting Agent did not return a conversation identifier.",
19
22
  "events-after-terminal": "The Consulting Agent sent data after its final result.",
20
23
  "malformed-json": "The Consulting Agent returned malformed JSON.",
@@ -27,9 +30,49 @@ const FAILURE_REASON_MESSAGES = {
27
30
  /** Fixed failure reasons that do not contain agent output. */
28
31
  export type ConsultingFailureReason = keyof typeof FAILURE_REASON_MESSAGES;
29
32
 
30
- /** Execution facts that can leave the adapter on failure. No resource names or raw output. */
33
+ const DENIED_OPERATIONS = [
34
+ "command",
35
+ "file-read",
36
+ "file-write",
37
+ "web-read",
38
+ "browser",
39
+ "mcp",
40
+ "other",
41
+ ] as const;
42
+
43
+ /** Fixed operation categories; raw agent tool names never leave the adapter on failure. */
44
+ export type ConsultingDeniedOperation = (typeof DENIED_OPERATIONS)[number];
45
+
46
+ /** Maximum distinct requested file targets in each failure resource list. */
47
+ export const MAX_FAILURE_RESOURCES = 8;
48
+ /** Longer paths are unavailable, never silently truncated into a different target. */
49
+ export const MAX_FAILURE_RESOURCE_PATH_CHARS = 1_024;
50
+
51
+ /** Structured requested file target, not a canonical path, rule decision, or outcome. */
52
+ export interface ConsultingFileResource {
53
+ operation: "file-read" | "file-write";
54
+ requestedPath: string;
55
+ }
56
+
57
+ /** Failure facts plus bounded requested file targets; never raw tool payloads or output. */
31
58
  export interface ConsultingFailureDiagnostics {
32
59
  permissionDenials?: number;
60
+ /** Deduplicated denial counts, classified into fixed operation categories. */
61
+ permissionDenialsByOperation?: Partial<Record<ConsultingDeniedOperation, number>>;
62
+ /** Denied file operations targeting outside approved roots; not a claim about denial cause. */
63
+ deniedFileAccessOutsideReadRoots?: number;
64
+ /** Distinct requested file targets extracted from correlated structured tool parameters. */
65
+ deniedResources?: readonly ConsultingFileResource[];
66
+ /** File-denial observations whose known targets were omitted because the list is full. */
67
+ deniedResourcesOmitted?: number;
68
+ /** Known file-denial observations without an available, bounded structured target. */
69
+ deniedResourcePathsUnavailable?: number;
70
+ /** Requested file targets of calls without a terminal outcome; not attributed to denials. */
71
+ unfinishedResources?: readonly ConsultingFileResource[];
72
+ /** Unfinished file observations omitted because their target list is full. */
73
+ unfinishedResourcesOmitted?: number;
74
+ /** Unfinished file observations lacking a usable structured target. */
75
+ unfinishedResourcePathsUnavailable?: number;
33
76
  eventCount?: number;
34
77
  terminalSeen?: boolean;
35
78
  terminalSucceeded?: boolean;
@@ -40,7 +83,7 @@ export interface ConsultingFailureDiagnostics {
40
83
  export type ConsultingAgentFailureKind = keyof typeof FAILURE_MESSAGES;
41
84
 
42
85
  /**
43
- * A bounded failure with fixed messages and safe execution facts, but no raw cause.
86
+ * A bounded failure with fixed reason text and extracted execution metadata, but no raw cause.
44
87
  * Permission denials are observations, not a claim about the cause of failure.
45
88
  */
46
89
  export class ConsultingAgentError extends Error {
@@ -55,22 +98,160 @@ export class ConsultingAgentError extends Error {
55
98
  const message = options.reason
56
99
  ? FAILURE_REASON_MESSAGES[options.reason]
57
100
  : FAILURE_MESSAGES[kind];
58
- const denial = (options.diagnostics?.permissionDenials ?? 0) > 0;
59
- super(
60
- `${message}${denial ? " A tool permission denial was observed. Check the requested resources and the Consulting Agent permissions." : ""}`,
61
- );
62
- this.name = "ConsultingAgentError";
63
- this.kind = kind;
64
- this.reason = options.reason;
65
101
  const facts = options.diagnostics;
66
- this.diagnostics = facts
102
+ const diagnostics: ConsultingFailureDiagnostics | undefined = facts
67
103
  ? {
68
104
  permissionDenials: facts.permissionDenials,
105
+ ...(facts.permissionDenialsByOperation
106
+ ? {
107
+ permissionDenialsByOperation: sanitizeOperationCounts(
108
+ facts.permissionDenialsByOperation,
109
+ ),
110
+ }
111
+ : {}),
112
+ ...(facts.deniedFileAccessOutsideReadRoots === undefined
113
+ ? {}
114
+ : { deniedFileAccessOutsideReadRoots: facts.deniedFileAccessOutsideReadRoots }),
115
+ ...(facts.deniedResources
116
+ ? { deniedResources: sanitizeFileResources(facts.deniedResources) }
117
+ : {}),
118
+ deniedResourcesOmitted: facts.deniedResourcesOmitted,
119
+ deniedResourcePathsUnavailable: facts.deniedResourcePathsUnavailable,
120
+ ...(facts.unfinishedResources
121
+ ? { unfinishedResources: sanitizeFileResources(facts.unfinishedResources) }
122
+ : {}),
123
+ unfinishedResourcesOmitted: facts.unfinishedResourcesOmitted,
124
+ unfinishedResourcePathsUnavailable: facts.unfinishedResourcePathsUnavailable,
69
125
  eventCount: facts.eventCount,
70
126
  terminalSeen: facts.terminalSeen,
71
127
  terminalSucceeded: facts.terminalSucceeded,
72
128
  exitCode: facts.exitCode,
73
129
  }
74
130
  : undefined;
131
+ super(`${message}${formatFailureContext(diagnostics)}`);
132
+ this.name = "ConsultingAgentError";
133
+ this.kind = kind;
134
+ this.reason = options.reason;
135
+ this.diagnostics = diagnostics;
136
+ }
137
+ }
138
+
139
+ function sanitizeFileResources(
140
+ resources: readonly ConsultingFileResource[],
141
+ ): ConsultingFileResource[] {
142
+ return resources
143
+ .slice(0, MAX_FAILURE_RESOURCES)
144
+ .flatMap((resource) =>
145
+ (resource.operation === "file-read" || resource.operation === "file-write") &&
146
+ typeof resource.requestedPath === "string" &&
147
+ resource.requestedPath.length > 0 &&
148
+ resource.requestedPath.length <= MAX_FAILURE_RESOURCE_PATH_CHARS
149
+ ? [{ operation: resource.operation, requestedPath: resource.requestedPath }]
150
+ : [],
151
+ );
152
+ }
153
+
154
+ function sanitizeOperationCounts(
155
+ counts: Partial<Record<ConsultingDeniedOperation, number>>,
156
+ ): Partial<Record<ConsultingDeniedOperation, number>> {
157
+ const result: Partial<Record<ConsultingDeniedOperation, number>> = {};
158
+ for (const operation of DENIED_OPERATIONS) {
159
+ const count = counts[operation];
160
+ if (typeof count === "number" && Number.isSafeInteger(count) && count > 0) {
161
+ result[operation] = count;
162
+ }
163
+ }
164
+ return result;
165
+ }
166
+
167
+ function formatFailureContext(facts: ConsultingFailureDiagnostics | undefined): string {
168
+ if (!facts) return "";
169
+ const parts: string[] = [];
170
+ if (facts.terminalSeen !== undefined) {
171
+ const status = facts.terminalSeen
172
+ ? facts.terminalSucceeded
173
+ ? "success"
174
+ : "unsuccessful"
175
+ : "not observed";
176
+ parts.push(`Agent terminal status: ${status}.`);
177
+ }
178
+ const denial = formatPermissionDenials(facts);
179
+ if (denial) parts.push(denial);
180
+ const unfinished = formatUnfinishedResources(facts);
181
+ if (unfinished) parts.push(unfinished);
182
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
183
+ }
184
+
185
+ function formatPermissionDenials(facts: ConsultingFailureDiagnostics): string {
186
+ if ((facts.permissionDenials ?? 0) === 0) return "";
187
+ const parts = [`Observed permission denials: ${facts.permissionDenials}.`];
188
+ const counts = Object.entries(facts.permissionDenialsByOperation ?? {});
189
+ if (counts.length > 0) {
190
+ parts.push(
191
+ `Permission denials by operation: ${counts.map(([operation, count]) => `${operation}=${count}`).join(", ")}.`,
192
+ );
193
+ }
194
+ if ((facts.deniedFileAccessOutsideReadRoots ?? 0) > 0) {
195
+ parts.push(
196
+ `Denied file access outside approved read roots: ${facts.deniedFileAccessOutsideReadRoots}.`,
197
+ );
198
+ }
199
+ const resources = formatDeniedResources(facts);
200
+ if (resources) parts.push(resources);
201
+ parts.push("Permission denials are observations, not proof of the failure cause.");
202
+ if (facts.permissionDenialsByOperation?.command) {
203
+ parts.push("Workspace access does not grant command execution.");
204
+ }
205
+ parts.push(
206
+ "Do not retry denied actions unchanged. Use permitted inspection tools and artifacts in approved read roots, or request a limited/blocked report.",
207
+ );
208
+ return parts.join(" ");
209
+ }
210
+
211
+ function quoteFileResources(resources: readonly ConsultingFileResource[]): string {
212
+ return JSON.stringify(resources).replace(
213
+ /[\u007f-\u009f\u2028\u2029]/g,
214
+ (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
215
+ );
216
+ }
217
+
218
+ function formatUnfinishedResources(facts: ConsultingFailureDiagnostics): string {
219
+ const parts: string[] = [];
220
+ if (facts.unfinishedResources?.length) {
221
+ parts.push(
222
+ `Requested file targets of unfinished actions: ${quoteFileResources(facts.unfinishedResources)}.`,
223
+ );
224
+ }
225
+ if ((facts.unfinishedResourcesOmitted ?? 0) > 0) {
226
+ parts.push(
227
+ `Additional unfinished file observations omitted: ${facts.unfinishedResourcesOmitted}.`,
228
+ );
229
+ }
230
+ if ((facts.unfinishedResourcePathsUnavailable ?? 0) > 0) {
231
+ parts.push(
232
+ `Unfinished file observations with unavailable structured targets: ${facts.unfinishedResourcePathsUnavailable}.`,
233
+ );
234
+ }
235
+ if (parts.length)
236
+ parts.push("Unfinished file operations have unknown outcomes; they are not confirmed denials.");
237
+ return parts.join(" ");
238
+ }
239
+
240
+ function formatDeniedResources(facts: ConsultingFailureDiagnostics): string {
241
+ const parts: string[] = [];
242
+ if (facts.deniedResources?.length) {
243
+ const targets = quoteFileResources(facts.deniedResources);
244
+ parts.push(`Requested file targets of denied actions: ${targets}.`);
245
+ }
246
+ if ((facts.deniedResourcesOmitted ?? 0) > 0) {
247
+ parts.push(
248
+ `Additional file-denial observations omitted from target list: ${facts.deniedResourcesOmitted}.`,
249
+ );
250
+ }
251
+ if ((facts.deniedResourcePathsUnavailable ?? 0) > 0) {
252
+ parts.push(
253
+ `File-denial observations with unavailable structured targets: ${facts.deniedResourcePathsUnavailable}.`,
254
+ );
75
255
  }
256
+ return parts.join(" ");
76
257
  }
@@ -25,6 +25,8 @@ export interface ConsultingAgentExecutionOptions {
25
25
  model: ConsultingModel;
26
26
  canonicalWorkingDirectory: string;
27
27
  workspaceAccess: boolean;
28
+ /** Canonical extra directory roots for inspection; writes and commands remain denied. */
29
+ additionalReadOnlyDirectories?: readonly string[];
28
30
  continuation?: string;
29
31
  signal?: AbortSignal;
30
32
  onProgress?: (activity: string) => void;
package/src/catalogue.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { StringEnum } from "@earendil-works/pi-ai";
2
- import type { TSchema } from "typebox";
1
+ import { type TSchema, Type } from "typebox";
3
2
 
4
3
  /** Build a provider-facing model enum from one immutable agent catalogue. */
5
4
  export function buildModelCatalogueEnum(catalogue: readonly string[]): TSchema {
6
5
  if (catalogue.length === 0) {
7
6
  throw new Error("Cannot build a model enum from an empty Consulting Agent catalogue.");
8
7
  }
9
- return StringEnum([...catalogue] as [string, ...string[]], {
8
+ return Type.Enum([...catalogue] as [string, ...string[]], {
9
+ type: "string",
10
10
  description: "Model available in the current Consulting Agent catalogue.",
11
11
  });
12
12
  }
package/src/config.ts CHANGED
@@ -1,4 +1,7 @@
1
- import { loadSupiConfig } from "@mrclrchtr/supi-core/config";
1
+ import { constants } from "node:fs";
2
+ import { access, realpath, stat } from "node:fs/promises";
3
+ import { isAbsolute } from "node:path";
4
+ import { loadSupiConfig, loadSupiConfigForScope } from "@mrclrchtr/supi-core/config";
2
5
 
3
6
  /** The persisted SuPi configuration section owned by this package. */
4
7
  export const CONSULTING_CONFIG_SECTION = "consulting";
@@ -7,20 +10,73 @@ export const CONSULTING_CONFIG_SECTION = "consulting";
7
10
  export interface ConsultingConfig extends Record<string, unknown> {
8
11
  /** Enable agent availability discovery and the consulting_run tool. */
9
12
  agentToolEnabled: boolean;
13
+ /** Absolute directories available for inspection with either Workspace Access selection. */
14
+ additionalReadOnlyDirectories: readonly string[];
10
15
  }
11
16
 
12
17
  /** Default Consulting configuration. */
13
18
  export const CONSULTING_DEFAULTS: ConsultingConfig = Object.freeze({
14
19
  agentToolEnabled: true,
20
+ additionalReadOnlyDirectories: Object.freeze([]),
15
21
  });
16
22
 
17
- /** Load only the new consulting configuration section for a workspace. */
18
- export function loadConsultingConfig(cwd: string, homeDir?: string): ConsultingConfig {
19
- const raw = loadSupiConfig(CONSULTING_CONFIG_SECTION, cwd, CONSULTING_DEFAULTS, { homeDir });
23
+ /** Load trusted scoped configuration; untrusted projects cannot grant external directory access. */
24
+ export function loadConsultingConfig(
25
+ cwd: string,
26
+ homeDir?: string,
27
+ projectTrusted = true,
28
+ ): ConsultingConfig {
29
+ const raw = projectTrusted
30
+ ? loadSupiConfig(CONSULTING_CONFIG_SECTION, cwd, CONSULTING_DEFAULTS, { homeDir })
31
+ : loadSupiConfigForScope(CONSULTING_CONFIG_SECTION, cwd, CONSULTING_DEFAULTS, {
32
+ scope: "global",
33
+ homeDir,
34
+ });
20
35
  return {
21
36
  agentToolEnabled:
22
37
  typeof raw.agentToolEnabled === "boolean"
23
38
  ? raw.agentToolEnabled
24
39
  : CONSULTING_DEFAULTS.agentToolEnabled,
40
+ additionalReadOnlyDirectories: validateAdditionalReadOnlyDirectories(
41
+ raw.additionalReadOnlyDirectories,
42
+ ),
25
43
  };
26
44
  }
45
+
46
+ /** Validate directory-list syntax before persistence or execution; paths are literal, not globs. */
47
+ export function validateAdditionalReadOnlyDirectories(value: unknown): string[] {
48
+ if (
49
+ !Array.isArray(value) ||
50
+ value.some((path) => typeof path !== "string" || !isAbsolute(path) || path.includes("\0"))
51
+ ) {
52
+ throw new Error(
53
+ "consulting.additionalReadOnlyDirectories must be a list of absolute directory paths.",
54
+ );
55
+ }
56
+ return [...new Set(value)];
57
+ }
58
+
59
+ /** Resolve current scoped grants for each run, following directory aliases and failing before process work. */
60
+ export async function resolveAdditionalReadOnlyDirectories(
61
+ cwd: string,
62
+ homeDir?: string,
63
+ projectTrusted = true,
64
+ ): Promise<string[]> {
65
+ const config = loadConsultingConfig(cwd, homeDir, projectTrusted);
66
+ const directories = await Promise.all(
67
+ config.additionalReadOnlyDirectories.map(async (path) => {
68
+ try {
69
+ const canonical = await realpath(path);
70
+ if (!(await stat(canonical)).isDirectory()) throw new Error("Not a directory.");
71
+ await access(canonical, constants.R_OK | constants.X_OK);
72
+ return canonical;
73
+ } catch (error) {
74
+ throw new Error(
75
+ `Consulting additional read-only directory is missing, inaccessible, or not a directory: ${path}`,
76
+ { cause: error },
77
+ );
78
+ }
79
+ }),
80
+ );
81
+ return [...new Set(directories)];
82
+ }
package/src/runtime.ts CHANGED
@@ -17,6 +17,7 @@ type FooterState = "idle" | "checking" | "ready";
17
17
  /** Context needed to report availability and update the footer. */
18
18
  export type ConsultingRefreshContext = {
19
19
  ui: Pick<ExtensionContext["ui"], "notify" | "setStatus">;
20
+ isProjectTrusted?: ExtensionContext["isProjectTrusted"];
20
21
  };
21
22
 
22
23
  /** Session runtime for immutable availability and Conversation Handle state. */
@@ -85,7 +86,7 @@ export class ConsultingRuntime {
85
86
  if (context) this.#statusUi = context.ui;
86
87
  this.#stopSpinner();
87
88
  this.#setFooterState("idle");
88
- const config = loadConsultingConfig(cwd, this.#homeDir);
89
+ const config = loadConsultingConfig(cwd, this.#homeDir, context?.isProjectTrusted?.() ?? true);
89
90
  if (!config.agentToolEnabled) {
90
91
  this.#deactivateTool();
91
92
  return;
@@ -141,6 +142,7 @@ export class ConsultingRuntime {
141
142
  adapter: this.adapter,
142
143
  availability,
143
144
  handles: this.handles,
145
+ homeDir: this.#homeDir,
144
146
  });
145
147
  this.#toolRegistered = true;
146
148
  }
package/src/settings.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { defineConfigSettings, registerSettings } from "@mrclrchtr/supi-core/settings";
3
- import { CONSULTING_CONFIG_SECTION, CONSULTING_DEFAULTS } from "./config.ts";
3
+ import {
4
+ CONSULTING_CONFIG_SECTION,
5
+ CONSULTING_DEFAULTS,
6
+ validateAdditionalReadOnlyDirectories,
7
+ } from "./config.ts";
4
8
  import type { ConsultingRuntime } from "./runtime.ts";
5
9
 
6
10
  /** Register the Consulting setting with an awaited availability refresh. */
@@ -21,6 +25,13 @@ export function registerConsultingSettings(
21
25
  label: "Consultation tool",
22
26
  description: "Enable consulting_run and its availability checks.",
23
27
  },
28
+ {
29
+ kind: "stringList" as const,
30
+ key: "additionalReadOnlyDirectories",
31
+ label: "Additional read-only directories",
32
+ description:
33
+ "Comma-separated absolute directories for every Consultation, including without Workspace Access. No shell expansion or globs; broad paths can expose sensitive files.",
34
+ },
24
35
  ],
25
36
  ...(homeDir ? { homeDir } : {}),
26
37
  });
@@ -28,9 +39,17 @@ export function registerConsultingSettings(
28
39
  registerSettings(pi, {
29
40
  ...fixedSettings,
30
41
  apply: async (request) => {
42
+ if (request.fieldKey === "additionalReadOnlyDirectories" && request.action.kind === "set") {
43
+ validateAdditionalReadOnlyDirectories(
44
+ request.action.value
45
+ .split(",")
46
+ .map((path) => path.trim())
47
+ .filter(Boolean),
48
+ );
49
+ }
31
50
  const result = await fixedSettings.apply(request);
32
51
  if (request.fieldKey === "agentToolEnabled") {
33
- await runtime.refresh(request.cwd, request.ctx ? { ui: request.ctx.ui } : undefined);
52
+ await runtime.refresh(request.cwd, request.ctx);
34
53
  }
35
54
  return result;
36
55
  },
@@ -4,7 +4,9 @@ import type {
4
4
  ExtensionAPI,
5
5
  ExtensionContext,
6
6
  } from "@earendil-works/pi-coding-agent";
7
+ import { ConsultingAgentError } from "../../agents/errors.ts";
7
8
  import type { ConsultingAgentAdapter, ConsultingAgentAvailability } from "../../agents/types.ts";
9
+ import { resolveAdditionalReadOnlyDirectories } from "../../config.ts";
8
10
  import {
9
11
  appendHandleCreated,
10
12
  appendHandleRetired,
@@ -19,6 +21,8 @@ export interface ConsultingRunDependencies {
19
21
  adapter: ConsultingAgentAdapter;
20
22
  availability: ConsultingAgentAvailability;
21
23
  handles: ConversationHandleStore;
24
+ /** Override the user home for scoped configuration loading. */
25
+ homeDir?: string;
22
26
  }
23
27
 
24
28
  type ConsultingExecute = NonNullable<Parameters<ExtensionAPI["registerTool"]>[0]["execute"]>;
@@ -84,6 +88,11 @@ async function runNew(options: NewRunOptions): Promise<ReturnType<typeof buildCo
84
88
  model: input.new.model,
85
89
  canonicalWorkingDirectory: cwd,
86
90
  workspaceAccess,
91
+ additionalReadOnlyDirectories: await resolveAdditionalReadOnlyDirectories(
92
+ ctx.cwd,
93
+ dependencies.homeDir,
94
+ ctx.isProjectTrusted(),
95
+ ),
87
96
  signal,
88
97
  onProgress: progress,
89
98
  onWarning: (warning) => {
@@ -136,6 +145,11 @@ async function runContinue(
136
145
  model: record.model,
137
146
  canonicalWorkingDirectory: record.canonicalWorkingDirectory,
138
147
  workspaceAccess: record.workspaceAccess,
148
+ additionalReadOnlyDirectories: await resolveAdditionalReadOnlyDirectories(
149
+ ctx.cwd,
150
+ dependencies.homeDir,
151
+ ctx.isProjectTrusted(),
152
+ ),
139
153
  continuation: record.continuation,
140
154
  signal,
141
155
  onProgress: progress,
@@ -154,9 +168,19 @@ async function runContinue(
154
168
  ...(hookWarning ? { hookWarning } : {}),
155
169
  });
156
170
  } catch (error) {
157
- if (processStarted) {
171
+ // Incompatible continuation state can never succeed, so retire it even though
172
+ // the adapter rejected it before any process work.
173
+ const incompatible =
174
+ error instanceof ConsultingAgentError && error.kind === "invalid-continuation";
175
+ if (processStarted || incompatible) {
158
176
  const retired = dependencies.handles.retire(record.handle);
159
- if (retired) appendHandleRetired(dependencies.pi, retired, "Consultation follow-up failed");
177
+ if (retired) {
178
+ appendHandleRetired(
179
+ dependencies.pi,
180
+ retired,
181
+ incompatible ? "Incompatible continuation state" : "Consultation follow-up failed",
182
+ );
183
+ }
160
184
  }
161
185
  throw error;
162
186
  } finally {
@@ -1,3 +1,3 @@
1
1
  /** Selection guidance for the dynamically activated consulting_run tool. */
2
2
  export const toolDescription =
3
- "Consult a supported external agent for focused web research, design advice, workspace analysis, or an independent second opinion. Use direct tools for repository facts that do not need external analysis. Expose the current workspace only when the Consultation needs repository evidence.";
3
+ "Consult an external agent for web research, design advice, workspace analysis, or an independent second opinion. Use direct tools for repository facts. Expose the current workspace only when the Consultation needs repository evidence. Configured extra read-only directories apply to either workspace selection.";
@@ -1,4 +1,3 @@
1
- import { StringEnum } from "@earendil-works/pi-ai";
2
1
  import { type TSchema, Type } from "typebox";
3
2
  import { Value } from "typebox/value";
4
3
  import { buildModelCatalogueEnum, isAvailableModel } from "../../catalogue.ts";
@@ -40,7 +39,8 @@ export function buildConsultingRunSchema(agent: string, catalogue: readonly stri
40
39
  new: Type.Optional(
41
40
  Type.Object(
42
41
  {
43
- agent: StringEnum([agent], {
42
+ agent: Type.Enum([agent], {
43
+ type: "string",
44
44
  description: "The Consulting Agent that will perform this Consultation.",
45
45
  }),
46
46
  model: buildModelCatalogueEnum(catalogue),
@@ -12,6 +12,8 @@ export interface RegisterConsultingRunOptions {
12
12
  adapter: ConsultingAgentAdapter;
13
13
  availability: ConsultingAgentAvailability;
14
14
  handles: ConversationHandleStore;
15
+ /** Override the user home for scoped configuration loading. */
16
+ homeDir?: string;
15
17
  }
16
18
 
17
19
  /** Register consulting_run from one immutable availability snapshot. */
@@ -31,6 +33,7 @@ export function registerConsultingRunTool(options: RegisterConsultingRunOptions)
31
33
  adapter: options.adapter,
32
34
  availability: options.availability,
33
35
  handles: options.handles,
36
+ homeDir: options.homeDir,
34
37
  }),
35
38
  });
36
39
  }