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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +27 -18
  2. package/README.md +9 -8
  3. package/dist/index.js +1968 -1081
  4. package/extensions/index.ts +2 -1
  5. package/package.json +48 -35
  6. package/src/attachments.ts +11 -10
  7. package/src/child-session.ts +15 -0
  8. package/src/commands/activation-preparation.ts +1 -0
  9. package/src/commands/activation.ts +12 -3
  10. package/src/commands/lifecycle.ts +7 -1
  11. package/src/commands/manual-command.ts +7 -1
  12. package/src/commands/manual-consultation.ts +102 -92
  13. package/src/commands/manual-progress.ts +7 -5
  14. package/src/commands/model-commands.ts +1 -1
  15. package/src/commands/model-options.ts +14 -3
  16. package/src/commands/model-picker.ts +1 -0
  17. package/src/commands/registration.ts +1 -0
  18. package/src/commands/renderers.ts +67 -55
  19. package/src/commands/runtime.ts +16 -14
  20. package/src/commands/settings-commands.ts +10 -4
  21. package/src/commands/settings-persistence.ts +16 -2
  22. package/src/commands/types.ts +1 -0
  23. package/src/commands.ts +0 -1
  24. package/src/config/args.ts +1 -1
  25. package/src/config/defaults.ts +8 -1
  26. package/src/config/schema.ts +42 -17
  27. package/src/config/state.ts +12 -3
  28. package/src/config/storage.ts +34 -23
  29. package/src/config/types.ts +16 -3
  30. package/src/config/validation.ts +33 -21
  31. package/src/config.ts +3 -0
  32. package/src/content-utils.ts +24 -4
  33. package/src/conversation.ts +16 -15
  34. package/src/git.ts +10 -3
  35. package/src/herdr-block.ts +83 -0
  36. package/src/herdr-shared.ts +108 -0
  37. package/src/herdr.ts +30 -192
  38. package/src/jev/client.ts +75 -85
  39. package/src/jev/failure.ts +19 -0
  40. package/src/jev/key-store.ts +26 -11
  41. package/src/jev/ledger.ts +58 -64
  42. package/src/jev/questions.ts +26 -23
  43. package/src/jev/state.ts +15 -12
  44. package/src/jev/transport.ts +8 -3
  45. package/src/model-stream.ts +35 -37
  46. package/src/outcomes.ts +15 -11
  47. package/src/pi-settings.ts +4 -9
  48. package/src/preferences.ts +4 -2
  49. package/src/redaction.ts +10 -12
  50. package/src/scout-context.ts +59 -60
  51. package/src/scout-curation.ts +4 -13
  52. package/src/scout-groups.ts +178 -175
  53. package/src/scout-protocol.ts +74 -70
  54. package/src/scout-reconstruct.ts +1 -1
  55. package/src/scout.ts +128 -77
  56. package/src/session-state.ts +64 -49
  57. package/src/tool-result-cap.ts +18 -17
  58. package/src/tools/consult-context.ts +7 -15
  59. package/src/tools/consultation.ts +14 -4
  60. package/src/tools/gate-policy.ts +1 -0
  61. package/src/tools/gate-protocol.ts +36 -23
  62. package/src/tools/jev-filter.ts +43 -41
  63. package/src/tools/jev-turn-gate.ts +29 -9
  64. package/src/tools/loop-gate.ts +31 -20
  65. package/src/tools/model-access.ts +46 -0
  66. package/src/tools/prompts.ts +20 -25
  67. package/src/tools/register-ask-advisor.ts +36 -16
  68. package/src/tools/register-lifecycle.ts +22 -0
  69. package/src/tools/register-outcome.ts +2 -0
  70. package/src/tools/register-renderers.ts +146 -128
  71. package/src/tools/registration.ts +12 -14
  72. package/src/tools/render-advisor-result.ts +35 -22
  73. package/src/tools/render-common.ts +16 -19
  74. package/src/tools/scout-status.ts +42 -27
  75. package/src/tools/types.ts +1 -0
  76. package/src/tools.ts +0 -1
  77. package/src/ui/jev-setup-submenu.ts +65 -59
  78. package/src/ui/manual-dialog-render.ts +1 -0
  79. package/src/ui/manual-dialog.ts +39 -43
  80. package/src/ui/masked-input.ts +5 -11
  81. package/src/ui/model-multi-selector.ts +9 -0
  82. package/src/ui/model-selector-adapter.ts +31 -0
  83. package/src/ui/model-selector.ts +14 -161
  84. package/src/ui/searchable-model-list.ts +230 -0
  85. package/src/ui/settings-formatting.ts +24 -14
  86. package/src/ui/settings-items.ts +38 -1
  87. package/src/ui/settings-list-adapter.ts +15 -12
  88. package/src/ui/settings-mutations.ts +157 -70
  89. package/src/ui/settings-selector.ts +18 -9
  90. package/src/ui/text-setting-submenu.ts +3 -6
  91. package/src/ui/types.ts +14 -0
  92. package/src/ui.ts +0 -1
  93. package/src/usage.ts +26 -19
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,55 @@ 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/child-session.ts
276
+ var isMarkedSubagent = () => process.env.PI_SUBAGENT_CHILD === "1";
277
+ var effectiveExecutorRef = (ctx) => {
278
+ if (isMarkedSubagent() && ctx.model) {
279
+ return `${ctx.model.provider}/${ctx.model.id}`;
280
+ }
281
+ return executorRef;
282
+ };
283
+ var effectiveExecutorEffort = (ctx) => isMarkedSubagent() ? ctx.thinkingLevel : executorEffortRef;
284
+
285
+ // src/content-utils.ts
286
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
287
+ var isRecordOf = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
288
+ var isString = (value) => typeof value === "string";
289
+ var isNumber = (value) => typeof value === "number";
290
+ var isBoolean = (value) => typeof value === "boolean";
291
+ var byteLength = (value) => Buffer.byteLength(value, "utf-8");
292
+ var contentParts = (content) => {
293
+ if (isString(content)) {
294
+ return [content];
295
+ }
296
+ return Array.isArray(content) ? content : [];
297
+ };
298
+ var capUtf8Bytes = (value, maxBytes) => {
299
+ let result = "";
300
+ let used = 0;
301
+ for (const character of value) {
302
+ const characterBytes = byteLength(character);
303
+ if (used + characterBytes > maxBytes) {
304
+ break;
305
+ }
306
+ result += character;
307
+ used += characterBytes;
308
+ }
309
+ return result;
310
+ };
311
+
264
312
  // src/git.ts
265
313
  import { execFileSync } from "node:child_process";
266
314
  var GIT_CONTEXT_LEVELS = ["off", "summary", "full"];
267
- var isValidGitContextLevel = (value) => GIT_CONTEXT_LEVELS.includes(value);
315
+ var GIT_CONTEXT_LEVEL_SET = new Set(GIT_CONTEXT_LEVELS);
316
+ var isValidGitContextLevel = (value) => isString(value) && GIT_CONTEXT_LEVEL_SET.has(value);
268
317
  var LEVEL_RANK = {
269
318
  full: 2,
270
319
  off: 0,
271
320
  summary: 1
272
321
  };
273
322
  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;");
323
+ var escapeRepositoryText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
275
324
  var TRUNCATION_NOTICE = `
276
325
  [Repository context truncated: it exceeded the configured limit.]`;
