pi-advisor-flow 0.7.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 (92) hide show
  1. package/CHANGELOG.md +19 -18
  2. package/README.md +9 -8
  3. package/dist/index.js +1390 -1068
  4. package/extensions/index.ts +2 -1
  5. package/package.json +48 -35
  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 +102 -92
  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 +16 -2
  21. package/src/commands/types.ts +1 -0
  22. package/src/commands.ts +0 -1
  23. package/src/config/args.ts +1 -1
  24. package/src/config/defaults.ts +8 -1
  25. package/src/config/schema.ts +42 -17
  26. package/src/config/state.ts +12 -3
  27. package/src/config/storage.ts +34 -23
  28. package/src/config/types.ts +16 -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 +75 -85
  38. package/src/jev/failure.ts +19 -0
  39. package/src/jev/key-store.ts +26 -11
  40. package/src/jev/ledger.ts +58 -64
  41. package/src/jev/questions.ts +26 -23
  42. package/src/jev/state.ts +15 -12
  43. package/src/jev/transport.ts +8 -3
  44. package/src/model-stream.ts +35 -37
  45. package/src/outcomes.ts +15 -11
  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 +64 -49
  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 +43 -41
  62. package/src/tools/jev-turn-gate.ts +29 -9
  63. package/src/tools/loop-gate.ts +31 -20
  64. package/src/tools/model-access.ts +46 -0
  65. package/src/tools/prompts.ts +20 -25
  66. package/src/tools/register-ask-advisor.ts +36 -16
  67. package/src/tools/register-lifecycle.ts +22 -0
  68. package/src/tools/register-outcome.ts +2 -0
  69. package/src/tools/register-renderers.ts +146 -128
  70. package/src/tools/registration.ts +12 -14
  71. package/src/tools/render-advisor-result.ts +35 -22
  72. package/src/tools/render-common.ts +16 -19
  73. package/src/tools/scout-status.ts +42 -27
  74. package/src/tools/types.ts +1 -0
  75. package/src/tools.ts +0 -1
  76. package/src/ui/jev-setup-submenu.ts +65 -59
  77. package/src/ui/manual-dialog-render.ts +1 -0
  78. package/src/ui/manual-dialog.ts +39 -43
  79. package/src/ui/masked-input.ts +5 -11
  80. package/src/ui/model-multi-selector.ts +9 -0
  81. package/src/ui/model-selector-adapter.ts +31 -0
  82. package/src/ui/model-selector.ts +14 -161
  83. package/src/ui/searchable-model-list.ts +230 -0
  84. package/src/ui/settings-formatting.ts +24 -14
  85. package/src/ui/settings-items.ts +38 -1
  86. package/src/ui/settings-list-adapter.ts +15 -12
  87. package/src/ui/settings-mutations.ts +157 -70
  88. package/src/ui/settings-selector.ts +18 -9
  89. package/src/ui/text-setting-submenu.ts +3 -6
  90. package/src/ui/types.ts +14 -0
  91. package/src/ui.ts +0 -1
  92. package/src/usage.ts +26 -19
package/dist/index.js CHANGED
@@ -1,12 +1,16 @@
1
1
  // src/config/types.ts
2
+ import {
3
+ DEFAULT_MAX_BYTES as PI_DEFAULT_MAX_BYTES,
4
+ DEFAULT_MAX_LINES as PI_DEFAULT_MAX_LINES
5
+ } from "@earendil-works/pi-coding-agent";
2
6
  import {
3
7
  DEFAULT_MAX_BYTES,
4
8
  DEFAULT_MAX_LINES
5
9
  } from "@earendil-works/pi-coding-agent";
6
10
  var DEFAULT_CONTEXT_MAX_CHARS = 15000;
7
11
  var MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
8
- var DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = DEFAULT_MAX_LINES;
9
- var DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES = DEFAULT_MAX_BYTES;
12
+ var DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = PI_DEFAULT_MAX_LINES;
13
+ var DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES = PI_DEFAULT_MAX_BYTES;
10
14
  var DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS = 20000;
11
15
  var DEFAULT_JEV_MODEL = "jev-latest";
12
16
  var DEFAULT_JEV_TIMEOUT_MS = 8000;
@@ -51,6 +55,7 @@ var advisorBlockOnBlockedRef = true;
51
55
  var advisorAutoLoopGateRef = true;
52
56
  var advisorLoopThresholdRef = 3;
53
57
  var advisorMaxCallsPerSessionRef;
58
+ var advisorModelWhitelistRef = [];
54
59
  var advisorSessionSummaryRef = false;
55
60
  var simpleModeRef = false;
56
61
  var alwaysOnRef = false;
