pi-advisor-flow 0.7.0 → 0.8.1-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +27 -18
  2. package/README.md +9 -8
  3. package/dist/index.js +1968 -1081
  4. package/extensions/index.ts +2 -1
  5. package/package.json +48 -35
  6. package/src/attachments.ts +11 -10
  7. package/src/child-session.ts +15 -0
  8. package/src/commands/activation-preparation.ts +1 -0
  9. package/src/commands/activation.ts +12 -3
  10. package/src/commands/lifecycle.ts +7 -1
  11. package/src/commands/manual-command.ts +7 -1
  12. package/src/commands/manual-consultation.ts +102 -92
  13. package/src/commands/manual-progress.ts +7 -5
  14. package/src/commands/model-commands.ts +1 -1
  15. package/src/commands/model-options.ts +14 -3
  16. package/src/commands/model-picker.ts +1 -0
  17. package/src/commands/registration.ts +1 -0
  18. package/src/commands/renderers.ts +67 -55
  19. package/src/commands/runtime.ts +16 -14
  20. package/src/commands/settings-commands.ts +10 -4
  21. package/src/commands/settings-persistence.ts +16 -2
  22. package/src/commands/types.ts +1 -0
  23. package/src/commands.ts +0 -1
  24. package/src/config/args.ts +1 -1
  25. package/src/config/defaults.ts +8 -1
  26. package/src/config/schema.ts +42 -17
  27. package/src/config/state.ts +12 -3
  28. package/src/config/storage.ts +34 -23
  29. package/src/config/types.ts +16 -3
  30. package/src/config/validation.ts +33 -21
  31. package/src/config.ts +3 -0
  32. package/src/content-utils.ts +24 -4
  33. package/src/conversation.ts +16 -15
  34. package/src/git.ts +10 -3
  35. package/src/herdr-block.ts +83 -0
  36. package/src/herdr-shared.ts +108 -0
  37. package/src/herdr.ts +30 -192
  38. package/src/jev/client.ts +75 -85
  39. package/src/jev/failure.ts +19 -0
  40. package/src/jev/key-store.ts +26 -11
  41. package/src/jev/ledger.ts +58 -64
  42. package/src/jev/questions.ts +26 -23
  43. package/src/jev/state.ts +15 -12
  44. package/src/jev/transport.ts +8 -3
  45. package/src/model-stream.ts +35 -37
  46. package/src/outcomes.ts +15 -11
  47. package/src/pi-settings.ts +4 -9
  48. package/src/preferences.ts +4 -2
  49. package/src/redaction.ts +10 -12
  50. package/src/scout-context.ts +59 -60
  51. package/src/scout-curation.ts +4 -13
  52. package/src/scout-groups.ts +178 -175
  53. package/src/scout-protocol.ts +74 -70
  54. package/src/scout-reconstruct.ts +1 -1
  55. package/src/scout.ts +128 -77
  56. package/src/session-state.ts +64 -49
  57. package/src/tool-result-cap.ts +18 -17
  58. package/src/tools/consult-context.ts +7 -15
  59. package/src/tools/consultation.ts +14 -4
  60. package/src/tools/gate-policy.ts +1 -0
  61. package/src/tools/gate-protocol.ts +36 -23
  62. package/src/tools/jev-filter.ts +43 -41
  63. package/src/tools/jev-turn-gate.ts +29 -9
  64. package/src/tools/loop-gate.ts +31 -20
  65. package/src/tools/model-access.ts +46 -0
  66. package/src/tools/prompts.ts +20 -25
  67. package/src/tools/register-ask-advisor.ts +36 -16
  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 +146 -128
  71. package/src/tools/registration.ts +12 -14
  72. package/src/tools/render-advisor-result.ts +35 -22
  73. package/src/tools/render-common.ts +16 -19
  74. package/src/tools/scout-status.ts +42 -27
  75. package/src/tools/types.ts +1 -0
  76. package/src/tools.ts +0 -1
  77. package/src/ui/jev-setup-submenu.ts +65 -59
  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 +5 -11
  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 +38 -1
  87. package/src/ui/settings-list-adapter.ts +15 -12
  88. package/src/ui/settings-mutations.ts +157 -70
  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 +14 -0
  92. package/src/ui.ts +0 -1
  93. package/src/usage.ts +26 -19
