pi-advisor-flow 0.5.1 → 0.5.3

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 +21 -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 +247 -0
  39. package/src/tools/register-lifecycle.ts +119 -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
@@ -0,0 +1,37 @@
1
+ import {
2
+ advisorRef,
3
+ contextMaxCharsRef,
4
+ executorRef,
5
+ setAdvisorRef,
6
+ setContextMaxCharsRef,
7
+ setExecutorRef,
8
+ } from "./state.js";
9
+ import { MAX_CONTEXT_MAX_CHARS } from "./types.js";
10
+ import { isValidContextMaxChars } from "./validation.js";
11
+
12
+ const ARGUMENT_WHITESPACE = /\s+/;
13
+
14
+ export const parseArgs = (args: string): string | undefined => {
15
+ let nextExecutor = executorRef;
16
+ let nextAdvisor = advisorRef;
17
+ let nextContextMaxChars = contextMaxCharsRef;
18
+ for (const token of args.trim().split(ARGUMENT_WHITESPACE).filter(Boolean)) {
19
+ const [key, value] = token.split("=");
20
+ if (key === "executor" && value) {
21
+ nextExecutor = value;
22
+ }
23
+ if (key === "advisor" && value) {
24
+ nextAdvisor = value;
25
+ }
26
+ if (key === "contextMaxChars") {
27
+ const parsed = Number(value);
28
+ if (!isValidContextMaxChars(parsed)) {
29
+ return `contextMaxChars must be a non-negative integer no greater than ${MAX_CONTEXT_MAX_CHARS}.`;
30
+ }
31
+ nextContextMaxChars = parsed;
32
+ }
33
+ }
34
+ setExecutorRef(nextExecutor);
35
+ setAdvisorRef(nextAdvisor);
36
+ setContextMaxCharsRef(nextContextMaxChars);
37
+ };
@@ -0,0 +1,190 @@
1
+ import {
2
+ setAdvisorAutoLoopGateRef,
3
+ setAdvisorBlockOnBlockedRef,
4
+ setAdvisorCollapseResponsesRef,
5
+ setAdvisorCompletionGateRef,
6
+ setAdvisorCustomInvocationRef,
7
+ setAdvisorEffortRef,
8
+ setAdvisorFailureGateRef,
9
+ setAdvisorFailureModeRef,
10
+ setAdvisorGitContextMaxCharsRef,
11
+ setAdvisorGitContextRef,
12
+ setAdvisorHerdrIntegrationRef,
13
+ setAdvisorLoopThresholdRef,
14
+ setAdvisorMaxCallsPerSessionRef,
15
+ setAdvisorOutcomeLoggingRef,
16
+ setAdvisorPlanGateRef,
17
+ setAdvisorRedactSecretsRef,
18
+ setAdvisorRef,
19
+ setAdvisorScoutEnabledRef,
20
+ setAdvisorSessionSummaryRef,
21
+ setAdvisorToolPoliciesRef,
22
+ setAdvisorToolResultMaxBytesRef,
23
+ setAdvisorToolResultMaxLinesRef,
24
+ setAdvisorTrackedFileContentRef,
25
+ setAdvisorUntrackedContentRef,
26
+ setAlwaysOnRef,
27
+ setContextMaxCharsRef,
28
+ setExecutorEffortRef,
29
+ setExecutorRef,
30
+ setPersistedModelRefs,
31
+ setShowUsageDetailsRef,
32
+ setShowUsageFooterRef,
33
+ setSimpleModeRef,
34
+ } from "./state.js";
35
+ import type { AdvisorConfig } from "./types.js";
36
+ import {
37
+ DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS,
38
+ DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES,
39
+ DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
40
+ DEFAULT_CONTEXT_MAX_CHARS,
41
+ } from "./types.js";
42
+
43
+ export const resetDefaults = () => {
44
+ setExecutorRef("");
45
+ setAdvisorRef("");
46
+ setPersistedModelRefs(undefined, undefined);
47
+ setExecutorEffortRef(undefined);
48
+ setAdvisorEffortRef(undefined);
49
+ setContextMaxCharsRef(DEFAULT_CONTEXT_MAX_CHARS);
50
+ setAdvisorPlanGateRef(true);
51
+ setAdvisorFailureGateRef(true);
52
+ setAdvisorCompletionGateRef(true);
53
+ setAdvisorCustomInvocationRef(undefined);
54
+ setAdvisorCollapseResponsesRef(false);
55
+ setAdvisorBlockOnBlockedRef(true);
56
+ setAdvisorAutoLoopGateRef(true);
57
+ setAdvisorLoopThresholdRef(3);
58
+ setAdvisorMaxCallsPerSessionRef(undefined);
59
+ setAdvisorSessionSummaryRef(false);
60
+ setSimpleModeRef(false);
61
+ setAlwaysOnRef(false);
62
+ setAdvisorFailureModeRef("block-session");
63
+ setAdvisorHerdrIntegrationRef(true);
64
+ setAdvisorToolResultMaxLinesRef(DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES);
65
+ setAdvisorToolResultMaxBytesRef(DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES);
66
+ setAdvisorRedactSecretsRef(false);
67
+ setAdvisorGitContextRef("summary");
68
+ setAdvisorGitContextMaxCharsRef(DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS);
69
+ setAdvisorToolPoliciesRef({});
70
+ setAdvisorOutcomeLoggingRef(false);
71
+ setAdvisorUntrackedContentRef(false);
72
+ setAdvisorTrackedFileContentRef(false);
73
+ setAdvisorScoutEnabledRef(false);
74
+ setShowUsageDetailsRef(true);
75
+ setShowUsageFooterRef(false);
76
+ };
77
+
78
+ const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
79
+ config: AdvisorConfig,
80
+ key: Key,
81
+ apply: (value: NonNullable<AdvisorConfig[Key]>) => void
82
+ ) => {
83
+ const value = config[key];
84
+ if (value !== undefined) {
85
+ apply(value as NonNullable<AdvisorConfig[Key]>);
86
+ }
87
+ };
88
+
89
+ const applyNonEmptyStringConfig = (
90
+ value: string | undefined,
91
+ apply: (value: string) => void
92
+ ) => {
93
+ if (value) {
94
+ apply(value);
95
+ }
96
+ };
97
+
98
+ export const applyConfig = (config: AdvisorConfig) => {
99
+ applyNonEmptyStringConfig(config.executor, setExecutorRef);
100
+ applyNonEmptyStringConfig(config.advisor, setAdvisorRef);
101
+ applyNonEmptyStringConfig(config.executorEffort, setExecutorEffortRef);
102
+ applyNonEmptyStringConfig(config.advisorEffort, setAdvisorEffortRef);
103
+ applyOptionalConfig(config, "contextMaxChars", setContextMaxCharsRef);
104
+ applyOptionalConfig(config, "advisorPlanGate", setAdvisorPlanGateRef);
105
+ applyOptionalConfig(config, "advisorFailureGate", setAdvisorFailureGateRef);
106
+ applyOptionalConfig(
107
+ config,
108
+ "advisorCompletionGate",
109
+ setAdvisorCompletionGateRef
110
+ );
111
+ applyOptionalConfig(
112
+ config,
113
+ "advisorCustomInvocation",
114
+ setAdvisorCustomInvocationRef
115
+ );
116
+ applyOptionalConfig(
117
+ config,
118
+ "advisorCollapseResponses",
119
+ setAdvisorCollapseResponsesRef
120
+ );
121
+ applyOptionalConfig(
122
+ config,
123
+ "advisorBlockOnBlocked",
124
+ setAdvisorBlockOnBlockedRef
125
+ );
126
+ applyOptionalConfig(config, "advisorAutoLoopGate", setAdvisorAutoLoopGateRef);
127
+ applyOptionalConfig(
128
+ config,
129
+ "advisorLoopThreshold",
130
+ setAdvisorLoopThresholdRef
131
+ );
132
+ applyOptionalConfig(
133
+ config,
134
+ "advisorMaxCallsPerSession",
135
+ setAdvisorMaxCallsPerSessionRef
136
+ );
137
+ applyOptionalConfig(
138
+ config,
139
+ "advisorSessionSummary",
140
+ setAdvisorSessionSummaryRef
141
+ );
142
+ applyOptionalConfig(config, "advisorScoutEnabled", setAdvisorScoutEnabledRef);
143
+ applyOptionalConfig(config, "showUsageDetails", setShowUsageDetailsRef);
144
+ applyOptionalConfig(config, "showUsageFooter", setShowUsageFooterRef);
145
+ applyOptionalConfig(config, "simpleMode", setSimpleModeRef);
146
+ applyOptionalConfig(config, "alwaysOn", setAlwaysOnRef);
147
+ applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
148
+ applyOptionalConfig(
149
+ config,
150
+ "advisorHerdrIntegration",
151
+ setAdvisorHerdrIntegrationRef
152
+ );
153
+ applyOptionalConfig(
154
+ config,
155
+ "advisorToolResultMaxLines",
156
+ setAdvisorToolResultMaxLinesRef
157
+ );
158
+ applyOptionalConfig(
159
+ config,
160
+ "advisorToolResultMaxBytes",
161
+ setAdvisorToolResultMaxBytesRef
162
+ );
163
+ applyOptionalConfig(
164
+ config,
165
+ "advisorRedactSecrets",
166
+ setAdvisorRedactSecretsRef
167
+ );
168
+ applyOptionalConfig(config, "advisorGitContext", setAdvisorGitContextRef);
169
+ applyOptionalConfig(
170
+ config,
171
+ "advisorGitContextMaxChars",
172
+ setAdvisorGitContextMaxCharsRef
173
+ );
174
+ applyOptionalConfig(config, "advisorToolPolicies", setAdvisorToolPoliciesRef);
175
+ applyOptionalConfig(
176
+ config,
177
+ "advisorUntrackedContent",
178
+ setAdvisorUntrackedContentRef
179
+ );
180
+ applyOptionalConfig(
181
+ config,
182
+ "advisorTrackedFileContent",
183
+ setAdvisorTrackedFileContentRef
184
+ );
185
+ applyOptionalConfig(
186
+ config,
187
+ "advisorOutcomeLogging",
188
+ setAdvisorOutcomeLoggingRef
189
+ );
190
+ };
@@ -0,0 +1,199 @@
1
+ import type { GitContextLevel } from "../git.js";
2
+ import {
3
+ type AdvisorToolPolicies,
4
+ DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS,
5
+ DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES,
6
+ DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
7
+ DEFAULT_CONTEXT_MAX_CHARS,
8
+ type GateFailureMode,
9
+ } from "./types.js";
10
+
11
+ // An empty ref means no model has been selected yet.
12
+ export let executorRef = "";
13
+ export let advisorRef = "";
14
+ let persistedExecutorRef: string | undefined;
15
+ let persistedAdvisorRef: string | undefined;
16
+ export let executorEffortRef: string | undefined;
17
+ export let advisorEffortRef: string | undefined;
18
+ export let contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
19
+ export let advisorPlanGateRef = true;
20
+ export let advisorFailureGateRef = true;
21
+ export let advisorCompletionGateRef = true;
22
+ export let advisorCustomInvocationRef: string | undefined;
23
+ export let advisorCollapseResponsesRef = false;
24
+ export let advisorBlockOnBlockedRef = true;
25
+ export let advisorAutoLoopGateRef = true;
26
+ export let advisorLoopThresholdRef = 3;
27
+ export let advisorMaxCallsPerSessionRef: number | undefined;
28
+ export let advisorSessionSummaryRef = false;
29
+ export let simpleModeRef = false;
30
+ export let alwaysOnRef = false;
31
+ export let advisorFailureModeRef: GateFailureMode = "block-session";
32
+ export let advisorHerdrIntegrationRef = true;
33
+ export let advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
34
+ export let advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
35
+ export let advisorRedactSecretsRef = false;
36
+ export let advisorGitContextRef: GitContextLevel = "summary";
37
+ export let advisorGitContextMaxCharsRef = DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS;
38
+ export let advisorToolPoliciesRef: AdvisorToolPolicies = {};
39
+ export let advisorOutcomeLoggingRef = false;
40
+ export let advisorUntrackedContentRef = false;
41
+ export let advisorTrackedFileContentRef = false;
42
+ export let advisorScoutEnabledRef = false;
43
+ export let showUsageDetailsRef = true;
44
+ export let showUsageFooterRef = false;
45
+
46
+ export const setExecutorRef = (ref: string) => {
47
+ executorRef = ref;
48
+ };
49
+ export const setAdvisorRef = (ref: string) => {
50
+ advisorRef = ref;
51
+ };
52
+ /** Returns model refs explicitly persisted in the global Advisor config. */
53
+ export const getPersistedModelRefs = () => ({
54
+ advisor: persistedAdvisorRef,
55
+ executor: persistedExecutorRef,
56
+ });
57
+
58
+ /** Updates persisted model refs without exposing the mutation in the facade. */
59
+ export const setPersistedModelRefs = (
60
+ advisor: string | undefined,
61
+ executor: string | undefined
62
+ ) => {
63
+ persistedAdvisorRef = advisor;
64
+ persistedExecutorRef = executor;
65
+ };
66
+
67
+ export const setExecutorEffortRef = (effort: string | undefined) => {
68
+ executorEffortRef = effort;
69
+ };
70
+ export const setAdvisorEffortRef = (effort: string | undefined) => {
71
+ advisorEffortRef = effort;
72
+ };
73
+ export const setContextMaxCharsRef = (value: number) => {
74
+ contextMaxCharsRef = value;
75
+ };
76
+ export const setAdvisorPlanGateRef = (enabled: boolean) => {
77
+ advisorPlanGateRef = enabled;
78
+ };
79
+ export const setAdvisorFailureGateRef = (enabled: boolean) => {
80
+ advisorFailureGateRef = enabled;
81
+ };
82
+ export const setAdvisorCompletionGateRef = (enabled: boolean) => {
83
+ advisorCompletionGateRef = enabled;
84
+ };
85
+ export const setAdvisorCustomInvocationRef = (rule: string | undefined) => {
86
+ advisorCustomInvocationRef = rule?.trim() || undefined;
87
+ };
88
+ export const setAdvisorCollapseResponsesRef = (enabled: boolean) => {
89
+ advisorCollapseResponsesRef = enabled;
90
+ };
91
+ export const setAdvisorBlockOnBlockedRef = (enabled: boolean) => {
92
+ advisorBlockOnBlockedRef = enabled;
93
+ };
94
+ export const setAdvisorAutoLoopGateRef = (enabled: boolean) => {
95
+ advisorAutoLoopGateRef = enabled;
96
+ };
97
+ export const setAdvisorLoopThresholdRef = (value: number) => {
98
+ advisorLoopThresholdRef = value;
99
+ };
100
+ export const setAdvisorMaxCallsPerSessionRef = (value: number | undefined) => {
101
+ advisorMaxCallsPerSessionRef = value;
102
+ };
103
+ export const setAdvisorSessionSummaryRef = (enabled: boolean) => {
104
+ advisorSessionSummaryRef = enabled;
105
+ };
106
+ export const setSimpleModeRef = (enabled: boolean) => {
107
+ simpleModeRef = enabled;
108
+ };
109
+ export const setAlwaysOnRef = (enabled: boolean) => {
110
+ alwaysOnRef = enabled;
111
+ };
112
+ /** Whether advanced Advisor automation is suppressed for this session. */
113
+ export const isSimpleMode = () => simpleModeRef;
114
+ export const setAdvisorFailureModeRef = (value: GateFailureMode) => {
115
+ advisorFailureModeRef = value;
116
+ };
117
+ export const setAdvisorHerdrIntegrationRef = (enabled: boolean) => {
118
+ advisorHerdrIntegrationRef = enabled;
119
+ };
120
+ export const setAdvisorToolResultMaxLinesRef = (value: number) => {
121
+ advisorToolResultMaxLinesRef = value;
122
+ };
123
+ export const setAdvisorToolResultMaxBytesRef = (value: number) => {
124
+ advisorToolResultMaxBytesRef = value;
125
+ };
126
+ export const setAdvisorRedactSecretsRef = (enabled: boolean) => {
127
+ advisorRedactSecretsRef = enabled;
128
+ };
129
+ export const setAdvisorGitContextRef = (level: GitContextLevel) => {
130
+ advisorGitContextRef = level;
131
+ };
132
+ export const setAdvisorGitContextMaxCharsRef = (value: number) => {
133
+ advisorGitContextMaxCharsRef = value;
134
+ };
135
+ export const setAdvisorToolPoliciesRef = (policies: AdvisorToolPolicies) => {
136
+ advisorToolPoliciesRef = { ...policies };
137
+ };
138
+ export const setAdvisorOutcomeLoggingRef = (enabled: boolean) => {
139
+ advisorOutcomeLoggingRef = enabled;
140
+ };
141
+ export const setAdvisorUntrackedContentRef = (enabled: boolean) => {
142
+ advisorUntrackedContentRef = enabled;
143
+ };
144
+ export const setAdvisorTrackedFileContentRef = (enabled: boolean) => {
145
+ advisorTrackedFileContentRef = enabled;
146
+ };
147
+ export const setAdvisorScoutEnabledRef = (enabled: boolean) => {
148
+ advisorScoutEnabledRef = enabled;
149
+ };
150
+ export const setShowUsageDetailsRef = (enabled: boolean) => {
151
+ showUsageDetailsRef = enabled;
152
+ };
153
+ export const setShowUsageFooterRef = (enabled: boolean) => {
154
+ showUsageFooterRef = enabled;
155
+ };
156
+
157
+ /**
158
+ * Returns the current live settings state. Use this at UI boundaries instead of
159
+ * imported mutable bindings, which can be snapshotted by extension loaders.
160
+ */
161
+ export const getAdvisorSettings = () => ({
162
+ alwaysOn: alwaysOnRef,
163
+ autoLoopGate: advisorAutoLoopGateRef,
164
+ blockOnBlocked: advisorBlockOnBlockedRef,
165
+ collapseResponses: advisorCollapseResponsesRef,
166
+ completionGate: advisorCompletionGateRef,
167
+ contextMaxChars: contextMaxCharsRef,
168
+ customRule: advisorCustomInvocationRef,
169
+ effort: advisorEffortRef,
170
+ failureGate: advisorFailureGateRef,
171
+ failureMode: advisorFailureModeRef,
172
+ gitContext: advisorGitContextRef,
173
+ gitContextMaxChars: advisorGitContextMaxCharsRef,
174
+ herdrIntegration: advisorHerdrIntegrationRef,
175
+ loopThreshold: advisorLoopThresholdRef,
176
+ maxCallsPerSession: advisorMaxCallsPerSessionRef,
177
+ outcomeLogging: advisorOutcomeLoggingRef,
178
+ planGate: advisorPlanGateRef,
179
+ redactSecrets: advisorRedactSecretsRef,
180
+ scoutEnabled: advisorScoutEnabledRef,
181
+ sessionSummary: advisorSessionSummaryRef,
182
+ showUsageDetails: showUsageDetailsRef,
183
+ showUsageFooter: showUsageFooterRef,
184
+ simpleMode: simpleModeRef,
185
+ toolPolicies: { ...advisorToolPoliciesRef },
186
+ toolResultMaxBytes: advisorToolResultMaxBytesRef,
187
+ toolResultMaxLines: advisorToolResultMaxLinesRef,
188
+ trackedFileContent: advisorTrackedFileContentRef,
189
+ untrackedContent: advisorUntrackedContentRef,
190
+ });
191
+
192
+ /** Reads the live session budget at each enforcement boundary. */
193
+ export const getAdvisorMaxCallsPerSession = () =>
194
+ getAdvisorSettings().maxCallsPerSession;
195
+
196
+ export const splitRef = (ref: string): [string, string] => {
197
+ const i = ref.indexOf("/");
198
+ return i === -1 ? ["openai-codex", ref] : [ref.slice(0, i), ref.slice(i + 1)];
199
+ };
@@ -0,0 +1,224 @@
1
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ CONFIG_DIR_NAME,
5
+ type ExtensionContext,
6
+ getAgentDir,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import { applyConfig, resetDefaults } from "./defaults.js";
9
+ import {
10
+ advisorAutoLoopGateRef,
11
+ advisorBlockOnBlockedRef,
12
+ advisorCollapseResponsesRef,
13
+ advisorCompletionGateRef,
14
+ advisorCustomInvocationRef,
15
+ advisorEffortRef,
16
+ advisorFailureGateRef,
17
+ advisorFailureModeRef,
18
+ advisorGitContextMaxCharsRef,
19
+ advisorGitContextRef,
20
+ advisorHerdrIntegrationRef,
21
+ advisorLoopThresholdRef,
22
+ advisorMaxCallsPerSessionRef,
23
+ advisorPlanGateRef,
24
+ advisorRedactSecretsRef,
25
+ advisorRef,
26
+ advisorScoutEnabledRef,
27
+ advisorSessionSummaryRef,
28
+ advisorToolPoliciesRef,
29
+ advisorToolResultMaxBytesRef,
30
+ advisorToolResultMaxLinesRef,
31
+ advisorTrackedFileContentRef,
32
+ advisorUntrackedContentRef,
33
+ alwaysOnRef,
34
+ contextMaxCharsRef,
35
+ executorEffortRef,
36
+ executorRef,
37
+ setAdvisorOutcomeLoggingRef,
38
+ setPersistedModelRefs,
39
+ showUsageDetailsRef,
40
+ showUsageFooterRef,
41
+ simpleModeRef,
42
+ } from "./state.js";
43
+ import type { AdvisorConfig } from "./types.js";
44
+ import { unknownConfigKeys, validateConfig } from "./validation.js";
45
+
46
+ export const configPaths = (ctx: ExtensionContext) => [
47
+ ctx.isProjectTrusted()
48
+ ? join(ctx.cwd, CONFIG_DIR_NAME, "advisor.json")
49
+ : null,
50
+ join(getAgentDir(), "advisor.json"),
51
+ ];
52
+
53
+ const configuredModelRef = (value: string | undefined): string | undefined =>
54
+ value?.trim() || undefined;
55
+
56
+ const readConfig = (path: string): AdvisorConfig => {
57
+ try {
58
+ const config = JSON.parse(readFileSync(path, "utf8"));
59
+ validateConfig(config, path);
60
+ return config;
61
+ } catch (error) {
62
+ throw error instanceof Error ? error : new Error(String(error));
63
+ }
64
+ };
65
+
66
+ // loadConfig runs on every tool call and every consultation. Caching the parsed
67
+ // file by path and stat identity removes that read and parse from the hot path
68
+ // while still applying the full reset-then-apply sequence on each call.
69
+ const configCache = new Map<
70
+ string,
71
+ { config: AdvisorConfig; identity: string }
72
+ >();
73
+ const warnedUnknownConfigIdentities = new Set<string>();
74
+
75
+ /** Drops the parsed-configuration cache; the next load re-reads from disk. */
76
+ export const resetConfigCache = () => {
77
+ configCache.clear();
78
+ warnedUnknownConfigIdentities.clear();
79
+ };
80
+
81
+ /**
82
+ * Nanosecond timestamps plus size and inode so a same-millisecond or
83
+ * same-size external rewrite cannot be served from the cache. An unstattable
84
+ * path yields a unique value, which always forces a re-read.
85
+ */
86
+ const configIdentity = (path: string): string => {
87
+ try {
88
+ const stats = statSync(path, { bigint: true });
89
+ return `${stats.mtimeNs}:${stats.ctimeNs}:${stats.size}:${stats.ino}:${stats.dev}`;
90
+ } catch {
91
+ return `unstattable:${process.hrtime.bigint()}`;
92
+ }
93
+ };
94
+
95
+ const readConfigCached = (path: string): AdvisorConfig => {
96
+ const identity = configIdentity(path);
97
+ const cached = configCache.get(path);
98
+ if (cached?.identity === identity) {
99
+ return cached.config;
100
+ }
101
+ const config = readConfig(path);
102
+ configCache.set(path, { config, identity });
103
+ return config;
104
+ };
105
+
106
+ export const loadConfig = (_ctx: ExtensionContext) => {
107
+ resetDefaults();
108
+ const global = join(getAgentDir(), "advisor.json");
109
+ const globalConfig = existsSync(global)
110
+ ? readConfigCached(global)
111
+ : undefined;
112
+ setPersistedModelRefs(
113
+ configuredModelRef(globalConfig?.advisor),
114
+ configuredModelRef(globalConfig?.executor)
115
+ );
116
+ if (globalConfig) {
117
+ applyConfig(globalConfig);
118
+ const unknownKeys = unknownConfigKeys(
119
+ globalConfig as Record<string, unknown>
120
+ );
121
+ const warningIdentity = `${global}:${configIdentity(global)}`;
122
+ if (
123
+ unknownKeys.length > 0 &&
124
+ _ctx.hasUI &&
125
+ !warnedUnknownConfigIdentities.has(warningIdentity)
126
+ ) {
127
+ _ctx.ui.notify(
128
+ `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.`,
129
+ "warning"
130
+ );
131
+ warnedUnknownConfigIdentities.add(warningIdentity);
132
+ }
133
+ }
134
+ // Repository-controlled project configuration is never applied. Models,
135
+ // prompts, gates, budgets, disclosure, redaction, integrations, and consent
136
+ // remain under the user's global Pi configuration.
137
+ setAdvisorOutcomeLoggingRef(globalConfig?.advisorOutcomeLogging === true);
138
+ return existsSync(global) ? global : null;
139
+ };
140
+
141
+ /** Saves user-controlled configuration globally without outcome consent. */
142
+ export interface SaveConfigOptions {
143
+ persistAdvisor?: boolean;
144
+ persistExecutor?: boolean;
145
+ }
146
+
147
+ export const saveConfig = (
148
+ _ctx: ExtensionContext,
149
+ options: SaveConfigOptions = {}
150
+ ) => {
151
+ const path = join(getAgentDir(), "advisor.json");
152
+ const persistAdvisor = options.persistAdvisor ?? true;
153
+ const persistExecutor = options.persistExecutor ?? true;
154
+ let existing: Record<string, unknown> = {};
155
+ try {
156
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
157
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
158
+ existing = parsed as Record<string, unknown>;
159
+ }
160
+ } catch {
161
+ /* replace a missing or malformed file */
162
+ }
163
+ if (advisorMaxCallsPerSessionRef === undefined) {
164
+ existing.advisorMaxCallsPerSession = undefined;
165
+ }
166
+ const data = {
167
+ ...existing,
168
+ ...(persistAdvisor && advisorRef ? { advisor: advisorRef } : {}),
169
+ advisorAutoLoopGate: advisorAutoLoopGateRef,
170
+ advisorBlockOnBlocked: advisorBlockOnBlockedRef,
171
+ advisorCollapseResponses: advisorCollapseResponsesRef,
172
+ advisorCompletionGate: advisorCompletionGateRef,
173
+ advisorCustomInvocation: advisorCustomInvocationRef,
174
+ advisorEffort: advisorEffortRef,
175
+ advisorFailureGate: advisorFailureGateRef,
176
+ advisorLoopThreshold: advisorLoopThresholdRef,
177
+ advisorPlanGate: advisorPlanGateRef,
178
+ contextMaxChars: contextMaxCharsRef,
179
+ ...(persistExecutor && executorRef ? { executor: executorRef } : {}),
180
+ executorEffort: executorEffortRef,
181
+ ...(advisorMaxCallsPerSessionRef === undefined
182
+ ? {}
183
+ : { advisorMaxCallsPerSession: advisorMaxCallsPerSessionRef }),
184
+ advisorGitContext: advisorGitContextRef,
185
+ advisorGitContextMaxChars: advisorGitContextMaxCharsRef,
186
+ advisorHerdrIntegration: advisorHerdrIntegrationRef,
187
+ advisorRedactSecrets: advisorRedactSecretsRef,
188
+ advisorScoutEnabled: advisorScoutEnabledRef,
189
+ advisorSessionSummary: advisorSessionSummaryRef,
190
+ advisorToolPolicies: advisorToolPoliciesRef,
191
+ advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
192
+ advisorToolResultMaxLines: advisorToolResultMaxLinesRef,
193
+ advisorTrackedFileContent: advisorTrackedFileContentRef,
194
+ advisorUntrackedContent: advisorUntrackedContentRef,
195
+ alwaysOn: alwaysOnRef,
196
+ gateFailureMode: advisorFailureModeRef,
197
+ showUsageDetails: showUsageDetailsRef,
198
+ showUsageFooter: showUsageFooterRef,
199
+ simpleMode: simpleModeRef,
200
+ };
201
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
202
+ resetConfigCache();
203
+ return path;
204
+ };
205
+
206
+ /** Outcome logging is deliberately written only to the global Pi configuration. */
207
+ export const saveGlobalOutcomeLogging = (enabled: boolean) => {
208
+ const path = join(getAgentDir(), "advisor.json");
209
+ let existing: Record<string, unknown> = {};
210
+ try {
211
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
212
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
213
+ existing = parsed as Record<string, unknown>;
214
+ }
215
+ } catch {
216
+ /* create it */
217
+ }
218
+ writeFileSync(
219
+ path,
220
+ `${JSON.stringify({ ...existing, advisorOutcomeLogging: enabled }, null, 2)}\n`
221
+ );
222
+ resetConfigCache();
223
+ return path;
224
+ };