pi-advisor-flow 0.5.5 → 0.6.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +1 -1
  3. package/dist/index.js +5786 -0
  4. package/extensions/index.ts +9 -9
  5. package/package.json +16 -10
  6. package/src/{untracked.ts → attachments.ts} +2 -5
  7. package/src/commands/activation-preparation.ts +6 -6
  8. package/src/commands/activation.ts +7 -7
  9. package/src/commands/lifecycle.ts +5 -5
  10. package/src/commands/manual-command.ts +13 -9
  11. package/src/commands/manual-consultation.ts +8 -8
  12. package/src/commands/manual-progress.ts +3 -3
  13. package/src/commands/model-commands.ts +6 -6
  14. package/src/commands/model-options.ts +4 -4
  15. package/src/commands/model-picker.ts +2 -2
  16. package/src/commands/registration.ts +8 -8
  17. package/src/commands/renderers.ts +5 -5
  18. package/src/commands/runtime.ts +8 -8
  19. package/src/commands/settings-commands.ts +8 -8
  20. package/src/commands/settings-persistence.ts +4 -4
  21. package/src/commands/types.ts +5 -13
  22. package/src/commands.ts +2 -2
  23. package/src/config/args.ts +3 -3
  24. package/src/config/defaults.ts +3 -3
  25. package/src/config/schema.ts +322 -0
  26. package/src/config/state.ts +2 -2
  27. package/src/config/storage.ts +113 -103
  28. package/src/config/types.ts +1 -1
  29. package/src/config/validation.ts +61 -181
  30. package/src/config.ts +7 -7
  31. package/src/content-utils.ts +30 -0
  32. package/src/conversation.ts +13 -168
  33. package/src/git.ts +4 -18
  34. package/src/herdr.ts +6 -8
  35. package/src/model-stream.ts +5 -8
  36. package/src/outcomes.ts +2 -5
  37. package/src/pi-settings.ts +59 -0
  38. package/src/preferences.ts +2 -2
  39. package/src/redaction.ts +50 -0
  40. package/src/scout-context.ts +121 -485
  41. package/src/scout-curation.ts +84 -0
  42. package/src/scout-groups.ts +485 -0
  43. package/src/scout-protocol.ts +122 -0
  44. package/src/scout-reconstruct.ts +44 -0
  45. package/src/scout-types.ts +66 -0
  46. package/src/scout.ts +53 -63
  47. package/src/session-state.ts +119 -76
  48. package/src/tool-result-cap.ts +88 -0
  49. package/src/tools/consult-context.ts +155 -0
  50. package/src/tools/consultation.ts +71 -220
  51. package/src/tools/gate-policy.ts +5 -5
  52. package/src/tools/gate-protocol.ts +2 -2
  53. package/src/tools/loop-gate.ts +70 -61
  54. package/src/tools/prompts.ts +4 -7
  55. package/src/tools/register-ask-advisor.ts +9 -14
  56. package/src/tools/register-lifecycle.ts +7 -7
  57. package/src/tools/register-outcome.ts +5 -5
  58. package/src/tools/register-renderers.ts +5 -5
  59. package/src/tools/registration.ts +10 -10
  60. package/src/tools/render-advisor-result.ts +11 -8
  61. package/src/tools/render-common.ts +25 -13
  62. package/src/tools/scout-status.ts +5 -5
  63. package/src/tools/session.ts +1 -1
  64. package/src/tools/types.ts +7 -7
  65. package/src/tools.ts +9 -9
  66. package/src/ui/manual-dialog-render.ts +10 -3
  67. package/src/ui/manual-dialog.ts +4 -9
  68. package/src/ui/model-selector.ts +7 -6
  69. package/src/ui/settings-formatting.ts +2 -2
  70. package/src/ui/settings-items.ts +4 -4
  71. package/src/ui/settings-mutations.ts +2 -2
  72. package/src/ui/settings-selector.ts +5 -5
  73. package/src/ui/text-setting-submenu.ts +6 -1
  74. package/src/ui/types.ts +2 -2
  75. package/src/ui.ts +4 -4
  76. package/src/usage.ts +1 -6