@@ -2,6 +2,7 @@ import type {
2
2
  ExtensionAPI,
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
+
5
6
  import { activateAdvisor } from "./activation.ts";
6
7
  import { registerCommandLifecycle } from "./lifecycle.ts";
7
8
  import { registerManualCommand } from "./manual-command.ts";
@@ -1,6 +1,12 @@
1
1
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
2
+ import type {
3
+ EntryRenderer,
4
+ MessageRenderer,
5
+ } from "@earendil-works/pi-coding-agent";
2
6
  import { Box, Markdown, Text } from "@earendil-works/pi-tui";
7
+
3
8
  import { getAdvisorSettings } from "../config/state.ts";
9
+ import { isString } from "../content-utils.ts";
4
10
  import {
5
11
  adviceForDisplay,
6
12
  hasSoundVerdict,
@@ -11,66 +17,72 @@ import { formatAdvisorUsage } from "../usage.ts";
11
17
  import { ManualAdvisorProgressComponent } from "./manual-progress.ts";
12
18
  import type { CommandRuntime } from "./types.ts";
13
19
 
20
+ interface ManualCallEntryDetails {
21
+ progressId?: string;
22
+ question?: string;
23
+ }
24
+
25
+ interface ManualResultDetails {
26
+ advisor?: string;
27
+ text?: string;
28
+ usage?: unknown;
29
+ }
30
+
31
+ const manualCallRenderer =
32
+ (runtime: CommandRuntime): EntryRenderer<ManualCallEntryDetails> =>
33
+ (entry, { expanded }, theme) => {
34
+ const { progressId, question } = entry.data ?? {};
35
+ const progress = progressId
36
+ ? runtime.manualProgress.get(progressId)
37
+ : undefined;
38
+ return progress
39
+ ? new ManualAdvisorProgressComponent(
40
+ question,
41
+ progress,
42
+ Boolean(expanded),
43
+ theme
44
+ )
45
+ : renderAdvisorCallBox(question, theme);
46
+ };
47
+
48
+ const manualResultRenderer: MessageRenderer<ManualResultDetails> = (
49
+ message,
50
+ { expanded },
51
+ theme
52
+ ) => {
53
+ const { details } = message;
54
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
55
+ const advice =
56
+ details?.text ??
57
+ (isString(message.content)
58
+ ? message.content
59
+ : "(Advisor returned no advice.)");
60
+ // Manual consultations must render exactly like an Executor ask_advisor call.
61
+ box.addChild(
62
+ new Text(renderAdvisorResponseHeader(hasSoundVerdict(advice), theme), 0, 0)
63
+ );
64
+ if (details?.advisor) {
65
+ box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
66
+ }
67
+ if (getAdvisorSettings().showUsageDetails) {
68
+ const usage = formatAdvisorUsage(details?.usage);
69
+ if (usage) {
70
+ box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
71
+ }
72
+ }
73
+ box.addChild(
74
+ new Markdown(adviceForDisplay(advice, expanded), 0, 0, getMarkdownTheme())
75
+ );
76
+ return box;
77
+ };
78
+
14
79
  export const registerCommandRenderers = (runtime: CommandRuntime) => {
15
80
  runtime.pi.registerEntryRenderer?.(
16
81
  "advisor-manual-call",
17
- (entry, { expanded }, theme) => {
18
- const { progressId, question } = (entry.data ?? {}) as {
19
- progressId?: string;
20
- question?: string;
21
- };
22
- const progress = progressId
23
- ? runtime.manualProgress.get(progressId)
24
- : undefined;
25
- return progress
26
- ? new ManualAdvisorProgressComponent(
27
- question,
28
- progress,
29
- Boolean(expanded),
30
- theme
31
- )
32
- : renderAdvisorCallBox(question, theme);
33
- }
82
+ manualCallRenderer(runtime)
34
83
  );
35
-
36
84
  runtime.pi.registerMessageRenderer?.(
37
85
  "advisor-manual-result",
38
- (message, { expanded }, theme) => {
39
- const details = message.details as
40
- | { advisor?: string; text?: string; usage?: unknown }
41
- | undefined;
42
- const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
43
- const advice =
44
- details?.text ??
45
- (typeof message.content === "string"
46
- ? message.content
47
- : "(Advisor returned no advice.)");
48
- // Manual consultations must render exactly like an Executor ask_advisor call.
49
- box.addChild(
50
- new Text(
51
- renderAdvisorResponseHeader(hasSoundVerdict(advice), theme),
52
- 0,
53
- 0
54
- )
55
- );
56
- if (details?.advisor) {
57
- box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
58
- }
59
- if (getAdvisorSettings().showUsageDetails) {
60
- const usage = formatAdvisorUsage(details?.usage);
61
- if (usage) {
62
- box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
63
- }
64
- }
65
- box.addChild(
66
- new Markdown(
67
- adviceForDisplay(advice, expanded),
68
- 0,
69
- 0,
70
- getMarkdownTheme()
71
- )
72
- );
73
- return box;
74
- }
86
+ manualResultRenderer
75
87
  );
76
88
  };
@@ -2,6 +2,7 @@ import type {
2
2
  ExtensionAPI,
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
+
5
6
  import { getAdvisorSettings } from "../config/state.ts";
6
7
  import { notifyHerdrAdvisorFailure } from "../herdr.ts";
7
8
  import type { AdvisorSessionState } from "../session-state.ts";
@@ -25,6 +26,18 @@ export const notify = (
25
26
  }
26
27
  };
27
28
 
29
+ const reportManualBudgetExhausted = (ctx: ExtensionContext) => {
30
+ const message = "Advisor call budget exhausted for this session.";
31
+ notify(ctx, message, "warning");
32
+ notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
33
+ };
34
+
35
+ const requestManualRender = (ctx: ExtensionContext) => {
36
+ if (ctx.hasUI) {
37
+ ctx.ui.setStatus("advisor-manual", undefined);
38
+ }
39
+ };
40
+
28
41
  class CommandRuntime implements CommandRuntimeContract {
29
42
  readonly advisorSessionState: AdvisorSessionState;
30
43
  readonly manualConsultations = new Map<AbortController, symbol>();
@@ -34,7 +47,9 @@ class CommandRuntime implements CommandRuntimeContract {
34
47
  ReturnType<typeof setInterval>
35
48
  >();
36
49
  readonly pi: ExtensionAPI;
50
+ readonly reportManualBudgetExhausted = reportManualBudgetExhausted;
37
51
  readonly requestAdvisor: ManualConsult;
52
+ readonly requestManualRender = requestManualRender;
38
53
  readonly scoutStatus: ScoutStatusManager;
39
54
  manualProgressSequence = 0;
40
55
  pendingExecutorModelRef: string | undefined;
@@ -59,8 +74,7 @@ class CommandRuntime implements CommandRuntimeContract {
59
74
  undefined,
60
75
  undefined,
61
76
  undefined,
62
- onScout,
63
- undefined
77
+ onScout
64
78
  ));
65
79
  }
66
80
 
@@ -94,18 +108,6 @@ class CommandRuntime implements CommandRuntimeContract {
94
108
  );
95
109
  }
96
110
  }
