pi-advisor-flow 0.5.1 → 0.5.2

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +3 -2
  3. package/extensions/index.ts +7 -7
  4. package/package.json +2 -1
  5. package/src/commands/activation-preparation.ts +117 -0
  6. package/src/commands/activation.ts +135 -0
  7. package/src/commands/lifecycle.ts +83 -0
  8. package/src/commands/manual-command.ts +107 -0
  9. package/src/commands/manual-consultation.ts +144 -0
  10. package/src/commands/manual-progress.ts +93 -0
  11. package/src/commands/model-commands.ts +66 -0
  12. package/src/commands/model-options.ts +181 -0
  13. package/src/commands/model-picker.ts +110 -0
  14. package/src/commands/registration.ts +27 -0
  15. package/src/commands/renderers.ts +76 -0
  16. package/src/commands/runtime.ts +114 -0
  17. package/src/commands/settings-commands.ts +80 -0
  18. package/src/commands/settings-persistence.ts +81 -0
  19. package/src/commands/types.ts +78 -0
  20. package/src/commands.ts +9 -1259
  21. package/src/config/args.ts +37 -0
  22. package/src/config/defaults.ts +190 -0
  23. package/src/config/state.ts +199 -0
  24. package/src/config/storage.ts +224 -0
  25. package/src/config/types.ts +63 -0
  26. package/src/config/validation.ts +234 -0
  27. package/src/config.ts +104 -814
  28. package/src/conversation.ts +4 -2
  29. package/src/herdr.ts +1 -1
  30. package/src/model-stream.ts +15 -1
  31. package/src/scout-context.ts +2 -2
  32. package/src/scout.ts +1 -1
  33. package/src/tools/consultation.ts +337 -0
  34. package/src/tools/gate-policy.ts +132 -0
  35. package/src/tools/gate-protocol.ts +127 -0
  36. package/src/tools/loop-gate.ts +210 -0
  37. package/src/tools/prompts.ts +159 -0
  38. package/src/tools/register-ask-advisor.ts +225 -0
  39. package/src/tools/register-lifecycle.ts +89 -0
  40. package/src/tools/register-outcome.ts +75 -0
  41. package/src/tools/register-renderers.ts +88 -0
  42. package/src/tools/registration.ts +35 -0
  43. package/src/tools/render-advisor-result.ts +168 -0
  44. package/src/tools/render-common.ts +120 -0
  45. package/src/tools/scout-status.ts +206 -0
  46. package/src/tools/session.ts +3 -0
  47. package/src/tools/types.ts +119 -0
  48. package/src/tools.ts +42 -1865
  49. package/src/ui/manual-dialog-render.ts +150 -0
  50. package/src/ui/manual-dialog.ts +305 -0
  51. package/src/ui/model-selector.ts +166 -0
  52. package/src/ui/settings-formatting.ts +133 -0
  53. package/src/ui/settings-items.ts +323 -0
  54. package/src/ui/settings-list-adapter.ts +103 -0
  55. package/src/ui/settings-mutations.ts +80 -0
  56. package/src/ui/settings-selector.ts +161 -0
  57. package/src/ui/text-setting-submenu.ts +66 -0
  58. package/src/ui/types.ts +111 -0
  59. package/src/ui.ts +9 -1418