@@ -0,0 +1,322 @@
1
+ import { GIT_CONTEXT_LEVELS, isValidGitContextLevel } from "../git.ts";
2
+ import {
3
+ advisorAutoLoopGateRef,
4
+ advisorBlockOnBlockedRef,
5
+ advisorCollapseResponsesRef,
6
+ advisorCompletionGateRef,
7
+ advisorCustomInvocationRef,
8
+ advisorEffortRef,
9
+ advisorFailureGateRef,
10
+ advisorFailureModeRef,
11
+ advisorGitContextMaxCharsRef,
12
+ advisorGitContextRef,
13
+ advisorHerdrIntegrationRef,
14
+ advisorLoopThresholdRef,
15
+ advisorMaxCallsPerSessionRef,
16
+ advisorOutcomeLoggingRef,
17
+ advisorPlanGateRef,
18
+ advisorRedactSecretsRef,
19
+ advisorRef,
20
+ advisorScoutEnabledRef,
21
+ advisorSessionSummaryRef,
22
+ advisorToolPoliciesRef,
23
+ advisorToolResultMaxBytesRef,
24
+ advisorToolResultMaxLinesRef,
25
+ advisorTrackedFileContentRef,
26
+ advisorUntrackedContentRef,
27
+ alwaysOnRef,
28
+ contextMaxCharsRef,
29
+ executorEffortRef,
30
+ executorRef,
31
+ showUsageDetailsRef,
32
+ showUsageFooterRef,
33
+ simpleModeRef,
34
+ } from "./state.ts";
35
+ import {
36
+ ADVISOR_TOOL_POLICIES,
37
+ type AdvisorConfig,
38
+ GATE_FAILURE_MODES,
39
+ MAX_CONTEXT_MAX_CHARS,
40
+ } from "./types.ts";
41
+
42
+ /** An empty ref means no model has been selected yet. */
43
+ const configuredModelRef = (value: string | undefined): string | undefined =>
44
+ value?.trim() || undefined;
45
+
46
+ export const isValidAdvisorToolPolicies = (
47
+ value: unknown
48
+ ): value is Record<string, (typeof ADVISOR_TOOL_POLICIES)[number]> => {
49
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
50
+ return false;
51
+ }
52
+ return Object.entries(value).every(
53
+ ([toolName, policy]) =>
54
+ toolName.trim().length > 0 &&
55
+ typeof policy === "string" &&
56
+ ADVISOR_TOOL_POLICIES.includes(
57
+ policy as (typeof ADVISOR_TOOL_POLICIES)[number]
58
+ )
59
+ );
60
+ };
61
+
62
+ const nonNegativeSafeInteger = (value: unknown): value is number =>
63
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
64
+
65
+ export const isValidContextMaxChars = (value: unknown): value is number =>
66
+ typeof value === "number" &&
67
+ Number.isSafeInteger(value) &&
68
+ value >= 0 &&
69
+ value <= MAX_CONTEXT_MAX_CHARS;
70
+
71
+ export const isValidLoopThreshold = (value: unknown): value is number =>
72
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 2;
73
+
74
+ export const isValidMaxCallsPerSession = (value: unknown): value is number =>
75
+ nonNegativeSafeInteger(value);
76
+
77
+ export const isValidGateFailureMode = (
78
+ value: unknown
79
+ ): value is (typeof GATE_FAILURE_MODES)[number] =>
80
+ typeof value === "string" &&
81
+ GATE_FAILURE_MODES.includes(value as (typeof GATE_FAILURE_MODES)[number]);
82
+
83
+ export const isValidToolResultMaxLines = (value: unknown): value is number =>
84
+ nonNegativeSafeInteger(value);
85
+
86
+ export const isValidToolResultMaxBytes = (value: unknown): value is number =>
87
+ nonNegativeSafeInteger(value);
88
+
89
+ /** One declarative entry per AdvisorConfig key: JSON type, persistence, and
90
+ * the live runtime value behind it. Validation and storage derive from this
91
+ * table; adding a setting means adding exactly one entry here. */
92
+ export interface ConfigKeySchema {
93
+ /** Human-readable accepted-value phrase for error messages. */
94
+ accepted: string;
95
+ /** Live runtime value captured by saveConfig. */
96
+ current: () => unknown;
97
+ /** Included in the state captured and diffed by saveConfig. */
98
+ persisted: boolean;
99
+ /** JSON value type used for the base type check. */
100
+ type: "string" | "boolean" | "number" | "enum" | "object";
101
+ /** Type beyond the JSON type, for enum and object keys. */
102
+ validate?: (value: unknown) => boolean;
103
+ }
104
+
105
+ export const CONFIG_SCHEMA = {
106
+ advisor: {
107
+ accepted: "a provider/model string",
108
+ current: () => configuredModelRef(advisorRef),
109
+ persisted: true,
110
+ type: "string",
111
+ },
112
+ advisorAutoLoopGate: {
113
+ accepted: "true or false",
114
+ current: () => advisorAutoLoopGateRef,
115
+ persisted: true,
116
+ type: "boolean",
117
+ },
118
+ advisorBlockOnBlocked: {
119
+ accepted: "true or false",
120
+ current: () => advisorBlockOnBlockedRef,
121
+ persisted: true,
122
+ type: "boolean",
123
+ },
124
+ advisorCollapseResponses: {
125
+ accepted: "true or false",
126
+ current: () => advisorCollapseResponsesRef,
127
+ persisted: true,
128
+ type: "boolean",
129
+ },
130
+ advisorCompletionGate: {
131
+ accepted: "true or false",
132
+ current: () => advisorCompletionGateRef,
133
+ persisted: true,
134
+ type: "boolean",
135
+ },
136
+ advisorCustomInvocation: {
137
+ accepted: "a string",
138
+ current: () => advisorCustomInvocationRef,
139
+ persisted: true,
140
+ type: "string",
141
+ },
142
+ advisorEffort: {
143
+ accepted: "a string",
144
+ current: () => advisorEffortRef,
145
+ persisted: true,
146
+ type: "string",
147
+ },
148
+ advisorFailureGate: {
149
+ accepted: "true or false",
150
+ current: () => advisorFailureGateRef,
151
+ persisted: true,
152
+ type: "boolean",
153
+ },
154
+ advisorGitContext: {
155
+ accepted: GIT_CONTEXT_LEVELS.join(", "),
156
+ current: () => advisorGitContextRef,
157
+ persisted: true,
158
+ type: "enum",
159
+ validate: isValidGitContextLevel,
160
+ },
161
+ advisorGitContextMaxChars: {
162
+ accepted: "a non-negative safe integer",
163
+ current: () => advisorGitContextMaxCharsRef,
164
+ persisted: true,
165
+ type: "number",
166
+ validate: nonNegativeSafeInteger,
167
+ },
168
+ advisorHerdrIntegration: {
169
+ accepted: "true or false",
170
+ current: () => advisorHerdrIntegrationRef,
171
+ persisted: true,
172
+ type: "boolean",
173
+ },
174
+ advisorLoopThreshold: {
175
+ accepted: "a safe integer of at least 2",
176
+ current: () => advisorLoopThresholdRef,
177
+ persisted: true,
178
+ type: "number",
179
+ validate: isValidLoopThreshold,
180
+ },
181
+ advisorMaxCallsPerSession: {
182
+ accepted: "a non-negative safe integer",
183
+ current: () => advisorMaxCallsPerSessionRef,
184
+ persisted: true,
185
+ type: "number",
186
+ validate: isValidMaxCallsPerSession,
187
+ },
188
+ advisorOutcomeLogging: {
189
+ accepted: "true or false",
190
+ current: () => advisorOutcomeLoggingRef,
191
+ persisted: false,
192
+ type: "boolean",
193
+ },
194
+ advisorPlanGate: {
195
+ accepted: "true or false",
196
+ current: () => advisorPlanGateRef,
197
+ persisted: true,
198
+ type: "boolean",
199
+ },
200
+ advisorRedactSecrets: {
201
+ accepted: "true or false",
202
+ current: () => advisorRedactSecretsRef,
203
+ persisted: true,
204
+ type: "boolean",
205
+ },
206
+ advisorScoutEnabled: {
207
+ accepted: "true or false",
208
+ current: () => advisorScoutEnabledRef,
209
+ persisted: true,
210
+ type: "boolean",
211
+ },
212
+ advisorSessionSummary: {
213
+ accepted: "true or false",
214
+ current: () => advisorSessionSummaryRef,
215
+ persisted: true,
216
+ type: "boolean",
217
+ },
218
+ advisorToolPolicies: {
219
+ accepted:
220
+ "a JSON object with non-empty tool names and full, summary, or exclude values",
221
+ current: () => ({ ...advisorToolPoliciesRef }),
222
+ persisted: true,
223
+ type: "object",
224
+ validate: isValidAdvisorToolPolicies,
225
+ },
226
+ advisorToolResultMaxBytes: {
227
+ accepted: "a non-negative safe integer",
228
+ current: () => advisorToolResultMaxBytesRef,
229
+ persisted: true,
230
+ type: "number",
231
+ validate: isValidToolResultMaxBytes,
232
+ },
233
+ advisorToolResultMaxLines: {
234
+ accepted: "a non-negative safe integer",
235
+ current: () => advisorToolResultMaxLinesRef,
236
+ persisted: true,
237
+ type: "number",
238
+ validate: isValidToolResultMaxLines,
239
+ },
240
+ advisorTrackedFileContent: {
241
+ accepted: "true or false",
242
+ current: () => advisorTrackedFileContentRef,
243
+ persisted: true,
244
+ type: "boolean",
245
+ },
246
+ advisorUntrackedContent: {
247
+ accepted: "true or false",
248
+ current: () => advisorUntrackedContentRef,
249
+ persisted: true,
250
+ type: "boolean",
251
+ },
252
+ alwaysOn: {
253
+ accepted: "true or false",
254
+ current: () => alwaysOnRef,
255
+ persisted: true,
256
+ type: "boolean",
257
+ },
258
+ contextMaxChars: {
259
+ accepted: `a safe integer from 0 through ${MAX_CONTEXT_MAX_CHARS}`,
260
+ current: () => contextMaxCharsRef,
261
+ persisted: true,
262
+ type: "number",
263
+ validate: isValidContextMaxChars,
264
+ },
265
+ executor: {
266
+ accepted: "a provider/model string",
267
+ current: () => configuredModelRef(executorRef),
268
+ persisted: true,
269
+ type: "string",
270
+ },
271
+ executorEffort: {
272
+ accepted: "a string",
273
+ current: () => executorEffortRef,
274
+ persisted: true,
275
+ type: "string",
276
+ },
277
+ gateFailureMode: {
278
+ accepted: GATE_FAILURE_MODES.join(", "),
279
+ current: () => advisorFailureModeRef,
280
+ persisted: true,
281
+ type: "enum",
282
+ validate: isValidGateFailureMode,
283
+ },
284
+ showUsageDetails: {
285
+ accepted: "true or false",
286
+ current: () => showUsageDetailsRef,
287
+ persisted: true,
288
+ type: "boolean",
289
+ },
290
+ showUsageFooter: {
291
+ accepted: "true or false",
292
+ current: () => showUsageFooterRef,
293
+ persisted: true,
294
+ type: "boolean",
295
+ },
296
+ simpleMode: {
297
+ accepted: "true or false",
298
+ current: () => simpleModeRef,
299
+ persisted: true,
300
+ type: "boolean",
301
+ },
302
+ } as const satisfies Record<keyof AdvisorConfig, ConfigKeySchema>;
303
+
304
+ export type ConfigKey = keyof typeof CONFIG_SCHEMA;
305
+
306
+ /** Keys saveConfig captures, diffs, and writes to advisor.json. */
307
+ export type PersistedConfigKey = {
308
+ [Key in ConfigKey]: (typeof CONFIG_SCHEMA)[Key]["persisted"] extends true
309
+ ? Key
310
+ : never;
311
+ }[ConfigKey];
312
+
313
+ export const SAVED_CONFIG_KEYS = (
314
+ Object.keys(CONFIG_SCHEMA) as ConfigKey[]
315
+ ).filter(
316
+ (key) => CONFIG_SCHEMA[key].persisted
317
+ ) as readonly PersistedConfigKey[];
318
+
319
+ /** Widened per-key view for consumers that index by dynamic key. */
320
+ const SCHEMA_BY_KEY: Record<ConfigKey, ConfigKeySchema> = CONFIG_SCHEMA;
321
+
322
+ export { configuredModelRef, SCHEMA_BY_KEY };
@@ -1,4 +1,4 @@
1
- import type { GitContextLevel } from "../git.js";
1
+ import type { GitContextLevel } from "../git.ts";
2
2
  import {
3
3
  type AdvisorToolPolicies,
4
4
  DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS,
@@ -6,7 +6,7 @@ import {
6
6
  DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
7
7
  DEFAULT_CONTEXT_MAX_CHARS,
8
8
  type GateFailureMode,
9
- } from "./types.js";
9
+ } from "./types.ts";
10
10
 