@@ -129,6 +134,11 @@ var setAdvisorLoopThresholdRef = (value) => {
129
134
  var setAdvisorMaxCallsPerSessionRef = (value) => {
130
135
  advisorMaxCallsPerSessionRef = value;
131
136
  };
137
+ var setAdvisorModelWhitelistRef = (models) => {
138
+ advisorModelWhitelistRef = [
139
+ ...new Set(models.map((model) => model.trim()).filter(Boolean))
140
+ ];
141
+ };
132
142
  var setAdvisorSessionSummaryRef = (enabled) => {
133
143
  advisorSessionSummaryRef = enabled;
134
144
  };
@@ -241,6 +251,7 @@ var getAdvisorSettings = () => ({
241
251
  jevTurnGateNoulThreshold: advisorJevTurnGateNoulThresholdRef,
242
252
  loopThreshold: advisorLoopThresholdRef,
243
253
  maxCallsPerSession: advisorMaxCallsPerSessionRef,
254
+ modelWhitelist: [...advisorModelWhitelistRef],
244
255
  outcomeLogging: advisorOutcomeLoggingRef,
245
256
  planGate: advisorPlanGateRef,
246
257
  redactSecrets: advisorRedactSecretsRef,
@@ -261,17 +272,45 @@ var splitRef = (ref) => {
261
272
  return i === -1 ? ["openai-codex", ref] : [ref.slice(0, i), ref.slice(i + 1)];
262
273
  };
263
274
 
275
+ // src/content-utils.ts
276
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
277
+ var isRecordOf = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
278
+ var isString = (value) => typeof value === "string";
279
+ var isNumber = (value) => typeof value === "number";
280
+ var isBoolean = (value) => typeof value === "boolean";
281
+ var byteLength = (value) => Buffer.byteLength(value, "utf-8");
282
+ var contentParts = (content) => {
283
+ if (isString(content)) {
284
+ return [content];
285
+ }
286
+ return Array.isArray(content) ? content : [];
287
+ };
288
+ var capUtf8Bytes = (value, maxBytes) => {
289
+ let result = "";
290
+ let used = 0;
291
+ for (const character of value) {
292
+ const characterBytes = byteLength(character);
293
+ if (used + characterBytes > maxBytes) {
294
+ break;
295
+ }
296
+ result += character;
297
+ used += characterBytes;
298
+ }
299
+ return result;
300
+ };
301
+
264
302
  // src/git.ts
265
303
  import { execFileSync } from "node:child_process";
266
304
  var GIT_CONTEXT_LEVELS = ["off", "summary", "full"];
267
- var isValidGitContextLevel = (value) => GIT_CONTEXT_LEVELS.includes(value);
305
+ var GIT_CONTEXT_LEVEL_SET = new Set(GIT_CONTEXT_LEVELS);
306
+ var isValidGitContextLevel = (value) => isString(value) && GIT_CONTEXT_LEVEL_SET.has(value);
268
307
  var LEVEL_RANK = {
269
308
  full: 2,
270
309
  off: 0,
271
310
  summary: 1
272
311
  };
273
312
  var clampGitContextLevel = (requested, allowed) => LEVEL_RANK[requested] <= LEVEL_RANK[allowed] ? requested : allowed;
274
- var escapeRepositoryText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
313
+ var escapeRepositoryText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
275
314
  var TRUNCATION_NOTICE = `
276
315
  [Repository context truncated: it exceeded the configured limit.]`;
277
316
  var capRepositoryContext = (value, maxChars) => {
@@ -296,7 +335,7 @@ var deadlineRunner = () => {
296
335
  }
297
336
  return execFileSync("git", args, {
298
337
  cwd,
299
- encoding: "utf8",
338
+ encoding: "utf-8",
300
339
  maxBuffer: GIT_MAX_BUFFER,
301
340
  shell: false,
302
341
  stdio: ["ignore", "pipe", "pipe"],
@@ -369,17 +408,21 @@ ${patch}` : "Patch: (no tracked-file content changes)");
369
408
 
370
409
  // src/config/schema.ts
371
410
  var configuredModelRef = (value) => value?.trim() || undefined;
411
+ var isNonEmptyModel = (model) => typeof model === "string" && model.trim().length > 0;
412
+ var isValidAdvisorModelWhitelist = (value) => Array.isArray(value) && value.every(isNonEmptyModel);
413
+ var ADVISOR_TOOL_POLICY_NAMES = new Set(ADVISOR_TOOL_POLICIES);
372
414
  var isValidAdvisorToolPolicies = (value) => {
373
415
  if (!value || typeof value !== "object" || Array.isArray(value)) {
374
416
  return false;
375
417
  }
376
- return Object.entries(value).every(([toolName, policy]) => toolName.trim().length > 0 && typeof policy === "string" && ADVISOR_TOOL_POLICIES.includes(policy));
418
+ return Object.entries(value).every(([toolName, policy]) => toolName.trim().length > 0 && isString(policy) && ADVISOR_TOOL_POLICY_NAMES.has(policy));
377
419
  };
378
420
  var nonNegativeSafeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
379
421
  var isValidContextMaxChars = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= MAX_CONTEXT_MAX_CHARS;
380
422
  var isValidLoopThreshold = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 2;
381
423
  var isValidMaxCallsPerSession = (value) => nonNegativeSafeInteger(value);
382
- var isValidGateFailureMode = (value) => typeof value === "string" && GATE_FAILURE_MODES.includes(value);
424
+ var GATE_FAILURE_MODE_NAMES = new Set(GATE_FAILURE_MODES);
425
+ var isValidGateFailureMode = (value) => isString(value) && GATE_FAILURE_MODE_NAMES.has(value);
383
426
  var isValidToolResultMaxLines = (value) => nonNegativeSafeInteger(value);
384
427
  var isValidToolResultMaxBytes = (value) => nonNegativeSafeInteger(value);
385
428
  var isValidJevTimeoutMs = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
@@ -387,7 +430,8 @@ var isValidJevDigestMaxChars = (value) => nonNegativeSafeInteger(value);
387
430
  var isValidJevPricePerMtok = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
388
431
  var isValidJevSkipConfidence = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0.5 && value <= 1;
389
432
  var isValidJevNoulMargin = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 0.5;
390
- var isValidJevTransport = (value) => typeof value === "string" && JEV_TRANSPORTS.includes(value);
433
+ var JEV_TRANSPORT_NAMES = new Set(JEV_TRANSPORTS);
434
+ var isValidJevTransport = (value) => isString(value) && JEV_TRANSPORT_NAMES.has(value);
391
435
  var CONFIG_SCHEMA = {
392
436
  advisor: {
393
437
  accepted: "a provider/model string",
@@ -546,6 +590,13 @@ var CONFIG_SCHEMA = {
546
590
  type: "number",
547
591
  validate: isValidMaxCallsPerSession
548
592
  },
593
+ advisorModelWhitelist: {
594
+ accepted: "an array of non-empty provider/model strings",
595
+ current: () => [...advisorModelWhitelistRef],
596
+ persisted: true,
597
+ type: "array",
598
+ validate: isValidAdvisorModelWhitelist
599
+ },
549
600
  advisorOutcomeLogging: {
550
601
  accepted: "true or false",
551
602
  current: () => advisorOutcomeLoggingRef,
@@ -660,12 +711,13 @@ var CONFIG_SCHEMA = {
660
711
  type: "boolean"
661
712
  }
662
713
  };
663
- var SAVED_CONFIG_KEYS = Object.keys(CONFIG_SCHEMA).filter((key) => CONFIG_SCHEMA[key].persisted);
714
+ var configKeys = Object.keys(CONFIG_SCHEMA);
715
+ var SAVED_CONFIG_KEYS = configKeys.filter((key) => CONFIG_SCHEMA[key].persisted);
664
716
  var SCHEMA_BY_KEY = CONFIG_SCHEMA;
665
717
 
666
718
  // src/config/validation.ts
667
- var CONFIG_KEYS = new Set(Object.keys(CONFIG_SCHEMA));
668
- var keysOfType = (type) => Object.keys(CONFIG_SCHEMA).filter((key) => SCHEMA_BY_KEY[key].type === type);
719
+ var CONFIG_KEYS = new Set(configKeys);
720
+ var keysOfType = (type) => configKeys.filter((key) => SCHEMA_BY_KEY[key].type === type);
669
721
  var BOOLEAN_CONFIG_KEYS = keysOfType("boolean");
670
722
  var STRING_CONFIG_KEYS = keysOfType("string");
671
723
  var invalidConfigValue = (path, key, accepted) => {
@@ -674,14 +726,14 @@ var invalidConfigValue = (path, key, accepted) => {
674
726
  var unknownConfigKeys = (config) => Object.keys(config).filter((key) => !CONFIG_KEYS.has(key));
675
727
  var validateStringValues = (config, path) => {
676
728
  for (const key of STRING_CONFIG_KEYS) {
677
- if (config[key] !== undefined && typeof config[key] !== "string") {
729
+ if (config[key] !== undefined && !isString(config[key])) {
678
730
  invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
679
731
  }
680
732
  }
681
733
  };
682
734
  var validateBooleanValues = (config, path) => {
683
735
  for (const key of BOOLEAN_CONFIG_KEYS) {
684
- if (config[key] !== undefined && typeof config[key] !== "boolean") {
736
+ if (config[key] !== undefined && !isBoolean(config[key])) {
685
737
  invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
686
738
  }
687
739
  }
@@ -694,6 +746,14 @@ var validateNumericValues = (config, path) => {
694
746
  }
695
747
  }
696
748
  };
749
+ var validateArrayValues = (config, path) => {
750
+ for (const key of keysOfType("array")) {
751
+ const isValid = SCHEMA_BY_KEY[key].validate;
752
+ if (config[key] !== undefined && isValid !== undefined && !isValid(config[key])) {
753
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
754
+ }
755
+ }
756
+ };
697
757
  var validateEnumValues = (config, path) => {
698
758
  for (const key of keysOfType("enum")) {
699
759
  const isValid = SCHEMA_BY_KEY[key].validate;
@@ -711,20 +771,20 @@ var validateObjectValues = (config, path) => {
711
771
  }
712
772
  };
713
773
  var validateConfig = (value, path = "advisor.json") => {
714
- if (!value || typeof value !== "object" || Array.isArray(value)) {
774
+ if (!isRecord(value)) {
715
775
  throw new TypeError(`Invalid advisor configuration at ${path}: expected a JSON object.`);
716
776
  }
717
- const config = value;
718
- validateStringValues(config, path);
719
- validateBooleanValues(config, path);
720
- validateNumericValues(config, path);
721
- validateObjectValues(config, path);
722
- validateEnumValues(config, path);
777
+ validateStringValues(value, path);
778
+ validateBooleanValues(value, path);
779
+ validateNumericValues(value, path);
780
+ validateObjectValues(value, path);
781
+ validateArrayValues(value, path);
782
+ validateEnumValues(value, path);
723
783
  return true;
724
784
  };
725
785
 
726
786
  // src/config/args.ts
727
- var ARGUMENT_WHITESPACE = /\s+/;
787
+ var ARGUMENT_WHITESPACE = /\s+/u;
728
788
  var parseArgs = (args) => {
729
789
  let nextExecutor = executorRef;
730
790
  let nextAdvisor = advisorRef;
@@ -753,10 +813,7 @@ var parseArgs = (args) => {
753
813
  // src/config/storage.ts
754
814
  import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
755
815
  import { join } from "node:path";
756
- import {
757
- CONFIG_DIR_NAME,
758
- getAgentDir
759
- } from "@earendil-works/pi-coding-agent";
816
+ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
760
817
 
761
818
  // src/config/defaults.ts
762
819
  var resetDefaults = () => {
@@ -775,6 +832,7 @@ var resetDefaults = () => {
775
832
  setAdvisorAutoLoopGateRef(true);
776
833
  setAdvisorLoopThresholdRef(3);
777
834
  setAdvisorMaxCallsPerSessionRef(undefined);
835
+ setAdvisorModelWhitelistRef([]);
778
836
  setAdvisorSessionSummaryRef(false);
779
837
  setSimpleModeRef(false);
780
838
  setAlwaysOnRef(false);
@@ -830,6 +888,7 @@ var applyConfig = (config) => {
830
888
  applyOptionalConfig(config, "advisorAutoLoopGate", setAdvisorAutoLoopGateRef);
831
889
  applyOptionalConfig(config, "advisorLoopThreshold", setAdvisorLoopThresholdRef);
832
890
  applyOptionalConfig(config, "advisorMaxCallsPerSession", setAdvisorMaxCallsPerSessionRef);
891
+ applyOptionalConfig(config, "advisorModelWhitelist", setAdvisorModelWhitelistRef);
833
892
  applyOptionalConfig(config, "advisorSessionSummary", setAdvisorSessionSummaryRef);
834
893
  applyOptionalConfig(config, "advisorScoutEnabled", setAdvisorScoutEnabledRef);
835
894
  applyOptionalConfig(config, "showUsageDetails", setShowUsageDetailsRef);
@@ -866,14 +925,16 @@ var sameConfigValue = (left, right) => JSON.stringify(left) === JSON.stringify(r
866
925
  var RESERVED_ADVISOR_JSON_KEYS = new Set(["typesafe_api_key"]);
867
926
  var readExistingConfig = (path) => {
868
927
  try {
869
- const parsed = JSON.parse(readFileSync(path, "utf8"));
870
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
928
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
929
+ return isRecord(parsed) ? parsed : {};
871
930
  } catch {
872
931
  return {};
873
932
  }
874
933
  };
875
934
  var shouldPersistConfigKey = (key, persistAdvisor, persistExecutor) => (key !== "advisor" || persistAdvisor) && (key !== "executor" || persistExecutor);
876
- var applyChangedConfigValues = (data, current, changedKeys, persistAdvisor, persistExecutor) => {
935
+ var applyChangedConfigValues = (existing, current, changedKeys, persistAdvisor, persistExecutor) => {
936
+ const dropped = new Set;
937
+ const data = { ...existing };
877
938
  for (const key of changedKeys) {
878
939
  if (!shouldPersistConfigKey(key, persistAdvisor, persistExecutor)) {
879
940
  continue;
@@ -881,16 +942,26 @@ var applyChangedConfigValues = (data, current, changedKeys, persistAdvisor, pers
881
942
  const value = current[key];
882
943
  const isEmptyModelRef = (key === "advisor" || key === "executor") && !value;
883
944
  if (value === undefined || isEmptyModelRef) {
884
- delete data[key];
945
+ dropped.add(key);
885
946
  } else {
886
947
  data[key] = value;
887
948
  }
888
949
  }
950
+ if (dropped.size === 0) {
951
+ return data;
952
+ }
953
+ const retained = {};
954
+ for (const key of Object.keys(data)) {
955
+ if (!dropped.has(key)) {
956
+ retained[key] = data[key];
957
+ }
958
+ }
959
+ return retained;
889
960
  };
890
961
  var loadedConfigState;
891
962
  var loadedConfigPath;
892
963
  var readConfig = (path) => {
893
- const config = JSON.parse(readFileSync(path, "utf8"));
964
+ const config = JSON.parse(readFileSync(path, "utf-8"));
894
965
  validateConfig(config, path);
895
966
  return config;
896
967
  };
@@ -948,17 +1019,16 @@ var saveConfig = (_ctx, options = {}) => {
948
1019
  if (changedKeys.length === 0) {
949
1020
  return path;
950
1021
  }
951
- const data = { ...existing };
952
- applyChangedConfigValues(data, current, changedKeys, persistAdvisor, persistExecutor);
1022
+ const data = applyChangedConfigValues(existing, current, changedKeys, persistAdvisor, persistExecutor);
953
1023
  writeFileSync(path, `${JSON.stringify(data, null, 2)}
954
1024
  `);
955
1025
  resetConfigCache();
956
1026
  const nextLoadedState = { ...current };
957
1027
  if (!persistAdvisor) {
958
- nextLoadedState.advisor = baseline ? baseline.advisor : configuredModelRef(typeof existing.advisor === "string" ? existing.advisor : undefined);
1028
+ nextLoadedState.advisor = baseline ? baseline.advisor : configuredModelRef(isString(existing.advisor) ? existing.advisor : undefined);
959
1029
  }
960
1030
  if (!persistExecutor) {
961
- nextLoadedState.executor = baseline ? baseline.executor : configuredModelRef(typeof existing.executor === "string" ? existing.executor : undefined);
1031
+ nextLoadedState.executor = baseline ? baseline.executor : configuredModelRef(isString(existing.executor) ? existing.executor : undefined);
962
1032
  }
963
1033
  loadedConfigState = nextLoadedState;
964
1034
  loadedConfigPath = path;
@@ -998,7 +1068,7 @@ var selectedEffort = (choice) => {
998
1068
  return effort === DEFAULT_EFFORT_LEVEL ? undefined : effort;
999
1069
  };
1000
1070
  var ADVISOR_ACTIVATION_EXPLANATION = "The Advisor is a second-opinion model that reviews the Executor's context and returns risks, alternatives, and verification steps without changing files or running tools.";
1001
- var ARGUMENT_WHITESPACE2 = /\s+/;
1071
+ var ARGUMENT_WHITESPACE2 = /\s+/u;
1002
1072
  var hasModelOverride = (args, key) => args.trim().split(ARGUMENT_WHITESPACE2).some((token) => {
1003
1073
  const [tokenKey, value] = token.split("=");
1004
1074
  return tokenKey === key && Boolean(value);
@@ -1045,11 +1115,18 @@ var findConfiguredModel = (ctx, ref) => {
1045
1115
  return ctx.modelRegistry.find(provider, modelId);
1046
1116
  };
1047
1117
  var getAvailableModelRefs = (ctx) => {
1048
- if (typeof ctx.modelRegistry.getAvailable !== "function") {
1118
+ if (!ctx.modelRegistry.getAvailable) {
1049
1119
  return;
1050
1120
  }
1051
1121
  return ctx.modelRegistry.getAvailable().map((model) => `${model.provider}/${model.id}`);
1052
1122
  };
1123
+ var getConfiguredModelRefs = (ctx) => {
1124
+ const registry = ctx.modelRegistry;
1125
+ const models = registry?.getAvailable ? registry.getAvailable() : [];
1126
+ return [
1127
+ ...new Set(models.map((model) => `${model.provider}/${model.id}`))
1128
+ ].toSorted((left, right) => left.localeCompare(right));
1129
+ };
1053
1130
  var isSelectableModel = (ctx, ref, availableRefs) => {
1054
1131
  if (!ref) {
1055
1132
  return false;
@@ -1084,18 +1161,39 @@ var planActivationModels = (ctx, executor, advisor, pendingExecutorRef, persiste
1084
1161
  };
1085
1162
  };
1086
1163
 
1087
- // src/ui/model-selector.ts
1088
- import {
1089
- fuzzyFilter,
1090
- Input,
1091
- truncateToWidth
1092
- } from "@earendil-works/pi-tui";
1164
+ // src/ui/model-selector-adapter.ts
1165
+ class ModelSelectorAdapter {
1166
+ list;
1167
+ constructor(list) {
1168
+ this.list = list;
1169
+ }
1170
+ get focused() {
1171
+ return this.list.focused;
1172
+ }
1173
+ set focused(value) {
1174
+ this.list.focused = value;
1175
+ }
1176
+ invalidate() {
1177
+ this.list.invalidate();
1178
+ }
1179
+ render(width) {
1180
+ return this.list.render(width);
1181
+ }
1182
+ handleInput(keyData) {
1183
+ this.list.handleInput(keyData);
1184
+ }
1185
+ }
1186
+
1187
+ // src/ui/searchable-model-list.ts
1188
+ import { fuzzyFilter, Input, truncateToWidth } from "@earendil-works/pi-tui";
1093
1189
 
1094
- class SearchableModelSelector {
1190
+ class SearchableModelList {
1095
1191
  tui;
1096
1192
  searchInput;
1097
1193
  allOptions;
1098
1194
  currentOption;
1195
+ multiSelect;
1196
+ selected = new Set;
1099
1197
  filteredOptions;
1100
1198
  selectedIndex = 0;
1101
1199
  title;
@@ -1112,13 +1210,30 @@ class SearchableModelSelector {
1112
1210
  this.searchInput.focused = val;
1113
1211
  }
1114
1212
  constructor(options) {
1213
+ this.multiSelect = options.multiSelect;
1115
1214
  this.tui = options.tui;
1116
1215
  this.title = options.title;
1117
- this.currentOption = options.currentOption && options.allOptions.includes(options.currentOption) ? options.currentOption : undefined;
1118
- this.allOptions = this.currentOption ? [
1119
- this.currentOption,
1120
- ...options.allOptions.filter((item) => item !== this.currentOption)
1121
- ] : options.allOptions;
1216
+ const [requestedCurrentOption] = options.currentOptions;
1217
+ this.currentOption = !this.multiSelect && requestedCurrentOption && options.allOptions.includes(requestedCurrentOption) ? requestedCurrentOption : undefined;
1218
+ let allOptions;
1219
+ if (this.multiSelect) {
1220
+ allOptions = [...new Set(options.allOptions)].toSorted((left, right) => left.localeCompare(right));
1221
+ } else if (this.currentOption) {
1222
+ allOptions = [
1223
+ this.currentOption,
1224
+ ...options.allOptions.filter((item) => item !== this.currentOption)
1225
+ ];
1226
+ } else {
1227
+ allOptions = [...options.allOptions];
1228
+ }
1229
+ this.allOptions = allOptions;
1230
+ if (this.multiSelect) {
1231
+ for (const value of options.currentOptions) {
1232
+ if (this.allOptions.includes(value)) {
1233
+ this.selected.add(value);
1234
+ }
1235
+ }
1236
+ }
1122
1237
  this.theme = options.theme;
1123
1238
  this.keybindings = options.keybindings;
1124
1239
  this.onSelect = options.onSelect;
@@ -1133,8 +1248,7 @@ class SearchableModelSelector {
1133
1248
  const lines = [this.theme.fg("border", "─".repeat(width))];
1134
1249
  lines.push(` ${this.theme.fg("accent", this.theme.bold(this.title))}`);
1135
1250
  const inputLines = this.searchInput.render(Math.max(1, width - 10));
1136
- lines.push(` ${this.theme.fg("accent", "Search: ")}${inputLines[0] || ""}`);
1137
- lines.push("");
1251
+ lines.push(` ${this.theme.fg("accent", "Search: ")}${inputLines[0] || ""}`, "");
1138
1252
  const query = this.searchInput.getValue().trim();
1139
1253
  this.filteredOptions = query ? fuzzyFilter(this.allOptions, query, (item) => item) : this.allOptions;
1140
1254
  this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredOptions.length - 1));
@@ -1146,21 +1260,13 @@ class SearchableModelSelector {
1146
1260
  const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
1147
1261
  const endIndex = Math.min(startIndex + maxVisible, total);
1148
1262
  for (let i = startIndex;i < endIndex; i += 1) {
1149
- const item = this.filteredOptions[i];
1150
- const tick = item === this.currentOption ? "✓ " : " ";
1151
- if (i === this.selectedIndex) {
1152
- lines.push(` ${this.theme.fg("accent", "→ ")}${this.theme.fg("accent", `${tick}${item}`)}`);
1153
- } else {
1154
- lines.push(` ${this.theme.fg("text", `${tick}${item}`)}`);
1155
- }
1263
+ lines.push(this.renderOption(this.filteredOptions[i], i));
1156
1264
  }
1157
1265
  if (total > maxVisible) {
1158
- lines.push(" " + this.theme.fg("muted", ` (${this.selectedIndex + 1}/${total})`));
1266
+ lines.push(` ${this.theme.fg("muted", ` (${this.selectedIndex + 1}/${total})`)}`);
1159
1267
  }
1160
1268
  }
1161
- lines.push("");
1162
- lines.push(` ${this.theme.fg("dim", "Type to search · ↑↓: navigate · Enter: select · Esc: cancel")}`);
1163
- lines.push(this.theme.fg("border", "─".repeat(width)));
1269
+ lines.push("", ` ${this.theme.fg("dim", this.interactionHint())}`, this.theme.fg("border", "─".repeat(width)));
1164
1270
  return lines.map((line) => truncateToWidth(line, width));
1165
1271
  }
1166
1272
  handleInput(keyData) {
@@ -1172,10 +1278,27 @@ class SearchableModelSelector {
1172
1278
  this.moveSelection(1);
1173
1279
  return;
1174
1280
  }
1281
+ if (this.multiSelect && keyData === " ") {
1282
+ const item = this.filteredOptions[this.selectedIndex];
1283
+ if (item) {
1284
+ if (this.selected.has(item)) {
1285
+ this.selected.delete(item);
1286
+ } else {
1287
+ this.selected.add(item);
1288
+ }
1289
+ }
1290
+ this.tui.requestRender();
1291
+ return;
1292
+ }
1175
1293
  if (this.matchesAction(keyData, "tui.select.confirm", `
1176
1294
  `) || keyData === "\r") {
1177
- if (this.filteredOptions.length > 0) {
1178
- this.onSelect(this.filteredOptions[this.selectedIndex]);
1295
+ if (this.multiSelect) {
1296
+ this.onSelect(this.allOptions.filter((item) => this.selected.has(item)));
1297
+ } else {
1298
+ const selectedOption = this.filteredOptions[this.selectedIndex];
1299
+ if (selectedOption !== undefined) {
1300
+ this.onSelect([selectedOption]);
1301
+ }
1179
1302
  }
1180
1303
  return;
1181
1304
  }
@@ -1187,6 +1310,19 @@ class SearchableModelSelector {
1187
1310
  this.selectedIndex = 0;
1188
1311
  this.tui.requestRender();
1189
1312
  }
1313
+ interactionHint() {
1314
+ return this.multiSelect ? "Type to search · ↑↓: navigate · Space: toggle · Enter: apply · Esc: cancel" : "Type to search · ↑↓: navigate · Enter: select · Esc: cancel";
1315
+ }
1316
+ renderOption(item, index) {
1317
+ let tick = " ";
1318
+ if (this.multiSelect ? this.selected.has(item) : item === this.currentOption) {
1319
+ tick = "✓ ";
1320
+ }
1321
+ if (index === this.selectedIndex) {
1322
+ return ` ${this.theme.fg("accent", `→ ${tick}${item}`)}`;
1323
+ }
1324
+ return ` ${this.theme.fg("text", `${tick}${item}`)}`;
1325
+ }
1190
1326
  matchesAction(keyData, action, fallback) {
1191
1327
  return this.keybindings.matches(keyData, action) || keyData === fallback;
1192
1328
  }
@@ -1206,6 +1342,22 @@ class SearchableModelSelector {
1206
1342
  }
1207
1343
  }
1208
1344
 
1345
+ // src/ui/model-selector.ts
1346
+ class SearchableModelSelector extends ModelSelectorAdapter {
1347
+ constructor(options) {
1348
+ super(new SearchableModelList({
1349
+ ...options,
1350
+ currentOptions: options.currentOption ? [options.currentOption] : [],
1351
+ multiSelect: false,
1352
+ onSelect: ([value]) => {
1353
+ if (value !== undefined) {
1354
+ options.onSelect(value);
1355
+ }
1356
+ }
1357
+ }));
1358
+ }
1359
+ }
1360
+
1209
1361
  // src/commands/model-picker.ts
1210
1362
  var selectAdvisorModels = async (ctx, options) => {
1211
1363
  if (!ctx.hasUI) {
@@ -1268,43 +1420,20 @@ var selectAdvisorModels = async (ctx, options) => {
1268
1420
  return { advisor, advisorEffort, executor, executorEffort };
1269
1421
  };
1270
1422
 
1271
- // src/herdr.ts
1423
+ // src/herdr-shared.ts
1272
1424
  import net from "node:net";
1273
1425
 
1274
- // src/content-utils.ts
1275
- var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
1276
- var byteLength = (value) => Buffer.byteLength(value, "utf8");
1277
- var contentParts = (content) => {
1278
- if (typeof content === "string") {
1279
- return [content];
1280
- }
1281
- return Array.isArray(content) ? content : [];
1282
- };
1283
- var capUtf8Bytes = (value, maxBytes) => {
1284
- let result = "";
1285
- let used = 0;
1286
- for (const character of value) {
1287
- const characterBytes = byteLength(character);
1288
- if (used + characterBytes > maxBytes) {
1289
- break;
1290
- }
1291
- result += character;
1292
- used += characterBytes;
1293
- }
1294
- return result;
1295
- };
1296
-
1297
1426
  // src/redaction.ts
1298
1427
  var REDACTION_MARKER = "[REDACTED SECRET]";
1299
- var PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/gi;
1300
- var PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/i;
1428
+ var PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/giu;
1429
+ var PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/iu;
1301
1430
  var SECRET_PATTERNS = [
1302
- /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/gi,
1303
- /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi,
1304
- /\b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s"'&,;)}\]]+)/gi,
1305
- /([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi,
1306
- /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
1307
- /\b(?:aws_secret_access_key|aws_session_token)\s*[:=]\s*[^\s"'&,;)}\]]+/gi
1431
+ /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/giu,
1432
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/giu,
1433
+ /\b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s"'&,;)}\]]+)/giu,
1434
+ /(?<scheme>[a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/giu,
1435
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/gu,
1436
+ /\b(?:aws_secret_access_key|aws_session_token)\s*[:=]\s*[^\s"'&,;)}\]]+/giu
1308
1437
  ];
1309
1438
  var redactUnterminatedPem = (value) => {
1310
1439
  const begins = [...value.matchAll(PEM_BEGIN_PATTERN)];
@@ -1318,7 +1447,7 @@ var redactUnterminatedPem = (value) => {
1318
1447
  var redactSecrets = (value) => {
1319
1448
  let redacted = redactUnterminatedPem(value);
1320
1449
  for (const pattern of SECRET_PATTERNS) {
1321
- redacted = redacted.replace(pattern, (_match, scheme) => typeof scheme === "string" ? `${scheme}${REDACTION_MARKER}@` : REDACTION_MARKER);
1450
+ redacted = redacted.replace(pattern, (_match, scheme) => isString(scheme) ? `${scheme}${REDACTION_MARKER}@` : REDACTION_MARKER);
1322
1451
  }
1323
1452
  return redacted;
1324
1453
  };
@@ -1327,11 +1456,11 @@ var redactAndCapText = (value, maxBytes, redact = true) => {
1327
1456
  return capUtf8Bytes(source, maxBytes);
1328
1457
  };
1329
1458
 
1330
- // src/herdr.ts
1459
+ // src/herdr-shared.ts
1331
1460
  var HERDR_NOTIFICATION_METHOD = "notification.show";
1332
1461
  var SOURCE = "pi-advisor:advisor-activity";
1333
- var BLOCK_SOURCE = "pi-advisor:advisor-block";
1334
1462
  var NOTIFICATION_SOURCE = "pi-advisor:advisor-notification";
1463
+ var BLOCK_SOURCE = "pi-advisor:advisor-block";
1335
1464
  var HERDR_PI_SOURCE = "herdr:pi";
1336
1465
  var sequence = Date.now() * 1000;
1337
1466
  var nextSequence = () => {
@@ -1348,17 +1477,7 @@ var safeEmitBlocked = (active, label = "Advisor blocked") => {
1348
1477
  } catch {}
1349
1478
  };
1350
1479
  var isControlCharacter = (character) => character <= "\x1F" || character === "";
1351
- var cleanNotification = (value, max) => [...redactSecrets(value)].map((character) => isControlCharacter(character) ? " " : character).join("").replace(/\s+/g, " ").trim().slice(0, max);
1352
- var createHerdrNotificationRequest = (title, body) => ({
1353
- id: `${NOTIFICATION_SOURCE}:${nextSequence()}`,
1354
- method: HERDR_NOTIFICATION_METHOD,
1355
- params: {
1356
- body: cleanNotification(body, 240),
1357
- position: "top-left",
1358
- sound: "request",
1359
- title: cleanNotification(title, 80)
1360
- }
1361
- });
1480
+ var cleanNotification = (value, max) => [...redactSecrets(value)].map((character) => isControlCharacter(character) ? " " : character).join("").replaceAll(/\s+/gu, " ").trim().slice(0, max);
1362
1481
  var sendToHerdr = (request) => {
1363
1482
  if (process.env.HERDR_ENV !== "1") {
1364
1483
  return;
@@ -1378,61 +1497,18 @@ var sendToHerdr = (request) => {
1378
1497
  socket.once("error", () => socket.destroy());
1379
1498
  socket.once("close", () => clearTimeout(timeout));
1380
1499
  };
1381
-
1382
- class HerdrAdvisorActivity {
1383
- #activeConsultations = 0;
1384
- report;
1385
- enabled;
1386
- constructor(report = sendToHerdr, enabled = () => true) {
1387
- this.report = report;
1388
- this.enabled = enabled;
1389
- }
1390
- start() {
1391
- if (!this.enabled()) {
1392
- return;
1393
- }
1394
- this.#activeConsultations += 1;
1395
- if (this.#activeConsultations === 1) {
1396
- this.safeReport(false);
1397
- }
1398
- }
1399
- finish() {
1400
- if (this.#activeConsultations === 0) {
1401
- return;
1402
- }
1403
- this.#activeConsultations -= 1;
1404
- if (this.#activeConsultations === 0) {
1405
- this.safeReport(true);
1406
- }
1407
- }
1408
- clear() {
1409
- if (this.#activeConsultations === 0) {
1410
- return;
1411
- }
1412
- this.#activeConsultations = 0;
1413
- this.safeReport(true);
1414
- }
1415
- safeReport(clear) {
1416
- try {
1417
- this.report(this.request(clear));
1418
- } catch {}
1419
- }
1420
- request(clear) {
1421
- return {
1422
- id: `${SOURCE}:${nextSequence()}`,
1423
- method: "pane.report_metadata",
1424
- params: {
1425
- agent: "pi",
1426
- applies_to_source: HERDR_PI_SOURCE,
1427
- pane_id: process.env.HERDR_PANE_ID ?? "",
1428
- source: SOURCE,
1429
- ...clear ? { clear_state_labels: true } : { state_labels: { working: "seeking advice" } },
1430
- seq: nextSequence()
1431
- }
1432
- };
1500
+ var createHerdrNotificationRequest = (title, body) => ({
1501
+ id: `${NOTIFICATION_SOURCE}:${nextSequence()}`,
1502
+ method: HERDR_NOTIFICATION_METHOD,
1503
+ params: {
1504
+ body: cleanNotification(body, 240),
1505
+ position: "top-left",
1506
+ sound: "request",
1507
+ title: cleanNotification(title, 80)
1433
1508
  }
1434
- }
1509
+ });
1435
1510
 
1511
+ // src/herdr-block.ts
1436
1512
  class HerdrAdvisorBlock {
1437
1513
  #blocked = false;
1438
1514
  report;
@@ -1492,6 +1568,60 @@ class HerdrAdvisorBlock {
1492
1568
  } catch {}
1493
1569
  }
1494
1570
  }
1571
+
1572
+ // src/herdr.ts
1573
+ var metadataRequest = (clear) => ({
1574
+ id: `${SOURCE}:${nextSequence()}`,
1575
+ method: "pane.report_metadata",
1576
+ params: {
1577
+ agent: "pi",
1578
+ applies_to_source: HERDR_PI_SOURCE,
1579
+ pane_id: process.env.HERDR_PANE_ID ?? "",
1580
+ source: SOURCE,
1581
+ ...clear ? { clear_state_labels: true } : { state_labels: { working: "seeking advice" } },
1582
+ seq: nextSequence()
1583
+ }
1584
+ });
1585
+
1586
+ class HerdrAdvisorActivity {
1587
+ #activeConsultations = 0;
1588
+ report;
1589
+ enabled;
1590
+ constructor(report = sendToHerdr, enabled = () => true) {
1591
+ this.report = report;
1592
+ this.enabled = enabled;
1593
+ }
1594
+ start() {
1595
+ if (!this.enabled()) {
1596
+ return;
1597
+ }
1598
+ this.#activeConsultations += 1;
1599
+ if (this.#activeConsultations === 1) {
1600
+ this.safeReport(false);
1601
+ }
1602
+ }
1603
+ finish() {
1604
+ if (this.#activeConsultations === 0) {
1605
+ return;
1606
+ }
1607
+ this.#activeConsultations -= 1;
1608
+ if (this.#activeConsultations === 0) {
1609
+ this.safeReport(true);
1610
+ }
1611
+ }
1612
+ clear() {
1613
+ if (this.#activeConsultations === 0) {
1614
+ return;
1615
+ }
1616
+ this.#activeConsultations = 0;
1617
+ this.safeReport(true);
1618
+ }
1619
+ safeReport(clear) {
1620
+ try {
1621
+ this.report(metadataRequest(clear));
1622
+ } catch {}
1623
+ }
1624
+ }
1495
1625
  var notifyHerdrAdvisorFailure = (title, body) => {
1496
1626
  if (!getAdvisorSettings().herdrIntegration) {
1497
1627
  return;
@@ -1507,15 +1637,14 @@ var herdrAdvisorBlock = new HerdrAdvisorBlock(sendToHerdr, () => getAdvisorSetti
1507
1637
  import { randomUUID } from "node:crypto";
1508
1638
 
1509
1639
  // src/model-stream.ts
1510
- import {
1511
- stream
1512
- } from "@earendil-works/pi-ai/compat";
1640
+ import { stream } from "@earendil-works/pi-ai/compat";
1513
1641
  var resolveConfiguredModel = async (ctx, ref, label) => {
1514
1642
  if (!ref) {
1515
1643
  throw new Error(`${label} model not configured`);
1516
1644
  }
1517
1645
  const [provider, modelId] = splitRef(ref);
1518
- const model = ctx.modelRegistry.find(provider, modelId);
1646
+ const lookup = [provider, modelId];
1647
+ const model = ctx.modelRegistry.find(...lookup);
1519
1648
  if (!model) {
1520
1649
  throw new Error(`${label} model not found: ${ref}`);
1521
1650
  }
@@ -1535,16 +1664,16 @@ var resolveConfiguredModel = async (ctx, ref, label) => {
1535
1664
  };
1536
1665
  };
1537
1666
  var ADVISOR_STREAM_UPDATE_INTERVAL_MS = 90;
1538
- var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVAL_MS, scheduler = {
1667
+ var defaultScheduler = {
1539
1668
  clearTimeout,
1540
1669
  now: Date.now,
1541
1670
  setTimeout: (callback, delay) => setTimeout(callback, delay)
1542
- }) => {
1671
+ };
1672
+ var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVAL_MS, scheduler = defaultScheduler) => {
1543
1673
  if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
1544
1674
  throw new Error("Coalesced update interval must be positive and finite.");
1545
1675
  }
1546
1676
  let closed = false;
1547
- let hasPending = false;
1548
1677
  let pending;
1549
1678
  let lastPublishedAt;
1550
1679
  let timer;
@@ -1558,12 +1687,11 @@ var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVA
1558
1687
  };
1559
1688
  const publishPending = () => {
1560
1689
  timer = undefined;
1561
- if (!hasPending) {
1690
+ if (!pending) {
1562
1691
  return;
1563
1692
  }
1564
- const value = pending;
1693
+ const { value } = pending;
1565
1694
  pending = undefined;
1566
- hasPending = false;
1567
1695
  lastPublishedAt = scheduler.now();
1568
1696
  try {
1569
1697
  publish(value);
@@ -1589,7 +1717,6 @@ var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVA
1589
1717
  closed = true;
1590
1718
  clearTimer();
1591
1719
  pending = undefined;
1592
- hasPending = false;
1593
1720
  },
1594
1721
  flush: () => {
1595
1722
  if (!closed) {
@@ -1606,8 +1733,7 @@ var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVA
1606
1733
  if (publishFailed) {
1607
1734
  throw publishError;
1608
1735
  }
1609
- pending = value;
1610
- hasPending = true;
1736
+ pending = { value };
1611
1737
  if (timer === undefined) {
1612
1738
  schedule();
1613
1739
  }
@@ -1620,14 +1746,17 @@ var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVA
1620
1746
  var collectTextStream = async (resolved, options, streamModel = stream) => {
1621
1747
  let thinking = "";
1622
1748
  let text = "";
1623
- const eventStream = streamModel(resolved.model, { messages: options.messages, systemPrompt: options.systemPrompt }, {
1749
+ const streamOptions = {
1624
1750
  apiKey: resolved.apiKey,
1625
1751
  env: resolved.env,
1626
1752
  headers: resolved.headers,
1627
1753
  reasoning: options.reasoning,
1628
- ...options.reasoning === undefined ? {} : { reasoningEffort: options.reasoning },
1629
1754
  signal: options.signal
1630
- });
1755
+ };
1756
+ if (options.reasoning !== undefined) {
1757
+ streamOptions.reasoningEffort = options.reasoning;
1758
+ }
1759
+ const eventStream = streamModel(resolved.model, { messages: options.messages, systemPrompt: options.systemPrompt }, streamOptions);
1631
1760
  for await (const event of eventStream) {
1632
1761
  if (event.type === "thinking_delta") {
1633
1762
  thinking += event.delta;
@@ -1661,21 +1790,21 @@ import { lstat, open, realpath } from "node:fs/promises";
1661
1790
  import { isAbsolute, relative, resolve } from "node:path";
1662
1791
  var ADVISOR_FILE_MAX_BYTES = 8 * 1024;
1663
1792
  var ADVISOR_FILES_TOTAL_MAX_BYTES = 24 * 1024;
1664
- var PATH_SEGMENTS = /[\\/]/;
1793
+ var PATH_SEGMENTS = /[\\/]/u;
1665
1794
  var within = (root, candidate) => {
1666
1795
  const path = relative(root, candidate);
1667
1796
  return path !== "" && !path.startsWith("..") && !path.includes("../");
1668
1797
  };
1669
1798
  var normalizeRelativePath = (root, path) => relative(root, resolve(root, path));
1670
1799
  var normalizeRequestedPath = (root, value) => {
1671
- if (typeof value !== "string" || !value || isAbsolute(value) || value.split(PATH_SEGMENTS).includes("..")) {
1800
+ if (!isString(value) || !value || isAbsolute(value) || value.split(PATH_SEGMENTS).includes("..")) {
1672
1801
  return;
1673
1802
  }
1674
1803
  return normalizeRelativePath(root, value);
1675
1804
  };
1676
1805
  var git = (cwd, args) => execFileSync2("git", args, {
1677
1806
  cwd,
1678
- encoding: "utf8",
1807
+ encoding: "utf-8",
1679
1808
  maxBuffer: 16 * 1024 * 1024,
1680
1809
  shell: false,
1681
1810
  stdio: ["ignore", "pipe", "pipe"],
@@ -1686,7 +1815,7 @@ var repositoryRoot = (cwd) => {
1686
1815
  try {
1687
1816
  return realpath(git(cwd, ["rev-parse", "--show-toplevel"]).trim());
1688
1817
  } catch {
1689
- return Promise.resolve(undefined);
1818
+ return Promise.resolve();
1690
1819
  }
1691
1820
  };
1692
1821
  var untracked = (cwd, path) => {
@@ -1726,7 +1855,7 @@ var readAttachment = async (root, normalizedName, redact, available) => {
1726
1855
  if (!within(root, resolved)) {
1727
1856
  return;
1728
1857
  }
1729
- const flags = constants.O_NOFOLLOW ? constants.O_RDONLY | constants.O_NOFOLLOW : constants.O_RDONLY;
1858
+ const flags = constants.O_NOFOLLOW ? constants.O_RDONLY + constants.O_NOFOLLOW : constants.O_RDONLY;
1730
1859
  const file = await open(resolved, flags);
1731
1860
  try {
1732
1861
  const openedStats = await file.stat();
@@ -1735,13 +1864,13 @@ var readAttachment = async (root, normalizedName, redact, available) => {
1735
1864
  }
1736
1865
  const buffer = Buffer.alloc(available + 1);
1737
1866
  const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
1738
- const raw = buffer.subarray(0, bytesRead).toString("utf8");
1867
+ const raw = buffer.subarray(0, bytesRead).toString("utf-8");
1739
1868
  if (raw.includes("\x00")) {
1740
1869
  return;
1741
1870
  }
1742
1871
  const text = redactAndCapText(raw, available, redact);
1743
1872
  return {
1744
- bytes: Buffer.byteLength(text, "utf8"),
1873
+ bytes: Buffer.byteLength(text, "utf-8"),
1745
1874
  path: normalizedName,
1746
1875
  text
1747
1876
  };
@@ -1814,9 +1943,9 @@ var readProjectPreferences = async (ctx, maxBytes = PREFERENCES_MAX_BYTES, redac
1814
1943
  try {
1815
1944
  const buffer = Buffer.alloc(maxBytes + 1);
1816
1945
  const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
1817
- const source = buffer.subarray(0, bytesRead).toString("utf8");
1946
+ const source = buffer.subarray(0, bytesRead).toString("utf-8");
1818
1947
  const capped = redactAndCapText(source, maxBytes, redact);
1819
- return { bytes: Buffer.byteLength(capped, "utf8"), text: capped };
1948
+ return { bytes: Buffer.byteLength(capped, "utf-8"), text: capped };
1820
1949
  } finally {
1821
1950
  await file.close();
1822
1951
  }
@@ -1828,6 +1957,19 @@ import { createHash } from "node:crypto";
1828
1957
 
1829
1958
  // src/tool-result-cap.ts
1830
1959
  var OMITTED_MARKER = "[... omitted tool-result section ...]";
1960
+ var collect = (candidates, maxEntries, maxContentBytes) => {
1961
+ const selected = [];
1962
+ let used = 0;
1963
+ for (const line of candidates.slice(0, maxEntries)) {
1964
+ const next = used + byteLength(line) + (selected.length ? 1 : 0);
1965
+ if (next > maxContentBytes) {
1966
+ break;
1967
+ }
1968
+ selected.push(line);
1969
+ used = next;
1970
+ }
1971
+ return selected;
1972
+ };
1831
1973
  var capToolResult = (value, maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES, maxBytes = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES) => {
1832
1974
  const lines = value.split(`
1833
1975
  `);
@@ -1863,19 +2005,6 @@ var capToolResult = (value, maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES, ma
1863
2005
  }
1864
2006
  const headCount = Math.floor((maxLines - 1) / 2);
1865
2007
  const tailCount = maxLines - 1 - headCount;
1866
- const collect = (candidates, maxEntries, maxContentBytes) => {
1867
- const selected = [];
1868
- let used = 0;
1869
- for (const line of candidates.slice(0, maxEntries)) {
1870
- const next = used + byteLength(line) + (selected.length ? 1 : 0);
1871
- if (next > maxContentBytes) {
1872
- break;
1873
- }
1874
- selected.push(line);
1875
- used = next;
1876
- }
1877
- return selected;
1878
- };
1879
2008
  const availableBytes = maxBytes - markerBytes - 2;
1880
2009
  const head = collect(lines, headCount, Math.floor(availableBytes / 2));
1881
2010
  const tail = collect(lines.slice(Math.max(head.length, lines.length - tailCount)), tailCount, availableBytes - byteLength(head.join(`
@@ -1893,13 +2022,13 @@ var capToolResult = (value, maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES, ma
1893
2022
 
1894
2023
  // src/conversation.ts
1895
2024
  var textFromPart = (part) => {
1896
- if (typeof part === "string") {
2025
+ if (isString(part)) {
1897
2026
  return part;
1898
2027
  }
1899
- if (!isRecord(part) || part.type !== "text") {
2028
+ if (!isRecordOf(part) || part.type !== "text") {
1900
2029
  return "";
1901
2030
  }
1902
- return typeof part.text === "string" ? part.text : "";
2031
+ return isString(part.text) ? part.text : "";
1903
2032
  };
1904
2033
  var textFrom = (content) => contentParts(content).map(textFromPart).join(`
1905
2034
  `).trim();
@@ -1910,10 +2039,10 @@ var assistantEntry = (message, policies, redact) => {
1910
2039
  parts.push(redact ? redactSecrets(text) : text);
1911
2040
  }
1912
2041
  for (const part of contentParts(message.content)) {
1913
- if (!isRecord(part) || part.type !== "toolCall") {
2042
+ if (!isRecordOf(part) || part.type !== "toolCall") {
1914
2043
  continue;
1915
2044
  }
1916
- const toolName = typeof part.name === "string" ? part.name : "unknown";
2045
+ const toolName = isString(part.name) ? part.name : "unknown";
1917
2046
  const policy = policies[toolName] ?? "full";
1918
2047
  if (policy === "exclude") {
1919
2048
  parts.push(`[Tool Call: ${toolName}] (excluded by Advisor tool policy)`);
@@ -1931,7 +2060,7 @@ var assistantEntry = (message, policies, redact) => {
1931
2060
  };
1932
2061
  var toolResultEntry = (message, toolResultMaxLines, toolResultMaxBytes, policies, redact) => {
1933
2062
  const status = message.isError ? "error" : "success";
1934
- const toolName = typeof message.toolName === "string" ? message.toolName : "unknown";
2063
+ const toolName = isString(message.toolName) ? message.toolName : "unknown";
1935
2064
  const policy = policies[toolName] ?? "full";
1936
2065
  const source = textFrom(message.content);
1937
2066
  if (policy === "exclude") {
@@ -1947,13 +2076,13 @@ var toolResultEntry = (message, toolResultMaxLines, toolResultMaxBytes, policies
1947
2076
  ${capped.content}`;
1948
2077
  };
1949
2078
  var conversationEntry = (entry, toolResultMaxLines, toolResultMaxBytes, policies, redact) => {
1950
- if (!isRecord(entry)) {
2079
+ if (!isRecordOf(entry)) {
1951
2080
  return;
1952
2081
  }
1953
- if (entry.type === "compaction" && typeof entry.summary === "string") {
2082
+ if (entry.type === "compaction" && isString(entry.summary)) {
1954
2083
  return `[System Compaction Summary]: ${redact ? redactSecrets(entry.summary) : entry.summary}`;
1955
2084
  }
1956
- if (entry.type !== "message" || !isRecord(entry.message)) {
2085
+ if (entry.type !== "message" || !isRecordOf(entry.message)) {
1957
2086
  return;
1958
2087
  }
1959
2088
  const { message } = entry;
@@ -2018,30 +2147,29 @@ var invalid = (message) => ({
2018
2147
  reason: "invalid-protocol"
2019
2148
  });
2020
2149
  var toolCalls = (message) => contentParts(message.content).filter((part) => isRecord(part) && part.type === "toolCall");
2021
- var toolCallId = (part) => typeof part.id === "string" ? part.id : undefined;
2022
- var indexToolCalls = (entries) => {
2023
- const callOwners = new Map;
2024
- const resultsByCall = new Map;
2025
- const state = { latestUserIndex: -1 };
2026
- for (let index = 0;index < entries.length; index += 1) {
2027
- const failure = indexEntry(entries[index], index, callOwners, resultsByCall, state);
2028
- if (failure) {
2029
- return failure;
2150
+ var toolCallId = (part) => isString(part.id) ? part.id : undefined;
2151
+ var indexAssistantCalls = (message, index, callOwners) => {
2152
+ for (const call of toolCalls(message)) {
2153
+ const id = toolCallId(call);
2154
+ if (!id || callOwners.has(id)) {
2155
+ return invalid(`Assistant tool calls at context entry ${index} have missing or duplicate IDs.`);
2030
2156
  }
2157
+ callOwners.set(id, {
2158
+ index,
2159
+ name: isString(call.name) ? call.name : "unknown"
2160
+ });
2031
2161
  }
2032
- for (const [id, results] of resultsByCall) {
2033
- if (results.length > 1) {
2034
- return invalid(`Tool call ${id} has duplicate result messages.`);
2035
- }
2162
+ return;
2163
+ };
2164
+ var indexToolResult = (entry, message, index, resultsByCall) => {
2165
+ const id = message.toolCallId;
2166
+ if (!isString(id)) {
2167
+ return invalid(`Tool result at context entry ${index} has no tool-call ID.`);
2036
2168
  }
2037
- return {
2038
- index: {
2039
- callOwners,
2040
- latestUserIndex: state.latestUserIndex,
2041
- resultsByCall
2042
- },
2043
- ok: true
2044
- };
2169
+ const results = resultsByCall.get(id) ?? [];
2170
+ results.push({ entry, index });
2171
+ resultsByCall.set(id, results);
2172
+ return;
2045
2173
  };
2046
2174
  var indexEntry = (entry, index, callOwners, resultsByCall, state) => {
2047
2175
  if (entry.type !== "message" || !isRecord(entry.message)) {
@@ -2058,28 +2186,32 @@ var indexEntry = (entry, index, callOwners, resultsByCall, state) => {
2058
2186
  }
2059
2187
  return;
2060
2188
  };
2061
- var indexToolResult = (entry, message, index, resultsByCall) => {
2062
- const id = message.toolCallId;
2063
- if (typeof id !== "string") {
2064
- return invalid(`Tool result at context entry ${index} has no tool-call ID.`);
2189
+ var indexToolCalls = (entries) => {
2190
+ const callOwners = new Map;
2191
+ const resultsByCall = new Map;
2192
+ const state = { latestUserIndex: -1 };
2193
+ for (let index = 0;index < entries.length; index += 1) {
2194
+ const entry = entries[index];
2195
+ if (isRecord(entry)) {
2196
+ const failure = indexEntry(entry, index, callOwners, resultsByCall, state);
2197
+ if (failure) {
2198
+ return failure;
2199
+ }
2200
+ }
2065
2201
  }
2066
- const results = resultsByCall.get(id) ?? [];
2067
- results.push({ entry, index });
2068
- resultsByCall.set(id, results);
2069
- return;
2070
- };
2071
- var indexAssistantCalls = (message, index, callOwners) => {
2072
- for (const call of toolCalls(message)) {
2073
- const id = toolCallId(call);
2074
- if (!id || callOwners.has(id)) {
2075
- return invalid(`Assistant tool calls at context entry ${index} have missing or duplicate IDs.`);
2202
+ for (const [id, results] of resultsByCall) {
2203
+ if (results.length > 1) {
2204
+ return invalid(`Tool call ${id} has duplicate result messages.`);
2076
2205
  }
2077
- callOwners.set(id, {
2078
- index,
2079
- name: typeof call.name === "string" ? call.name : "unknown"
2080
- });
2081
2206
  }
2082
- return;
2207
+ return {
2208
+ index: {
2209
+ callOwners,
2210
+ latestUserIndex: state.latestUserIndex,
2211
+ resultsByCall
2212
+ },
2213
+ ok: true
2214
+ };
2083
2215
  };
2084
2216
 
2085
2217
  // src/scout-types.ts
@@ -2091,8 +2223,8 @@ var SCOUT_SELECTION_MAX_IDS = 32;
2091
2223
  var SCOUT_SYNTHESIS_MAX_BYTES = 4 * 1024;
2092
2224
 
2093
2225
  // src/scout-groups.ts
2094
- var SPEAKER_PREFIX = /^(User|Executor):\s*/;
2095
- var boundedLabel = (value) => [...value.replace(/\s+/g, " ").trim()].slice(0, SCOUT_LABEL_MAX_CHARS).join("");
2226
+ var SPEAKER_PREFIX = /^(?<speaker>User|Executor):\s*/u;
2227
+ var boundedLabel = (value) => [...value.replaceAll(/\s+/gu, " ").trim()].slice(0, SCOUT_LABEL_MAX_CHARS).join("");
2096
2228
  var labelFor = (kind, content) => {
2097
2229
  const preview = boundedLabel(content.replace(SPEAKER_PREFIX, ""));
2098
2230
  const prefix = {
@@ -2123,10 +2255,8 @@ var createGroup = (originalIndex, entryIds, kind, content, required) => ({
2123
2255
  originalIndex,
2124
2256
  required
2125
2257
  });
2126
- var pendingAdvisorArguments = (value) => {
2127
- if (!isRecord(value)) {
2128
- return {};
2129
- }
2258
+ var pendingAdvisorArguments = (part) => {
2259
+ const value = isRecord(part.arguments) ? part.arguments : {};
2130
2260
  const allowed = {};
2131
2261
  for (const key of ["gitContext", "question"]) {
2132
2262
  if (key in value) {
@@ -2139,41 +2269,17 @@ var pendingInvocationDisclosure = (entry, invocationId, toolResultMaxLines, tool
2139
2269
  if (!isRecord(entry.message)) {
2140
2270
  return disclosed;
2141
2271
  }
2142
- const content = contentParts(entry.message.content).map((part) => {
2272
+ const message = entry.message;
2273
+ const content = contentParts(message.content).map((part) => {
2143
2274
  if (!isRecord(part) || part.type !== "toolCall" || toolCallId(part) !== invocationId || part.name !== "ask_advisor") {
2144
2275
  return part;
2145
2276
  }
2146
- return { ...part, arguments: pendingAdvisorArguments(part.arguments) };
2277
+ return { ...part, arguments: pendingAdvisorArguments(part) };
2147
2278
  });
2148
- return conversationEntry({ ...entry, message: { ...entry.message, content } }, toolResultMaxLines, toolResultMaxBytes, policies, redact);
2149
- };
2150
- var toolExchangeGroup = (entry, index, entryId, disclosed, immediate, indexed, consumedResultIndexes, caps) => {
2151
- const calls = toolCalls(entry.message);
2152
- const callIds = calls.map(toolCallId);
2153
- const adjacentFailure = adjacentResultMismatch(immediate, index, callIds, indexed.callOwners);
2154
- if (adjacentFailure) {
2155
- return { kind: "invalid", message: adjacentFailure };
2156
- }
2157
- const missing = new Set;
2158
- const resultParts = [];
2159
- const resultEntryIds = [];
2160
- const matchFailure = collectResults(calls, callIds, index, indexed.resultsByCall, consumedResultIndexes, caps, missing, resultParts, resultEntryIds);
2161
- if (matchFailure) {
2162
- return { kind: "invalid", message: matchFailure };
2163
- }
2164
- if (missing.size > 0) {
2165
- return missingOutcome(entry, index, entryId, disclosed, callIds, missing, resultParts, resultEntryIds, caps);
2166
- }
2167
- return {
2168
- group: createGroup(index, [entryId, ...resultEntryIds], "tool-exchange", [disclosed, ...resultParts].join(`
2169
-
2170
- `), false),
2171
- kind: "group"
2172
- };
2279
+ return conversationEntry({ ...entry, message: { ...message, content } }, toolResultMaxLines, toolResultMaxBytes, policies, redact);
2173
2280
  };
2174
2281
  var adjacentResultMismatch = (immediate, index, callIds, callOwners) => {
2175
- const next = immediate;
2176
- if (next?.type === "message" && isRecord(next.message) && next.message.role === "toolResult" && typeof next.message.toolCallId === "string" && !callIds.includes(next.message.toolCallId) && !callOwners.has(next.message.toolCallId)) {
2282
+ if (immediate?.type === "message" && isRecord(immediate.message) && immediate.message.role === "toolResult" && isString(immediate.message.toolCallId) && !callIds.includes(immediate.message.toolCallId) && !callOwners.has(immediate.message.toolCallId)) {
2177
2283
  return `Tool result at context entry ${index + 1} does not match its adjacent assistant group.`;
2178
2284
  }
2179
2285
  return;
@@ -2189,7 +2295,8 @@ var collectResults = (calls, callIds, index, resultsByCall, consumedResultIndexe
2189
2295
  return `Tool result at context entry ${resultMatch.index} precedes its assistant call.`;
2190
2296
  }
2191
2297
  const resultMessage = resultMatch.entry.message;
2192
- const expectedName = typeof calls[callIndex].name === "string" ? calls[callIndex].name : "unknown";
2298
+ const call = calls[callIndex];
2299
+ const expectedName = isString(call.name) ? call.name : "unknown";
2193
2300
  if (!isRecord(resultMessage) || resultMessage.toolName !== expectedName) {
2194
2301
  return `Tool result at context entry ${resultMatch.index} conflicts with call ${callId}.`;
2195
2302
  }
@@ -2198,7 +2305,7 @@ var collectResults = (calls, callIds, index, resultsByCall, consumedResultIndexe
2198
2305
  if (resultText) {
2199
2306
  resultParts.push(resultText);
2200
2307
  }
2201
- resultEntryIds.push(typeof resultMatch.entry.id === "string" ? resultMatch.entry.id : String(resultMatch.index));
2308
+ resultEntryIds.push(isString(resultMatch.entry.id) ? resultMatch.entry.id : String(resultMatch.index));
2202
2309
  }
2203
2310
  return;
2204
2311
  };
@@ -2221,37 +2328,47 @@ var missingOutcome = (entry, index, entryId, disclosed, callIds, missing, result
2221
2328
  kind: "group"
2222
2329
  };
2223
2330
  };
2224
- var buildGroups = (entries, indexed, caps) => {
2225
- const groups = [];
2226
- const consumedResultIndexes = new Set;
2227
- let protocolOmittedBytes = 0;
2228
- let protocolOmittedCount = 0;
2229
- for (let index = 0;index < entries.length; index += 1) {
2230
- const entry = entries[index];
2231
- const disclosed = conversationEntry(entry, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact);
2232
- if (!disclosed) {
2233
- continue;
2234
- }
2235
- const outcome = groupForEntry(entry, index, disclosed, entries[index + 1], indexed, consumedResultIndexes, caps);
2236
- if (outcome.kind === "invalid") {
2237
- return invalid(outcome.message);
2238
- }
2239
- if (outcome.kind === "omitted") {
2240
- protocolOmittedCount += 1;
2241
- protocolOmittedBytes += outcome.bytes;
2242
- } else if (outcome.kind === "group") {
2243
- groups.push(outcome.group);
2244
- }
2331
+ var toolExchangeGroup = (entry, message, index, entryId, disclosed, immediate, indexed, consumedResultIndexes, caps) => {
2332
+ const calls = toolCalls(message);
2333
+ const callIds = calls.map(toolCallId).filter(isString);
2334
+ const adjacentFailure = adjacentResultMismatch(immediate, index, callIds, indexed.callOwners);
2335
+ if (adjacentFailure) {
2336
+ return { kind: "invalid", message: adjacentFailure };
2337
+ }
2338
+ const missing = new Set;
2339
+ const resultParts = [];
2340
+ const resultEntryIds = [];
2341
+ const matchFailure = collectResults(calls, callIds, index, indexed.resultsByCall, consumedResultIndexes, caps, missing, resultParts, resultEntryIds);
2342
+ if (matchFailure) {
2343
+ return { kind: "invalid", message: matchFailure };
2344
+ }
2345
+ if (missing.size > 0) {
2346
+ return missingOutcome(entry, index, entryId, disclosed, callIds, missing, resultParts, resultEntryIds, caps);
2245
2347
  }
2246
2348
  return {
2247
- groups,
2248
- ok: true,
2249
- protocolOmittedBytes,
2250
- protocolOmittedCount
2349
+ group: createGroup(index, [entryId, ...resultEntryIds], "tool-exchange", [disclosed, ...resultParts].join(`
2350
+
2351
+ `), false),
2352
+ kind: "group"
2251
2353
  };
2252
2354
  };
2355
+ var ownerOf = (message, callOwners) => isString(message.toolCallId) ? callOwners.get(message.toolCallId) : undefined;
2356
+ var contentPartsOf = (message) => Array.isArray(message.content) ? message.content : [];
2357
+ var hasCalls = (message) => contentPartsOf(message).some((part) => isRecord(part) && part.type === "toolCall");
2358
+ var toolResultOutcome = (message, index, disclosed, callOwners, consumedResultIndexes) => {
2359
+ if (consumedResultIndexes.has(index)) {
2360
+ return { kind: "skipped" };
2361
+ }
2362
+ if (ownerOf(message, callOwners)) {
2363
+ return {
2364
+ kind: "invalid",
2365
+ message: `Tool result at context entry ${index} precedes or conflicts with its retained call.`
2366
+ };
2367
+ }
2368
+ return { bytes: byteLength(disclosed), kind: "omitted" };
2369
+ };
2253
2370
  var groupForEntry = (entry, index, disclosed, immediate, indexed, consumedResultIndexes, caps) => {
2254
- const entryId = typeof entry.id === "string" ? entry.id : String(index);
2371
+ const entryId = isString(entry.id) ? entry.id : String(index);
2255
2372
  if (entry.type !== "message" || !isRecord(entry.message)) {
2256
2373
  return {
2257
2374
  group: createGroup(index, [entryId], "compaction", disclosed, false),
@@ -2277,23 +2394,42 @@ var groupForEntry = (entry, index, disclosed, immediate, indexed, consumedResult
2277
2394
  kind: "group"
2278
2395
  };
2279
2396
  }
2280
- return toolExchangeGroup(entry, index, entryId, disclosed, immediate, indexed, consumedResultIndexes, caps);
2397
+ return toolExchangeGroup(entry, message, index, entryId, disclosed, immediate, indexed, consumedResultIndexes, caps);
2281
2398
  };
2282
- var toolResultOutcome = (message, index, disclosed, callOwners, consumedResultIndexes) => {
2283
- if (consumedResultIndexes.has(index)) {
2284
- return { kind: "skipped" };
2285
- }
2286
- if (ownerOf(message, callOwners)) {
2287
- return {
2288
- kind: "invalid",
2289
- message: `Tool result at context entry ${index} precedes or conflicts with its retained call.`
2290
- };
2399
+ var buildGroups = (entries, indexed, caps) => {
2400
+ const groups = [];
2401
+ const consumedResultIndexes = new Set;
2402
+ let protocolOmittedBytes = 0;
2403
+ let protocolOmittedCount = 0;
2404
+ for (let index = 0;index < entries.length; index += 1) {
2405
+ const entry = entries[index];
2406
+ if (!isRecord(entry)) {
2407
+ continue;
2408
+ }
2409
+ const disclosed = conversationEntry(entry, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact);
2410
+ if (!disclosed) {
2411
+ continue;
2412
+ }
2413
+ const next = entries[index + 1];
2414
+ const immediate = isRecord(next) ? next : undefined;
2415
+ const outcome = groupForEntry(entry, index, disclosed, immediate, indexed, consumedResultIndexes, caps);
2416
+ if (outcome.kind === "invalid") {
2417
+ return invalid(outcome.message);
2418
+ }
2419
+ if (outcome.kind === "omitted") {
2420
+ protocolOmittedCount += 1;
2421
+ protocolOmittedBytes += outcome.bytes;
2422
+ } else if (outcome.kind === "group") {
2423
+ groups.push(outcome.group);
2424
+ }
2291
2425
  }
2292
- return { bytes: byteLength(disclosed), kind: "omitted" };
2426
+ return {
2427
+ groups,
2428
+ ok: true,
2429
+ protocolOmittedBytes,
2430
+ protocolOmittedCount
2431
+ };
2293
2432
  };
2294
- var ownerOf = (message, callOwners) => typeof message.toolCallId === "string" ? callOwners.get(message.toolCallId) : undefined;
2295
- var hasCalls = (message) => contentPartsOf(message).some((part) => isRecord(part) && part.type === "toolCall");
2296
- var contentPartsOf = (message) => Array.isArray(message.content) ? message.content : [];
2297
2433
 
2298
2434
  // src/scout-reconstruct.ts
2299
2435
  var prefixWithinCharBudget = (value, maxChars) => {
@@ -2308,7 +2444,7 @@ var prefixWithinCharBudget = (value, maxChars) => {
2308
2444
  };
2309
2445
  var reconstructScoutConversation = (manifest, selectedIds, synthesis, maxChars = Number.MAX_SAFE_INTEGER) => {
2310
2446
  const selected = new Set(selectedIds);
2311
- const evidence = manifest.groups.filter((group) => group.required || selected.has(group.id)).sort((left, right) => left.originalIndex - right.originalIndex).map((group) => group.content);
2447
+ const evidence = manifest.groups.filter((group) => group.required || selected.has(group.id)).toSorted((left, right) => left.originalIndex - right.originalIndex).map((group) => group.content);
2312
2448
  const evidenceText = evidence.join(`
2313
2449
 
2314
2450
  `);
@@ -2333,15 +2469,106 @@ ${synthesis.trim()}` : undefined;
2333
2469
  return `${evidenceText}${separator}${prefixWithinCharBudget(inference, remaining)}`;
2334
2470
  };
2335
2471
 
2472
+ // src/scout-context.ts
2473
+ var resolveCaps = (options) => ({
2474
+ currentInvocationId: options.currentInvocationId,
2475
+ maxConversationChars: options.maxConversationChars,
2476
+ maxGroupBytes: options.maxGroupBytes ?? SCOUT_GROUP_MAX_BYTES,
2477
+ maxGroups: options.maxGroups ?? SCOUT_MANIFEST_MAX_GROUPS,
2478
+ maxManifestBytes: options.maxManifestBytes ?? options.maxBytes ?? SCOUT_MANIFEST_MAX_BYTES,
2479
+ policies: options.policies ?? advisorToolPoliciesRef,
2480
+ redact: options.redact ?? advisorRedactSecretsRef,
2481
+ toolResultMaxBytes: options.toolResultMaxBytes ?? advisorToolResultMaxBytesRef,
2482
+ toolResultMaxLines: options.toolResultMaxLines ?? advisorToolResultMaxLinesRef
2483
+ });
2484
+ var contentChars = (items) => items.reduce((sum, group) => sum + group.content.length, 0) + Math.max(0, items.length - 1) * 2;
2485
+ var requiredOverflow = (required, caps) => {
2486
+ if (required.some((group) => group.bytes > caps.maxGroupBytes) || required.length > caps.maxGroups || required.reduce((sum, group) => sum + groupWireBytes(group), 0) > caps.maxManifestBytes) {
2487
+ return {
2488
+ message: "Required Scout context exceeds the Scout manifest transport limit.",
2489
+ ok: false,
2490
+ reason: "required-group-overflow"
2491
+ };
2492
+ }
2493
+ if (caps.maxConversationChars !== undefined && contentChars(required) > caps.maxConversationChars) {
2494
+ return {
2495
+ message: "Required Scout context exceeds the Advisor conversation budget.",
2496
+ ok: false,
2497
+ reason: "required-group-overflow"
2498
+ };
2499
+ }
2500
+ return;
2501
+ };
2502
+ var fits = (selected, caps) => selected.length <= caps.maxGroups && selected.reduce((sum, group) => sum + groupWireBytes(group), 0) <= caps.maxManifestBytes && (caps.maxConversationChars === undefined || contentChars(selected) <= caps.maxConversationChars);
2503
+ var fitToBudget = (built, caps) => {
2504
+ const { groups, protocolOmittedBytes, protocolOmittedCount } = built;
2505
+ const availableCount = groups.length + protocolOmittedCount;
2506
+ const availableBytes = groups.reduce((sum, group) => sum + groupWireBytes(group), 0) + protocolOmittedBytes;
2507
+ if (caps.maxManifestBytes <= 0) {
2508
+ return {
2509
+ manifest: {
2510
+ availableBytes: 0,
2511
+ availableCount: 0,
2512
+ groups: [],
2513
+ omittedBytes: 0,
2514
+ omittedCount: 0
2515
+ },
2516
+ ok: true
2517
+ };
2518
+ }
2519
+ const required = groups.filter((group) => group.required);
2520
+ const overflow = requiredOverflow(required, caps);
2521
+ if (overflow) {
2522
+ return overflow;
2523
+ }
2524
+ const selected = groups.filter((group) => group.required || group.bytes <= caps.maxGroupBytes);
2525
+ while (!fits(selected, caps)) {
2526
+ const optionalIndex = selected.findIndex((group) => !group.required);
2527
+ if (optionalIndex === -1) {
2528
+ return {
2529
+ message: "Required Scout context exceeds fixed manifest limits.",
2530
+ ok: false,
2531
+ reason: "required-group-overflow"
2532
+ };
2533
+ }
2534
+ selected.splice(optionalIndex, 1);
2535
+ }
2536
+ const selectedIds = new Set(selected.map((group) => group.id));
2537
+ const omitted = groups.filter((group) => !selectedIds.has(group.id));
2538
+ return {
2539
+ manifest: {
2540
+ availableBytes,
2541
+ availableCount,
2542
+ groups: selected,
2543
+ omittedBytes: protocolOmittedBytes + omitted.reduce((sum, group) => sum + group.bytes, 0),
2544
+ omittedCount: protocolOmittedCount + omitted.length
2545
+ },
2546
+ ok: true
2547
+ };
2548
+ };
2549
+ var buildScoutManifest = (ctx, options = {}) => {
2550
+ const entries = ctx.sessionManager.buildContextEntries();
2551
+ const caps = resolveCaps(options);
2552
+ const indexed = indexToolCalls(entries);
2553
+ if (!indexed.ok) {
2554
+ return indexed;
2555
+ }
2556
+ const built = buildGroups(entries, indexed.index, caps);
2557
+ if (!built.ok) {
2558
+ return built;
2559
+ }
2560
+ return fitToBudget(built, caps);
2561
+ };
2562
+
2336
2563
  // src/usage.ts
2337
- var finite = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
2564
+ var finite = (value) => isNumber(value) && Number.isFinite(value) && value >= 0 ? value : undefined;
2338
2565
  var add = (left, right) => left === undefined || right === undefined ? left ?? right : left + right;
2339
2566
  var costFields = ["input", "output", "cacheRead", "cacheWrite", "total"];
2340
2567
  var snapshotAdvisorUsage = (usage) => {
2341
- if (!isRecord(usage)) {
2568
+ if (!isRecordOf(usage)) {
2342
2569
  return;
2343
2570
  }
2344
- const cost = isRecord(usage.cost) ? usage.cost : undefined;
2571
+ const cost = isRecordOf(usage.cost) ? usage.cost : undefined;
2345
2572
  const snapshot = {
2346
2573
  cacheRead: finite(usage.cacheRead),
2347
2574
  cacheWrite: finite(usage.cacheWrite),
@@ -2356,21 +2583,19 @@ var snapshotAdvisorUsage = (usage) => {
2356
2583
  var advisorUsageCost = (usage) => snapshotAdvisorUsage(usage)?.cost;
2357
2584
  var advisorUsageForPi = (usage) => {
2358
2585
  const snapshot = snapshotAdvisorUsage(usage);
2359
- if (!(snapshot && isRecord(usage))) {
2586
+ if (!(snapshot && isRecordOf(usage))) {
2360
2587
  return;
2361
2588
  }
2362
- const cost = isRecord(usage.cost) ? usage.cost : undefined;
2589
+ const cost = isRecordOf(usage.cost) ? usage.cost : undefined;
2363
2590
  const input = snapshot.input ?? 0;
2364
2591
  const output = snapshot.output ?? 0;
2365
2592
  const cacheRead = snapshot.cacheRead ?? 0;
2366
2593
  const cacheWrite = snapshot.cacheWrite ?? 0;
2367
2594
  const cacheWrite1h = finite(usage.cacheWrite1h);
2368
2595
  const reasoning = finite(usage.reasoning);
2369
- return {
2596
+ const piUsage = {
2370
2597
  cacheRead,
2371
2598
  cacheWrite,
2372
- ...cacheWrite1h === undefined ? {} : { cacheWrite1h },
2373
- ...reasoning === undefined ? {} : { reasoning },
2374
2599
  cost: {
2375
2600
  cacheRead: finite(cost?.cacheRead) ?? 0,
2376
2601
  cacheWrite: finite(cost?.cacheWrite) ?? 0,
@@ -2382,6 +2607,13 @@ var advisorUsageForPi = (usage) => {
2382
2607
  output,
2383
2608
  totalTokens: snapshot.totalTokens ?? input + output + cacheRead + cacheWrite
2384
2609
  };
2610
+ if (cacheWrite1h !== undefined) {
2611
+ piUsage.cacheWrite1h = cacheWrite1h;
2612
+ }
2613
+ if (reasoning !== undefined) {
2614
+ piUsage.reasoning = reasoning;
2615
+ }
2616
+ return piUsage;
2385
2617
  };
2386
2618
  var emptyAdvisorUsageTotals = () => ({
2387
2619
  calls: 0,
@@ -2476,8 +2708,8 @@ var defaultDependencies = {
2476
2708
  collect: collectTextStream,
2477
2709
  resolve: resolveConfiguredModel
2478
2710
  };
2479
- var byteLength2 = (value) => Buffer.byteLength(value, "utf8");
2480
- var AUTH_ERROR_PATTERN = /api key|auth|login|credential/i;
2711
+ var byteLength2 = (value) => Buffer.byteLength(value, "utf-8");
2712
+ var AUTH_ERROR_PATTERN = /api key|auth|login|credential/iu;
2481
2713
  var manifestMessage = (manifest) => ({
2482
2714
  content: [
2483
2715
  {
@@ -2494,6 +2726,7 @@ var manifestMessage = (manifest) => ({
2494
2726
  role: "user",
2495
2727
  timestamp: Date.now()
2496
2728
  });
2729
+ var isStringArray = (value) => Array.isArray(value) && value.every(isString);
2497
2730
  var parseScoutSelection = (text, manifest) => {
2498
2731
  if (!text.trim()) {
2499
2732
  throw new Error("Scout returned an empty response.");
@@ -2504,18 +2737,17 @@ var parseScoutSelection = (text, manifest) => {
2504
2737
  } catch (error) {
2505
2738
  throw new Error("Scout response is not a JSON object.", { cause: error });
2506
2739
  }
2507
- if (!value || typeof value !== "object" || Array.isArray(value)) {
2740
+ if (!isRecord(value)) {
2508
2741
  throw new Error("Scout response must be a JSON object.");
2509
2742
  }
2510
- const record = value;
2511
- const keys = Object.keys(record).sort();
2743
+ const keys = Object.keys(value).toSorted();
2512
2744
  if (keys.length !== 2 || keys[0] !== "selectedIds" || keys[1] !== "synthesis") {
2513
2745
  throw new Error("Scout response must contain only selectedIds and synthesis.");
2514
2746
  }
2515
- if (!(Array.isArray(record.selectedIds) && record.selectedIds.every((id) => typeof id === "string"))) {
2747
+ if (!isStringArray(value.selectedIds)) {
2516
2748
  throw new Error("Scout selectedIds must be an array of strings.");
2517
2749
  }
2518
- const selectedIds = record.selectedIds;
2750
+ const { selectedIds } = value;
2519
2751
  if (new Set(selectedIds).size !== selectedIds.length) {
2520
2752
  throw new Error("Scout selected duplicate group IDs.");
2521
2753
  }
@@ -2529,15 +2761,15 @@ var parseScoutSelection = (text, manifest) => {
2529
2761
  const optionalIds = knownSelectedIds.filter((id) => !required.has(id)).slice(0, SCOUT_SELECTION_MAX_IDS - requiredIds.length);
2530
2762
  const retained = new Set([...requiredIds, ...optionalIds]);
2531
2763
  const normalizedIds = manifest.groups.filter((group) => retained.has(group.id)).map((group) => group.id);
2532
- if (typeof record.synthesis !== "string") {
2533
- throw new Error("Scout synthesis must be a string.");
2764
+ if (!isString(value.synthesis)) {
2765
+ throw new TypeError("Scout synthesis must be a string.");
2534
2766
  }
2535
- if (byteLength2(record.synthesis) > SCOUT_SYNTHESIS_MAX_BYTES) {
2767
+ if (byteLength2(value.synthesis) > SCOUT_SYNTHESIS_MAX_BYTES) {
2536
2768
  throw new Error(`Scout synthesis exceeds ${SCOUT_SYNTHESIS_MAX_BYTES} UTF-8 bytes.`);
2537
2769
  }
2538
2770
  return {
2539
2771
  selectedIds: normalizedIds,
2540
- synthesis: knownSelectedIds.length > 0 ? record.synthesis : ""
2772
+ synthesis: knownSelectedIds.length > 0 ? value.synthesis : ""
2541
2773
  };
2542
2774
  };
2543
2775
  var baseMetrics = (manifest, startedAt) => ({
@@ -2556,6 +2788,61 @@ var classifyResolutionError = (message) => {
2556
2788
  }
2557
2789
  return "provider-error";
2558
2790
  };
2791
+ var setupAbortWatch = (parentSignal, timeoutMs) => {
2792
+ const controller = new AbortController;
2793
+ let timedOut = false;
2794
+ const abortFromParent = () => controller.abort(parentSignal?.reason);
2795
+ parentSignal?.addEventListener("abort", abortFromParent, { once: true });
2796
+ const timer = setTimeout(() => {
2797
+ timedOut = true;
2798
+ controller.abort(new Error("Scout timed out."));
2799
+ }, timeoutMs);
2800
+ timer.unref?.();
2801
+ const { promise: abortPromise, reject: rejectOnAbort } = Promise.withResolvers();
2802
+ const onControllerAbort = () => rejectOnAbort(controller.signal.reason ?? new Error("Scout aborted."));
2803
+ controller.signal.addEventListener("abort", onControllerAbort, {
2804
+ once: true
2805
+ });
2806
+ return {
2807
+ abortPromise,
2808
+ controller,
2809
+ teardown: () => {
2810
+ clearTimeout(timer);
2811
+ parentSignal?.removeEventListener("abort", abortFromParent);
2812
+ controller.signal.removeEventListener("abort", onControllerAbort);
2813
+ },
2814
+ wasTimedOut: () => timedOut
2815
+ };
2816
+ };
2817
+ var streamScoutResponse = async (dependencies, resolved, manifest, parentSignal, timeoutMs, publish) => {
2818
+ const { abortPromise, controller, teardown, wasTimedOut } = setupAbortWatch(parentSignal, timeoutMs);
2819
+ try {
2820
+ const collection = dependencies.collect(resolved, {
2821
+ messages: [manifestMessage(manifest)],
2822
+ onChunk: (thinking, text) => {
2823
+ if (!controller.signal.aborted) {
2824
+ publish({ model: executorRef, text, thinking, type: "chunk" });
2825
+ }
2826
+ },
2827
+ reasoning: executorEffortRef,
2828
+ signal: controller.signal,
2829
+ systemPrompt: SCOUT_SYSTEM
2830
+ });
2831
+ return {
2832
+ ok: true,
2833
+ streamed: await Promise.race([collection, abortPromise])
2834
+ };
2835
+ } catch (error) {
2836
+ const message = error instanceof Error ? error.message : String(error);
2837
+ return wasTimedOut() ? {
2838
+ category: "timeout",
2839
+ message: `Scout timed out after ${timeoutMs} ms.`,
2840
+ ok: false
2841
+ } : { category: "provider-error", message, ok: false };
2842
+ } finally {
2843
+ teardown();
2844
+ }
2845
+ };
2559
2846
  var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = SCOUT_TIMEOUT_MS, dependencies = defaultDependencies) => {
2560
2847
  const startedAt = Date.now();
2561
2848
  const publish = (event) => {
@@ -2593,169 +2880,40 @@ var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = S
2593
2880
  return cancelled();
2594
2881
  }
2595
2882
  publish({ model: executorRef, type: "call" });
2596
- const controller = new AbortController;
2597
- let timedOut = false;
2598
- const abortFromParent = () => controller.abort(parentSignal?.reason);
2599
- parentSignal?.addEventListener("abort", abortFromParent, { once: true });
2600
- const timer = setTimeout(() => {
2601
- timedOut = true;
2602
- controller.abort(new Error("Scout timed out."));
2603
- }, timeoutMs);
2604
- timer.unref?.();
2605
- let rejectOnAbort;
2606
- const onControllerAbort = () => rejectOnAbort?.(controller.signal.reason ?? new Error("Scout aborted."));
2607
- const abortPromise = new Promise((_resolve, reject) => {
2608
- rejectOnAbort = reject;
2609
- controller.signal.addEventListener("abort", onControllerAbort, {
2610
- once: true
2611
- });
2612
- });
2613
- const teardown = () => {
2614
- clearTimeout(timer);
2615
- parentSignal?.removeEventListener("abort", abortFromParent);
2616
- controller.signal.removeEventListener("abort", onControllerAbort);
2617
- };
2618
- let streamed;
2619
- try {
2620
- const collection = dependencies.collect(resolved, {
2621
- messages: [manifestMessage(manifest)],
2622
- onChunk: (thinking, text) => {
2623
- if (!controller.signal.aborted) {
2624
- publish({ model: executorRef, text, thinking, type: "chunk" });
2625
- }
2626
- },
2627
- reasoning: executorEffortRef,
2628
- signal: controller.signal,
2629
- systemPrompt: SCOUT_SYSTEM
2630
- });
2631
- streamed = await Promise.race([collection, abortPromise]);
2632
- } catch (error) {
2633
- teardown();
2883
+ const streamed = await streamScoutResponse(dependencies, resolved, manifest, parentSignal, timeoutMs, publish);
2884
+ if (!streamed.ok) {
2634
2885
  if (parentSignal?.aborted) {
2635
2886
  return cancelled();
2636
2887
  }
2637
- const message = error instanceof Error ? error.message : String(error);
2638
- return timedOut ? fallback("timeout", `Scout timed out after ${timeoutMs} ms.`) : fallback("provider-error", message);
2888
+ return fallback(streamed.category, streamed.message);
2639
2889
  }
2640
- teardown();
2641
2890
  if (parentSignal?.aborted) {
2642
2891
  return cancelled();
2643
2892
  }
2644
- let selection;
2645
- try {
2646
- selection = parseScoutSelection(streamed.text, manifest);
2647
- } catch (error) {
2648
- const message = error instanceof Error ? error.message : String(error);
2649
- return fallback(streamed.text.trim() ? "invalid-selection" : "empty-response", message, snapshotAdvisorUsage(streamed.usage));
2650
- }
2651
- const outcome = {
2652
- conversation: reconstructScoutConversation(manifest, selection.selectedIds, selection.synthesis),
2653
- metrics: {
2654
- ...baseMetrics(manifest, startedAt),
2655
- selectedCount: new Set([
2656
- ...selection.selectedIds,
2657
- ...manifest.groups.filter((group) => group.required).map((group) => group.id)
2658
- ]).size,
2659
- usage: snapshotAdvisorUsage(streamed.usage)
2660
- },
2661
- model: executorRef,
2662
- ok: true,
2663
- selectedLabels: manifest.groups.filter((group) => group.required || selection.selectedIds.includes(group.id)).map((group) => group.label),
2664
- selection
2665
- };
2666
- publish({ outcome, type: "success" });
2667
- return outcome;
2668
- };
2669
-
2670
- // src/scout-context.ts
2671
- var resolveCaps = (options) => ({
2672
- currentInvocationId: options.currentInvocationId,
2673
- maxConversationChars: options.maxConversationChars,
2674
- maxGroupBytes: options.maxGroupBytes ?? SCOUT_GROUP_MAX_BYTES,
2675
- maxGroups: options.maxGroups ?? SCOUT_MANIFEST_MAX_GROUPS,
2676
- maxManifestBytes: options.maxManifestBytes ?? options.maxBytes ?? SCOUT_MANIFEST_MAX_BYTES,
2677
- policies: options.policies ?? advisorToolPoliciesRef,
2678
- redact: options.redact ?? advisorRedactSecretsRef,
2679
- toolResultMaxBytes: options.toolResultMaxBytes ?? advisorToolResultMaxBytesRef,
2680
- toolResultMaxLines: options.toolResultMaxLines ?? advisorToolResultMaxLinesRef
2681
- });
2682
- var buildScoutManifest = (ctx, options = {}) => {
2683
- const entries = ctx.sessionManager.buildContextEntries();
2684
- const caps = resolveCaps(options);
2685
- const indexed = indexToolCalls(entries);
2686
- if (!indexed.ok) {
2687
- return indexed;
2688
- }
2689
- const built = buildGroups(entries, indexed.index, caps);
2690
- if (!built.ok) {
2691
- return built;
2692
- }
2693
- return fitToBudget(built, caps);
2694
- };
2695
- var fits = (selected, caps) => selected.length <= caps.maxGroups && selected.reduce((sum, group) => sum + groupWireBytes(group), 0) <= caps.maxManifestBytes && (caps.maxConversationChars === undefined || contentChars(selected) <= caps.maxConversationChars);
2696
- var contentChars = (items) => items.reduce((sum, group) => sum + group.content.length, 0) + Math.max(0, items.length - 1) * 2;
2697
- var fitToBudget = (built, caps) => {
2698
- const { groups, protocolOmittedBytes, protocolOmittedCount } = built;
2699
- const availableCount = groups.length + protocolOmittedCount;
2700
- const availableBytes = groups.reduce((sum, group) => sum + groupWireBytes(group), 0) + protocolOmittedBytes;
2701
- if (caps.maxManifestBytes <= 0) {
2702
- return {
2703
- manifest: {
2704
- availableBytes: 0,
2705
- availableCount: 0,
2706
- groups: [],
2707
- omittedBytes: 0,
2708
- omittedCount: 0
2709
- },
2710
- ok: true
2711
- };
2712
- }
2713
- const required = groups.filter((group) => group.required);
2714
- const overflow = requiredOverflow(required, caps);
2715
- if (overflow) {
2716
- return overflow;
2717
- }
2718
- const selected = groups.filter((group) => group.required || group.bytes <= caps.maxGroupBytes);
2719
- while (!fits(selected, caps)) {
2720
- const optionalIndex = selected.findIndex((group) => !group.required);
2721
- if (optionalIndex < 0) {
2722
- return {
2723
- message: "Required Scout context exceeds fixed manifest limits.",
2724
- ok: false,
2725
- reason: "required-group-overflow"
2726
- };
2727
- }
2728
- selected.splice(optionalIndex, 1);
2729
- }
2730
- const selectedIds = new Set(selected.map((group) => group.id));
2731
- const omitted = groups.filter((group) => !selectedIds.has(group.id));
2732
- return {
2733
- manifest: {
2734
- availableBytes,
2735
- availableCount,
2736
- groups: selected,
2737
- omittedBytes: protocolOmittedBytes + omitted.reduce((sum, group) => sum + group.bytes, 0),
2738
- omittedCount: protocolOmittedCount + omitted.length
2893
+ let selection;
2894
+ try {
2895
+ selection = parseScoutSelection(streamed.streamed.text, manifest);
2896
+ } catch (error) {
2897
+ const message = error instanceof Error ? error.message : String(error);
2898
+ return fallback(streamed.streamed.text.trim() ? "invalid-selection" : "empty-response", message, snapshotAdvisorUsage(streamed.streamed.usage));
2899
+ }
2900
+ const outcome = {
2901
+ conversation: reconstructScoutConversation(manifest, selection.selectedIds, selection.synthesis),
2902
+ metrics: {
2903
+ ...baseMetrics(manifest, startedAt),
2904
+ selectedCount: new Set([
2905
+ ...selection.selectedIds,
2906
+ ...manifest.groups.filter((group) => group.required).map((group) => group.id)
2907
+ ]).size,
2908
+ usage: snapshotAdvisorUsage(streamed.streamed.usage)
2739
2909
  },
2740
- ok: true
2910
+ model: executorRef,
2911
+ ok: true,
2912
+ selectedLabels: manifest.groups.filter((group) => group.required || selection.selectedIds.includes(group.id)).map((group) => group.label),
2913
+ selection
2741
2914
  };
2742
- };
2743
- var requiredOverflow = (required, caps) => {
2744
- if (required.some((group) => group.bytes > caps.maxGroupBytes) || required.length > caps.maxGroups || required.reduce((sum, group) => sum + groupWireBytes(group), 0) > caps.maxManifestBytes) {
2745
- return {
2746
- message: "Required Scout context exceeds the Scout manifest transport limit.",
2747
- ok: false,
2748
- reason: "required-group-overflow"
2749
- };
2750
- }
2751
- if (caps.maxConversationChars !== undefined && contentChars(required) > caps.maxConversationChars) {
2752
- return {
2753
- message: "Required Scout context exceeds the Advisor conversation budget.",
2754
- ok: false,
2755
- reason: "required-group-overflow"
2756
- };
2757
- }
2758
- return;
2915
+ publish({ outcome, type: "success" });
2916
+ return outcome;
2759
2917
  };
2760
2918
 
2761
2919
  // src/scout-curation.ts
@@ -2788,7 +2946,7 @@ var curateAdvisorConversation = async (ctx, legacyConversation, signal, onScout,
2788
2946
  onScout?.({ outcome: scout, type: "fallback" });
2789
2947
  return { conversation: legacyConversation, scout };
2790
2948
  }
2791
- const outcome = await runScout(ctx, built.manifest, signal, onScout, undefined, undefined);
2949
+ const outcome = await runScout(ctx, built.manifest, signal, onScout);
2792
2950
  if (!outcome.ok && outcome.cancelled) {
2793
2951
  throw signal?.reason instanceof Error ? signal.reason : new Error("Advisor operation cancelled during Scout.");
2794
2952
  }
@@ -2839,26 +2997,21 @@ ${question}` : ""}`;
2839
2997
  return text.trim() || "No conversation context is available. State that you cannot review without context.";
2840
2998
  };
2841
2999
  var advisorGitContextBudget = (contextMaxChars, gitContextMaxChars) => Math.min(gitContextMaxChars, Math.floor(contextMaxChars / 2));
3000
+ var LEVEL_WITHHELD = {
3001
+ collected: true,
3002
+ "no-changes": false
3003
+ };
3004
+ var STATUS_NOTES = {
3005
+ disabled: "Repository context was disabled or had no disclosure budget; it was withheld. Do not assume the working tree is clean.",
3006
+ failed: "Repository context could not be collected. Do not assume the working tree is clean.",
3007
+ "no-changes": "The working tree has no uncommitted changes.",
3008
+ "not-a-repository": "No Git repository is available for this session."
3009
+ };
2842
3010
  var gitContextNote = (result, requested, allowed) => {
2843
3011
  if (requested !== allowed && LEVEL_WITHHELD[result.status]) {
2844
3012
  return `Repository context was limited to "${allowed}" by user configuration; a fuller view was requested but withheld.`;
2845
3013
  }
2846
- switch (result.status) {
2847
- case "disabled":
2848
- return "Repository context was disabled or had no disclosure budget; it was withheld. Do not assume the working tree is clean.";
2849
- case "no-changes":
2850
- return "The working tree has no uncommitted changes.";
2851
- case "not-a-repository":
2852
- return "No Git repository is available for this session.";
2853
- case "failed":
2854
- return "Repository context could not be collected. Do not assume the working tree is clean.";
2855
- default:
2856
- return;
2857
- }
2858
- };
2859
- var LEVEL_WITHHELD = {
2860
- collected: true,
2861
- "no-changes": false
3014
+ return STATUS_NOTES[result.status];
2862
3015
  };
2863
3016
  var advisorRepositoryContext = (result, requested, allowed, budget) => {
2864
3017
  const note = gitContextNote(result, requested, allowed);
@@ -2937,9 +3090,10 @@ var assembleConsultationContext = async (options) => {
2937
3090
  };
2938
3091
 
2939
3092
  // src/tools/gate-protocol.ts
2940
- var DECISION_LINE = /^Decision\s*:\s*(proceed|revise|blocked)\s*$/i;
2941
- var CODE_FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
2942
- var LINE_BREAK = /\r?\n/;
3093
+ var namedGroups = (match) => match.groups ?? {};
3094
+ var DECISION_LINE = /^Decision\s*:\s*(?<decision>proceed|revise|blocked)\s*$/iu;
3095
+ var CODE_FENCE = /^ {0,3}(?<marker>`{3,}|~{3,})(?<suffix>.*)$/u;
3096
+ var LINE_BREAK = /\r?\n/u;
2943
3097
  var advanceFence = (openingFence, marker, suffix) => {
2944
3098
  if (!openingFence) {
2945
3099
  if (marker[0] === "`" && suffix.includes("`")) {
@@ -2975,7 +3129,7 @@ var parseAutomaticDecision = (text) => {
2975
3129
  ok: false
2976
3130
  };
2977
3131
  }
2978
- const decision = match[1].toLowerCase();
3132
+ const decision = namedGroups(match).decision.toLowerCase();
2979
3133
  let openingFence;
2980
3134
  const decisions = [];
2981
3135
  let pendingFencedDecisions = [];
@@ -2983,7 +3137,8 @@ var parseAutomaticDecision = (text) => {
2983
3137
  const trimmed = line.trim();
2984
3138
  const fence = CODE_FENCE.exec(line);
2985
3139
  if (fence) {
2986
- const { closed, openingFence: nextOpeningFence } = advanceFence(openingFence, fence[1], fence[2]);
3140
+ const groups = namedGroups(fence);
3141
+ const { closed, openingFence: nextOpeningFence } = advanceFence(openingFence, groups.marker, groups.suffix);
2987
3142
  openingFence = nextOpeningFence;
2988
3143
  if (closed) {
2989
3144
  pendingFencedDecisions = [];
@@ -2994,7 +3149,7 @@ var parseAutomaticDecision = (text) => {
2994
3149
  if (!subsequent) {
2995
3150
  continue;
2996
3151
  }
2997
- const repeated = subsequent[1].trim().toLowerCase();
3152
+ const repeated = namedGroups(subsequent).decision.trim().toLowerCase();
2998
3153
  if (openingFence) {
2999
3154
  pendingFencedDecisions.push(repeated);
3000
3155
  } else {
@@ -3004,16 +3159,14 @@ var parseAutomaticDecision = (text) => {
3004
3159
  if (openingFence) {
3005
3160
  decisions.push(...pendingFencedDecisions);
3006
3161
  }
3007
- for (const repeated of decisions) {
3008
- if (repeated === decision) {
3009
- return {
3010
- category: "duplicate-decision",
3011
- markdown: text,
3012
- message: "Advisor gate response contains duplicate decision lines.",
3013
- ok: false
3014
- };
3015
- }
3016
- return {
3162
+ const [repeated] = decisions;
3163
+ if (repeated !== undefined) {
3164
+ return repeated === decision ? {
3165
+ category: "duplicate-decision",
3166
+ markdown: text,
3167
+ message: "Advisor gate response contains duplicate decision lines.",
3168
+ ok: false
3169
+ } : {
3017
3170
  category: "contradictory-decision",
3018
3171
  markdown: text,
3019
3172
  message: "Advisor gate response contains contradictory decision lines.",
@@ -3033,6 +3186,35 @@ var adviceForGateText = (result) => `**Decision: ${result.decision}**
3033
3186
 
3034
3187
  ${result.markdown}`;
3035
3188
 
3189
+ // src/tools/model-access.ts
3190
+ var currentModelRef = (ctx) => {
3191
+ const { model } = ctx;
3192
+ return model ? `${model.provider}/${model.id}` : undefined;
3193
+ };
3194
+ var advisorModelAccess = (ctx) => {
3195
+ const modelRef = currentModelRef(ctx);
3196
+ if (advisorModelWhitelistRef.length === 0) {
3197
+ return modelRef ? { allowed: true, modelRef } : { allowed: true };
3198
+ }
3199
+ if (modelRef && advisorModelWhitelistRef.includes(modelRef)) {
3200
+ return { allowed: true, modelRef };
3201
+ }
3202
+ const current = modelRef ?? "no current model";
3203
+ const denial = {
3204
+ allowed: false,
3205
+ reason: `Advisor calls are restricted to the configured model whitelist (${advisorModelWhitelistRef.join(", ")}). Current model: ${current}.`
3206
+ };
3207
+ if (modelRef) {
3208
+ denial.modelRef = modelRef;
3209
+ }
3210
+ return denial;
3211
+ };
3212
+ var advisorModelIsAllowed = (ctx) => advisorModelAccess(ctx).allowed;
3213
+ var advisorModelAccessReason = (ctx) => {
3214
+ const access = advisorModelAccess(ctx);
3215
+ return access.allowed ? undefined : access.reason;
3216
+ };
3217
+
3036
3218
  // src/tools/consultation.ts
3037
3219
  class AdvisorNoAdviceError extends Error {
3038
3220
  constructor() {
@@ -3046,6 +3228,10 @@ ${item.text}
3046
3228
  var collectAdvisorResponse = async (options) => {
3047
3229
  const { ctx, question, signal, systemPrompt } = options;
3048
3230
  loadConfig(ctx);
3231
+ const accessReason = advisorModelAccessReason(ctx);
3232
+ if (accessReason) {
3233
+ throw new Error(accessReason);
3234
+ }
3049
3235
  const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
3050
3236
  const context = await assembleConsultationContext(options);
3051
3237
  const outboundQuestion = advisorRedactSecretsRef && question !== undefined ? redactSecrets(question) : question;
@@ -3072,17 +3258,20 @@ var collectAdvisorResponse = async (options) => {
3072
3258
  if (!markdown.trim()) {
3073
3259
  throw new AdvisorNoAdviceError;
3074
3260
  }
3075
- return {
3076
- draftBytes: context.draftText ? Buffer.byteLength(context.draftText, "utf8") : undefined,
3261
+ const response = {
3262
+ draftBytes: context.draftText ? Buffer.byteLength(context.draftText, "utf-8") : undefined,
3077
3263
  markdown,
3078
3264
  model: advisorRef,
3079
3265
  preferenceBytes: context.preferences?.bytes,
3080
3266
  thinkingText: streamed.thinking,
3081
3267
  trackedBytes: context.tracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
3082
3268
  untrackedBytes: context.untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
3083
- usage: streamed.usage,
3084
- ...context.scout ? { scout: context.scout } : {}
3269
+ usage: streamed.usage
3085
3270
  };
3271
+ if (context.scout) {
3272
+ response.scout = context.scout;
3273
+ }
3274
+ return response;
3086
3275
  };
3087
3276
  var consultAdvisor = async (ctx, question, signal, onChunk, trigger = "executor-requested", gitContext, draft, includeUntracked, includeTracked, onScout, currentInvocationId) => {
3088
3277
  const result = await collectAdvisorResponse({
@@ -3164,8 +3353,8 @@ var settingsIdentity = (path) => {
3164
3353
  };
3165
3354
  var readHideThinking = (path) => {
3166
3355
  try {
3167
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
3168
- return typeof parsed === "object" && parsed !== null && parsed.hideThinkingBlock === true;
3356
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
3357
+ return isRecord(parsed) && parsed.hideThinkingBlock === true;
3169
3358
  } catch {
3170
3359
  return false;
3171
3360
  }
@@ -3201,6 +3390,9 @@ var SPINNER_FRAMES = [
3201
3390
  ];
3202
3391
  var THINKING_PREFIX = " ";
3203
3392
  var THINKING_PREFIX_WIDTH = visibleWidth(THINKING_PREFIX);
3393
+ var noop = () => {
3394
+ return;
3395
+ };
3204
3396
 
3205
3397
  class ThinkingMarkdown {
3206
3398
  markdown;
@@ -3222,18 +3414,14 @@ class ThinkingMarkdown {
3222
3414
  this.markdown.invalidate();
3223
3415
  }
3224
3416
  }
3225
-
3226
- class HiddenThinkingLabel {
3227
- label;
3228
- constructor(theme) {
3229
- this.label = theme.fg("thinkingText", `${THINKING_PREFIX}Thinking…`);
3230
- }
3231
- render() {
3232
- return [this.label];
3233
- }
3234
- invalidate() {}
3235
- }
3236
- var renderThinkingMarkdown = (thinking, theme) => piHideThinkingEnabled() ? new HiddenThinkingLabel(theme) : new ThinkingMarkdown(thinking, theme);
3417
+ var hiddenThinkingLabel = (theme) => {
3418
+ const label = theme.fg("thinkingText", `${THINKING_PREFIX}Thinking…`);
3419
+ return {
3420
+ invalidate: noop,
3421
+ render: () => [label]
3422
+ };
3423
+ };
3424
+ var renderThinkingMarkdown = (thinking, theme) => piHideThinkingEnabled() ? hiddenThinkingLabel(theme) : new ThinkingMarkdown(thinking, theme);
3237
3425
  var resolveAdvisorRequest = (question) => question?.trim() || undefined;
3238
3426
  var renderAdvisorCallBox = (question, theme) => {
3239
3427
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
@@ -3244,7 +3432,7 @@ ${theme.fg("dim", ` ${question}`)}` : `${label} ${title}`, 0, 0));
3244
3432
  return box;
3245
3433
  };
3246
3434
  var COLLAPSED_ADVICE_LINES = 12;
3247
- var SOUND_VERDICT = /^Verdict:\s*sound$/;
3435
+ var SOUND_VERDICT = /^Verdict:\s*sound$/u;
3248
3436
  var hasSoundVerdict = (advice) => SOUND_VERDICT.test((advice.split(`
3249
3437
  `).find((line) => line.trim()) ?? "").trim());
3250
3438
  var renderAdvisorResponseHeader = (sound, theme) => sound ? theme.fg("accent", theme.bold("◆ ADVISOR · SOUND")) : theme.fg("warning", theme.bold("◆ ADVISOR RESPONSE"));
@@ -3372,13 +3560,27 @@ var scoutTitle = (scout, frame) => {
3372
3560
  }
3373
3561
  return "◆ SCOUT · FALLBACK";
3374
3562
  };
3563
+ var scoutSummaryLine = (scout) => ` ${scout.model}${scout.selectedCount === undefined ? "" : ` · ${scout.selectedCount} kept / ${Math.max(0, (scout.availableCount ?? 0) - scout.selectedCount)} omitted`}${scout.latencyMs === undefined ? "" : ` · ${(scout.latencyMs / 1000).toFixed(1)}s`}`;
3564
+ var scoutExpandedLines = (scout, expanded, theme) => {
3565
+ const lines = [];
3566
+ if (expanded && scout.selectedLabels?.length) {
3567
+ lines.push(theme.fg("dim", ` Selected: ${scout.selectedLabels.join("; ")}`));
3568
+ }
3569
+ if (expanded && scout.synthesis) {
3570
+ lines.push(theme.fg("dim", ` Scout synthesis (untrusted inference): ${scout.synthesis}`));
3571
+ }
3572
+ if (expanded && scout.omittedBeforeScout) {
3573
+ lines.push(theme.fg("dim", ` ${scout.omittedBeforeScout} group(s) omitted before Scout`));
3574
+ }
3575
+ return lines;
3576
+ };
3375
3577
  var renderScoutDetails = (box, scout, expanded, theme) => {
3376
3578
  const active = scout.status === "calling" || scout.status === "streaming";
3377
3579
  const frame = SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length];
3378
3580
  const title = scoutTitle(scout, frame);
3379
3581
  const lines = [
3380
3582
  theme.fg(scout.status === "fallback" || scout.status === "cancelled" ? "warning" : "accent", theme.bold(title)),
3381
- theme.fg("dim", ` ${scout.model}${scout.selectedCount === undefined ? "" : ` · ${scout.selectedCount} kept / ${Math.max(0, (scout.availableCount ?? 0) - scout.selectedCount)} omitted`}${scout.latencyMs === undefined ? "" : ` · ${(scout.latencyMs / 1000).toFixed(1)}s`}`)
3583
+ theme.fg("dim", scoutSummaryLine(scout))
3382
3584
  ];
3383
3585
  if (getAdvisorSettings().showUsageDetails) {
3384
3586
  const usage = formatAdvisorUsage(scout.usage);
@@ -3395,16 +3597,7 @@ var renderScoutDetails = (box, scout, expanded, theme) => {
3395
3597
  if (thinking.trim()) {
3396
3598
  box.addChild(renderThinkingMarkdown(thinking, theme));
3397
3599
  }
3398
- const expandedLines = [];
3399
- if (expanded && scout.selectedLabels?.length) {
3400
- expandedLines.push(theme.fg("dim", ` Selected: ${scout.selectedLabels.join("; ")}`));
3401
- }
3402
- if (expanded && scout.synthesis) {
3403
- expandedLines.push(theme.fg("dim", ` Scout synthesis (untrusted inference): ${scout.synthesis}`));
3404
- }
3405
- if (expanded && scout.omittedBeforeScout) {
3406
- expandedLines.push(theme.fg("dim", ` ${scout.omittedBeforeScout} group(s) omitted before Scout`));
3407
- }
3600
+ const expandedLines = scoutExpandedLines(scout, expanded, theme);
3408
3601
  if (expandedLines.length > 0) {
3409
3602
  box.addChild(new Text2(expandedLines.join(`
3410
3603
  `), 0, 0));
@@ -3434,6 +3627,31 @@ var addJevUsage = (totals, usage) => {
3434
3627
  totals.inputTokens += usage.inputTokens;
3435
3628
  totals.outputTokens += usage.outputTokens;
3436
3629
  };
3630
+ var savingsLine = (costs, skipped) => {
3631
+ if (costs.length === 0) {
3632
+ return "Estimated saving from skips: unavailable — no observed consultation cost this session";
3633
+ }
3634
+ const mean = costs.reduce((sum, cost) => sum + cost, 0) / costs.length;
3635
+ return `Estimated saving from skips: ≤ $${(mean * skipped).toFixed(4)} — upper bound; assumes each skipped consultation would have cost this session's mean allowed-consultation cost ($${mean.toFixed(4)}), which the skipped calls would likely have undercut`;
3636
+ };
3637
+ var formatJevTokens = (usage) => `↑${formatTokenCount(usage.inputTokens + usage.outputTokens)}`;
3638
+ var markdownCosts = (invocations) => invocations.filter((item) => item.kind === "markdown" && typeof item.cost === "number").map((item) => item.cost);
3639
+ var filterLine = (filter) => {
3640
+ const head = `${filter.screened} screened (${filter.allowed} allowed, ${filter.skipped} skipped${filter.repeatSkipped > 0 ? ` [${filter.repeatSkipped} repeat]` : ""})`;
3641
+ const parts = [head];
3642
+ if (filter.overrides > 0) {
3643
+ parts.push(`${filter.overrides} override${filter.overrides === 1 ? "" : "s"}`);
3644
+ }
3645
+ if (filter.failures > 0) {
3646
+ parts.push(`${filter.failures} failure${filter.failures === 1 ? "" : "s"}`);
3647
+ }
3648
+ return `Jev filter: ${parts.join(", ")}`;
3649
+ };
3650
+ var gateLine = (gate, invocations) => {
3651
+ const consultationCosts = invocations.filter((item) => item.trigger === "turn-gate" && typeof item.cost === "number").map((item) => item.cost);
3652
+ const gateSpend = consultationCosts.reduce((sum, cost) => sum + cost, 0);
3653
+ return `Turn gate: ${gate.checks} check${gate.checks === 1 ? "" : "s"} (Jev ${formatJevTokens(gate.usage)} · $${gate.usage.cost.toFixed(4)}), ${gate.consultations} consultation${gate.consultations === 1 ? "" : "s"} ($${gateSpend.toFixed(4)})`;
3654
+ };
3437
3655
 
3438
3656
  class AdvisorJevLedgerState {
3439
3657
  #ledger = freshJevLedger();
@@ -3455,10 +3673,11 @@ class AdvisorJevLedgerState {
3455
3673
  if (repeat) {
3456
3674
  this.#ledger.filter.repeatSkipped += 1;
3457
3675
  }
3458
- this.#lastSkip = {
3459
- ...normalizedQuestion ? { normalizedQuestion } : {},
3460
- turn
3461
- };
3676
+ const skip = { turn };
3677
+ if (normalizedQuestion) {
3678
+ skip.normalizedQuestion = normalizedQuestion;
3679
+ }
3680
+ this.#lastSkip = skip;
3462
3681
  }
3463
3682
  recordFilterOverride() {
3464
3683
  this.#ledger.filter.overrides += 1;
@@ -3492,20 +3711,19 @@ class AdvisorJevLedgerState {
3492
3711
  if (filter.overrides > 0) {
3493
3712
  parts.push(`${filter.overrides} override${filter.overrides === 1 ? "" : "s"}`);
3494
3713
  }
3495
- lines.push(`Consultation dedup: ${parts.join(", ")}`);
3496
- lines.push(this.#savingsLine(this.#markdownCosts(invocations), filter.skipped));
3714
+ lines.push(`Consultation dedup: ${parts.join(", ")}`, savingsLine(markdownCosts(invocations), filter.skipped));
3497
3715
  } else if (this.#filterActive()) {
3498
- lines.push(this.#filterLine(filter));
3716
+ lines.push(filterLine(filter));
3499
3717
  const jevTokens = usage.inputTokens + usage.outputTokens;
3500
3718
  if (jevTokens > 0) {
3501
- lines.push(`Jev cost: ${this.#formatJevTokens(usage)} tokens · $${usage.cost.toFixed(4)} (input only; output free)`);
3719
+ lines.push(`Jev cost: ${formatJevTokens(usage)} tokens · $${usage.cost.toFixed(4)} (input only; output free)`);
3502
3720
  }
3503
3721
  if (filter.skipped > 0) {
3504
- lines.push(this.#savingsLine(this.#markdownCosts(invocations), filter.skipped));
3722
+ lines.push(savingsLine(markdownCosts(invocations), filter.skipped));
3505
3723
  }
3506
3724
  }
3507
3725
  if (gate.checks > 0 || gate.consultations > 0) {
3508
- lines.push(this.#gateLine(gate, invocations));
3726
+ lines.push(gateLine(gate, invocations));
3509
3727
  }
3510
3728
  return lines;
3511
3729
  }
@@ -3513,39 +3731,10 @@ class AdvisorJevLedgerState {
3513
3731
  const { filter } = this.#ledger;
3514
3732
  return filter.screened > 0 || filter.overrides > 0 || filter.failures > 0;
3515
3733
  }
3516
- #savingsLine(markdownCosts, skipped) {
3517
- if (markdownCosts.length === 0) {
3518
- return "Estimated saving from skips: unavailable — no observed consultation cost this session";
3519
- }
3520
- const mean = markdownCosts.reduce((sum, cost) => sum + cost, 0) / markdownCosts.length;
3521
- return `Estimated saving from skips: ≤ $${(mean * skipped).toFixed(4)} — upper bound; assumes each skipped consultation would have cost this session's mean allowed-consultation cost ($${mean.toFixed(4)}), which the skipped calls would likely have undercut`;
3522
- }
3523
- #gateLine(gate, invocations) {
3524
- const consultationCosts = invocations.filter((item) => item.trigger === "turn-gate" && typeof item.cost === "number").map((item) => item.cost);
3525
- const gateSpend = consultationCosts.reduce((sum, cost) => sum + cost, 0);
3526
- return `Turn gate: ${gate.checks} check${gate.checks === 1 ? "" : "s"} (Jev ${this.#formatJevTokens(gate.usage)} · $${gate.usage.cost.toFixed(4)}), ${gate.consultations} consultation${gate.consultations === 1 ? "" : "s"} ($${gateSpend.toFixed(4)})`;
3527
- }
3528
- #formatJevTokens(usage) {
3529
- return `↑${formatTokenCount(usage.inputTokens + usage.outputTokens)}`;
3530
- }
3531
- #markdownCosts(invocations) {
3532
- return invocations.filter((item) => item.kind === "markdown" && typeof item.cost === "number").map((item) => item.cost);
3533
- }
3534
- #filterLine(filter) {
3535
- const head = `${filter.screened} screened (${filter.allowed} allowed, ${filter.skipped} skipped${filter.repeatSkipped > 0 ? ` [${filter.repeatSkipped} repeat]` : ""})`;
3536
- const parts = [head];
3537
- if (filter.overrides > 0) {
3538
- parts.push(`${filter.overrides} override${filter.overrides === 1 ? "" : "s"}`);
3539
- }
3540
- if (filter.failures > 0) {
3541
- parts.push(`${filter.failures} failure${filter.failures === 1 ? "" : "s"}`);
3542
- }
3543
- return `Jev filter: ${parts.join(", ")}`;
3544
- }
3545
3734
  }
3546
3735
 
3547
3736
  // src/session-state.ts
3548
- var WHITESPACE = /\s/;
3737
+ var WHITESPACE = /\s/u;
3549
3738
  var TIMESTAMP_KEYS = new Set([
3550
3739
  "createdat",
3551
3740
  "date",
@@ -3555,7 +3744,7 @@ var TIMESTAMP_KEYS = new Set([
3555
3744
  "updatedat"
3556
3745
  ]);
3557
3746
  var REQUEST_ID_KEYS = new Set(["correlationid", "requestid", "traceid"]);
3558
- var normalizedKey = (key) => key.replace(/[-_]/g, "").toLowerCase();
3747
+ var normalizedKey = (key) => key.replaceAll(/[-_]/gu, "").toLowerCase();
3559
3748
  var isVolatileKey = (key, keys) => keys.has(normalizedKey(key));
3560
3749
  var normalizeShellWhitespace = (command) => {
3561
3750
  let result = "";
@@ -3588,10 +3777,10 @@ var normalizeShellWhitespace = (command) => {
3588
3777
  }
3589
3778
  return result;
3590
3779
  };
3591
- var normalizeString = (value) => value.replace(/\/(?:private\/)?tmp\/[^\s/]+/g, "/tmp/<temporary>").replace(/\/var\/folders\/[^\s/]+/g, "/var/folders/<temporary>");
3780
+ var normalizeString = (value) => value.replaceAll(/\/(?:private\/)?tmp\/[^\s/]+/gu, "/tmp/<temporary>").replaceAll(/\/var\/folders\/[^\s/]+/gu, "/var/folders/<temporary>");
3592
3781
  var normalizeToolInput = (toolName, input) => {
3593
3782
  const visit = (value, key) => {
3594
- if (typeof value === "string") {
3783
+ if (isString(value)) {
3595
3784
  if (key && isVolatileKey(key, TIMESTAMP_KEYS)) {
3596
3785
  return "<timestamp>";
3597
3786
  }
@@ -3604,9 +3793,8 @@ var normalizeToolInput = (toolName, input) => {
3604
3793
  if (Array.isArray(value)) {
3605
3794
  return value.map((item) => visit(item));
3606
3795
  }
3607
- if (value && typeof value === "object") {
3608
- const record = value;
3609
- return Object.fromEntries(Object.keys(record).sort().map((childKey) => [childKey, visit(record[childKey], childKey)]));
3796
+ if (isRecordOf(value)) {
3797
+ return Object.fromEntries(Object.keys(value).toSorted().map((childKey) => [childKey, visit(value[childKey], childKey)]));
3610
3798
  }
3611
3799
  return value;
3612
3800
  };
@@ -3713,11 +3901,11 @@ class AdvisorSessionState {
3713
3901
  addAdvisorUsage(this.#usage.totals, record.usage);
3714
3902
  }
3715
3903
  issueAdvice(id, advice, trigger, draft = false, normalizedQuestion) {
3716
- this.#ledger.issued.set(id, {
3717
- advice,
3718
- ...normalizedQuestion ? { normalizedQuestion } : {},
3719
- trigger
3720
- });
3904
+ const issued = { advice, trigger };
3905
+ if (normalizedQuestion) {
3906
+ issued.normalizedQuestion = normalizedQuestion;
3907
+ }
3908
+ this.#ledger.issued.set(id, issued);
3721
3909
  this.#ledger.lastAdvice = advice;
3722
3910
  if (draft) {
3723
3911
  this.#ledger.draftConsultations += 1;
@@ -3740,9 +3928,9 @@ class AdvisorSessionState {
3740
3928
  return false;
3741
3929
  }
3742
3930
  const mentioned = paths.every((path) => {
3743
- const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3744
- const boundary = "(^|[\\s\\\"'`()\\[])" + escaped + "(?=$|[\\s\\\"'`),;:!?\\]]|\\.(?=\\s|$))";
3745
- return new RegExp(boundary).test(advice);
3931
+ const escaped = path.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
3932
+ const boundary = `(^|[\\s"'\`()\\[])${escaped}(?=$|[\\s"'\`),;:!?\\]]|\\.(?=\\s|$))`;
3933
+ return new RegExp(boundary, "u").test(advice);
3746
3934
  });
3747
3935
  if (!mentioned) {
3748
3936
  return false;
@@ -3867,6 +4055,16 @@ var notify = (ctx, message, level) => {
3867
4055
  ctx.ui.notify(message, level);
3868
4056
  }
3869
4057
  };
4058
+ var reportManualBudgetExhausted = (ctx) => {
4059
+ const message = "Advisor call budget exhausted for this session.";
4060
+ notify(ctx, message, "warning");
4061
+ notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
4062
+ };
4063
+ var requestManualRender = (ctx) => {
4064
+ if (ctx.hasUI) {
4065
+ ctx.ui.setStatus("advisor-manual", undefined);
4066
+ }
4067
+ };
3870
4068
 
3871
4069
  class CommandRuntime {
3872
4070
  advisorSessionState;
@@ -3874,7 +4072,9 @@ class CommandRuntime {
3874
4072
  manualProgress = new Map;
3875
4073
  manualProgressTimers = new Map;
3876
4074
  pi;
4075
+ reportManualBudgetExhausted = reportManualBudgetExhausted;
3877
4076
  requestAdvisor;
4077
+ requestManualRender = requestManualRender;
3878
4078
  scoutStatus;
3879
4079
  manualProgressSequence = 0;
3880
4080
  pendingExecutorModelRef;
@@ -3883,7 +4083,7 @@ class CommandRuntime {
3883
4083
  this.pi = pi;
3884
4084
  this.advisorSessionState = dependencies.sessionState ?? advisorSessionState;
3885
4085
  this.scoutStatus = dependencies.statusManager ?? new ScoutStatusManager(false);
3886
- this.requestAdvisor = dependencies.consult ?? ((ctx, question, signal, onChunk, onScout, gitContext) => consultAdvisor(ctx, question, signal, onChunk, "manual", gitContext, undefined, undefined, undefined, onScout, undefined));
4086
+ this.requestAdvisor = dependencies.consult ?? ((ctx, question, signal, onChunk, onScout, gitContext) => consultAdvisor(ctx, question, signal, onChunk, "manual", gitContext, undefined, undefined, undefined, onScout));
3887
4087
  }
3888
4088
  flowEnabled() {
3889
4089
  return this.pi.getActiveTools().includes("ask_advisor");
@@ -3905,16 +4105,6 @@ class CommandRuntime {
3905
4105
  ctx.ui.setStatus("advisor-usage", getAdvisorSettings().showUsageFooter ? this.advisorSessionState.usageStatus() : undefined);
3906
4106
  }
3907
4107
  }
3908
- reportManualBudgetExhausted(ctx) {
3909
- const message = "Advisor call budget exhausted for this session.";
3910
- notify(ctx, message, "warning");
3911
- notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
3912
- }
3913
- requestManualRender(ctx) {
3914
- if (ctx.hasUI) {
3915
- ctx.ui.setStatus("advisor-manual", undefined);
3916
- }
3917
- }
3918
4108
  }
3919
4109
  var createCommandRuntime = (pi, dependencies = {}) => new CommandRuntime(pi, dependencies);
3920
4110
 
@@ -4102,11 +4292,7 @@ var registerCommandLifecycle = (runtime, activateAdvisor) => {
4102
4292
  };
4103
4293
 
4104
4294
  // src/ui/manual-dialog.ts
4105
- import {
4106
- Editor,
4107
- Key,
4108
- matchesKey
4109
- } from "@earendil-works/pi-tui";
4295
+ import { Editor, Key, matchesKey } from "@earendil-works/pi-tui";
4110
4296
 
4111
4297
  // src/ui/manual-dialog-render.ts
4112
4298
  import {
@@ -4202,6 +4388,7 @@ var renderManualAdvisorDialog = (view) => {
4202
4388
 
4203
4389
  // src/ui/manual-dialog.ts
4204
4390
  var TUI_INPUT_TAB = ["tui", "input", "tab"].join(".");
4391
+ var isShiftTab = (keyData) => matchesKey(keyData, Key.shift("tab"));
4205
4392
 
4206
4393
  class ManualAdvisorDialog {
4207
4394
  options;
@@ -4224,7 +4411,7 @@ class ManualAdvisorDialog {
4224
4411
  }
4225
4412
  constructor(options) {
4226
4413
  this.options = options;
4227
- this.gitLevels = GIT_CONTEXT_LEVELS.filter((level) => clampGitContextLevel(level, options.gitContext) === level).reverse();
4414
+ this.gitLevels = GIT_CONTEXT_LEVELS.filter((level) => clampGitContextLevel(level, options.gitContext) === level).toReversed();
4228
4415
  this.gitIndex = Math.max(0, this.gitLevels.indexOf(options.gitContext));
4229
4416
  const editorTheme = {
4230
4417
  borderColor: (text) => options.theme.fg("border", text),
@@ -4256,7 +4443,7 @@ class ManualAdvisorDialog {
4256
4443
  this.cancel();
4257
4444
  return;
4258
4445
  }
4259
- if (this.isShiftTab(keyData)) {
4446
+ if (isShiftTab(keyData)) {
4260
4447
  this.changeFocus(-1);
4261
4448
  return;
4262
4449
  }
@@ -4264,19 +4451,12 @@ class ManualAdvisorDialog {
4264
4451
  this.changeFocus(1);
4265
4452
  return;
4266
4453
  }
4267
- switch (this.focusTarget) {
4268
- case "editor":
4269
- this.handleEditorInput(keyData);
4270
- return;
4271
- case "git":
4272
- this.handleGitInput(keyData);
4273
- return;
4274
- case "actions":
4275
- this.handleActionInput(keyData);
4276
- return;
4277
- default:
4278
- return;
4279
- }
4454
+ const handlers = {
4455
+ actions: (key) => this.handleActionInput(key),
4456
+ editor: (key) => this.handleEditorInput(key),
4457
+ git: (key) => this.handleGitInput(key)
4458
+ };
4459
+ handlers[this.focusTarget](keyData);
4280
4460
  }
4281
4461
  render(width) {
4282
4462
  const renderWidth = Math.max(1, Math.floor(width));
@@ -4384,9 +4564,6 @@ class ManualAdvisorDialog {
4384
4564
  isTab(keyData) {
4385
4565
  return this.matches(keyData, TUI_INPUT_TAB, Key.tab) && !matchesKey(keyData, Key.shift("tab"));
4386
4566
  }
4387
- isShiftTab(keyData) {
4388
- return matchesKey(keyData, Key.shift("tab"));
4389
- }
4390
4567
  isCancel(keyData) {
4391
4568
  return matchesKey(keyData, Key.escape) || this.options.keybindings.matches(keyData, "tui.select.cancel");
4392
4569
  }
@@ -4398,10 +4575,13 @@ class ManualAdvisorDialog {
4398
4575
  return;
4399
4576
  }
4400
4577
  this.state.completed = true;
4401
- this.options.onSubmit({
4402
- gitContext: this.gitLevels[this.gitIndex] ?? "off",
4403
- ...message ? { message } : {}
4404
- });
4578
+ const request = {
4579
+ gitContext: this.gitLevels[this.gitIndex] ?? "off"
4580
+ };
4581
+ if (message) {
4582
+ request.message = message;
4583
+ }
4584
+ this.options.onSubmit(request);
4405
4585
  }
4406
4586
  cancel() {
4407
4587
  if (this.state.completed) {
@@ -4412,31 +4592,31 @@ class ManualAdvisorDialog {
4412
4592
  }
4413
4593
  }
4414
4594
 
4415
- // src/jev/client.ts
4416
- class JevFailure extends Error {
4595
+ // src/jev/failure.ts
4596
+ class JevFailureError extends Error {
4417
4597
  category;
4418
4598
  constructor(category, message) {
4419
4599
  super(message);
4420
- this.name = "JevFailure";
4600
+ this.name = "JevFailureError";
4421
4601
  this.category = category;
4422
4602
  }
4423
4603
  }
4604
+
4605
+ // src/jev/client.ts
4424
4606
  var ENDPOINTS = {
4425
4607
  openrouter: "https://openrouter.ai/api/alpha/decisions",
4426
4608
  typesafe: "https://api.typesafe.ai/v1/systemone"
4427
4609
  };
4610
+ var range = (from, to) => Array.from({ length: to - from + 1 }, (_, index) => from + index);
4428
4611
  var RETRYABLE_STATUSES = new Set([408, 429, ...range(500, 599)]);
4429
4612
  var RETRY_BACKOFF_MS = 250;
4430
4613
  var MAX_ATTEMPTS = 2;
4431
- function range(from, to) {
4432
- return Array.from({ length: to - from + 1 }, (_, index) => from + index);
4433
- }
4434
- var finiteTokens = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
4614
+ var finiteTokens = (value) => isNumber(value) && Number.isFinite(value) && value >= 0 ? value : 0;
4435
4615
  var errorDetail = (error) => {
4436
- if (typeof error === "string") {
4616
+ if (isString(error)) {
4437
4617
  return error;
4438
4618
  }
4439
- if (error && typeof error === "object" && "message" in error) {
4619
+ if (isRecordOf(error) && "message" in error) {
4440
4620
  return String(error.message);
4441
4621
  }
4442
4622
  return "";
@@ -4450,7 +4630,22 @@ var statusCategory = (status) => {
4450
4630
  }
4451
4631
  return "error";
4452
4632
  };
4633
+ var isObjectLike = (value) => typeof value === "object";
4453
4634
  var openRouterModelId = (model) => model.includes("/") ? model : `~typesafe/${model}`;
4635
+ var connectionFailure = (error) => {
4636
+ const message = redactSecrets(error instanceof Error ? error.message : String(error));
4637
+ return {
4638
+ failure: new JevFailureError("network", `Jev connection failed: ${message}`)
4639
+ };
4640
+ };
4641
+ var sleepWithAbort = (signal, ms) => new Promise((resolve) => {
4642
+ const timer = setTimeout(resolve, ms);
4643
+ timer.unref?.();
4644
+ signal.addEventListener("abort", () => {
4645
+ clearTimeout(timer);
4646
+ resolve();
4647
+ }, { once: true });
4648
+ });
4454
4649
 
4455
4650
  class JevClient {
4456
4651
  #apiKey;
@@ -4499,7 +4694,7 @@ class JevClient {
4499
4694
  if (!outcome.retryable || attempt >= MAX_ATTEMPTS) {
4500
4695
  break;
4501
4696
  }
4502
- await this.#backoff(deadline.signal);
4697
+ await sleepWithAbort(deadline.signal, RETRY_BACKOFF_MS);
4503
4698
  if (deadline.signal.aborted) {
4504
4699
  break;
4505
4700
  }
@@ -4509,11 +4704,11 @@ class JevClient {
4509
4704
  if (signal?.aborted && !deadlineHit) {
4510
4705
  throw error;
4511
4706
  }
4512
- if (error instanceof JevFailure) {
4707
+ if (error instanceof JevFailureError) {
4513
4708
  throw error;
4514
4709
  }
4515
4710
  const message = redactSecrets(error instanceof Error ? error.message : String(error));
4516
- throw deadlineHit ? new JevFailure("timeout", `Jev call exceeded its ${this.#timeoutMs} ms wall-time budget.`) : new JevFailure("error", message);
4711
+ throw deadlineHit ? new JevFailureError("timeout", `Jev call exceeded its ${this.#timeoutMs} ms wall-time budget.`) : new JevFailureError("error", message);
4517
4712
  } finally {
4518
4713
  clearTimeout(timer);
4519
4714
  signal?.removeEventListener("abort", abortFromCaller);
@@ -4524,7 +4719,7 @@ class JevClient {
4524
4719
  return this.#result(outcome);
4525
4720
  }
4526
4721
  if (deadlineHit && outcome.failure?.category !== "auth") {
4527
- throw new JevFailure("timeout", `Jev call exceeded its ${this.#timeoutMs} ms wall-time budget.`);
4722
+ throw new JevFailureError("timeout", `Jev call exceeded its ${this.#timeoutMs} ms wall-time budget.`);
4528
4723
  }
4529
4724
  if (outcome.failure) {
4530
4725
  throw outcome.failure;
@@ -4532,21 +4727,7 @@ class JevClient {
4532
4727
  if (signal?.aborted) {
4533
4728
  throw new Error("Jev call aborted by the caller.");
4534
4729
  }
4535
- throw new JevFailure("error", "Jev call failed.");
4536
- }
4537
- async#backoff(signal) {
4538
- if (signal.aborted) {
4539
- return;
4540
- }
4541
- await new Promise((resolve) => {
4542
- const timer = setTimeout(resolve, RETRY_BACKOFF_MS);
4543
- timer.unref?.();
4544
- const onAbort = () => {
4545
- clearTimeout(timer);
4546
- resolve();
4547
- };
4548
- signal.addEventListener("abort", onAbort, { once: true });
4549
- });
4730
+ throw new JevFailureError("error", "Jev call failed.");
4550
4731
  }
4551
4732
  async#attempt(body, signal) {
4552
4733
  if (signal.aborted) {
@@ -4569,7 +4750,7 @@ class JevClient {
4569
4750
  }
4570
4751
  return {
4571
4752
  retryable: true,
4572
- ...this.#connectionFailure(error)
4753
+ ...connectionFailure(error)
4573
4754
  };
4574
4755
  }
4575
4756
  if (response.ok) {
@@ -4581,23 +4762,17 @@ class JevClient {
4581
4762
  retryable: RETRYABLE_STATUSES.has(response.status)
4582
4763
  };
4583
4764
  }
4584
- #connectionFailure(error) {
4585
- const message = redactSecrets(error instanceof Error ? error.message : String(error));
4586
- return {
4587
- failure: new JevFailure("network", `Jev connection failed: ${message}`)
4588
- };
4589
- }
4590
4765
  async#failureFromStatus(response) {
4591
4766
  let detail = "";
4592
4767
  try {
4593
4768
  const parsed = await response.json();
4594
- const error = parsed?.error;
4769
+ const error = isRecord(parsed) ? parsed.error : undefined;
4595
4770
  detail = errorDetail(error);
4596
4771
  } catch {
4597
4772
  detail = "";
4598
4773
  }
4599
4774
  const message = redactSecrets(`Jev ${this.#transportLabel()} request failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}.`);
4600
- return new JevFailure(statusCategory(response.status), message);
4775
+ return new JevFailureError(statusCategory(response.status), message);
4601
4776
  }
4602
4777
  #transportLabel() {
4603
4778
  return this.#endpoint === ENDPOINTS.openrouter ? "OpenRouter" : "TypeSafe";
@@ -4608,23 +4783,25 @@ class JevClient {
4608
4783
  parsed = await response.json();
4609
4784
  } catch (error) {
4610
4785
  return {
4611
- failure: new JevFailure("malformed", `Jev response was not JSON: ${redactSecrets(error instanceof Error ? error.message : String(error))}`)
4786
+ failure: new JevFailureError("malformed", `Jev response was not JSON: ${redactSecrets(error instanceof Error ? error.message : String(error))}`)
4612
4787
  };
4613
4788
  }
4614
- const record = parsed;
4615
- if (!record || typeof record !== "object" || !record.answers || typeof record.answers !== "object") {
4789
+ if (!isRecord(parsed) || !parsed.answers || !isObjectLike(parsed.answers)) {
4616
4790
  return {
4617
- failure: new JevFailure("malformed", "Jev response did not include an answers object.")
4791
+ failure: new JevFailureError("malformed", "Jev response did not include an answers object.")
4618
4792
  };
4619
4793
  }
4620
4794
  return {
4621
- answers: record.answers,
4622
- model: typeof record.model === "string" ? record.model : this.#model,
4623
- usage: record.usage
4795
+ answers: parsed.answers,
4796
+ model: isString(parsed.model) ? parsed.model : this.#model,
4797
+ usage: isRecordOf(parsed.usage) ? {
4798
+ input_tokens: parsed.usage.input_tokens,
4799
+ output_tokens: parsed.usage.output_tokens
4800
+ } : undefined
4624
4801
  };
4625
4802
  }
4626
4803
  #result(outcome) {
4627
- const usage = outcome.usage;
4804
+ const { usage } = outcome;
4628
4805
  const inputTokens = finiteTokens(usage?.input_tokens);
4629
4806
  const outputTokens = finiteTokens(usage?.output_tokens);
4630
4807
  const price = this.#pricePerMtok ?? advisorJevPricePerMtokRef;
@@ -4663,12 +4840,15 @@ var TYPESAFE_KEY_NAME = "typesafe-api-key";
4663
4840
  var TYPESAFE_KEY_CONFIG_FIELD = "typesafe_api_key";
4664
4841
  var KEY_FILE_MODE = 384;
4665
4842
  var keyFilePath = () => join4(getAgentDir3(), "typesafe_api_key");
4666
- var runtimeSecrets = () => globalThis.Bun?.secrets;
4843
+ var runtimeSecrets = () => {
4844
+ const bun = globalThis;
4845
+ return bun.Bun?.secrets;
4846
+ };
4667
4847
  var normalizeKey = (value) => value?.trim() || undefined;
4668
4848
  var readAdvisorJsonConfig = () => readExistingConfig(join4(getAgentDir3(), "advisor.json"));
4669
4849
  var defaultReadFileStore = () => {
4670
4850
  try {
4671
- return normalizeKey(readFileSync3(keyFilePath(), "utf8"));
4851
+ return normalizeKey(readFileSync3(keyFilePath(), "utf-8"));
4672
4852
  } catch {
4673
4853
  return;
4674
4854
  }
@@ -4708,7 +4888,7 @@ var resolveTypeSafeKey = async (deps = {}) => {
4708
4888
  }
4709
4889
  const config = (deps.readAdvisorJson ?? readAdvisorJsonConfig)();
4710
4890
  const staged = config[TYPESAFE_KEY_CONFIG_FIELD];
4711
- if (typeof staged === "string") {
4891
+ if (isString(staged)) {
4712
4892
  const fromConfig = normalizeKey(staged);
4713
4893
  if (fromConfig) {
4714
4894
  return { key: fromConfig, source: "advisor-json" };
@@ -4793,8 +4973,8 @@ var removeTypeSafeKeyFromAdvisorJson = () => {
4793
4973
  if (!(TYPESAFE_KEY_CONFIG_FIELD in existing)) {
4794
4974
  return { message: "No plaintext key in advisor.json.", ok: true };
4795
4975
  }
4796
- delete existing[TYPESAFE_KEY_CONFIG_FIELD];
4797
- writeFileSync2(path, `${JSON.stringify(existing, null, 2)}
4976
+ const retained = Object.fromEntries(Object.entries(existing).filter(([key]) => key !== TYPESAFE_KEY_CONFIG_FIELD));
4977
+ writeFileSync2(path, `${JSON.stringify(retained, null, 2)}
4798
4978
  `);
4799
4979
  resetConfigCache();
4800
4980
  return { message: "Plaintext key removed from advisor.json.", ok: true };
@@ -4836,15 +5016,14 @@ var screeningQuestions = {
4836
5016
  type: "score"
4837
5017
  }
4838
5018
  };
4839
- var NUMERIC_KEY_PATTERN = /^\d+$/;
4840
- var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
4841
- var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
5019
+ var NUMERIC_KEY_PATTERN = /^\d+$/u;
5020
+ var finiteNumber = (value) => isNumber(value) && Number.isFinite(value) ? value : undefined;
4842
5021
  var lowestStakesProbability = (answer) => {
4843
- if (!isRecord2(answer)) {
5022
+ if (!isRecordOf(answer)) {
4844
5023
  return;
4845
5024
  }
4846
- const probabilities = isRecord2(answer.probabilities) ? answer.probabilities : {};
4847
- const legend = isRecord2(answer.legend) ? answer.legend : undefined;
5025
+ const probabilities = isRecordOf(answer.probabilities) ? answer.probabilities : {};
5026
+ const legend = isRecordOf(answer.legend) ? answer.legend : undefined;
4848
5027
  if (legend) {
4849
5028
  const exact = Object.keys(legend).find((key) => legend[key] === STAKES_RUBRIC[0]);
4850
5029
  if (exact) {
@@ -4855,12 +5034,17 @@ var lowestStakesProbability = (answer) => {
4855
5034
  if (numericKeys.length === 0) {
4856
5035
  return;
4857
5036
  }
4858
- const lowest = numericKeys.reduce((left, right) => Number(left) <= Number(right) ? left : right);
5037
+ let [lowest] = numericKeys;
5038
+ for (const key of numericKeys) {
5039
+ if (Number(key) < Number(lowest)) {
5040
+ lowest = key;
5041
+ }
5042
+ }
4859
5043
  return finiteNumber(probabilities[lowest]);
4860
5044
  };
4861
- var selfAnswerableNoul = (answer) => isRecord2(answer) ? finiteNumber(answer.noul) : undefined;
5045
+ var selfAnswerableNoul = (answer) => isRecordOf(answer) ? finiteNumber(answer.noul) : undefined;
4862
5046
  var composeScreeningVerdict = (answers, { noulMargin, skipConfidence }) => {
4863
- if (!isRecord2(answers)) {
5047
+ if (!isRecordOf(answers)) {
4864
5048
  return { skip: false };
4865
5049
  }
4866
5050
  const negligibleMass = lowestStakesProbability(answers.stakes);
@@ -4874,29 +5058,29 @@ var composeScreeningVerdict = (answers, { noulMargin, skipConfidence }) => {
4874
5058
  };
4875
5059
  };
4876
5060
  var composeTurnGateVerdict = (answers, threshold) => {
4877
- if (!isRecord2(answers)) {
5061
+ if (!isRecordOf(answers)) {
4878
5062
  return false;
4879
5063
  }
4880
5064
  const answer = answers.should_consult;
4881
- const noul = isRecord2(answer) ? finiteNumber(answer.noul) : undefined;
5065
+ const noul = isRecordOf(answer) ? finiteNumber(answer.noul) : undefined;
4882
5066
  return noul !== undefined && noul >= threshold;
4883
5067
  };
4884
5068
 
4885
5069
  // src/jev/state.ts
4886
5070
  var JEV_TEXT_CAP_BYTES = 8 * 1024;
4887
5071
  var buildJevState = (ctx, input = {}) => {
4888
- const state = { role: "executor" };
5072
+ const fields = new Map([["role", "executor"]]);
4889
5073
  if (input.question) {
4890
- state.executor_question = redactAndCapText(input.question, JEV_TEXT_CAP_BYTES, advisorRedactSecretsRef);
5074
+ fields.set("executor_question", redactAndCapText(input.question, JEV_TEXT_CAP_BYTES, advisorRedactSecretsRef));
4891
5075
  }
4892
5076
  if (input.draft) {
4893
- state.executor_draft = redactAndCapText(input.draft, JEV_TEXT_CAP_BYTES, advisorRedactSecretsRef);
5077
+ fields.set("executor_draft", redactAndCapText(input.draft, JEV_TEXT_CAP_BYTES, advisorRedactSecretsRef));
4894
5078
  }
4895
5079
  const digest = recentConversation(ctx, advisorJevDigestMaxCharsRef);
4896
5080
  if (digest) {
4897
- state.recent_conversation = digest;
5081
+ fields.set("recent_conversation", digest);
4898
5082
  }
4899
- return state;
5083
+ return Object.fromEntries(fields);
4900
5084
  };
4901
5085
 
4902
5086
  // src/jev/transport.ts
@@ -4911,11 +5095,14 @@ var resolveJevTransport = async (ctx, deps = {}) => {
4911
5095
  const resolveTypesafe = deps.resolveTypesafe ?? resolveTypeSafeKey;
4912
5096
  const resolution = await resolveTypesafe();
4913
5097
  if (resolution.key) {
4914
- return {
5098
+ const credentials = {
4915
5099
  apiKey: resolution.key,
4916
- ...resolution.source ? { source: resolution.source } : {},
4917
5100
  transport: "typesafe"
4918
5101
  };
5102
+ if (resolution.source) {
5103
+ credentials.source = resolution.source;
5104
+ }
5105
+ return credentials;
4919
5106
  }
4920
5107
  }
4921
5108
  if (preference === "typesafe") {
@@ -4946,7 +5133,7 @@ var createOutageNotifier = (format) => {
4946
5133
  };
4947
5134
 
4948
5135
  // src/tools/jev-filter.ts
4949
- var normalizeScreeningQuestion = (question) => question?.trim().toLowerCase().replace(/\s+/g, " ") || undefined;
5136
+ var normalizeScreeningQuestion = (question) => question?.trim().toLowerCase().replaceAll(/\s+/gu, " ") || undefined;
4950
5137
  var REATTACHED_ADVICE_CAP_BYTES = 4 * 1024;
4951
5138
  var SCREENED_SKIP_TEXT = "Advisor consultation skipped (screened out): the stakes are low and you can resolve this yourself with available tools and context. Proceed on your own judgment with what you already have.";
4952
5139
  var repeatSkipText = (advice) => `Advisor consultation skipped (already answered): this question was answered earlier in this session; the earlier advice is reattached below. Consult again only if the situation has materially changed.
@@ -4956,30 +5143,6 @@ var outageNotifier = createOutageNotifier((category, message) => `Advisor Jev fi
4956
5143
  var notifyOutageOnce = outageNotifier.notify;
4957
5144
  var resetJevOutageNotification = outageNotifier.reset;
4958
5145
  var allow = () => ({ decision: "allow" });
4959
- var screenConsultation = (ctx, session, options, deps = {}) => {
4960
- if (isSimpleMode()) {
4961
- return Promise.resolve(allow());
4962
- }
4963
- const normalizedQuestion = normalizeScreeningQuestion(options.question);
4964
- const bypass = bypassOutcome(session, options, normalizedQuestion);
4965
- if (bypass) {
4966
- return Promise.resolve(bypass);
4967
- }
4968
- const reattached = session.reattachedAdviceFor(normalizedQuestion);
4969
- if (reattached) {
4970
- session.recordJevFilterSkipped(true, normalizedQuestion);
4971
- return Promise.resolve({
4972
- decision: "skip",
4973
- kind: "repeat",
4974
- reason: "already answered earlier in this session",
4975
- reattachedAdvice: reattached.slice(0, REATTACHED_ADVICE_CAP_BYTES)
4976
- });
4977
- }
4978
- if (!advisorJevFilterEnabledRef) {
4979
- return Promise.resolve(allow());
4980
- }
4981
- return screenWithJev(ctx, session, options, deps, normalizedQuestion);
4982
- };
4983
5146
  var bypassOutcome = (session, options, normalizedQuestion) => {
4984
5147
  const lastSkip = session.lastJevSkip;
4985
5148
  if (options.force) {
@@ -5027,7 +5190,7 @@ var screenWithJev = async (ctx, session, options, deps, normalizedQuestion) => {
5027
5190
  return allow();
5028
5191
  } catch (error) {
5029
5192
  session.recordJevFilterFailure();
5030
- if (error instanceof JevFailure) {
5193
+ if (error instanceof JevFailureError) {
5031
5194
  notifyOutageOnce(ctx, error.category, error.message);
5032
5195
  } else if (options.signal?.aborted) {
5033
5196
  throw error;
@@ -5037,10 +5200,34 @@ var screenWithJev = async (ctx, session, options, deps, normalizedQuestion) => {
5037
5200
  return allow();
5038
5201
  }
5039
5202
  };
5203
+ var screenConsultation = (ctx, session, options, deps = {}) => {
5204
+ if (isSimpleMode()) {
5205
+ return Promise.resolve(allow());
5206
+ }
5207
+ const normalizedQuestion = normalizeScreeningQuestion(options.question);
5208
+ const bypass = bypassOutcome(session, options, normalizedQuestion);
5209
+ if (bypass) {
5210
+ return Promise.resolve(bypass);
5211
+ }
5212
+ const reattached = session.reattachedAdviceFor(normalizedQuestion);
5213
+ if (reattached) {
5214
+ session.recordJevFilterSkipped(true, normalizedQuestion);
5215
+ return Promise.resolve({
5216
+ decision: "skip",
5217
+ kind: "repeat",
5218
+ reason: "already answered earlier in this session",
5219
+ reattachedAdvice: reattached.slice(0, REATTACHED_ADVICE_CAP_BYTES)
5220
+ });
5221
+ }
5222
+ if (!advisorJevFilterEnabledRef) {
5223
+ return Promise.resolve(allow());
5224
+ }
5225
+ return screenWithJev(ctx, session, options, deps, normalizedQuestion);
5226
+ };
5040
5227
  var screeningSkipText = (outcome) => outcome.kind === "repeat" && outcome.reattachedAdvice ? repeatSkipText(outcome.reattachedAdvice) : SCREENED_SKIP_TEXT;
5041
5228
 
5042
5229
  // src/commands/manual-consultation.ts
5043
- var startManualConsultation = (runtime, ctx, question, controller, scoutStatusToken, progress, gitContext) => {
5230
+ var startManualConsultation = async (runtime, ctx, question, controller, scoutStatusToken, progress, gitContext) => {
5044
5231
  herdrAdvisorActivity.start();
5045
5232
  progress.phase = "preparing";
5046
5233
  runtime.requestManualRender(ctx);
@@ -5055,23 +5242,24 @@ var startManualConsultation = (runtime, ctx, question, controller, scoutStatusTo
5055
5242
  runtime.manualProgressTimers.set(controller, timer);
5056
5243
  }
5057
5244
  let scoutDetails;
5058
- return runtime.requestAdvisor(ctx, question, controller.signal, (thinking, text) => {
5059
- if (controller.signal.aborted) {
5060
- return;
5061
- }
5062
- progress.phase = "active";
5063
- progress.thinking = thinking;
5064
- progress.text = text;
5065
- runtime.requestManualRender(ctx);
5066
- }, (event) => {
5067
- if (!controller.signal.aborted) {
5068
- runtime.scoutStatus.update(ctx, scoutStatusToken, event);
5069
- scoutDetails = appendScoutLifecycleEntry(runtime.pi, event, scoutDetails);
5070
- progress.scout = scoutDetails;
5245
+ try {
5246
+ const { adviceId, markdown, usage } = await runtime.requestAdvisor(ctx, question, controller.signal, (thinking, text) => {
5247
+ if (controller.signal.aborted) {
5248
+ return;
5249
+ }
5071
5250
  progress.phase = "active";
5251
+ progress.thinking = thinking;
5252
+ progress.text = text;
5072
5253
  runtime.requestManualRender(ctx);
5073
- }
5074
- }, gitContext).then(({ adviceId, markdown, usage }) => {
5254
+ }, (event) => {
5255
+ if (!controller.signal.aborted) {
5256
+ runtime.scoutStatus.update(ctx, scoutStatusToken, event);
5257
+ scoutDetails = appendScoutLifecycleEntry(runtime.pi, event, scoutDetails);
5258
+ progress.scout = scoutDetails;
5259
+ progress.phase = "active";
5260
+ runtime.requestManualRender(ctx);
5261
+ }
5262
+ }, gitContext);
5075
5263
  if (controller.signal.aborted) {
5076
5264
  return;
5077
5265
  }
@@ -5085,28 +5273,31 @@ var startManualConsultation = (runtime, ctx, question, controller, scoutStatusTo
5085
5273
  trigger: "manual",
5086
5274
  usage
5087
5275
  });
5088
- if (typeof adviceId === "string") {
5276
+ if (isString(adviceId)) {
5089
5277
  runtime.advisorSessionState.issueAdvice(adviceId, markdown, "manual", false, normalizeScreeningQuestion(question));
5090
5278
  }
5091
5279
  runtime.updateAdvisorUsageStatus(ctx);
5280
+ const details = {
5281
+ advisor: advisorRef,
5282
+ question,
5283
+ text: markdown
5284
+ };
5092
5285
  const normalizedUsage = snapshotAdvisorUsage(usage);
5286
+ if (normalizedUsage) {
5287
+ details.usage = normalizedUsage;
5288
+ }
5093
5289
  runtime.pi.sendMessage({
5094
5290
  content: `Manual Advisor consultation${question ? ` (${question})` : ""} — for your awareness; no action or follow-up consultation is needed unless the user asks:
5095
5291
 
5096
5292
  ${markdown}`,
5097
5293
  customType: "advisor-manual-result",
5098
- details: {
5099
- advisor: advisorRef,
5100
- question,
5101
- text: markdown,
5102
- ...normalizedUsage ? { usage: normalizedUsage } : {}
5103
- },
5294
+ details,
5104
5295
  display: true
5105
5296
  }, {
5106
5297
  deliverAs: "steer",
5107
5298
  triggerTurn: true
5108
5299
  });
5109
- }).catch((error) => {
5300
+ } catch (error) {
5110
5301
  if (controller.signal.aborted) {
5111
5302
  return;
5112
5303
  }
@@ -5132,7 +5323,7 @@ ${markdown}`,
5132
5323
  }, { deliverAs: "steer", triggerTurn: true });
5133
5324
  notify(ctx, `Advisor consultation failed: ${message}`, "error");
5134
5325
  notifyHerdrAdvisorFailure("Advisor consultation failed", message);
5135
- }).finally(() => {
5326
+ } finally {
5136
5327
  if (controller.signal.aborted) {
5137
5328
  progress.phase = "cancelled";
5138
5329
  }
@@ -5145,7 +5336,7 @@ ${markdown}`,
5145
5336
  runtime.scoutStatus.release(ctx, scoutStatusToken);
5146
5337
  runtime.manualConsultations.delete(controller);
5147
5338
  herdrAdvisorActivity.finish();
5148
- });
5339
+ }
5149
5340
  };
5150
5341
 
5151
5342
  // src/commands/manual-command.ts
@@ -5156,6 +5347,11 @@ var registerManualCommand = (runtime) => {
5156
5347
  if (!loadCommandConfig(ctx)) {
5157
5348
  return;
5158
5349
  }
5350
+ const accessReason = advisorModelAccessReason(ctx);
5351
+ if (accessReason) {
5352
+ notify(ctx, accessReason, "warning");
5353
+ return;
5354
+ }
5159
5355
  if (!(isSimpleMode() || runtime.advisorSessionState.canConsult(getAdvisorMaxCallsPerSession()))) {
5160
5356
  runtime.reportManualBudgetExhausted(ctx);
5161
5357
  return;
@@ -5305,41 +5501,40 @@ class ManualAdvisorProgressComponent {
5305
5501
  }
5306
5502
  return box.render(width);
5307
5503
  }
5308
- invalidate() {}
5504
+ invalidate = noop;
5309
5505
  }
5310
5506
 
5311
5507
  // src/commands/renderers.ts
5312
- var registerCommandRenderers = (runtime) => {
5313
- runtime.pi.registerEntryRenderer?.("advisor-manual-call", (entry, { expanded }, theme) => {
5314
- const { progressId, question } = entry.data ?? {};
5315
- const progress = progressId ? runtime.manualProgress.get(progressId) : undefined;
5316
- return progress ? new ManualAdvisorProgressComponent(question, progress, Boolean(expanded), theme) : renderAdvisorCallBox(question, theme);
5317
- });
5318
- runtime.pi.registerMessageRenderer?.("advisor-manual-result", (message, { expanded }, theme) => {
5319
- const details = message.details;
5320
- const box = new Box2(1, 1, (text) => theme.bg("customMessageBg", text));
5321
- const advice = details?.text ?? (typeof message.content === "string" ? message.content : "(Advisor returned no advice.)");
5322
- box.addChild(new Text4(renderAdvisorResponseHeader(hasSoundVerdict(advice), theme), 0, 0));
5323
- if (details?.advisor) {
5324
- box.addChild(new Text4(theme.fg("dim", ` ${details.advisor}`), 0, 0));
5325
- }
5326
- if (getAdvisorSettings().showUsageDetails) {
5327
- const usage = formatAdvisorUsage(details?.usage);
5328
- if (usage) {
5329
- box.addChild(new Text4(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
5330
- }
5508
+ var manualCallRenderer = (runtime) => (entry, { expanded }, theme) => {
5509
+ const { progressId, question } = entry.data ?? {};
5510
+ const progress = progressId ? runtime.manualProgress.get(progressId) : undefined;
5511
+ return progress ? new ManualAdvisorProgressComponent(question, progress, Boolean(expanded), theme) : renderAdvisorCallBox(question, theme);
5512
+ };
5513
+ var manualResultRenderer = (message, { expanded }, theme) => {
5514
+ const { details } = message;
5515
+ const box = new Box2(1, 1, (text) => theme.bg("customMessageBg", text));
5516
+ const advice = details?.text ?? (isString(message.content) ? message.content : "(Advisor returned no advice.)");
5517
+ box.addChild(new Text4(renderAdvisorResponseHeader(hasSoundVerdict(advice), theme), 0, 0));
5518
+ if (details?.advisor) {
5519
+ box.addChild(new Text4(theme.fg("dim", ` ${details.advisor}`), 0, 0));
5520
+ }
5521
+ if (getAdvisorSettings().showUsageDetails) {
5522
+ const usage = formatAdvisorUsage(details?.usage);
5523
+ if (usage) {
5524
+ box.addChild(new Text4(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
5331
5525
  }
5332
- box.addChild(new Markdown3(adviceForDisplay(advice, expanded), 0, 0, getMarkdownTheme3()));
5333
- return box;
5334
- });
5526
+ }
5527
+ box.addChild(new Markdown3(adviceForDisplay(advice, expanded), 0, 0, getMarkdownTheme3()));
5528
+ return box;
5529
+ };
5530
+ var registerCommandRenderers = (runtime) => {
5531
+ runtime.pi.registerEntryRenderer?.("advisor-manual-call", manualCallRenderer(runtime));
5532
+ runtime.pi.registerMessageRenderer?.("advisor-manual-result", manualResultRenderer);
5335
5533
  };
5336
5534
 
5337
5535
  // src/ui/settings-selector.ts
5338
5536
  import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
5339
- import {
5340
- SettingsList,
5341
- truncateToWidth as truncateToWidth7
5342
- } from "@earendil-works/pi-tui";
5537
+ import { SettingsList, truncateToWidth as truncateToWidth7 } from "@earendil-works/pi-tui";
5343
5538
 
5344
5539
  // src/ui/settings-formatting.ts
5345
5540
  import { visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
@@ -5357,7 +5552,7 @@ var SIMPLE_MODE_GRADIENT_COLORS = [
5357
5552
  var withCurrentValue = (current, values) => values.includes(current) ? values : [current, ...values];
5358
5553
  var numericValues = (current, values) => {
5359
5554
  const all = values.includes(current) ? values : [...values, current];
5360
- return all.sort((a, b) => a - b).map(String);
5555
+ return all.toSorted((a, b) => a - b).map(String);
5361
5556
  };
5362
5557
  var maxCallValues = (current) => {
5363
5558
  const values = ["0", "1", "2", "3", "5", "10", "25", "50", "∞"];
@@ -5374,9 +5569,18 @@ var maxCallValues = (current) => {
5374
5569
  };
5375
5570
  var settingValue = (value, defaultValue) => value ?? defaultValue ? "On" : "Off";
5376
5571
  var currentContextLabel = (presets, contextMaxChars) => presets.find((preset) => preset.value === contextMaxChars)?.label ?? String(contextMaxChars);
5572
+ var closestPresetIndex = (presets, contextMaxChars) => {
5573
+ let closestIndex = 0;
5574
+ for (let index = 0;index < presets.length; index += 1) {
5575
+ if (Math.abs(presets[index].value - contextMaxChars) < Math.abs(presets[closestIndex].value - contextMaxChars)) {
5576
+ closestIndex = index;
5577
+ }
5578
+ }
5579
+ return closestIndex;
5580
+ };
5377
5581
  var contextDescription = (presets, contextMaxChars) => {
5378
5582
  const exactIndex = presets.findIndex((preset) => preset.value === contextMaxChars);
5379
- const selectedIndex = exactIndex >= 0 ? exactIndex : presets.reduce((closestIndex, preset, index) => Math.abs(preset.value - contextMaxChars) < Math.abs(presets[closestIndex].value - contextMaxChars) ? index : closestIndex, 0);
5583
+ const selectedIndex = exactIndex === -1 ? closestPresetIndex(presets, contextMaxChars) : exactIndex;
5380
5584
  const selectedPreset = presets[selectedIndex];
5381
5585
  const isFullContext = selectedPreset?.value === Number.MAX_SAFE_INTEGER || selectedPreset?.label.toUpperCase() === "FULL" || selectedPreset?.label.toUpperCase() === "ALL";
5382
5586
  const progress = isFullContext ? 1 : selectedIndex / Math.max(1, presets.length - 1);
@@ -5394,7 +5598,7 @@ var contextDescription = (presets, contextMaxChars) => {
5394
5598
  const markerColumn = meterPrefix.length + marker;
5395
5599
  const labelStart = Math.max(0, Math.min(meterPrefix.length + meter.length + 2 - labelWidth, markerColumn - Math.floor((labelWidth - 1) / 2)));
5396
5600
  const markerLabel = `${" ".repeat(labelStart)}${label}`;
5397
- const description = exactIndex >= 0 ? selectedPreset?.description : "Custom context limit.";
5601
+ const description = exactIndex === -1 ? "Custom context limit." : selectedPreset?.description;
5398
5602
  return `${description ?? "Custom context limit."}
5399
5603
  ${meterPrefix}${meter} full
5400
5604
  ${markerLabel}`;
@@ -5419,12 +5623,11 @@ var rainbowGradient = (text, startedAt) => {
5419
5623
  }).join("").concat("\x1B[0m");
5420
5624
  };
5421
5625
 
5626
+ // src/ui/settings-items.ts
5627
+ import { getKeybindings } from "@earendil-works/pi-tui";
5628
+
5422
5629
  // src/ui/jev-setup-submenu.ts
5423
- import {
5424
- Key as Key2,
5425
- matchesKey as matchesKey2,
5426
- truncateToWidth as truncateToWidth5
5427
- } from "@earendil-works/pi-tui";
5630
+ import { Key as Key2, matchesKey as matchesKey2, truncateToWidth as truncateToWidth5 } from "@earendil-works/pi-tui";
5428
5631
 
5429
5632
  // node_modules/@typesafe-ai/sdk/dist/index.mjs
5430
5633
  var range2 = (from, to) => Array.from({ length: to - from }, (_, i) => from + i);
@@ -5470,10 +5673,7 @@ var describeRuntime = () => {
5470
5673
  var RUNTIME = describeRuntime();
5471
5674
 
5472
5675
  // src/ui/masked-input.ts
5473
- import {
5474
- Input as Input2,
5475
- truncateToWidth as truncateToWidth4
5476
- } from "@earendil-works/pi-tui";
5676
+ import { Input as Input2, truncateToWidth as truncateToWidth4 } from "@earendil-works/pi-tui";
5477
5677
 
5478
5678
  class MaskedInput {
5479
5679
  input;
@@ -5515,19 +5715,36 @@ class MaskedInput {
5515
5715
  }
5516
5716
 
5517
5717
  // src/ui/jev-setup-submenu.ts
5718
+ var ACTION_LABELS = {
5719
+ disable: "Disable",
5720
+ "disable-clear": "Disable and clear stored key",
5721
+ done: "Done",
5722
+ "enter-key": "Enter a TypeSafe API key",
5723
+ "verify-again": "Verify again",
5724
+ "verify-enable": "Verify and enable"
5725
+ };
5726
+ var fireAndForget = async (action) => {
5727
+ try {
5728
+ await action;
5729
+ } catch {}
5730
+ };
5518
5731
  var transportLabel = (credentials) => {
5519
5732
  if (credentials.transport === "openrouter") {
5520
5733
  return "OpenRouter (reusing pi login)";
5521
5734
  }
5522
5735
  switch (credentials.source) {
5523
- case "advisor-json":
5736
+ case "advisor-json": {
5524
5737
  return "TypeSafe (key: advisor.json — plaintext, not recommended)";
5525
- case "bun-secrets":
5738
+ }
5739
+ case "bun-secrets": {
5526
5740
  return "TypeSafe (key: Bun.secrets)";
5527
- case "file":
5741
+ }
5742
+ case "file": {
5528
5743
  return "TypeSafe (key: stored file, mode 0600)";
5529
- default:
5744
+ }
5745
+ default: {
5530
5746
  return "TypeSafe (key: TYPESAFE_API_KEY)";
5747
+ }
5531
5748
  }
5532
5749
  };
5533
5750
  var defaultVerify = async (credentials) => {
@@ -5566,9 +5783,7 @@ class JevSetupSubmenu {
5566
5783
  onSubmit: (value) => this.submitEnteredKey(value),
5567
5784
  placeholder: "Paste a TypeSafe API key"
5568
5785
  });
5569
- this.refresh().catch(() => {
5570
- return;
5571
- });
5786
+ fireAndForget(this.refresh());
5572
5787
  }
5573
5788
  get focused() {
5574
5789
  return this._focused;
@@ -5609,24 +5824,20 @@ class JevSetupSubmenu {
5609
5824
  ""
5610
5825
  ];
5611
5826
  if (this.credentials) {
5612
- lines.push(` Transport: ${transportLabel(this.credentials)}`);
5613
- lines.push(` Filter: ${enabled ? "On" : "Off"}`);
5827
+ lines.push(` Transport: ${transportLabel(this.credentials)}`, ` Filter: ${enabled ? "On" : "Off"}`);
5614
5828
  } else if (this.mode === "verifying") {
5615
5829
  lines.push(" Checking available Jev credentials…");
5616
5830
  } else {
5617
- lines.push(" No Jev credentials found. Enter a TypeSafe API key below, or add");
5618
- lines.push(" an OpenRouter login in pi; it is reused automatically.");
5831
+ lines.push(" No Jev credentials found. Enter a TypeSafe API key below, or add", " an OpenRouter login in pi; it is reused automatically.");
5619
5832
  }
5620
5833
  if (this.notice) {
5621
- lines.push("");
5622
- lines.push(theme.fg("warning", ` ${this.notice}`));
5834
+ lines.push("", theme.fg("warning", ` ${this.notice}`));
5623
5835
  }
5624
5836
  lines.push("");
5625
5837
  if (this.mode === "verifying") {
5626
5838
  lines.push(" Verifying with a live Jev call…");
5627
5839
  } else if (this.mode === "key-entry") {
5628
- lines.push(` ${this.maskedInput.render(Math.max(10, width - 4))[0] ?? ""}`);
5629
- lines.push(theme.fg("dim", " Enter: verify · Esc: cancel"));
5840
+ lines.push(` ${this.maskedInput.render(Math.max(10, width - 4))[0] ?? ""}`, theme.fg("dim", " Enter: verify · Esc: cancel"));
5630
5841
  } else {
5631
5842
  for (const [index, label] of this.labels().entries()) {
5632
5843
  const prefix = index === this.selectedIndex ? "→ " : " ";
@@ -5648,19 +5859,8 @@ class JevSetupSubmenu {
5648
5859
  actions.push("done");
5649
5860
  return actions;
5650
5861
  }
5651
- actionLabels() {
5652
- return {
5653
- disable: "Disable",
5654
- "disable-clear": "Disable and clear stored key",
5655
- done: "Done",
5656
- "enter-key": "Enter a TypeSafe API key",
5657
- "verify-again": "Verify again",
5658
- "verify-enable": "Verify and enable"
5659
- };
5660
- }
5661
5862
  labels() {
5662
- const labels = this.actionLabels();
5663
- return this.actions().map((action) => labels[action]);
5863
+ return this.actions().map((action) => ACTION_LABELS[action]);
5664
5864
  }
5665
5865
  async refresh() {
5666
5866
  const wasVerifying = this.mode === "verifying";
@@ -5677,30 +5877,25 @@ class JevSetupSubmenu {
5677
5877
  this.options.tui.requestRender();
5678
5878
  }
5679
5879
  activate(action) {
5680
- switch (action) {
5681
- case "done":
5682
- this.options.done();
5683
- return;
5684
- case "enter-key":
5685
- this.mode = "key-entry";
5686
- return;
5687
- case "disable":
5688
- this.notice = undefined;
5689
- this.options.done("Off");
5690
- return;
5691
- case "disable-clear":
5692
- this.disableAndClear().catch(() => {
5693
- return;
5694
- });
5695
- return;
5696
- case "verify-again":
5697
- case "verify-enable":
5698
- this.verifyAndEnable().catch(() => {
5699
- return;
5700
- });
5701
- return;
5702
- default:
5703
- return;
5880
+ if (action === "done") {
5881
+ this.options.done();
5882
+ return;
5883
+ }
5884
+ if (action === "enter-key") {
5885
+ this.mode = "key-entry";
5886
+ return;
5887
+ }
5888
+ if (action === "disable") {
5889
+ this.notice = undefined;
5890
+ this.options.done("Off");
5891
+ return;
5892
+ }
5893
+ if (action === "disable-clear") {
5894
+ fireAndForget(this.disableAndClear());
5895
+ return;
5896
+ }
5897
+ if (action === "verify-again" || action === "verify-enable") {
5898
+ fireAndForget(this.verifyAndEnable());
5704
5899
  }
5705
5900
  }
5706
5901
  async disableAndClear() {
@@ -5772,11 +5967,15 @@ class JevSetupSubmenu {
5772
5967
  }
5773
5968
  }
5774
5969
 
5970
+ // src/ui/model-multi-selector.ts
5971
+ class SearchableModelMultiSelector extends ModelSelectorAdapter {
5972
+ constructor(options) {
5973
+ super(new SearchableModelList(options));
5974
+ }
5975
+ }
5976
+
5775
5977
  // src/ui/text-setting-submenu.ts
5776
- import {
5777
- Input as Input3,
5778
- truncateToWidth as truncateToWidth6
5779
- } from "@earendil-works/pi-tui";
5978
+ import { Input as Input3, truncateToWidth as truncateToWidth6 } from "@earendil-works/pi-tui";
5780
5979
 
5781
5980
  class TextSettingSubmenu {
5782
5981
  input = new Input3;
@@ -5939,8 +6138,28 @@ var jevItems = (settings, theme, tui) => [
5939
6138
  ])
5940
6139
  }
5941
6140
  ];
6141
+ var advisorModelWhitelistItem = (settings, modelRefs, keybindings, theme, tui) => ({
6142
+ currentValue: settings.modelWhitelist?.length ? settings.modelWhitelist.join(", ") : "Any model",
6143
+ description: "Only the exact provider/model references listed here may call the Advisor; an empty list allows every model.",
6144
+ id: "modelWhitelist",
6145
+ label: "Advisor model whitelist",
6146
+ submenu: (_currentValue, done) => new SearchableModelMultiSelector({
6147
+ allOptions: [
6148
+ ...new Set([...modelRefs ?? [], ...settings.modelWhitelist ?? []])
6149
+ ],
6150
+ currentOptions: settings.modelWhitelist ?? [],
6151
+ keybindings: keybindings ?? getKeybindings(),
6152
+ multiSelect: true,
6153
+ onCancel: done,
6154
+ onSelect: (values) => done(values.join(",")),
6155
+ theme,
6156
+ title: "Advisor model whitelist",
6157
+ tui
6158
+ })
6159
+ });
5942
6160
  var createSettingsItems = ({
5943
6161
  effortLevels,
6162
+ modelWhitelist,
5944
6163
  presets,
5945
6164
  settings,
5946
6165
  theme,
@@ -5970,6 +6189,7 @@ var createSettingsItems = ({
5970
6189
  }
5971
6190
  ];
5972
6191
  if (settings.simpleMode) {
6192
+ items.push(modelWhitelist);
5973
6193
  return items;
5974
6194
  }
5975
6195
  items.push({
@@ -5978,7 +6198,7 @@ var createSettingsItems = ({
5978
6198
  id: "effort",
5979
6199
  label: "Advisor reasoning",
5980
6200
  values: withCurrentValue(currentEffort(settings.effort), effortLevels)
5981
- }, toggle("scoutEnabled", "Experimental Advisor Scout", "Enable the experimental Scout before Advisor calls.", settings.scoutEnabled, false), toggle("showUsageDetails", "Show usage and cost details", "Show token usage and cost details in Advisor responses.", settings.showUsageDetails, true), toggle("showUsageFooter", "Show usage in footer", "Show the current Advisor usage summary in the footer.", settings.showUsageFooter, false), toggle("planGate", "Plan gate", "Ask the Advisor to review implementation plans.", settings.planGate, true), toggle("failureGate", "Failure gate", "Ask the Advisor to review repeated failures.", settings.failureGate, true), toggle("completionGate", "Completion gate", "Ask the Advisor to review work before declaring success.", settings.completionGate, true), toggle("collapseResponses", "Collapse long responses", "Collapse long Advisor responses in the transcript.", settings.collapseResponses, false), {
6201
+ }, modelWhitelist, toggle("scoutEnabled", "Experimental Advisor Scout", "Enable the experimental Scout before Advisor calls.", settings.scoutEnabled, false), toggle("showUsageDetails", "Show usage and cost details", "Show token usage and cost details in Advisor responses.", settings.showUsageDetails, true), toggle("showUsageFooter", "Show usage in footer", "Show the current Advisor usage summary in the footer.", settings.showUsageFooter, false), toggle("planGate", "Plan gate", "Ask the Advisor to review implementation plans.", settings.planGate, true), toggle("failureGate", "Failure gate", "Ask the Advisor to review repeated failures.", settings.failureGate, true), toggle("completionGate", "Completion gate", "Ask the Advisor to review work before declaring success.", settings.completionGate, true), toggle("collapseResponses", "Collapse long responses", "Collapse long Advisor responses in the transcript.", settings.collapseResponses, false), {
5982
6202
  currentValue: settings.customRule || "None",
5983
6203
  description: "Add a rule that triggers Advisor involvement.",
5984
6204
  id: "customRule",
@@ -6080,10 +6300,7 @@ var createSettingsItems = ({
6080
6300
  };
6081
6301
 
6082
6302
  // src/ui/settings-list-adapter.ts
6083
- import {
6084
- Key as Key3,
6085
- matchesKey as matchesKey3
6086
- } from "@earendil-works/pi-tui";
6303
+ import { Key as Key3, matchesKey as matchesKey3 } from "@earendil-works/pi-tui";
6087
6304
 
6088
6305
  class SettingsListAdapter {
6089
6306
  list;
@@ -6111,7 +6328,7 @@ class SettingsListAdapter {
6111
6328
  }
6112
6329
  setSelectedId(items, selectedId) {
6113
6330
  const selectedIndex = items.findIndex((item) => item.id === selectedId);
6114
- if (selectedIndex >= 0) {
6331
+ if (selectedIndex !== -1) {
6115
6332
  this.privateFields().selectedIndex = selectedIndex;
6116
6333
  }
6117
6334
  }
@@ -6154,102 +6371,160 @@ class SettingsListAdapter {
6154
6371
  }
6155
6372
 
6156
6373
  // src/ui/settings-mutations.ts
6157
- var BOOLEAN_SETTING_IDS = new Set([
6158
- "scoutEnabled",
6159
- "showUsageDetails",
6160
- "showUsageFooter",
6161
- "planGate",
6162
- "failureGate",
6163
- "completionGate",
6164
- "collapseResponses",
6165
- "blockOnBlocked",
6374
+ var BOOLEAN_SETTING_FIELDS = [
6166
6375
  "autoLoopGate",
6167
- "sessionSummary",
6376
+ "blockOnBlocked",
6377
+ "collapseResponses",
6378
+ "completionGate",
6379
+ "failureGate",
6168
6380
  "herdrIntegration",
6381
+ "outcomeLogging",
6382
+ "planGate",
6169
6383
  "redactSecrets",
6384
+ "scoutEnabled",
6385
+ "sessionSummary",
6386
+ "showUsageDetails",
6387
+ "showUsageFooter",
6170
6388
  "trackedFileContent",
6171
- "untrackedContent",
6172
- "outcomeLogging"
6173
- ]);
6174
- var mutateAdvisorSettings = (settings, id, value, presets) => {
6389
+ "untrackedContent"
6390
+ ];
6391
+ var BOOLEAN_SETTING_IDS = new Set(BOOLEAN_SETTING_FIELDS);
6392
+ var parseModelWhitelist = (value) => [
6393
+ ...new Set(value.split(",").map((model) => model.trim()).filter(Boolean))
6394
+ ];
6395
+ var applyCoreMutation = (settings, id, value, presets) => {
6175
6396
  switch (id) {
6176
- case "context":
6397
+ case "context": {
6177
6398
  settings.contextMaxChars = presets.find((preset) => preset.label === value)?.value ?? settings.contextMaxChars;
6178
- break;
6179
- case "simpleMode":
6399
+ return true;
6400
+ }
6401
+ case "simpleMode": {
6180
6402
  settings.simpleMode = value === "On";
6181
- break;
6182
- case "alwaysOn":
6403
+ return true;
6404
+ }
6405
+ case "alwaysOn": {
6183
6406
  settings.alwaysOn = value === "On";
6184
- break;
6185
- case "effort":
6407
+ return true;
6408
+ }
6409
+ case "effort": {
6186
6410
  settings.effort = value;
6187
- break;
6188
- case "customRule":
6411
+ return true;
6412
+ }
6413
+ case "customRule": {
6189
6414
  settings.customRule = value.trim() || undefined;
6190
- break;
6191
- case "toolPolicies":
6415
+ return true;
6416
+ }
6417
+ case "toolPolicies": {
6192
6418
  settings.toolPolicies = JSON.parse(value);
6193
- break;
6194
- case "loopThreshold":
6419
+ return true;
6420
+ }
6421
+ case "loopThreshold": {
6195
6422
  settings.loopThreshold = Number(value.replace("After ", "").replace(" repeats", ""));
6196
- break;
6197
- case "maxCallsPerSession":
6423
+ return true;
6424
+ }
6425
+ case "maxCallsPerSession": {
6198
6426
  settings.maxCallsPerSession = value === "∞" ? undefined : Number(value);
6199
- break;
6200
- case "failureMode":
6427
+ return true;
6428
+ }
6429
+ case "modelWhitelist": {
6430
+ settings.modelWhitelist = parseModelWhitelist(value);
6431
+ return true;
6432
+ }
6433
+ case "failureMode": {
6201
6434
  settings.failureMode = value;
6202
- break;
6203
- case "gitContext":
6435
+ return true;
6436
+ }
6437
+ case "gitContext": {
6204
6438
  settings.gitContext = value;
6205
- break;
6206
- case "toolResultMaxLines":
6439
+ return true;
6440
+ }
6441
+ default: {
6442
+ return false;
6443
+ }
6444
+ }
6445
+ };
6446
+ var applyNumericMutation = (settings, id, value) => {
6447
+ switch (id) {
6448
+ case "toolResultMaxLines": {
6207
6449
  settings.toolResultMaxLines = Number(value);
6208
- break;
6209
- case "toolResultMaxBytes":
6450
+ return true;
6451
+ }
6452
+ case "toolResultMaxBytes": {
6210
6453
  settings.toolResultMaxBytes = Number(value);
6211
- break;
6212
- case "gitContextMaxChars":
6454
+ return true;
6455
+ }
6456
+ case "gitContextMaxChars": {
6213
6457
  settings.gitContextMaxChars = Number(value);
6214
- break;
6215
- case "jevFilter":
6458
+ return true;
6459
+ }
6460
+ default: {
6461
+ return false;
6462
+ }
6463
+ }
6464
+ };
6465
+ var applyJevMutation = (settings, id, value) => {
6466
+ switch (id) {
6467
+ case "jevFilter": {
6216
6468
  settings.jevFilterEnabled = value === "On";
6217
- break;
6218
- case "jevFilterSkipConfidence":
6469
+ return true;
6470
+ }
6471
+ case "jevFilterSkipConfidence": {
6219
6472
  settings.jevFilterSkipConfidence = Number(value);
6220
- break;
6221
- case "jevFilterNoulMargin":
6473
+ return true;
6474
+ }
6475
+ case "jevFilterNoulMargin": {
6222
6476
  settings.jevFilterNoulMargin = Number(value);
6223
- break;
6224
- case "jevFilterOverrideWindow":
6477
+ return true;
6478
+ }
6479
+ case "jevFilterOverrideWindow": {
6225
6480
  settings.jevFilterOverrideWindow = Number(value.replace(" turns", ""));
6226
- break;
6227
- case "jevTurnGateEveryTurns":
6228
- settings.jevTurnGateEveryTurns = value === "Off" ? 0 : Number(value.replace(/[^0-9]/g, ""));
6229
- break;
6230
- case "jevTurnGateNoulThreshold":
6481
+ return true;
6482
+ }
6483
+ case "jevTurnGateEveryTurns": {
6484
+ settings.jevTurnGateEveryTurns = value === "Off" ? 0 : Number(value.replaceAll(/[^0-9]/gu, ""));
6485
+ return true;
6486
+ }
6487
+ case "jevTurnGateNoulThreshold": {
6231
6488
  settings.jevTurnGateNoulThreshold = Number(value);
6232
- break;
6233
- case "jevModel":
6489
+ return true;
6490
+ }
6491
+ case "jevModel": {
6234
6492
  settings.jevModel = value.trim() || DEFAULT_JEV_MODEL;
6235
- break;
6236
- case "jevTimeoutMs":
6493
+ return true;
6494
+ }
6495
+ case "jevTimeoutMs": {
6237
6496
  settings.jevTimeoutMs = Number(value);
6238
- break;
6239
- case "jevDigestMaxChars":
6497
+ return true;
6498
+ }
6499
+ case "jevDigestMaxChars": {
6240
6500
  settings.jevDigestMaxChars = Number(value);
6241
- break;
6242
- case "jevPricePerMtok":
6501
+ return true;
6502
+ }
6503
+ case "jevPricePerMtok": {
6243
6504
  settings.jevPricePerMtok = Number(value);
6244
- break;
6245
- case "jevTransport":
6505
+ return true;
6506
+ }
6507
+ case "jevTransport": {
6246
6508
  settings.jevTransport = value;
6247
- break;
6248
- default:
6249
- if (BOOLEAN_SETTING_IDS.has(id)) {
6250
- settings[id] = value === "On";
6251
- }
6252
- break;
6509
+ return true;
6510
+ }
6511
+ default: {
6512
+ return false;
6513
+ }
6514
+ }
6515
+ };
6516
+ var mutateAdvisorSettings = (settings, id, value, presets) => {
6517
+ if (applyCoreMutation(settings, id, value, presets)) {
6518
+ return;
6519
+ }
6520
+ if (applyNumericMutation(settings, id, value)) {
6521
+ return;
6522
+ }
6523
+ if (applyJevMutation(settings, id, value)) {
6524
+ return;
6525
+ }
6526
+ if (BOOLEAN_SETTING_IDS.has(id)) {
6527
+ settings[id] = value === "On";
6253
6528
  }
6254
6529
  };
6255
6530
 
@@ -6280,7 +6555,7 @@ class AdvisorSettingsSelector {
6280
6555
  label: String(configuredContext),
6281
6556
  value: configuredContext
6282
6557
  }
6283
- ].sort((a, b) => a.value - b.value);
6558
+ ].toSorted((a, b) => a.value - b.value);
6284
6559
  if (this.settings.simpleMode) {
6285
6560
  this.startSimpleModeGradient();
6286
6561
  }
@@ -6311,8 +6586,10 @@ class AdvisorSettingsSelector {
6311
6586
  }
6312
6587
  return defaultLabel(text, selected);
6313
6588
  };
6589
+ const modelWhitelist = advisorModelWhitelistItem(this.settings, this.options.modelRefs, this.options.keybindings, this.options.theme, this.options.tui);
6314
6590
  const items = createSettingsItems({
6315
6591
  effortLevels: this.options.effortLevels,
6592
+ modelWhitelist,
6316
6593
  presets: this.presets,
6317
6594
  settings: this.settings,
6318
6595
  theme: this.options.theme,
@@ -6352,16 +6629,16 @@ class AdvisorSettingsSelector {
6352
6629
  (this.options.onChange ?? this.options.onSave)?.({
6353
6630
  ...this.settings,
6354
6631
  showUsageDetails: this.settings.showUsageDetails ?? true,
6355
- toolPolicies: { ...this.settings.toolPolicies ?? {} }
6632
+ toolPolicies: { ...this.settings.toolPolicies }
6356
6633
  });
6357
- if (id === "context" || id === "simpleMode" || id === "customRule" || id === "toolPolicies") {
6634
+ if (id === "context" || id === "simpleMode" || id === "customRule" || id === "modelWhitelist" || id === "toolPolicies") {
6358
6635
  this.settingsList = this.createSettingsList(id);
6359
6636
  }
6360
6637
  }
6361
6638
  }
6362
6639
 
6363
6640
  // src/commands/settings-persistence.ts
6364
- var applyAdvisorSettings = (settings) => {
6641
+ var applySessionSettings = (settings) => {
6365
6642
  setAdvisorEffortRef(settings.effort === "Default (Model Default)" ? undefined : settings.effort);
6366
6643
  setContextMaxCharsRef(settings.contextMaxChars);
6367
6644
  setAdvisorPlanGateRef(settings.planGate);
@@ -6373,6 +6650,7 @@ var applyAdvisorSettings = (settings) => {
6373
6650
  setAdvisorAutoLoopGateRef(settings.autoLoopGate ?? true);
6374
6651
  setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
6375
6652
  setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
6653
+ setAdvisorModelWhitelistRef(settings.modelWhitelist ?? []);
6376
6654
  setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
6377
6655
  setAdvisorScoutEnabledRef(settings.scoutEnabled ?? false);
6378
6656
  setShowUsageDetailsRef(settings.showUsageDetails ?? true);
@@ -6381,6 +6659,8 @@ var applyAdvisorSettings = (settings) => {
6381
6659
  setAlwaysOnRef(settings.alwaysOn ?? false);
6382
6660
  setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
6383
6661
  setAdvisorHerdrIntegrationRef(settings.herdrIntegration ?? true);
6662
+ };
6663
+ var applyJevSettings = (settings) => {
6384
6664
  setAdvisorJevFilterEnabledRef(settings.jevFilterEnabled ?? false);
6385
6665
  setAdvisorJevFilterSkipConfidenceRef(settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE);
6386
6666
  setAdvisorJevFilterNoulMarginRef(settings.jevFilterNoulMargin ?? DEFAULT_JEV_FILTER_NOUL_MARGIN);
@@ -6392,6 +6672,8 @@ var applyAdvisorSettings = (settings) => {
6392
6672
  setAdvisorJevTransportRef(settings.jevTransport ?? DEFAULT_JEV_TRANSPORT);
6393
6673
  setAdvisorJevTurnGateEveryTurnsRef(settings.jevTurnGateEveryTurns ?? DEFAULT_JEV_TURN_GATE_EVERY_TURNS);
6394
6674
  setAdvisorJevTurnGateNoulThresholdRef(settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD);
6675
+ };
6676
+ var applyDisclosureSettings = (settings) => {
6395
6677
  setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
6396
6678
  setAdvisorToolResultMaxBytesRef(settings.toolResultMaxBytes ?? 50 * 1024);
6397
6679
  setAdvisorRedactSecretsRef(settings.redactSecrets ?? false);
@@ -6402,6 +6684,11 @@ var applyAdvisorSettings = (settings) => {
6402
6684
  setAdvisorUntrackedContentRef(settings.untrackedContent ?? false);
6403
6685
  setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
6404
6686
  };
6687
+ var applyAdvisorSettings = (settings) => {
6688
+ applySessionSettings(settings);
6689
+ applyJevSettings(settings);
6690
+ applyDisclosureSettings(settings);
6691
+ };
6405
6692
  var saveAdvisorSettings = (ctx, settings) => {
6406
6693
  applyAdvisorSettings(settings);
6407
6694
  const persisted = getPersistedModelRefs();
@@ -6421,10 +6708,12 @@ var registerSettingsCommands = (runtime) => {
6421
6708
  return;
6422
6709
  }
6423
6710
  const initial = getAdvisorSettings();
6424
- await ctx.ui.custom((tui, theme, _keybindings, done) => new AdvisorSettingsSelector({
6711
+ await ctx.ui.custom((tui, theme, keybindings, done) => new AdvisorSettingsSelector({
6425
6712
  effortLevels: EFFORT_LEVELS,
6426
6713
  initial,
6427
- onCancel: () => done(),
6714
+ keybindings,
6715
+ modelRefs: getConfiguredModelRefs(ctx),
6716
+ onCancel: () => done(undefined),
6428
6717
  onChange: (settings) => {
6429
6718
  try {
6430
6719
  saveAdvisorSettings(ctx, settings);
@@ -6484,6 +6773,7 @@ import {
6484
6773
  writeFile
6485
6774
  } from "node:fs/promises";
6486
6775
  import { join as join5 } from "node:path";
6776
+ import { setTimeout as sleep } from "node:timers/promises";
6487
6777
  import { getAgentDir as getAgentDir4 } from "@earendil-works/pi-coding-agent";
6488
6778
  var ADOPTIONS = [
6489
6779
  "followed",
@@ -6499,6 +6789,7 @@ var VALIDATIONS = [
6499
6789
  var MAX_LOG_BYTES = 1024 * 1024;
6500
6790
  var statePath = () => join5(getAgentDir4(), "advisor-outcomes-salt");
6501
6791
  var outcomeLogPath = () => join5(getAgentDir4(), "advisor-outcomes.jsonl");
6792
+ var isErrnoException = (error) => error instanceof Error && ("code" in error);
6502
6793
  var salt = async () => {
6503
6794
  const path = statePath();
6504
6795
  await mkdir(getAgentDir4(), { mode: 448, recursive: true });
@@ -6510,7 +6801,7 @@ var salt = async () => {
6510
6801
  }
6511
6802
  await unlink(path);
6512
6803
  } catch (error) {
6513
- if (error.code !== "ENOENT") {
6804
+ if (!isErrnoException(error) || error.code !== "ENOENT") {
6514
6805
  throw error;
6515
6806
  }
6516
6807
  }
@@ -6521,7 +6812,7 @@ var salt = async () => {
6521
6812
  await link(temporary, path);
6522
6813
  return value;
6523
6814
  } catch (error) {
6524
- if (error.code !== "EEXIST") {
6815
+ if (!isErrnoException(error) || error.code !== "EEXIST") {
6525
6816
  throw error;
6526
6817
  }
6527
6818
  } finally {
@@ -6553,7 +6844,7 @@ var withOutcomeLock = async (run) => {
6553
6844
  }
6554
6845
  }
6555
6846
  } catch (error) {
6556
- if (error.code !== "EEXIST") {
6847
+ if (!isErrnoException(error) || error.code !== "EEXIST") {
6557
6848
  throw error;
6558
6849
  }
6559
6850
  const observed = await stat(lockPath).catch(() => {
@@ -6570,7 +6861,7 @@ var withOutcomeLock = async (run) => {
6570
6861
  }
6571
6862
  continue;
6572
6863
  }
6573
- await new Promise((resolve) => setTimeout(resolve, 5));
6864
+ await sleep(5);
6574
6865
  }
6575
6866
  }
6576
6867
  throw new Error("Timed out waiting to append an Advisor outcome.");
@@ -6596,11 +6887,8 @@ var appendOutcome = async (record) => {
6596
6887
  }
6597
6888
  throw error;
6598
6889
  });
6599
- if (currentBytes + Buffer.byteLength(line) > MAX_LOG_BYTES) {
6600
- await writeFile(path, line, { encoding: "utf8", mode: 384 });
6601
- } else {
6602
- await appendFile(path, line, { encoding: "utf8", mode: 384 });
6603
- }
6890
+ const overflow = currentBytes + Buffer.byteLength(line) > MAX_LOG_BYTES;
6891
+ await (overflow ? writeFile(path, line, { encoding: "utf-8", mode: 384 }) : appendFile(path, line, { encoding: "utf-8", mode: 384 }));
6604
6892
  await chmod(path, 384);
6605
6893
  return next;
6606
6894
  });
@@ -6698,7 +6986,7 @@ var handleJevTurnEnd = async (registration, ctx) => {
6698
6986
  if (interval <= 0 || session.turnsSinceConsultation <= 0 || session.turnsSinceConsultation % interval !== 0) {
6699
6987
  return;
6700
6988
  }
6701
- if (isSimpleMode() || session.blocked || !registration.activeTools().includes("ask_advisor") || !session.canConsult(getAdvisorMaxCallsPerSession())) {
6989
+ if (isSimpleMode() || session.blocked || !registration.activeTools().includes("ask_advisor") || !advisorModelIsAllowed(ctx) || !session.canConsult(getAdvisorMaxCallsPerSession())) {
6702
6990
  return;
6703
6991
  }
6704
6992
  const consult = registration.deps?.consult ?? registration.consult;
@@ -6758,7 +7046,7 @@ var handleJevTurnEnd = async (registration, ctx) => {
6758
7046
  }
6759
7047
  } catch (error) {
6760
7048
  session.recordJevGateFailure();
6761
- if (error instanceof JevFailure) {
7049
+ if (error instanceof JevFailureError) {
6762
7050
  notifyFailureOnce(ctx, error.category, error.message);
6763
7051
  } else if (!ctx.signal?.aborted) {
6764
7052
  notifyFailureOnce(ctx, "error", error instanceof Error ? error.message : String(error));
@@ -6773,9 +7061,7 @@ var registerJevTurnGate = (on, registration) => {
6773
7061
  import { Type } from "typebox";
6774
7062
 
6775
7063
  // src/tools/render-advisor-result.ts
6776
- import {
6777
- getMarkdownTheme as getMarkdownTheme4
6778
- } from "@earendil-works/pi-coding-agent";
7064
+ import { getMarkdownTheme as getMarkdownTheme4 } from "@earendil-works/pi-coding-agent";
6779
7065
  import { Box as Box3, Markdown as Markdown4, Spacer, Text as Text5 } from "@earendil-works/pi-tui";
6780
7066
  var advisorResultDetails = (result) => result.details;
6781
7067
  var syncRenderPhase = (context, phase) => {
@@ -6832,6 +7118,24 @@ var renderPartialAdvisorResult = (box, result, expanded, theme, context) => {
6832
7118
  box.addChild(new Markdown4(adviceForDisplay(details.text, expanded), 0, 0, getMarkdownTheme4()));
6833
7119
  }
6834
7120
  };
7121
+ var thinkingPreview = (details) => details?.thinking?.trim() ? `${details.thinking.slice(0, 300)}${details.thinking.length > 300 ? "…" : ""}` : "";
7122
+ var finalResultLines = (details, advice, theme) => {
7123
+ const lines = [renderAdvisorResponseHeader(hasSoundVerdict(advice), theme)];
7124
+ if (details?.advisor) {
7125
+ lines.push(theme.fg("dim", ` ${details.advisor}`));
7126
+ }
7127
+ if (getAdvisorSettings().showUsageDetails) {
7128
+ const usage = formatAdvisorUsage(details?.usage);
7129
+ if (usage) {
7130
+ lines.push(theme.fg("dim", ` Usage: ${usage}`));
7131
+ }
7132
+ }
7133
+ const attachments = attachmentLabels(details);
7134
+ if (attachments.length) {
7135
+ lines.push(theme.fg("dim", ` ${attachments.join(" · ")}`));
7136
+ }
7137
+ return lines;
7138
+ };
6835
7139
  var renderFinalAdvisorResult = (box, result, expanded, theme, context) => {
6836
7140
  syncRenderPhase(context, "final");
6837
7141
  if (context.state.timerId) {
@@ -6854,21 +7158,8 @@ var renderFinalAdvisorResult = (box, result, expanded, theme, context) => {
6854
7158
  return;
6855
7159
  }
6856
7160
  const advice = details?.text || textFrom(result.content);
6857
- const lines = [renderAdvisorResponseHeader(hasSoundVerdict(advice), theme)];
6858
- if (details?.advisor) {
6859
- lines.push(theme.fg("dim", ` ${details.advisor}`));
6860
- }
6861
- if (getAdvisorSettings().showUsageDetails) {
6862
- const usage = formatAdvisorUsage(details?.usage);
6863
- if (usage) {
6864
- lines.push(theme.fg("dim", ` Usage: ${usage}`));
6865
- }
6866
- }
6867
- const attachments = attachmentLabels(details);
6868
- if (attachments.length) {
6869
- lines.push(theme.fg("dim", ` ${attachments.join(" · ")}`));
6870
- }
6871
- const thinking = details?.thinking?.trim() ? `${details.thinking.slice(0, 300)}${details.thinking.length > 300 ? "…" : ""}` : "";
7161
+ const thinking = thinkingPreview(details);
7162
+ const lines = finalResultLines(details, advice, theme);
6872
7163
  const displayAdvice = advice || "(Advisor returned no advice.)";
6873
7164
  box.addChild(new Text5(lines.join(`
6874
7165
  `), 0, 0));
@@ -6891,6 +7182,12 @@ var renderAdvisorResult = (result, { isPartial, expanded }, theme, context) => {
6891
7182
  };
6892
7183
 
6893
7184
  // src/tools/register-ask-advisor.ts
7185
+ var assertAdvisorModelAccess = (ctx) => {
7186
+ const accessReason = advisorModelAccessReason(ctx);
7187
+ if (accessReason) {
7188
+ throw new Error(accessReason);
7189
+ }
7190
+ };
6894
7191
  var claimTrackedHandoff = (session, includeTrackedFiles) => {
6895
7192
  if (!includeTrackedFiles?.length) {
6896
7193
  return;
@@ -6913,6 +7210,7 @@ var registerAskAdvisorTool = ({
6913
7210
  description: "Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a contextual review; attach an optional draft for concrete plan or completion review. If the Advisor explicitly names a missing file, you may make a sequential follow-up call with includeTrackedFiles when enabled and relevant.",
6914
7211
  async execute(_id, params, signal, onUpdate, ctx) {
6915
7212
  reservedCalls.delete(_id);
7213
+ assertAdvisorModelAccess(ctx);
6916
7214
  if (!(isSimpleMode() || session.canConsult(getAdvisorMaxCallsPerSession()))) {
6917
7215
  throw new Error("Advisor call budget exhausted for this session.");
6918
7216
  }
@@ -6985,7 +7283,22 @@ var registerAskAdvisorTool = ({
6985
7283
  const usage = snapshotAdvisorUsage(result.usage);
6986
7284
  const piUsage = advisorUsageForPi(result.usage);
6987
7285
  updateAdvisorUsageStatus(ctx, session);
6988
- return {
7286
+ const details = {
7287
+ adviceId: result.adviceId,
7288
+ advisor: result.model,
7289
+ draftBytes: result.draftBytes,
7290
+ preferenceBytes: result.preferenceBytes,
7291
+ question: resolveAdvisorRequest(params.question),
7292
+ scout: scoutDetails,
7293
+ text: result.markdown,
7294
+ thinking: result.thinkingText,
7295
+ trackedBytes: result.trackedBytes,
7296
+ untrackedBytes: result.untrackedBytes
7297
+ };
7298
+ if (usage) {
7299
+ details.usage = usage;
7300
+ }
7301
+ const response = {
6989
7302
  content: [
6990
7303
  {
6991
7304
  text: `Advisor (${result.model})
@@ -6994,21 +7307,12 @@ ${result.markdown}`,
6994
7307
  type: "text"
6995
7308
  }
6996
7309
  ],
6997
- details: {
6998
- adviceId: result.adviceId,
6999
- advisor: result.model,
7000
- draftBytes: result.draftBytes,
7001
- preferenceBytes: result.preferenceBytes,
7002
- question: resolveAdvisorRequest(params.question),
7003
- scout: scoutDetails,
7004
- text: result.markdown,
7005
- thinking: result.thinkingText,
7006
- trackedBytes: result.trackedBytes,
7007
- untrackedBytes: result.untrackedBytes,
7008
- ...usage ? { usage } : {}
7009
- },
7010
- ...piUsage ? { usage: piUsage } : {}
7310
+ details
7011
7311
  };
7312
+ if (piUsage) {
7313
+ response.usage = piUsage;
7314
+ }
7315
+ return response;
7012
7316
  } catch (error) {
7013
7317
  coalescedUpdate.flush();
7014
7318
  const message = error instanceof Error ? error.message : String(error);
@@ -7076,27 +7380,31 @@ var sendAutomaticGateCall = (pi, event) => {
7076
7380
  }, { deliverAs: "steer" });
7077
7381
  };
7078
7382
  var sendAutomaticGateFailure = (pi, markdown, usage) => {
7079
- const normalizedUsage = snapshotAdvisorUsage(usage);
7383
+ const details = { text: markdown };
7384
+ if (usage) {
7385
+ details.usage = usage;
7386
+ }
7080
7387
  pi.sendMessage({
7081
7388
  content: markdown,
7082
7389
  customType: "advisor-loop-result",
7083
- details: {
7084
- text: markdown,
7085
- ...normalizedUsage ? { usage: normalizedUsage } : {}
7086
- },
7390
+ details,
7087
7391
  display: true
7088
7392
  }, { deliverAs: "steer" });
7089
7393
  };
7090
7394
  var sendAutomaticGateResult = (pi, result) => {
7395
+ const details = {
7396
+ advisor: result.model,
7397
+ decision: result.decision,
7398
+ text: result.markdown
7399
+ };
7400
+ const normalizedUsage = snapshotAdvisorUsage(result.usage);
7401
+ if (normalizedUsage) {
7402
+ details.usage = normalizedUsage;
7403
+ }
7091
7404
  pi.sendMessage({
7092
7405
  content: adviceForGateText(result),
7093
7406
  customType: "advisor-loop-result",
7094
- details: {
7095
- advisor: result.model,
7096
- decision: result.decision,
7097
- text: result.markdown,
7098
- ...snapshotAdvisorUsage(result.usage) ? { usage: snapshotAdvisorUsage(result.usage) } : {}
7099
- },
7407
+ details,
7100
7408
  display: true
7101
7409
  }, { deliverAs: "steer" });
7102
7410
  };
@@ -7112,7 +7420,7 @@ var applyGateDecision = (pi, ctx, session, result, reason, failureMode) => {
7112
7420
  });
7113
7421
  updateAdvisorUsageStatus(ctx, session);
7114
7422
  const failure = failureEffect(result.category, result.message, ctx, session, failureMode);
7115
- sendAutomaticGateFailure(pi, `**Advisor gate failure (${result.category}):** ${result.message}`, result.usage);
7423
+ sendAutomaticGateFailure(pi, `**Advisor gate failure (${result.category}):** ${result.message}`, snapshotAdvisorUsage(result.usage));
7116
7424
  return failure.block ? { block: true, reason: `${reason}
7117
7425
  ${failure.reason}` } : undefined;
7118
7426
  }
@@ -7139,7 +7447,7 @@ ${failure.reason}` } : undefined;
7139
7447
  return { block: true, reason: gateReason };
7140
7448
  };
7141
7449
  var handleAutomaticGate = async (pi, event, ctx, session, runGate, scoutStatus) => {
7142
- if (isSimpleMode() || event.toolName === "ask_advisor" || !advisorAutoLoopGateRef || !session.recordToolCall(event.toolName, event.input, advisorLoopThresholdRef)) {
7450
+ if (isSimpleMode() || event.toolName === "ask_advisor" || !advisorModelIsAllowed(ctx) || !advisorAutoLoopGateRef || !session.recordToolCall(event.toolName, event.input, advisorLoopThresholdRef)) {
7143
7451
  return;
7144
7452
  }
7145
7453
  const reason = `Advisor loop gate: normalized signature for ${event.toolName} repeated ${advisorLoopThresholdRef} times without a materially different tool action.`;
@@ -7181,6 +7489,16 @@ var handleAutomaticGate = async (pi, event, ctx, session, runGate, scoutStatus)
7181
7489
  };
7182
7490
 
7183
7491
  // src/tools/register-lifecycle.ts
7492
+ var modelAccessBlock = (toolName, ctx) => {
7493
+ const access = advisorModelAccess(ctx);
7494
+ if (access.allowed || toolName !== "ask_advisor") {
7495
+ return;
7496
+ }
7497
+ return {
7498
+ block: true,
7499
+ reason: access.reason
7500
+ };
7501
+ };
7184
7502
  var registerToolLifecycle = ({
7185
7503
  pi,
7186
7504
  reservedCalls,
@@ -7218,6 +7536,9 @@ var registerToolLifecycle = ({
7218
7536
  return;
7219
7537
  }
7220
7538
  loadConfig(ctx);
7539
+ if (!advisorModelAccess(ctx).allowed) {
7540
+ return;
7541
+ }
7221
7542
  const guidelines = advisorInvocationGuidelines();
7222
7543
  const budget = isSimpleMode() ? undefined : session.remainingCalls(getAdvisorMaxCallsPerSession());
7223
7544
  if (budget !== undefined) {
@@ -7245,6 +7566,10 @@ ${guidelines.map((rule) => `- ${rule}`).join(`
7245
7566
  if (!loadConfigOrSkipGating(ctx)) {
7246
7567
  return;
7247
7568
  }
7569
+ const accessBlock = modelAccessBlock(event.toolName, ctx);
7570
+ if (accessBlock) {
7571
+ return accessBlock;
7572
+ }
7248
7573
  const reservation = reserveAdvisorCall(event, ctx, session, reservedCalls);
7249
7574
  if (event.toolName === "ask_advisor") {
7250
7575
  return reservation;
@@ -7338,65 +7663,59 @@ var registerOutcomeTool = ({
7338
7663
  };
7339
7664
 
7340
7665
  // src/tools/register-renderers.ts
7341
- import {
7342
- getMarkdownTheme as getMarkdownTheme5
7343
- } from "@earendil-works/pi-coding-agent";
7666
+ import { getMarkdownTheme as getMarkdownTheme5 } from "@earendil-works/pi-coding-agent";
7344
7667
  import { Box as Box4, Markdown as Markdown5, Text as Text7 } from "@earendil-works/pi-tui";
7668
+ var addUsageLine = (box, details, theme) => {
7669
+ if (!getAdvisorSettings().showUsageDetails) {
7670
+ return;
7671
+ }
7672
+ const usageText = formatAdvisorUsage(details?.usage);
7673
+ if (usageText) {
7674
+ box.addChild(new Text7(theme.fg("dim", ` Usage: ${usageText}`), 0, 0));
7675
+ }
7676
+ };
7677
+ var callQuestionRenderer = (message, _options, theme) => renderAdvisorCallBox(message.details?.question, theme);
7678
+ var scoutResultRenderer = (entry, { expanded }, theme) => {
7679
+ const box = new Box4(1, 1, (text) => theme.bg("customMessageBg", text));
7680
+ renderScoutDetails(box, entry.data, Boolean(expanded), theme);
7681
+ return box;
7682
+ };
7683
+ var turnGateResultRenderer = (message, { expanded }, theme) => {
7684
+ const { details } = message;
7685
+ const box = new Box4(1, 1, (text) => theme.bg("customMessageBg", text));
7686
+ box.addChild(new Text7(theme.fg("warning", theme.bold("◆ ADVISOR · TURN REVIEW")), 0, 0));
7687
+ if (details?.advisor) {
7688
+ box.addChild(new Text7(theme.fg("dim", ` ${details.advisor}`), 0, 0));
7689
+ }
7690
+ addUsageLine(box, details, theme);
7691
+ if (details?.text) {
7692
+ box.addChild(new Markdown5(adviceForDisplay(details.text, Boolean(expanded)), 0, 0, getMarkdownTheme5()));
7693
+ } else {
7694
+ box.addChild(new Text7(theme.fg("error", isString(message.content) ? message.content : "Advisor turn review failed."), 0, 0));
7695
+ }
7696
+ return box;
7697
+ };
7698
+ var loopResultRenderer = (message, { expanded }, theme) => {
7699
+ const { details } = message;
7700
+ const box = new Box4(1, 1, (text) => theme.bg("customMessageBg", text));
7701
+ box.addChild(new Text7(theme.fg("warning", theme.bold(`◆ ADVISOR GATE: ${details?.decision ?? "failure"}`)), 0, 0));
7702
+ if (details?.advisor) {
7703
+ box.addChild(new Text7(theme.fg("dim", ` ${details.advisor}`), 0, 0));
7704
+ }
7705
+ addUsageLine(box, details, theme);
7706
+ if (details?.text) {
7707
+ box.addChild(new Markdown5(adviceForDisplay(details.text, Boolean(expanded)), 0, 0, getMarkdownTheme5()));
7708
+ } else {
7709
+ box.addChild(new Text7(theme.fg("error", isString(message.content) ? message.content : "Advisor gate failed."), 0, 0));
7710
+ }
7711
+ return box;
7712
+ };
7345
7713
  var registerToolRenderers = (pi) => {
7346
- pi.registerEntryRenderer?.("advisor-scout-result", (entry, { expanded }, theme) => {
7347
- const scout = entry.data;
7348
- const box = new Box4(1, 1, (text) => theme.bg("customMessageBg", text));
7349
- renderScoutDetails(box, scout, Boolean(expanded), theme);
7350
- return box;
7351
- });
7352
- pi.registerMessageRenderer?.("advisor-turn-gate-call", (message, _options, theme) => {
7353
- const details = message.details;
7354
- return renderAdvisorCallBox(details?.question, theme);
7355
- });
7356
- pi.registerMessageRenderer?.("advisor-turn-gate-result", (message, { expanded }, theme) => {
7357
- const details = message.details;
7358
- const box = new Box4(1, 1, (text) => theme.bg("customMessageBg", text));
7359
- box.addChild(new Text7(theme.fg("warning", theme.bold("◆ ADVISOR · TURN REVIEW")), 0, 0));
7360
- if (details?.advisor) {
7361
- box.addChild(new Text7(theme.fg("dim", ` ${details.advisor}`), 0, 0));
7362
- }
7363
- if (getAdvisorSettings().showUsageDetails) {
7364
- const usage = formatAdvisorUsage(details?.usage);
7365
- if (usage) {
7366
- box.addChild(new Text7(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
7367
- }
7368
- }
7369
- if (details?.text) {
7370
- box.addChild(new Markdown5(adviceForDisplay(details.text, Boolean(expanded)), 0, 0, getMarkdownTheme5()));
7371
- } else {
7372
- box.addChild(new Text7(theme.fg("error", typeof message.content === "string" ? message.content : "Advisor turn review failed."), 0, 0));
7373
- }
7374
- return box;
7375
- });
7376
- pi.registerMessageRenderer?.("advisor-loop-call", (message, _options, theme) => {
7377
- const details = message.details;
7378
- return renderAdvisorCallBox(details?.question, theme);
7379
- });
7380
- pi.registerMessageRenderer?.("advisor-loop-result", (message, { expanded }, theme) => {
7381
- const details = message.details;
7382
- const box = new Box4(1, 1, (text) => theme.bg("customMessageBg", text));
7383
- box.addChild(new Text7(theme.fg("warning", theme.bold(`◆ ADVISOR GATE: ${details?.decision ?? "failure"}`)), 0, 0));
7384
- if (details?.advisor) {
7385
- box.addChild(new Text7(theme.fg("dim", ` ${details.advisor}`), 0, 0));
7386
- }
7387
- if (getAdvisorSettings().showUsageDetails) {
7388
- const usage = formatAdvisorUsage(details?.usage);
7389
- if (usage) {
7390
- box.addChild(new Text7(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
7391
- }
7392
- }
7393
- if (details?.text) {
7394
- box.addChild(new Markdown5(adviceForDisplay(details.text, Boolean(expanded)), 0, 0, getMarkdownTheme5()));
7395
- } else {
7396
- box.addChild(new Text7(theme.fg("error", typeof message.content === "string" ? message.content : "Advisor gate failed."), 0, 0));
7397
- }
7398
- return box;
7399
- });
7714
+ pi.registerEntryRenderer?.("advisor-scout-result", scoutResultRenderer);
7715
+ pi.registerMessageRenderer?.("advisor-turn-gate-call", callQuestionRenderer);
7716
+ pi.registerMessageRenderer?.("advisor-turn-gate-result", turnGateResultRenderer);
7717
+ pi.registerMessageRenderer?.("advisor-loop-call", callQuestionRenderer);
7718
+ pi.registerMessageRenderer?.("advisor-loop-result", loopResultRenderer);
7400
7719
  };
7401
7720
 
7402
7721
  // src/tools/registration.ts
@@ -7415,20 +7734,23 @@ var registerAdvisorTool = (pi, session = advisorSessionState, dependencies = {})
7415
7734
  registerToolLifecycle(registration);
7416
7735
  registerAskAdvisorTool(registration);
7417
7736
  registerOutcomeTool(registration);
7418
- registerJevTurnGate((event, handler) => pi.on(event, handler), {
7737
+ const turnGate = {
7419
7738
  activeTools: () => pi.getActiveTools(),
7420
7739
  consult: registration.consult,
7421
- ...dependencies.turnGateDeps ? { deps: dependencies.turnGateDeps } : {},
7422
7740
  send: (message) => pi.sendMessage(message, { deliverAs: "steer" }),
7423
7741
  session
7424
- });
7742
+ };
7743
+ if (dependencies.turnGateDeps) {
7744
+ turnGate.deps = dependencies.turnGateDeps;
7745
+ }
7746
+ registerJevTurnGate((event, handler) => pi.on(event, handler), turnGate);
7425
7747
  };
7426
7748
 
7427
7749
  // extensions/index.ts
7428
7750
  var consultAdvisor2 = (...args) => consultAdvisor(...args);
7429
7751
  var parseAutomaticDecision2 = (...args) => parseAutomaticDecision(...args);
7430
7752
  var runAdvisorGate2 = (...args) => runAdvisorGate(...args);
7431
- function extensions_default(pi) {
7753
+ function registerPiAdvisor(pi) {
7432
7754
  const sessionState = new AdvisorSessionState;
7433
7755
  const scoutStatus = new ScoutStatusManager;
7434
7756
  setHerdrBlockedEmitter((active, label) => pi.events.emit("herdr:blocked", { active, label }));
@@ -7442,7 +7764,7 @@ function extensions_default(pi) {
7442
7764
  }
7443
7765
  export {
7444
7766
  consultAdvisor2 as consultAdvisor,
7445
- extensions_default as default,
7767
+ registerPiAdvisor as default,
7446
7768
  parseAutomaticDecision2 as parseAutomaticDecision,
7447
7769
  runAdvisorGate2 as runAdvisorGate
7448
7770
  };