277
326
  var capRepositoryContext = (value, maxChars) => {
@@ -296,7 +345,7 @@ var deadlineRunner = () => {
296
345
  }
297
346
  return execFileSync("git", args, {
298
347
  cwd,
299
- encoding: "utf8",
348
+ encoding: "utf-8",
300
349
  maxBuffer: GIT_MAX_BUFFER,
301
350
  shell: false,
302
351
  stdio: ["ignore", "pipe", "pipe"],
@@ -369,17 +418,21 @@ ${patch}` : "Patch: (no tracked-file content changes)");
369
418
 
370
419
  // src/config/schema.ts
371
420
  var configuredModelRef = (value) => value?.trim() || undefined;
421
+ var isNonEmptyModel = (model) => typeof model === "string" && model.trim().length > 0;
422
+ var isValidAdvisorModelWhitelist = (value) => Array.isArray(value) && value.every(isNonEmptyModel);
423
+ var ADVISOR_TOOL_POLICY_NAMES = new Set(ADVISOR_TOOL_POLICIES);
372
424
  var isValidAdvisorToolPolicies = (value) => {
373
425
  if (!value || typeof value !== "object" || Array.isArray(value)) {
374
426
  return false;
375
427
  }
376
- return Object.entries(value).every(([toolName, policy]) => toolName.trim().length > 0 && typeof policy === "string" && ADVISOR_TOOL_POLICIES.includes(policy));
428
+ return Object.entries(value).every(([toolName, policy]) => toolName.trim().length > 0 && isString(policy) && ADVISOR_TOOL_POLICY_NAMES.has(policy));
377
429
  };
378
430
  var nonNegativeSafeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
379
431
  var isValidContextMaxChars = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= MAX_CONTEXT_MAX_CHARS;
380
432
  var isValidLoopThreshold = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 2;
381
433
  var isValidMaxCallsPerSession = (value) => nonNegativeSafeInteger(value);
382
- var isValidGateFailureMode = (value) => typeof value === "string" && GATE_FAILURE_MODES.includes(value);
434
+ var GATE_FAILURE_MODE_NAMES = new Set(GATE_FAILURE_MODES);
435
+ var isValidGateFailureMode = (value) => isString(value) && GATE_FAILURE_MODE_NAMES.has(value);
383
436
  var isValidToolResultMaxLines = (value) => nonNegativeSafeInteger(value);
384
437
  var isValidToolResultMaxBytes = (value) => nonNegativeSafeInteger(value);
385
438
  var isValidJevTimeoutMs = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
@@ -387,7 +440,8 @@ var isValidJevDigestMaxChars = (value) => nonNegativeSafeInteger(value);
387
440
  var isValidJevPricePerMtok = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
388
441
  var isValidJevSkipConfidence = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0.5 && value <= 1;
389
442
  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);
443
+ var JEV_TRANSPORT_NAMES = new Set(JEV_TRANSPORTS);
444
+ var isValidJevTransport = (value) => isString(value) && JEV_TRANSPORT_NAMES.has(value);
391
445
  var CONFIG_SCHEMA = {
392
446
  advisor: {
393
447
  accepted: "a provider/model string",
@@ -546,6 +600,13 @@ var CONFIG_SCHEMA = {
546
600
  type: "number",
547
601
  validate: isValidMaxCallsPerSession
548
602
  },
603
+ advisorModelWhitelist: {
604
+ accepted: "an array of non-empty provider/model strings",
605
+ current: () => [...advisorModelWhitelistRef],
606
+ persisted: true,
607
+ type: "array",
608
+ validate: isValidAdvisorModelWhitelist
609
+ },
549
610
  advisorOutcomeLogging: {
550
611
  accepted: "true or false",
551
612
  current: () => advisorOutcomeLoggingRef,
@@ -660,12 +721,13 @@ var CONFIG_SCHEMA = {
660
721
  type: "boolean"
661
722
  }
662
723
  };
663
- var SAVED_CONFIG_KEYS = Object.keys(CONFIG_SCHEMA).filter((key) => CONFIG_SCHEMA[key].persisted);
724
+ var configKeys = Object.keys(CONFIG_SCHEMA);
725
+ var SAVED_CONFIG_KEYS = configKeys.filter((key) => CONFIG_SCHEMA[key].persisted);
664
726
  var SCHEMA_BY_KEY = CONFIG_SCHEMA;
665
727
 
666
728
  // 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);
729
+ var CONFIG_KEYS = new Set(configKeys);
730
+ var keysOfType = (type) => configKeys.filter((key) => SCHEMA_BY_KEY[key].type === type);
669
731
  var BOOLEAN_CONFIG_KEYS = keysOfType("boolean");
670
732
  var STRING_CONFIG_KEYS = keysOfType("string");
671
733
  var invalidConfigValue = (path, key, accepted) => {
@@ -674,14 +736,14 @@ var invalidConfigValue = (path, key, accepted) => {
674
736
  var unknownConfigKeys = (config) => Object.keys(config).filter((key) => !CONFIG_KEYS.has(key));
675
737
  var validateStringValues = (config, path) => {
676
738
  for (const key of STRING_CONFIG_KEYS) {
677
- if (config[key] !== undefined && typeof config[key] !== "string") {
739
+ if (config[key] !== undefined && !isString(config[key])) {
678
740
  invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
679
741
  }
680
742
  }
681
743
  };
682
744
  var validateBooleanValues = (config, path) => {
683
745
  for (const key of BOOLEAN_CONFIG_KEYS) {
684
- if (config[key] !== undefined && typeof config[key] !== "boolean") {
746
+ if (config[key] !== undefined && !isBoolean(config[key])) {
685
747
  invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
686
748
  }
687
749
  }
@@ -694,6 +756,14 @@ var validateNumericValues = (config, path) => {
694
756
  }
695
757
  }
696
758
  };
759
+ var validateArrayValues = (config, path) => {
760
+ for (const key of keysOfType("array")) {
761
+ const isValid = SCHEMA_BY_KEY[key].validate;
762
+ if (config[key] !== undefined && isValid !== undefined && !isValid(config[key])) {
763
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
764
+ }
765
+ }
766
+ };
697
767
  var validateEnumValues = (config, path) => {
698
768
  for (const key of keysOfType("enum")) {
699
769
  const isValid = SCHEMA_BY_KEY[key].validate;
@@ -711,20 +781,20 @@ var validateObjectValues = (config, path) => {
711
781
  }
712
782
  };
713
783
  var validateConfig = (value, path = "advisor.json") => {
714
- if (!value || typeof value !== "object" || Array.isArray(value)) {
784
+ if (!isRecord(value)) {
715
785
  throw new TypeError(`Invalid advisor configuration at ${path}: expected a JSON object.`);
716
786
  }
717
- const config = value;
718
- validateStringValues(config, path);
719
- validateBooleanValues(config, path);
720
- validateNumericValues(config, path);
721
- validateObjectValues(config, path);
722
- validateEnumValues(config, path);
787
+ validateStringValues(value, path);
788
+ validateBooleanValues(value, path);
789
+ validateNumericValues(value, path);
790
+ validateObjectValues(value, path);
791
+ validateArrayValues(value, path);
792
+ validateEnumValues(value, path);
723
793
  return true;
724
794
  };
725
795
 
726
796
  // src/config/args.ts
727
- var ARGUMENT_WHITESPACE = /\s+/;
797
+ var ARGUMENT_WHITESPACE = /\s+/u;
728
798
  var parseArgs = (args) => {
729
799
  let nextExecutor = executorRef;
730
800
  let nextAdvisor = advisorRef;
@@ -753,10 +823,7 @@ var parseArgs = (args) => {
753
823
  // src/config/storage.ts
754
824
  import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
755
825
  import { join } from "node:path";
756
- import {
757
- CONFIG_DIR_NAME,
758
- getAgentDir
759
- } from "@earendil-works/pi-coding-agent";
826
+ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
760
827
 
761
828
  // src/config/defaults.ts
762
829
  var resetDefaults = () => {
@@ -775,6 +842,7 @@ var resetDefaults = () => {
775
842
  setAdvisorAutoLoopGateRef(true);
776
843
  setAdvisorLoopThresholdRef(3);
777
844
  setAdvisorMaxCallsPerSessionRef(undefined);
845
+ setAdvisorModelWhitelistRef([]);
778
846
  setAdvisorSessionSummaryRef(false);
779
847
  setSimpleModeRef(false);
780
848
  setAlwaysOnRef(false);
@@ -830,6 +898,7 @@ var applyConfig = (config) => {
830
898
  applyOptionalConfig(config, "advisorAutoLoopGate", setAdvisorAutoLoopGateRef);
831
899
  applyOptionalConfig(config, "advisorLoopThreshold", setAdvisorLoopThresholdRef);
832
900
  applyOptionalConfig(config, "advisorMaxCallsPerSession", setAdvisorMaxCallsPerSessionRef);
901
+ applyOptionalConfig(config, "advisorModelWhitelist", setAdvisorModelWhitelistRef);
833
902
  applyOptionalConfig(config, "advisorSessionSummary", setAdvisorSessionSummaryRef);
834
903
  applyOptionalConfig(config, "advisorScoutEnabled", setAdvisorScoutEnabledRef);
835
904
  applyOptionalConfig(config, "showUsageDetails", setShowUsageDetailsRef);
@@ -866,14 +935,16 @@ var sameConfigValue = (left, right) => JSON.stringify(left) === JSON.stringify(r
866
935
  var RESERVED_ADVISOR_JSON_KEYS = new Set(["typesafe_api_key"]);
867
936
  var readExistingConfig = (path) => {
868
937
  try {
869
- const parsed = JSON.parse(readFileSync(path, "utf8"));
870
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
938
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
939
+ return isRecord(parsed) ? parsed : {};
871
940
  } catch {
872
941
  return {};
873
942
  }
874
943
  };
875
944
  var shouldPersistConfigKey = (key, persistAdvisor, persistExecutor) => (key !== "advisor" || persistAdvisor) && (key !== "executor" || persistExecutor);
876
- var applyChangedConfigValues = (data, current, changedKeys, persistAdvisor, persistExecutor) => {
945
+ var applyChangedConfigValues = (existing, current, changedKeys, persistAdvisor, persistExecutor) => {
946
+ const dropped = new Set;
947
+ const data = { ...existing };
877
948
  for (const key of changedKeys) {
878
949
  if (!shouldPersistConfigKey(key, persistAdvisor, persistExecutor)) {
879
950
  continue;
@@ -881,16 +952,26 @@ var applyChangedConfigValues = (data, current, changedKeys, persistAdvisor, pers
881
952
  const value = current[key];
882
953
  const isEmptyModelRef = (key === "advisor" || key === "executor") && !value;
883
954
  if (value === undefined || isEmptyModelRef) {
884
- delete data[key];
955
+ dropped.add(key);
885
956
  } else {
886
957
  data[key] = value;
887
958
  }
888
959
  }
960
+ if (dropped.size === 0) {
961
+ return data;
962
+ }
963
+ const retained = {};
964
+ for (const key of Object.keys(data)) {
965
+ if (!dropped.has(key)) {
966
+ retained[key] = data[key];
967
+ }
968
+ }
969
+ return retained;
889
970
  };
890
971
  var loadedConfigState;
891
972
  var loadedConfigPath;
892
973
  var readConfig = (path) => {
893
- const config = JSON.parse(readFileSync(path, "utf8"));
974
+ const config = JSON.parse(readFileSync(path, "utf-8"));
894
975
  validateConfig(config, path);
895
976
  return config;
896
977
  };
@@ -948,17 +1029,16 @@ var saveConfig = (_ctx, options = {}) => {
948
1029
  if (changedKeys.length === 0) {
949
1030
  return path;
950
1031
  }
951
- const data = { ...existing };
952
- applyChangedConfigValues(data, current, changedKeys, persistAdvisor, persistExecutor);
1032
+ const data = applyChangedConfigValues(existing, current, changedKeys, persistAdvisor, persistExecutor);
953
1033
  writeFileSync(path, `${JSON.stringify(data, null, 2)}
954
1034
  `);
955
1035
  resetConfigCache();
956
1036
  const nextLoadedState = { ...current };
957
1037
  if (!persistAdvisor) {
958
- nextLoadedState.advisor = baseline ? baseline.advisor : configuredModelRef(typeof existing.advisor === "string" ? existing.advisor : undefined);
1038
+ nextLoadedState.advisor = baseline ? baseline.advisor : configuredModelRef(isString(existing.advisor) ? existing.advisor : undefined);
959
1039
  }
960
1040
  if (!persistExecutor) {
961
- nextLoadedState.executor = baseline ? baseline.executor : configuredModelRef(typeof existing.executor === "string" ? existing.executor : undefined);
1041
+ nextLoadedState.executor = baseline ? baseline.executor : configuredModelRef(isString(existing.executor) ? existing.executor : undefined);
962
1042
  }
963
1043
  loadedConfigState = nextLoadedState;
964
1044
  loadedConfigPath = path;
@@ -998,7 +1078,7 @@ var selectedEffort = (choice) => {
998
1078
  return effort === DEFAULT_EFFORT_LEVEL ? undefined : effort;
999
1079
  };
1000
1080
  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+/;
1081
+ var ARGUMENT_WHITESPACE2 = /\s+/u;
1002
1082
  var hasModelOverride = (args, key) => args.trim().split(ARGUMENT_WHITESPACE2).some((token) => {
1003
1083
  const [tokenKey, value] = token.split("=");
1004
1084
  return tokenKey === key && Boolean(value);
@@ -1045,11 +1125,18 @@ var findConfiguredModel = (ctx, ref) => {
1045
1125
  return ctx.modelRegistry.find(provider, modelId);
1046
1126
  };
1047
1127
  var getAvailableModelRefs = (ctx) => {
1048
- if (typeof ctx.modelRegistry.getAvailable !== "function") {
1128
+ if (!ctx.modelRegistry.getAvailable) {
1049
1129
  return;
1050
1130
  }
1051
1131
  return ctx.modelRegistry.getAvailable().map((model) => `${model.provider}/${model.id}`);
1052
1132
  };
1133
+ var getConfiguredModelRefs = (ctx) => {
1134
+ const registry = ctx.modelRegistry;
1135
+ const models = registry?.getAvailable ? registry.getAvailable() : [];
1136
+ return [
1137
+ ...new Set(models.map((model) => `${model.provider}/${model.id}`))
1138
+ ].toSorted((left, right) => left.localeCompare(right));
1139
+ };
1053
1140
  var isSelectableModel = (ctx, ref, availableRefs) => {
1054
1141
  if (!ref) {
1055
1142
  return false;
@@ -1084,18 +1171,39 @@ var planActivationModels = (ctx, executor, advisor, pendingExecutorRef, persiste
1084
1171
  };
1085
1172
  };
1086
1173
 
1087
- // src/ui/model-selector.ts
1088
- import {
1089
- fuzzyFilter,
1090
- Input,
1091
- truncateToWidth
1092
- } from "@earendil-works/pi-tui";
1174
+ // src/ui/model-selector-adapter.ts
1175
+ class ModelSelectorAdapter {
1176
+ list;
1177
+ constructor(list) {
1178
+ this.list = list;
1179
+ }
1180
+ get focused() {
1181
+ return this.list.focused;
1182
+ }
1183
+ set focused(value) {
1184
+ this.list.focused = value;
1185
+ }
1186
+ invalidate() {
1187
+ this.list.invalidate();
1188
+ }
1189
+ render(width) {
1190
+ return this.list.render(width);
1191
+ }
1192
+ handleInput(keyData) {
1193
+ this.list.handleInput(keyData);
1194
+ }
1195
+ }
1093
1196
 
1094
- class SearchableModelSelector {
1197
+ // src/ui/searchable-model-list.ts
1198
+ import { fuzzyFilter, Input, truncateToWidth } from "@earendil-works/pi-tui";
1199
+
1200
+ class SearchableModelList {
1095
1201
  tui;
1096
1202
  searchInput;
1097
1203
  allOptions;
1098
1204
  currentOption;
1205
+ multiSelect;
1206
+ selected = new Set;
1099
1207
  filteredOptions;
1100
1208
  selectedIndex = 0;
1101
1209
  title;
@@ -1112,13 +1220,30 @@ class SearchableModelSelector {
1112
1220
  this.searchInput.focused = val;
1113
1221
  }
1114
1222
  constructor(options) {
1223
+ this.multiSelect = options.multiSelect;
1115
1224
  this.tui = options.tui;
1116
1225
  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;
1226
+ const [requestedCurrentOption] = options.currentOptions;
1227
+ this.currentOption = !this.multiSelect && requestedCurrentOption && options.allOptions.includes(requestedCurrentOption) ? requestedCurrentOption : undefined;
1228
+ let allOptions;
1229
+ if (this.multiSelect) {
1230
+ allOptions = [...new Set(options.allOptions)].toSorted((left, right) => left.localeCompare(right));
1231
+ } else if (this.currentOption) {
1232
+ allOptions = [
1233
+ this.currentOption,
1234
+ ...options.allOptions.filter((item) => item !== this.currentOption)
1235
+ ];
1236
+ } else {
1237
+ allOptions = [...options.allOptions];
1238
+ }
1239
+ this.allOptions = allOptions;
1240
+ if (this.multiSelect) {
1241
+ for (const value of options.currentOptions) {
1242
+ if (this.allOptions.includes(value)) {
1243
+ this.selected.add(value);
1244
+ }
1245
+ }
1246
+ }
1122
1247
  this.theme = options.theme;
1123
1248
  this.keybindings = options.keybindings;
1124
1249
  this.onSelect = options.onSelect;
@@ -1133,8 +1258,7 @@ class SearchableModelSelector {
1133
1258
  const lines = [this.theme.fg("border", "─".repeat(width))];
1134
1259
  lines.push(` ${this.theme.fg("accent", this.theme.bold(this.title))}`);
1135
1260
  const inputLines = this.searchInput.render(Math.max(1, width - 10));
1136
- lines.push(` ${this.theme.fg("accent", "Search: ")}${inputLines[0] || ""}`);
1137
- lines.push("");
1261
+ lines.push(` ${this.theme.fg("accent", "Search: ")}${inputLines[0] || ""}`, "");
1138
1262
  const query = this.searchInput.getValue().trim();
1139
1263
  this.filteredOptions = query ? fuzzyFilter(this.allOptions, query, (item) => item) : this.allOptions;
1140
1264
  this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredOptions.length - 1));
@@ -1146,21 +1270,13 @@ class SearchableModelSelector {
1146
1270
  const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
1147
1271
  const endIndex = Math.min(startIndex + maxVisible, total);
1148
1272
  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
- }
1273
+ lines.push(this.renderOption(this.filteredOptions[i], i));
1156
1274
  }
1157
1275
  if (total > maxVisible) {
1158
- lines.push(" " + this.theme.fg("muted", ` (${this.selectedIndex + 1}/${total})`));
1276
+ lines.push(` ${this.theme.fg("muted", ` (${this.selectedIndex + 1}/${total})`)}`);
1159
1277
  }
1160
1278
  }
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)));
1279
+ lines.push("", ` ${this.theme.fg("dim", this.interactionHint())}`, this.theme.fg("border", "─".repeat(width)));
1164
1280
  return lines.map((line) => truncateToWidth(line, width));
1165
1281
  }
1166
1282
  handleInput(keyData) {
@@ -1172,10 +1288,27 @@ class SearchableModelSelector {
1172
1288
  this.moveSelection(1);
1173
1289
  return;
1174
1290
  }
1291
+ if (this.multiSelect && keyData === " ") {
1292
+ const item = this.filteredOptions[this.selectedIndex];
1293
+ if (item) {
1294
+ if (this.selected.has(item)) {
1295
+ this.selected.delete(item);
1296
+ } else {
1297
+ this.selected.add(item);
1298
+ }
1299
+ }
1300
+ this.tui.requestRender();
1301
+ return;
1302
+ }
1175
1303
  if (this.matchesAction(keyData, "tui.select.confirm", `
1176
1304
  `) || keyData === "\r") {
1177
- if (this.filteredOptions.length > 0) {
1178
- this.onSelect(this.filteredOptions[this.selectedIndex]);
1305
+ if (this.multiSelect) {
1306
+ this.onSelect(this.allOptions.filter((item) => this.selected.has(item)));
1307
+ } else {
1308
+ const selectedOption = this.filteredOptions[this.selectedIndex];
1309
+ if (selectedOption !== undefined) {
1310
+ this.onSelect([selectedOption]);
1311
+ }
1179
1312
  }
1180
1313
  return;
1181
1314
  }
@@ -1187,6 +1320,19 @@ class SearchableModelSelector {
1187
1320
  this.selectedIndex = 0;
1188
1321
  this.tui.requestRender();
1189
1322
  }
1323
+ interactionHint() {
1324
+ return this.multiSelect ? "Type to search · ↑↓: navigate · Space: toggle · Enter: apply · Esc: cancel" : "Type to search · ↑↓: navigate · Enter: select · Esc: cancel";
1325
+ }
1326
+ renderOption(item, index) {
1327
+ let tick = " ";
1328
+ if (this.multiSelect ? this.selected.has(item) : item === this.currentOption) {
1329
+ tick = "✓ ";
1330
+ }
1331
+ if (index === this.selectedIndex) {
1332
+ return ` ${this.theme.fg("accent", `→ ${tick}${item}`)}`;
1333
+ }
1334
+ return ` ${this.theme.fg("text", `${tick}${item}`)}`;
1335
+ }
1190
1336
  matchesAction(keyData, action, fallback) {
1191
1337
  return this.keybindings.matches(keyData, action) || keyData === fallback;
1192
1338
  }
@@ -1206,6 +1352,22 @@ class SearchableModelSelector {
1206
1352
  }
1207
1353
  }
1208
1354
 
1355
+ // src/ui/model-selector.ts
1356
+ class SearchableModelSelector extends ModelSelectorAdapter {
1357
+ constructor(options) {
1358
+ super(new SearchableModelList({
1359
+ ...options,
1360
+ currentOptions: options.currentOption ? [options.currentOption] : [],
1361
+ multiSelect: false,
1362
+ onSelect: ([value]) => {
1363
+ if (value !== undefined) {
1364
+ options.onSelect(value);
1365
+ }
1366
+ }
1367
+ }));
1368
+ }
1369
+ }
1370
+
1209
1371
  // src/commands/model-picker.ts
1210
1372
  var selectAdvisorModels = async (ctx, options) => {
1211
1373
  if (!ctx.hasUI) {
@@ -1268,43 +1430,20 @@ var selectAdvisorModels = async (ctx, options) => {
1268
1430
  return { advisor, advisorEffort, executor, executorEffort };
1269
1431
  };
1270
1432
 
1271
- // src/herdr.ts
1433
+ // src/herdr-shared.ts
1272
1434
  import net from "node:net";
1273
1435
 
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
1436
  // src/redaction.ts
1298
1437
  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;
1438
+ var PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/giu;
1439
+ var PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/iu;
1301
1440
  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
1441
+ /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/giu,
1442
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/giu,
1443
+ /\b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s"'&,;)}\]]+)/giu,
1444
+ /(?<scheme>[a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/giu,
1445
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/gu,
1446
+ /\b(?:aws_secret_access_key|aws_session_token)\s*[:=]\s*[^\s"'&,;)}\]]+/giu
1308
1447
  ];
1309
1448
  var redactUnterminatedPem = (value) => {
1310
1449
  const begins = [...value.matchAll(PEM_BEGIN_PATTERN)];
@@ -1318,7 +1457,7 @@ var redactUnterminatedPem = (value) => {
1318
1457
  var redactSecrets = (value) => {
1319
1458
  let redacted = redactUnterminatedPem(value);
1320
1459
  for (const pattern of SECRET_PATTERNS) {
1321
- redacted = redacted.replace(pattern, (_match, scheme) => typeof scheme === "string" ? `${scheme}${REDACTION_MARKER}@` : REDACTION_MARKER);
1460
+ redacted = redacted.replace(pattern, (_match, scheme) => isString(scheme) ? `${scheme}${REDACTION_MARKER}@` : REDACTION_MARKER);
1322
1461
  }
1323
1462
  return redacted;
1324
1463
  };
@@ -1327,11 +1466,11 @@ var redactAndCapText = (value, maxBytes, redact = true) => {
1327
1466
  return capUtf8Bytes(source, maxBytes);
1328
1467
  };
1329
1468
 
1330
- // src/herdr.ts
1469
+ // src/herdr-shared.ts
1331
1470
  var HERDR_NOTIFICATION_METHOD = "notification.show";
1332
1471
  var SOURCE = "pi-advisor:advisor-activity";
1333
- var BLOCK_SOURCE = "pi-advisor:advisor-block";
1334
1472
  var NOTIFICATION_SOURCE = "pi-advisor:advisor-notification";
1473
+ var BLOCK_SOURCE = "pi-advisor:advisor-block";
1335
1474
  var HERDR_PI_SOURCE = "herdr:pi";
1336
1475
  var sequence = Date.now() * 1000;
1337
1476
  var nextSequence = () => {
@@ -1348,17 +1487,7 @@ var safeEmitBlocked = (active, label = "Advisor blocked") => {
1348
1487
  } catch {}
1349
1488
  };
1350
1489
  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
- });
1490
+ var cleanNotification = (value, max) => [...redactSecrets(value)].map((character) => isControlCharacter(character) ? " " : character).join("").replaceAll(/\s+/gu, " ").trim().slice(0, max);
1362
1491
  var sendToHerdr = (request) => {
1363
1492
  if (process.env.HERDR_ENV !== "1") {
1364
1493
  return;
@@ -1378,61 +1507,18 @@ var sendToHerdr = (request) => {
1378
1507
  socket.once("error", () => socket.destroy());
1379
1508
  socket.once("close", () => clearTimeout(timeout));
1380
1509
  };
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
- };
1510
+ var createHerdrNotificationRequest = (title, body) => ({
1511
+ id: `${NOTIFICATION_SOURCE}:${nextSequence()}`,
1512
+ method: HERDR_NOTIFICATION_METHOD,
1513
+ params: {
1514
+ body: cleanNotification(body, 240),
1515
+ position: "top-left",
1516
+ sound: "request",
1517
+ title: cleanNotification(title, 80)
1433
1518
  }
1434
- }
1519
+ });
1435
1520
 
1521
+ // src/herdr-block.ts
1436
1522
  class HerdrAdvisorBlock {
1437
1523
  #blocked = false;
1438
1524
  report;
@@ -1492,6 +1578,60 @@ class HerdrAdvisorBlock {
1492
1578
  } catch {}
1493
1579
  }
1494
1580
  }
1581
+
1582
+ // src/herdr.ts
1583
+ var metadataRequest = (clear) => ({
1584
+ id: `${SOURCE}:${nextSequence()}`,
1585
+ method: "pane.report_metadata",
1586
+ params: {
1587
+ agent: "pi",
1588
+ applies_to_source: HERDR_PI_SOURCE,
1589
+ pane_id: process.env.HERDR_PANE_ID ?? "",
1590
+ source: SOURCE,
1591
+ ...clear ? { clear_state_labels: true } : { state_labels: { working: "seeking advice" } },
1592
+ seq: nextSequence()
1593
+ }
1594
+ });
1595
+
1596
+ class HerdrAdvisorActivity {
1597
+ #activeConsultations = 0;
1598
+ report;
1599
+ enabled;
1600
+ constructor(report = sendToHerdr, enabled = () => true) {
1601
+ this.report = report;
1602
+ this.enabled = enabled;
1603
+ }
1604
+ start() {
1605
+ if (!this.enabled()) {
1606
+ return;
1607
+ }
1608
+ this.#activeConsultations += 1;
1609
+ if (this.#activeConsultations === 1) {
1610
+ this.safeReport(false);
1611
+ }
1612
+ }
1613
+ finish() {
1614
+ if (this.#activeConsultations === 0) {
1615
+ return;
1616
+ }
1617
+ this.#activeConsultations -= 1;
1618
+ if (this.#activeConsultations === 0) {
1619
+ this.safeReport(true);
1620
+ }
1621
+ }
1622
+ clear() {
1623
+ if (this.#activeConsultations === 0) {
1624
+ return;
1625
+ }
1626
+ this.#activeConsultations = 0;
1627
+ this.safeReport(true);
1628
+ }
1629
+ safeReport(clear) {
1630
+ try {
1631
+ this.report(metadataRequest(clear));
1632
+ } catch {}
1633
+ }
1634
+ }
1495
1635
  var notifyHerdrAdvisorFailure = (title, body) => {
1496
1636
  if (!getAdvisorSettings().herdrIntegration) {
1497
1637
  return;
@@ -1507,15 +1647,14 @@ var herdrAdvisorBlock = new HerdrAdvisorBlock(sendToHerdr, () => getAdvisorSetti
1507
1647
  import { randomUUID } from "node:crypto";
1508
1648
 
1509
1649
  // src/model-stream.ts
1510
- import {
1511
- stream
1512
- } from "@earendil-works/pi-ai/compat";
1650
+ import { stream } from "@earendil-works/pi-ai/compat";
1513
1651
  var resolveConfiguredModel = async (ctx, ref, label) => {
1514
1652
  if (!ref) {
1515
1653
  throw new Error(`${label} model not configured`);
1516
1654
  }
1517
1655
  const [provider, modelId] = splitRef(ref);
1518
- const model = ctx.modelRegistry.find(provider, modelId);
1656
+ const lookup = [provider, modelId];
1657
+ const model = ctx.modelRegistry.find(...lookup);
1519
1658
  if (!model) {
1520
1659
  throw new Error(`${label} model not found: ${ref}`);
1521
1660
  }
@@ -1535,16 +1674,16 @@ var resolveConfiguredModel = async (ctx, ref, label) => {
1535
1674
  };
1536
1675
  };
1537
1676
  var ADVISOR_STREAM_UPDATE_INTERVAL_MS = 90;
1538
- var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVAL_MS, scheduler = {
1677
+ var defaultScheduler = {
1539
1678
  clearTimeout,
1540
1679
  now: Date.now,
1541
1680
  setTimeout: (callback, delay) => setTimeout(callback, delay)
1542
- }) => {
1681
+ };
1682
+ var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVAL_MS, scheduler = defaultScheduler) => {
1543
1683
  if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
1544
1684
  throw new Error("Coalesced update interval must be positive and finite.");
1545
1685
  }
1546
1686
  let closed = false;
1547
- let hasPending = false;
1548
1687
  let pending;
1549
1688
  let lastPublishedAt;
1550
1689
  let timer;
@@ -1558,12 +1697,11 @@ var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVA
1558
1697
  };
1559
1698
  const publishPending = () => {
1560
1699
  timer = undefined;
1561
- if (!hasPending) {
1700
+ if (!pending) {
1562
1701
  return;
1563
1702
  }
1564
- const value = pending;
1703
+ const { value } = pending;
1565
1704
  pending = undefined;
1566
- hasPending = false;
1567
1705
  lastPublishedAt = scheduler.now();
1568
1706
  try {
1569
1707
  publish(value);
@@ -1589,7 +1727,6 @@ var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVA
1589
1727
  closed = true;
1590
1728
  clearTimer();
1591
1729
  pending = undefined;
1592
- hasPending = false;
1593
1730
  },
1594
1731
  flush: () => {
1595
1732
  if (!closed) {
@@ -1606,8 +1743,7 @@ var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVA
1606
1743
  if (publishFailed) {
1607
1744
  throw publishError;
1608
1745
  }
1609
- pending = value;
1610
- hasPending = true;
1746
+ pending = { value };
1611
1747
  if (timer === undefined) {
1612
1748
  schedule();
1613
1749
  }
@@ -1620,14 +1756,17 @@ var createCoalescedUpdate = (publish, intervalMs = ADVISOR_STREAM_UPDATE_INTERVA
1620
1756
  var collectTextStream = async (resolved, options, streamModel = stream) => {
1621
1757
  let thinking = "";
1622
1758
  let text = "";
1623
- const eventStream = streamModel(resolved.model, { messages: options.messages, systemPrompt: options.systemPrompt }, {
1759
+ const streamOptions = {
1624
1760
  apiKey: resolved.apiKey,
1625
1761
  env: resolved.env,
1626
1762
  headers: resolved.headers,
1627
1763
  reasoning: options.reasoning,
1628
- ...options.reasoning === undefined ? {} : { reasoningEffort: options.reasoning },
1629
1764
  signal: options.signal
1630
- });
1765
+ };
1766
+ if (options.reasoning !== undefined) {
1767
+ streamOptions.reasoningEffort = options.reasoning;
1768
+ }
1769
+ const eventStream = streamModel(resolved.model, { messages: options.messages, systemPrompt: options.systemPrompt }, streamOptions);
1631
1770
  for await (const event of eventStream) {
1632
1771
  if (event.type === "thinking_delta") {
1633
1772
  thinking += event.delta;
@@ -1661,21 +1800,21 @@ import { lstat, open, realpath } from "node:fs/promises";
1661
1800
  import { isAbsolute, relative, resolve } from "node:path";
1662
1801
  var ADVISOR_FILE_MAX_BYTES = 8 * 1024;
1663
1802
  var ADVISOR_FILES_TOTAL_MAX_BYTES = 24 * 1024;
1664
- var PATH_SEGMENTS = /[\\/]/;
1803
+ var PATH_SEGMENTS = /[\\/]/u;
1665
1804
  var within = (root, candidate) => {
1666
1805
  const path = relative(root, candidate);
1667
1806
  return path !== "" && !path.startsWith("..") && !path.includes("../");
1668
1807
  };
1669
1808
  var normalizeRelativePath = (root, path) => relative(root, resolve(root, path));
1670
1809
  var normalizeRequestedPath = (root, value) => {
1671
- if (typeof value !== "string" || !value || isAbsolute(value) || value.split(PATH_SEGMENTS).includes("..")) {
1810
+ if (!isString(value) || !value || isAbsolute(value) || value.split(PATH_SEGMENTS).includes("..")) {
1672
1811
  return;
1673
1812
  }
1674
1813
  return normalizeRelativePath(root, value);
1675
1814
  };
1676
1815
  var git = (cwd, args) => execFileSync2("git", args, {
1677
1816
  cwd,
1678
- encoding: "utf8",
1817
+ encoding: "utf-8",
1679
1818
  maxBuffer: 16 * 1024 * 1024,
1680
1819
  shell: false,
1681
1820
  stdio: ["ignore", "pipe", "pipe"],
@@ -1686,7 +1825,7 @@ var repositoryRoot = (cwd) => {
1686
1825
  try {
1687
1826
  return realpath(git(cwd, ["rev-parse", "--show-toplevel"]).trim());
1688
1827
  } catch {
1689
- return Promise.resolve(undefined);
1828
+ return Promise.resolve();
1690
1829
  }
1691
1830
  };
1692
1831
  var untracked = (cwd, path) => {
@@ -1726,7 +1865,7 @@ var readAttachment = async (root, normalizedName, redact, available) => {
1726
1865
  if (!within(root, resolved)) {
1727
1866
  return;
1728
1867
  }
1729
- const flags = constants.O_NOFOLLOW ? constants.O_RDONLY | constants.O_NOFOLLOW : constants.O_RDONLY;
1868
+ const flags = constants.O_NOFOLLOW ? constants.O_RDONLY + constants.O_NOFOLLOW : constants.O_RDONLY;
1730
1869
  const file = await open(resolved, flags);
1731
1870
  try {
1732
1871
  const openedStats = await file.stat();
@@ -1735,13 +1874,13 @@ var readAttachment = async (root, normalizedName, redact, available) => {
1735
1874
  }
1736
1875
  const buffer = Buffer.alloc(available + 1);
1737
1876
  const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
1738
- const raw = buffer.subarray(0, bytesRead).toString("utf8");
1877
+ const raw = buffer.subarray(0, bytesRead).toString("utf-8");
1739
1878
  if (raw.includes("\x00")) {
1740
1879
  return;
1741
1880
  }
1742
1881
  const text = redactAndCapText(raw, available, redact);
1743
1882
  return {
1744
- bytes: Buffer.byteLength(text, "utf8"),
1883
+ bytes: Buffer.byteLength(text, "utf-8"),
1745
1884
  path: normalizedName,
1746
1885
  text
1747
1886
  };
@@ -1814,9 +1953,9 @@ var readProjectPreferences = async (ctx, maxBytes = PREFERENCES_MAX_BYTES, redac
1814
1953
  try {
1815
1954
  const buffer = Buffer.alloc(maxBytes + 1);
1816
1955
  const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
1817
- const source = buffer.subarray(0, bytesRead).toString("utf8");
1956
+ const source = buffer.subarray(0, bytesRead).toString("utf-8");
1818
1957
  const capped = redactAndCapText(source, maxBytes, redact);
1819
- return { bytes: Buffer.byteLength(capped, "utf8"), text: capped };
1958
+ return { bytes: Buffer.byteLength(capped, "utf-8"), text: capped };
1820
1959
  } finally {
1821
1960
  await file.close();
1822
1961
  }
@@ -1828,6 +1967,19 @@ import { createHash } from "node:crypto";
1828
1967
 
1829
1968
  // src/tool-result-cap.ts
1830
1969
  var OMITTED_MARKER = "[... omitted tool-result section ...]";
1970
+ var collect = (candidates, maxEntries, maxContentBytes) => {
1971
+ const selected = [];
1972
+ let used = 0;
1973
+ for (const line of candidates.slice(0, maxEntries)) {
1974
+ const next = used + byteLength(line) + (selected.length ? 1 : 0);
1975
+ if (next > maxContentBytes) {
1976
+ break;
1977
+ }
1978
+ selected.push(line);
1979
+ used = next;
1980
+ }
1981
+ return selected;
1982
+ };
1831
1983
  var capToolResult = (value, maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES, maxBytes = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES) => {
1832
1984
  const lines = value.split(`
1833
1985
  `);
@@ -1863,19 +2015,6 @@ var capToolResult = (value, maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES, ma
1863
2015
  }
1864
2016
  const headCount = Math.floor((maxLines - 1) / 2);
1865
2017
  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
2018
  const availableBytes = maxBytes - markerBytes - 2;
1880
2019
  const head = collect(lines, headCount, Math.floor(availableBytes / 2));
1881
2020
  const tail = collect(lines.slice(Math.max(head.length, lines.length - tailCount)), tailCount, availableBytes - byteLength(head.join(`
@@ -1893,13 +2032,13 @@ var capToolResult = (value, maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES, ma
1893
2032
 
1894
2033
  // src/conversation.ts
1895
2034
  var textFromPart = (part) => {
1896
- if (typeof part === "string") {
2035
+ if (isString(part)) {
1897
2036
  return part;
1898
2037
  }
1899
- if (!isRecord(part) || part.type !== "text") {
2038
+ if (!isRecordOf(part) || part.type !== "text") {
1900
2039
  return "";
1901
2040
  }
1902
- return typeof part.text === "string" ? part.text : "";
2041
+ return isString(part.text) ? part.text : "";
1903
2042
  };
1904
2043
  var textFrom = (content) => contentParts(content).map(textFromPart).join(`
1905
2044
  `).trim();
@@ -1910,10 +2049,10 @@ var assistantEntry = (message, policies, redact) => {
1910
2049
  parts.push(redact ? redactSecrets(text) : text);
1911
2050
  }
1912
2051
  for (const part of contentParts(message.content)) {
1913
- if (!isRecord(part) || part.type !== "toolCall") {
2052
+ if (!isRecordOf(part) || part.type !== "toolCall") {
1914
2053
  continue;
1915
2054
  }
1916
- const toolName = typeof part.name === "string" ? part.name : "unknown";
2055
+ const toolName = isString(part.name) ? part.name : "unknown";
1917
2056
  const policy = policies[toolName] ?? "full";
1918
2057
  if (policy === "exclude") {
1919
2058
  parts.push(`[Tool Call: ${toolName}] (excluded by Advisor tool policy)`);
@@ -1931,15 +2070,15 @@ var assistantEntry = (message, policies, redact) => {
1931
2070
  };
1932
2071
  var toolResultEntry = (message, toolResultMaxLines, toolResultMaxBytes, policies, redact) => {
1933
2072
  const status = message.isError ? "error" : "success";
1934
- const toolName = typeof message.toolName === "string" ? message.toolName : "unknown";
2073
+ const toolName = isString(message.toolName) ? message.toolName : "unknown";
1935
2074
  const policy = policies[toolName] ?? "full";
1936
2075
  const source = textFrom(message.content);
1937
2076
  if (policy === "exclude") {
1938
2077
  return `[Tool Result for ${toolName}] (excluded by Advisor tool policy)`;
1939
2078
  }
1940
2079
  if (policy === "summary") {
1941
- const capped = capToolResult(source, toolResultMaxLines, toolResultMaxBytes);
1942
- return `[Tool Result for ${toolName}] (output omitted by Advisor tool policy: summary; status: ${status}; ${capped.totalLines} lines, ${capped.totalBytes} bytes; source output was${capped.truncated ? "" : " not"} truncated)`;
2080
+ const capped2 = capToolResult(source, toolResultMaxLines, toolResultMaxBytes);
2081
+ return `[Tool Result for ${toolName}] (output omitted by Advisor tool policy: summary; status: ${status}; ${capped2.totalLines} lines, ${capped2.totalBytes} bytes; source output was${capped2.truncated ? "" : " not"} truncated)`;
1943
2082
  }
1944
2083
  const disclosed = redact ? redactSecrets(source) : source;
1945
2084
  const capped = capToolResult(disclosed, toolResultMaxLines, toolResultMaxBytes);
@@ -1947,13 +2086,13 @@ var toolResultEntry = (message, toolResultMaxLines, toolResultMaxBytes, policies
1947
2086
  ${capped.content}`;
1948
2087
  };
1949
2088
  var conversationEntry = (entry, toolResultMaxLines, toolResultMaxBytes, policies, redact) => {
1950
- if (!isRecord(entry)) {
2089
+ if (!isRecordOf(entry)) {
1951
2090
  return;
1952
2091
  }
1953
- if (entry.type === "compaction" && typeof entry.summary === "string") {
2092
+ if (entry.type === "compaction" && isString(entry.summary)) {
1954
2093
  return `[System Compaction Summary]: ${redact ? redactSecrets(entry.summary) : entry.summary}`;
1955
2094
  }
1956
- if (entry.type !== "message" || !isRecord(entry.message)) {
2095
+ if (entry.type !== "message" || !isRecordOf(entry.message)) {
1957
2096
  return;
1958
2097
  }
1959
2098
  const { message } = entry;
@@ -1979,17 +2118,17 @@ var selectRecentEntries = (entries, maxChars) => {
1979
2118
  }
1980
2119
  const newestTruncated = "[Newest entry truncated]";
1981
2120
  if (entries.length === 1) {
1982
- const prefix = `${newestTruncated}${separator}`;
1983
- return `${prefix}${entries[0].slice(0, Math.max(0, maxChars - prefix.length))}`.slice(0, maxChars);
2121
+ const prefix2 = `${newestTruncated}${separator}`;
2122
+ return `${prefix2}${entries[0].slice(0, Math.max(0, maxChars - prefix2.length))}`.slice(0, maxChars);
1984
2123
  }
1985
2124
  const selected = [];
1986
2125
  let selectedLength = 0;
1987
2126
  for (let index = entries.length - 1;index >= 0; index -= 1) {
1988
2127
  const entry = entries[index];
1989
2128
  const candidateCount = selected.length + 1;
1990
- const omitted = entries.length - candidateCount;
2129
+ const omitted2 = entries.length - candidateCount;
1991
2130
  const candidateLength = selectedLength + entry.length + (selected.length > 0 ? separator.length : 0);
1992
- if (omissionMarker(omitted).length + separator.length + candidateLength > maxChars) {
2131
+ if (omissionMarker(omitted2).length + separator.length + candidateLength > maxChars) {
1993
2132
  break;
1994
2133
  }
1995
2134
  selected.unshift(entry);
@@ -2018,30 +2157,29 @@ var invalid = (message) => ({
2018
2157
  reason: "invalid-protocol"
2019
2158
  });
2020
2159
  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;
2030
- }
2031
- }
2032
- for (const [id, results] of resultsByCall) {
2033
- if (results.length > 1) {
2034
- return invalid(`Tool call ${id} has duplicate result messages.`);
2160
+ var toolCallId = (part) => isString(part.id) ? part.id : undefined;
2161
+ var indexAssistantCalls = (message, index, callOwners) => {
2162
+ for (const call of toolCalls(message)) {
2163
+ const id = toolCallId(call);
2164
+ if (!id || callOwners.has(id)) {
2165
+ return invalid(`Assistant tool calls at context entry ${index} have missing or duplicate IDs.`);
2035
2166
  }
2167
+ callOwners.set(id, {
2168
+ index,
2169
+ name: isString(call.name) ? call.name : "unknown"
2170
+ });
2036
2171
  }
2037
- return {
2038
- index: {
2039
- callOwners,
2040
- latestUserIndex: state.latestUserIndex,
2041
- resultsByCall
2042
- },
2043
- ok: true
2044
- };
2172
+ return;
2173
+ };
2174
+ var indexToolResult = (entry, message, index, resultsByCall) => {
2175
+ const id = message.toolCallId;
2176
+ if (!isString(id)) {
2177
+ return invalid(`Tool result at context entry ${index} has no tool-call ID.`);
2178
+ }
2179
+ const results = resultsByCall.get(id) ?? [];
2180
+ results.push({ entry, index });
2181
+ resultsByCall.set(id, results);
2182
+ return;
2045
2183
  };
2046
2184
  var indexEntry = (entry, index, callOwners, resultsByCall, state) => {
2047
2185
  if (entry.type !== "message" || !isRecord(entry.message)) {
@@ -2058,28 +2196,32 @@ var indexEntry = (entry, index, callOwners, resultsByCall, state) => {
2058
2196
  }
2059
2197
  return;
2060
2198
  };
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.`);
2199
+ var indexToolCalls = (entries) => {
2200
+ const callOwners = new Map;
2201
+ const resultsByCall = new Map;
2202
+ const state = { latestUserIndex: -1 };
2203
+ for (let index = 0;index < entries.length; index += 1) {
2204
+ const entry = entries[index];
2205
+ if (isRecord(entry)) {
2206
+ const failure = indexEntry(entry, index, callOwners, resultsByCall, state);
2207
+ if (failure) {
2208
+ return failure;
2209
+ }
2210
+ }
2065
2211
  }
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.`);
2212
+ for (const [id, results] of resultsByCall) {
2213
+ if (results.length > 1) {
2214
+ return invalid(`Tool call ${id} has duplicate result messages.`);
2076
2215
  }
2077
- callOwners.set(id, {
2078
- index,
2079
- name: typeof call.name === "string" ? call.name : "unknown"
2080
- });
2081
2216
  }
2082
- return;
2217
+ return {
2218
+ index: {
2219
+ callOwners,
2220
+ latestUserIndex: state.latestUserIndex,
2221
+ resultsByCall
2222
+ },
2223
+ ok: true
2224
+ };
2083
2225
  };
2084
2226
 
2085
2227
  // src/scout-types.ts
@@ -2091,8 +2233,8 @@ var SCOUT_SELECTION_MAX_IDS = 32;
2091
2233
  var SCOUT_SYNTHESIS_MAX_BYTES = 4 * 1024;
2092
2234
 
2093
2235
  // 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("");
2236
+ var SPEAKER_PREFIX = /^(?<speaker>User|Executor):\s*/u;
2237
+ var boundedLabel = (value) => [...value.replaceAll(/\s+/gu, " ").trim()].slice(0, SCOUT_LABEL_MAX_CHARS).join("");
2096
2238
  var labelFor = (kind, content) => {
2097
2239
  const preview = boundedLabel(content.replace(SPEAKER_PREFIX, ""));
2098
2240
  const prefix = {
@@ -2123,10 +2265,8 @@ var createGroup = (originalIndex, entryIds, kind, content, required) => ({
2123
2265
  originalIndex,
2124
2266
  required
2125
2267
  });
2126
- var pendingAdvisorArguments = (value) => {
2127
- if (!isRecord(value)) {
2128
- return {};
2129
- }
2268
+ var pendingAdvisorArguments = (part) => {
2269
+ const value = isRecord(part.arguments) ? part.arguments : {};
2130
2270
  const allowed = {};
2131
2271
  for (const key of ["gitContext", "question"]) {
2132
2272
  if (key in value) {
@@ -2139,41 +2279,17 @@ var pendingInvocationDisclosure = (entry, invocationId, toolResultMaxLines, tool
2139
2279
  if (!isRecord(entry.message)) {
2140
2280
  return disclosed;
2141
2281
  }
2142
- const content = contentParts(entry.message.content).map((part) => {
2282
+ const message = entry.message;
2283
+ const content = contentParts(message.content).map((part) => {
2143
2284
  if (!isRecord(part) || part.type !== "toolCall" || toolCallId(part) !== invocationId || part.name !== "ask_advisor") {
2144
2285
  return part;
2145
2286
  }
2146
- return { ...part, arguments: pendingAdvisorArguments(part.arguments) };
2287
+ return { ...part, arguments: pendingAdvisorArguments(part) };
2147
2288
  });
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
- };
2289
+ return conversationEntry({ ...entry, message: { ...message, content } }, toolResultMaxLines, toolResultMaxBytes, policies, redact);
2173
2290
  };
2174
2291
  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)) {
2292
+ 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
2293
  return `Tool result at context entry ${index + 1} does not match its adjacent assistant group.`;
2178
2294
  }
2179
2295
  return;
@@ -2189,7 +2305,8 @@ var collectResults = (calls, callIds, index, resultsByCall, consumedResultIndexe
2189
2305
  return `Tool result at context entry ${resultMatch.index} precedes its assistant call.`;
2190
2306
  }
2191
2307
  const resultMessage = resultMatch.entry.message;
2192
- const expectedName = typeof calls[callIndex].name === "string" ? calls[callIndex].name : "unknown";
2308
+ const call = calls[callIndex];
2309
+ const expectedName = isString(call.name) ? call.name : "unknown";
2193
2310
  if (!isRecord(resultMessage) || resultMessage.toolName !== expectedName) {
2194
2311
  return `Tool result at context entry ${resultMatch.index} conflicts with call ${callId}.`;
2195
2312
  }
@@ -2198,7 +2315,7 @@ var collectResults = (calls, callIds, index, resultsByCall, consumedResultIndexe
2198
2315
  if (resultText) {
2199
2316
  resultParts.push(resultText);
2200
2317
  }
2201
- resultEntryIds.push(typeof resultMatch.entry.id === "string" ? resultMatch.entry.id : String(resultMatch.index));
2318
+ resultEntryIds.push(isString(resultMatch.entry.id) ? resultMatch.entry.id : String(resultMatch.index));
2202
2319
  }
2203
2320
  return;
2204
2321
  };
@@ -2221,37 +2338,47 @@ var missingOutcome = (entry, index, entryId, disclosed, callIds, missing, result
2221
2338
  kind: "group"
2222
2339
  };
2223
2340
  };
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
- }
2341
+ var toolExchangeGroup = (entry, message, index, entryId, disclosed, immediate, indexed, consumedResultIndexes, caps) => {
2342
+ const calls = toolCalls(message);
2343
+ const callIds = calls.map(toolCallId).filter(isString);
2344
+ const adjacentFailure = adjacentResultMismatch(immediate, index, callIds, indexed.callOwners);
2345
+ if (adjacentFailure) {
2346
+ return { kind: "invalid", message: adjacentFailure };
2347
+ }
2348
+ const missing = new Set;
2349
+ const resultParts = [];
2350
+ const resultEntryIds = [];
2351
+ const matchFailure = collectResults(calls, callIds, index, indexed.resultsByCall, consumedResultIndexes, caps, missing, resultParts, resultEntryIds);
2352
+ if (matchFailure) {
2353
+ return { kind: "invalid", message: matchFailure };
2354
+ }
2355
+ if (missing.size > 0) {
2356
+ return missingOutcome(entry, index, entryId, disclosed, callIds, missing, resultParts, resultEntryIds, caps);
2245
2357
  }
2246
2358
  return {
2247
- groups,
2248
- ok: true,
2249
- protocolOmittedBytes,
2250
- protocolOmittedCount
2359
+ group: createGroup(index, [entryId, ...resultEntryIds], "tool-exchange", [disclosed, ...resultParts].join(`
2360
+
2361
+ `), false),
2362
+ kind: "group"
2251
2363
  };
2252
2364
  };
2365
+ var ownerOf = (message, callOwners) => isString(message.toolCallId) ? callOwners.get(message.toolCallId) : undefined;
2366
+ var contentPartsOf = (message) => Array.isArray(message.content) ? message.content : [];
2367
+ var hasCalls = (message) => contentPartsOf(message).some((part) => isRecord(part) && part.type === "toolCall");
2368
+ var toolResultOutcome = (message, index, disclosed, callOwners, consumedResultIndexes) => {
2369
+ if (consumedResultIndexes.has(index)) {
2370
+ return { kind: "skipped" };
2371
+ }
2372
+ if (ownerOf(message, callOwners)) {
2373
+ return {
2374
+ kind: "invalid",
2375
+ message: `Tool result at context entry ${index} precedes or conflicts with its retained call.`
2376
+ };
2377
+ }
2378
+ return { bytes: byteLength(disclosed), kind: "omitted" };
2379
+ };
2253
2380
  var groupForEntry = (entry, index, disclosed, immediate, indexed, consumedResultIndexes, caps) => {
2254
- const entryId = typeof entry.id === "string" ? entry.id : String(index);
2381
+ const entryId = isString(entry.id) ? entry.id : String(index);
2255
2382
  if (entry.type !== "message" || !isRecord(entry.message)) {
2256
2383
  return {
2257
2384
  group: createGroup(index, [entryId], "compaction", disclosed, false),
@@ -2277,23 +2404,42 @@ var groupForEntry = (entry, index, disclosed, immediate, indexed, consumedResult
2277
2404
  kind: "group"
2278
2405
  };
2279
2406
  }
2280
- return toolExchangeGroup(entry, index, entryId, disclosed, immediate, indexed, consumedResultIndexes, caps);
2407
+ return toolExchangeGroup(entry, message, index, entryId, disclosed, immediate, indexed, consumedResultIndexes, caps);
2281
2408
  };
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
- };
2409
+ var buildGroups = (entries, indexed, caps) => {
2410
+ const groups = [];
2411
+ const consumedResultIndexes = new Set;
2412
+ let protocolOmittedBytes = 0;
2413
+ let protocolOmittedCount = 0;
2414
+ for (let index = 0;index < entries.length; index += 1) {
2415
+ const entry = entries[index];
2416
+ if (!isRecord(entry)) {
2417
+ continue;
2418
+ }
2419
+ const disclosed = conversationEntry(entry, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact);
2420
+ if (!disclosed) {
2421
+ continue;
2422
+ }
2423
+ const next = entries[index + 1];
2424
+ const immediate = isRecord(next) ? next : undefined;
2425
+ const outcome = groupForEntry(entry, index, disclosed, immediate, indexed, consumedResultIndexes, caps);
2426
+ if (outcome.kind === "invalid") {
2427
+ return invalid(outcome.message);
2428
+ }
2429
+ if (outcome.kind === "omitted") {
2430
+ protocolOmittedCount += 1;
2431
+ protocolOmittedBytes += outcome.bytes;
2432
+ } else if (outcome.kind === "group") {
2433
+ groups.push(outcome.group);
2434
+ }
2291
2435
  }
2292
- return { bytes: byteLength(disclosed), kind: "omitted" };
2436
+ return {
2437
+ groups,
2438
+ ok: true,
2439
+ protocolOmittedBytes,
2440
+ protocolOmittedCount
2441
+ };
2293
2442
  };
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
2443
 
2298
2444
  // src/scout-reconstruct.ts
2299
2445
  var prefixWithinCharBudget = (value, maxChars) => {
@@ -2308,7 +2454,7 @@ var prefixWithinCharBudget = (value, maxChars) => {
2308
2454
  };
2309
2455
  var reconstructScoutConversation = (manifest, selectedIds, synthesis, maxChars = Number.MAX_SAFE_INTEGER) => {
2310
2456
  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);
2457
+ const evidence = manifest.groups.filter((group) => group.required || selected.has(group.id)).toSorted((left, right) => left.originalIndex - right.originalIndex).map((group) => group.content);
2312
2458
  const evidenceText = evidence.join(`
2313
2459
 
2314
2460
  `);
@@ -2333,15 +2479,106 @@ ${synthesis.trim()}` : undefined;
2333
2479
  return `${evidenceText}${separator}${prefixWithinCharBudget(inference, remaining)}`;
2334
2480
  };
2335
2481
 
2482
+ // src/scout-context.ts
2483
+ var resolveCaps = (options) => ({
2484
+ currentInvocationId: options.currentInvocationId,
2485
+ maxConversationChars: options.maxConversationChars,
2486
+ maxGroupBytes: options.maxGroupBytes ?? SCOUT_GROUP_MAX_BYTES,
2487
+ maxGroups: options.maxGroups ?? SCOUT_MANIFEST_MAX_GROUPS,
2488
+ maxManifestBytes: options.maxManifestBytes ?? options.maxBytes ?? SCOUT_MANIFEST_MAX_BYTES,
2489
+ policies: options.policies ?? advisorToolPoliciesRef,
2490
+ redact: options.redact ?? advisorRedactSecretsRef,
2491
+ toolResultMaxBytes: options.toolResultMaxBytes ?? advisorToolResultMaxBytesRef,
2492
+ toolResultMaxLines: options.toolResultMaxLines ?? advisorToolResultMaxLinesRef
2493
+ });
2494
+ var contentChars = (items) => items.reduce((sum, group) => sum + group.content.length, 0) + Math.max(0, items.length - 1) * 2;
2495
+ var requiredOverflow = (required, caps) => {
2496
+ if (required.some((group) => group.bytes > caps.maxGroupBytes) || required.length > caps.maxGroups || required.reduce((sum, group) => sum + groupWireBytes(group), 0) > caps.maxManifestBytes) {
2497
+ return {
2498
+ message: "Required Scout context exceeds the Scout manifest transport limit.",
2499
+ ok: false,
2500
+ reason: "required-group-overflow"
2501
+ };
2502
+ }
2503
+ if (caps.maxConversationChars !== undefined && contentChars(required) > caps.maxConversationChars) {
2504
+ return {
2505
+ message: "Required Scout context exceeds the Advisor conversation budget.",
2506
+ ok: false,
2507
+ reason: "required-group-overflow"
2508
+ };
2509
+ }
2510
+ return;
2511
+ };
2512
+ 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);
2513
+ var fitToBudget = (built, caps) => {
2514
+ const { groups, protocolOmittedBytes, protocolOmittedCount } = built;
2515
+ const availableCount = groups.length + protocolOmittedCount;
2516
+ const availableBytes = groups.reduce((sum, group) => sum + groupWireBytes(group), 0) + protocolOmittedBytes;
2517
+ if (caps.maxManifestBytes <= 0) {
2518
+ return {
2519
+ manifest: {
2520
+ availableBytes: 0,
2521
+ availableCount: 0,
2522
+ groups: [],
2523
+ omittedBytes: 0,
2524
+ omittedCount: 0
2525
+ },
2526
+ ok: true
2527
+ };
2528
+ }
2529
+ const required = groups.filter((group) => group.required);
2530
+ const overflow = requiredOverflow(required, caps);
2531
+ if (overflow) {
2532
+ return overflow;
2533
+ }
2534
+ const selected = groups.filter((group) => group.required || group.bytes <= caps.maxGroupBytes);
2535
+ while (!fits(selected, caps)) {
2536
+ const optionalIndex = selected.findIndex((group) => !group.required);
2537
+ if (optionalIndex === -1) {
2538
+ return {
2539
+ message: "Required Scout context exceeds fixed manifest limits.",
2540
+ ok: false,
2541
+ reason: "required-group-overflow"
2542
+ };
2543
+ }
2544
+ selected.splice(optionalIndex, 1);
2545
+ }
2546
+ const selectedIds = new Set(selected.map((group) => group.id));
2547
+ const omitted = groups.filter((group) => !selectedIds.has(group.id));
2548
+ return {
2549
+ manifest: {
2550
+ availableBytes,
2551
+ availableCount,
2552
+ groups: selected,
2553
+ omittedBytes: protocolOmittedBytes + omitted.reduce((sum, group) => sum + group.bytes, 0),
2554
+ omittedCount: protocolOmittedCount + omitted.length
2555
+ },
2556
+ ok: true
2557
+ };
2558
+ };
2559
+ var buildScoutManifest = (ctx, options = {}) => {
2560
+ const entries = ctx.sessionManager.buildContextEntries();
2561
+ const caps = resolveCaps(options);
2562
+ const indexed = indexToolCalls(entries);
2563
+ if (!indexed.ok) {
2564
+ return indexed;
2565
+ }
2566
+ const built = buildGroups(entries, indexed.index, caps);
2567
+ if (!built.ok) {
2568
+ return built;
2569
+ }
2570
+ return fitToBudget(built, caps);
2571
+ };
2572
+
2336
2573
  // src/usage.ts
2337
- var finite = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
2574
+ var finite = (value) => isNumber(value) && Number.isFinite(value) && value >= 0 ? value : undefined;
2338
2575
  var add = (left, right) => left === undefined || right === undefined ? left ?? right : left + right;
2339
2576
  var costFields = ["input", "output", "cacheRead", "cacheWrite", "total"];
2340
2577
  var snapshotAdvisorUsage = (usage) => {
2341
- if (!isRecord(usage)) {
2578
+ if (!isRecordOf(usage)) {
2342
2579
  return;
2343
2580
  }
2344
- const cost = isRecord(usage.cost) ? usage.cost : undefined;
2581
+ const cost = isRecordOf(usage.cost) ? usage.cost : undefined;
2345
2582
  const snapshot = {
2346
2583
  cacheRead: finite(usage.cacheRead),
2347
2584
  cacheWrite: finite(usage.cacheWrite),
@@ -2356,21 +2593,19 @@ var snapshotAdvisorUsage = (usage) => {
2356
2593
  var advisorUsageCost = (usage) => snapshotAdvisorUsage(usage)?.cost;
2357
2594
  var advisorUsageForPi = (usage) => {
2358
2595
  const snapshot = snapshotAdvisorUsage(usage);
2359
- if (!(snapshot && isRecord(usage))) {
2596
+ if (!(snapshot && isRecordOf(usage))) {
2360
2597
  return;
2361
2598
  }
2362
- const cost = isRecord(usage.cost) ? usage.cost : undefined;
2599
+ const cost = isRecordOf(usage.cost) ? usage.cost : undefined;
2363
2600
  const input = snapshot.input ?? 0;
2364
2601
  const output = snapshot.output ?? 0;
2365
2602
  const cacheRead = snapshot.cacheRead ?? 0;
2366
2603
  const cacheWrite = snapshot.cacheWrite ?? 0;
2367
2604
  const cacheWrite1h = finite(usage.cacheWrite1h);
2368
2605
  const reasoning = finite(usage.reasoning);
2369
- return {
2606
+ const piUsage = {
2370
2607
  cacheRead,
2371
2608
  cacheWrite,
2372
- ...cacheWrite1h === undefined ? {} : { cacheWrite1h },
2373
- ...reasoning === undefined ? {} : { reasoning },
2374
2609
  cost: {
2375
2610
  cacheRead: finite(cost?.cacheRead) ?? 0,
2376
2611
  cacheWrite: finite(cost?.cacheWrite) ?? 0,
@@ -2382,6 +2617,13 @@ var advisorUsageForPi = (usage) => {
2382
2617
  output,
2383
2618
  totalTokens: snapshot.totalTokens ?? input + output + cacheRead + cacheWrite
2384
2619
  };
2620
+ if (cacheWrite1h !== undefined) {
2621
+ piUsage.cacheWrite1h = cacheWrite1h;
2622
+ }
2623
+ if (reasoning !== undefined) {
2624
+ piUsage.reasoning = reasoning;
2625
+ }
2626
+ return piUsage;
2385
2627
  };
2386
2628
  var emptyAdvisorUsageTotals = () => ({
2387
2629
  calls: 0,
@@ -2476,8 +2718,8 @@ var defaultDependencies = {
2476
2718
  collect: collectTextStream,
2477
2719
  resolve: resolveConfiguredModel
2478
2720
  };
2479
- var byteLength2 = (value) => Buffer.byteLength(value, "utf8");
2480
- var AUTH_ERROR_PATTERN = /api key|auth|login|credential/i;
2721
+ var byteLength2 = (value) => Buffer.byteLength(value, "utf-8");
2722
+ var AUTH_ERROR_PATTERN = /api key|auth|login|credential/iu;
2481
2723
  var manifestMessage = (manifest) => ({
2482
2724
  content: [
2483
2725
  {
@@ -2494,6 +2736,7 @@ var manifestMessage = (manifest) => ({
2494
2736
  role: "user",
2495
2737
  timestamp: Date.now()
2496
2738
  });
2739
+ var isStringArray = (value) => Array.isArray(value) && value.every(isString);
2497
2740
  var parseScoutSelection = (text, manifest) => {
2498
2741
  if (!text.trim()) {
2499
2742
  throw new Error("Scout returned an empty response.");
@@ -2504,18 +2747,17 @@ var parseScoutSelection = (text, manifest) => {
2504
2747
  } catch (error) {
2505
2748
  throw new Error("Scout response is not a JSON object.", { cause: error });
2506
2749
  }
2507
- if (!value || typeof value !== "object" || Array.isArray(value)) {
2750
+ if (!isRecord(value)) {
2508
2751
  throw new Error("Scout response must be a JSON object.");
2509
2752
  }
2510
- const record = value;
2511
- const keys = Object.keys(record).sort();
2753
+ const keys = Object.keys(value).toSorted();
2512
2754
  if (keys.length !== 2 || keys[0] !== "selectedIds" || keys[1] !== "synthesis") {
2513
2755
  throw new Error("Scout response must contain only selectedIds and synthesis.");
2514
2756
  }
2515
- if (!(Array.isArray(record.selectedIds) && record.selectedIds.every((id) => typeof id === "string"))) {
2757
+ if (!isStringArray(value.selectedIds)) {
2516
2758
  throw new Error("Scout selectedIds must be an array of strings.");
2517
2759
  }
2518
- const selectedIds = record.selectedIds;
2760
+ const { selectedIds } = value;
2519
2761
  if (new Set(selectedIds).size !== selectedIds.length) {
2520
2762
  throw new Error("Scout selected duplicate group IDs.");
2521
2763
  }
@@ -2529,15 +2771,15 @@ var parseScoutSelection = (text, manifest) => {
2529
2771
  const optionalIds = knownSelectedIds.filter((id) => !required.has(id)).slice(0, SCOUT_SELECTION_MAX_IDS - requiredIds.length);
2530
2772
  const retained = new Set([...requiredIds, ...optionalIds]);
2531
2773
  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.");
2774
+ if (!isString(value.synthesis)) {
2775
+ throw new TypeError("Scout synthesis must be a string.");
2534
2776
  }
2535
- if (byteLength2(record.synthesis) > SCOUT_SYNTHESIS_MAX_BYTES) {
2777
+ if (byteLength2(value.synthesis) > SCOUT_SYNTHESIS_MAX_BYTES) {
2536
2778
  throw new Error(`Scout synthesis exceeds ${SCOUT_SYNTHESIS_MAX_BYTES} UTF-8 bytes.`);
2537
2779
  }
2538
2780
  return {
2539
2781
  selectedIds: normalizedIds,
2540
- synthesis: knownSelectedIds.length > 0 ? record.synthesis : ""
2782
+ synthesis: knownSelectedIds.length > 0 ? value.synthesis : ""
2541
2783
  };
2542
2784
  };
2543
2785
  var baseMetrics = (manifest, startedAt) => ({
@@ -2556,6 +2798,61 @@ var classifyResolutionError = (message) => {
2556
2798
  }
2557
2799
  return "provider-error";
2558
2800
  };
2801
+ var setupAbortWatch = (parentSignal, timeoutMs) => {
2802
+ const controller = new AbortController;
2803
+ let timedOut = false;
2804
+ const abortFromParent = () => controller.abort(parentSignal?.reason);
2805
+ parentSignal?.addEventListener("abort", abortFromParent, { once: true });
2806
+ const timer = setTimeout(() => {
2807
+ timedOut = true;
2808
+ controller.abort(new Error("Scout timed out."));
2809
+ }, timeoutMs);
2810
+ timer.unref?.();
2811
+ const { promise: abortPromise, reject: rejectOnAbort } = Promise.withResolvers();
2812
+ const onControllerAbort = () => rejectOnAbort(controller.signal.reason ?? new Error("Scout aborted."));
2813
+ controller.signal.addEventListener("abort", onControllerAbort, {
2814
+ once: true
2815
+ });
2816
+ return {
2817
+ abortPromise,
2818
+ controller,
2819
+ teardown: () => {
2820
+ clearTimeout(timer);
2821
+ parentSignal?.removeEventListener("abort", abortFromParent);
2822
+ controller.signal.removeEventListener("abort", onControllerAbort);
2823
+ },
2824
+ wasTimedOut: () => timedOut
2825
+ };
2826
+ };
2827
+ var streamScoutResponse = async (dependencies, resolved, executorModel, executorEffort, manifest, parentSignal, timeoutMs, publish) => {
2828
+ const { abortPromise, controller, teardown, wasTimedOut } = setupAbortWatch(parentSignal, timeoutMs);
2829
+ try {
2830
+ const collection = dependencies.collect(resolved, {
2831
+ messages: [manifestMessage(manifest)],
2832
+ onChunk: (thinking, text) => {
2833
+ if (!controller.signal.aborted) {
2834
+ publish({ model: executorModel, text, thinking, type: "chunk" });
2835
+ }
2836
+ },
2837
+ reasoning: executorEffort,
2838
+ signal: controller.signal,
2839
+ systemPrompt: SCOUT_SYSTEM
2840
+ });
2841
+ return {
2842
+ ok: true,
2843
+ streamed: await Promise.race([collection, abortPromise])
2844
+ };
2845
+ } catch (error) {
2846
+ const message = error instanceof Error ? error.message : String(error);
2847
+ return wasTimedOut() ? {
2848
+ category: "timeout",
2849
+ message: `Scout timed out after ${timeoutMs} ms.`,
2850
+ ok: false
2851
+ } : { category: "provider-error", message, ok: false };
2852
+ } finally {
2853
+ teardown();
2854
+ }
2855
+ };
2559
2856
  var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = SCOUT_TIMEOUT_MS, dependencies = defaultDependencies) => {
2560
2857
  const startedAt = Date.now();
2561
2858
  const publish = (event) => {
@@ -2566,22 +2863,24 @@ var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = S
2566
2863
  return { cancelled: true, ok: false };
2567
2864
  };
2568
2865
  const fallback = (category, message, usage) => {
2569
- const outcome = {
2866
+ const outcome2 = {
2570
2867
  category,
2571
2868
  message,
2572
2869
  metrics: usage === undefined ? baseMetrics(manifest, startedAt) : { ...baseMetrics(manifest, startedAt), usage },
2573
- model: executorRef,
2870
+ model: effectiveExecutorRef(ctx),
2574
2871
  ok: false
2575
2872
  };
2576
- publish({ outcome, type: "fallback" });
2577
- return outcome;
2873
+ publish({ outcome: outcome2, type: "fallback" });
2874
+ return outcome2;
2578
2875
  };
2579
2876
  if (parentSignal?.aborted) {
2580
2877
  return cancelled();
2581
2878
  }
2879
+ const executorModel = effectiveExecutorRef(ctx);
2880
+ const executorEffort = effectiveExecutorEffort(ctx);
2582
2881
  let resolved;
2583
2882
  try {
2584
- resolved = await dependencies.resolve(ctx, executorRef, "Scout");
2883
+ resolved = await dependencies.resolve(ctx, executorModel, "Scout");
2585
2884
  } catch (error) {
2586
2885
  if (parentSignal?.aborted) {
2587
2886
  return cancelled();
@@ -2592,61 +2891,23 @@ var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = S
2592
2891
  if (parentSignal?.aborted) {
2593
2892
  return cancelled();
2594
2893
  }
2595
- 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();
2894
+ publish({ model: executorModel, type: "call" });
2895
+ const streamed = await streamScoutResponse(dependencies, resolved, executorModel, executorEffort, manifest, parentSignal, timeoutMs, publish);
2896
+ if (!streamed.ok) {
2634
2897
  if (parentSignal?.aborted) {
2635
2898
  return cancelled();
2636
2899
  }
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);
2900
+ return fallback(streamed.category, streamed.message);
2639
2901
  }
2640
- teardown();
2641
2902
  if (parentSignal?.aborted) {
2642
2903
  return cancelled();
2643
2904
  }
2644
2905
  let selection;
2645
2906
  try {
2646
- selection = parseScoutSelection(streamed.text, manifest);
2907
+ selection = parseScoutSelection(streamed.streamed.text, manifest);
2647
2908
  } catch (error) {
2648
2909
  const message = error instanceof Error ? error.message : String(error);
2649
- return fallback(streamed.text.trim() ? "invalid-selection" : "empty-response", message, snapshotAdvisorUsage(streamed.usage));
2910
+ return fallback(streamed.streamed.text.trim() ? "invalid-selection" : "empty-response", message, snapshotAdvisorUsage(streamed.streamed.usage));
2650
2911
  }
2651
2912
  const outcome = {
2652
2913
  conversation: reconstructScoutConversation(manifest, selection.selectedIds, selection.synthesis),
@@ -2656,9 +2917,9 @@ var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = S
2656
2917
  ...selection.selectedIds,
2657
2918
  ...manifest.groups.filter((group) => group.required).map((group) => group.id)
2658
2919
  ]).size,
2659
- usage: snapshotAdvisorUsage(streamed.usage)
2920
+ usage: snapshotAdvisorUsage(streamed.streamed.usage)
2660
2921
  },
2661
- model: executorRef,
2922
+ model: executorModel,
2662
2923
  ok: true,
2663
2924
  selectedLabels: manifest.groups.filter((group) => group.required || selection.selectedIds.includes(group.id)).map((group) => group.label),
2664
2925
  selection
@@ -2667,97 +2928,6 @@ var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = S
2667
2928
  return outcome;
2668
2929
  };
2669
2930
 
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
2739
- },
2740
- ok: true
2741
- };
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;
2759
- };
2760
-
2761
2931
  // src/scout-curation.ts
2762
2932
  var curateAdvisorConversation = async (ctx, legacyConversation, signal, onScout, enabled = advisorScoutEnabledRef, runScout = runAdvisorScout, currentInvocationId, maxChars) => {
2763
2933
  if (!enabled) {
@@ -2788,7 +2958,7 @@ var curateAdvisorConversation = async (ctx, legacyConversation, signal, onScout,
2788
2958
  onScout?.({ outcome: scout, type: "fallback" });
2789
2959
  return { conversation: legacyConversation, scout };
2790
2960
  }
2791
- const outcome = await runScout(ctx, built.manifest, signal, onScout, undefined, undefined);
2961
+ const outcome = await runScout(ctx, built.manifest, signal, onScout);
2792
2962
  if (!outcome.ok && outcome.cancelled) {
2793
2963
  throw signal?.reason instanceof Error ? signal.reason : new Error("Advisor operation cancelled during Scout.");
2794
2964
  }
@@ -2800,12 +2970,12 @@ var curateAdvisorConversation = async (ctx, legacyConversation, signal, onScout,
2800
2970
  };
2801
2971
 
2802
2972
  // src/tools/prompts.ts
2803
- var advisorMessageText = (conversation, question, changes, draft, preferences, untracked, tracked) => {
2973
+ var advisorMessageText = (conversation, question, changes, draft, preferences, untracked2, tracked2) => {
2804
2974
  const safeConversation = escapeRepositoryText(conversation);
2805
2975
  const safeDraft = draft ? escapeRepositoryText(draft) : undefined;
2806
2976
  const safePreferences = preferences ? escapeRepositoryText(preferences) : undefined;
2807
- const safeUntracked = (untracked ?? []).map(escapeRepositoryText);
2808
- const safeTracked = (tracked ?? []).map(escapeRepositoryText);
2977
+ const safeUntracked = (untracked2 ?? []).map(escapeRepositoryText);
2978
+ const safeTracked = (tracked2 ?? []).map(escapeRepositoryText);
2809
2979
  const text = `${safeConversation ? `<conversation>
2810
2980
  ${safeConversation}
2811
2981
  </conversation>` : ""}${changes ? `
@@ -2839,26 +3009,21 @@ ${question}` : ""}`;
2839
3009
  return text.trim() || "No conversation context is available. State that you cannot review without context.";
2840
3010
  };
2841
3011
  var advisorGitContextBudget = (contextMaxChars, gitContextMaxChars) => Math.min(gitContextMaxChars, Math.floor(contextMaxChars / 2));
3012
+ var LEVEL_WITHHELD = {
3013
+ collected: true,
3014
+ "no-changes": false
3015
+ };
3016
+ var STATUS_NOTES = {
3017
+ disabled: "Repository context was disabled or had no disclosure budget; it was withheld. Do not assume the working tree is clean.",
3018
+ failed: "Repository context could not be collected. Do not assume the working tree is clean.",
3019
+ "no-changes": "The working tree has no uncommitted changes.",
3020
+ "not-a-repository": "No Git repository is available for this session."
3021
+ };
2842
3022
  var gitContextNote = (result, requested, allowed) => {
2843
3023
  if (requested !== allowed && LEVEL_WITHHELD[result.status]) {
2844
3024
  return `Repository context was limited to "${allowed}" by user configuration; a fuller view was requested but withheld.`;
2845
3025
  }
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
3026
+ return STATUS_NOTES[result.status];
2862
3027
  };
2863
3028
  var advisorRepositoryContext = (result, requested, allowed, budget) => {
2864
3029
  const note = gitContextNote(result, requested, allowed);
@@ -2923,23 +3088,24 @@ var assembleConsultationContext = async (options) => {
2923
3088
  const curated = await curateAdvisorConversation(ctx, legacyConversation, options.signal, options.onScout, advisorScoutEnabledRef, runAdvisorScout, options.currentInvocationId, conversationBudget);
2924
3089
  const preferences = await readProjectPreferences(ctx, ATTACHMENT_TEXT_MAX_BYTES, advisorRedactSecretsRef);
2925
3090
  const draftText = options.draft ? redactAndCapText(options.draft, ATTACHMENT_TEXT_MAX_BYTES, advisorRedactSecretsRef) : undefined;
2926
- const untracked = await readUntrackedFiles(ctx.cwd, options.includeUntracked ?? [], advisorUntrackedContentRef, advisorRedactSecretsRef);
2927
- const tracked = await readTrackedFiles(ctx.cwd, options.includeTracked ?? [], advisorTrackedFileContentRef, advisorRedactSecretsRef, Math.max(0, ATTACHMENTS_TOTAL_MAX_BYTES - untracked.reduce((sum, item) => sum + item.bytes, 0)));
3091
+ const untracked2 = await readUntrackedFiles(ctx.cwd, options.includeUntracked ?? [], advisorUntrackedContentRef, advisorRedactSecretsRef);
3092
+ const tracked2 = await readTrackedFiles(ctx.cwd, options.includeTracked ?? [], advisorTrackedFileContentRef, advisorRedactSecretsRef, Math.max(0, ATTACHMENTS_TOTAL_MAX_BYTES - untracked2.reduce((sum, item) => sum + item.bytes, 0)));
2928
3093
  return {
2929
3094
  changeText,
2930
3095
  conversation: curated.conversation,
2931
3096
  draftText,
2932
3097
  preferences,
2933
3098
  scout: curated.scout,
2934
- tracked,
2935
- untracked
3099
+ tracked: tracked2,
3100
+ untracked: untracked2
2936
3101
  };
2937
3102
  };
2938
3103
 
2939
3104
  // 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/;
3105
+ var namedGroups = (match) => match.groups ?? {};
3106
+ var DECISION_LINE = /^Decision\s*:\s*(?<decision>proceed|revise|blocked)\s*$/iu;
3107
+ var CODE_FENCE = /^ {0,3}(?<marker>`{3,}|~{3,})(?<suffix>.*)$/u;
3108
+ var LINE_BREAK = /\r?\n/u;
2943
3109
  var advanceFence = (openingFence, marker, suffix) => {
2944
3110
  if (!openingFence) {
2945
3111
  if (marker[0] === "`" && suffix.includes("`")) {
@@ -2975,7 +3141,7 @@ var parseAutomaticDecision = (text) => {
2975
3141
  ok: false
2976
3142
  };
2977
3143
  }
2978
- const decision = match[1].toLowerCase();
3144
+ const decision = namedGroups(match).decision.toLowerCase();
2979
3145
  let openingFence;
2980
3146
  const decisions = [];
2981
3147
  let pendingFencedDecisions = [];
@@ -2983,7 +3149,8 @@ var parseAutomaticDecision = (text) => {
2983
3149
  const trimmed = line.trim();
2984
3150
  const fence = CODE_FENCE.exec(line);
2985
3151
  if (fence) {
2986
- const { closed, openingFence: nextOpeningFence } = advanceFence(openingFence, fence[1], fence[2]);
3152
+ const groups = namedGroups(fence);
3153
+ const { closed, openingFence: nextOpeningFence } = advanceFence(openingFence, groups.marker, groups.suffix);
2987
3154
  openingFence = nextOpeningFence;
2988
3155
  if (closed) {
2989
3156
  pendingFencedDecisions = [];
@@ -2994,26 +3161,24 @@ var parseAutomaticDecision = (text) => {
2994
3161
  if (!subsequent) {
2995
3162
  continue;
2996
3163
  }
2997
- const repeated = subsequent[1].trim().toLowerCase();
3164
+ const repeated2 = namedGroups(subsequent).decision.trim().toLowerCase();
2998
3165
  if (openingFence) {
2999
- pendingFencedDecisions.push(repeated);
3166
+ pendingFencedDecisions.push(repeated2);
3000
3167
  } else {
3001
- decisions.push(repeated);
3168
+ decisions.push(repeated2);
3002
3169
  }
3003
3170
  }
3004
3171
  if (openingFence) {
3005
3172
  decisions.push(...pendingFencedDecisions);
3006
3173
  }
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 {
3174
+ const [repeated] = decisions;
3175
+ if (repeated !== undefined) {
3176
+ return repeated === decision ? {
3177
+ category: "duplicate-decision",
3178
+ markdown: text,
3179
+ message: "Advisor gate response contains duplicate decision lines.",
3180
+ ok: false
3181
+ } : {
3017
3182
  category: "contradictory-decision",
3018
3183
  markdown: text,
3019
3184
  message: "Advisor gate response contains contradictory decision lines.",
@@ -3033,6 +3198,35 @@ var adviceForGateText = (result) => `**Decision: ${result.decision}**
3033
3198
 
3034
3199
  ${result.markdown}`;
3035
3200
 
3201
+ // src/tools/model-access.ts
3202
+ var currentModelRef = (ctx) => {
3203
+ const { model } = ctx;
3204
+ return model ? `${model.provider}/${model.id}` : undefined;
3205
+ };
3206
+ var advisorModelAccess = (ctx) => {
3207
+ const modelRef = currentModelRef(ctx);
3208
+ if (advisorModelWhitelistRef.length === 0) {
3209
+ return modelRef ? { allowed: true, modelRef } : { allowed: true };
3210
+ }
3211
+ if (modelRef && advisorModelWhitelistRef.includes(modelRef)) {
3212
+ return { allowed: true, modelRef };
3213
+ }
3214
+ const current = modelRef ?? "no current model";
3215
+ const denial = {
3216
+ allowed: false,
3217
+ reason: `Advisor calls are restricted to the configured model whitelist (${advisorModelWhitelistRef.join(", ")}). Current model: ${current}.`
3218
+ };
3219
+ if (modelRef) {
3220
+ denial.modelRef = modelRef;
3221
+ }
3222
+ return denial;
3223
+ };
3224
+ var advisorModelIsAllowed = (ctx) => advisorModelAccess(ctx).allowed;
3225
+ var advisorModelAccessReason = (ctx) => {
3226
+ const access = advisorModelAccess(ctx);
3227
+ return access.allowed ? undefined : access.reason;
3228
+ };
3229
+
3036
3230
  // src/tools/consultation.ts
3037
3231
  class AdvisorNoAdviceError extends Error {
3038
3232
  constructor() {
@@ -3046,6 +3240,10 @@ ${item.text}
3046
3240
  var collectAdvisorResponse = async (options) => {
3047
3241
  const { ctx, question, signal, systemPrompt } = options;
3048
3242
  loadConfig(ctx);
3243
+ const accessReason = advisorModelAccessReason(ctx);
3244
+ if (accessReason) {
3245
+ throw new Error(accessReason);
3246
+ }
3049
3247
  const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
3050
3248
  const context = await assembleConsultationContext(options);
3051
3249
  const outboundQuestion = advisorRedactSecretsRef && question !== undefined ? redactSecrets(question) : question;
@@ -3072,17 +3270,20 @@ var collectAdvisorResponse = async (options) => {
3072
3270
  if (!markdown.trim()) {
3073
3271
  throw new AdvisorNoAdviceError;
3074
3272
  }
3075
- return {
3076
- draftBytes: context.draftText ? Buffer.byteLength(context.draftText, "utf8") : undefined,
3273
+ const response = {
3274
+ draftBytes: context.draftText ? Buffer.byteLength(context.draftText, "utf-8") : undefined,
3077
3275
  markdown,
3078
3276
  model: advisorRef,
3079
3277
  preferenceBytes: context.preferences?.bytes,
3080
3278
  thinkingText: streamed.thinking,
3081
3279
  trackedBytes: context.tracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
3082
3280
  untrackedBytes: context.untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
3083
- usage: streamed.usage,
3084
- ...context.scout ? { scout: context.scout } : {}
3281
+ usage: streamed.usage
3085
3282
  };
3283
+ if (context.scout) {
3284
+ response.scout = context.scout;
3285
+ }
3286
+ return response;
3086
3287
  };
3087
3288
  var consultAdvisor = async (ctx, question, signal, onChunk, trigger = "executor-requested", gitContext, draft, includeUntracked, includeTracked, onScout, currentInvocationId) => {
3088
3289
  const result = await collectAdvisorResponse({
@@ -3164,8 +3365,8 @@ var settingsIdentity = (path) => {
3164
3365
  };
3165
3366
  var readHideThinking = (path) => {
3166
3367
  try {
3167
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
3168
- return typeof parsed === "object" && parsed !== null && parsed.hideThinkingBlock === true;
3368
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
3369
+ return isRecord(parsed) && parsed.hideThinkingBlock === true;
3169
3370
  } catch {
3170
3371
  return false;
3171
3372
  }
@@ -3201,6 +3402,9 @@ var SPINNER_FRAMES = [
3201
3402
  ];
3202
3403
  var THINKING_PREFIX = " ";
3203
3404
  var THINKING_PREFIX_WIDTH = visibleWidth(THINKING_PREFIX);
3405
+ var noop = () => {
3406
+ return;
3407
+ };
3204
3408
 
3205
3409
  class ThinkingMarkdown {
3206
3410
  markdown;
@@ -3222,18 +3426,14 @@ class ThinkingMarkdown {
3222
3426
  this.markdown.invalidate();
3223
3427
  }
3224
3428
  }
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);
3429
+ var hiddenThinkingLabel = (theme) => {
3430
+ const label = theme.fg("thinkingText", `${THINKING_PREFIX}Thinking…`);
3431
+ return {
3432
+ invalidate: noop,
3433
+ render: () => [label]
3434
+ };
3435
+ };
3436
+ var renderThinkingMarkdown = (thinking, theme) => piHideThinkingEnabled() ? hiddenThinkingLabel(theme) : new ThinkingMarkdown(thinking, theme);
3237
3437
  var resolveAdvisorRequest = (question) => question?.trim() || undefined;
3238
3438
  var renderAdvisorCallBox = (question, theme) => {
3239
3439
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
@@ -3244,7 +3444,7 @@ ${theme.fg("dim", ` ${question}`)}` : `${label} ${title}`, 0, 0));
3244
3444
  return box;
3245
3445
  };
3246
3446
  var COLLAPSED_ADVICE_LINES = 12;
3247
- var SOUND_VERDICT = /^Verdict:\s*sound$/;
3447
+ var SOUND_VERDICT = /^Verdict:\s*sound$/u;
3248
3448
  var hasSoundVerdict = (advice) => SOUND_VERDICT.test((advice.split(`
3249
3449
  `).find((line) => line.trim()) ?? "").trim());
3250
3450
  var renderAdvisorResponseHeader = (sound, theme) => sound ? theme.fg("accent", theme.bold("◆ ADVISOR · SOUND")) : theme.fg("warning", theme.bold("◆ ADVISOR RESPONSE"));
@@ -3370,7 +3570,21 @@ var scoutTitle = (scout, frame) => {
3370
3570
  if (scout.status === "cancelled") {
3371
3571
  return "◆ SCOUT · CANCELLED";
3372
3572
  }
3373
- return "◆ SCOUT · FALLBACK";
3573
+ return "◆ SCOUT · FALLBACK";
3574
+ };
3575
+ 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`}`;
3576
+ var scoutExpandedLines = (scout, expanded, theme) => {
3577
+ const lines = [];
3578
+ if (expanded && scout.selectedLabels?.length) {
3579
+ lines.push(theme.fg("dim", ` Selected: ${scout.selectedLabels.join("; ")}`));
3580
+ }
3581
+ if (expanded && scout.synthesis) {
3582
+ lines.push(theme.fg("dim", ` Scout synthesis (untrusted inference): ${scout.synthesis}`));
3583
+ }
3584
+ if (expanded && scout.omittedBeforeScout) {
3585
+ lines.push(theme.fg("dim", ` ${scout.omittedBeforeScout} group(s) omitted before Scout`));
3586
+ }
3587
+ return lines;
3374
3588
  };
3375
3589
  var renderScoutDetails = (box, scout, expanded, theme) => {
3376
3590
  const active = scout.status === "calling" || scout.status === "streaming";
@@ -3378,7 +3592,7 @@ var renderScoutDetails = (box, scout, expanded, theme) => {
3378
3592
  const title = scoutTitle(scout, frame);
3379
3593
  const lines = [
3380
3594
  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`}`)
3595
+ theme.fg("dim", scoutSummaryLine(scout))
3382
3596
  ];
3383
3597
  if (getAdvisorSettings().showUsageDetails) {
3384
3598
  const usage = formatAdvisorUsage(scout.usage);
@@ -3395,16 +3609,7 @@ var renderScoutDetails = (box, scout, expanded, theme) => {
3395
3609
  if (thinking.trim()) {
3396
3610
  box.addChild(renderThinkingMarkdown(thinking, theme));
3397
3611
  }
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
- }
3612
+ const expandedLines = scoutExpandedLines(scout, expanded, theme);
3408
3613
  if (expandedLines.length > 0) {
3409
3614
  box.addChild(new Text2(expandedLines.join(`
3410
3615
  `), 0, 0));
@@ -3434,6 +3639,31 @@ var addJevUsage = (totals, usage) => {
3434
3639
  totals.inputTokens += usage.inputTokens;
3435
3640
  totals.outputTokens += usage.outputTokens;
3436
3641
  };
3642
+ var savingsLine = (costs, skipped) => {
3643
+ if (costs.length === 0) {
3644
+ return "Estimated saving from skips: unavailable — no observed consultation cost this session";
3645
+ }
3646
+ const mean = costs.reduce((sum, cost) => sum + cost, 0) / costs.length;
3647
+ 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`;
3648
+ };
3649
+ var formatJevTokens = (usage) => `↑${formatTokenCount(usage.inputTokens + usage.outputTokens)}`;
3650
+ var markdownCosts = (invocations) => invocations.filter((item) => item.kind === "markdown" && typeof item.cost === "number").map((item) => item.cost);
3651
+ var filterLine = (filter) => {
3652
+ const head = `${filter.screened} screened (${filter.allowed} allowed, ${filter.skipped} skipped${filter.repeatSkipped > 0 ? ` [${filter.repeatSkipped} repeat]` : ""})`;
3653
+ const parts = [head];
3654
+ if (filter.overrides > 0) {
3655
+ parts.push(`${filter.overrides} override${filter.overrides === 1 ? "" : "s"}`);
3656
+ }
3657
+ if (filter.failures > 0) {
3658
+ parts.push(`${filter.failures} failure${filter.failures === 1 ? "" : "s"}`);
3659
+ }
3660
+ return `Jev filter: ${parts.join(", ")}`;
3661
+ };
3662
+ var gateLine = (gate, invocations) => {
3663
+ const consultationCosts = invocations.filter((item) => item.trigger === "turn-gate" && typeof item.cost === "number").map((item) => item.cost);
3664
+ const gateSpend = consultationCosts.reduce((sum, cost) => sum + cost, 0);
3665
+ 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)})`;
3666
+ };
3437
3667
 
3438
3668
  class AdvisorJevLedgerState {
3439
3669
  #ledger = freshJevLedger();
@@ -3455,10 +3685,11 @@ class AdvisorJevLedgerState {
3455
3685
  if (repeat) {
3456
3686
  this.#ledger.filter.repeatSkipped += 1;
3457
3687
  }
3458
- this.#lastSkip = {
3459
- ...normalizedQuestion ? { normalizedQuestion } : {},
3460
- turn
3461
- };
3688
+ const skip = { turn };
3689
+ if (normalizedQuestion) {
3690
+ skip.normalizedQuestion = normalizedQuestion;
3691
+ }
3692
+ this.#lastSkip = skip;
3462
3693
  }
3463
3694
  recordFilterOverride() {
3464
3695
  this.#ledger.filter.overrides += 1;
@@ -3492,20 +3723,19 @@ class AdvisorJevLedgerState {
3492
3723
  if (filter.overrides > 0) {
3493
3724
  parts.push(`${filter.overrides} override${filter.overrides === 1 ? "" : "s"}`);
3494
3725
  }
3495
- lines.push(`Consultation dedup: ${parts.join(", ")}`);
3496
- lines.push(this.#savingsLine(this.#markdownCosts(invocations), filter.skipped));
3726
+ lines.push(`Consultation dedup: ${parts.join(", ")}`, savingsLine(markdownCosts(invocations), filter.skipped));
3497
3727
  } else if (this.#filterActive()) {
3498
- lines.push(this.#filterLine(filter));
3728
+ lines.push(filterLine(filter));
3499
3729
  const jevTokens = usage.inputTokens + usage.outputTokens;
3500
3730
  if (jevTokens > 0) {
3501
- lines.push(`Jev cost: ${this.#formatJevTokens(usage)} tokens · $${usage.cost.toFixed(4)} (input only; output free)`);
3731
+ lines.push(`Jev cost: ${formatJevTokens(usage)} tokens · $${usage.cost.toFixed(4)} (input only; output free)`);
3502
3732
  }
3503
3733
  if (filter.skipped > 0) {
3504
- lines.push(this.#savingsLine(this.#markdownCosts(invocations), filter.skipped));
3734
+ lines.push(savingsLine(markdownCosts(invocations), filter.skipped));
3505
3735
  }
3506
3736
  }
3507
3737
  if (gate.checks > 0 || gate.consultations > 0) {
3508
- lines.push(this.#gateLine(gate, invocations));
3738
+ lines.push(gateLine(gate, invocations));
3509
3739
  }
3510
3740
  return lines;
3511
3741
  }
@@ -3513,39 +3743,10 @@ class AdvisorJevLedgerState {
3513
3743
  const { filter } = this.#ledger;
3514
3744
  return filter.screened > 0 || filter.overrides > 0 || filter.failures > 0;
3515
3745
  }
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
3746
  }
3546
3747
 
3547
3748
  // src/session-state.ts
3548
- var WHITESPACE = /\s/;
3749
+ var WHITESPACE = /\s/u;
3549
3750
  var TIMESTAMP_KEYS = new Set([
3550
3751
  "createdat",
3551
3752
  "date",
@@ -3555,7 +3756,7 @@ var TIMESTAMP_KEYS = new Set([
3555
3756
  "updatedat"
3556
3757
  ]);
3557
3758
  var REQUEST_ID_KEYS = new Set(["correlationid", "requestid", "traceid"]);
3558
- var normalizedKey = (key) => key.replace(/[-_]/g, "").toLowerCase();
3759
+ var normalizedKey = (key) => key.replaceAll(/[-_]/gu, "").toLowerCase();
3559
3760
  var isVolatileKey = (key, keys) => keys.has(normalizedKey(key));
3560
3761
  var normalizeShellWhitespace = (command) => {
3561
3762
  let result = "";
@@ -3588,10 +3789,10 @@ var normalizeShellWhitespace = (command) => {
3588
3789
  }
3589
3790
  return result;
3590
3791
  };
3591
- var normalizeString = (value) => value.replace(/\/(?:private\/)?tmp\/[^\s/]+/g, "/tmp/<temporary>").replace(/\/var\/folders\/[^\s/]+/g, "/var/folders/<temporary>");
3792
+ var normalizeString = (value) => value.replaceAll(/\/(?:private\/)?tmp\/[^\s/]+/gu, "/tmp/<temporary>").replaceAll(/\/var\/folders\/[^\s/]+/gu, "/var/folders/<temporary>");
3592
3793
  var normalizeToolInput = (toolName, input) => {
3593
3794
  const visit = (value, key) => {
3594
- if (typeof value === "string") {
3795
+ if (isString(value)) {
3595
3796
  if (key && isVolatileKey(key, TIMESTAMP_KEYS)) {
3596
3797
  return "<timestamp>";
3597
3798
  }
@@ -3604,9 +3805,8 @@ var normalizeToolInput = (toolName, input) => {
3604
3805
  if (Array.isArray(value)) {
3605
3806
  return value.map((item) => visit(item));
3606
3807
  }
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)]));
3808
+ if (isRecordOf(value)) {
3809
+ return Object.fromEntries(Object.keys(value).toSorted().map((childKey) => [childKey, visit(value[childKey], childKey)]));
3610
3810
  }
3611
3811
  return value;
3612
3812
  };
@@ -3713,11 +3913,11 @@ class AdvisorSessionState {
3713
3913
  addAdvisorUsage(this.#usage.totals, record.usage);
3714
3914
  }
3715
3915
  issueAdvice(id, advice, trigger, draft = false, normalizedQuestion) {
3716
- this.#ledger.issued.set(id, {
3717
- advice,
3718
- ...normalizedQuestion ? { normalizedQuestion } : {},
3719
- trigger
3720
- });
3916
+ const issued = { advice, trigger };
3917
+ if (normalizedQuestion) {
3918
+ issued.normalizedQuestion = normalizedQuestion;
3919
+ }
3920
+ this.#ledger.issued.set(id, issued);
3721
3921
  this.#ledger.lastAdvice = advice;
3722
3922
  if (draft) {
3723
3923
  this.#ledger.draftConsultations += 1;
@@ -3740,9 +3940,9 @@ class AdvisorSessionState {
3740
3940
  return false;
3741
3941
  }
3742
3942
  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);
3943
+ const escaped = path.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
3944
+ const boundary = `(^|[\\s"'\`()\\[])${escaped}(?=$|[\\s"'\`),;:!?\\]]|\\.(?=\\s|$))`;
3945
+ return new RegExp(boundary, "u").test(advice);
3746
3946
  });
3747
3947
  if (!mentioned) {
3748
3948
  return false;
@@ -3867,6 +4067,16 @@ var notify = (ctx, message, level) => {
3867
4067
  ctx.ui.notify(message, level);
3868
4068
  }
3869
4069
  };
4070
+ var reportManualBudgetExhausted = (ctx) => {
4071
+ const message = "Advisor call budget exhausted for this session.";
4072
+ notify(ctx, message, "warning");
4073
+ notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
4074
+ };
4075
+ var requestManualRender = (ctx) => {
4076
+ if (ctx.hasUI) {
4077
+ ctx.ui.setStatus("advisor-manual", undefined);
4078
+ }
4079
+ };
3870
4080
 
3871
4081
  class CommandRuntime {
3872
4082
  advisorSessionState;
@@ -3874,7 +4084,9 @@ class CommandRuntime {
3874
4084
  manualProgress = new Map;
3875
4085
  manualProgressTimers = new Map;
3876
4086
  pi;
4087
+ reportManualBudgetExhausted = reportManualBudgetExhausted;
3877
4088
  requestAdvisor;
4089
+ requestManualRender = requestManualRender;
3878
4090
  scoutStatus;
3879
4091
  manualProgressSequence = 0;
3880
4092
  pendingExecutorModelRef;
@@ -3883,7 +4095,7 @@ class CommandRuntime {
3883
4095
  this.pi = pi;
3884
4096
  this.advisorSessionState = dependencies.sessionState ?? advisorSessionState;
3885
4097
  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));
4098
+ this.requestAdvisor = dependencies.consult ?? ((ctx, question, signal, onChunk, onScout, gitContext) => consultAdvisor(ctx, question, signal, onChunk, "manual", gitContext, undefined, undefined, undefined, onScout));
3887
4099
  }
3888
4100
  flowEnabled() {
3889
4101
  return this.pi.getActiveTools().includes("ask_advisor");
@@ -3905,16 +4117,6 @@ class CommandRuntime {
3905
4117
  ctx.ui.setStatus("advisor-usage", getAdvisorSettings().showUsageFooter ? this.advisorSessionState.usageStatus() : undefined);
3906
4118
  }
3907
4119
  }
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
4120
  }
3919
4121
  var createCommandRuntime = (pi, dependencies = {}) => new CommandRuntime(pi, dependencies);
3920
4122
 
@@ -3983,7 +4185,7 @@ var resolveActivationModels = async (runtime, ctx) => {
3983
4185
  if (!(advisorAuth.ok && advisorAuth.apiKey)) {
3984
4186
  return { error: `No API key for Advisor ${advisorRef}` };
3985
4187
  }
3986
- if (!await runtime.setExecutorModel(executor)) {
4188
+ if (!isMarkedSubagent() && !await runtime.setExecutorModel(executor)) {
3987
4189
  return { error: `No API key for Executor ${executorRef}` };
3988
4190
  }
3989
4191
  return {};
@@ -4029,7 +4231,7 @@ var activateAdvisor = async (runtime, args, ctx, announce = true) => {
4029
4231
  saveConfig(ctx, { persistAdvisor: true, persistExecutor: true });
4030
4232
  }
4031
4233
  runtime.pendingExecutorModelRef = undefined;
4032
- if (executorEffortRef) {
4234
+ if (executorEffortRef && !isMarkedSubagent()) {
4033
4235
  runtime.pi.setThinkingLevel(executorEffortRef);
4034
4236
  }
4035
4237
  if (!runtime.flowEnabled()) {
@@ -4040,20 +4242,22 @@ var activateAdvisor = async (runtime, args, ctx, announce = true) => {
4040
4242
  ]);
4041
4243
  }
4042
4244
  if (announce) {
4245
+ const activeExecutorRef = effectiveExecutorRef(ctx);
4246
+ const activeExecutorEffort = effectiveExecutorEffort(ctx);
4043
4247
  notify(ctx, `${ADVISOR_ACTIVATION_EXPLANATION}
4044
4248
 
4045
- Advisor flow ready — Executor: ${executorRef} (thinking: ${executorEffortRef || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`, "info");
4249
+ Advisor flow ready — Executor: ${activeExecutorRef} (thinking: ${activeExecutorEffort || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`, "info");
4046
4250
  }
4047
4251
  };
4048
4252
 
4049
4253
  // src/commands/lifecycle.ts
4050
- var registerCommandLifecycle = (runtime, activateAdvisor) => {
4254
+ var registerCommandLifecycle = (runtime, activateAdvisor2) => {
4051
4255
  runtime.pi.on("session_start", async (_event, ctx) => {
4052
4256
  runtime.pendingExecutorModelRef = undefined;
4053
4257
  try {
4054
4258
  loadConfig(ctx);
4055
4259
  if (alwaysOnRef) {
4056
- await activateAdvisor("", ctx, false);
4260
+ await activateAdvisor2("", ctx, false);
4057
4261
  }
4058
4262
  } catch (error) {
4059
4263
  const message = error instanceof Error ? error.message : String(error);
@@ -4061,7 +4265,7 @@ var registerCommandLifecycle = (runtime, activateAdvisor) => {
4061
4265
  }
4062
4266
  });
4063
4267
  runtime.pi.on("model_select", (event, ctx) => {
4064
- if (event.source !== "set" || runtime.suppressModelSelectionSync) {
4268
+ if (event.source !== "set" || runtime.suppressModelSelectionSync || isMarkedSubagent()) {
4065
4269
  return;
4066
4270
  }
4067
4271
  const selected = `${event.model.provider}/${event.model.id}`;
@@ -4102,11 +4306,7 @@ var registerCommandLifecycle = (runtime, activateAdvisor) => {
4102
4306
  };
4103
4307
 
4104
4308
  // src/ui/manual-dialog.ts
4105
- import {
4106
- Editor,
4107
- Key,
4108
- matchesKey
4109
- } from "@earendil-works/pi-tui";
4309
+ import { Editor, Key, matchesKey } from "@earendil-works/pi-tui";
4110
4310
 
4111
4311
  // src/ui/manual-dialog-render.ts
4112
4312
  import {
@@ -4202,6 +4402,7 @@ var renderManualAdvisorDialog = (view) => {
4202
4402
 
4203
4403
  // src/ui/manual-dialog.ts
4204
4404
  var TUI_INPUT_TAB = ["tui", "input", "tab"].join(".");
4405
+ var isShiftTab = (keyData) => matchesKey(keyData, Key.shift("tab"));
4205
4406
 
4206
4407
  class ManualAdvisorDialog {
4207
4408
  options;
@@ -4224,7 +4425,7 @@ class ManualAdvisorDialog {
4224
4425
  }
4225
4426
  constructor(options) {
4226
4427
  this.options = options;
4227
- this.gitLevels = GIT_CONTEXT_LEVELS.filter((level) => clampGitContextLevel(level, options.gitContext) === level).reverse();
4428
+ this.gitLevels = GIT_CONTEXT_LEVELS.filter((level) => clampGitContextLevel(level, options.gitContext) === level).toReversed();
4228
4429
  this.gitIndex = Math.max(0, this.gitLevels.indexOf(options.gitContext));
4229
4430
  const editorTheme = {
4230
4431
  borderColor: (text) => options.theme.fg("border", text),
@@ -4256,7 +4457,7 @@ class ManualAdvisorDialog {
4256
4457
  this.cancel();
4257
4458
  return;
4258
4459
  }
4259
- if (this.isShiftTab(keyData)) {
4460
+ if (isShiftTab(keyData)) {
4260
4461
  this.changeFocus(-1);
4261
4462
  return;
4262
4463
  }
@@ -4264,19 +4465,12 @@ class ManualAdvisorDialog {
4264
4465
  this.changeFocus(1);
4265
4466
  return;
4266
4467
  }
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
- }
4468
+ const handlers = {
4469
+ actions: (key) => this.handleActionInput(key),
4470
+ editor: (key) => this.handleEditorInput(key),
4471
+ git: (key) => this.handleGitInput(key)
4472
+ };
4473
+ handlers[this.focusTarget](keyData);
4280
4474
  }
4281
4475
  render(width) {
4282
4476
  const renderWidth = Math.max(1, Math.floor(width));
@@ -4384,9 +4578,6 @@ class ManualAdvisorDialog {
4384
4578
  isTab(keyData) {
4385
4579
  return this.matches(keyData, TUI_INPUT_TAB, Key.tab) && !matchesKey(keyData, Key.shift("tab"));
4386
4580
  }
4387
- isShiftTab(keyData) {
4388
- return matchesKey(keyData, Key.shift("tab"));
4389
- }
4390
4581
  isCancel(keyData) {
4391
4582
  return matchesKey(keyData, Key.escape) || this.options.keybindings.matches(keyData, "tui.select.cancel");
4392
4583
  }
@@ -4398,10 +4589,13 @@ class ManualAdvisorDialog {
4398
4589
  return;
4399
4590
  }
4400
4591
  this.state.completed = true;
4401
- this.options.onSubmit({
4402
- gitContext: this.gitLevels[this.gitIndex] ?? "off",
4403
- ...message ? { message } : {}
4404
- });
4592
+ const request = {
4593
+ gitContext: this.gitLevels[this.gitIndex] ?? "off"
4594
+ };
4595
+ if (message) {
4596
+ request.message = message;
4597
+ }
4598
+ this.options.onSubmit(request);
4405
4599
  }
4406
4600
  cancel() {
4407
4601
  if (this.state.completed) {
@@ -4412,31 +4606,31 @@ class ManualAdvisorDialog {
4412
4606
  }
4413
4607
  }
4414
4608
 
4415
- // src/jev/client.ts
4416
- class JevFailure extends Error {
4609
+ // src/jev/failure.ts
4610
+ class JevFailureError extends Error {
4417
4611
  category;
4418
4612
  constructor(category, message) {
4419
4613
  super(message);
4420
- this.name = "JevFailure";
4614
+ this.name = "JevFailureError";
4421
4615
  this.category = category;
4422
4616
  }
4423
4617
  }
4618
+
4619
+ // src/jev/client.ts
4424
4620
  var ENDPOINTS = {
4425
4621
  openrouter: "https://openrouter.ai/api/alpha/decisions",
4426
4622
  typesafe: "https://api.typesafe.ai/v1/systemone"
4427
4623
  };
4624
+ var range = (from, to) => Array.from({ length: to - from + 1 }, (_, index) => from + index);
4428
4625
  var RETRYABLE_STATUSES = new Set([408, 429, ...range(500, 599)]);
4429
4626
  var RETRY_BACKOFF_MS = 250;
4430
4627
  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;
4628
+ var finiteTokens = (value) => isNumber(value) && Number.isFinite(value) && value >= 0 ? value : 0;
4435
4629
  var errorDetail = (error) => {
4436
- if (typeof error === "string") {
4630
+ if (isString(error)) {
4437
4631
  return error;
4438
4632
  }
4439
- if (error && typeof error === "object" && "message" in error) {
4633
+ if (isRecordOf(error) && "message" in error) {
4440
4634
  return String(error.message);
4441
4635
  }
4442
4636
  return "";
@@ -4450,7 +4644,22 @@ var statusCategory = (status) => {
4450
4644
  }
4451
4645
  return "error";
4452
4646
  };
4647
+ var isObjectLike = (value) => typeof value === "object";
4453
4648
  var openRouterModelId = (model) => model.includes("/") ? model : `~typesafe/${model}`;
4649
+ var connectionFailure = (error) => {
4650
+ const message = redactSecrets(error instanceof Error ? error.message : String(error));
4651
+ return {
4652
+ failure: new JevFailureError("network", `Jev connection failed: ${message}`)
4653
+ };
4654
+ };
4655
+ var sleepWithAbort = (signal, ms) => new Promise((resolve2) => {
4656
+ const timer = setTimeout(resolve2, ms);
4657
+ timer.unref?.();
4658
+ signal.addEventListener("abort", () => {
4659
+ clearTimeout(timer);
4660
+ resolve2();
4661
+ }, { once: true });
4662
+ });
4454
4663
 
4455
4664
  class JevClient {
4456
4665
  #apiKey;
@@ -4499,7 +4708,7 @@ class JevClient {
4499
4708
  if (!outcome.retryable || attempt >= MAX_ATTEMPTS) {
4500
4709
  break;
4501
4710
  }
4502
- await this.#backoff(deadline.signal);
4711
+ await sleepWithAbort(deadline.signal, RETRY_BACKOFF_MS);
4503
4712
  if (deadline.signal.aborted) {
4504
4713
  break;
4505
4714
  }
@@ -4509,11 +4718,11 @@ class JevClient {
4509
4718
  if (signal?.aborted && !deadlineHit) {
4510
4719
  throw error;
4511
4720
  }
4512
- if (error instanceof JevFailure) {
4721
+ if (error instanceof JevFailureError) {
4513
4722
  throw error;
4514
4723
  }
4515
4724
  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);
4725
+ throw deadlineHit ? new JevFailureError("timeout", `Jev call exceeded its ${this.#timeoutMs} ms wall-time budget.`) : new JevFailureError("error", message);
4517
4726
  } finally {
4518
4727
  clearTimeout(timer);
4519
4728
  signal?.removeEventListener("abort", abortFromCaller);
@@ -4524,7 +4733,7 @@ class JevClient {
4524
4733
  return this.#result(outcome);
4525
4734
  }
4526
4735
  if (deadlineHit && outcome.failure?.category !== "auth") {
4527
- throw new JevFailure("timeout", `Jev call exceeded its ${this.#timeoutMs} ms wall-time budget.`);
4736
+ throw new JevFailureError("timeout", `Jev call exceeded its ${this.#timeoutMs} ms wall-time budget.`);
4528
4737
  }
4529
4738
  if (outcome.failure) {
4530
4739
  throw outcome.failure;
@@ -4532,21 +4741,7 @@ class JevClient {
4532
4741
  if (signal?.aborted) {
4533
4742
  throw new Error("Jev call aborted by the caller.");
4534
4743
  }
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
- });
4744
+ throw new JevFailureError("error", "Jev call failed.");
4550
4745
  }
4551
4746
  async#attempt(body, signal) {
4552
4747
  if (signal.aborted) {
@@ -4569,7 +4764,7 @@ class JevClient {
4569
4764
  }
4570
4765
  return {
4571
4766
  retryable: true,
4572
- ...this.#connectionFailure(error)
4767
+ ...connectionFailure(error)
4573
4768
  };
4574
4769
  }
4575
4770
  if (response.ok) {
@@ -4581,23 +4776,17 @@ class JevClient {
4581
4776
  retryable: RETRYABLE_STATUSES.has(response.status)
4582
4777
  };
4583
4778
  }
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
4779
  async#failureFromStatus(response) {
4591
4780
  let detail = "";
4592
4781
  try {
4593
4782
  const parsed = await response.json();
4594
- const error = parsed?.error;
4783
+ const error = isRecord(parsed) ? parsed.error : undefined;
4595
4784
  detail = errorDetail(error);
4596
4785
  } catch {
4597
4786
  detail = "";
4598
4787
  }
4599
4788
  const message = redactSecrets(`Jev ${this.#transportLabel()} request failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}.`);
4600
- return new JevFailure(statusCategory(response.status), message);
4789
+ return new JevFailureError(statusCategory(response.status), message);
4601
4790
  }
4602
4791
  #transportLabel() {
4603
4792
  return this.#endpoint === ENDPOINTS.openrouter ? "OpenRouter" : "TypeSafe";
@@ -4608,23 +4797,25 @@ class JevClient {
4608
4797
  parsed = await response.json();
4609
4798
  } catch (error) {
4610
4799
  return {
4611
- failure: new JevFailure("malformed", `Jev response was not JSON: ${redactSecrets(error instanceof Error ? error.message : String(error))}`)
4800
+ failure: new JevFailureError("malformed", `Jev response was not JSON: ${redactSecrets(error instanceof Error ? error.message : String(error))}`)
4612
4801
  };
4613
4802
  }
4614
- const record = parsed;
4615
- if (!record || typeof record !== "object" || !record.answers || typeof record.answers !== "object") {
4803
+ if (!isRecord(parsed) || !parsed.answers || !isObjectLike(parsed.answers)) {
4616
4804
  return {
4617
- failure: new JevFailure("malformed", "Jev response did not include an answers object.")
4805
+ failure: new JevFailureError("malformed", "Jev response did not include an answers object.")
4618
4806
  };
4619
4807
  }
4620
4808
  return {
4621
- answers: record.answers,
4622
- model: typeof record.model === "string" ? record.model : this.#model,
4623
- usage: record.usage
4809
+ answers: parsed.answers,
4810
+ model: isString(parsed.model) ? parsed.model : this.#model,
4811
+ usage: isRecordOf(parsed.usage) ? {
4812
+ input_tokens: parsed.usage.input_tokens,
4813
+ output_tokens: parsed.usage.output_tokens
4814
+ } : undefined
4624
4815
  };
4625
4816
  }
4626
4817
  #result(outcome) {
4627
- const usage = outcome.usage;
4818
+ const { usage } = outcome;
4628
4819
  const inputTokens = finiteTokens(usage?.input_tokens);
4629
4820
  const outputTokens = finiteTokens(usage?.output_tokens);
4630
4821
  const price = this.#pricePerMtok ?? advisorJevPricePerMtokRef;
@@ -4663,12 +4854,15 @@ var TYPESAFE_KEY_NAME = "typesafe-api-key";
4663
4854
  var TYPESAFE_KEY_CONFIG_FIELD = "typesafe_api_key";
4664
4855
  var KEY_FILE_MODE = 384;
4665
4856
  var keyFilePath = () => join4(getAgentDir3(), "typesafe_api_key");
4666
- var runtimeSecrets = () => globalThis.Bun?.secrets;
4857
+ var runtimeSecrets = () => {
4858
+ const bun = globalThis;
4859
+ return bun.Bun?.secrets;
4860
+ };
4667
4861
  var normalizeKey = (value) => value?.trim() || undefined;
4668
4862
  var readAdvisorJsonConfig = () => readExistingConfig(join4(getAgentDir3(), "advisor.json"));
4669
4863
  var defaultReadFileStore = () => {
4670
4864
  try {
4671
- return normalizeKey(readFileSync3(keyFilePath(), "utf8"));
4865
+ return normalizeKey(readFileSync3(keyFilePath(), "utf-8"));
4672
4866
  } catch {
4673
4867
  return;
4674
4868
  }
@@ -4708,7 +4902,7 @@ var resolveTypeSafeKey = async (deps = {}) => {
4708
4902
  }
4709
4903
  const config = (deps.readAdvisorJson ?? readAdvisorJsonConfig)();
4710
4904
  const staged = config[TYPESAFE_KEY_CONFIG_FIELD];
4711
- if (typeof staged === "string") {
4905
+ if (isString(staged)) {
4712
4906
  const fromConfig = normalizeKey(staged);
4713
4907
  if (fromConfig) {
4714
4908
  return { key: fromConfig, source: "advisor-json" };
@@ -4793,8 +4987,8 @@ var removeTypeSafeKeyFromAdvisorJson = () => {
4793
4987
  if (!(TYPESAFE_KEY_CONFIG_FIELD in existing)) {
4794
4988
  return { message: "No plaintext key in advisor.json.", ok: true };
4795
4989
  }
4796
- delete existing[TYPESAFE_KEY_CONFIG_FIELD];
4797
- writeFileSync2(path, `${JSON.stringify(existing, null, 2)}
4990
+ const retained = Object.fromEntries(Object.entries(existing).filter(([key]) => key !== TYPESAFE_KEY_CONFIG_FIELD));
4991
+ writeFileSync2(path, `${JSON.stringify(retained, null, 2)}
4798
4992
  `);
4799
4993
  resetConfigCache();
4800
4994
  return { message: "Plaintext key removed from advisor.json.", ok: true };
@@ -4836,15 +5030,14 @@ var screeningQuestions = {
4836
5030
  type: "score"
4837
5031
  }
4838
5032
  };
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;
5033
+ var NUMERIC_KEY_PATTERN = /^\d+$/u;
5034
+ var finiteNumber = (value) => isNumber(value) && Number.isFinite(value) ? value : undefined;
4842
5035
  var lowestStakesProbability = (answer) => {
4843
- if (!isRecord2(answer)) {
5036
+ if (!isRecordOf(answer)) {
4844
5037
  return;
4845
5038
  }
4846
- const probabilities = isRecord2(answer.probabilities) ? answer.probabilities : {};
4847
- const legend = isRecord2(answer.legend) ? answer.legend : undefined;
5039
+ const probabilities = isRecordOf(answer.probabilities) ? answer.probabilities : {};
5040
+ const legend = isRecordOf(answer.legend) ? answer.legend : undefined;
4848
5041
  if (legend) {
4849
5042
  const exact = Object.keys(legend).find((key) => legend[key] === STAKES_RUBRIC[0]);
4850
5043
  if (exact) {
@@ -4855,12 +5048,17 @@ var lowestStakesProbability = (answer) => {
4855
5048
  if (numericKeys.length === 0) {
4856
5049
  return;
4857
5050
  }
4858
- const lowest = numericKeys.reduce((left, right) => Number(left) <= Number(right) ? left : right);
5051
+ let [lowest] = numericKeys;
5052
+ for (const key of numericKeys) {
5053
+ if (Number(key) < Number(lowest)) {
5054
+ lowest = key;
5055
+ }
5056
+ }
4859
5057
  return finiteNumber(probabilities[lowest]);
4860
5058
  };
4861
- var selfAnswerableNoul = (answer) => isRecord2(answer) ? finiteNumber(answer.noul) : undefined;
5059
+ var selfAnswerableNoul = (answer) => isRecordOf(answer) ? finiteNumber(answer.noul) : undefined;
4862
5060
  var composeScreeningVerdict = (answers, { noulMargin, skipConfidence }) => {
4863
- if (!isRecord2(answers)) {
5061
+ if (!isRecordOf(answers)) {
4864
5062
  return { skip: false };
4865
5063
  }
4866
5064
  const negligibleMass = lowestStakesProbability(answers.stakes);
@@ -4874,29 +5072,29 @@ var composeScreeningVerdict = (answers, { noulMargin, skipConfidence }) => {
4874
5072
  };
4875
5073
  };
4876
5074
  var composeTurnGateVerdict = (answers, threshold) => {
4877
- if (!isRecord2(answers)) {
5075
+ if (!isRecordOf(answers)) {
4878
5076
  return false;
4879
5077
  }
4880
5078
  const answer = answers.should_consult;
4881
- const noul = isRecord2(answer) ? finiteNumber(answer.noul) : undefined;
5079
+ const noul = isRecordOf(answer) ? finiteNumber(answer.noul) : undefined;
4882
5080
  return noul !== undefined && noul >= threshold;
4883
5081
  };
4884
5082
 
4885
5083
  // src/jev/state.ts
4886
5084
  var JEV_TEXT_CAP_BYTES = 8 * 1024;
4887
5085
  var buildJevState = (ctx, input = {}) => {
4888
- const state = { role: "executor" };
5086
+ const fields = new Map([["role", "executor"]]);
4889
5087
  if (input.question) {
4890
- state.executor_question = redactAndCapText(input.question, JEV_TEXT_CAP_BYTES, advisorRedactSecretsRef);
5088
+ fields.set("executor_question", redactAndCapText(input.question, JEV_TEXT_CAP_BYTES, advisorRedactSecretsRef));
4891
5089
  }
4892
5090
  if (input.draft) {
4893
- state.executor_draft = redactAndCapText(input.draft, JEV_TEXT_CAP_BYTES, advisorRedactSecretsRef);
5091
+ fields.set("executor_draft", redactAndCapText(input.draft, JEV_TEXT_CAP_BYTES, advisorRedactSecretsRef));
4894
5092
  }
4895
5093
  const digest = recentConversation(ctx, advisorJevDigestMaxCharsRef);
4896
5094
  if (digest) {
4897
- state.recent_conversation = digest;
5095
+ fields.set("recent_conversation", digest);
4898
5096
  }
4899
- return state;
5097
+ return Object.fromEntries(fields);
4900
5098
  };
4901
5099
 
4902
5100
  // src/jev/transport.ts
@@ -4911,11 +5109,14 @@ var resolveJevTransport = async (ctx, deps = {}) => {
4911
5109
  const resolveTypesafe = deps.resolveTypesafe ?? resolveTypeSafeKey;
4912
5110
  const resolution = await resolveTypesafe();
4913
5111
  if (resolution.key) {
4914
- return {
5112
+ const credentials = {
4915
5113
  apiKey: resolution.key,
4916
- ...resolution.source ? { source: resolution.source } : {},
4917
5114
  transport: "typesafe"
4918
5115
  };
5116
+ if (resolution.source) {
5117
+ credentials.source = resolution.source;
5118
+ }
5119
+ return credentials;
4919
5120
  }
4920
5121
  }
4921
5122
  if (preference === "typesafe") {
@@ -4946,7 +5147,7 @@ var createOutageNotifier = (format) => {
4946
5147
  };
4947
5148
 
4948
5149
  // src/tools/jev-filter.ts
4949
- var normalizeScreeningQuestion = (question) => question?.trim().toLowerCase().replace(/\s+/g, " ") || undefined;
5150
+ var normalizeScreeningQuestion = (question) => question?.trim().toLowerCase().replaceAll(/\s+/gu, " ") || undefined;
4950
5151
  var REATTACHED_ADVICE_CAP_BYTES = 4 * 1024;
4951
5152
  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
5153
  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 +5157,6 @@ var outageNotifier = createOutageNotifier((category, message) => `Advisor Jev fi
4956
5157
  var notifyOutageOnce = outageNotifier.notify;
4957
5158
  var resetJevOutageNotification = outageNotifier.reset;
4958
5159
  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
5160
  var bypassOutcome = (session, options, normalizedQuestion) => {
4984
5161
  const lastSkip = session.lastJevSkip;
4985
5162
  if (options.force) {
@@ -5027,7 +5204,7 @@ var screenWithJev = async (ctx, session, options, deps, normalizedQuestion) => {
5027
5204
  return allow();
5028
5205
  } catch (error) {
5029
5206
  session.recordJevFilterFailure();
5030
- if (error instanceof JevFailure) {
5207
+ if (error instanceof JevFailureError) {
5031
5208
  notifyOutageOnce(ctx, error.category, error.message);
5032
5209
  } else if (options.signal?.aborted) {
5033
5210
  throw error;
@@ -5037,10 +5214,34 @@ var screenWithJev = async (ctx, session, options, deps, normalizedQuestion) => {
5037
5214
  return allow();
5038
5215
  }
5039
5216
  };
5217
+ var screenConsultation = (ctx, session, options, deps = {}) => {
5218
+ if (isSimpleMode()) {
5219
+ return Promise.resolve(allow());
5220
+ }
5221
+ const normalizedQuestion = normalizeScreeningQuestion(options.question);
5222
+ const bypass = bypassOutcome(session, options, normalizedQuestion);
5223
+ if (bypass) {
5224
+ return Promise.resolve(bypass);
5225
+ }
5226
+ const reattached = session.reattachedAdviceFor(normalizedQuestion);
5227
+ if (reattached) {
5228
+ session.recordJevFilterSkipped(true, normalizedQuestion);
5229
+ return Promise.resolve({
5230
+ decision: "skip",
5231
+ kind: "repeat",
5232
+ reason: "already answered earlier in this session",
5233
+ reattachedAdvice: reattached.slice(0, REATTACHED_ADVICE_CAP_BYTES)
5234
+ });
5235
+ }
5236
+ if (!advisorJevFilterEnabledRef) {
5237
+ return Promise.resolve(allow());
5238
+ }
5239
+ return screenWithJev(ctx, session, options, deps, normalizedQuestion);
5240
+ };
5040
5241
  var screeningSkipText = (outcome) => outcome.kind === "repeat" && outcome.reattachedAdvice ? repeatSkipText(outcome.reattachedAdvice) : SCREENED_SKIP_TEXT;
5041
5242
 
5042
5243
  // src/commands/manual-consultation.ts
5043
- var startManualConsultation = (runtime, ctx, question, controller, scoutStatusToken, progress, gitContext) => {
5244
+ var startManualConsultation = async (runtime, ctx, question, controller, scoutStatusToken, progress, gitContext) => {
5044
5245
  herdrAdvisorActivity.start();
5045
5246
  progress.phase = "preparing";
5046
5247
  runtime.requestManualRender(ctx);
@@ -5055,23 +5256,24 @@ var startManualConsultation = (runtime, ctx, question, controller, scoutStatusTo
5055
5256
  runtime.manualProgressTimers.set(controller, timer);
5056
5257
  }
5057
5258
  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;
5259
+ try {
5260
+ const { adviceId, markdown, usage } = await runtime.requestAdvisor(ctx, question, controller.signal, (thinking, text) => {
5261
+ if (controller.signal.aborted) {
5262
+ return;
5263
+ }
5071
5264
  progress.phase = "active";
5265
+ progress.thinking = thinking;
5266
+ progress.text = text;
5072
5267
  runtime.requestManualRender(ctx);
5073
- }
5074
- }, gitContext).then(({ adviceId, markdown, usage }) => {
5268
+ }, (event) => {
5269
+ if (!controller.signal.aborted) {
5270
+ runtime.scoutStatus.update(ctx, scoutStatusToken, event);
5271
+ scoutDetails = appendScoutLifecycleEntry(runtime.pi, event, scoutDetails);
5272
+ progress.scout = scoutDetails;
5273
+ progress.phase = "active";
5274
+ runtime.requestManualRender(ctx);
5275
+ }
5276
+ }, gitContext);
5075
5277
  if (controller.signal.aborted) {
5076
5278
  return;
5077
5279
  }
@@ -5085,28 +5287,31 @@ var startManualConsultation = (runtime, ctx, question, controller, scoutStatusTo
5085
5287
  trigger: "manual",
5086
5288
  usage
5087
5289
  });
5088
- if (typeof adviceId === "string") {
5290
+ if (isString(adviceId)) {
5089
5291
  runtime.advisorSessionState.issueAdvice(adviceId, markdown, "manual", false, normalizeScreeningQuestion(question));
5090
5292
  }
5091
5293
  runtime.updateAdvisorUsageStatus(ctx);
5294
+ const details = {
5295
+ advisor: advisorRef,
5296
+ question,
5297
+ text: markdown
5298
+ };
5092
5299
  const normalizedUsage = snapshotAdvisorUsage(usage);
5300
+ if (normalizedUsage) {
5301
+ details.usage = normalizedUsage;
5302
+ }
5093
5303
  runtime.pi.sendMessage({
5094
5304
  content: `Manual Advisor consultation${question ? ` (${question})` : ""} — for your awareness; no action or follow-up consultation is needed unless the user asks:
5095
5305
 
5096
5306
  ${markdown}`,
5097
5307
  customType: "advisor-manual-result",
5098
- details: {
5099
- advisor: advisorRef,
5100
- question,
5101
- text: markdown,
5102
- ...normalizedUsage ? { usage: normalizedUsage } : {}
5103
- },
5308
+ details,
5104
5309
  display: true
5105
5310
  }, {
5106
5311
  deliverAs: "steer",
5107
5312
  triggerTurn: true
5108
5313
  });
5109
- }).catch((error) => {
5314
+ } catch (error) {
5110
5315
  if (controller.signal.aborted) {
5111
5316
  return;
5112
5317
  }
@@ -5132,7 +5337,7 @@ ${markdown}`,
5132
5337
  }, { deliverAs: "steer", triggerTurn: true });
5133
5338
  notify(ctx, `Advisor consultation failed: ${message}`, "error");
5134
5339
  notifyHerdrAdvisorFailure("Advisor consultation failed", message);
5135
- }).finally(() => {
5340
+ } finally {
5136
5341
  if (controller.signal.aborted) {
5137
5342
  progress.phase = "cancelled";
5138
5343
  }
@@ -5145,7 +5350,7 @@ ${markdown}`,
5145
5350
  runtime.scoutStatus.release(ctx, scoutStatusToken);
5146
5351
  runtime.manualConsultations.delete(controller);
5147
5352
  herdrAdvisorActivity.finish();
5148
- });
5353
+ }
5149
5354
  };
5150
5355
 
5151
5356
  // src/commands/manual-command.ts
@@ -5156,6 +5361,11 @@ var registerManualCommand = (runtime) => {
5156
5361
  if (!loadCommandConfig(ctx)) {
5157
5362
  return;
5158
5363
  }
5364
+ const accessReason = advisorModelAccessReason(ctx);
5365
+ if (accessReason) {
5366
+ notify(ctx, accessReason, "warning");
5367
+ return;
5368
+ }
5159
5369
  if (!(isSimpleMode() || runtime.advisorSessionState.canConsult(getAdvisorMaxCallsPerSession()))) {
5160
5370
  runtime.reportManualBudgetExhausted(ctx);
5161
5371
  return;
@@ -5305,41 +5515,40 @@ class ManualAdvisorProgressComponent {
5305
5515
  }
5306
5516
  return box.render(width);
5307
5517
  }
5308
- invalidate() {}
5518
+ invalidate = noop;
5309
5519
  }
5310
5520
 
5311
5521
  // 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
- }
5522
+ var manualCallRenderer = (runtime) => (entry, { expanded }, theme) => {
5523
+ const { progressId, question } = entry.data ?? {};
5524
+ const progress = progressId ? runtime.manualProgress.get(progressId) : undefined;
5525
+ return progress ? new ManualAdvisorProgressComponent(question, progress, Boolean(expanded), theme) : renderAdvisorCallBox(question, theme);
5526
+ };
5527
+ var manualResultRenderer = (message, { expanded }, theme) => {
5528
+ const { details } = message;
5529
+ const box = new Box2(1, 1, (text) => theme.bg("customMessageBg", text));
5530
+ const advice = details?.text ?? (isString(message.content) ? message.content : "(Advisor returned no advice.)");
5531
+ box.addChild(new Text4(renderAdvisorResponseHeader(hasSoundVerdict(advice), theme), 0, 0));
5532
+ if (details?.advisor) {
5533
+ box.addChild(new Text4(theme.fg("dim", ` ${details.advisor}`), 0, 0));
5534
+ }
5535
+ if (getAdvisorSettings().showUsageDetails) {
5536
+ const usage = formatAdvisorUsage(details?.usage);
5537
+ if (usage) {
5538
+ box.addChild(new Text4(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
5331
5539
  }
5332
- box.addChild(new Markdown3(adviceForDisplay(advice, expanded), 0, 0, getMarkdownTheme3()));
5333
- return box;
5334
- });
5540
+ }
5541
+ box.addChild(new Markdown3(adviceForDisplay(advice, expanded), 0, 0, getMarkdownTheme3()));
5542
+ return box;
5543
+ };
5544
+ var registerCommandRenderers = (runtime) => {
5545
+ runtime.pi.registerEntryRenderer?.("advisor-manual-call", manualCallRenderer(runtime));
5546
+ runtime.pi.registerMessageRenderer?.("advisor-manual-result", manualResultRenderer);
5335
5547
  };
5336
5548
 
5337
5549
  // src/ui/settings-selector.ts
5338
5550
  import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
5339
- import {
5340
- SettingsList,
5341
- truncateToWidth as truncateToWidth7
5342
- } from "@earendil-works/pi-tui";
5551
+ import { SettingsList, truncateToWidth as truncateToWidth7 } from "@earendil-works/pi-tui";
5343
5552
 
5344
5553
  // src/ui/settings-formatting.ts
5345
5554
  import { visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
@@ -5357,7 +5566,7 @@ var SIMPLE_MODE_GRADIENT_COLORS = [
5357
5566
  var withCurrentValue = (current, values) => values.includes(current) ? values : [current, ...values];
5358
5567
  var numericValues = (current, values) => {
5359
5568
  const all = values.includes(current) ? values : [...values, current];
5360
- return all.sort((a, b) => a - b).map(String);
5569
+ return all.toSorted((a, b) => a - b).map(String);
5361
5570
  };
5362
5571
  var maxCallValues = (current) => {
5363
5572
  const values = ["0", "1", "2", "3", "5", "10", "25", "50", "∞"];
@@ -5374,9 +5583,18 @@ var maxCallValues = (current) => {
5374
5583
  };
5375
5584
  var settingValue = (value, defaultValue) => value ?? defaultValue ? "On" : "Off";
5376
5585
  var currentContextLabel = (presets, contextMaxChars) => presets.find((preset) => preset.value === contextMaxChars)?.label ?? String(contextMaxChars);
5586
+ var closestPresetIndex = (presets, contextMaxChars) => {
5587
+ let closestIndex = 0;
5588
+ for (let index = 0;index < presets.length; index += 1) {
5589
+ if (Math.abs(presets[index].value - contextMaxChars) < Math.abs(presets[closestIndex].value - contextMaxChars)) {
5590
+ closestIndex = index;
5591
+ }
5592
+ }
5593
+ return closestIndex;
5594
+ };
5377
5595
  var contextDescription = (presets, contextMaxChars) => {
5378
5596
  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);
5597
+ const selectedIndex = exactIndex === -1 ? closestPresetIndex(presets, contextMaxChars) : exactIndex;
5380
5598
  const selectedPreset = presets[selectedIndex];
5381
5599
  const isFullContext = selectedPreset?.value === Number.MAX_SAFE_INTEGER || selectedPreset?.label.toUpperCase() === "FULL" || selectedPreset?.label.toUpperCase() === "ALL";
5382
5600
  const progress = isFullContext ? 1 : selectedIndex / Math.max(1, presets.length - 1);
@@ -5394,7 +5612,7 @@ var contextDescription = (presets, contextMaxChars) => {
5394
5612
  const markerColumn = meterPrefix.length + marker;
5395
5613
  const labelStart = Math.max(0, Math.min(meterPrefix.length + meter.length + 2 - labelWidth, markerColumn - Math.floor((labelWidth - 1) / 2)));
5396
5614
  const markerLabel = `${" ".repeat(labelStart)}${label}`;
5397
- const description = exactIndex >= 0 ? selectedPreset?.description : "Custom context limit.";
5615
+ const description = exactIndex === -1 ? "Custom context limit." : selectedPreset?.description;
5398
5616
  return `${description ?? "Custom context limit."}
5399
5617
  ${meterPrefix}${meter} full
5400
5618
  ${markerLabel}`;
@@ -5419,14 +5637,63 @@ var rainbowGradient = (text, startedAt) => {
5419
5637
  }).join("").concat("\x1B[0m");
5420
5638
  };
5421
5639
 
5640
+ // src/ui/settings-items.ts
5641
+ import { getKeybindings } from "@earendil-works/pi-tui";
5642
+
5422
5643
  // 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";
5644
+ import { Key as Key2, matchesKey as matchesKey2, truncateToWidth as truncateToWidth5 } from "@earendil-works/pi-tui";
5428
5645
 
5429
5646
  // node_modules/@typesafe-ai/sdk/dist/index.mjs
5647
+ var requestIdFrom = (headers) => headers.get("x-typesafe-request-id") ?? undefined;
5648
+ var APIPromise = class APIPromise2 extends Promise {
5649
+ #responsePromise;
5650
+ #parseResponse;
5651
+ #parsed;
5652
+ constructor(responsePromise, parseResponse) {
5653
+ super((resolve2) => resolve2(undefined));
5654
+ this.#responsePromise = responsePromise;
5655
+ this.#parseResponse = parseResponse;
5656
+ }
5657
+ asResponse() {
5658
+ return this.#responsePromise;
5659
+ }
5660
+ async withResponse() {
5661
+ const [data, response] = await Promise.all([this.#parse(), this.#responsePromise]);
5662
+ return {
5663
+ data,
5664
+ response,
5665
+ requestId: requestIdFrom(response.headers)
5666
+ };
5667
+ }
5668
+ map(fn) {
5669
+ return new APIPromise2(this.#responsePromise, () => this.#parse().then(fn));
5670
+ }
5671
+ #parse() {
5672
+ this.#parsed ??= this.#responsePromise.then(this.#parseResponse);
5673
+ return this.#parsed;
5674
+ }
5675
+ then(onfulfilled, onrejected) {
5676
+ return this.#parse().then(onfulfilled, onrejected);
5677
+ }
5678
+ catch(onrejected) {
5679
+ return this.#parse().catch(onrejected);
5680
+ }
5681
+ finally(onfinally) {
5682
+ return this.#parse().finally(onfinally);
5683
+ }
5684
+ };
5685
+ var ENV = {
5686
+ apiKey: "TYPESAFE_API_KEY",
5687
+ baseURL: "TYPESAFE_BASE_URL",
5688
+ defaultModel: "TYPESAFE_DEFAULT_MODEL",
5689
+ logLevel: "TYPESAFE_LOG_LEVEL"
5690
+ };
5691
+ var readEnv = (name) => {
5692
+ if (typeof process === "undefined" || !process.env)
5693
+ return;
5694
+ return process.env[name]?.trim() || undefined;
5695
+ };
5696
+ var fromCodeOrEnv = (fromCode, envVar) => fromCode ?? readEnv(envVar);
5430
5697
  var range2 = (from, to) => Array.from({ length: to - from }, (_, i) => from + i);
5431
5698
  var DEFAULT_RETRY_POLICY = {
5432
5699
  maxRetries: 2,
@@ -5444,11 +5711,239 @@ var DEFAULT_RETRY_POLICY = {
5444
5711
  apiTimeoutError: true
5445
5712
  };
5446
5713
  DEFAULT_RETRY_POLICY.maxRetries;
5714
+ var isRetryableStatus = (status, policy = DEFAULT_RETRY_POLICY) => policy.httpStatuses.has(status);
5715
+ var parseRetryAfter = (headers, now = Date.now()) => {
5716
+ const ms = Number(headers.get("retry-after-ms"));
5717
+ if (headers.has("retry-after-ms") && Number.isFinite(ms) && ms >= 0)
5718
+ return ms;
5719
+ const raw = headers.get("retry-after");
5720
+ if (raw === null)
5721
+ return;
5722
+ const seconds = Number(raw);
5723
+ if (Number.isFinite(seconds))
5724
+ return seconds >= 0 ? seconds * 1000 : undefined;
5725
+ const date = Date.parse(raw);
5726
+ if (!Number.isNaN(date))
5727
+ return Math.max(0, date - now);
5728
+ };
5729
+ var retryDelayMs = (attempt, headers, policy = DEFAULT_RETRY_POLICY, random = Math.random) => {
5730
+ if (policy.respectRetryAfter && headers !== undefined) {
5731
+ const retryAfter = parseRetryAfter(headers);
5732
+ if (retryAfter !== undefined && retryAfter <= policy.maxRetryAfterMs)
5733
+ return retryAfter;
5734
+ }
5735
+ const exponential = Math.min(policy.backoffInitialMs * 2 ** attempt, policy.backoffMaxMs);
5736
+ return Math.round(exponential * (1 - random() * policy.backoffJitter));
5737
+ };
5738
+ var sleep = (ms, signal) => new Promise((resolve2, reject) => {
5739
+ if (signal?.aborted)
5740
+ return reject(signal.reason);
5741
+ const onAbort = () => {
5742
+ clearTimeout(timer);
5743
+ reject(signal?.reason);
5744
+ };
5745
+ const timer = setTimeout(() => {
5746
+ signal?.removeEventListener("abort", onAbort);
5747
+ resolve2();
5748
+ }, ms);
5749
+ signal?.addEventListener("abort", onAbort, { once: true });
5750
+ });
5751
+ var TypeSafeError = class extends Error {
5752
+ constructor(message, options) {
5753
+ super(message, options);
5754
+ this.name = new.target.name;
5755
+ }
5756
+ };
5757
+ var isRecord2 = (value) => typeof value === "object" && value !== null;
5758
+ var extractMessage = (body) => {
5759
+ if (typeof body === "string")
5760
+ return body || undefined;
5761
+ if (!isRecord2(body))
5762
+ return;
5763
+ const { error, message, detail } = body;
5764
+ if (typeof error === "string")
5765
+ return error;
5766
+ if (isRecord2(error) && typeof error.message === "string")
5767
+ return error.message;
5768
+ if (typeof message === "string")
5769
+ return message;
5770
+ if (typeof detail === "string")
5771
+ return detail;
5772
+ if (isRecord2(detail) && typeof detail.message === "string")
5773
+ return detail.message;
5774
+ if (Array.isArray(detail))
5775
+ return describeValidationErrors(detail);
5776
+ };
5777
+ var describeValidationErrors = (errors) => {
5778
+ const parts = errors.flatMap((e) => {
5779
+ if (!isRecord2(e) || typeof e.msg !== "string")
5780
+ return [];
5781
+ const loc = Array.isArray(e.loc) ? e.loc.filter((x) => x !== "body").join(".") : "";
5782
+ return [loc ? `${loc}: ${e.msg}` : e.msg];
5783
+ });
5784
+ return parts.length > 0 ? parts.join("; ") : undefined;
5785
+ };
5786
+ var MAX_RAW_BODY_IN_MESSAGE = 200;
5787
+ var APIError = class APIError2 extends TypeSafeError {
5788
+ status;
5789
+ headers;
5790
+ body;
5791
+ requestId;
5792
+ constructor(status, body, headers, message) {
5793
+ super(message ?? APIError2.describe(status, body));
5794
+ this.status = status;
5795
+ this.body = body;
5796
+ this.headers = headers;
5797
+ this.requestId = requestIdFrom(headers);
5798
+ }
5799
+ static describe(status, body) {
5800
+ const detail = extractMessage(body);
5801
+ if (detail)
5802
+ return `${status} ${detail}`;
5803
+ if (body === undefined)
5804
+ return `${status} status code (no body)`;
5805
+ const raw = typeof body === "string" ? body : JSON.stringify(body);
5806
+ return `${status} ${raw.length > MAX_RAW_BODY_IN_MESSAGE ? `${raw.slice(0, MAX_RAW_BODY_IN_MESSAGE)}…` : raw}`;
5807
+ }
5808
+ static fromResponse(status, body, headers) {
5809
+ if (status === 400)
5810
+ return new BadRequestError(status, body, headers);
5811
+ if (status === 401)
5812
+ return new AuthenticationError(status, body, headers);
5813
+ if (status === 403)
5814
+ return new PermissionDeniedError(status, body, headers);
5815
+ if (status === 404)
5816
+ return new NotFoundError(status, body, headers);
5817
+ if (status === 422)
5818
+ return new UnprocessableEntityError(status, body, headers);
5819
+ if (status === 429)
5820
+ return new RateLimitError(status, body, headers);
5821
+ if (status >= 500)
5822
+ return new InternalServerError(status, body, headers);
5823
+ return new APIError2(status, body, headers);
5824
+ }
5825
+ };
5826
+ var BadRequestError = class extends APIError {
5827
+ };
5828
+ var AuthenticationError = class extends APIError {
5829
+ };
5830
+ var PermissionDeniedError = class extends APIError {
5831
+ };
5832
+ var NotFoundError = class extends APIError {
5833
+ };
5834
+ var UnprocessableEntityError = class extends APIError {
5835
+ };
5836
+ var RateLimitError = class extends APIError {
5837
+ retryAfterMs = parseRetryAfter(this.headers);
5838
+ };
5839
+ var InternalServerError = class extends APIError {
5840
+ };
5841
+ var APIConnectionError = class extends TypeSafeError {
5842
+ constructor(message = "Connection error.", options) {
5843
+ super(message, options);
5844
+ }
5845
+ };
5846
+ var APITimeoutError = class extends APIConnectionError {
5847
+ timeoutMs;
5848
+ constructor(timeoutMs, options) {
5849
+ super(`Request timed out after ${timeoutMs}ms.`, options);
5850
+ this.timeoutMs = timeoutMs;
5851
+ }
5852
+ };
5853
+ var APIUserAbortError = class extends TypeSafeError {
5854
+ constructor(message = "Request was aborted.", options) {
5855
+ super(message, options);
5856
+ }
5857
+ };
5858
+ var LOG_LEVELS = [
5859
+ "debug",
5860
+ "info",
5861
+ "warn",
5862
+ "error",
5863
+ "off"
5864
+ ];
5865
+ var DEFAULT_LOG_LEVEL = "warn";
5866
+ var isLogLevel = (value) => LOG_LEVELS.includes(value);
5867
+ var parseLogLevel = (value, source) => {
5868
+ if (isLogLevel(value))
5869
+ return value;
5870
+ throw new TypeSafeError(`Invalid log level "${value}" from ${source}. Expected one of: ${LOG_LEVELS.join(", ")}.`);
5871
+ };
5872
+ var PREFIX = "[typesafe-sdk]";
5873
+ var consoleLogger = {
5874
+ debug: (message, ...args) => console.debug(`${PREFIX} ${message}`, ...args),
5875
+ info: (message, ...args) => console.info(`${PREFIX} ${message}`, ...args),
5876
+ warn: (message, ...args) => console.warn(`${PREFIX} ${message}`, ...args),
5877
+ error: (message, ...args) => console.error(`${PREFIX} ${message}`, ...args)
5878
+ };
5879
+ var RANK = {
5880
+ debug: 0,
5881
+ info: 1,
5882
+ warn: 2,
5883
+ error: 3,
5884
+ off: 4
5885
+ };
5886
+ var drop = () => {};
5887
+ var withLevel = (sink, level) => {
5888
+ const enabled = (at) => RANK[at] >= RANK[level];
5889
+ return {
5890
+ debug: enabled("debug") ? (message, ...args) => sink.debug(message, ...args) : drop,
5891
+ info: enabled("info") ? (message, ...args) => sink.info(message, ...args) : drop,
5892
+ warn: enabled("warn") ? (message, ...args) => sink.warn(message, ...args) : drop,
5893
+ error: enabled("error") ? (message, ...args) => sink.error(message, ...args) : drop
5894
+ };
5895
+ };
5896
+ var KEY_HEADERS = /* @__PURE__ */ new Set([
5897
+ "authorization",
5898
+ "proxy-authorization",
5899
+ "x-api-key"
5900
+ ]);
5901
+ var OPAQUE_HEADERS = /* @__PURE__ */ new Set(["cookie", "set-cookie"]);
5902
+ var redactKey = (value) => {
5903
+ const [scheme, secret] = value.includes(" ") ? value.split(/\s+/, 2) : [undefined, value];
5904
+ const tail = secret && secret.length > 8 ? secret.slice(-4) : "";
5905
+ return `${scheme ? `${scheme} ` : ""}***${tail}`;
5906
+ };
5907
+ var redact = (name, value) => {
5908
+ const lower = name.toLowerCase();
5909
+ if (KEY_HEADERS.has(lower))
5910
+ return redactKey(value);
5911
+ if (OPAQUE_HEADERS.has(lower))
5912
+ return "***";
5913
+ return value;
5914
+ };
5915
+ var redactHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, redact(name, value)]));
5447
5916
  var noul = (instructions = null, criteria) => ({
5448
5917
  type: "noul",
5449
5918
  instructions,
5450
5919
  criteria
5451
5920
  });
5921
+ var validateQuestions = (questions) => {
5922
+ if (Object.keys(questions).length === 0)
5923
+ throw new TypeSafeError("At least one question is required.");
5924
+ for (const [name, question] of Object.entries(questions)) {
5925
+ if (question.type !== "score")
5926
+ continue;
5927
+ if (!Array.isArray(question.criteria))
5928
+ throw new TypeSafeError(`Score question "${name}" has criteria that are not a list; score criteria must be a list of descriptions indexed by score from zero.`);
5929
+ if (question.criteria.length < 2)
5930
+ throw new TypeSafeError(`Score question "${name}" has ${question.criteria.length} criteria; at least two scores are required.`);
5931
+ }
5932
+ };
5933
+ var Models = class {
5934
+ #transport;
5935
+ constructor(transport) {
5936
+ this.#transport = transport;
5937
+ }
5938
+ list(options = {}) {
5939
+ return this.#transport.request("GET", "/v1/models", options).map(unwrapModels);
5940
+ }
5941
+ };
5942
+ var unwrapModels = (wire) => {
5943
+ if (Array.isArray(wire?.models))
5944
+ return wire.models;
5945
+ throw new TypeSafeError("Unexpected response shape from GET /v1/models; expected { models: [...] }.");
5946
+ };
5452
5947
  var g = globalThis;
5453
5948
  var isBrowser = () => typeof g.window !== "undefined" && typeof g.window.document !== "undefined" && typeof g.navigator !== "undefined";
5454
5949
  var describeRuntime = () => {
@@ -5467,13 +5962,283 @@ var describeRuntime = () => {
5467
5962
  return "browser";
5468
5963
  return "unknown";
5469
5964
  };
5965
+ var VERSION = "0.6.0";
5966
+ var missingApiKey = () => {
5967
+ throw new TypeSafeError(`No API key was provided. Pass \`apiKey\` to the TypeSafeClient constructor or set the ${ENV.apiKey} environment variable.`);
5968
+ };
5969
+ var missingFetch = () => {
5970
+ throw new TypeSafeError("No global `fetch` is available in this runtime. Pass a `fetch` implementation to the TypeSafeClient constructor.");
5971
+ };
5972
+ var refuseBrowser = () => {
5973
+ throw new TypeSafeError("TypeSafeClient is running in a browser, which would expose your API key to anyone using the page. Call the API from a server instead, or pass `dangerouslyAllowBrowser: true` if you understand the risk.");
5974
+ };
5975
+ var defaultFetch = (input, init) => globalThis.fetch(input, init);
5976
+ var assertNonNegativeInteger = (name, value) => {
5977
+ if (!Number.isInteger(value) || value < 0)
5978
+ throw new TypeSafeError(`\`${name}\` must be a non-negative integer, got ${String(value)}.`);
5979
+ return value;
5980
+ };
5981
+ var assertPositiveMs = (name, value) => {
5982
+ if (!Number.isFinite(value) || value <= 0)
5983
+ throw new TypeSafeError(`\`${name}\` must be a positive number of milliseconds, got ${String(value)}.`);
5984
+ return value;
5985
+ };
5986
+ var assertNonNegativeMs = (name, value) => {
5987
+ if (!Number.isFinite(value) || value < 0)
5988
+ throw new TypeSafeError(`\`${name}\` must be a non-negative number of milliseconds, got ${String(value)}.`);
5989
+ return value;
5990
+ };
5991
+ var assertFraction = (name, value) => {
5992
+ if (!Number.isFinite(value) || value < 0 || value > 1)
5993
+ throw new TypeSafeError(`\`${name}\` must be between 0 and 1, got ${String(value)}.`);
5994
+ return value;
5995
+ };
5996
+ var assertStatusSet = (name, statuses) => {
5997
+ for (const status of statuses)
5998
+ if (!Number.isInteger(status) || status < 100 || status > 999)
5999
+ throw new TypeSafeError(`\`${name}\` must contain HTTP status codes, got ${String(status)}.`);
6000
+ return statuses;
6001
+ };
6002
+ var resolveRetryPolicy = (base, overrides) => {
6003
+ const o = overrides ?? {};
6004
+ return {
6005
+ maxRetries: o.maxRetries === undefined ? base.maxRetries : assertNonNegativeInteger("retry.maxRetries", o.maxRetries),
6006
+ backoffInitialMs: o.backoffInitialMs === undefined ? base.backoffInitialMs : assertNonNegativeMs("retry.backoffInitialMs", o.backoffInitialMs),
6007
+ backoffMaxMs: o.backoffMaxMs === undefined ? base.backoffMaxMs : assertNonNegativeMs("retry.backoffMaxMs", o.backoffMaxMs),
6008
+ backoffJitter: o.backoffJitter === undefined ? base.backoffJitter : assertFraction("retry.backoffJitter", o.backoffJitter),
6009
+ httpStatuses: new Set(o.httpStatuses === undefined ? base.httpStatuses : assertStatusSet("retry.httpStatuses", o.httpStatuses)),
6010
+ respectRetryAfter: o.respectRetryAfter ?? base.respectRetryAfter,
6011
+ maxRetryAfterMs: o.maxRetryAfterMs === undefined ? base.maxRetryAfterMs : assertNonNegativeMs("retry.maxRetryAfterMs", o.maxRetryAfterMs),
6012
+ apiConnectionError: o.apiConnectionError ?? base.apiConnectionError,
6013
+ apiTimeoutError: o.apiTimeoutError ?? base.apiTimeoutError
6014
+ };
6015
+ };
6016
+ var isRetryableError = (err, policy) => {
6017
+ if (err instanceof APITimeoutError)
6018
+ return policy.apiTimeoutError;
6019
+ if (err instanceof APIConnectionError)
6020
+ return policy.apiConnectionError;
6021
+ return false;
6022
+ };
6023
+ var resolveLogLevel = (fromCode) => {
6024
+ if (fromCode !== undefined)
6025
+ return parseLogLevel(fromCode, "the `logLevel` option");
6026
+ const fromEnv = readEnv(ENV.logLevel);
6027
+ if (fromEnv !== undefined)
6028
+ return parseLogLevel(fromEnv, ENV.logLevel);
6029
+ return DEFAULT_LOG_LEVEL;
6030
+ };
6031
+ var stripTrailingSlashes = (url) => url.replace(/\/+$/, "");
6032
+ var mergeHeaders = (...sources) => {
6033
+ const entries = /* @__PURE__ */ new Map;
6034
+ for (const source of sources)
6035
+ for (const [name, value] of Object.entries(source))
6036
+ if (value === undefined)
6037
+ entries.delete(name.toLowerCase());
6038
+ else
6039
+ entries.set(name.toLowerCase(), [name, value]);
6040
+ return Object.fromEntries(entries.values());
6041
+ };
6042
+ var bufferResponse = async (response, signal) => {
6043
+ const reader = response.clone().body?.getReader();
6044
+ if (!reader)
6045
+ return;
6046
+ const cancel = () => {
6047
+ reader.cancel(signal.reason).catch(() => {});
6048
+ response.body?.cancel(signal.reason).catch(() => {});
6049
+ };
6050
+ signal.addEventListener("abort", cancel, { once: true });
6051
+ try {
6052
+ if (signal.aborted)
6053
+ cancel();
6054
+ signal.throwIfAborted();
6055
+ while (!(await reader.read()).done)
6056
+ signal.throwIfAborted();
6057
+ signal.throwIfAborted();
6058
+ } finally {
6059
+ signal.removeEventListener("abort", cancel);
6060
+ reader.releaseLock();
6061
+ }
6062
+ };
5470
6063
  var RUNTIME = describeRuntime();
6064
+ var TypeSafeClient = class {
6065
+ #apiKey;
6066
+ baseURL;
6067
+ defaultModel;
6068
+ logLevel;
6069
+ logger;
6070
+ retry;
6071
+ timeout;
6072
+ defaultHeaders;
6073
+ fetch;
6074
+ models;
6075
+ #requestCount = 0;
6076
+ constructor(config = {}) {
6077
+ if (isBrowser() && !config.dangerouslyAllowBrowser)
6078
+ refuseBrowser();
6079
+ this.#apiKey = fromCodeOrEnv(config.apiKey, ENV.apiKey) ?? missingApiKey();
6080
+ this.baseURL = stripTrailingSlashes(fromCodeOrEnv(config.baseURL, ENV.baseURL) ?? "https://api.typesafe.ai");
6081
+ this.defaultModel = fromCodeOrEnv(config.defaultModel, ENV.defaultModel) ?? "jev-latest";
6082
+ this.logLevel = resolveLogLevel(config.logLevel);
6083
+ this.logger = withLevel(config.logger ?? consoleLogger, this.logLevel);
6084
+ this.retry = resolveRetryPolicy(DEFAULT_RETRY_POLICY, config.retry);
6085
+ this.timeout = assertPositiveMs("timeout", config.timeout ?? 1e4);
6086
+ this.defaultHeaders = { ...config.defaultHeaders };
6087
+ if (config.fetch === undefined && typeof globalThis.fetch !== "function")
6088
+ missingFetch();
6089
+ this.fetch = config.fetch ?? defaultFetch;
6090
+ const transport = {
6091
+ request: (method, path, options) => this.#request(method, path, options),
6092
+ defaultModel: this.defaultModel
6093
+ };
6094
+ this.models = new Models(transport);
6095
+ }
6096
+ systemOne(request, options = {}) {
6097
+ validateQuestions(request.questions);
6098
+ const body = {
6099
+ ...request,
6100
+ model: request.model ?? this.defaultModel
6101
+ };
6102
+ return this.#request("POST", "/v1/systemone", {
6103
+ ...options,
6104
+ body
6105
+ });
6106
+ }
6107
+ #request(method, path, options = {}) {
6108
+ const resolved = {
6109
+ method,
6110
+ path,
6111
+ body: options.body,
6112
+ headers: mergeHeaders(this.defaultHeaders, options.headers ?? {}),
6113
+ signal: options.signal,
6114
+ timeout: options.timeout === undefined ? this.timeout : assertPositiveMs("timeout", options.timeout),
6115
+ retry: resolveRetryPolicy(this.retry, options.retry)
6116
+ };
6117
+ const tag = `#${++this.#requestCount} ${method} ${path}`;
6118
+ return new APIPromise(this.fetchWithRetries(tag, resolved), async (res) => {
6119
+ const parsed = await parseBody(res);
6120
+ this.logger.debug(`${tag} <- body`, parsed);
6121
+ return parsed;
6122
+ });
6123
+ }
6124
+ async fetchWithRetries(tag, req) {
6125
+ const url = `${this.baseURL}${req.path}`;
6126
+ const headers = mergeHeaders(req.headers, {
6127
+ Authorization: `Bearer ${this.#apiKey}`,
6128
+ Accept: "application/json",
6129
+ "User-Agent": `typesafe-sdk/${VERSION}`,
6130
+ "X-TypeSafe-SDK": `typesafe-sdk/${VERSION}`,
6131
+ "X-TypeSafe-Runtime": RUNTIME,
6132
+ "Content-Type": req.body === undefined ? undefined : "application/json",
6133
+ "X-TypeSafe-Retry-Count": undefined
6134
+ });
6135
+ const body = req.body === undefined ? undefined : JSON.stringify(req.body);
6136
+ for (let attempt = 0;; attempt++) {
6137
+ const retriesLeft = req.retry.maxRetries - attempt;
6138
+ const attemptHeaders = attempt === 0 ? headers : {
6139
+ ...headers,
6140
+ "X-TypeSafe-Retry-Count": String(attempt)
6141
+ };
6142
+ this.logger.debug(`${tag} -> ${url}`, {
6143
+ headers: redactHeaders(attemptHeaders),
6144
+ body: req.body
6145
+ });
6146
+ const started = Date.now();
6147
+ let res;
6148
+ try {
6149
+ res = await this.attempt(tag, url, {
6150
+ method: req.method,
6151
+ headers: attemptHeaders,
6152
+ body
6153
+ }, req);
6154
+ } catch (err) {
6155
+ if (err instanceof APIUserAbortError || retriesLeft <= 0)
6156
+ throw err;
6157
+ if (!isRetryableError(err, req.retry))
6158
+ throw err;
6159
+ await this.backOff(tag, attempt, retriesLeft, err.message, undefined, req);
6160
+ continue;
6161
+ }
6162
+ const requestId = requestIdFrom(res.headers);
6163
+ this.logger.info(`${tag} <- ${res.status} in ${Date.now() - started}ms${requestId ? ` (request ${requestId})` : ""}`);
6164
+ if (res.ok)
6165
+ return res;
6166
+ const errorBody = await parseBody(res);
6167
+ this.logger.debug(`${tag} <- error body`, errorBody);
6168
+ const error = APIError.fromResponse(res.status, errorBody, res.headers);
6169
+ if (retriesLeft <= 0 || !isRetryableStatus(res.status, req.retry))
6170
+ throw error;
6171
+ await this.backOff(tag, attempt, retriesLeft, `${res.status}`, res.headers, req);
6172
+ }
6173
+ }
6174
+ async attempt(tag, url, init, { signal, timeout }) {
6175
+ const controller = new AbortController;
6176
+ const abortFromCaller = () => controller.abort(signal?.reason);
6177
+ if (signal?.aborted)
6178
+ abortFromCaller();
6179
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
6180
+ let timedOut = false;
6181
+ const timer = setTimeout(() => {
6182
+ timedOut = true;
6183
+ controller.abort();
6184
+ }, timeout);
6185
+ const started = Date.now();
6186
+ const elapsed = () => `${Date.now() - started}ms`;
6187
+ try {
6188
+ const response = await this.fetch(url, {
6189
+ ...init,
6190
+ signal: controller.signal
6191
+ });
6192
+ await bufferResponse(response, controller.signal);
6193
+ return response;
6194
+ } catch (err) {
6195
+ if (signal?.aborted) {
6196
+ this.logger.info(`${tag} aborted by caller after ${elapsed()}`);
6197
+ throw new APIUserAbortError(undefined, { cause: err });
6198
+ }
6199
+ if (timedOut) {
6200
+ this.logger.info(`${tag} timed out after ${elapsed()}`);
6201
+ throw new APITimeoutError(timeout, { cause: err });
6202
+ }
6203
+ this.logger.info(`${tag} connection error after ${elapsed()}`, err);
6204
+ throw new APIConnectionError(err instanceof Error ? `Connection error: ${err.message}` : undefined, { cause: err });
6205
+ } finally {
6206
+ clearTimeout(timer);
6207
+ signal?.removeEventListener("abort", abortFromCaller);
6208
+ }
6209
+ }
6210
+ async backOff(tag, attempt, retriesLeft, reason, headers, { retry, signal }) {
6211
+ const delay = retryDelayMs(attempt, headers, retry);
6212
+ const nth = attempt + 1;
6213
+ const total = attempt + retriesLeft;
6214
+ this.logger.info(`${tag} retrying in ${delay}ms (retry ${nth}/${total}) after ${reason}`);
6215
+ try {
6216
+ await sleep(delay, signal);
6217
+ } catch (err) {
6218
+ this.logger.info(`${tag} aborted by caller while waiting to retry`);
6219
+ throw new APIUserAbortError(undefined, { cause: err });
6220
+ }
6221
+ }
6222
+ };
6223
+ var parseBody = async (res) => {
6224
+ const text = await res.text();
6225
+ if (text.length === 0)
6226
+ return;
6227
+ if ((res.headers.get("content-type") ?? "").includes("application/json"))
6228
+ try {
6229
+ return JSON.parse(text);
6230
+ } catch {
6231
+ return text;
6232
+ }
6233
+ try {
6234
+ return JSON.parse(text);
6235
+ } catch {
6236
+ return text;
6237
+ }
6238
+ };
5471
6239
 
5472
6240
  // src/ui/masked-input.ts
5473
- import {
5474
- Input as Input2,
5475
- truncateToWidth as truncateToWidth4
5476
- } from "@earendil-works/pi-tui";
6241
+ import { Input as Input2, truncateToWidth as truncateToWidth4 } from "@earendil-works/pi-tui";
5477
6242
 
5478
6243
  class MaskedInput {
5479
6244
  input;
@@ -5515,19 +6280,36 @@ class MaskedInput {
5515
6280
  }
5516
6281
 
5517
6282
  // src/ui/jev-setup-submenu.ts
6283
+ var ACTION_LABELS = {
6284
+ disable: "Disable",
6285
+ "disable-clear": "Disable and clear stored key",
6286
+ done: "Done",
6287
+ "enter-key": "Enter a TypeSafe API key",
6288
+ "verify-again": "Verify again",
6289
+ "verify-enable": "Verify and enable"
6290
+ };
6291
+ var fireAndForget = async (action) => {
6292
+ try {
6293
+ await action;
6294
+ } catch {}
6295
+ };
5518
6296
  var transportLabel = (credentials) => {
5519
6297
  if (credentials.transport === "openrouter") {
5520
6298
  return "OpenRouter (reusing pi login)";
5521
6299
  }
5522
6300
  switch (credentials.source) {
5523
- case "advisor-json":
6301
+ case "advisor-json": {
5524
6302
  return "TypeSafe (key: advisor.json — plaintext, not recommended)";
5525
- case "bun-secrets":
6303
+ }
6304
+ case "bun-secrets": {
5526
6305
  return "TypeSafe (key: Bun.secrets)";
5527
- case "file":
6306
+ }
6307
+ case "file": {
5528
6308
  return "TypeSafe (key: stored file, mode 0600)";
5529
- default:
6309
+ }
6310
+ default: {
5530
6311
  return "TypeSafe (key: TYPESAFE_API_KEY)";
6312
+ }
5531
6313
  }
5532
6314
  };
5533
6315
  var defaultVerify = async (credentials) => {
@@ -5566,9 +6348,7 @@ class JevSetupSubmenu {
5566
6348
  onSubmit: (value) => this.submitEnteredKey(value),
5567
6349
  placeholder: "Paste a TypeSafe API key"
5568
6350
  });
5569
- this.refresh().catch(() => {
5570
- return;
5571
- });
6351
+ fireAndForget(this.refresh());
5572
6352
  }
5573
6353
  get focused() {
5574
6354
  return this._focused;
@@ -5609,24 +6389,20 @@ class JevSetupSubmenu {
5609
6389
  ""
5610
6390
  ];
5611
6391
  if (this.credentials) {
5612
- lines.push(` Transport: ${transportLabel(this.credentials)}`);
5613
- lines.push(` Filter: ${enabled ? "On" : "Off"}`);
6392
+ lines.push(` Transport: ${transportLabel(this.credentials)}`, ` Filter: ${enabled ? "On" : "Off"}`);
5614
6393
  } else if (this.mode === "verifying") {
5615
6394
  lines.push(" Checking available Jev credentials…");
5616
6395
  } 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.");
6396
+ lines.push(" No Jev credentials found. Enter a TypeSafe API key below, or add", " an OpenRouter login in pi; it is reused automatically.");
5619
6397
  }
5620
6398
  if (this.notice) {
5621
- lines.push("");
5622
- lines.push(theme.fg("warning", ` ${this.notice}`));
6399
+ lines.push("", theme.fg("warning", ` ${this.notice}`));
5623
6400
  }
5624
6401
  lines.push("");
5625
6402
  if (this.mode === "verifying") {
5626
6403
  lines.push(" Verifying with a live Jev call…");
5627
6404
  } 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"));
6405
+ lines.push(` ${this.maskedInput.render(Math.max(10, width - 4))[0] ?? ""}`, theme.fg("dim", " Enter: verify · Esc: cancel"));
5630
6406
  } else {
5631
6407
  for (const [index, label] of this.labels().entries()) {
5632
6408
  const prefix = index === this.selectedIndex ? "→ " : " ";
@@ -5648,19 +6424,8 @@ class JevSetupSubmenu {
5648
6424
  actions.push("done");
5649
6425
  return actions;
5650
6426
  }
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
6427
  labels() {
5662
- const labels = this.actionLabels();
5663
- return this.actions().map((action) => labels[action]);
6428
+ return this.actions().map((action) => ACTION_LABELS[action]);
5664
6429
  }
5665
6430
  async refresh() {
5666
6431
  const wasVerifying = this.mode === "verifying";
@@ -5677,30 +6442,25 @@ class JevSetupSubmenu {
5677
6442
  this.options.tui.requestRender();
5678
6443
  }
5679
6444
  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;
6445
+ if (action === "done") {
6446
+ this.options.done();
6447
+ return;
6448
+ }
6449
+ if (action === "enter-key") {
6450
+ this.mode = "key-entry";
6451
+ return;
6452
+ }
6453
+ if (action === "disable") {
6454
+ this.notice = undefined;
6455
+ this.options.done("Off");
6456
+ return;
6457
+ }
6458
+ if (action === "disable-clear") {
6459
+ fireAndForget(this.disableAndClear());
6460
+ return;
6461
+ }
6462
+ if (action === "verify-again" || action === "verify-enable") {
6463
+ fireAndForget(this.verifyAndEnable());
5704
6464
  }
5705
6465
  }
5706
6466
  async disableAndClear() {
@@ -5772,11 +6532,15 @@ class JevSetupSubmenu {
5772
6532
  }
5773
6533
  }
5774
6534
 
6535
+ // src/ui/model-multi-selector.ts
6536
+ class SearchableModelMultiSelector extends ModelSelectorAdapter {
6537
+ constructor(options) {
6538
+ super(new SearchableModelList(options));
6539
+ }
6540
+ }
6541
+
5775
6542
  // src/ui/text-setting-submenu.ts
5776
- import {
5777
- Input as Input3,
5778
- truncateToWidth as truncateToWidth6
5779
- } from "@earendil-works/pi-tui";
6543
+ import { Input as Input3, truncateToWidth as truncateToWidth6 } from "@earendil-works/pi-tui";
5780
6544
 
5781
6545
  class TextSettingSubmenu {
5782
6546
  input = new Input3;
@@ -5939,8 +6703,28 @@ var jevItems = (settings, theme, tui) => [
5939
6703
  ])
5940
6704
  }
5941
6705
  ];
6706
+ var advisorModelWhitelistItem = (settings, modelRefs, keybindings, theme, tui) => ({
6707
+ currentValue: settings.modelWhitelist?.length ? settings.modelWhitelist.join(", ") : "Any model",
6708
+ description: "Only the exact provider/model references listed here may call the Advisor; an empty list allows every model.",
6709
+ id: "modelWhitelist",
6710
+ label: "Advisor model whitelist",
6711
+ submenu: (_currentValue, done) => new SearchableModelMultiSelector({
6712
+ allOptions: [
6713
+ ...new Set([...modelRefs ?? [], ...settings.modelWhitelist ?? []])
6714
+ ],
6715
+ currentOptions: settings.modelWhitelist ?? [],
6716
+ keybindings: keybindings ?? getKeybindings(),
6717
+ multiSelect: true,
6718
+ onCancel: done,
6719
+ onSelect: (values) => done(values.join(",")),
6720
+ theme,
6721
+ title: "Advisor model whitelist",
6722
+ tui
6723
+ })
6724
+ });
5942
6725
  var createSettingsItems = ({
5943
6726
  effortLevels,
6727
+ modelWhitelist,
5944
6728
  presets,
5945
6729
  settings,
5946
6730
  theme,
@@ -5970,6 +6754,7 @@ var createSettingsItems = ({
5970
6754
  }
5971
6755
  ];
5972
6756
  if (settings.simpleMode) {
6757
+ items.push(modelWhitelist);
5973
6758
  return items;
5974
6759
  }
5975
6760
  items.push({
@@ -5978,7 +6763,7 @@ var createSettingsItems = ({
5978
6763
  id: "effort",
5979
6764
  label: "Advisor reasoning",
5980
6765
  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), {
6766
+ }, 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
6767
  currentValue: settings.customRule || "None",
5983
6768
  description: "Add a rule that triggers Advisor involvement.",
5984
6769
  id: "customRule",
@@ -6080,10 +6865,7 @@ var createSettingsItems = ({
6080
6865
  };
6081
6866
 
6082
6867
  // src/ui/settings-list-adapter.ts
6083
- import {
6084
- Key as Key3,
6085
- matchesKey as matchesKey3
6086
- } from "@earendil-works/pi-tui";
6868
+ import { Key as Key3, matchesKey as matchesKey3 } from "@earendil-works/pi-tui";
6087
6869
 
6088
6870
  class SettingsListAdapter {
6089
6871
  list;
@@ -6111,7 +6893,7 @@ class SettingsListAdapter {
6111
6893
  }
6112
6894
  setSelectedId(items, selectedId) {
6113
6895
  const selectedIndex = items.findIndex((item) => item.id === selectedId);
6114
- if (selectedIndex >= 0) {
6896
+ if (selectedIndex !== -1) {
6115
6897
  this.privateFields().selectedIndex = selectedIndex;
6116
6898
  }
6117
6899
  }
@@ -6154,102 +6936,160 @@ class SettingsListAdapter {
6154
6936
  }
6155
6937
 
6156
6938
  // 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",
6939
+ var BOOLEAN_SETTING_FIELDS = [
6166
6940
  "autoLoopGate",
6167
- "sessionSummary",
6941
+ "blockOnBlocked",
6942
+ "collapseResponses",
6943
+ "completionGate",
6944
+ "failureGate",
6168
6945
  "herdrIntegration",
6946
+ "outcomeLogging",
6947
+ "planGate",
6169
6948
  "redactSecrets",
6949
+ "scoutEnabled",
6950
+ "sessionSummary",
6951
+ "showUsageDetails",
6952
+ "showUsageFooter",
6170
6953
  "trackedFileContent",
6171
- "untrackedContent",
6172
- "outcomeLogging"
6173
- ]);
6174
- var mutateAdvisorSettings = (settings, id, value, presets) => {
6954
+ "untrackedContent"
6955
+ ];
6956
+ var BOOLEAN_SETTING_IDS = new Set(BOOLEAN_SETTING_FIELDS);
6957
+ var parseModelWhitelist = (value) => [
6958
+ ...new Set(value.split(",").map((model) => model.trim()).filter(Boolean))
6959
+ ];
6960
+ var applyCoreMutation = (settings, id, value, presets) => {
6175
6961
  switch (id) {
6176
- case "context":
6962
+ case "context": {
6177
6963
  settings.contextMaxChars = presets.find((preset) => preset.label === value)?.value ?? settings.contextMaxChars;
6178
- break;
6179
- case "simpleMode":
6964
+ return true;
6965
+ }
6966
+ case "simpleMode": {
6180
6967
  settings.simpleMode = value === "On";
6181
- break;
6182
- case "alwaysOn":
6968
+ return true;
6969
+ }
6970
+ case "alwaysOn": {
6183
6971
  settings.alwaysOn = value === "On";
6184
- break;
6185
- case "effort":
6972
+ return true;
6973
+ }
6974
+ case "effort": {
6186
6975
  settings.effort = value;
6187
- break;
6188
- case "customRule":
6976
+ return true;
6977
+ }
6978
+ case "customRule": {
6189
6979
  settings.customRule = value.trim() || undefined;
6190
- break;
6191
- case "toolPolicies":
6980
+ return true;
6981
+ }
6982
+ case "toolPolicies": {
6192
6983
  settings.toolPolicies = JSON.parse(value);
6193
- break;
6194
- case "loopThreshold":
6984
+ return true;
6985
+ }
6986
+ case "loopThreshold": {
6195
6987
  settings.loopThreshold = Number(value.replace("After ", "").replace(" repeats", ""));
6196
- break;
6197
- case "maxCallsPerSession":
6988
+ return true;
6989
+ }
6990
+ case "maxCallsPerSession": {
6198
6991
  settings.maxCallsPerSession = value === "∞" ? undefined : Number(value);
6199
- break;
6200
- case "failureMode":
6992
+ return true;
6993
+ }
6994
+ case "modelWhitelist": {
6995
+ settings.modelWhitelist = parseModelWhitelist(value);
6996
+ return true;
6997
+ }
6998
+ case "failureMode": {
6201
6999
  settings.failureMode = value;
6202
- break;
6203
- case "gitContext":
7000
+ return true;
7001
+ }
7002
+ case "gitContext": {
6204
7003
  settings.gitContext = value;
6205
- break;
6206
- case "toolResultMaxLines":
7004
+ return true;
7005
+ }
7006
+ default: {
7007
+ return false;
7008
+ }
7009
+ }
7010
+ };
7011
+ var applyNumericMutation = (settings, id, value) => {
7012
+ switch (id) {
7013
+ case "toolResultMaxLines": {
6207
7014
  settings.toolResultMaxLines = Number(value);
6208
- break;
6209
- case "toolResultMaxBytes":
7015
+ return true;
7016
+ }
7017
+ case "toolResultMaxBytes": {
6210
7018
  settings.toolResultMaxBytes = Number(value);
6211
- break;
6212
- case "gitContextMaxChars":
7019
+ return true;
7020
+ }
7021
+ case "gitContextMaxChars": {
6213
7022
  settings.gitContextMaxChars = Number(value);
6214
- break;
6215
- case "jevFilter":
7023
+ return true;
7024
+ }
7025
+ default: {
7026
+ return false;
7027
+ }
7028
+ }
7029
+ };
7030
+ var applyJevMutation = (settings, id, value) => {
7031
+ switch (id) {
7032
+ case "jevFilter": {
6216
7033
  settings.jevFilterEnabled = value === "On";
6217
- break;
6218
- case "jevFilterSkipConfidence":
7034
+ return true;
7035
+ }
7036
+ case "jevFilterSkipConfidence": {
6219
7037
  settings.jevFilterSkipConfidence = Number(value);
6220
- break;
6221
- case "jevFilterNoulMargin":
7038
+ return true;
7039
+ }
7040
+ case "jevFilterNoulMargin": {
6222
7041
  settings.jevFilterNoulMargin = Number(value);
6223
- break;
6224
- case "jevFilterOverrideWindow":
7042
+ return true;
7043
+ }
7044
+ case "jevFilterOverrideWindow": {
6225
7045
  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":
7046
+ return true;
7047
+ }
7048
+ case "jevTurnGateEveryTurns": {
7049
+ settings.jevTurnGateEveryTurns = value === "Off" ? 0 : Number(value.replaceAll(/[^0-9]/gu, ""));
7050
+ return true;
7051
+ }
7052
+ case "jevTurnGateNoulThreshold": {
6231
7053
  settings.jevTurnGateNoulThreshold = Number(value);
6232
- break;
6233
- case "jevModel":
7054
+ return true;
7055
+ }
7056
+ case "jevModel": {
6234
7057
  settings.jevModel = value.trim() || DEFAULT_JEV_MODEL;
6235
- break;
6236
- case "jevTimeoutMs":
7058
+ return true;
7059
+ }
7060
+ case "jevTimeoutMs": {
6237
7061
  settings.jevTimeoutMs = Number(value);
6238
- break;
6239
- case "jevDigestMaxChars":
7062
+ return true;
7063
+ }
7064
+ case "jevDigestMaxChars": {
6240
7065
  settings.jevDigestMaxChars = Number(value);
6241
- break;
6242
- case "jevPricePerMtok":
7066
+ return true;
7067
+ }
7068
+ case "jevPricePerMtok": {
6243
7069
  settings.jevPricePerMtok = Number(value);
6244
- break;
6245
- case "jevTransport":
7070
+ return true;
7071
+ }
7072
+ case "jevTransport": {
6246
7073
  settings.jevTransport = value;
6247
- break;
6248
- default:
6249
- if (BOOLEAN_SETTING_IDS.has(id)) {
6250
- settings[id] = value === "On";
6251
- }
6252
- break;
7074
+ return true;
7075
+ }
7076
+ default: {
7077
+ return false;
7078
+ }
7079
+ }
7080
+ };
7081
+ var mutateAdvisorSettings = (settings, id, value, presets) => {
7082
+ if (applyCoreMutation(settings, id, value, presets)) {
7083
+ return;
7084
+ }
7085
+ if (applyNumericMutation(settings, id, value)) {
7086
+ return;
7087
+ }
7088
+ if (applyJevMutation(settings, id, value)) {
7089
+ return;
7090
+ }
7091
+ if (BOOLEAN_SETTING_IDS.has(id)) {
7092
+ settings[id] = value === "On";
6253
7093
  }
6254
7094
  };
6255
7095
 
@@ -6280,7 +7120,7 @@ class AdvisorSettingsSelector {
6280
7120
  label: String(configuredContext),
6281
7121
  value: configuredContext
6282
7122
  }
6283
- ].sort((a, b) => a.value - b.value);
7123
+ ].toSorted((a, b) => a.value - b.value);
6284
7124
  if (this.settings.simpleMode) {
6285
7125
  this.startSimpleModeGradient();
6286
7126
  }
@@ -6311,8 +7151,10 @@ class AdvisorSettingsSelector {
6311
7151
  }
6312
7152
  return defaultLabel(text, selected);
6313
7153
  };
7154
+ const modelWhitelist = advisorModelWhitelistItem(this.settings, this.options.modelRefs, this.options.keybindings, this.options.theme, this.options.tui);
6314
7155
  const items = createSettingsItems({
6315
7156
  effortLevels: this.options.effortLevels,
7157
+ modelWhitelist,
6316
7158
  presets: this.presets,
6317
7159
  settings: this.settings,
6318
7160
  theme: this.options.theme,
@@ -6352,16 +7194,16 @@ class AdvisorSettingsSelector {
6352
7194
  (this.options.onChange ?? this.options.onSave)?.({
6353
7195
  ...this.settings,
6354
7196
  showUsageDetails: this.settings.showUsageDetails ?? true,
6355
- toolPolicies: { ...this.settings.toolPolicies ?? {} }
7197
+ toolPolicies: { ...this.settings.toolPolicies }
6356
7198
  });
6357
- if (id === "context" || id === "simpleMode" || id === "customRule" || id === "toolPolicies") {
7199
+ if (id === "context" || id === "simpleMode" || id === "customRule" || id === "modelWhitelist" || id === "toolPolicies") {
6358
7200
  this.settingsList = this.createSettingsList(id);
6359
7201
  }
6360
7202
  }
6361
7203
  }
6362
7204
 
6363
7205
  // src/commands/settings-persistence.ts
6364
- var applyAdvisorSettings = (settings) => {
7206
+ var applySessionSettings = (settings) => {
6365
7207
  setAdvisorEffortRef(settings.effort === "Default (Model Default)" ? undefined : settings.effort);
6366
7208
  setContextMaxCharsRef(settings.contextMaxChars);
6367
7209
  setAdvisorPlanGateRef(settings.planGate);
@@ -6373,6 +7215,7 @@ var applyAdvisorSettings = (settings) => {
6373
7215
  setAdvisorAutoLoopGateRef(settings.autoLoopGate ?? true);
6374
7216
  setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
6375
7217
  setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
7218
+ setAdvisorModelWhitelistRef(settings.modelWhitelist ?? []);
6376
7219
  setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
6377
7220
  setAdvisorScoutEnabledRef(settings.scoutEnabled ?? false);
6378
7221
  setShowUsageDetailsRef(settings.showUsageDetails ?? true);
@@ -6381,6 +7224,8 @@ var applyAdvisorSettings = (settings) => {
6381
7224
  setAlwaysOnRef(settings.alwaysOn ?? false);
6382
7225
  setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
6383
7226
  setAdvisorHerdrIntegrationRef(settings.herdrIntegration ?? true);
7227
+ };
7228
+ var applyJevSettings = (settings) => {
6384
7229
  setAdvisorJevFilterEnabledRef(settings.jevFilterEnabled ?? false);
6385
7230
  setAdvisorJevFilterSkipConfidenceRef(settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE);
6386
7231
  setAdvisorJevFilterNoulMarginRef(settings.jevFilterNoulMargin ?? DEFAULT_JEV_FILTER_NOUL_MARGIN);
@@ -6392,6 +7237,8 @@ var applyAdvisorSettings = (settings) => {
6392
7237
  setAdvisorJevTransportRef(settings.jevTransport ?? DEFAULT_JEV_TRANSPORT);
6393
7238
  setAdvisorJevTurnGateEveryTurnsRef(settings.jevTurnGateEveryTurns ?? DEFAULT_JEV_TURN_GATE_EVERY_TURNS);
6394
7239
  setAdvisorJevTurnGateNoulThresholdRef(settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD);
7240
+ };
7241
+ var applyDisclosureSettings = (settings) => {
6395
7242
  setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
6396
7243
  setAdvisorToolResultMaxBytesRef(settings.toolResultMaxBytes ?? 50 * 1024);
6397
7244
  setAdvisorRedactSecretsRef(settings.redactSecrets ?? false);
@@ -6402,6 +7249,11 @@ var applyAdvisorSettings = (settings) => {
6402
7249
  setAdvisorUntrackedContentRef(settings.untrackedContent ?? false);
6403
7250
  setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
6404
7251
  };
7252
+ var applyAdvisorSettings = (settings) => {
7253
+ applySessionSettings(settings);
7254
+ applyJevSettings(settings);
7255
+ applyDisclosureSettings(settings);
7256
+ };
6405
7257
  var saveAdvisorSettings = (ctx, settings) => {
6406
7258
  applyAdvisorSettings(settings);
6407
7259
  const persisted = getPersistedModelRefs();
@@ -6421,10 +7273,12 @@ var registerSettingsCommands = (runtime) => {
6421
7273
  return;
6422
7274
  }
6423
7275
  const initial = getAdvisorSettings();
6424
- await ctx.ui.custom((tui, theme, _keybindings, done) => new AdvisorSettingsSelector({
7276
+ await ctx.ui.custom((tui, theme, keybindings, done) => new AdvisorSettingsSelector({
6425
7277
  effortLevels: EFFORT_LEVELS,
6426
7278
  initial,
6427
- onCancel: () => done(),
7279
+ keybindings,
7280
+ modelRefs: getConfiguredModelRefs(ctx),
7281
+ onCancel: () => done(undefined),
6428
7282
  onChange: (settings) => {
6429
7283
  try {
6430
7284
  saveAdvisorSettings(ctx, settings);
@@ -6484,6 +7338,7 @@ import {
6484
7338
  writeFile
6485
7339
  } from "node:fs/promises";
6486
7340
  import { join as join5 } from "node:path";
7341
+ import { setTimeout as sleep2 } from "node:timers/promises";
6487
7342
  import { getAgentDir as getAgentDir4 } from "@earendil-works/pi-coding-agent";
6488
7343
  var ADOPTIONS = [
6489
7344
  "followed",
@@ -6499,6 +7354,7 @@ var VALIDATIONS = [
6499
7354
  var MAX_LOG_BYTES = 1024 * 1024;
6500
7355
  var statePath = () => join5(getAgentDir4(), "advisor-outcomes-salt");
6501
7356
  var outcomeLogPath = () => join5(getAgentDir4(), "advisor-outcomes.jsonl");
7357
+ var isErrnoException = (error) => error instanceof Error && ("code" in error);
6502
7358
  var salt = async () => {
6503
7359
  const path = statePath();
6504
7360
  await mkdir(getAgentDir4(), { mode: 448, recursive: true });
@@ -6510,7 +7366,7 @@ var salt = async () => {
6510
7366
  }
6511
7367
  await unlink(path);
6512
7368
  } catch (error) {
6513
- if (error.code !== "ENOENT") {
7369
+ if (!isErrnoException(error) || error.code !== "ENOENT") {
6514
7370
  throw error;
6515
7371
  }
6516
7372
  }
@@ -6521,7 +7377,7 @@ var salt = async () => {
6521
7377
  await link(temporary, path);
6522
7378
  return value;
6523
7379
  } catch (error) {
6524
- if (error.code !== "EEXIST") {
7380
+ if (!isErrnoException(error) || error.code !== "EEXIST") {
6525
7381
  throw error;
6526
7382
  }
6527
7383
  } finally {
@@ -6553,7 +7409,7 @@ var withOutcomeLock = async (run) => {
6553
7409
  }
6554
7410
  }
6555
7411
  } catch (error) {
6556
- if (error.code !== "EEXIST") {
7412
+ if (!isErrnoException(error) || error.code !== "EEXIST") {
6557
7413
  throw error;
6558
7414
  }
6559
7415
  const observed = await stat(lockPath).catch(() => {
@@ -6570,7 +7426,7 @@ var withOutcomeLock = async (run) => {
6570
7426
  }
6571
7427
  continue;
6572
7428
  }
6573
- await new Promise((resolve) => setTimeout(resolve, 5));
7429
+ await sleep2(5);
6574
7430
  }
6575
7431
  }
6576
7432
  throw new Error("Timed out waiting to append an Advisor outcome.");
@@ -6596,11 +7452,8 @@ var appendOutcome = async (record) => {
6596
7452
  }
6597
7453
  throw error;
6598
7454
  });
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
- }
7455
+ const overflow = currentBytes + Buffer.byteLength(line) > MAX_LOG_BYTES;
7456
+ await (overflow ? writeFile(path, line, { encoding: "utf-8", mode: 384 }) : appendFile(path, line, { encoding: "utf-8", mode: 384 }));
6604
7457
  await chmod(path, 384);
6605
7458
  return next;
6606
7459
  });
@@ -6698,7 +7551,7 @@ var handleJevTurnEnd = async (registration, ctx) => {
6698
7551
  if (interval <= 0 || session.turnsSinceConsultation <= 0 || session.turnsSinceConsultation % interval !== 0) {
6699
7552
  return;
6700
7553
  }
6701
- if (isSimpleMode() || session.blocked || !registration.activeTools().includes("ask_advisor") || !session.canConsult(getAdvisorMaxCallsPerSession())) {
7554
+ if (isSimpleMode() || session.blocked || !registration.activeTools().includes("ask_advisor") || !advisorModelIsAllowed(ctx) || !session.canConsult(getAdvisorMaxCallsPerSession())) {
6702
7555
  return;
6703
7556
  }
6704
7557
  const consult = registration.deps?.consult ?? registration.consult;
@@ -6758,7 +7611,7 @@ var handleJevTurnEnd = async (registration, ctx) => {
6758
7611
  }
6759
7612
  } catch (error) {
6760
7613
  session.recordJevGateFailure();
6761
- if (error instanceof JevFailure) {
7614
+ if (error instanceof JevFailureError) {
6762
7615
  notifyFailureOnce(ctx, error.category, error.message);
6763
7616
  } else if (!ctx.signal?.aborted) {
6764
7617
  notifyFailureOnce(ctx, "error", error instanceof Error ? error.message : String(error));
@@ -6773,9 +7626,7 @@ var registerJevTurnGate = (on, registration) => {
6773
7626
  import { Type } from "typebox";
6774
7627
 
6775
7628
  // src/tools/render-advisor-result.ts
6776
- import {
6777
- getMarkdownTheme as getMarkdownTheme4
6778
- } from "@earendil-works/pi-coding-agent";
7629
+ import { getMarkdownTheme as getMarkdownTheme4 } from "@earendil-works/pi-coding-agent";
6779
7630
  import { Box as Box3, Markdown as Markdown4, Spacer, Text as Text5 } from "@earendil-works/pi-tui";
6780
7631
  var advisorResultDetails = (result) => result.details;
6781
7632
  var syncRenderPhase = (context, phase) => {
@@ -6832,6 +7683,24 @@ var renderPartialAdvisorResult = (box, result, expanded, theme, context) => {
6832
7683
  box.addChild(new Markdown4(adviceForDisplay(details.text, expanded), 0, 0, getMarkdownTheme4()));
6833
7684
  }
6834
7685
  };
7686
+ var thinkingPreview = (details) => details?.thinking?.trim() ? `${details.thinking.slice(0, 300)}${details.thinking.length > 300 ? "…" : ""}` : "";
7687
+ var finalResultLines = (details, advice, theme) => {
7688
+ const lines = [renderAdvisorResponseHeader(hasSoundVerdict(advice), theme)];
7689
+ if (details?.advisor) {
7690
+ lines.push(theme.fg("dim", ` ${details.advisor}`));
7691
+ }
7692
+ if (getAdvisorSettings().showUsageDetails) {
7693
+ const usage = formatAdvisorUsage(details?.usage);
7694
+ if (usage) {
7695
+ lines.push(theme.fg("dim", ` Usage: ${usage}`));
7696
+ }
7697
+ }
7698
+ const attachments = attachmentLabels(details);
7699
+ if (attachments.length) {
7700
+ lines.push(theme.fg("dim", ` ${attachments.join(" · ")}`));
7701
+ }
7702
+ return lines;
7703
+ };
6835
7704
  var renderFinalAdvisorResult = (box, result, expanded, theme, context) => {
6836
7705
  syncRenderPhase(context, "final");
6837
7706
  if (context.state.timerId) {
@@ -6854,21 +7723,8 @@ var renderFinalAdvisorResult = (box, result, expanded, theme, context) => {
6854
7723
  return;
6855
7724
  }
6856
7725
  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 ? "…" : ""}` : "";
7726
+ const thinking = thinkingPreview(details);
7727
+ const lines = finalResultLines(details, advice, theme);
6872
7728
  const displayAdvice = advice || "(Advisor returned no advice.)";
6873
7729
  box.addChild(new Text5(lines.join(`
6874
7730
  `), 0, 0));
@@ -6891,6 +7747,12 @@ var renderAdvisorResult = (result, { isPartial, expanded }, theme, context) => {
6891
7747
  };
6892
7748
 
6893
7749
  // src/tools/register-ask-advisor.ts
7750
+ var assertAdvisorModelAccess = (ctx) => {
7751
+ const accessReason = advisorModelAccessReason(ctx);
7752
+ if (accessReason) {
7753
+ throw new Error(accessReason);
7754
+ }
7755
+ };
6894
7756
  var claimTrackedHandoff = (session, includeTrackedFiles) => {
6895
7757
  if (!includeTrackedFiles?.length) {
6896
7758
  return;
@@ -6913,6 +7775,7 @@ var registerAskAdvisorTool = ({
6913
7775
  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
7776
  async execute(_id, params, signal, onUpdate, ctx) {
6915
7777
  reservedCalls.delete(_id);
7778
+ assertAdvisorModelAccess(ctx);
6916
7779
  if (!(isSimpleMode() || session.canConsult(getAdvisorMaxCallsPerSession()))) {
6917
7780
  throw new Error("Advisor call budget exhausted for this session.");
6918
7781
  }
@@ -6985,7 +7848,22 @@ var registerAskAdvisorTool = ({
6985
7848
  const usage = snapshotAdvisorUsage(result.usage);
6986
7849
  const piUsage = advisorUsageForPi(result.usage);
6987
7850
  updateAdvisorUsageStatus(ctx, session);
6988
- return {
7851
+ const details = {
7852
+ adviceId: result.adviceId,
7853
+ advisor: result.model,
7854
+ draftBytes: result.draftBytes,
7855
+ preferenceBytes: result.preferenceBytes,
7856
+ question: resolveAdvisorRequest(params.question),
7857
+ scout: scoutDetails,
7858
+ text: result.markdown,
7859
+ thinking: result.thinkingText,
7860
+ trackedBytes: result.trackedBytes,
7861
+ untrackedBytes: result.untrackedBytes
7862
+ };
7863
+ if (usage) {
7864
+ details.usage = usage;
7865
+ }
7866
+ const response = {
6989
7867
  content: [
6990
7868
  {
6991
7869
  text: `Advisor (${result.model})
@@ -6994,21 +7872,12 @@ ${result.markdown}`,
6994
7872
  type: "text"
6995
7873
  }
6996
7874
  ],
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 } : {}
7875
+ details
7011
7876
  };
7877
+ if (piUsage) {
7878
+ response.usage = piUsage;
7879
+ }
7880
+ return response;
7012
7881
  } catch (error) {
7013
7882
  coalescedUpdate.flush();
7014
7883
  const message = error instanceof Error ? error.message : String(error);
@@ -7076,27 +7945,31 @@ var sendAutomaticGateCall = (pi, event) => {
7076
7945
  }, { deliverAs: "steer" });
7077
7946
  };
7078
7947
  var sendAutomaticGateFailure = (pi, markdown, usage) => {
7079
- const normalizedUsage = snapshotAdvisorUsage(usage);
7948
+ const details = { text: markdown };
7949
+ if (usage) {
7950
+ details.usage = usage;
7951
+ }
7080
7952
  pi.sendMessage({
7081
7953
  content: markdown,
7082
7954
  customType: "advisor-loop-result",
7083
- details: {
7084
- text: markdown,
7085
- ...normalizedUsage ? { usage: normalizedUsage } : {}
7086
- },
7955
+ details,
7087
7956
  display: true
7088
7957
  }, { deliverAs: "steer" });
7089
7958
  };
7090
7959
  var sendAutomaticGateResult = (pi, result) => {
7960
+ const details = {
7961
+ advisor: result.model,
7962
+ decision: result.decision,
7963
+ text: result.markdown
7964
+ };
7965
+ const normalizedUsage = snapshotAdvisorUsage(result.usage);
7966
+ if (normalizedUsage) {
7967
+ details.usage = normalizedUsage;
7968
+ }
7091
7969
  pi.sendMessage({
7092
7970
  content: adviceForGateText(result),
7093
7971
  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
- },
7972
+ details,
7100
7973
  display: true
7101
7974
  }, { deliverAs: "steer" });
7102
7975
  };
@@ -7112,7 +7985,7 @@ var applyGateDecision = (pi, ctx, session, result, reason, failureMode) => {
7112
7985
  });
7113
7986
  updateAdvisorUsageStatus(ctx, session);
7114
7987
  const failure = failureEffect(result.category, result.message, ctx, session, failureMode);
7115
- sendAutomaticGateFailure(pi, `**Advisor gate failure (${result.category}):** ${result.message}`, result.usage);
7988
+ sendAutomaticGateFailure(pi, `**Advisor gate failure (${result.category}):** ${result.message}`, snapshotAdvisorUsage(result.usage));
7116
7989
  return failure.block ? { block: true, reason: `${reason}
7117
7990
  ${failure.reason}` } : undefined;
7118
7991
  }
@@ -7139,7 +8012,7 @@ ${failure.reason}` } : undefined;
7139
8012
  return { block: true, reason: gateReason };
7140
8013
  };
7141
8014
  var handleAutomaticGate = async (pi, event, ctx, session, runGate, scoutStatus) => {
7142
- if (isSimpleMode() || event.toolName === "ask_advisor" || !advisorAutoLoopGateRef || !session.recordToolCall(event.toolName, event.input, advisorLoopThresholdRef)) {
8015
+ if (isSimpleMode() || event.toolName === "ask_advisor" || !advisorModelIsAllowed(ctx) || !advisorAutoLoopGateRef || !session.recordToolCall(event.toolName, event.input, advisorLoopThresholdRef)) {
7143
8016
  return;
7144
8017
  }
7145
8018
  const reason = `Advisor loop gate: normalized signature for ${event.toolName} repeated ${advisorLoopThresholdRef} times without a materially different tool action.`;
@@ -7181,6 +8054,16 @@ var handleAutomaticGate = async (pi, event, ctx, session, runGate, scoutStatus)
7181
8054
  };
7182
8055
 
7183
8056
  // src/tools/register-lifecycle.ts
8057
+ var modelAccessBlock = (toolName, ctx) => {
8058
+ const access = advisorModelAccess(ctx);
8059
+ if (access.allowed || toolName !== "ask_advisor") {
8060
+ return;
8061
+ }
8062
+ return {
8063
+ block: true,
8064
+ reason: access.reason
8065
+ };
8066
+ };
7184
8067
  var registerToolLifecycle = ({
7185
8068
  pi,
7186
8069
  reservedCalls,
@@ -7218,6 +8101,9 @@ var registerToolLifecycle = ({
7218
8101
  return;
7219
8102
  }
7220
8103
  loadConfig(ctx);
8104
+ if (!advisorModelAccess(ctx).allowed) {
8105
+ return;
8106
+ }
7221
8107
  const guidelines = advisorInvocationGuidelines();
7222
8108
  const budget = isSimpleMode() ? undefined : session.remainingCalls(getAdvisorMaxCallsPerSession());
7223
8109
  if (budget !== undefined) {
@@ -7245,6 +8131,10 @@ ${guidelines.map((rule) => `- ${rule}`).join(`
7245
8131
  if (!loadConfigOrSkipGating(ctx)) {
7246
8132
  return;
7247
8133
  }
8134
+ const accessBlock = modelAccessBlock(event.toolName, ctx);
8135
+ if (accessBlock) {
8136
+ return accessBlock;
8137
+ }
7248
8138
  const reservation = reserveAdvisorCall(event, ctx, session, reservedCalls);
7249
8139
  if (event.toolName === "ask_advisor") {
7250
8140
  return reservation;
@@ -7338,65 +8228,59 @@ var registerOutcomeTool = ({
7338
8228
  };
7339
8229
 
7340
8230
  // src/tools/register-renderers.ts
7341
- import {
7342
- getMarkdownTheme as getMarkdownTheme5
7343
- } from "@earendil-works/pi-coding-agent";
8231
+ import { getMarkdownTheme as getMarkdownTheme5 } from "@earendil-works/pi-coding-agent";
7344
8232
  import { Box as Box4, Markdown as Markdown5, Text as Text7 } from "@earendil-works/pi-tui";
8233
+ var addUsageLine = (box, details, theme) => {
8234
+ if (!getAdvisorSettings().showUsageDetails) {
8235
+ return;
8236
+ }
8237
+ const usageText = formatAdvisorUsage(details?.usage);
8238
+ if (usageText) {
8239
+ box.addChild(new Text7(theme.fg("dim", ` Usage: ${usageText}`), 0, 0));
8240
+ }
8241
+ };
8242
+ var callQuestionRenderer = (message, _options, theme) => renderAdvisorCallBox(message.details?.question, theme);
8243
+ var scoutResultRenderer = (entry, { expanded }, theme) => {
8244
+ const box = new Box4(1, 1, (text) => theme.bg("customMessageBg", text));
8245
+ renderScoutDetails(box, entry.data, Boolean(expanded), theme);
8246
+ return box;
8247
+ };
8248
+ var turnGateResultRenderer = (message, { expanded }, theme) => {
8249
+ const { details } = message;
8250
+ const box = new Box4(1, 1, (text) => theme.bg("customMessageBg", text));
8251
+ box.addChild(new Text7(theme.fg("warning", theme.bold("◆ ADVISOR · TURN REVIEW")), 0, 0));
8252
+ if (details?.advisor) {
8253
+ box.addChild(new Text7(theme.fg("dim", ` ${details.advisor}`), 0, 0));
8254
+ }
8255
+ addUsageLine(box, details, theme);
8256
+ if (details?.text) {
8257
+ box.addChild(new Markdown5(adviceForDisplay(details.text, Boolean(expanded)), 0, 0, getMarkdownTheme5()));
8258
+ } else {
8259
+ box.addChild(new Text7(theme.fg("error", isString(message.content) ? message.content : "Advisor turn review failed."), 0, 0));
8260
+ }
8261
+ return box;
8262
+ };
8263
+ var loopResultRenderer = (message, { expanded }, theme) => {
8264
+ const { details } = message;
8265
+ const box = new Box4(1, 1, (text) => theme.bg("customMessageBg", text));
8266
+ box.addChild(new Text7(theme.fg("warning", theme.bold(`◆ ADVISOR GATE: ${details?.decision ?? "failure"}`)), 0, 0));
8267
+ if (details?.advisor) {
8268
+ box.addChild(new Text7(theme.fg("dim", ` ${details.advisor}`), 0, 0));
8269
+ }
8270
+ addUsageLine(box, details, theme);
8271
+ if (details?.text) {
8272
+ box.addChild(new Markdown5(adviceForDisplay(details.text, Boolean(expanded)), 0, 0, getMarkdownTheme5()));
8273
+ } else {
8274
+ box.addChild(new Text7(theme.fg("error", isString(message.content) ? message.content : "Advisor gate failed."), 0, 0));
8275
+ }
8276
+ return box;
8277
+ };
7345
8278
  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
- });
8279
+ pi.registerEntryRenderer?.("advisor-scout-result", scoutResultRenderer);
8280
+ pi.registerMessageRenderer?.("advisor-turn-gate-call", callQuestionRenderer);
8281
+ pi.registerMessageRenderer?.("advisor-turn-gate-result", turnGateResultRenderer);
8282
+ pi.registerMessageRenderer?.("advisor-loop-call", callQuestionRenderer);
8283
+ pi.registerMessageRenderer?.("advisor-loop-result", loopResultRenderer);
7400
8284
  };
7401
8285
 
7402
8286
  // src/tools/registration.ts
@@ -7415,20 +8299,23 @@ var registerAdvisorTool = (pi, session = advisorSessionState, dependencies = {})
7415
8299
  registerToolLifecycle(registration);
7416
8300
  registerAskAdvisorTool(registration);
7417
8301
  registerOutcomeTool(registration);
7418
- registerJevTurnGate((event, handler) => pi.on(event, handler), {
8302
+ const turnGate = {
7419
8303
  activeTools: () => pi.getActiveTools(),
7420
8304
  consult: registration.consult,
7421
- ...dependencies.turnGateDeps ? { deps: dependencies.turnGateDeps } : {},
7422
8305
  send: (message) => pi.sendMessage(message, { deliverAs: "steer" }),
7423
8306
  session
7424
- });
8307
+ };
8308
+ if (dependencies.turnGateDeps) {
8309
+ turnGate.deps = dependencies.turnGateDeps;
8310
+ }
8311
+ registerJevTurnGate((event, handler) => pi.on(event, handler), turnGate);
7425
8312
  };
7426
8313
 
7427
8314
  // extensions/index.ts
7428
8315
  var consultAdvisor2 = (...args) => consultAdvisor(...args);
7429
8316
  var parseAutomaticDecision2 = (...args) => parseAutomaticDecision(...args);
7430
8317
  var runAdvisorGate2 = (...args) => runAdvisorGate(...args);
7431
- function extensions_default(pi) {
8318
+ function registerPiAdvisor(pi) {
7432
8319
  const sessionState = new AdvisorSessionState;
7433
8320
  const scoutStatus = new ScoutStatusManager;
7434
8321
  setHerdrBlockedEmitter((active, label) => pi.events.emit("herdr:blocked", { active, label }));
@@ -7441,8 +8328,8 @@ function extensions_default(pi) {
7441
8328
  });
7442
8329
  }
7443
8330
  export {
7444
- consultAdvisor2 as consultAdvisor,
7445
- extensions_default as default,
8331
+ runAdvisorGate2 as runAdvisorGate,
7446
8332
  parseAutomaticDecision2 as parseAutomaticDecision,
7447
- runAdvisorGate2 as runAdvisorGate
8333
+ registerPiAdvisor as default,
8334
+ consultAdvisor2 as consultAdvisor
7448
8335
  };