11
11
  // An empty ref means no model has been selected yet.
12
12
  export let executorRef = "";
@@ -5,43 +5,16 @@ import {
5
5
  type ExtensionContext,
6
6
  getAgentDir,
7
7
  } from "@earendil-works/pi-coding-agent";
8
- import { applyConfig, resetDefaults } from "./defaults.js";
8
+ import { applyConfig, resetDefaults } from "./defaults.ts";
9
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";
10
+ CONFIG_SCHEMA,
11
+ configuredModelRef,
12
+ type PersistedConfigKey,
13
+ SAVED_CONFIG_KEYS,
14
+ } from "./schema.ts";
15
+ import { setAdvisorOutcomeLoggingRef, setPersistedModelRefs } from "./state.ts";
16
+ import type { AdvisorConfig } from "./types.ts";
17
+ import { unknownConfigKeys, validateConfig } from "./validation.ts";
45
18
 
46
19
  export const configPaths = (ctx: ExtensionContext) => [
47
20
  ctx.isProjectTrusted()
@@ -50,22 +23,71 @@ export const configPaths = (ctx: ExtensionContext) => [
50
23
  join(getAgentDir(), "advisor.json"),
51
24
  ];
52
25
 
53
- const configuredModelRef = (value: string | undefined): string | undefined =>
54
- value?.trim() || undefined;
26
+ type SavedConfigKey = PersistedConfigKey;
27
+ type ConfigState = {
28
+ [Key in SavedConfigKey]: AdvisorConfig[Key];
29
+ };
55
30
 
56
- const readConfig = (path: string): AdvisorConfig => {
31
+ const currentConfigState = (): ConfigState =>
32
+ Object.fromEntries(
33
+ SAVED_CONFIG_KEYS.map((key) => [key, CONFIG_SCHEMA[key].current()])
34
+ ) as ConfigState;
35
+
36
+ const sameConfigValue = <Value>(left: Value, right: Value) =>
37
+ JSON.stringify(left) === JSON.stringify(right);
38
+
39
+ const readExistingConfig = (path: string): Record<string, unknown> => {
57
40
  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));
41
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
42
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
43
+ ? (parsed as Record<string, unknown>)
44
+ : {};
45
+ } catch {
46
+ return {};
63
47
  }