package/src/config.ts CHANGED
@@ -1,814 +1,104 @@
1
- import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
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";
10
- import {
11
- GIT_CONTEXT_LEVELS,
12
- type GitContextLevel,
13
- isValidGitContextLevel,
14
- } from "./git.js";
15
-
16
- export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
17
- export const MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
18
- export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = DEFAULT_MAX_LINES;
19
- export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES = DEFAULT_MAX_BYTES;
20
- export const DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS = 20_000;
21
- export type AdvisorToolPolicy = "full" | "summary" | "exclude";
22
- export type AdvisorToolPolicies = Record<string, AdvisorToolPolicy>;
23
- export const ADVISOR_TOOL_POLICIES: AdvisorToolPolicy[] = [
24
- "full",
25
- "summary",
26
- "exclude",
27
- ];
28
- export type GateFailureMode =
29
- | "block-session"
30
- | "block-tool"
31
- | "warn-and-continue";
32
- export const GATE_FAILURE_MODES: GateFailureMode[] = [
33
- "block-session",
34
- "block-tool",
35
- "warn-and-continue",
36
- ];
37
-
38
- // An empty ref means no model has been selected yet.
39
- export let executorRef = "";
40
- export let advisorRef = "";
41
- let persistedExecutorRef: string | undefined;
42
- let persistedAdvisorRef: string | undefined;
43
- export let executorEffortRef: string | undefined;
44
- export let advisorEffortRef: string | undefined;
45
- export let contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
46
- export let advisorPlanGateRef = true;
47
- export let advisorFailureGateRef = true;
48
- export let advisorCompletionGateRef = true;
49
- export let advisorCustomInvocationRef: string | undefined;
50
- export let advisorCollapseResponsesRef = false;
51
- export let advisorBlockOnBlockedRef = true;
52
- export let advisorAutoLoopGateRef = true;
53
- export let advisorLoopThresholdRef = 3;
54
- export let advisorMaxCallsPerSessionRef: number | undefined;
55
- export let advisorSessionSummaryRef = false;
56
- export let simpleModeRef = false;
57
- export let alwaysOnRef = false;
58
- export let advisorFailureModeRef: GateFailureMode = "block-session";
59
- export let advisorHerdrIntegrationRef = true;
60
- export let advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
61
- export let advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
62
- export let advisorRedactSecretsRef = false;
63
- export let advisorGitContextRef: GitContextLevel = "summary";
64
- export let advisorGitContextMaxCharsRef = DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS;
65
- export let advisorToolPoliciesRef: AdvisorToolPolicies = {};
66
- export let advisorOutcomeLoggingRef = false;
67
- export let advisorUntrackedContentRef = false;
68
- export let advisorTrackedFileContentRef = false;
69
- export let advisorScoutEnabledRef = false;
70
- export let showUsageDetailsRef = true;
71
- export let showUsageFooterRef = false;
72
-
73
- export const setExecutorRef = (ref: string) => {
74
- executorRef = ref;
75
- };
76
- export const setAdvisorRef = (ref: string) => {
77
- advisorRef = ref;
78
- };
79
- /** Returns model refs explicitly persisted in the global Advisor config. */
80
- export const getPersistedModelRefs = () => ({
81
- advisor: persistedAdvisorRef,
82
- executor: persistedExecutorRef,
83
- });
84
- export const setExecutorEffortRef = (effort: string | undefined) => {
85
- executorEffortRef = effort;
86
- };
87
- export const setAdvisorEffortRef = (effort: string | undefined) => {
88
- advisorEffortRef = effort;
89
- };
90
- export const isValidContextMaxChars = (value: unknown): value is number =>
91
- typeof value === "number" &&
92
- Number.isSafeInteger(value) &&
93
- value >= 0 &&
94
- value <= MAX_CONTEXT_MAX_CHARS;
95
- export const setContextMaxCharsRef = (value: number) => {
96
- contextMaxCharsRef = value;
97
- };
98
- export const setAdvisorPlanGateRef = (enabled: boolean) => {
99
- advisorPlanGateRef = enabled;
100
- };
101
- export const setAdvisorFailureGateRef = (enabled: boolean) => {
102
- advisorFailureGateRef = enabled;
103
- };
104
- export const setAdvisorCompletionGateRef = (enabled: boolean) => {
105
- advisorCompletionGateRef = enabled;
106
- };
107
- export const setAdvisorCustomInvocationRef = (rule: string | undefined) => {
108
- advisorCustomInvocationRef = rule?.trim() || undefined;
109
- };
110
- export const setAdvisorCollapseResponsesRef = (enabled: boolean) => {
111
- advisorCollapseResponsesRef = enabled;
112
- };
113
- export const setAdvisorBlockOnBlockedRef = (enabled: boolean) => {
114
- advisorBlockOnBlockedRef = enabled;
115
- };
116
- export const setAdvisorAutoLoopGateRef = (enabled: boolean) => {
117
- advisorAutoLoopGateRef = enabled;
118
- };
119
- export const isValidLoopThreshold = (value: unknown): value is number =>
120
- typeof value === "number" && Number.isSafeInteger(value) && value >= 2;
121
- export const setAdvisorLoopThresholdRef = (value: number) => {
122
- advisorLoopThresholdRef = value;
123
- };
124
- export const isValidMaxCallsPerSession = (value: unknown): value is number =>
125
- typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
126
- export const setAdvisorMaxCallsPerSessionRef = (value: number | undefined) => {
127
- advisorMaxCallsPerSessionRef = value;
128
- };
129
- export const setAdvisorSessionSummaryRef = (enabled: boolean) => {
130
- advisorSessionSummaryRef = enabled;
131
- };
132
- export const setSimpleModeRef = (enabled: boolean) => {
133
- simpleModeRef = enabled;
134
- };
135
- export const setAlwaysOnRef = (enabled: boolean) => {
136
- alwaysOnRef = enabled;
137
- };
138
- /** Whether advanced Advisor automation is suppressed for this session. */
139
- export const isSimpleMode = () => simpleModeRef;
140
- export const isValidGateFailureMode = (
141
- value: unknown
142
- ): value is GateFailureMode =>
143
- typeof value === "string" &&
144
- GATE_FAILURE_MODES.includes(value as GateFailureMode);
145
- export const setAdvisorFailureModeRef = (value: GateFailureMode) => {
146
- advisorFailureModeRef = value;
147
- };
148
- export const setAdvisorHerdrIntegrationRef = (enabled: boolean) => {
149
- advisorHerdrIntegrationRef = enabled;
150
- };
151
- export const isValidToolResultMaxLines = (value: unknown): value is number =>
152
- typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
153
- export const isValidToolResultMaxBytes = (value: unknown): value is number =>
154
- typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
155
- export const setAdvisorToolResultMaxLinesRef = (value: number) => {
156
- advisorToolResultMaxLinesRef = value;
157
- };
158
- export const setAdvisorToolResultMaxBytesRef = (value: number) => {
159
- advisorToolResultMaxBytesRef = value;
160
- };
161
- export const setAdvisorRedactSecretsRef = (enabled: boolean) => {
162
- advisorRedactSecretsRef = enabled;
163
- };
164
- export const setAdvisorGitContextRef = (level: GitContextLevel) => {
165
- advisorGitContextRef = level;
166
- };
167
- export const setAdvisorGitContextMaxCharsRef = (value: number) => {
168
- advisorGitContextMaxCharsRef = value;
169
- };
170
- export const setAdvisorToolPoliciesRef = (policies: AdvisorToolPolicies) => {
171
- advisorToolPoliciesRef = { ...policies };
172
- };
173
- export const setAdvisorOutcomeLoggingRef = (enabled: boolean) => {
174
- advisorOutcomeLoggingRef = enabled;
175
- };
176
- export const setAdvisorUntrackedContentRef = (enabled: boolean) => {
177
- advisorUntrackedContentRef = enabled;
178
- };
179
- export const setAdvisorTrackedFileContentRef = (enabled: boolean) => {
180
- advisorTrackedFileContentRef = enabled;
181
- };
182
- export const setAdvisorScoutEnabledRef = (enabled: boolean) => {
183
- advisorScoutEnabledRef = enabled;
184
- };
185
- export const setShowUsageDetailsRef = (enabled: boolean) => {
186
- showUsageDetailsRef = enabled;
187
- };
188
- export const setShowUsageFooterRef = (enabled: boolean) => {
189
- showUsageFooterRef = enabled;
190
- };
191
-
192
- /**
193
- * Returns the current live settings state. Use this at UI boundaries instead of
194
- * imported mutable bindings, which can be snapshotted by extension loaders.
195
- */
196
- export const getAdvisorSettings = () => ({
197
- alwaysOn: alwaysOnRef,
198
- autoLoopGate: advisorAutoLoopGateRef,
199
- blockOnBlocked: advisorBlockOnBlockedRef,
200
- collapseResponses: advisorCollapseResponsesRef,
201
- completionGate: advisorCompletionGateRef,
202
- contextMaxChars: contextMaxCharsRef,
203
- customRule: advisorCustomInvocationRef,
204
- effort: advisorEffortRef,
205
- failureGate: advisorFailureGateRef,
206
- failureMode: advisorFailureModeRef,
207
- gitContext: advisorGitContextRef,
208
- gitContextMaxChars: advisorGitContextMaxCharsRef,
209
- herdrIntegration: advisorHerdrIntegrationRef,
210
- loopThreshold: advisorLoopThresholdRef,
211
- maxCallsPerSession: advisorMaxCallsPerSessionRef,
212
- outcomeLogging: advisorOutcomeLoggingRef,
213
- planGate: advisorPlanGateRef,
214
- redactSecrets: advisorRedactSecretsRef,
215
- scoutEnabled: advisorScoutEnabledRef,
216
- sessionSummary: advisorSessionSummaryRef,
217
- showUsageDetails: showUsageDetailsRef,
218
- showUsageFooter: showUsageFooterRef,
219
- simpleMode: simpleModeRef,
220
- toolPolicies: { ...advisorToolPoliciesRef },
221
- toolResultMaxBytes: advisorToolResultMaxBytesRef,
222
- toolResultMaxLines: advisorToolResultMaxLinesRef,
223
- trackedFileContent: advisorTrackedFileContentRef,
224
- untrackedContent: advisorUntrackedContentRef,
225
- });
226
-
227
- /** Reads the live session budget at each enforcement boundary. */
228
- export const getAdvisorMaxCallsPerSession = () =>
229
- getAdvisorSettings().maxCallsPerSession;
230
-
231
- export const splitRef = (ref: string): [string, string] => {
232
- const i = ref.indexOf("/");
233
- return i === -1 ? ["openai-codex", ref] : [ref.slice(0, i), ref.slice(i + 1)];
234
- };
235
-
236
- export const configPaths = (ctx: ExtensionContext) => [
237
- ctx.isProjectTrusted()
238
- ? join(ctx.cwd, CONFIG_DIR_NAME, "advisor.json")
239
- : null,
240
- join(getAgentDir(), "advisor.json"),
241
- ];
242
-
243
- export interface AdvisorConfig {
244
- advisor?: string;
245
- advisorAutoLoopGate?: boolean;
246
- advisorBlockOnBlocked?: boolean;
247
- advisorCollapseResponses?: boolean;
248
- advisorCompletionGate?: boolean;
249
- advisorCustomInvocation?: string;
250
- advisorEffort?: string;
251
- advisorFailureGate?: boolean;
252
- advisorGitContext?: GitContextLevel;
253
- advisorGitContextMaxChars?: number;
254
- advisorHerdrIntegration?: boolean;
255
- advisorLoopThreshold?: number;
256
- advisorMaxCallsPerSession?: number;
257
- advisorOutcomeLogging?: boolean;
258
- advisorPlanGate?: boolean;
259
- advisorRedactSecrets?: boolean;
260
- advisorScoutEnabled?: boolean;
261
- advisorSessionSummary?: boolean;
262
- advisorToolPolicies?: AdvisorToolPolicies;
263
- advisorToolResultMaxBytes?: number;
264
- advisorToolResultMaxLines?: number;
265
- advisorTrackedFileContent?: boolean;
266
- advisorUntrackedContent?: boolean;
267
- alwaysOn?: boolean;
268
- contextMaxChars?: number;
269
- executor?: string;
270
- executorEffort?: string;
271
- gateFailureMode?: GateFailureMode;
272
- showUsageDetails?: boolean;
273
- showUsageFooter?: boolean;
274
- simpleMode?: boolean;
275
- }
276
-
277
- const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
278
- "advisor",
279
- "advisorAutoLoopGate",
280
- "advisorBlockOnBlocked",
281
- "advisorCollapseResponses",
282
- "advisorCompletionGate",
283
- "advisorCustomInvocation",
284
- "advisorEffort",
285
- "advisorFailureGate",
286
- "advisorGitContext",
287
- "advisorGitContextMaxChars",
288
- "advisorHerdrIntegration",
289
- "advisorLoopThreshold",
290
- "advisorMaxCallsPerSession",
291
- "advisorPlanGate",
292
- "advisorSessionSummary",
293
- "advisorScoutEnabled",
294
- "showUsageDetails",
295
- "simpleMode",
296
- "alwaysOn",
297
- "advisorToolResultMaxBytes",
298
- "advisorToolResultMaxLines",
299
- "advisorTrackedFileContent",
300
- "advisorRedactSecrets",
301
- "advisorUntrackedContent",
302
- "advisorOutcomeLogging",
303
- "advisorToolPolicies",
304
- "contextMaxChars",
305
- "executor",
306
- "executorEffort",
307
- "gateFailureMode",
308
- "showUsageFooter",
309
- ]);
310
- const BOOLEAN_CONFIG_KEYS = [
311
- "advisorPlanGate",
312
- "advisorFailureGate",
313
- "advisorCompletionGate",
314
- "advisorCollapseResponses",
315
- "advisorBlockOnBlocked",
316
- "advisorAutoLoopGate",
317
- "advisorSessionSummary",
318
- "advisorScoutEnabled",
319
- "showUsageDetails",
320
- "showUsageFooter",
321
- "simpleMode",
322
- "alwaysOn",
323
- "advisorHerdrIntegration",
324
- "advisorTrackedFileContent",
325
- "advisorRedactSecrets",
326
- "advisorUntrackedContent",
327
- "advisorOutcomeLogging",
328
- ] as const;
329
- const STRING_CONFIG_KEYS = [
330
- "executor",
331
- "advisor",
332
- "executorEffort",
333
- "advisorEffort",
334
- "advisorCustomInvocation",
335
- ] as const;
336
- const ARGUMENT_WHITESPACE = /\s+/;
337
-
338
- type ConfigRecord = Record<string, unknown>;
339
-
340
- const invalidConfigValue = (
341
- path: string,
342
- key: string,
343
- accepted: string
344
- ): never => {
345
- throw new TypeError(
346
- `Invalid advisor configuration at ${path}, key ${JSON.stringify(key)}: expected ${accepted}.`
347
- );
348
- };
349
-
350
- const unknownConfigKeys = (config: ConfigRecord) =>
351
- Object.keys(config).filter(
352
- (key) => !CONFIG_KEYS.has(key as keyof AdvisorConfig)
353
- );
354
-
355
- const validateStringValues = (config: ConfigRecord, path: string) => {
356
- for (const key of STRING_CONFIG_KEYS) {
357
- if (config[key] !== undefined && typeof config[key] !== "string") {
358
- invalidConfigValue(
359
- path,
360
- key,
361
- key === "executor" || key === "advisor"
362
- ? "a provider/model string"
363
- : "a string"
364
- );
365
- }
366
- }
367
- };
368
-
369
- const validateBooleanValues = (config: ConfigRecord, path: string) => {
370
- for (const key of BOOLEAN_CONFIG_KEYS) {
371
- if (config[key] !== undefined && typeof config[key] !== "boolean") {
372
- invalidConfigValue(path, key, "true or false");
373
- }
374
- }
375
- };
376
-
377
- export const isValidAdvisorToolPolicies = (
378
- value: unknown
379
- ): value is AdvisorToolPolicies => {
380
- if (!value || typeof value !== "object" || Array.isArray(value)) {
381
- return false;
382
- }
383
- return Object.entries(value).every(
384
- ([toolName, policy]) =>
385
- toolName.trim().length > 0 &&
386
- typeof policy === "string" &&
387
- ADVISOR_TOOL_POLICIES.includes(policy as AdvisorToolPolicy)
388
- );
389
- };
390
-
391
- const validateNumericValues = (config: ConfigRecord, path: string) => {
392
- const numericRules: [
393
- keyof AdvisorConfig,
394
- (value: unknown) => boolean,
395
- string,
396
- ][] = [
397
- [
398
- "contextMaxChars",
399
- isValidContextMaxChars,
400
- `a safe integer from 0 through ${MAX_CONTEXT_MAX_CHARS}`,
401
- ],
402
- [
403
- "advisorLoopThreshold",
404
- isValidLoopThreshold,
405
- "a safe integer of at least 2",
406
- ],
407
- [
408
- "advisorMaxCallsPerSession",
409
- isValidMaxCallsPerSession,
410
- "a non-negative safe integer",
411
- ],
412
- [
413
- "advisorToolResultMaxLines",
414
- isValidToolResultMaxLines,
415
- "a non-negative safe integer",
416
- ],
417
- [
418
- "advisorToolResultMaxBytes",
419
- isValidToolResultMaxBytes,
420
- "a non-negative safe integer",
421
- ],
422
- [
423
- "advisorGitContextMaxChars",
424
- isValidToolResultMaxBytes,
425
- "a non-negative safe integer",
426
- ],
427
- ];
428
- for (const [key, isValid, description] of numericRules) {
429
- if (config[key] !== undefined && !isValid(config[key])) {
430
- invalidConfigValue(path, key, description);
431
- }
432
- }
433
- };
434
-
435
- export const validateConfig = (
436
- value: unknown,
437
- path = "advisor.json"
438
- ): value is AdvisorConfig => {
439
- if (!value || typeof value !== "object" || Array.isArray(value)) {
440
- throw new TypeError(
441
- `Invalid advisor configuration at ${path}: expected a JSON object.`
442
- );
443
- }
444
- const config = value as ConfigRecord;
445
- validateStringValues(config, path);
446
- validateBooleanValues(config, path);
447
- validateNumericValues(config, path);
448
- if (
449
- config.advisorToolPolicies !== undefined &&
450
- !isValidAdvisorToolPolicies(config.advisorToolPolicies)
451
- ) {
452
- invalidConfigValue(
453
- path,
454
- "advisorToolPolicies",
455
- "a JSON object with non-empty tool names and full, summary, or exclude values"
456
- );
457
- }
458
- if (
459
- config.gateFailureMode !== undefined &&
460
- !isValidGateFailureMode(config.gateFailureMode)
461
- ) {
462
- invalidConfigValue(path, "gateFailureMode", GATE_FAILURE_MODES.join(", "));
463
- }
464
- if (
465
- config.advisorGitContext !== undefined &&
466
- !isValidGitContextLevel(config.advisorGitContext)
467
- ) {
468
- invalidConfigValue(
469
- path,
470
- "advisorGitContext",
471
- GIT_CONTEXT_LEVELS.join(", ")
472
- );
473
- }
474
- return true;
475
- };
476
-
477
- const resetDefaults = () => {
478
- executorRef = "";
479
- advisorRef = "";
480
- persistedExecutorRef = undefined;
481
- persistedAdvisorRef = undefined;
482
- executorEffortRef = undefined;
483
- advisorEffortRef = undefined;
484
- contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
485
- advisorPlanGateRef = true;
486
- advisorFailureGateRef = true;
487
- advisorCompletionGateRef = true;
488
- advisorCustomInvocationRef = undefined;
489
- advisorCollapseResponsesRef = false;
490
- advisorBlockOnBlockedRef = true;
491
- advisorAutoLoopGateRef = true;
492
- advisorLoopThresholdRef = 3;
493
- advisorMaxCallsPerSessionRef = undefined;
494
- advisorSessionSummaryRef = false;
495
- simpleModeRef = false;
496
- alwaysOnRef = false;
497
- advisorFailureModeRef = "block-session";
498
- advisorHerdrIntegrationRef = true;
499
- advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
500
- advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
501
- advisorRedactSecretsRef = false;
502
- advisorGitContextRef = "summary";
503
- advisorGitContextMaxCharsRef = DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS;
504
- advisorToolPoliciesRef = {};
505
- advisorOutcomeLoggingRef = false;
506
- advisorUntrackedContentRef = false;
507
- advisorTrackedFileContentRef = false;
508
- advisorScoutEnabledRef = false;
509
- showUsageDetailsRef = true;
510
- showUsageFooterRef = false;
511
- };
512
-
513
- const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
514
- config: AdvisorConfig,
515
- key: Key,
516
- apply: (value: NonNullable<AdvisorConfig[Key]>) => void
517
- ) => {
518
- const value = config[key];
519
- if (value !== undefined) {
520
- apply(value as NonNullable<AdvisorConfig[Key]>);
521
- }
522
- };
523
-
524
- const applyNonEmptyStringConfig = (
525
- value: string | undefined,
526
- apply: (value: string) => void
527
- ) => {
528
- if (value) {
529
- apply(value);
530
- }
531
- };
532
-
533
- const configuredModelRef = (value: string | undefined): string | undefined =>
534
- value?.trim() || undefined;
535
-
536
- const applyConfig = (config: AdvisorConfig) => {
537
- applyNonEmptyStringConfig(config.executor, setExecutorRef);
538
- applyNonEmptyStringConfig(config.advisor, setAdvisorRef);
539
- applyNonEmptyStringConfig(config.executorEffort, setExecutorEffortRef);
540
- applyNonEmptyStringConfig(config.advisorEffort, setAdvisorEffortRef);
541
- applyOptionalConfig(config, "contextMaxChars", setContextMaxCharsRef);
542
- applyOptionalConfig(config, "advisorPlanGate", setAdvisorPlanGateRef);
543
- applyOptionalConfig(config, "advisorFailureGate", setAdvisorFailureGateRef);
544
- applyOptionalConfig(
545
- config,
546
- "advisorCompletionGate",
547
- setAdvisorCompletionGateRef
548
- );
549
- applyOptionalConfig(
550
- config,
551
- "advisorCustomInvocation",
552
- setAdvisorCustomInvocationRef
553
- );
554
- applyOptionalConfig(
555
- config,
556
- "advisorCollapseResponses",
557
- setAdvisorCollapseResponsesRef
558
- );
559
- applyOptionalConfig(
560
- config,
561
- "advisorBlockOnBlocked",
562
- setAdvisorBlockOnBlockedRef
563
- );
564
- applyOptionalConfig(config, "advisorAutoLoopGate", setAdvisorAutoLoopGateRef);
565
- applyOptionalConfig(
566
- config,
567
- "advisorLoopThreshold",
568
- setAdvisorLoopThresholdRef
569
- );
570
- applyOptionalConfig(
571
- config,
572
- "advisorMaxCallsPerSession",
573
- setAdvisorMaxCallsPerSessionRef
574
- );
575
- applyOptionalConfig(
576
- config,
577
- "advisorSessionSummary",
578
- setAdvisorSessionSummaryRef
579
- );
580
- applyOptionalConfig(config, "advisorScoutEnabled", setAdvisorScoutEnabledRef);
581
- applyOptionalConfig(config, "showUsageDetails", setShowUsageDetailsRef);
582
- applyOptionalConfig(config, "showUsageFooter", setShowUsageFooterRef);
583
- applyOptionalConfig(config, "simpleMode", setSimpleModeRef);
584
- applyOptionalConfig(config, "alwaysOn", setAlwaysOnRef);
585
- applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
586
- applyOptionalConfig(
587
- config,
588
- "advisorHerdrIntegration",
589
- setAdvisorHerdrIntegrationRef
590
- );
591
- applyOptionalConfig(
592
- config,
593
- "advisorToolResultMaxLines",
594
- setAdvisorToolResultMaxLinesRef
595
- );
596
- applyOptionalConfig(
597
- config,
598
- "advisorToolResultMaxBytes",
599
- setAdvisorToolResultMaxBytesRef
600
- );
601
- applyOptionalConfig(
602
- config,
603
- "advisorRedactSecrets",
604
- setAdvisorRedactSecretsRef
605
- );
606
- applyOptionalConfig(config, "advisorGitContext", setAdvisorGitContextRef);
607
- applyOptionalConfig(
608
- config,
609
- "advisorGitContextMaxChars",
610
- setAdvisorGitContextMaxCharsRef
611
- );
612
- applyOptionalConfig(config, "advisorToolPolicies", setAdvisorToolPoliciesRef);
613
- applyOptionalConfig(
614
- config,
615
- "advisorUntrackedContent",
616
- setAdvisorUntrackedContentRef
617
- );
618
- applyOptionalConfig(
619
- config,
620
- "advisorTrackedFileContent",
621
- setAdvisorTrackedFileContentRef
622
- );
623
- };
624
-
625
- const readConfig = (path: string): AdvisorConfig => {
626
- try {
627
- const config = JSON.parse(readFileSync(path, "utf8"));
628
- validateConfig(config, path);
629
- return config;
630
- } catch (error) {
631
- throw error instanceof Error ? error : new Error(String(error));
632
- }
633
- };
634
-
635
- // loadConfig runs on every tool call and every consultation. Caching the parsed
636
- // file by path and stat identity removes that read and parse from the hot path
637
- // while still applying the full reset-then-apply sequence on each call.
638
- const configCache = new Map<
639
- string,
640
- { config: AdvisorConfig; identity: string }
641
- >();
642
- const warnedUnknownConfigIdentities = new Set<string>();
643
-
644
- /** Drops the parsed-configuration cache; the next load re-reads from disk. */
645
- export const resetConfigCache = () => {
646
- configCache.clear();
647
- warnedUnknownConfigIdentities.clear();
648
- };
649
-
650
- /**
651
- * Nanosecond timestamps plus size and inode so a same-millisecond or
652
- * same-size external rewrite cannot be served from the cache. An unstattable
653
- * path yields a unique value, which always forces a re-read.
654
- */
655
- const configIdentity = (path: string): string => {
656
- try {
657
- const stats = statSync(path, { bigint: true });
658
- return `${stats.mtimeNs}:${stats.ctimeNs}:${stats.size}:${stats.ino}:${stats.dev}`;
659
- } catch {
660
- return `unstattable:${process.hrtime.bigint()}`;
661
- }
662
- };
663
-
664
- const readConfigCached = (path: string): AdvisorConfig => {
665
- const identity = configIdentity(path);
666
- const cached = configCache.get(path);
667
- if (cached?.identity === identity) {
668
- return cached.config;
669
- }
670
- const config = readConfig(path);
671
- configCache.set(path, { config, identity });
672
- return config;
673
- };
674
-
675
- export const loadConfig = (_ctx: ExtensionContext) => {
676
- resetDefaults();
677
- const global = join(getAgentDir(), "advisor.json");
678
- const globalConfig = existsSync(global)
679
- ? readConfigCached(global)
680
- : undefined;
681
- persistedExecutorRef = configuredModelRef(globalConfig?.executor);
682
- persistedAdvisorRef = configuredModelRef(globalConfig?.advisor);
683
- if (globalConfig) {
684
- applyConfig(globalConfig);
685
- const unknownKeys = unknownConfigKeys(globalConfig as ConfigRecord);
686
- const warningIdentity = `${global}:${configIdentity(global)}`;
687
- if (
688
- unknownKeys.length > 0 &&
689
- _ctx.hasUI &&
690
- !warnedUnknownConfigIdentities.has(warningIdentity)
691
- ) {
692
- _ctx.ui.notify(
693
- `Advisor configuration at ${global} contains unrecognized key(s) ${unknownKeys.map((key) => JSON.stringify(key)).join(", ")}. They were preserved but ignored; check for typos or upgrade pi-advisor.`,
694
- "warning"
695
- );
696
- warnedUnknownConfigIdentities.add(warningIdentity);
697
- }
698
- }
699
- // Repository-controlled project configuration is never applied. Models,
700
- // prompts, gates, budgets, disclosure, redaction, integrations, and consent
701
- // remain under the user's global Pi configuration.
702
- advisorOutcomeLoggingRef = globalConfig?.advisorOutcomeLogging === true;
703
- return existsSync(global) ? global : null;
704
- };
705
-
706
- /** Saves user-controlled configuration globally without outcome consent. */
707
- export interface SaveConfigOptions {
708
- persistAdvisor?: boolean;
709
- persistExecutor?: boolean;
710
- }
711
-
712
- export const saveConfig = (
713
- _ctx: ExtensionContext,
714
- options: SaveConfigOptions = {}
715
- ) => {
716
- const path = join(getAgentDir(), "advisor.json");
717
- const persistAdvisor = options.persistAdvisor ?? true;
718
- const persistExecutor = options.persistExecutor ?? true;
719
- let existing: Record<string, unknown> = {};
720
- try {
721
- const parsed = JSON.parse(readFileSync(path, "utf8"));
722
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
723
- existing = parsed as Record<string, unknown>;
724
- }
725
- } catch {
726
- /* replace a missing or malformed file */
727
- }
728
- if (advisorMaxCallsPerSessionRef === undefined) {
729
- existing.advisorMaxCallsPerSession = undefined;
730
- }
731
- const data = {
732
- ...existing,
733
- ...(persistAdvisor && advisorRef ? { advisor: advisorRef } : {}),
734
- advisorAutoLoopGate: advisorAutoLoopGateRef,
735
- advisorBlockOnBlocked: advisorBlockOnBlockedRef,
736
- advisorCollapseResponses: advisorCollapseResponsesRef,
737
- advisorCompletionGate: advisorCompletionGateRef,
738
- advisorCustomInvocation: advisorCustomInvocationRef,
739
- advisorEffort: advisorEffortRef,
740
- advisorFailureGate: advisorFailureGateRef,
741
- advisorLoopThreshold: advisorLoopThresholdRef,
742
- advisorPlanGate: advisorPlanGateRef,
743
- contextMaxChars: contextMaxCharsRef,
744
- ...(persistExecutor && executorRef ? { executor: executorRef } : {}),
745
- executorEffort: executorEffortRef,
746
- ...(advisorMaxCallsPerSessionRef === undefined
747
- ? {}
748
- : { advisorMaxCallsPerSession: advisorMaxCallsPerSessionRef }),
749
- advisorGitContext: advisorGitContextRef,
750
- advisorGitContextMaxChars: advisorGitContextMaxCharsRef,
751
- advisorHerdrIntegration: advisorHerdrIntegrationRef,
752
- advisorRedactSecrets: advisorRedactSecretsRef,
753
- advisorScoutEnabled: advisorScoutEnabledRef,
754
- advisorSessionSummary: advisorSessionSummaryRef,
755
- advisorToolPolicies: advisorToolPoliciesRef,
756
- advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
757
- advisorToolResultMaxLines: advisorToolResultMaxLinesRef,
758
- advisorTrackedFileContent: advisorTrackedFileContentRef,
759
- advisorUntrackedContent: advisorUntrackedContentRef,
760
- alwaysOn: alwaysOnRef,
761
- gateFailureMode: advisorFailureModeRef,
762
- showUsageDetails: showUsageDetailsRef,
763
- showUsageFooter: showUsageFooterRef,
764
- simpleMode: simpleModeRef,
765
- };
766
- writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
767
- resetConfigCache();
768
- return path;
769
- };
770
-
771
- /** Outcome logging is deliberately written only to the global Pi configuration. */
772
- export const saveGlobalOutcomeLogging = (enabled: boolean) => {
773
- const path = join(getAgentDir(), "advisor.json");
774
- let existing: Record<string, unknown> = {};
775
- try {
776
- const parsed = JSON.parse(readFileSync(path, "utf8"));
777
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
778
- existing = parsed as Record<string, unknown>;
779
- }
780
- } catch {
781
- /* create it */
782
- }
783
- writeFileSync(
784
- path,
785
- `${JSON.stringify({ ...existing, advisorOutcomeLogging: enabled }, null, 2)}\n`
786
- );
787
- resetConfigCache();
788
- return path;
789
- };
790
-
791
- export const parseArgs = (args: string): string | undefined => {
792
- let nextExecutor = executorRef;
793
- let nextAdvisor = advisorRef;
794
- let nextContextMaxChars = contextMaxCharsRef;
795
- for (const token of args.trim().split(ARGUMENT_WHITESPACE).filter(Boolean)) {
796
- const [key, value] = token.split("=");
797
- if (key === "executor" && value) {
798
- nextExecutor = value;
799
- }
800
- if (key === "advisor" && value) {
801
- nextAdvisor = value;
802
- }
803
- if (key === "contextMaxChars") {
804
- const parsed = Number(value);
805
- if (!isValidContextMaxChars(parsed)) {
806
- return `contextMaxChars must be a non-negative integer no greater than ${MAX_CONTEXT_MAX_CHARS}.`;
807
- }
808
- nextContextMaxChars = parsed;
809
- }
810
- }
811
- executorRef = nextExecutor;
812
- advisorRef = nextAdvisor;
813
- contextMaxCharsRef = nextContextMaxChars;
814
- };
1
+ // biome-ignore lint/performance/noBarrelFile: this facade intentionally preserves the public config module contract.
2
+ export { parseArgs } from "./config/args.js";
3
+ export {
4
+ advisorAutoLoopGateRef,
5
+ advisorBlockOnBlockedRef,
6
+ advisorCollapseResponsesRef,
7
+ advisorCompletionGateRef,
8
+ advisorCustomInvocationRef,
9
+ advisorEffortRef,
10
+ advisorFailureGateRef,
11
+ advisorFailureModeRef,
12
+ advisorGitContextMaxCharsRef,
13
+ advisorGitContextRef,
14
+ advisorHerdrIntegrationRef,
15
+ advisorLoopThresholdRef,
16
+ advisorMaxCallsPerSessionRef,
17
+ advisorOutcomeLoggingRef,
18
+ advisorPlanGateRef,
19
+ advisorRedactSecretsRef,
20
+ advisorRef,
21
+ advisorScoutEnabledRef,
22
+ advisorSessionSummaryRef,
23
+ advisorToolPoliciesRef,
24
+ advisorToolResultMaxBytesRef,
25
+ advisorToolResultMaxLinesRef,
26
+ advisorTrackedFileContentRef,
27
+ advisorUntrackedContentRef,
28
+ alwaysOnRef,
29
+ contextMaxCharsRef,
30
+ executorEffortRef,
31
+ executorRef,
32
+ getAdvisorMaxCallsPerSession,
33
+ getAdvisorSettings,
34
+ getPersistedModelRefs,
35
+ isSimpleMode,
36
+ setAdvisorAutoLoopGateRef,
37
+ setAdvisorBlockOnBlockedRef,
38
+ setAdvisorCollapseResponsesRef,
39
+ setAdvisorCompletionGateRef,
40
+ setAdvisorCustomInvocationRef,
41
+ setAdvisorEffortRef,
42
+ setAdvisorFailureGateRef,
43
+ setAdvisorFailureModeRef,
44
+ setAdvisorGitContextMaxCharsRef,
45
+ setAdvisorGitContextRef,
46
+ setAdvisorHerdrIntegrationRef,
47
+ setAdvisorLoopThresholdRef,
48
+ setAdvisorMaxCallsPerSessionRef,
49
+ setAdvisorOutcomeLoggingRef,
50
+ setAdvisorPlanGateRef,
51
+ setAdvisorRedactSecretsRef,
52
+ setAdvisorRef,
53
+ setAdvisorScoutEnabledRef,
54
+ setAdvisorSessionSummaryRef,
55
+ setAdvisorToolPoliciesRef,
56
+ setAdvisorToolResultMaxBytesRef,
57
+ setAdvisorToolResultMaxLinesRef,
58
+ setAdvisorTrackedFileContentRef,
59
+ setAdvisorUntrackedContentRef,
60
+ setAlwaysOnRef,
61
+ setContextMaxCharsRef,
62
+ setExecutorEffortRef,
63
+ setExecutorRef,
64
+ setShowUsageDetailsRef,
65
+ setShowUsageFooterRef,
66
+ setSimpleModeRef,
67
+ showUsageDetailsRef,
68
+ showUsageFooterRef,
69
+ simpleModeRef,
70
+ splitRef,
71
+ } from "./config/state.js";
72
+ export type { SaveConfigOptions } from "./config/storage.js";
73
+ export {
74
+ configPaths,
75
+ loadConfig,
76
+ resetConfigCache,
77
+ saveConfig,
78
+ saveGlobalOutcomeLogging,
79
+ } from "./config/storage.js";
80
+ export type {
81
+ AdvisorConfig,
82
+ AdvisorToolPolicies,
83
+ AdvisorToolPolicy,
84
+ GateFailureMode,
85
+ } from "./config/types.js";
86
+ export {
87
+ ADVISOR_TOOL_POLICIES,
88
+ DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS,
89
+ DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES,
90
+ DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
91
+ DEFAULT_CONTEXT_MAX_CHARS,
92
+ GATE_FAILURE_MODES,
93
+ MAX_CONTEXT_MAX_CHARS,
94
+ } from "./config/types.js";
95
+ export {
96
+ isValidAdvisorToolPolicies,
97
+ isValidContextMaxChars,
98
+ isValidGateFailureMode,
99
+ isValidLoopThreshold,
100
+ isValidMaxCallsPerSession,
101
+ isValidToolResultMaxBytes,
102
+ isValidToolResultMaxLines,
103
+ validateConfig,
104
+ } from "./config/validation.js";