97
-
98
- reportManualBudgetExhausted(ctx: ExtensionContext) {
99
- const message = "Advisor call budget exhausted for this session.";
100
- notify(ctx, message, "warning");
101
- notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
102
- }
103
-
104
- requestManualRender(ctx: ExtensionContext) {
105
- if (ctx.hasUI) {
106
- ctx.ui.setStatus("advisor-manual", undefined);
107
- }
108
- }
109
111
  }
110
112
 
111
113
  export const createCommandRuntime = (
@@ -7,7 +7,11 @@ import {
7
7
  import { saveConfig } from "../config/storage.ts";
8
8
  import { AdvisorSettingsSelector } from "../ui/settings-selector.ts";
9
9
  import { loadCommandConfig } from "./activation-preparation.ts";
10
- import { CONTEXT_PRESETS, EFFORT_LEVELS } from "./model-options.ts";
10
+ import {
11
+ CONTEXT_PRESETS,
12
+ EFFORT_LEVELS,
13
+ getConfiguredModelRefs,
14
+ } from "./model-options.ts";
11
15
  import { notify } from "./runtime.ts";
12
16
  import { saveAdvisorSettings } from "./settings-persistence.ts";
13
17
  import type { CommandRuntime } from "./types.ts";
@@ -21,12 +25,14 @@ export const registerSettingsCommands = (runtime: CommandRuntime) => {
21
25
  }
22
26
 
23
27
  const initial = getAdvisorSettings();
24
- await ctx.ui.custom<void>(
25
- (tui, theme, _keybindings, done) =>
28
+ await ctx.ui.custom<undefined>(
29
+ (tui, theme, keybindings, done) =>
26
30
  new AdvisorSettingsSelector({
27
31
  effortLevels: EFFORT_LEVELS,
28
32
  initial,
29
- onCancel: () => done(),
33
+ keybindings,
34
+ modelRefs: getConfiguredModelRefs(ctx),
35
+ onCancel: () => done(undefined),
30
36
  onChange: (settings) => {
31
37
  try {
32
38
  saveAdvisorSettings(ctx, settings);
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import {
3
4
  getPersistedModelRefs,
4
5
  setAdvisorAutoLoopGateRef,
@@ -25,6 +26,7 @@ import {
25
26
  setAdvisorJevTurnGateNoulThresholdRef,
26
27
  setAdvisorLoopThresholdRef,
27
28
  setAdvisorMaxCallsPerSessionRef,
29
+ setAdvisorModelWhitelistRef,
28
30
  setAdvisorOutcomeLoggingRef,
29
31
  setAdvisorPlanGateRef,
30
32
  setAdvisorRedactSecretsRef,
@@ -56,8 +58,7 @@ import {
56
58
  } from "../config/types.ts";
57
59
  import type { AdvisorSettings } from "../ui/types.ts";
58
60
 
59
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one settings form maps every persisted control.
60
- const applyAdvisorSettings = (settings: AdvisorSettings) => {
61
+ const applySessionSettings = (settings: AdvisorSettings) => {
61
62
  setAdvisorEffortRef(
62
63
  settings.effort === "Default (Model Default)" ? undefined : settings.effort
63
64
  );
@@ -71,6 +72,7 @@ const applyAdvisorSettings = (settings: AdvisorSettings) => {
71
72
  setAdvisorAutoLoopGateRef(settings.autoLoopGate ?? true);
72
73
  setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
73
74
  setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
75
+ setAdvisorModelWhitelistRef(settings.modelWhitelist ?? []);
74
76
  setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
75
77
  setAdvisorScoutEnabledRef(settings.scoutEnabled ?? false);
76
78
  setShowUsageDetailsRef(settings.showUsageDetails ?? true);
@@ -79,6 +81,9 @@ const applyAdvisorSettings = (settings: AdvisorSettings) => {
79
81
  setAlwaysOnRef(settings.alwaysOn ?? false);
80
82
  setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
81
83
  setAdvisorHerdrIntegrationRef(settings.herdrIntegration ?? true);
84
+ };
85
+
86
+ const applyJevSettings = (settings: AdvisorSettings) => {
82
87
  setAdvisorJevFilterEnabledRef(settings.jevFilterEnabled ?? false);
83
88
  setAdvisorJevFilterSkipConfidenceRef(
84
89
  settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE
@@ -104,6 +109,9 @@ const applyAdvisorSettings = (settings: AdvisorSettings) => {
104
109
  setAdvisorJevTurnGateNoulThresholdRef(
105
110
  settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD
106
111
  );
112
+ };
113
+
114
+ const applyDisclosureSettings = (settings: AdvisorSettings) => {
107
115
  setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
108
116
  setAdvisorToolResultMaxBytesRef(settings.toolResultMaxBytes ?? 50 * 1024);
109
117
  setAdvisorRedactSecretsRef(settings.redactSecrets ?? false);
@@ -115,6 +123,12 @@ const applyAdvisorSettings = (settings: AdvisorSettings) => {
115
123
  setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
116
124
  };
117
125
 
126
+ const applyAdvisorSettings = (settings: AdvisorSettings) => {
127
+ applySessionSettings(settings);
128
+ applyJevSettings(settings);
129
+ applyDisclosureSettings(settings);
130
+ };
131
+
118
132
  export const saveAdvisorSettings = (
119
133
  ctx: ExtensionContext,
120
134
  settings: AdvisorSettings
@@ -2,6 +2,7 @@ import type {
2
2
  ExtensionAPI,
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
+
5
6
  import type { GitContextLevel } from "../git.ts";
6
7
  import type { ScoutLifecycleEvent } from "../scout.ts";
7
8
  import type { AdvisorSessionState } from "../session-state.ts";
package/src/commands.ts CHANGED
@@ -1,4 +1,3 @@
1
- // biome-ignore lint/performance/noBarrelFile: this facade intentionally preserves the public command module contract.
2
1
  export { registerCommands } from "./commands/registration.ts";
3
2
  export type {
4
3
  CommandDependencies,
@@ -9,7 +9,7 @@ import {
9
9
  import { MAX_CONTEXT_MAX_CHARS } from "./types.ts";
10
10
  import { isValidContextMaxChars } from "./validation.ts";
11
11
 
12
- const ARGUMENT_WHITESPACE = /\s+/;
12
+ const ARGUMENT_WHITESPACE = /\s+/u;
13
13
 
14
14
  export const parseArgs = (args: string): string | undefined => {
15
15
  let nextExecutor = executorRef;
@@ -23,6 +23,7 @@ import {
23
23
  setAdvisorJevTurnGateNoulThresholdRef,
24
24
  setAdvisorLoopThresholdRef,
25
25
  setAdvisorMaxCallsPerSessionRef,
26
+ setAdvisorModelWhitelistRef,
26
27
  setAdvisorOutcomeLoggingRef,
27
28
  setAdvisorPlanGateRef,
28
29
  setAdvisorRedactSecretsRef,
@@ -77,6 +78,7 @@ export const resetDefaults = () => {
77
78
  setAdvisorAutoLoopGateRef(true);
78
79
  setAdvisorLoopThresholdRef(3);
79
80
  setAdvisorMaxCallsPerSessionRef(undefined);
81
+ setAdvisorModelWhitelistRef([]);
80
82
  setAdvisorSessionSummaryRef(false);
81
83
  setSimpleModeRef(false);
82
84
  setAlwaysOnRef(false);
@@ -114,7 +116,7 @@ const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
114
116
  ) => {
115
117
  const value = config[key];
116
118
  if (value !== undefined) {
117
- apply(value as NonNullable<AdvisorConfig[Key]>);
119
+ apply(value);
118
120
  }
119
121
  };
120
122
 
@@ -166,6 +168,11 @@ export const applyConfig = (config: AdvisorConfig) => {
166
168
  "advisorMaxCallsPerSession",
167
169
  setAdvisorMaxCallsPerSessionRef
168
170
  );
171
+ applyOptionalConfig(
172
+ config,
173
+ "advisorModelWhitelist",
174
+ setAdvisorModelWhitelistRef
175
+ );
169
176
  applyOptionalConfig(
170
177
  config,
171
178
  "advisorSessionSummary",
@@ -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,
@@ -24,6 +26,7 @@ import {
24
26
  advisorJevTurnGateNoulThresholdRef,
25
27
  advisorLoopThresholdRef,
26
28
  advisorMaxCallsPerSessionRef,
29
+ advisorModelWhitelistRef,
27
30
  advisorOutcomeLoggingRef,
28
31
  advisorPlanGateRef,
29
32
  advisorRedactSecretsRef,
@@ -43,12 +46,15 @@ import {
43
46
  showUsageFooterRef,
44
47
  simpleModeRef,
45
48
  } from "./state.ts";
49
+ import type {
50
+ AdvisorConfig,
51
+ AdvisorConfigValue,
52
+ JevTransport,
53
+ } from "./types.ts";
46
54
  import {
47
55
  ADVISOR_TOOL_POLICIES,
48
- type AdvisorConfig,
49
56
  GATE_FAILURE_MODES,
50
57
  JEV_TRANSPORTS,
51
- type JevTransport,
52
58
  MAX_CONTEXT_MAX_CHARS,
53
59
  } from "./types.ts";
54
60
 
@@ -56,6 +62,15 @@ import {
56
62
  const configuredModelRef = (value: string | undefined): string | undefined =>
57
63
  value?.trim() || undefined;
58
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
+
59
74
  export const isValidAdvisorToolPolicies = (
60
75
  value: unknown
61
76
  ): value is Record<string, (typeof ADVISOR_TOOL_POLICIES)[number]> => {
@@ -65,10 +80,8 @@ export const isValidAdvisorToolPolicies = (
65
80
  return Object.entries(value).every(
66
81
  ([toolName, policy]) =>
67
82
  toolName.trim().length > 0 &&
68
- typeof policy === "string" &&
69
- ADVISOR_TOOL_POLICIES.includes(
70
- policy as (typeof ADVISOR_TOOL_POLICIES)[number]
71
- )
83
+ isString(policy) &&
84
+ ADVISOR_TOOL_POLICY_NAMES.has(policy)
72
85
  );
73
86
  };
74
87
 
@@ -87,11 +100,12 @@ export const isValidLoopThreshold = (value: unknown): value is number =>
87
100
  export const isValidMaxCallsPerSession = (value: unknown): value is number =>
88
101
  nonNegativeSafeInteger(value);
89
102
 
103
+ const GATE_FAILURE_MODE_NAMES = new Set<string>(GATE_FAILURE_MODES);
104
+
90
105
  export const isValidGateFailureMode = (
91
106
  value: unknown
92
107
  ): value is (typeof GATE_FAILURE_MODES)[number] =>
93
- typeof value === "string" &&
94
- GATE_FAILURE_MODES.includes(value as (typeof GATE_FAILURE_MODES)[number]);
108
+ isString(value) && GATE_FAILURE_MODE_NAMES.has(value);
95
109
 
96
110
  export const isValidToolResultMaxLines = (value: unknown): value is number =>
97
111
  nonNegativeSafeInteger(value);
@@ -120,8 +134,10 @@ export const isValidJevNoulMargin = (value: unknown): value is number =>
120
134
  value >= 0 &&
121
135
  value <= 0.5;
122
136
 
137
+ const JEV_TRANSPORT_NAMES = new Set<string>(JEV_TRANSPORTS);
138
+
123
139
  export const isValidJevTransport = (value: unknown): value is JevTransport =>
124
- typeof value === "string" && JEV_TRANSPORTS.includes(value as JevTransport);
140
+ isString(value) && JEV_TRANSPORT_NAMES.has(value);
125
141
 
126
142
  /** One declarative entry per AdvisorConfig key: JSON type, persistence, and
127
143
  * the live runtime value behind it. Validation and storage derive from this
@@ -130,13 +146,13 @@ export interface ConfigKeySchema {
130
146
  /** Human-readable accepted-value phrase for error messages. */
131
147
  accepted: string;
132
148
  /** Live runtime value captured by saveConfig. */
133
- current: () => unknown;
149
+ current: () => AdvisorConfigValue;
134
150
  /** Included in the state captured and diffed by saveConfig. */
135
151
  persisted: boolean;
136
152
  /** JSON value type used for the base type check. */
137
- type: "string" | "boolean" | "number" | "enum" | "object";
153
+ type: "string" | "boolean" | "number" | "enum" | "object" | "array";
138
154
  /** Type beyond the JSON type, for enum and object keys. */
139
- validate?: (value: unknown) => boolean;
155
+ validate?: (value: unknown) => value is JsonValue;
140
156
  }
141
157
 
142
158
  export const CONFIG_SCHEMA = {
@@ -297,6 +313,13 @@ export const CONFIG_SCHEMA = {
297
313
  type: "number",
298
314
  validate: isValidMaxCallsPerSession,
299
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
+ },
300
323
  advisorOutcomeLogging: {
301
324
  accepted: "true or false",
302
325
  current: () => advisorOutcomeLoggingRef,
@@ -415,6 +438,9 @@ export const CONFIG_SCHEMA = {
415
438
 
416
439
  export type ConfigKey = keyof typeof CONFIG_SCHEMA;
417
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
+
418
444
  /** Keys saveConfig captures, diffs, and writes to advisor.json. */
419
445
  export type PersistedConfigKey = {
420
446
  [Key in ConfigKey]: (typeof CONFIG_SCHEMA)[Key]["persisted"] extends true
@@ -422,11 +448,10 @@ export type PersistedConfigKey = {
422
448
  : never;
423
449
  }[ConfigKey];
424
450
 
425
- export const SAVED_CONFIG_KEYS = (
426
- Object.keys(CONFIG_SCHEMA) as ConfigKey[]
427
- ).filter(
428
- (key) => CONFIG_SCHEMA[key].persisted
429
- ) as readonly PersistedConfigKey[];
451
+ export const SAVED_CONFIG_KEYS: readonly PersistedConfigKey[] =
452
+ configKeys.filter(
453
+ (key): key is PersistedConfigKey => CONFIG_SCHEMA[key].persisted
454
+ );
430
455
 
431
456
  /** Widened per-key view for consumers that index by dynamic key. */
432
457
  const SCHEMA_BY_KEY: Record<ConfigKey, ConfigKeySchema> = CONFIG_SCHEMA;
@@ -1,6 +1,5 @@
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,
@@ -15,8 +14,11 @@ import {
15
14
  DEFAULT_JEV_TRANSPORT,
16
15
  DEFAULT_JEV_TURN_GATE_EVERY_TURNS,
17
16
  DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD,
18
- type GateFailureMode,
19
- type JevTransport,
17
+ } from "./types.ts";
18
+ import type {
19
+ AdvisorToolPolicies,
20
+ GateFailureMode,
21
+ JevTransport,
20
22
  } from "./types.ts";
21
23
 
22
24
  // An empty ref means no model has been selected yet.
@@ -36,6 +38,7 @@ export let advisorBlockOnBlockedRef = true;
36
38
  export let advisorAutoLoopGateRef = true;
37
39
  export let advisorLoopThresholdRef = 3;
38
40
  export let advisorMaxCallsPerSessionRef: number | undefined;
41
+ export let advisorModelWhitelistRef: string[] = [];
39
42
  export let advisorSessionSummaryRef = false;
40
43
  export let simpleModeRef = false;
41
44
  export let alwaysOnRef = false;
@@ -125,6 +128,11 @@ export const setAdvisorLoopThresholdRef = (value: number) => {
125
128
  export const setAdvisorMaxCallsPerSessionRef = (value: number | undefined) => {
126
129
  advisorMaxCallsPerSessionRef = value;
127
130
  };
131
+ export const setAdvisorModelWhitelistRef = (models: string[]) => {
132
+ advisorModelWhitelistRef = [
133
+ ...new Set(models.map((model) => model.trim()).filter(Boolean)),
134
+ ];
135
+ };
128
136
  export const setAdvisorSessionSummaryRef = (enabled: boolean) => {
129
137
  advisorSessionSummaryRef = enabled;
130
138
  };
@@ -243,6 +251,7 @@ export const getAdvisorSettings = () => ({
243
251
  jevTurnGateNoulThreshold: advisorJevTurnGateNoulThresholdRef,
244
252
  loopThreshold: advisorLoopThresholdRef,
245
253
  maxCallsPerSession: advisorMaxCallsPerSessionRef,
254
+ modelWhitelist: [...advisorModelWhitelistRef],
246
255
  outcomeLogging: advisorOutcomeLoggingRef,
247
256
  planGate: advisorPlanGateRef,
248
257
  redactSecrets: advisorRedactSecretsRef,