64
48
  };
65
49
 
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.
50
+ const shouldPersistConfigKey = (
51
+ key: SavedConfigKey,
52
+ persistAdvisor: boolean,
53
+ persistExecutor: boolean
54
+ ) =>
55
+ (key !== "advisor" || persistAdvisor) &&
56
+ (key !== "executor" || persistExecutor);
57
+
58
+ const applyChangedConfigValues = (
59
+ data: Record<string, unknown>,
60
+ current: ConfigState,
61
+ changedKeys: readonly SavedConfigKey[],
62
+ persistAdvisor: boolean,
63
+ persistExecutor: boolean
64
+ ) => {
65
+ for (const key of changedKeys) {
66
+ if (!shouldPersistConfigKey(key, persistAdvisor, persistExecutor)) {
67
+ continue;
68
+ }
69
+ const value = current[key];
70
+ const isEmptyModelRef = (key === "advisor" || key === "executor") && !value;
71
+ if (value === undefined || isEmptyModelRef) {
72
+ delete data[key];
73
+ } else {
74
+ data[key] = value;
75
+ }
76
+ }
77
+ };
78
+
79
+ // Tracks the last load/save state so a later save merges fresh disk contents instead of overwriting an external edit.
80
+ let loadedConfigState: ConfigState | undefined;
81
+ let loadedConfigPath: string | undefined;
82
+
83
+ const readConfig = (path: string): AdvisorConfig => {
84
+ const config = JSON.parse(readFileSync(path, "utf8"));
85
+ validateConfig(config, path);
86
+ return config;
87
+ };
88
+
89
+ // loadConfig runs per tool call and consultation; caching by path and stat
90
+ // identity keeps the parse off the hot path while still reset-applying each call.
69
91
  const configCache = new Map<
