pi-advisor-flow 0.6.0 → 0.8.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 (93) hide show
  1. package/CHANGELOG.md +34 -17
  2. package/README.md +10 -8
  3. package/dist/index.js +2852 -868
  4. package/extensions/index.ts +2 -1
  5. package/package.json +55 -40
  6. package/src/attachments.ts +11 -10
  7. package/src/commands/activation-preparation.ts +1 -0
  8. package/src/commands/activation.ts +2 -0
  9. package/src/commands/lifecycle.ts +1 -0
  10. package/src/commands/manual-command.ts +7 -1
  11. package/src/commands/manual-consultation.ts +103 -79
  12. package/src/commands/manual-progress.ts +7 -5
  13. package/src/commands/model-commands.ts +1 -1
  14. package/src/commands/model-options.ts +14 -3
  15. package/src/commands/model-picker.ts +1 -0
  16. package/src/commands/registration.ts +1 -0
  17. package/src/commands/renderers.ts +67 -55
  18. package/src/commands/runtime.ts +16 -14
  19. package/src/commands/settings-commands.ts +10 -4
  20. package/src/commands/settings-persistence.ts +64 -2
  21. package/src/commands/types.ts +2 -0
  22. package/src/commands.ts +0 -1
  23. package/src/config/args.ts +1 -1
  24. package/src/config/defaults.ts +83 -1
  25. package/src/config/schema.ts +152 -15
  26. package/src/config/state.ts +80 -2
  27. package/src/config/storage.ts +38 -22
  28. package/src/config/types.ts +45 -3
  29. package/src/config/validation.ts +33 -21
  30. package/src/config.ts +3 -0
  31. package/src/content-utils.ts +24 -4
  32. package/src/conversation.ts +16 -15
  33. package/src/git.ts +10 -3
  34. package/src/herdr-block.ts +83 -0
  35. package/src/herdr-shared.ts +108 -0
  36. package/src/herdr.ts +30 -192
  37. package/src/jev/client.ts +338 -0
  38. package/src/jev/failure.ts +19 -0
  39. package/src/jev/key-store.ts +267 -0
  40. package/src/jev/ledger.ts +228 -0
  41. package/src/jev/questions.ts +122 -0
  42. package/src/jev/state.ts +50 -0
  43. package/src/jev/transport.ts +63 -0
  44. package/src/model-stream.ts +35 -37
  45. package/src/outcomes.ts +20 -12
  46. package/src/pi-settings.ts +4 -9
  47. package/src/preferences.ts +4 -2
  48. package/src/redaction.ts +10 -12
  49. package/src/scout-context.ts +59 -60
  50. package/src/scout-curation.ts +4 -13
  51. package/src/scout-groups.ts +178 -175
  52. package/src/scout-protocol.ts +74 -70
  53. package/src/scout-reconstruct.ts +1 -1
  54. package/src/scout.ts +115 -73
  55. package/src/session-state.ts +148 -39
  56. package/src/tool-result-cap.ts +18 -17
  57. package/src/tools/consult-context.ts +7 -15
  58. package/src/tools/consultation.ts +14 -4
  59. package/src/tools/gate-policy.ts +1 -0
  60. package/src/tools/gate-protocol.ts +36 -23
  61. package/src/tools/jev-filter.ts +204 -0
  62. package/src/tools/jev-turn-gate.ts +203 -0
  63. package/src/tools/loop-gate.ts +32 -20
  64. package/src/tools/model-access.ts +46 -0
  65. package/src/tools/outage-notifier.ts +30 -0
  66. package/src/tools/prompts.ts +20 -25
  67. package/src/tools/register-ask-advisor.ts +73 -17
  68. package/src/tools/register-lifecycle.ts +22 -0
  69. package/src/tools/register-outcome.ts +2 -0
  70. package/src/tools/register-renderers.ts +149 -73
  71. package/src/tools/registration.ts +15 -0
  72. package/src/tools/render-advisor-result.ts +77 -35
  73. package/src/tools/render-common.ts +16 -19
  74. package/src/tools/scout-status.ts +42 -27
  75. package/src/tools/types.ts +11 -0
  76. package/src/tools.ts +0 -1
  77. package/src/ui/jev-setup-submenu.ts +350 -0
  78. package/src/ui/manual-dialog-render.ts +1 -0
  79. package/src/ui/manual-dialog.ts +39 -43
  80. package/src/ui/masked-input.ts +62 -0
  81. package/src/ui/model-multi-selector.ts +9 -0
  82. package/src/ui/model-selector-adapter.ts +31 -0
  83. package/src/ui/model-selector.ts +14 -161
  84. package/src/ui/searchable-model-list.ts +230 -0
  85. package/src/ui/settings-formatting.ts +24 -14
  86. package/src/ui/settings-items.ts +200 -2
  87. package/src/ui/settings-list-adapter.ts +15 -12
  88. package/src/ui/settings-mutations.ts +169 -47
  89. package/src/ui/settings-selector.ts +18 -9
  90. package/src/ui/text-setting-submenu.ts +3 -6
  91. package/src/ui/types.ts +25 -0
  92. package/src/ui.ts +0 -1
  93. package/src/usage.ts +29 -20
