pi-advisor-flow 0.1.9 → 0.2.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/config.ts CHANGED
@@ -1,48 +1,124 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { CONFIG_DIR_NAME, type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
3
+ import {
4
+ CONFIG_DIR_NAME,
5
+ DEFAULT_MAX_BYTES,
6
+ DEFAULT_MAX_LINES,
7
+ type ExtensionContext,
8
+ getAgentDir,
9
+ } from "@earendil-works/pi-coding-agent";
4
10
 
5
11
  export const FALLBACK_EXECUTOR = "aikeys/claude-sonnet-5";
6
12
  export const FALLBACK_ADVISOR = "aikeys/claude-fable-5";
7
13
  export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
8
- // MAX_SAFE_INTEGER represents the complete reconstructed branch (the ALL preset).
9
14
  export const MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
15
+ export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = DEFAULT_MAX_LINES;
16
+ export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES = DEFAULT_MAX_BYTES;
17
+ export type GateFailureMode =
18
+ | "block-session"
19
+ | "block-tool"
20
+ | "warn-and-continue";
21
+ export const GATE_FAILURE_MODES: GateFailureMode[] = [
22
+ "block-session",
23
+ "block-tool",
24
+ "warn-and-continue",
25
+ ];
10
26
 
11
27
  export let executorRef = FALLBACK_EXECUTOR;
12
28
  export let advisorRef = FALLBACK_ADVISOR;
13
- export let executorEffortRef: string | undefined = undefined;
14
- export let advisorEffortRef: string | undefined = undefined;
29
+ export let executorEffortRef: string | undefined;
30
+ export let advisorEffortRef: string | undefined;
15
31
  export let contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
16
32
  export let advisorPlanGateRef = true;
17
33
  export let advisorFailureGateRef = true;
18
34
  export let advisorCompletionGateRef = true;
19
- export let advisorCustomInvocationRef: string | undefined = undefined;
35
+ export let advisorCustomInvocationRef: string | undefined;
20
36
  export let advisorCollapseResponsesRef = false;
21
37
  export let advisorBlockOnBlockedRef = true;
22
38
  export let advisorAutoLoopGateRef = true;
23
39
  export let advisorLoopThresholdRef = 3;
24
- export let advisorMaxCallsPerSessionRef: number | undefined = undefined;
40
+ export let advisorMaxCallsPerSessionRef: number | undefined;
25
41
  export let advisorSessionSummaryRef = true;
42
+ export let advisorFailureModeRef: GateFailureMode = "block-session";
43
+ export let advisorHerdrIntegrationRef = true;
44
+ export let advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
45
+ export let advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
26
46
 
27
- export const setExecutorRef = (ref: string) => { executorRef = ref; };
28
- export const setAdvisorRef = (ref: string) => { advisorRef = ref; };
29
- export const setExecutorEffortRef = (effort: string | undefined) => { executorEffortRef = effort; };
30
- export const setAdvisorEffortRef = (effort: string | undefined) => { advisorEffortRef = effort; };
47
+ export const setExecutorRef = (ref: string) => {
48
+ executorRef = ref;
49
+ };
50
+ export const setAdvisorRef = (ref: string) => {
51
+ advisorRef = ref;
52
+ };
53
+ export const setExecutorEffortRef = (effort: string | undefined) => {
54
+ executorEffortRef = effort;
55
+ };
56
+ export const setAdvisorEffortRef = (effort: string | undefined) => {
57
+ advisorEffortRef = effort;
58
+ };
31
59
  export const isValidContextMaxChars = (value: unknown): value is number =>
32
- typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= MAX_CONTEXT_MAX_CHARS;
33
- export const setContextMaxCharsRef = (value: number) => { contextMaxCharsRef = value; };
34
- export const setAdvisorPlanGateRef = (enabled: boolean) => { advisorPlanGateRef = enabled; };
35
- export const setAdvisorFailureGateRef = (enabled: boolean) => { advisorFailureGateRef = enabled; };
36
- export const setAdvisorCompletionGateRef = (enabled: boolean) => { advisorCompletionGateRef = enabled; };
37
- export const setAdvisorCustomInvocationRef = (rule: string | undefined) => { advisorCustomInvocationRef = rule?.trim() || undefined; };
38
- export const setAdvisorCollapseResponsesRef = (enabled: boolean) => { advisorCollapseResponsesRef = enabled; };
39
- export const setAdvisorBlockOnBlockedRef = (enabled: boolean) => { advisorBlockOnBlockedRef = enabled; };
40
- export const setAdvisorAutoLoopGateRef = (enabled: boolean) => { advisorAutoLoopGateRef = enabled; };
41
- export const isValidLoopThreshold = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 2;
42
- export const setAdvisorLoopThresholdRef = (value: number) => { advisorLoopThresholdRef = value; };
43
- export const isValidMaxCallsPerSession = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
44
- export const setAdvisorMaxCallsPerSessionRef = (value: number | undefined) => { advisorMaxCallsPerSessionRef = value; };
45
- export const setAdvisorSessionSummaryRef = (enabled: boolean) => { advisorSessionSummaryRef = enabled; };
60
+ typeof value === "number" &&
61
+ Number.isSafeInteger(value) &&
62
+ value >= 0 &&
63
+ value <= MAX_CONTEXT_MAX_CHARS;
64
+ export const setContextMaxCharsRef = (value: number) => {
65
+ contextMaxCharsRef = value;
66
+ };
67
+ export const setAdvisorPlanGateRef = (enabled: boolean) => {
68
+ advisorPlanGateRef = enabled;
69
+ };
70
+ export const setAdvisorFailureGateRef = (enabled: boolean) => {
71
+ advisorFailureGateRef = enabled;
72
+ };
73
+ export const setAdvisorCompletionGateRef = (enabled: boolean) => {
74
+ advisorCompletionGateRef = enabled;
75
+ };
76
+ export const setAdvisorCustomInvocationRef = (rule: string | undefined) => {
77
+ advisorCustomInvocationRef = rule?.trim() || undefined;
78
+ };
79
+ export const setAdvisorCollapseResponsesRef = (enabled: boolean) => {
80
+ advisorCollapseResponsesRef = enabled;
81
+ };
82
+ export const setAdvisorBlockOnBlockedRef = (enabled: boolean) => {
83
+ advisorBlockOnBlockedRef = enabled;
84
+ };
85
+ export const setAdvisorAutoLoopGateRef = (enabled: boolean) => {
86
+ advisorAutoLoopGateRef = enabled;
87
+ };
88
+ export const isValidLoopThreshold = (value: unknown): value is number =>
89
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 2;
90
+ export const setAdvisorLoopThresholdRef = (value: number) => {
91
+ advisorLoopThresholdRef = value;
92
+ };
93
+ export const isValidMaxCallsPerSession = (value: unknown): value is number =>
94
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
95
+ export const setAdvisorMaxCallsPerSessionRef = (value: number | undefined) => {
96
+ advisorMaxCallsPerSessionRef = value;
97
+ };
98
+ export const setAdvisorSessionSummaryRef = (enabled: boolean) => {
99
+ advisorSessionSummaryRef = enabled;
100
+ };
101
+ export const isValidGateFailureMode = (
102
+ value: unknown
103
+ ): value is GateFailureMode =>
104
+ typeof value === "string" &&
105
+ GATE_FAILURE_MODES.includes(value as GateFailureMode);
106
+ export const setAdvisorFailureModeRef = (value: GateFailureMode) => {
107
+ advisorFailureModeRef = value;
108
+ };
109
+ export const setAdvisorHerdrIntegrationRef = (enabled: boolean) => {
110
+ advisorHerdrIntegrationRef = enabled;
111
+ };
112
+ export const isValidToolResultMaxLines = (value: unknown): value is number =>
113
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
114
+ export const isValidToolResultMaxBytes = (value: unknown): value is number =>
115
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
116
+ export const setAdvisorToolResultMaxLinesRef = (value: number) => {
117
+ advisorToolResultMaxLinesRef = value;
118
+ };
119
+ export const setAdvisorToolResultMaxBytesRef = (value: number) => {
120
+ advisorToolResultMaxBytesRef = value;
121
+ };
46
122
 
47
123
  export const splitRef = (ref: string): [string, string] => {
48
124
  const i = ref.indexOf("/");
@@ -50,49 +126,182 @@ export const splitRef = (ref: string): [string, string] => {
50
126
  };
51
127
 
52
128
  export const configPaths = (ctx: ExtensionContext) => [
53
- ctx.isProjectTrusted() ? join(ctx.cwd, CONFIG_DIR_NAME, "advisor.json") : null,
129
+ ctx.isProjectTrusted()
130
+ ? join(ctx.cwd, CONFIG_DIR_NAME, "advisor.json")
131
+ : null,
54
132
  join(getAgentDir(), "advisor.json"),
55
133
  ];
56
134
 
57
- type AdvisorConfig = {
58
- executor?: string;
135
+ export interface AdvisorConfig {
59
136
  advisor?: string;
60
- executorEffort?: string;
61
- advisorEffort?: string;
62
- contextMaxChars?: number;
63
- advisorPlanGate?: boolean;
64
- advisorFailureGate?: boolean;
137
+ advisorAutoLoopGate?: boolean;
138
+ advisorBlockOnBlocked?: boolean;
139
+ advisorCollapseResponses?: boolean;
65
140
  advisorCompletionGate?: boolean;
66
141
  advisorCustomInvocation?: string;
67
- advisorCollapseResponses?: boolean;
68
- advisorBlockOnBlocked?: boolean;
69
- advisorAutoLoopGate?: boolean;
142
+ advisorEffort?: string;
143
+ advisorFailureGate?: boolean;
144
+ advisorHerdrIntegration?: boolean;
70
145
  advisorLoopThreshold?: number;
71
146
  advisorMaxCallsPerSession?: number;
147
+ advisorPlanGate?: boolean;
72
148
  advisorSessionSummary?: boolean;
149
+ advisorToolResultMaxBytes?: number;
150
+ advisorToolResultMaxLines?: number;
151
+ contextMaxChars?: number;
152
+ executor?: string;
153
+ executorEffort?: string;
154
+ gateFailureMode?: GateFailureMode;
155
+ }
156
+
157
+ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
158
+ "advisor",
159
+ "advisorAutoLoopGate",
160
+ "advisorBlockOnBlocked",
161
+ "advisorCollapseResponses",
162
+ "advisorCompletionGate",
163
+ "advisorCustomInvocation",
164
+ "advisorEffort",
165
+ "advisorFailureGate",
166
+ "advisorHerdrIntegration",
167
+ "advisorLoopThreshold",
168
+ "advisorMaxCallsPerSession",
169
+ "advisorPlanGate",
170
+ "advisorSessionSummary",
171
+ "advisorToolResultMaxBytes",
172
+ "advisorToolResultMaxLines",
173
+ "contextMaxChars",
174
+ "executor",
175
+ "executorEffort",
176
+ "gateFailureMode",
177
+ ]);
178
+ const BOOLEAN_CONFIG_KEYS = [
179
+ "advisorPlanGate",
180
+ "advisorFailureGate",
181
+ "advisorCompletionGate",
182
+ "advisorCollapseResponses",
183
+ "advisorBlockOnBlocked",
184
+ "advisorAutoLoopGate",
185
+ "advisorSessionSummary",
186
+ "advisorHerdrIntegration",
187
+ ] as const;
188
+ const STRING_CONFIG_KEYS = [
189
+ "executor",
190
+ "advisor",
191
+ "executorEffort",
192
+ "advisorEffort",
193
+ "advisorCustomInvocation",
194
+ ] as const;
195
+ const ARGUMENT_WHITESPACE = /\s+/;
196
+
197
+ type ConfigRecord = Record<string, unknown>;
198
+
199
+ const invalidConfigValue = (
200
+ path: string,
201
+ key: string,
202
+ accepted: string
203
+ ): never => {
204
+ throw new TypeError(
205
+ `Invalid advisor configuration at ${path}, key ${JSON.stringify(key)}: expected ${accepted}.`
206
+ );
73
207
  };
74
208
 
75
- const isValidConfig = (value: unknown): value is AdvisorConfig => {
76
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
77
- const config = value as Record<string, unknown>;
78
- return (config.executor === undefined || typeof config.executor === "string")
79
- && (config.advisor === undefined || typeof config.advisor === "string")
80
- && (config.executorEffort === undefined || typeof config.executorEffort === "string")
81
- && (config.advisorEffort === undefined || typeof config.advisorEffort === "string")
82
- && (config.contextMaxChars === undefined || isValidContextMaxChars(config.contextMaxChars))
83
- && (config.advisorPlanGate === undefined || typeof config.advisorPlanGate === "boolean")
84
- && (config.advisorFailureGate === undefined || typeof config.advisorFailureGate === "boolean")
85
- && (config.advisorCompletionGate === undefined || typeof config.advisorCompletionGate === "boolean")
86
- && (config.advisorCustomInvocation === undefined || typeof config.advisorCustomInvocation === "string")
87
- && (config.advisorCollapseResponses === undefined || typeof config.advisorCollapseResponses === "boolean")
88
- && (config.advisorBlockOnBlocked === undefined || typeof config.advisorBlockOnBlocked === "boolean")
89
- && (config.advisorAutoLoopGate === undefined || typeof config.advisorAutoLoopGate === "boolean")
90
- && (config.advisorLoopThreshold === undefined || isValidLoopThreshold(config.advisorLoopThreshold))
91
- && (config.advisorMaxCallsPerSession === undefined || isValidMaxCallsPerSession(config.advisorMaxCallsPerSession))
92
- && (config.advisorSessionSummary === undefined || typeof config.advisorSessionSummary === "boolean");
209
+ const validateKnownKeys = (config: ConfigRecord, path: string) => {
210
+ const unknownKeys = Object.keys(config).filter(
211
+ (key) => !CONFIG_KEYS.has(key as keyof AdvisorConfig)
212
+ );
213
+ if (unknownKeys.length > 0) {
214
+ throw new TypeError(
215
+ `Invalid advisor configuration at ${path}: unknown key(s) ${unknownKeys.map((key) => JSON.stringify(key)).join(", ")}. Remove them or upgrade pi-advisor.`
216
+ );
217
+ }
93
218
  };
94
219
 
95
- export const loadConfig = (ctx: ExtensionContext) => {
220
+ const validateStringValues = (config: ConfigRecord, path: string) => {
221
+ for (const key of STRING_CONFIG_KEYS) {
222
+ if (config[key] !== undefined && typeof config[key] !== "string") {
223
+ invalidConfigValue(
224
+ path,
225
+ key,
226
+ key === "executor" || key === "advisor"
227
+ ? "a provider/model string"
228
+ : "a string"
229
+ );
230
+ }
231
+ }
232
+ };
233
+
234
+ const validateBooleanValues = (config: ConfigRecord, path: string) => {
235
+ for (const key of BOOLEAN_CONFIG_KEYS) {
236
+ if (config[key] !== undefined && typeof config[key] !== "boolean") {
237
+ invalidConfigValue(path, key, "true or false");
238
+ }
239
+ }
240
+ };
241
+
242
+ const validateNumericValues = (config: ConfigRecord, path: string) => {
243
+ const numericRules: [
244
+ keyof AdvisorConfig,
245
+ (value: unknown) => boolean,
246
+ string,
247
+ ][] = [
248
+ [
249
+ "contextMaxChars",
250
+ isValidContextMaxChars,
251
+ `a safe integer from 0 through ${MAX_CONTEXT_MAX_CHARS}`,
252
+ ],
253
+ [
254
+ "advisorLoopThreshold",
255
+ isValidLoopThreshold,
256
+ "a safe integer of at least 2",
257
+ ],
258
+ [
259
+ "advisorMaxCallsPerSession",
260
+ isValidMaxCallsPerSession,
261
+ "a non-negative safe integer",
262
+ ],
263
+ [
264
+ "advisorToolResultMaxLines",
265
+ isValidToolResultMaxLines,
266
+ "a non-negative safe integer",
267
+ ],
268
+ [
269
+ "advisorToolResultMaxBytes",
270
+ isValidToolResultMaxBytes,
271
+ "a non-negative safe integer",
272
+ ],
273
+ ];
274
+ for (const [key, isValid, description] of numericRules) {
275
+ if (config[key] !== undefined && !isValid(config[key])) {
276
+ invalidConfigValue(path, key, description);
277
+ }
278
+ }
279
+ };
280
+
281
+ export const validateConfig = (
282
+ value: unknown,
283
+ path = "advisor.json"
284
+ ): value is AdvisorConfig => {
285
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
286
+ throw new TypeError(
287
+ `Invalid advisor configuration at ${path}: expected a JSON object.`
288
+ );
289
+ }
290
+ const config = value as ConfigRecord;
291
+ validateKnownKeys(config, path);
292
+ validateStringValues(config, path);
293
+ validateBooleanValues(config, path);
294
+ validateNumericValues(config, path);
295
+ if (
296
+ config.gateFailureMode !== undefined &&
297
+ !isValidGateFailureMode(config.gateFailureMode)
298
+ ) {
299
+ invalidConfigValue(path, "gateFailureMode", GATE_FAILURE_MODES.join(", "));
300
+ }
301
+ return true;
302
+ };
303
+
304
+ const resetDefaults = () => {
96
305
  executorRef = FALLBACK_EXECUTOR;
97
306
  advisorRef = FALLBACK_ADVISOR;
98
307
  executorEffortRef = undefined;
@@ -108,62 +317,158 @@ export const loadConfig = (ctx: ExtensionContext) => {
108
317
  advisorLoopThresholdRef = 3;
109
318
  advisorMaxCallsPerSessionRef = undefined;
110
319
  advisorSessionSummaryRef = true;
111
- for (const path of configPaths(ctx)) {
112
- if (!path || !existsSync(path)) continue;
113
- try {
114
- const config = JSON.parse(readFileSync(path, "utf8"));
115
- if (!isValidConfig(config)) throw new TypeError("Invalid advisor configuration");
116
- if (config.executor) executorRef = config.executor;
117
- if (config.advisor) advisorRef = config.advisor;
118
- if (config.executorEffort) executorEffortRef = config.executorEffort;
119
- if (config.advisorEffort) advisorEffortRef = config.advisorEffort;
120
- if (isValidContextMaxChars(config.contextMaxChars)) contextMaxCharsRef = config.contextMaxChars;
121
- if (typeof config.advisorPlanGate === "boolean") advisorPlanGateRef = config.advisorPlanGate;
122
- if (typeof config.advisorFailureGate === "boolean") advisorFailureGateRef = config.advisorFailureGate;
123
- if (typeof config.advisorCompletionGate === "boolean") advisorCompletionGateRef = config.advisorCompletionGate;
124
- if (typeof config.advisorCustomInvocation === "string") advisorCustomInvocationRef = config.advisorCustomInvocation || undefined;
125
- if (typeof config.advisorCollapseResponses === "boolean") advisorCollapseResponsesRef = config.advisorCollapseResponses;
126
- if (typeof config.advisorBlockOnBlocked === "boolean") advisorBlockOnBlockedRef = config.advisorBlockOnBlocked;
127
- if (typeof config.advisorAutoLoopGate === "boolean") advisorAutoLoopGateRef = config.advisorAutoLoopGate;
128
- if (isValidLoopThreshold(config.advisorLoopThreshold)) advisorLoopThresholdRef = config.advisorLoopThreshold;
129
- if (isValidMaxCallsPerSession(config.advisorMaxCallsPerSession)) advisorMaxCallsPerSessionRef = config.advisorMaxCallsPerSession;
130
- if (typeof config.advisorSessionSummary === "boolean") advisorSessionSummaryRef = config.advisorSessionSummary;
131
- return path;
132
- } catch {
133
- // Ignore malformed config and keep looking for a valid fallback.
134
- }
320
+ advisorFailureModeRef = "block-session";
321
+ advisorHerdrIntegrationRef = true;
322
+ advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
323
+ advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
324
+ };
325
+
326
+ const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
327
+ config: AdvisorConfig,
328
+ key: Key,
329
+ apply: (value: NonNullable<AdvisorConfig[Key]>) => void
330
+ ) => {
331
+ const value = config[key];
332
+ if (value !== undefined) {
333
+ apply(value as NonNullable<AdvisorConfig[Key]>);
334
+ }
335
+ };
336
+
337
+ const applyNonEmptyStringConfig = (
338
+ value: string | undefined,
339
+ apply: (value: string) => void
340
+ ) => {
341
+ if (value) {
342
+ apply(value);
343
+ }
344
+ };
345
+
346
+ const applyConfig = (config: AdvisorConfig) => {
347
+ applyNonEmptyStringConfig(config.executor, setExecutorRef);
348
+ applyNonEmptyStringConfig(config.advisor, setAdvisorRef);
349
+ applyNonEmptyStringConfig(config.executorEffort, setExecutorEffortRef);
350
+ applyNonEmptyStringConfig(config.advisorEffort, setAdvisorEffortRef);
351
+ applyOptionalConfig(config, "contextMaxChars", setContextMaxCharsRef);
352
+ applyOptionalConfig(config, "advisorPlanGate", setAdvisorPlanGateRef);
353
+ applyOptionalConfig(config, "advisorFailureGate", setAdvisorFailureGateRef);
354
+ applyOptionalConfig(
355
+ config,
356
+ "advisorCompletionGate",
357
+ setAdvisorCompletionGateRef
358
+ );
359
+ applyOptionalConfig(
360
+ config,
361
+ "advisorCustomInvocation",
362
+ setAdvisorCustomInvocationRef
363
+ );
364
+ applyOptionalConfig(
365
+ config,
366
+ "advisorCollapseResponses",
367
+ setAdvisorCollapseResponsesRef
368
+ );
369
+ applyOptionalConfig(
370
+ config,
371
+ "advisorBlockOnBlocked",
372
+ setAdvisorBlockOnBlockedRef
373
+ );
374
+ applyOptionalConfig(config, "advisorAutoLoopGate", setAdvisorAutoLoopGateRef);
375
+ applyOptionalConfig(
376
+ config,
377
+ "advisorLoopThreshold",
378
+ setAdvisorLoopThresholdRef
379
+ );
380
+ applyOptionalConfig(
381
+ config,
382
+ "advisorMaxCallsPerSession",
383
+ setAdvisorMaxCallsPerSessionRef
384
+ );
385
+ applyOptionalConfig(
386
+ config,
387
+ "advisorSessionSummary",
388
+ setAdvisorSessionSummaryRef
389
+ );
390
+ applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
391
+ applyOptionalConfig(
392
+ config,
393
+ "advisorHerdrIntegration",
394
+ setAdvisorHerdrIntegrationRef
395
+ );
396
+ applyOptionalConfig(
397
+ config,
398
+ "advisorToolResultMaxLines",
399
+ setAdvisorToolResultMaxLinesRef
400
+ );
401
+ applyOptionalConfig(
402
+ config,
403
+ "advisorToolResultMaxBytes",
404
+ setAdvisorToolResultMaxBytesRef
405
+ );
406
+ };
407
+
408
+ const readConfig = (path: string): AdvisorConfig => {
409
+ try {
410
+ const config = JSON.parse(readFileSync(path, "utf8"));
411
+ validateConfig(config, path);
412
+ return config;
413
+ } catch (error) {
414
+ throw error instanceof Error ? error : new Error(String(error));
415
+ }
416
+ };
417
+
418
+ export const loadConfig = (ctx: ExtensionContext) => {
419
+ resetDefaults();
420
+ const path = configPaths(ctx).find(
421
+ (candidate): candidate is string =>
422
+ candidate !== null && existsSync(candidate)
423
+ );
424
+ if (!path) {
425
+ return null;
135
426
  }
136
- return null;
427
+ applyConfig(readConfig(path));
428
+ return path;
137
429
  };
138
430
 
139
431
  export const saveConfig = (ctx: ExtensionContext) => {
140
432
  const project = join(ctx.cwd, CONFIG_DIR_NAME, "advisor.json");
141
- const path = ctx.isProjectTrusted() && existsSync(project) ? project : join(getAgentDir(), "advisor.json");
142
- // Preserve settings owned by future versions or other tools that share this file.
433
+ const path =
434
+ ctx.isProjectTrusted() && existsSync(project)
435
+ ? project
436
+ : join(getAgentDir(), "advisor.json");
143
437
  let existing: Record<string, unknown> = {};
144
438
  try {
145
439
  const parsed = JSON.parse(readFileSync(path, "utf8"));
146
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed as Record<string, unknown>;
440
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
441
+ existing = parsed as Record<string, unknown>;
442
+ }
147
443
  } catch {
148
- // A missing or malformed file is safely replaced with the current settings.
444
+ /* replace a missing or malformed file */
445
+ }
446
+ if (advisorMaxCallsPerSessionRef === undefined) {
447
+ existing.advisorMaxCallsPerSession = undefined;
149
448
  }
150
449
  const data = {
151
450
  ...existing,
152
- executor: executorRef,
153
451
  advisor: advisorRef,
154
- executorEffort: executorEffortRef,
155
- advisorEffort: advisorEffortRef,
156
- contextMaxChars: contextMaxCharsRef,
157
- advisorPlanGate: advisorPlanGateRef,
158
- advisorFailureGate: advisorFailureGateRef,
452
+ advisorAutoLoopGate: advisorAutoLoopGateRef,
453
+ advisorBlockOnBlocked: advisorBlockOnBlockedRef,
454
+ advisorCollapseResponses: advisorCollapseResponsesRef,
159
455
  advisorCompletionGate: advisorCompletionGateRef,
160
456
  advisorCustomInvocation: advisorCustomInvocationRef,
161
- advisorCollapseResponses: advisorCollapseResponsesRef,
162
- advisorBlockOnBlocked: advisorBlockOnBlockedRef,
163
- advisorAutoLoopGate: advisorAutoLoopGateRef,
457
+ advisorEffort: advisorEffortRef,
458
+ advisorFailureGate: advisorFailureGateRef,
164
459
  advisorLoopThreshold: advisorLoopThresholdRef,
165
- ...(advisorMaxCallsPerSessionRef === undefined ? {} : { advisorMaxCallsPerSession: advisorMaxCallsPerSessionRef }),
460
+ advisorPlanGate: advisorPlanGateRef,
461
+ contextMaxChars: contextMaxCharsRef,
462
+ executor: executorRef,
463
+ executorEffort: executorEffortRef,
464
+ ...(advisorMaxCallsPerSessionRef === undefined
465
+ ? {}
466
+ : { advisorMaxCallsPerSession: advisorMaxCallsPerSessionRef }),
467
+ advisorHerdrIntegration: advisorHerdrIntegrationRef,
166
468
  advisorSessionSummary: advisorSessionSummaryRef,
469
+ advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
470
+ advisorToolResultMaxLines: advisorToolResultMaxLinesRef,
471
+ gateFailureMode: advisorFailureModeRef,
167
472
  };
168
473
  writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
169
474
  return path;
@@ -173,11 +478,14 @@ export const parseArgs = (args: string): string | undefined => {
173
478
  let nextExecutor = executorRef;
174
479
  let nextAdvisor = advisorRef;
175
480
  let nextContextMaxChars = contextMaxCharsRef;
176
-
177
- for (const token of args.trim().split(/\s+/).filter(Boolean)) {
481
+ for (const token of args.trim().split(ARGUMENT_WHITESPACE).filter(Boolean)) {
178
482
  const [key, value] = token.split("=");
179
- if (key === "executor" && value) nextExecutor = value;
180
- if (key === "advisor" && value) nextAdvisor = value;
483
+ if (key === "executor" && value) {
484
+ nextExecutor = value;
485
+ }
486
+ if (key === "advisor" && value) {
487
+ nextAdvisor = value;
488
+ }
181
489
  if (key === "contextMaxChars") {
182
490
  const parsed = Number(value);
183
491
  if (!isValidContextMaxChars(parsed)) {
@@ -186,9 +494,7 @@ export const parseArgs = (args: string): string | undefined => {
186
494
  nextContextMaxChars = parsed;
187
495
  }
188
496
  }
189
-
190
497
  executorRef = nextExecutor;
191
498
  advisorRef = nextAdvisor;
192
499
  contextMaxCharsRef = nextContextMaxChars;
193
- return undefined;
194
500
  };