70
92
  string,
71
93
  { config: AdvisorConfig; identity: string }
@@ -135,10 +157,17 @@ export const loadConfig = (_ctx: ExtensionContext) => {
135
157
  // prompts, gates, budgets, disclosure, redaction, integrations, and consent
136
158
  // remain under the user's global Pi configuration.
137
159
  setAdvisorOutcomeLoggingRef(globalConfig?.advisorOutcomeLogging === true);
160
+ loadedConfigState = currentConfigState();
161
+ loadedConfigPath = global;
138
162
  return existsSync(global) ? global : null;
139
163
  };
140
164
 
141
- /** Saves user-controlled configuration globally without outcome consent. */
165
+ /**
166
+ * Saves user-controlled configuration globally without outcome consent.
167
+ *
168
+ * Only values changed since the last load/save are written. This lets an
169
+ * external edit to advisor.json coexist with a later settings or model save.
170
+ */
142
171
  export interface SaveConfigOptions {
143
172
  persistAdvisor?: boolean;
144
173
  persistExecutor?: boolean;
@@ -151,70 +180,51 @@ export const saveConfig = (
151
180
  const path = join(getAgentDir(), "advisor.json");
152
181
  const persistAdvisor = options.persistAdvisor ?? true;
153
182
  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;
183
+ const existing = readExistingConfig(path);
184
+ const current = currentConfigState();
185
+ const baseline = loadedConfigPath === path ? loadedConfigState : undefined;
186
+ const changedKeys = baseline
187
+ ? SAVED_CONFIG_KEYS.filter(
188
+ (key) => !sameConfigValue(current[key], baseline[key])
189
+ )
190
+ : [...SAVED_CONFIG_KEYS];
191
+ if (changedKeys.length === 0) {
192
+ return path;
165
193
  }
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
- };
194
+ const data = { ...existing };
195
+ applyChangedConfigValues(
196
+ data,
197
+ current,
198
+ changedKeys,
199
+ persistAdvisor,
200
+ persistExecutor
201
+ );
201
202
  writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
202
203
  resetConfigCache();
204
+ const nextLoadedState = { ...current };
205
+ if (!persistAdvisor) {
206
+ nextLoadedState.advisor = baseline
207
+ ? baseline.advisor
208
+ : configuredModelRef(
209
+ typeof existing.advisor === "string" ? existing.advisor : undefined
210
+ );
211
+ }
212
+ if (!persistExecutor) {
213
+ nextLoadedState.executor = baseline
214
+ ? baseline.executor
215
+ : configuredModelRef(
216
+ typeof existing.executor === "string" ? existing.executor : undefined
217
+ );
218
+ }
219
+ loadedConfigState = nextLoadedState;
220
+ loadedConfigPath = path;
203
221
  return path;
204
222
  };
205
223
 
206
224
  /** Outcome logging is deliberately written only to the global Pi configuration. */
207
225
  export const saveGlobalOutcomeLogging = (enabled: boolean) => {
208
226
  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
- }
227
+ const existing = readExistingConfig(path);
218
228
  writeFileSync(
219
229
  path,
220
230
  `${JSON.stringify({ ...existing, advisorOutcomeLogging: enabled }, null, 2)}\n`
@@ -2,7 +2,7 @@ import {
2
2
  DEFAULT_MAX_BYTES,
3
3
  DEFAULT_MAX_LINES,
4
4
  } from "@earendil-works/pi-coding-agent";
5
- import type { GitContextLevel } from "../git.js";
5
+ import type { GitContextLevel } from "../git.ts";
6
6
 
7
7
  export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
8
8
  export const MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;