@@ -1,3 +1,5 @@
1
+ import { isString } from "../content-utils.ts";
2
+ import type { JsonValue } from "../content-utils.ts";
1
3
  import { GIT_CONTEXT_LEVELS, isValidGitContextLevel } from "../git.ts";
2
4
  import {
3
5
  advisorAutoLoopGateRef,
@@ -11,8 +13,20 @@ import {
11
13
  advisorGitContextMaxCharsRef,
12
14
  advisorGitContextRef,
13
15
  advisorHerdrIntegrationRef,
16
+ advisorJevDigestMaxCharsRef,
17
+ advisorJevFilterEnabledRef,
18
+ advisorJevFilterNoulMarginRef,
19
+ advisorJevFilterOverrideWindowRef,
20
+ advisorJevFilterSkipConfidenceRef,
21
+ advisorJevModelRef,
22
+ advisorJevPricePerMtokRef,
23
+ advisorJevTimeoutMsRef,
24
+ advisorJevTransportRef,
25
+ advisorJevTurnGateEveryTurnsRef,
26
+ advisorJevTurnGateNoulThresholdRef,
14
27
  advisorLoopThresholdRef,
15
28
  advisorMaxCallsPerSessionRef,
29
+ advisorModelWhitelistRef,
16
30
  advisorOutcomeLoggingRef,
17
31
  advisorPlanGateRef,
18
32
  advisorRedactSecretsRef,
@@ -32,10 +46,15 @@ import {
32
46
  showUsageFooterRef,
33
47
  simpleModeRef,
34
48
  } from "./state.ts";
49
+ import type {
50
+ AdvisorConfig,
51
+ AdvisorConfigValue,
52
+ JevTransport,
53
+ } from "./types.ts";
35
54
  import {
36
55
  ADVISOR_TOOL_POLICIES,
37
- type AdvisorConfig,
38
56
  GATE_FAILURE_MODES,
57
+ JEV_TRANSPORTS,
39
58
  MAX_CONTEXT_MAX_CHARS,
40
59
  } from "./types.ts";
41
60
 
@@ -43,6 +62,15 @@ import {
43
62
  const configuredModelRef = (value: string | undefined): string | undefined =>
44
63
  value?.trim() || undefined;
45
64
 
65
+ const isNonEmptyModel = (model: unknown): model is string =>
66
+ typeof model === "string" && model.trim().length > 0;
67
+
68
+ export const isValidAdvisorModelWhitelist = (
69
+ value: unknown
70
+ ): value is string[] => Array.isArray(value) && value.every(isNonEmptyModel);
71
+
72
+ const ADVISOR_TOOL_POLICY_NAMES = new Set<string>(ADVISOR_TOOL_POLICIES);
73
+
46
74
  export const isValidAdvisorToolPolicies = (
47
75
  value: unknown
48
76
  ): value is Record<string, (typeof ADVISOR_TOOL_POLICIES)[number]> => {
@@ -52,10 +80,8 @@ export const isValidAdvisorToolPolicies = (
52
80
  return Object.entries(value).every(
53
81
  ([toolName, policy]) =>
54
82
  toolName.trim().length > 0 &&
55
- typeof policy === "string" &&
56
- ADVISOR_TOOL_POLICIES.includes(
57
- policy as (typeof ADVISOR_TOOL_POLICIES)[number]
58
- )
83
+ isString(policy) &&
84
+ ADVISOR_TOOL_POLICY_NAMES.has(policy)
59
85
  );
60
86
  };
61
87
 
@@ -74,11 +100,12 @@ export const isValidLoopThreshold = (value: unknown): value is number =>
74
100
  export const isValidMaxCallsPerSession = (value: unknown): value is number =>
75
101
  nonNegativeSafeInteger(value);
76
102
 
103
+ const GATE_FAILURE_MODE_NAMES = new Set<string>(GATE_FAILURE_MODES);
104
+
77
105
  export const isValidGateFailureMode = (
78
106
  value: unknown
79
107
  ): value is (typeof GATE_FAILURE_MODES)[number] =>
80
- typeof value === "string" &&
81
- GATE_FAILURE_MODES.includes(value as (typeof GATE_FAILURE_MODES)[number]);
108
+ isString(value) && GATE_FAILURE_MODE_NAMES.has(value);
82
109
 
83
110
  export const isValidToolResultMaxLines = (value: unknown): value is number =>
84
111
  nonNegativeSafeInteger(value);
@@ -86,6 +113,32 @@ export const isValidToolResultMaxLines = (value: unknown): value is number =>
86
113
  export const isValidToolResultMaxBytes = (value: unknown): value is number =>
87
114
  nonNegativeSafeInteger(value);
88
115
 
116
+ export const isValidJevTimeoutMs = (value: unknown): value is number =>
117
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
118
+
119
+ export const isValidJevDigestMaxChars = (value: unknown): value is number =>
120
+ nonNegativeSafeInteger(value);
121
+
122
+ export const isValidJevPricePerMtok = (value: unknown): value is number =>
123
+ typeof value === "number" && Number.isFinite(value) && value > 0;
124
+
125
+ export const isValidJevSkipConfidence = (value: unknown): value is number =>
126
+ typeof value === "number" &&
127
+ Number.isFinite(value) &&
128
+ value >= 0.5 &&
129
+ value <= 1;
130
+
131
+ export const isValidJevNoulMargin = (value: unknown): value is number =>
132
+ typeof value === "number" &&
133
+ Number.isFinite(value) &&
134
+ value >= 0 &&
135
+ value <= 0.5;
136
+
137
+ const JEV_TRANSPORT_NAMES = new Set<string>(JEV_TRANSPORTS);
138
+
139
+ export const isValidJevTransport = (value: unknown): value is JevTransport =>
140
+ isString(value) && JEV_TRANSPORT_NAMES.has(value);
141
+
89
142
  /** One declarative entry per AdvisorConfig key: JSON type, persistence, and
90
143
  * the live runtime value behind it. Validation and storage derive from this
91
144
  * table; adding a setting means adding exactly one entry here. */
@@ -93,13 +146,13 @@ export interface ConfigKeySchema {
93
146
  /** Human-readable accepted-value phrase for error messages. */
94
147
  accepted: string;
95
148
  /** Live runtime value captured by saveConfig. */
96
- current: () => unknown;
149
+ current: () => AdvisorConfigValue;
97
150
  /** Included in the state captured and diffed by saveConfig. */
98
151
  persisted: boolean;
99
152
  /** JSON value type used for the base type check. */
100
- type: "string" | "boolean" | "number" | "enum" | "object";
153
+ type: "string" | "boolean" | "number" | "enum" | "object" | "array";
101
154
  /** Type beyond the JSON type, for enum and object keys. */
102
- validate?: (value: unknown) => boolean;
155
+ validate?: (value: unknown) => value is JsonValue;
103
156
  }
104
157
 
105
158
  export const CONFIG_SCHEMA = {
@@ -171,6 +224,81 @@ export const CONFIG_SCHEMA = {
171
224
  persisted: true,
172
225
  type: "boolean",
173
226
  },
227
+ advisorJevDigestMaxChars: {
228
+ accepted: "a non-negative safe integer",
229
+ current: () => advisorJevDigestMaxCharsRef,
230
+ persisted: true,
231
+ type: "number",
232
+ validate: isValidJevDigestMaxChars,
233
+ },
234
+ advisorJevFilterEnabled: {
235
+ accepted: "true or false",
236
+ current: () => advisorJevFilterEnabledRef,
237
+ persisted: true,
238
+ type: "boolean",
239
+ },
240
+ advisorJevFilterNoulMargin: {
241
+ accepted: "a number from 0 through 0.5",
242
+ current: () => advisorJevFilterNoulMarginRef,
243
+ persisted: true,
244
+ type: "number",
245
+ validate: isValidJevNoulMargin,
246
+ },
247
+ advisorJevFilterOverrideWindow: {
248
+ accepted: "a non-negative safe integer",
249
+ current: () => advisorJevFilterOverrideWindowRef,
250
+ persisted: true,
251
+ type: "number",
252
+ validate: nonNegativeSafeInteger,
253
+ },
254
+ advisorJevFilterSkipConfidence: {
255
+ accepted: "a number between 0.5 and 1 inclusive",
256
+ current: () => advisorJevFilterSkipConfidenceRef,
257
+ persisted: true,
258
+ type: "number",
259
+ validate: isValidJevSkipConfidence,
260
+ },
261
+ advisorJevModel: {
262
+ accepted: "a non-empty string",
263
+ current: () => advisorJevModelRef,
264
+ persisted: true,
265
+ type: "string",
266
+ },
267
+ advisorJevPricePerMtok: {
268
+ accepted: "a positive number",
269
+ current: () => advisorJevPricePerMtokRef,
270
+ persisted: true,
271
+ type: "number",
272
+ validate: isValidJevPricePerMtok,
273
+ },
274
+ advisorJevTimeoutMs: {
275
+ accepted: "a positive safe integer",
276
+ current: () => advisorJevTimeoutMsRef,
277
+ persisted: true,
278
+ type: "number",
279
+ validate: isValidJevTimeoutMs,
280
+ },
281
+ advisorJevTransport: {
282
+ accepted: JEV_TRANSPORTS.join(", "),
283
+ current: () => advisorJevTransportRef,
284
+ persisted: true,
285
+ type: "enum",
286
+ validate: isValidJevTransport,
287
+ },
288
+ advisorJevTurnGateEveryTurns: {
289
+ accepted: "a non-negative safe integer (0 disables the turn gate)",
290
+ current: () => advisorJevTurnGateEveryTurnsRef,
291
+ persisted: true,
292
+ type: "number",
293
+ validate: nonNegativeSafeInteger,
294
+ },
295
+ advisorJevTurnGateNoulThreshold: {
296
+ accepted: "a number between 0.5 and 1 inclusive",
297
+ current: () => advisorJevTurnGateNoulThresholdRef,
298
+ persisted: true,
299
+ type: "number",
300
+ validate: isValidJevSkipConfidence,
301
+ },
174
302
  advisorLoopThreshold: {
175
303
  accepted: "a safe integer of at least 2",
176
304
  current: () => advisorLoopThresholdRef,
@@ -185,6 +313,13 @@ export const CONFIG_SCHEMA = {
185
313
  type: "number",
186
314
  validate: isValidMaxCallsPerSession,
187
315
  },
316
+ advisorModelWhitelist: {
317
+ accepted: "an array of non-empty provider/model strings",
318
+ current: () => [...advisorModelWhitelistRef],
319
+ persisted: true,
320
+ type: "array",
321
+ validate: isValidAdvisorModelWhitelist,
322
+ },
188
323
  advisorOutcomeLogging: {
189
324
  accepted: "true or false",
190
325
  current: () => advisorOutcomeLoggingRef,
@@ -303,6 +438,9 @@ export const CONFIG_SCHEMA = {
303
438
 
304
439
  export type ConfigKey = keyof typeof CONFIG_SCHEMA;
305
440
 
441
+ // SAFETY: CONFIG_SCHEMA satisfies Record<keyof AdvisorConfig, ConfigKeySchema>, so Object.keys yields exactly ConfigKey.
442
+ export const configKeys = Object.keys(CONFIG_SCHEMA) as ConfigKey[];
443
+
306
444
  /** Keys saveConfig captures, diffs, and writes to advisor.json. */
307
445
  export type PersistedConfigKey = {
308
446
  [Key in ConfigKey]: (typeof CONFIG_SCHEMA)[Key]["persisted"] extends true
@@ -310,11 +448,10 @@ export type PersistedConfigKey = {
310
448
  : never;
311
449
  }[ConfigKey];
312
450
 
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[];
451
+ export const SAVED_CONFIG_KEYS: readonly PersistedConfigKey[] =
452
+ configKeys.filter(
453
+ (key): key is PersistedConfigKey => CONFIG_SCHEMA[key].persisted
454
+ );
318
455
 
319
456
  /** Widened per-key view for consumers that index by dynamic key. */
320
457
  const SCHEMA_BY_KEY: Record<ConfigKey, ConfigKeySchema> = CONFIG_SCHEMA;
@@ -1,11 +1,24 @@
1
1
  import type { GitContextLevel } from "../git.ts";
2
2
  import {
3
- type AdvisorToolPolicies,
4
3
  DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS,
5
4
  DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES,
6
5
  DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
7
6
  DEFAULT_CONTEXT_MAX_CHARS,
8
- type GateFailureMode,
7
+ DEFAULT_JEV_DIGEST_MAX_CHARS,
8
+ DEFAULT_JEV_FILTER_NOUL_MARGIN,
9
+ DEFAULT_JEV_FILTER_OVERRIDE_WINDOW,
10
+ DEFAULT_JEV_FILTER_SKIP_CONFIDENCE,
11
+ DEFAULT_JEV_MODEL,
12
+ DEFAULT_JEV_PRICE_PER_MTOK,
13
+ DEFAULT_JEV_TIMEOUT_MS,
14
+ DEFAULT_JEV_TRANSPORT,
15
+ DEFAULT_JEV_TURN_GATE_EVERY_TURNS,
16
+ DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD,
17
+ } from "./types.ts";
18
+ import type {
19
+ AdvisorToolPolicies,
20
+ GateFailureMode,
21
+ JevTransport,
9
22
  } from "./types.ts";
10
23
 
11
24
  // An empty ref means no model has been selected yet.
@@ -25,11 +38,26 @@ export let advisorBlockOnBlockedRef = true;
25
38
  export let advisorAutoLoopGateRef = true;
26
39
  export let advisorLoopThresholdRef = 3;
27
40
  export let advisorMaxCallsPerSessionRef: number | undefined;
41
+ export let advisorModelWhitelistRef: string[] = [];
28
42
  export let advisorSessionSummaryRef = false;
29
43
  export let simpleModeRef = false;
30
44
  export let alwaysOnRef = false;
31
45
  export let advisorFailureModeRef: GateFailureMode = "block-session";
32
46
  export let advisorHerdrIntegrationRef = true;
47
+ export let advisorJevFilterEnabledRef = false;
48
+ export let advisorJevFilterSkipConfidenceRef =
49
+ DEFAULT_JEV_FILTER_SKIP_CONFIDENCE;
50
+ export let advisorJevFilterNoulMarginRef = DEFAULT_JEV_FILTER_NOUL_MARGIN;
51
+ export let advisorJevFilterOverrideWindowRef =
52
+ DEFAULT_JEV_FILTER_OVERRIDE_WINDOW;
53
+ export let advisorJevModelRef = DEFAULT_JEV_MODEL;
54
+ export let advisorJevTimeoutMsRef = DEFAULT_JEV_TIMEOUT_MS;
55
+ export let advisorJevDigestMaxCharsRef = DEFAULT_JEV_DIGEST_MAX_CHARS;
56
+ export let advisorJevPricePerMtokRef = DEFAULT_JEV_PRICE_PER_MTOK;
57
+ export let advisorJevTransportRef: JevTransport = DEFAULT_JEV_TRANSPORT;
58
+ export let advisorJevTurnGateEveryTurnsRef = DEFAULT_JEV_TURN_GATE_EVERY_TURNS;
59
+ export let advisorJevTurnGateNoulThresholdRef =
60
+ DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD;
33
61
  export let advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
34
62
  export let advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
35
63
  export let advisorRedactSecretsRef = false;
@@ -100,6 +128,11 @@ export const setAdvisorLoopThresholdRef = (value: number) => {
100
128
  export const setAdvisorMaxCallsPerSessionRef = (value: number | undefined) => {
101
129
  advisorMaxCallsPerSessionRef = value;
102
130
  };
131
+ export const setAdvisorModelWhitelistRef = (models: string[]) => {
132
+ advisorModelWhitelistRef = [
133
+ ...new Set(models.map((model) => model.trim()).filter(Boolean)),
134
+ ];
135
+ };
103
136
  export const setAdvisorSessionSummaryRef = (enabled: boolean) => {
104
137
  advisorSessionSummaryRef = enabled;
105
138
  };
@@ -117,6 +150,39 @@ export const setAdvisorFailureModeRef = (value: GateFailureMode) => {
117
150
  export const setAdvisorHerdrIntegrationRef = (enabled: boolean) => {
118
151
  advisorHerdrIntegrationRef = enabled;
119
152
  };
153
+ export const setAdvisorJevFilterEnabledRef = (enabled: boolean) => {
154
+ advisorJevFilterEnabledRef = enabled;
155
+ };
156
+ export const setAdvisorJevFilterSkipConfidenceRef = (value: number) => {
157
+ advisorJevFilterSkipConfidenceRef = value;
158
+ };
159
+ export const setAdvisorJevFilterNoulMarginRef = (value: number) => {
160
+ advisorJevFilterNoulMarginRef = value;
161
+ };
162
+ export const setAdvisorJevFilterOverrideWindowRef = (value: number) => {
163
+ advisorJevFilterOverrideWindowRef = value;
164
+ };
165
+ export const setAdvisorJevModelRef = (model: string | undefined) => {
166
+ advisorJevModelRef = model?.trim() || DEFAULT_JEV_MODEL;
167
+ };
168
+ export const setAdvisorJevTimeoutMsRef = (value: number) => {
169
+ advisorJevTimeoutMsRef = value;
170
+ };
171
+ export const setAdvisorJevDigestMaxCharsRef = (value: number) => {
172
+ advisorJevDigestMaxCharsRef = value;
173
+ };
174
+ export const setAdvisorJevPricePerMtokRef = (value: number) => {
175
+ advisorJevPricePerMtokRef = value;
176
+ };
177
+ export const setAdvisorJevTransportRef = (value: JevTransport) => {
178
+ advisorJevTransportRef = value;
179
+ };
180
+ export const setAdvisorJevTurnGateEveryTurnsRef = (value: number) => {
181
+ advisorJevTurnGateEveryTurnsRef = value;
182
+ };
183
+ export const setAdvisorJevTurnGateNoulThresholdRef = (value: number) => {
184
+ advisorJevTurnGateNoulThresholdRef = value;
185
+ };
120
186
  export const setAdvisorToolResultMaxLinesRef = (value: number) => {
121
187
  advisorToolResultMaxLinesRef = value;
122
188
  };
@@ -172,8 +238,20 @@ export const getAdvisorSettings = () => ({
172
238
  gitContext: advisorGitContextRef,
173
239
  gitContextMaxChars: advisorGitContextMaxCharsRef,
174
240
  herdrIntegration: advisorHerdrIntegrationRef,
241
+ jevDigestMaxChars: advisorJevDigestMaxCharsRef,
242
+ jevFilterEnabled: advisorJevFilterEnabledRef,
243
+ jevFilterNoulMargin: advisorJevFilterNoulMarginRef,
244
+ jevFilterOverrideWindow: advisorJevFilterOverrideWindowRef,
245
+ jevFilterSkipConfidence: advisorJevFilterSkipConfidenceRef,
246
+ jevModel: advisorJevModelRef,
247
+ jevPricePerMtok: advisorJevPricePerMtokRef,
248
+ jevTimeoutMs: advisorJevTimeoutMsRef,
249
+ jevTransport: advisorJevTransportRef,
250
+ jevTurnGateEveryTurns: advisorJevTurnGateEveryTurnsRef,
251
+ jevTurnGateNoulThreshold: advisorJevTurnGateNoulThresholdRef,
175
252
  loopThreshold: advisorLoopThresholdRef,
176
253
  maxCallsPerSession: advisorMaxCallsPerSessionRef,
254
+ modelWhitelist: [...advisorModelWhitelistRef],
177
255
  outcomeLogging: advisorOutcomeLoggingRef,
178
256
  planGate: advisorPlanGateRef,
179
257
  redactSecrets: advisorRedactSecretsRef,
@@ -1,17 +1,18 @@
1
1
  import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import {
4
- CONFIG_DIR_NAME,
5
- type ExtensionContext,
6
- getAgentDir,
7
- } from "@earendil-works/pi-coding-agent";
3
+
4
+ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
5
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
+
7
+ import { isRecord, isString } from "../content-utils.ts";
8
+ import type { RecordValue } from "../content-utils.ts";
8
9
  import { applyConfig, resetDefaults } from "./defaults.ts";
9
10
  import {
10
11
  CONFIG_SCHEMA,
11
12
  configuredModelRef,
12
- type PersistedConfigKey,
13
13
  SAVED_CONFIG_KEYS,
14
14
  } from "./schema.ts";
15
+ import type { PersistedConfigKey } from "./schema.ts";
15
16
  import { setAdvisorOutcomeLoggingRef, setPersistedModelRefs } from "./state.ts";
16
17
  import type { AdvisorConfig } from "./types.ts";
17
18
  import { unknownConfigKeys, validateConfig } from "./validation.ts";
@@ -29,6 +30,7 @@ type ConfigState = {
29
30
  };
30
31
 
31
32
  const currentConfigState = (): ConfigState =>
33
+ // SAFETY: SAVED_CONFIG_KEYS enumerates exactly the ConfigState keys, so fromEntries yields ConfigState.
32
34
  Object.fromEntries(
33
35
  SAVED_CONFIG_KEYS.map((key) => [key, CONFIG_SCHEMA[key].current()])
34
36
  ) as ConfigState;
@@ -36,12 +38,15 @@ const currentConfigState = (): ConfigState =>
36
38
  const sameConfigValue = <Value>(left: Value, right: Value) =>
37
39
  JSON.stringify(left) === JSON.stringify(right);
38
40
 
39
- const readExistingConfig = (path: string): Record<string, unknown> => {
41
+ /** Keys advisor.json may hold that are not AdvisorConfig settings but are
42
+ * still legitimate (hand-placed secrets). They stay out of CONFIG_SCHEMA, are
43
+ * preserved like any unknown key, and never trigger the typo warning. */
44
+ const RESERVED_ADVISOR_JSON_KEYS = new Set(["typesafe_api_key"]);
45
+
46
+ export const readExistingConfig = (path: string): RecordValue => {
40
47
  try {
41
- const parsed = JSON.parse(readFileSync(path, "utf8"));
42
- return parsed && typeof parsed === "object" && !Array.isArray(parsed)
43
- ? (parsed as Record<string, unknown>)
44
- : {};
48
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
49
+ return isRecord(parsed) ? parsed : {};
45
50
  } catch {
46
51
  return {};
47
52
  }
@@ -56,12 +61,14 @@ const shouldPersistConfigKey = (
56
61
  (key !== "executor" || persistExecutor);
57
62
 
58
63
  const applyChangedConfigValues = (
59
- data: Record<string, unknown>,
64
+ existing: RecordValue,
60
65
  current: ConfigState,
61
66
  changedKeys: readonly SavedConfigKey[],
62
67
  persistAdvisor: boolean,
63
68
  persistExecutor: boolean
64
- ) => {
69
+ ): RecordValue => {
70
+ const dropped = new Set<string>();
71
+ const data: RecordValue = { ...existing };
65
72
  for (const key of changedKeys) {
66
73
  if (!shouldPersistConfigKey(key, persistAdvisor, persistExecutor)) {
67
74
  continue;
@@ -69,11 +76,21 @@ const applyChangedConfigValues = (
69
76
  const value = current[key];
70
77
  const isEmptyModelRef = (key === "advisor" || key === "executor") && !value;
71
78
  if (value === undefined || isEmptyModelRef) {
72
- delete data[key];
79
+ dropped.add(key);
73
80
  } else {
74
81
  data[key] = value;
75
82
  }
76
83
  }
84
+ if (dropped.size === 0) {
85
+ return data;
86
+ }
87
+ const retained: RecordValue = {};
88
+ for (const key of Object.keys(data)) {
89
+ if (!dropped.has(key)) {
90
+ retained[key] = data[key];
91
+ }
92
+ }
93
+ return retained;
77
94
  };
78
95
 
79
96
  // Tracks the last load/save state so a later save merges fresh disk contents instead of overwriting an external edit.
@@ -81,7 +98,7 @@ let loadedConfigState: ConfigState | undefined;
81
98
  let loadedConfigPath: string | undefined;
82
99
 
83
100
  const readConfig = (path: string): AdvisorConfig => {
84
- const config = JSON.parse(readFileSync(path, "utf8"));
101
+ const config = JSON.parse(readFileSync(path, "utf-8"));
85
102
  validateConfig(config, path);
86
103
  return config;
87
104
  };
@@ -137,8 +154,8 @@ export const loadConfig = (_ctx: ExtensionContext) => {
137
154
  );
138
155
  if (globalConfig) {
139
156
  applyConfig(globalConfig);
140
- const unknownKeys = unknownConfigKeys(
141
- globalConfig as Record<string, unknown>
157
+ const unknownKeys = unknownConfigKeys(globalConfig).filter(
158
+ (key) => !RESERVED_ADVISOR_JSON_KEYS.has(key)
142
159
  );
143
160
  const warningIdentity = `${global}:${configIdentity(global)}`;
144
161
  if (
@@ -191,9 +208,8 @@ export const saveConfig = (
191
208
  if (changedKeys.length === 0) {
192
209
  return path;
193
210
  }
194
- const data = { ...existing };
195
- applyChangedConfigValues(
196
- data,
211
+ const data = applyChangedConfigValues(
212
+ existing,
197
213
  current,
198
214
  changedKeys,
199
215
  persistAdvisor,
@@ -206,14 +222,14 @@ export const saveConfig = (
206
222
  nextLoadedState.advisor = baseline
207
223
  ? baseline.advisor
208
224
  : configuredModelRef(
209
- typeof existing.advisor === "string" ? existing.advisor : undefined
225
+ isString(existing.advisor) ? existing.advisor : undefined
210
226
  );
211
227
  }
212
228
  if (!persistExecutor) {
213
229
  nextLoadedState.executor = baseline
214
230
  ? baseline.executor
215
231
  : configuredModelRef(
216
- typeof existing.executor === "string" ? existing.executor : undefined
232
+ isString(existing.executor) ? existing.executor : undefined
217
233
  );
218
234
  }
219
235
  loadedConfigState = nextLoadedState;
@@ -1,14 +1,42 @@
1
1
  import {
2
+ DEFAULT_MAX_BYTES as PI_DEFAULT_MAX_BYTES,
3
+ DEFAULT_MAX_LINES as PI_DEFAULT_MAX_LINES,
4
+ } from "@earendil-works/pi-coding-agent";
5
+
6
+ import type { GitContextLevel } from "../git.ts";
7
+
8
+ export {
2
9
  DEFAULT_MAX_BYTES,
3
10
  DEFAULT_MAX_LINES,
4
11
  } from "@earendil-works/pi-coding-agent";
5
- import type { GitContextLevel } from "../git.ts";
6
12
 
7
13
  export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
8
14
  export const MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
9
- export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = DEFAULT_MAX_LINES;
10
- export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES = DEFAULT_MAX_BYTES;
15
+ const DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = PI_DEFAULT_MAX_LINES;
16
+ const DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES = PI_DEFAULT_MAX_BYTES;
17
+ export {
18
+ DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES,
19
+ DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
20
+ };
11
21
  export const DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS = 20_000;
22
+ export const DEFAULT_JEV_MODEL = "jev-latest";
23
+ // Provisional: the latency benchmark was dropped from scope; revisit if live
24
+ // measurements suggest a different bound.
25
+ export const DEFAULT_JEV_TIMEOUT_MS = 8000;
26
+ export const DEFAULT_JEV_DIGEST_MAX_CHARS = 4000;
27
+ export const DEFAULT_JEV_PRICE_PER_MTOK = 0.042;
28
+ export const DEFAULT_JEV_FILTER_SKIP_CONFIDENCE = 0.85;
29
+ export const DEFAULT_JEV_FILTER_NOUL_MARGIN = 0.35;
30
+ export const DEFAULT_JEV_FILTER_OVERRIDE_WINDOW = 10;
31
+ export const DEFAULT_JEV_TURN_GATE_EVERY_TURNS = 0;
32
+ export const DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD = 0.8;
33
+ export type JevTransport = "auto" | "typesafe" | "openrouter";
34
+ export const DEFAULT_JEV_TRANSPORT: JevTransport = "auto";
35
+ export const JEV_TRANSPORTS: JevTransport[] = [
36
+ "auto",
37
+ "typesafe",
38
+ "openrouter",
39
+ ];
12
40
 
13
41
  export type AdvisorToolPolicy = "full" | "summary" | "exclude";
14
42
  export type AdvisorToolPolicies = Record<string, AdvisorToolPolicy>;
@@ -40,8 +68,20 @@ export interface AdvisorConfig {
40
68
  advisorGitContext?: GitContextLevel;
41
69
  advisorGitContextMaxChars?: number;
42
70
  advisorHerdrIntegration?: boolean;
71
+ advisorJevDigestMaxChars?: number;
72
+ advisorJevFilterEnabled?: boolean;
73
+ advisorJevFilterNoulMargin?: number;
74
+ advisorJevFilterOverrideWindow?: number;
75
+ advisorJevFilterSkipConfidence?: number;
76
+ advisorJevModel?: string;
77
+ advisorJevPricePerMtok?: number;
78
+ advisorJevTimeoutMs?: number;
79
+ advisorJevTransport?: JevTransport;
80
+ advisorJevTurnGateEveryTurns?: number;
81
+ advisorJevTurnGateNoulThreshold?: number;
43
82
  advisorLoopThreshold?: number;
44
83
  advisorMaxCallsPerSession?: number;
84
+ advisorModelWhitelist?: string[];
45
85
  advisorOutcomeLogging?: boolean;
46
86
  advisorPlanGate?: boolean;
47
87
  advisorRedactSecrets?: boolean;
@@ -61,3 +101,5 @@ export interface AdvisorConfig {
61
101
  showUsageFooter?: boolean;
62
102
  simpleMode?: boolean;
63
103
  }
104
+
105
+ export type AdvisorConfigValue = AdvisorConfig[keyof AdvisorConfig];