pi-advisor-flow 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,6 +8,21 @@ var MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
8
8
  var DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = DEFAULT_MAX_LINES;
9
9
  var DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES = DEFAULT_MAX_BYTES;
10
10
  var DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS = 20000;
11
+ var DEFAULT_JEV_MODEL = "jev-latest";
12
+ var DEFAULT_JEV_TIMEOUT_MS = 8000;
13
+ var DEFAULT_JEV_DIGEST_MAX_CHARS = 4000;
14
+ var DEFAULT_JEV_PRICE_PER_MTOK = 0.042;
15
+ var DEFAULT_JEV_FILTER_SKIP_CONFIDENCE = 0.85;
16
+ var DEFAULT_JEV_FILTER_NOUL_MARGIN = 0.35;
17
+ var DEFAULT_JEV_FILTER_OVERRIDE_WINDOW = 10;
18
+ var DEFAULT_JEV_TURN_GATE_EVERY_TURNS = 0;
19
+ var DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD = 0.8;
20
+ var DEFAULT_JEV_TRANSPORT = "auto";
21
+ var JEV_TRANSPORTS = [
22
+ "auto",
23
+ "typesafe",
24
+ "openrouter"
25
+ ];
11
26
  var ADVISOR_TOOL_POLICIES = [
12
27
  "full",
13
28
  "summary",
@@ -41,6 +56,17 @@ var simpleModeRef = false;
41
56
  var alwaysOnRef = false;
42
57
  var advisorFailureModeRef = "block-session";
43
58
  var advisorHerdrIntegrationRef = true;
59
+ var advisorJevFilterEnabledRef = false;
60
+ var advisorJevFilterSkipConfidenceRef = DEFAULT_JEV_FILTER_SKIP_CONFIDENCE;
61
+ var advisorJevFilterNoulMarginRef = DEFAULT_JEV_FILTER_NOUL_MARGIN;
62
+ var advisorJevFilterOverrideWindowRef = DEFAULT_JEV_FILTER_OVERRIDE_WINDOW;
63
+ var advisorJevModelRef = DEFAULT_JEV_MODEL;
64
+ var advisorJevTimeoutMsRef = DEFAULT_JEV_TIMEOUT_MS;
65
+ var advisorJevDigestMaxCharsRef = DEFAULT_JEV_DIGEST_MAX_CHARS;
66
+ var advisorJevPricePerMtokRef = DEFAULT_JEV_PRICE_PER_MTOK;
67
+ var advisorJevTransportRef = DEFAULT_JEV_TRANSPORT;
68
+ var advisorJevTurnGateEveryTurnsRef = DEFAULT_JEV_TURN_GATE_EVERY_TURNS;
69
+ var advisorJevTurnGateNoulThresholdRef = DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD;
44
70
  var advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
45
71
  var advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
46
72
  var advisorRedactSecretsRef = false;
@@ -119,6 +145,39 @@ var setAdvisorFailureModeRef = (value) => {
119
145
  var setAdvisorHerdrIntegrationRef = (enabled) => {
120
146
  advisorHerdrIntegrationRef = enabled;
121
147
  };
148
+ var setAdvisorJevFilterEnabledRef = (enabled) => {
149
+ advisorJevFilterEnabledRef = enabled;
150
+ };
151
+ var setAdvisorJevFilterSkipConfidenceRef = (value) => {
152
+ advisorJevFilterSkipConfidenceRef = value;
153
+ };
154
+ var setAdvisorJevFilterNoulMarginRef = (value) => {
155
+ advisorJevFilterNoulMarginRef = value;
156
+ };
157
+ var setAdvisorJevFilterOverrideWindowRef = (value) => {
158
+ advisorJevFilterOverrideWindowRef = value;
159
+ };
160
+ var setAdvisorJevModelRef = (model) => {
161
+ advisorJevModelRef = model?.trim() || DEFAULT_JEV_MODEL;
162
+ };
163
+ var setAdvisorJevTimeoutMsRef = (value) => {
164
+ advisorJevTimeoutMsRef = value;
165
+ };
166
+ var setAdvisorJevDigestMaxCharsRef = (value) => {
167
+ advisorJevDigestMaxCharsRef = value;
168
+ };
169
+ var setAdvisorJevPricePerMtokRef = (value) => {
170
+ advisorJevPricePerMtokRef = value;
171
+ };
172
+ var setAdvisorJevTransportRef = (value) => {
173
+ advisorJevTransportRef = value;
174
+ };
175
+ var setAdvisorJevTurnGateEveryTurnsRef = (value) => {
176
+ advisorJevTurnGateEveryTurnsRef = value;
177
+ };
178
+ var setAdvisorJevTurnGateNoulThresholdRef = (value) => {
179
+ advisorJevTurnGateNoulThresholdRef = value;
180
+ };
122
181
  var setAdvisorToolResultMaxLinesRef = (value) => {
123
182
  advisorToolResultMaxLinesRef = value;
124
183
  };
@@ -169,6 +228,17 @@ var getAdvisorSettings = () => ({
169
228
  gitContext: advisorGitContextRef,
170
229
  gitContextMaxChars: advisorGitContextMaxCharsRef,
171
230
  herdrIntegration: advisorHerdrIntegrationRef,
231
+ jevDigestMaxChars: advisorJevDigestMaxCharsRef,
232
+ jevFilterEnabled: advisorJevFilterEnabledRef,
233
+ jevFilterNoulMargin: advisorJevFilterNoulMarginRef,
234
+ jevFilterOverrideWindow: advisorJevFilterOverrideWindowRef,
235
+ jevFilterSkipConfidence: advisorJevFilterSkipConfidenceRef,
236
+ jevModel: advisorJevModelRef,
237
+ jevPricePerMtok: advisorJevPricePerMtokRef,
238
+ jevTimeoutMs: advisorJevTimeoutMsRef,
239
+ jevTransport: advisorJevTransportRef,
240
+ jevTurnGateEveryTurns: advisorJevTurnGateEveryTurnsRef,
241
+ jevTurnGateNoulThreshold: advisorJevTurnGateNoulThresholdRef,
172
242
  loopThreshold: advisorLoopThresholdRef,
173
243
  maxCallsPerSession: advisorMaxCallsPerSessionRef,
174
244
  outcomeLogging: advisorOutcomeLoggingRef,
@@ -312,6 +382,12 @@ var isValidMaxCallsPerSession = (value) => nonNegativeSafeInteger(value);
312
382
  var isValidGateFailureMode = (value) => typeof value === "string" && GATE_FAILURE_MODES.includes(value);
313
383
  var isValidToolResultMaxLines = (value) => nonNegativeSafeInteger(value);
314
384
  var isValidToolResultMaxBytes = (value) => nonNegativeSafeInteger(value);
385
+ var isValidJevTimeoutMs = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
386
+ var isValidJevDigestMaxChars = (value) => nonNegativeSafeInteger(value);
387
+ var isValidJevPricePerMtok = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
388
+ var isValidJevSkipConfidence = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0.5 && value <= 1;
389
+ 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);
315
391
  var CONFIG_SCHEMA = {
316
392
  advisor: {
317
393
  accepted: "a provider/model string",
@@ -381,6 +457,81 @@ var CONFIG_SCHEMA = {
381
457
  persisted: true,
382
458
  type: "boolean"
383
459
  },
460
+ advisorJevDigestMaxChars: {
461
+ accepted: "a non-negative safe integer",
462
+ current: () => advisorJevDigestMaxCharsRef,
463
+ persisted: true,
464
+ type: "number",
465
+ validate: isValidJevDigestMaxChars
466
+ },
467
+ advisorJevFilterEnabled: {
468
+ accepted: "true or false",
469
+ current: () => advisorJevFilterEnabledRef,
470
+ persisted: true,
471
+ type: "boolean"
472
+ },
473
+ advisorJevFilterNoulMargin: {
474
+ accepted: "a number from 0 through 0.5",
475
+ current: () => advisorJevFilterNoulMarginRef,
476
+ persisted: true,
477
+ type: "number",
478
+ validate: isValidJevNoulMargin
479
+ },
480
+ advisorJevFilterOverrideWindow: {
481
+ accepted: "a non-negative safe integer",
482
+ current: () => advisorJevFilterOverrideWindowRef,
483
+ persisted: true,
484
+ type: "number",
485
+ validate: nonNegativeSafeInteger
486
+ },
487
+ advisorJevFilterSkipConfidence: {
488
+ accepted: "a number between 0.5 and 1 inclusive",
489
+ current: () => advisorJevFilterSkipConfidenceRef,
490
+ persisted: true,
491
+ type: "number",
492
+ validate: isValidJevSkipConfidence
493
+ },
494
+ advisorJevModel: {
495
+ accepted: "a non-empty string",
496
+ current: () => advisorJevModelRef,
497
+ persisted: true,
498
+ type: "string"
499
+ },
500
+ advisorJevPricePerMtok: {
501
+ accepted: "a positive number",
502
+ current: () => advisorJevPricePerMtokRef,
503
+ persisted: true,
504
+ type: "number",
505
+ validate: isValidJevPricePerMtok
506
+ },
507
+ advisorJevTimeoutMs: {
508
+ accepted: "a positive safe integer",
509
+ current: () => advisorJevTimeoutMsRef,
510
+ persisted: true,
511
+ type: "number",
512
+ validate: isValidJevTimeoutMs
513
+ },
514
+ advisorJevTransport: {
515
+ accepted: JEV_TRANSPORTS.join(", "),
516
+ current: () => advisorJevTransportRef,
517
+ persisted: true,
518
+ type: "enum",
519
+ validate: isValidJevTransport
520
+ },
521
+ advisorJevTurnGateEveryTurns: {
522
+ accepted: "a non-negative safe integer (0 disables the turn gate)",
523
+ current: () => advisorJevTurnGateEveryTurnsRef,
524
+ persisted: true,
525
+ type: "number",
526
+ validate: nonNegativeSafeInteger
527
+ },
528
+ advisorJevTurnGateNoulThreshold: {
529
+ accepted: "a number between 0.5 and 1 inclusive",
530
+ current: () => advisorJevTurnGateNoulThresholdRef,
531
+ persisted: true,
532
+ type: "number",
533
+ validate: isValidJevSkipConfidence
534
+ },
384
535
  advisorLoopThreshold: {
385
536
  accepted: "a safe integer of at least 2",
386
537
  current: () => advisorLoopThresholdRef,
@@ -629,6 +780,17 @@ var resetDefaults = () => {
629
780
  setAlwaysOnRef(false);
630
781
  setAdvisorFailureModeRef("block-session");
631
782
  setAdvisorHerdrIntegrationRef(true);
783
+ setAdvisorJevModelRef(DEFAULT_JEV_MODEL);
784
+ setAdvisorJevFilterEnabledRef(false);
785
+ setAdvisorJevFilterSkipConfidenceRef(DEFAULT_JEV_FILTER_SKIP_CONFIDENCE);
786
+ setAdvisorJevFilterNoulMarginRef(DEFAULT_JEV_FILTER_NOUL_MARGIN);
787
+ setAdvisorJevFilterOverrideWindowRef(DEFAULT_JEV_FILTER_OVERRIDE_WINDOW);
788
+ setAdvisorJevTimeoutMsRef(DEFAULT_JEV_TIMEOUT_MS);
789
+ setAdvisorJevDigestMaxCharsRef(DEFAULT_JEV_DIGEST_MAX_CHARS);
790
+ setAdvisorJevPricePerMtokRef(DEFAULT_JEV_PRICE_PER_MTOK);
791
+ setAdvisorJevTransportRef(DEFAULT_JEV_TRANSPORT);
792
+ setAdvisorJevTurnGateEveryTurnsRef(DEFAULT_JEV_TURN_GATE_EVERY_TURNS);
793
+ setAdvisorJevTurnGateNoulThresholdRef(DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD);
632
794
  setAdvisorToolResultMaxLinesRef(DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES);
633
795
  setAdvisorToolResultMaxBytesRef(DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES);
634
796
  setAdvisorRedactSecretsRef(false);
@@ -676,6 +838,17 @@ var applyConfig = (config) => {
676
838
  applyOptionalConfig(config, "alwaysOn", setAlwaysOnRef);
677
839
  applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
678
840
  applyOptionalConfig(config, "advisorHerdrIntegration", setAdvisorHerdrIntegrationRef);
841
+ applyOptionalConfig(config, "advisorJevFilterEnabled", setAdvisorJevFilterEnabledRef);
842
+ applyOptionalConfig(config, "advisorJevFilterSkipConfidence", setAdvisorJevFilterSkipConfidenceRef);
843
+ applyOptionalConfig(config, "advisorJevFilterNoulMargin", setAdvisorJevFilterNoulMarginRef);
844
+ applyOptionalConfig(config, "advisorJevFilterOverrideWindow", setAdvisorJevFilterOverrideWindowRef);
845
+ applyNonEmptyStringConfig(config.advisorJevModel, setAdvisorJevModelRef);
846
+ applyOptionalConfig(config, "advisorJevTimeoutMs", setAdvisorJevTimeoutMsRef);
847
+ applyOptionalConfig(config, "advisorJevDigestMaxChars", setAdvisorJevDigestMaxCharsRef);
848
+ applyOptionalConfig(config, "advisorJevPricePerMtok", setAdvisorJevPricePerMtokRef);
849
+ applyOptionalConfig(config, "advisorJevTransport", setAdvisorJevTransportRef);
850
+ applyOptionalConfig(config, "advisorJevTurnGateEveryTurns", setAdvisorJevTurnGateEveryTurnsRef);
851
+ applyOptionalConfig(config, "advisorJevTurnGateNoulThreshold", setAdvisorJevTurnGateNoulThresholdRef);
679
852
  applyOptionalConfig(config, "advisorToolResultMaxLines", setAdvisorToolResultMaxLinesRef);
680
853
  applyOptionalConfig(config, "advisorToolResultMaxBytes", setAdvisorToolResultMaxBytesRef);
681
854
  applyOptionalConfig(config, "advisorRedactSecrets", setAdvisorRedactSecretsRef);
@@ -690,6 +863,7 @@ var applyConfig = (config) => {
690
863
  // src/config/storage.ts
691
864
  var currentConfigState = () => Object.fromEntries(SAVED_CONFIG_KEYS.map((key) => [key, CONFIG_SCHEMA[key].current()]));
692
865
  var sameConfigValue = (left, right) => JSON.stringify(left) === JSON.stringify(right);
866
+ var RESERVED_ADVISOR_JSON_KEYS = new Set(["typesafe_api_key"]);
693
867
  var readExistingConfig = (path) => {
694
868
  try {
695
869
  const parsed = JSON.parse(readFileSync(path, "utf8"));
@@ -751,7 +925,7 @@ var loadConfig = (_ctx) => {
751
925
  setPersistedModelRefs(configuredModelRef(globalConfig?.advisor), configuredModelRef(globalConfig?.executor));
752
926
  if (globalConfig) {
753
927
  applyConfig(globalConfig);
754
- const unknownKeys = unknownConfigKeys(globalConfig);
928
+ const unknownKeys = unknownConfigKeys(globalConfig).filter((key) => !RESERVED_ADVISOR_JSON_KEYS.has(key));
755
929
  const warningIdentity = `${global}:${configIdentity(global)}`;
756
930
  if (unknownKeys.length > 0 && _ctx.hasUI && !warnedUnknownConfigIdentities.has(warningIdentity)) {
757
931
  _ctx.ui.notify(`Advisor configuration at ${global} contains unrecognized key(s) ${unknownKeys.map((key) => JSON.stringify(key)).join(", ")}. They were preserved but ignored; check for typos or upgrade pi-advisor.`, "warning");
@@ -2231,7 +2405,7 @@ var addAdvisorUsage = (totals, usage) => {
2231
2405
  totals.costCalls += 1;
2232
2406
  }
2233
2407
  };
2234
- var formatTokens = (value) => {
2408
+ var formatTokenCount = (value) => {
2235
2409
  if (value < 1000) {
2236
2410
  return String(value);
2237
2411
  }
@@ -2246,6 +2420,7 @@ var formatTokens = (value) => {
2246
2420
  }
2247
2421
  return `${Math.round(value / 1e6)}M`;
2248
2422
  };
2423
+ var formatTokens = formatTokenCount;
2249
2424
  var formatCost = (value) => `$${value.toFixed(4)}`;
2250
2425
  var formatUsageFields = (usage) => {
2251
2426
  const tokens = [
@@ -3236,6 +3411,139 @@ var renderScoutDetails = (box, scout, expanded, theme) => {
3236
3411
  }
3237
3412
  };
3238
3413
 
3414
+ // src/jev/ledger.ts
3415
+ var freshJevUsage = () => ({
3416
+ cost: 0,
3417
+ inputTokens: 0,
3418
+ outputTokens: 0
3419
+ });
3420
+ var freshJevLedger = () => ({
3421
+ filter: {
3422
+ allowed: 0,
3423
+ failures: 0,
3424
+ overrides: 0,
3425
+ repeatSkipped: 0,
3426
+ screened: 0,
3427
+ skipped: 0
3428
+ },
3429
+ gate: { checks: 0, consultations: 0, failures: 0, usage: freshJevUsage() },
3430
+ usage: freshJevUsage()
3431
+ });
3432
+ var addJevUsage = (totals, usage) => {
3433
+ totals.cost += usage.cost;
3434
+ totals.inputTokens += usage.inputTokens;
3435
+ totals.outputTokens += usage.outputTokens;
3436
+ };
3437
+
3438
+ class AdvisorJevLedgerState {
3439
+ #ledger = freshJevLedger();
3440
+ #lastSkip;
3441
+ reset() {
3442
+ this.#ledger = freshJevLedger();
3443
+ this.#lastSkip = undefined;
3444
+ }
3445
+ get lastSkip() {
3446
+ return this.#lastSkip;
3447
+ }
3448
+ recordFilterAllowed() {
3449
+ this.#ledger.filter.allowed += 1;
3450
+ this.#ledger.filter.screened += 1;
3451
+ }
3452
+ recordFilterSkipped(repeat, normalizedQuestion, turn) {
3453
+ this.#ledger.filter.skipped += 1;
3454
+ this.#ledger.filter.screened += 1;
3455
+ if (repeat) {
3456
+ this.#ledger.filter.repeatSkipped += 1;
3457
+ }
3458
+ this.#lastSkip = {
3459
+ ...normalizedQuestion ? { normalizedQuestion } : {},
3460
+ turn
3461
+ };
3462
+ }
3463
+ recordFilterOverride() {
3464
+ this.#ledger.filter.overrides += 1;
3465
+ }
3466
+ recordFilterFailure() {
3467
+ this.#ledger.filter.failures += 1;
3468
+ }
3469
+ recordFilterUsage(usage) {
3470
+ addJevUsage(this.#ledger.usage, usage);
3471
+ }
3472
+ recordGateCheck(usage) {
3473
+ this.#ledger.gate.checks += 1;
3474
+ if (usage) {
3475
+ addJevUsage(this.#ledger.gate.usage, usage);
3476
+ }
3477
+ }
3478
+ recordGateConsultation() {
3479
+ this.#ledger.gate.consultations += 1;
3480
+ }
3481
+ recordGateFailure() {
3482
+ this.#ledger.gate.failures += 1;
3483
+ }
3484
+ summaryLines(invocations) {
3485
+ const lines = [];
3486
+ const { filter, gate, usage } = this.#ledger;
3487
+ const nonRepeatJevActivity = filter.allowed + (filter.skipped - filter.repeatSkipped) + filter.failures;
3488
+ if (nonRepeatJevActivity === 0 && filter.repeatSkipped > 0) {
3489
+ const parts = [
3490
+ `${filter.repeatSkipped} repeat question${filter.repeatSkipped === 1 ? "" : "s"} skipped, earlier advice reattached`
3491
+ ];
3492
+ if (filter.overrides > 0) {
3493
+ parts.push(`${filter.overrides} override${filter.overrides === 1 ? "" : "s"}`);
3494
+ }
3495
+ lines.push(`Consultation dedup: ${parts.join(", ")}`);
3496
+ lines.push(this.#savingsLine(this.#markdownCosts(invocations), filter.skipped));
3497
+ } else if (this.#filterActive()) {
3498
+ lines.push(this.#filterLine(filter));
3499
+ const jevTokens = usage.inputTokens + usage.outputTokens;
3500
+ if (jevTokens > 0) {
3501
+ lines.push(`Jev cost: ${this.#formatJevTokens(usage)} tokens · $${usage.cost.toFixed(4)} (input only; output free)`);
3502
+ }
3503
+ if (filter.skipped > 0) {
3504
+ lines.push(this.#savingsLine(this.#markdownCosts(invocations), filter.skipped));
3505
+ }
3506
+ }
3507
+ if (gate.checks > 0 || gate.consultations > 0) {
3508
+ lines.push(this.#gateLine(gate, invocations));
3509
+ }
3510
+ return lines;
3511
+ }
3512
+ #filterActive() {
3513
+ const { filter } = this.#ledger;
3514
+ return filter.screened > 0 || filter.overrides > 0 || filter.failures > 0;
3515
+ }
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
+ }
3546
+
3239
3547
  // src/session-state.ts
3240
3548
  var WHITESPACE = /\s/;
3241
3549
  var TIMESTAMP_KEYS = new Set([
@@ -3327,12 +3635,18 @@ class AdvisorSessionState {
3327
3635
  #ledger = freshAdviceLedger();
3328
3636
  #usage = freshUsage();
3329
3637
  #consumedCalls = 0;
3638
+ #jev = new AdvisorJevLedgerState;
3639
+ #sessionTurnOrdinal = 0;
3640
+ #turnsSinceConsultation = 0;
3330
3641
  resetTask() {
3331
3642
  this.#repetition = freshRepetition();
3332
3643
  this.#blockedReason = undefined;
3333
3644
  this.#ledger = freshAdviceLedger();
3334
3645
  this.#usage = freshUsage();
3335
3646
  this.#consumedCalls = 0;
3647
+ this.#jev.reset();
3648
+ this.#sessionTurnOrdinal = 0;
3649
+ this.#turnsSinceConsultation = 0;
3336
3650
  }
3337
3651
  clearBlocked() {
3338
3652
  this.#blockedReason = undefined;
@@ -3375,6 +3689,19 @@ class AdvisorSessionState {
3375
3689
  get consumedCalls() {
3376
3690
  return this.#consumedCalls;
3377
3691
  }
3692
+ recordCompletedTurn() {
3693
+ this.#sessionTurnOrdinal += 1;
3694
+ this.#turnsSinceConsultation += 1;
3695
+ }
3696
+ resetTurnsSinceConsultation() {
3697
+ this.#turnsSinceConsultation = 0;
3698
+ }
3699
+ get sessionTurnOrdinal() {
3700
+ return this.#sessionTurnOrdinal;
3701
+ }
3702
+ get turnsSinceConsultation() {
3703
+ return this.#turnsSinceConsultation;
3704
+ }
3378
3705
  get usageTotals() {
3379
3706
  return { ...this.#usage.totals };
3380
3707
  }
@@ -3385,13 +3712,28 @@ class AdvisorSessionState {
3385
3712
  this.#usage.invocations.push(record);
3386
3713
  addAdvisorUsage(this.#usage.totals, record.usage);
3387
3714
  }
3388
- issueAdvice(id, advice, trigger, draft = false) {
3389
- this.#ledger.issued.set(id, { advice, trigger });
3715
+ issueAdvice(id, advice, trigger, draft = false, normalizedQuestion) {
3716
+ this.#ledger.issued.set(id, {
3717
+ advice,
3718
+ ...normalizedQuestion ? { normalizedQuestion } : {},
3719
+ trigger
3720
+ });
3390
3721
  this.#ledger.lastAdvice = advice;
3391
3722
  if (draft) {
3392
3723
  this.#ledger.draftConsultations += 1;
3393
3724
  }
3394
3725
  }
3726
+ reattachedAdviceFor(normalizedQuestion) {
3727
+ if (!normalizedQuestion) {
3728
+ return;
3729
+ }
3730
+ for (const entry of this.#ledger.issued.values()) {
3731
+ if (entry.normalizedQuestion === normalizedQuestion) {
3732
+ return entry.advice;
3733
+ }
3734
+ }
3735
+ return;
3736
+ }
3395
3737
  claimTrackedFiles(paths) {
3396
3738
  const advice = this.#ledger.lastAdvice;
3397
3739
  if (!advice || paths.length === 0) {
@@ -3452,14 +3794,43 @@ class AdvisorSessionState {
3452
3794
  return [
3453
3795
  "manual",
3454
3796
  "executor-requested",
3797
+ "turn-gate",
3455
3798
  "repeated-tool-call",
3456
3799
  "completion-review",
3457
3800
  "custom-rule"
3458
3801
  ].filter((trigger) => this.#countTrigger(trigger) > 0).join(", ") || "none";
3459
3802
  }
3803
+ recordJevFilterAllowed() {
3804
+ this.#jev.recordFilterAllowed();
3805
+ }
3806
+ recordJevFilterSkipped(repeat, normalizedQuestion) {
3807
+ this.#jev.recordFilterSkipped(repeat, normalizedQuestion, this.#sessionTurnOrdinal);
3808
+ }
3809
+ get lastJevSkip() {
3810
+ return this.#jev.lastSkip;
3811
+ }
3812
+ recordJevFilterOverride() {
3813
+ this.#jev.recordFilterOverride();
3814
+ }
3815
+ recordJevFilterFailure() {
3816
+ this.#jev.recordFilterFailure();
3817
+ }
3818
+ recordJevFilterUsage(usage) {
3819
+ this.#jev.recordFilterUsage(usage);
3820
+ }
3821
+ recordJevGateCheck(usage) {
3822
+ this.#jev.recordGateCheck(usage);
3823
+ }
3824
+ recordJevGateConsultation() {
3825
+ this.#jev.recordGateConsultation();
3826
+ }
3827
+ recordJevGateFailure() {
3828
+ this.#jev.recordGateFailure();
3829
+ }
3460
3830
  summary(limit) {
3461
3831
  const { invocations, totals } = this.#usage;
3462
- if (invocations.length === 0 && this.#repetition.interventions === 0) {
3832
+ const jevLines = this.#jev.summaryLines(invocations);
3833
+ if (invocations.length === 0 && this.#repetition.interventions === 0 && jevLines.length === 0) {
3463
3834
  return;
3464
3835
  }
3465
3836
  const markdown = invocations.filter((item) => item.kind === "markdown");
@@ -3480,7 +3851,8 @@ class AdvisorSessionState {
3480
3851
  `Gate decisions: ${this.#decisionsLine()}`,
3481
3852
  `Loop matching: normalized tool signatures; ${this.#repetition.interventions} gate intervention${this.#repetition.interventions === 1 ? "" : "s"}`,
3482
3853
  `Execution effects: ${effects("tool-blocked")} tool blocked, ${effects("session-blocked")} sessions blocked, ${effects("continued")} continued`,
3483
- `Failures: ${failures.length ? failures.join(", ") : "none"}`
3854
+ `Failures: ${failures.length ? failures.join(", ") : "none"}`,
3855
+ ...jevLines
3484
3856
  ].join(`
3485
3857
  `);
3486
3858
  }
@@ -4040,138 +4412,770 @@ class ManualAdvisorDialog {
4040
4412
  }
4041
4413
  }
4042
4414
 
4043
- // src/commands/manual-consultation.ts
4044
- var startManualConsultation = (runtime, ctx, question, controller, scoutStatusToken, progress, gitContext) => {
4045
- herdrAdvisorActivity.start();
4046
- progress.phase = "preparing";
4047
- runtime.requestManualRender(ctx);
4048
- if (ctx.hasUI) {
4049
- const timer = setInterval(() => {
4050
- if (controller.signal.aborted || runtime.manualConsultations.get(controller) !== scoutStatusToken) {
4051
- clearInterval(timer);
4052
- return;
4415
+ // src/jev/client.ts
4416
+ class JevFailure extends Error {
4417
+ category;
4418
+ constructor(category, message) {
4419
+ super(message);
4420
+ this.name = "JevFailure";
4421
+ this.category = category;
4422
+ }
4423
+ }
4424
+ var ENDPOINTS = {
4425
+ openrouter: "https://openrouter.ai/api/alpha/decisions",
4426
+ typesafe: "https://api.typesafe.ai/v1/systemone"
4427
+ };
4428
+ var RETRYABLE_STATUSES = new Set([408, 429, ...range(500, 599)]);
4429
+ var RETRY_BACKOFF_MS = 250;
4430
+ 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;
4435
+ var errorDetail = (error) => {
4436
+ if (typeof error === "string") {
4437
+ return error;
4438
+ }
4439
+ if (error && typeof error === "object" && "message" in error) {
4440
+ return String(error.message);
4441
+ }
4442
+ return "";
4443
+ };
4444
+ var statusCategory = (status) => {
4445
+ if (status === 401 || status === 403) {
4446
+ return "auth";
4447
+ }
4448
+ if (status === 400 || status === 422) {
4449
+ return "malformed";
4450
+ }
4451
+ return "error";
4452
+ };
4453
+ var openRouterModelId = (model) => model.includes("/") ? model : `~typesafe/${model}`;
4454
+
4455
+ class JevClient {
4456
+ #apiKey;
4457
+ #endpoint;
4458
+ #fetch;
4459
+ #model;
4460
+ #pricePerMtok;
4461
+ #timeoutMs;
4462
+ constructor({
4463
+ apiKey,
4464
+ fetch,
4465
+ model,
4466
+ pricePerMtok,
4467
+ timeoutMs,
4468
+ transport
4469
+ }) {
4470
+ this.#apiKey = apiKey;
4471
+ this.#endpoint = ENDPOINTS[transport];
4472
+ this.#fetch = fetch ?? globalThis.fetch.bind(globalThis);
4473
+ this.#model = transport === "openrouter" ? openRouterModelId(model) : model;
4474
+ this.#pricePerMtok = pricePerMtok;
4475
+ this.#timeoutMs = timeoutMs;
4476
+ }
4477
+ async ask(state, questions, signal) {
4478
+ const deadline = new AbortController;
4479
+ const abortFromCaller = () => deadline.abort(signal?.reason);
4480
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
4481
+ if (signal?.aborted) {
4482
+ abortFromCaller();
4483
+ }
4484
+ let deadlineHit = false;
4485
+ const timer = setTimeout(() => {
4486
+ deadlineHit = true;
4487
+ deadline.abort(new Error("Jev wall-time budget elapsed"));
4488
+ }, this.#timeoutMs);
4489
+ timer.unref?.();
4490
+ const body = JSON.stringify({
4491
+ model: this.#model,
4492
+ questions,
4493
+ state
4494
+ });
4495
+ try {
4496
+ let outcome = {};
4497
+ for (let attempt = 1;; attempt += 1) {
4498
+ outcome = await this.#attempt(body, deadline.signal);
4499
+ if (!outcome.retryable || attempt >= MAX_ATTEMPTS) {
4500
+ break;
4501
+ }
4502
+ await this.#backoff(deadline.signal);
4503
+ if (deadline.signal.aborted) {
4504
+ break;
4505
+ }
4053
4506
  }
4054
- runtime.requestManualRender(ctx);
4055
- }, 80);
4056
- runtime.manualProgressTimers.set(controller, timer);
4507
+ return this.#settle(outcome, deadlineHit, signal);
4508
+ } catch (error) {
4509
+ if (signal?.aborted && !deadlineHit) {
4510
+ throw error;
4511
+ }
4512
+ if (error instanceof JevFailure) {
4513
+ throw error;
4514
+ }
4515
+ 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);
4517
+ } finally {
4518
+ clearTimeout(timer);
4519
+ signal?.removeEventListener("abort", abortFromCaller);
4520
+ }
4057
4521
  }
4058
- let scoutDetails;
4059
- return runtime.requestAdvisor(ctx, question, controller.signal, (thinking, text) => {
4060
- if (controller.signal.aborted) {
4061
- return;
4522
+ #settle(outcome, deadlineHit, signal) {
4523
+ if (outcome.answers) {
4524
+ return this.#result(outcome);
4062
4525
  }
4063
- progress.phase = "active";
4064
- progress.thinking = thinking;
4065
- progress.text = text;
4066
- runtime.requestManualRender(ctx);
4067
- }, (event) => {
4068
- if (!controller.signal.aborted) {
4069
- runtime.scoutStatus.update(ctx, scoutStatusToken, event);
4070
- scoutDetails = appendScoutLifecycleEntry(runtime.pi, event, scoutDetails);
4071
- progress.scout = scoutDetails;
4072
- progress.phase = "active";
4073
- runtime.requestManualRender(ctx);
4526
+ if (deadlineHit && outcome.failure?.category !== "auth") {
4527
+ throw new JevFailure("timeout", `Jev call exceeded its ${this.#timeoutMs} ms wall-time budget.`);
4074
4528
  }
4075
- }, gitContext).then(({ markdown, usage }) => {
4076
- if (controller.signal.aborted) {
4077
- return;
4529
+ if (outcome.failure) {
4530
+ throw outcome.failure;
4078
4531
  }
4079
- progress.phase = "complete";
4080
- runtime.advisorSessionState.recordInvocation({
4081
- cost: advisorUsageCost(usage),
4082
- executionEffect: "continued",
4083
- kind: "markdown",
4084
- model: advisorRef,
4085
- trigger: "manual",
4086
- usage
4087
- });
4088
- runtime.updateAdvisorUsageStatus(ctx);
4089
- const normalizedUsage = snapshotAdvisorUsage(usage);
4090
- runtime.pi.sendMessage({
4091
- content: `Manual Advisor consultation${question ? ` (${question})` : ""}:
4092
-
4093
- ${markdown}`,
4094
- customType: "advisor-manual-result",
4095
- details: {
4096
- advisor: advisorRef,
4097
- question,
4098
- text: markdown,
4099
- ...normalizedUsage ? { usage: normalizedUsage } : {}
4100
- },
4101
- display: true
4102
- }, {
4103
- deliverAs: "steer",
4104
- triggerTurn: true
4105
- });
4106
- }).catch((error) => {
4107
- if (controller.signal.aborted) {
4532
+ if (signal?.aborted) {
4533
+ throw new Error("Jev call aborted by the caller.");
4534
+ }
4535
+ throw new JevFailure("error", "Jev call failed.");
4536
+ }
4537
+ async#backoff(signal) {
4538
+ if (signal.aborted) {
4108
4539
  return;
4109
4540
  }
4110
- progress.phase = "error";
4111
- const message = error instanceof Error ? error.message : String(error);
4112
- runtime.advisorSessionState.recordInvocation({
4113
- executionEffect: "continued",
4114
- failure: "provider-error",
4115
- kind: "markdown",
4116
- model: advisorRef,
4117
- trigger: "manual"
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 });
4118
4549
  });
4119
- runtime.updateAdvisorUsageStatus(ctx);
4120
- runtime.pi.sendMessage({
4121
- content: `Manual Advisor consultation failed: ${message}`,
4122
- customType: "advisor-manual-result",
4123
- details: {
4124
- advisor: advisorRef,
4125
- text: `**Advisor consultation failed:** ${message}`
4126
- },
4127
- display: true
4128
- }, { deliverAs: "steer", triggerTurn: true });
4129
- notify(ctx, `Advisor consultation failed: ${message}`, "error");
4130
- notifyHerdrAdvisorFailure("Advisor consultation failed", message);
4131
- }).finally(() => {
4132
- if (controller.signal.aborted) {
4133
- progress.phase = "cancelled";
4134
- }
4135
- const timer = runtime.manualProgressTimers.get(controller);
4136
- if (timer) {
4137
- clearInterval(timer);
4138
- runtime.manualProgressTimers.delete(controller);
4550
+ }
4551
+ async#attempt(body, signal) {
4552
+ if (signal.aborted) {
4553
+ return { retryable: false };
4139
4554
  }
4140
- runtime.requestManualRender(ctx);
4141
- runtime.scoutStatus.release(ctx, scoutStatusToken);
4142
- runtime.manualConsultations.delete(controller);
4143
- herdrAdvisorActivity.finish();
4144
- });
4145
- };
4146
-
4147
- // src/commands/manual-command.ts
4148
- var registerManualCommand = (runtime) => {
4149
- runtime.pi.registerCommand("advisor-manual", {
4150
- description: "Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
4151
- handler: async (args, ctx) => {
4152
- if (!loadCommandConfig(ctx)) {
4153
- return;
4154
- }
4155
- if (!(isSimpleMode() || runtime.advisorSessionState.canConsult(getAdvisorMaxCallsPerSession()))) {
4156
- runtime.reportManualBudgetExhausted(ctx);
4157
- return;
4555
+ let response;
4556
+ try {
4557
+ response = await this.#fetch(this.#endpoint, {
4558
+ body,
4559
+ headers: {
4560
+ authorization: `Bearer ${this.#apiKey}`,
4561
+ "content-type": "application/json"
4562
+ },
4563
+ method: "POST",
4564
+ signal
4565
+ });
4566
+ } catch (error) {
4567
+ if (signal.aborted) {
4568
+ return { retryable: false };
4158
4569
  }
4159
- let gitContext;
4160
- let question;
4161
- if (ctx.mode === "tui") {
4162
- const request = await ctx.ui.custom((tui, theme, keybindings, done) => new ManualAdvisorDialog({
4163
- gitContext: getAdvisorSettings().gitContext,
4164
- initialMessage: args,
4165
- keybindings,
4166
- onCancel: () => done(undefined),
4167
- onSubmit: done,
4168
- theme,
4169
- tui
4170
- }), {
4171
- overlay: true,
4172
- overlayOptions: {
4173
- anchor: "center",
4174
- margin: 2,
4570
+ return {
4571
+ retryable: true,
4572
+ ...this.#connectionFailure(error)
4573
+ };
4574
+ }
4575
+ if (response.ok) {
4576
+ return this.#parseSuccess(response);
4577
+ }
4578
+ const failure = await this.#failureFromStatus(response);
4579
+ return {
4580
+ failure,
4581
+ retryable: RETRYABLE_STATUSES.has(response.status)
4582
+ };
4583
+ }
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
+ async#failureFromStatus(response) {
4591
+ let detail = "";
4592
+ try {
4593
+ const parsed = await response.json();
4594
+ const error = parsed?.error;
4595
+ detail = errorDetail(error);
4596
+ } catch {
4597
+ detail = "";
4598
+ }
4599
+ const message = redactSecrets(`Jev ${this.#transportLabel()} request failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}.`);
4600
+ return new JevFailure(statusCategory(response.status), message);
4601
+ }
4602
+ #transportLabel() {
4603
+ return this.#endpoint === ENDPOINTS.openrouter ? "OpenRouter" : "TypeSafe";
4604
+ }
4605
+ async#parseSuccess(response) {
4606
+ let parsed;
4607
+ try {
4608
+ parsed = await response.json();
4609
+ } catch (error) {
4610
+ return {
4611
+ failure: new JevFailure("malformed", `Jev response was not JSON: ${redactSecrets(error instanceof Error ? error.message : String(error))}`)
4612
+ };
4613
+ }
4614
+ const record = parsed;
4615
+ if (!record || typeof record !== "object" || !record.answers || typeof record.answers !== "object") {
4616
+ return {
4617
+ failure: new JevFailure("malformed", "Jev response did not include an answers object.")
4618
+ };
4619
+ }
4620
+ return {
4621
+ answers: record.answers,
4622
+ model: typeof record.model === "string" ? record.model : this.#model,
4623
+ usage: record.usage
4624
+ };
4625
+ }
4626
+ #result(outcome) {
4627
+ const usage = outcome.usage;
4628
+ const inputTokens = finiteTokens(usage?.input_tokens);
4629
+ const outputTokens = finiteTokens(usage?.output_tokens);
4630
+ const price = this.#pricePerMtok ?? advisorJevPricePerMtokRef;
4631
+ return {
4632
+ answers: outcome.answers ?? {},
4633
+ model: outcome.model ?? this.#model,
4634
+ usage: {
4635
+ cost: inputTokens / 1e6 * price,
4636
+ inputTokens,
4637
+ outputTokens
4638
+ }
4639
+ };
4640
+ }
4641
+ }
4642
+ var jevClientFromCredentials = (credentials, fetch) => new JevClient({
4643
+ apiKey: credentials.apiKey,
4644
+ fetch,
4645
+ model: advisorJevModelRef,
4646
+ timeoutMs: advisorJevTimeoutMsRef,
4647
+ transport: credentials.transport
4648
+ });
4649
+
4650
+ // src/jev/key-store.ts
4651
+ import {
4652
+ chmodSync,
4653
+ existsSync as existsSync3,
4654
+ readFileSync as readFileSync3,
4655
+ rmSync,
4656
+ writeFileSync as writeFileSync2
4657
+ } from "node:fs";
4658
+ import { join as join4 } from "node:path";
4659
+ import { getAgentDir as getAgentDir3 } from "@earendil-works/pi-coding-agent";
4660
+ var TYPESAFE_KEY_ENV_VAR = "TYPESAFE_API_KEY";
4661
+ var TYPESAFE_KEY_SERVICE = "pi-advisor";
4662
+ var TYPESAFE_KEY_NAME = "typesafe-api-key";
4663
+ var TYPESAFE_KEY_CONFIG_FIELD = "typesafe_api_key";
4664
+ var KEY_FILE_MODE = 384;
4665
+ var keyFilePath = () => join4(getAgentDir3(), "typesafe_api_key");
4666
+ var runtimeSecrets = () => globalThis.Bun?.secrets;
4667
+ var normalizeKey = (value) => value?.trim() || undefined;
4668
+ var readAdvisorJsonConfig = () => readExistingConfig(join4(getAgentDir3(), "advisor.json"));
4669
+ var defaultReadFileStore = () => {
4670
+ try {
4671
+ return normalizeKey(readFileSync3(keyFilePath(), "utf8"));
4672
+ } catch {
4673
+ return;
4674
+ }
4675
+ };
4676
+ var defaultWriteFileStore = (key) => {
4677
+ const path = keyFilePath();
4678
+ writeFileSync2(path, `${key}
4679
+ `, { mode: KEY_FILE_MODE });
4680
+ chmodSync(path, KEY_FILE_MODE);
4681
+ };
4682
+ var defaultDeleteFileStore = () => {
4683
+ rmSync(keyFilePath(), { force: true });
4684
+ };
4685
+ var messageOf = (error) => redactSecrets(error instanceof Error ? error.message : String(error));
4686
+ var resolveTypeSafeKey = async (deps = {}) => {
4687
+ const secrets = deps.secrets === undefined ? runtimeSecrets() : deps.secrets;
4688
+ if (secrets) {
4689
+ try {
4690
+ const stored = normalizeKey(await secrets.get({
4691
+ name: TYPESAFE_KEY_NAME,
4692
+ service: TYPESAFE_KEY_SERVICE
4693
+ }));
4694
+ if (stored) {
4695
+ return { key: stored, source: "bun-secrets" };
4696
+ }
4697
+ } catch {}
4698
+ }
4699
+ const env = deps.env ?? process.env;
4700
+ const fromEnv = normalizeKey(env[TYPESAFE_KEY_ENV_VAR]);
4701
+ if (fromEnv) {
4702
+ return { key: fromEnv, source: "env" };
4703
+ }
4704
+ const readFileStore = deps.readFileStore ?? defaultReadFileStore;
4705
+ const fromFile = normalizeKey(readFileStore());
4706
+ if (fromFile) {
4707
+ return { key: fromFile, source: "file" };
4708
+ }
4709
+ const config = (deps.readAdvisorJson ?? readAdvisorJsonConfig)();
4710
+ const staged = config[TYPESAFE_KEY_CONFIG_FIELD];
4711
+ if (typeof staged === "string") {
4712
+ const fromConfig = normalizeKey(staged);
4713
+ if (fromConfig) {
4714
+ return { key: fromConfig, source: "advisor-json" };
4715
+ }
4716
+ }
4717
+ return {};
4718
+ };
4719
+ var writeKeyTypeSafeKey = async (key, deps = {}) => {
4720
+ const normalized = normalizeKey(key);
4721
+ if (!normalized) {
4722
+ return { message: "The key is empty.", ok: false };
4723
+ }
4724
+ const secrets = deps.secrets === undefined ? runtimeSecrets() : deps.secrets;
4725
+ if (secrets) {
4726
+ try {
4727
+ await secrets.set({
4728
+ name: TYPESAFE_KEY_NAME,
4729
+ service: TYPESAFE_KEY_SERVICE,
4730
+ value: normalized
4731
+ });
4732
+ return { message: "Key stored in Bun.secrets.", ok: true };
4733
+ } catch (error) {
4734
+ return {
4735
+ message: `Storing the key in Bun.secrets failed: ${messageOf(error)}. Alternatively set the ${TYPESAFE_KEY_ENV_VAR} environment variable in your shell profile.`,
4736
+ ok: false
4737
+ };
4738
+ }
4739
+ }
4740
+ try {
4741
+ (deps.writeFileStore ?? defaultWriteFileStore)(normalized);
4742
+ return {
4743
+ message: "Key stored in ~/.pi/agent/typesafe_api_key (mode 0600).",
4744
+ ok: true
4745
+ };
4746
+ } catch (error) {
4747
+ return {
4748
+ message: `Storing the key failed: ${messageOf(error)}. Alternatively set the ${TYPESAFE_KEY_ENV_VAR} environment variable in your shell profile.`,
4749
+ ok: false
4750
+ };
4751
+ }
4752
+ };
4753
+ var clearKeyTypeSafeKey = async (deps = {}) => {
4754
+ let clearedSomething = false;
4755
+ let firstError;
4756
+ const secrets = deps.secrets === undefined ? runtimeSecrets() : deps.secrets;
4757
+ if (secrets) {
4758
+ try {
4759
+ await secrets.delete({
4760
+ name: TYPESAFE_KEY_NAME,
4761
+ service: TYPESAFE_KEY_SERVICE
4762
+ });
4763
+ clearedSomething = true;
4764
+ } catch (error) {
4765
+ firstError = messageOf(error);
4766
+ }
4767
+ }
4768
+ try {
4769
+ if (deps.deleteFileStore) {
4770
+ deps.deleteFileStore();
4771
+ } else if (existsSync3(keyFilePath())) {
4772
+ defaultDeleteFileStore();
4773
+ }
4774
+ clearedSomething = true;
4775
+ } catch (error) {
4776
+ firstError ??= messageOf(error);
4777
+ }
4778
+ if (firstError) {
4779
+ return {
4780
+ message: `Clearing the stored key failed: ${firstError}.`,
4781
+ ok: false
4782
+ };
4783
+ }
4784
+ return {
4785
+ message: `Stored key cleared.${clearedSomething ? "" : ` Nothing was stored; unset ${TYPESAFE_KEY_ENV_VAR} and remove ${TYPESAFE_KEY_CONFIG_FIELD} from advisor.json yourself if you use them.`}`,
4786
+ ok: true
4787
+ };
4788
+ };
4789
+ var removeTypeSafeKeyFromAdvisorJson = () => {
4790
+ try {
4791
+ const path = join4(getAgentDir3(), "advisor.json");
4792
+ const existing = readExistingConfig(path);
4793
+ if (!(TYPESAFE_KEY_CONFIG_FIELD in existing)) {
4794
+ return { message: "No plaintext key in advisor.json.", ok: true };
4795
+ }
4796
+ delete existing[TYPESAFE_KEY_CONFIG_FIELD];
4797
+ writeFileSync2(path, `${JSON.stringify(existing, null, 2)}
4798
+ `);
4799
+ resetConfigCache();
4800
+ return { message: "Plaintext key removed from advisor.json.", ok: true };
4801
+ } catch (error) {
4802
+ return {
4803
+ message: `Removing the plaintext key failed: ${messageOf(error)}.`,
4804
+ ok: false
4805
+ };
4806
+ }
4807
+ };
4808
+ var warnedPlaintextKey = false;
4809
+ var consumePlaintextKeyWarning = () => {
4810
+ if (warnedPlaintextKey) {
4811
+ return;
4812
+ }
4813
+ warnedPlaintextKey = true;
4814
+ return `Advisor is using a plaintext ${TYPESAFE_KEY_CONFIG_FIELD} from advisor.json; this is not recommended. Open /advisor-settings → Jev consultation filter to migrate it into a secure store, or use the ${TYPESAFE_KEY_ENV_VAR} environment variable.`;
4815
+ };
4816
+
4817
+ // src/jev/questions.ts
4818
+ var STAKES_RUBRIC = [
4819
+ "Negligible: routine, low-risk, mechanical, or reversible; a wrong call costs little and is easy to undo.",
4820
+ "Moderate: some risk or rework, but bounded and recoverable.",
4821
+ "High: material consequences for correctness, security, cost, user trust, or irreversibility."
4822
+ ];
4823
+ var EVIDENCE_RULE = "Judge from `executor_question` and `executor_draft` when present, otherwise from `recent_conversation`; when both are absent, `recent_conversation` is the evidence to judge from.";
4824
+ var screeningQuestions = {
4825
+ self_answerable: {
4826
+ criteria: {
4827
+ false: "The executor needs the Advisor's second opinion.",
4828
+ true: "The executor can resolve this alone with available tools and context."
4829
+ },
4830
+ instructions: `Can the executor confidently resolve this request alone, using available tools and context? ${EVIDENCE_RULE}`,
4831
+ type: "noul"
4832
+ },
4833
+ stakes: {
4834
+ criteria: [...STAKES_RUBRIC],
4835
+ instructions: `How material are the stakes of the decision behind this consultation request? ${EVIDENCE_RULE}`,
4836
+ type: "score"
4837
+ }
4838
+ };
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;
4842
+ var lowestStakesProbability = (answer) => {
4843
+ if (!isRecord2(answer)) {
4844
+ return;
4845
+ }
4846
+ const probabilities = isRecord2(answer.probabilities) ? answer.probabilities : {};
4847
+ const legend = isRecord2(answer.legend) ? answer.legend : undefined;
4848
+ if (legend) {
4849
+ const exact = Object.keys(legend).find((key) => legend[key] === STAKES_RUBRIC[0]);
4850
+ if (exact) {
4851
+ return finiteNumber(probabilities[exact]);
4852
+ }
4853
+ }
4854
+ const numericKeys = Object.keys(probabilities).filter((key) => NUMERIC_KEY_PATTERN.test(key));
4855
+ if (numericKeys.length === 0) {
4856
+ return;
4857
+ }
4858
+ const lowest = numericKeys.reduce((left, right) => Number(left) <= Number(right) ? left : right);
4859
+ return finiteNumber(probabilities[lowest]);
4860
+ };
4861
+ var selfAnswerableNoul = (answer) => isRecord2(answer) ? finiteNumber(answer.noul) : undefined;
4862
+ var composeScreeningVerdict = (answers, { noulMargin, skipConfidence }) => {
4863
+ if (!isRecord2(answers)) {
4864
+ return { skip: false };
4865
+ }
4866
+ const negligibleMass = lowestStakesProbability(answers.stakes);
4867
+ const noul = selfAnswerableNoul(answers.self_answerable);
4868
+ if (negligibleMass === undefined || noul === undefined) {
4869
+ return { skip: false };
4870
+ }
4871
+ const confidentlySelfAnswerable = noul >= 0.5 + noulMargin;
4872
+ return {
4873
+ skip: negligibleMass >= skipConfidence && confidentlySelfAnswerable
4874
+ };
4875
+ };
4876
+ var composeTurnGateVerdict = (answers, threshold) => {
4877
+ if (!isRecord2(answers)) {
4878
+ return false;
4879
+ }
4880
+ const answer = answers.should_consult;
4881
+ const noul = isRecord2(answer) ? finiteNumber(answer.noul) : undefined;
4882
+ return noul !== undefined && noul >= threshold;
4883
+ };
4884
+
4885
+ // src/jev/state.ts
4886
+ var JEV_TEXT_CAP_BYTES = 8 * 1024;
4887
+ var buildJevState = (ctx, input = {}) => {
4888
+ const state = { role: "executor" };
4889
+ if (input.question) {
4890
+ state.executor_question = redactAndCapText(input.question, JEV_TEXT_CAP_BYTES, advisorRedactSecretsRef);
4891
+ }
4892
+ if (input.draft) {
4893
+ state.executor_draft = redactAndCapText(input.draft, JEV_TEXT_CAP_BYTES, advisorRedactSecretsRef);
4894
+ }
4895
+ const digest = recentConversation(ctx, advisorJevDigestMaxCharsRef);
4896
+ if (digest) {
4897
+ state.recent_conversation = digest;
4898
+ }
4899
+ return state;
4900
+ };
4901
+
4902
+ // src/jev/transport.ts
4903
+ var OPENROUTER_PROVIDER = "openrouter";
4904
+ var openRouterKey = async (ctx, deps) => {
4905
+ const key = deps.getProviderKey ? await deps.getProviderKey(OPENROUTER_PROVIDER) : await ctx?.modelRegistry?.getApiKeyForProvider(OPENROUTER_PROVIDER);
4906
+ return key?.trim() || undefined;
4907
+ };
4908
+ var resolveJevTransport = async (ctx, deps = {}) => {
4909
+ const preference = advisorJevTransportRef;
4910
+ if (preference !== "openrouter") {
4911
+ const resolveTypesafe = deps.resolveTypesafe ?? resolveTypeSafeKey;
4912
+ const resolution = await resolveTypesafe();
4913
+ if (resolution.key) {
4914
+ return {
4915
+ apiKey: resolution.key,
4916
+ ...resolution.source ? { source: resolution.source } : {},
4917
+ transport: "typesafe"
4918
+ };
4919
+ }
4920
+ }
4921
+ if (preference === "typesafe") {
4922
+ return;
4923
+ }
4924
+ const openrouter = await openRouterKey(ctx, deps);
4925
+ return openrouter ? { apiKey: openrouter, transport: "openrouter" } : undefined;
4926
+ };
4927
+
4928
+ // src/tools/outage-notifier.ts
4929
+ var createOutageNotifier = (format) => {
4930
+ let lastKey;
4931
+ return {
4932
+ notify: (ctx, category, message) => {
4933
+ const key = `${category}:${message}`;
4934
+ if (key === lastKey) {
4935
+ return;
4936
+ }
4937
+ lastKey = key;
4938
+ if (ctx.hasUI) {
4939
+ ctx.ui.notify(format(category, message), "warning");
4940
+ }
4941
+ },
4942
+ reset: () => {
4943
+ lastKey = undefined;
4944
+ }
4945
+ };
4946
+ };
4947
+
4948
+ // src/tools/jev-filter.ts
4949
+ var normalizeScreeningQuestion = (question) => question?.trim().toLowerCase().replace(/\s+/g, " ") || undefined;
4950
+ var REATTACHED_ADVICE_CAP_BYTES = 4 * 1024;
4951
+ 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
+ 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.
4953
+
4954
+ ${advice}`;
4955
+ var outageNotifier = createOutageNotifier((category, message) => `Advisor Jev filter failed (${category}); allowing consultations. ${message}`);
4956
+ var notifyOutageOnce = outageNotifier.notify;
4957
+ var resetJevOutageNotification = outageNotifier.reset;
4958
+ 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
+ var bypassOutcome = (session, options, normalizedQuestion) => {
4984
+ const lastSkip = session.lastJevSkip;
4985
+ if (options.force) {
4986
+ if (lastSkip?.normalizedQuestion !== undefined && lastSkip.normalizedQuestion === normalizedQuestion) {
4987
+ session.recordJevFilterOverride();
4988
+ }
4989
+ return allow();
4990
+ }
4991
+ if (normalizedQuestion !== undefined && lastSkip?.normalizedQuestion === normalizedQuestion && session.sessionTurnOrdinal - lastSkip.turn <= advisorJevFilterOverrideWindowRef) {
4992
+ session.recordJevFilterOverride();
4993
+ return allow();
4994
+ }
4995
+ return;
4996
+ };
4997
+ var screenWithJev = async (ctx, session, options, deps, normalizedQuestion) => {
4998
+ const credentials = await (deps.resolveTransport ?? resolveJevTransport)(ctx);
4999
+ if (!credentials) {
5000
+ session.recordJevFilterFailure();
5001
+ notifyOutageOnce(ctx, "missing-key", "No Jev credentials resolved (no TypeSafe key and no OpenRouter login).");
5002
+ return allow();
5003
+ }
5004
+ if (credentials.source === "advisor-json") {
5005
+ const warning = consumePlaintextKeyWarning();
5006
+ if (warning && ctx.hasUI) {
5007
+ ctx.ui.notify(warning, "warning");
5008
+ }
5009
+ }
5010
+ const client = jevClientFromCredentials(credentials, deps.fetch);
5011
+ try {
5012
+ const result = await client.ask(buildJevState(ctx, options), screeningQuestions, options.signal);
5013
+ session.recordJevFilterUsage(result.usage);
5014
+ const verdict = composeScreeningVerdict(result.answers, {
5015
+ noulMargin: advisorJevFilterNoulMarginRef,
5016
+ skipConfidence: advisorJevFilterSkipConfidenceRef
5017
+ });
5018
+ if (verdict.skip) {
5019
+ session.recordJevFilterSkipped(false, normalizedQuestion);
5020
+ return {
5021
+ decision: "skip",
5022
+ kind: "screened",
5023
+ reason: "low stakes and resolvable without a consultation"
5024
+ };
5025
+ }
5026
+ session.recordJevFilterAllowed();
5027
+ return allow();
5028
+ } catch (error) {
5029
+ session.recordJevFilterFailure();
5030
+ if (error instanceof JevFailure) {
5031
+ notifyOutageOnce(ctx, error.category, error.message);
5032
+ } else if (options.signal?.aborted) {
5033
+ throw error;
5034
+ } else {
5035
+ notifyOutageOnce(ctx, "error", error instanceof Error ? error.message : String(error));
5036
+ }
5037
+ return allow();
5038
+ }
5039
+ };
5040
+ var screeningSkipText = (outcome) => outcome.kind === "repeat" && outcome.reattachedAdvice ? repeatSkipText(outcome.reattachedAdvice) : SCREENED_SKIP_TEXT;
5041
+
5042
+ // src/commands/manual-consultation.ts
5043
+ var startManualConsultation = (runtime, ctx, question, controller, scoutStatusToken, progress, gitContext) => {
5044
+ herdrAdvisorActivity.start();
5045
+ progress.phase = "preparing";
5046
+ runtime.requestManualRender(ctx);
5047
+ if (ctx.hasUI) {
5048
+ const timer = setInterval(() => {
5049
+ if (controller.signal.aborted || runtime.manualConsultations.get(controller) !== scoutStatusToken) {
5050
+ clearInterval(timer);
5051
+ return;
5052
+ }
5053
+ runtime.requestManualRender(ctx);
5054
+ }, 80);
5055
+ runtime.manualProgressTimers.set(controller, timer);
5056
+ }
5057
+ 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;
5071
+ progress.phase = "active";
5072
+ runtime.requestManualRender(ctx);
5073
+ }
5074
+ }, gitContext).then(({ adviceId, markdown, usage }) => {
5075
+ if (controller.signal.aborted) {
5076
+ return;
5077
+ }
5078
+ progress.phase = "complete";
5079
+ runtime.advisorSessionState.resetTurnsSinceConsultation();
5080
+ runtime.advisorSessionState.recordInvocation({
5081
+ cost: advisorUsageCost(usage),
5082
+ executionEffect: "continued",
5083
+ kind: "markdown",
5084
+ model: advisorRef,
5085
+ trigger: "manual",
5086
+ usage
5087
+ });
5088
+ if (typeof adviceId === "string") {
5089
+ runtime.advisorSessionState.issueAdvice(adviceId, markdown, "manual", false, normalizeScreeningQuestion(question));
5090
+ }
5091
+ runtime.updateAdvisorUsageStatus(ctx);
5092
+ const normalizedUsage = snapshotAdvisorUsage(usage);
5093
+ runtime.pi.sendMessage({
5094
+ content: `Manual Advisor consultation${question ? ` (${question})` : ""} — for your awareness; no action or follow-up consultation is needed unless the user asks:
5095
+
5096
+ ${markdown}`,
5097
+ customType: "advisor-manual-result",
5098
+ details: {
5099
+ advisor: advisorRef,
5100
+ question,
5101
+ text: markdown,
5102
+ ...normalizedUsage ? { usage: normalizedUsage } : {}
5103
+ },
5104
+ display: true
5105
+ }, {
5106
+ deliverAs: "steer",
5107
+ triggerTurn: true
5108
+ });
5109
+ }).catch((error) => {
5110
+ if (controller.signal.aborted) {
5111
+ return;
5112
+ }
5113
+ progress.phase = "error";
5114
+ const message = error instanceof Error ? error.message : String(error);
5115
+ runtime.advisorSessionState.resetTurnsSinceConsultation();
5116
+ runtime.advisorSessionState.recordInvocation({
5117
+ executionEffect: "continued",
5118
+ failure: "provider-error",
5119
+ kind: "markdown",
5120
+ model: advisorRef,
5121
+ trigger: "manual"
5122
+ });
5123
+ runtime.updateAdvisorUsageStatus(ctx);
5124
+ runtime.pi.sendMessage({
5125
+ content: `Manual Advisor consultation failed: ${message}`,
5126
+ customType: "advisor-manual-result",
5127
+ details: {
5128
+ advisor: advisorRef,
5129
+ text: `**Advisor consultation failed:** ${message}`
5130
+ },
5131
+ display: true
5132
+ }, { deliverAs: "steer", triggerTurn: true });
5133
+ notify(ctx, `Advisor consultation failed: ${message}`, "error");
5134
+ notifyHerdrAdvisorFailure("Advisor consultation failed", message);
5135
+ }).finally(() => {
5136
+ if (controller.signal.aborted) {
5137
+ progress.phase = "cancelled";
5138
+ }
5139
+ const timer = runtime.manualProgressTimers.get(controller);
5140
+ if (timer) {
5141
+ clearInterval(timer);
5142
+ runtime.manualProgressTimers.delete(controller);
5143
+ }
5144
+ runtime.requestManualRender(ctx);
5145
+ runtime.scoutStatus.release(ctx, scoutStatusToken);
5146
+ runtime.manualConsultations.delete(controller);
5147
+ herdrAdvisorActivity.finish();
5148
+ });
5149
+ };
5150
+
5151
+ // src/commands/manual-command.ts
5152
+ var registerManualCommand = (runtime) => {
5153
+ runtime.pi.registerCommand("advisor-manual", {
5154
+ description: "Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
5155
+ handler: async (args, ctx) => {
5156
+ if (!loadCommandConfig(ctx)) {
5157
+ return;
5158
+ }
5159
+ if (!(isSimpleMode() || runtime.advisorSessionState.canConsult(getAdvisorMaxCallsPerSession()))) {
5160
+ runtime.reportManualBudgetExhausted(ctx);
5161
+ return;
5162
+ }
5163
+ let gitContext;
5164
+ let question;
5165
+ if (ctx.mode === "tui") {
5166
+ const request = await ctx.ui.custom((tui, theme, keybindings, done) => new ManualAdvisorDialog({
5167
+ gitContext: getAdvisorSettings().gitContext,
5168
+ initialMessage: args,
5169
+ keybindings,
5170
+ onCancel: () => done(undefined),
5171
+ onSubmit: done,
5172
+ theme,
5173
+ tui
5174
+ }), {
5175
+ overlay: true,
5176
+ overlayOptions: {
5177
+ anchor: "center",
5178
+ margin: 2,
4175
5179
  maxHeight: "80%",
4176
5180
  minWidth: 56,
4177
5181
  width: 76
@@ -4334,7 +5338,7 @@ var registerCommandRenderers = (runtime) => {
4334
5338
  import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
4335
5339
  import {
4336
5340
  SettingsList,
4337
- truncateToWidth as truncateToWidth5
5341
+ truncateToWidth as truncateToWidth7
4338
5342
  } from "@earendil-works/pi-tui";
4339
5343
 
4340
5344
  // src/ui/settings-formatting.ts
@@ -4415,14 +5419,367 @@ var rainbowGradient = (text, startedAt) => {
4415
5419
  }).join("").concat("\x1B[0m");
4416
5420
  };
4417
5421
 
4418
- // src/ui/text-setting-submenu.ts
5422
+ // 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";
5428
+
5429
+ // node_modules/@typesafe-ai/sdk/dist/index.mjs
5430
+ var range2 = (from, to) => Array.from({ length: to - from }, (_, i) => from + i);
5431
+ var DEFAULT_RETRY_POLICY = {
5432
+ maxRetries: 2,
5433
+ backoffInitialMs: 500,
5434
+ backoffMaxMs: 5000,
5435
+ backoffJitter: 0.25,
5436
+ httpStatuses: /* @__PURE__ */ new Set([
5437
+ 408,
5438
+ 429,
5439
+ ...range2(500, 600)
5440
+ ]),
5441
+ respectRetryAfter: true,
5442
+ maxRetryAfterMs: 60000,
5443
+ apiConnectionError: true,
5444
+ apiTimeoutError: true
5445
+ };
5446
+ DEFAULT_RETRY_POLICY.maxRetries;
5447
+ var noul = (instructions = null, criteria) => ({
5448
+ type: "noul",
5449
+ instructions,
5450
+ criteria
5451
+ });
5452
+ var g = globalThis;
5453
+ var isBrowser = () => typeof g.window !== "undefined" && typeof g.window.document !== "undefined" && typeof g.navigator !== "undefined";
5454
+ var describeRuntime = () => {
5455
+ const platform = g.process?.platform && g.process?.arch ? ` (${g.process.platform}; ${g.process.arch})` : "";
5456
+ if (g.Bun?.version)
5457
+ return `bun/${g.Bun.version}${platform}`;
5458
+ if (g.Deno?.version?.deno)
5459
+ return `deno/${g.Deno.version.deno}${platform}`;
5460
+ if (g.EdgeRuntime !== undefined)
5461
+ return "vercel-edge";
5462
+ if (g.navigator?.userAgent === "Cloudflare-Workers")
5463
+ return "cloudflare-workers";
5464
+ if (g.process?.versions?.node)
5465
+ return `node/${g.process.versions.node}${platform}`;
5466
+ if (isBrowser())
5467
+ return "browser";
5468
+ return "unknown";
5469
+ };
5470
+ var RUNTIME = describeRuntime();
5471
+
5472
+ // src/ui/masked-input.ts
4419
5473
  import {
4420
5474
  Input as Input2,
4421
5475
  truncateToWidth as truncateToWidth4
4422
5476
  } from "@earendil-works/pi-tui";
4423
5477
 
5478
+ class MaskedInput {
5479
+ input;
5480
+ options;
5481
+ _focused = true;
5482
+ constructor(options = {}) {
5483
+ this.options = options;
5484
+ this.input = new Input2({ placeholder: options.placeholder });
5485
+ this.input.focused = true;
5486
+ this.input.onSubmit = (value) => options.onSubmit?.(value);
5487
+ this.input.onEscape = () => options.onEscape?.();
5488
+ }
5489
+ get focused() {
5490
+ return this._focused;
5491
+ }
5492
+ set focused(value) {
5493
+ this._focused = value;
5494
+ this.input.focused = value;
5495
+ }
5496
+ getValue() {
5497
+ return this.input.getValue();
5498
+ }
5499
+ setValue(value) {
5500
+ this.input.setValue(value);
5501
+ }
5502
+ handleInput(keyData) {
5503
+ this.input.handleInput(keyData);
5504
+ }
5505
+ invalidate() {
5506
+ this.input.invalidate();
5507
+ }
5508
+ render(width) {
5509
+ const value = this.input.getValue();
5510
+ const { cursor } = this.input;
5511
+ const masked = `${"•".repeat(Math.min(cursor, value.length))}█${"•".repeat(Math.max(0, value.length - cursor))}`;
5512
+ const placeholder = value.length === 0 && this.options.placeholder ? this.options.placeholder : masked;
5513
+ return [truncateToWidth4(placeholder, Math.max(1, width))];
5514
+ }
5515
+ }
5516
+
5517
+ // src/ui/jev-setup-submenu.ts
5518
+ var transportLabel = (credentials) => {
5519
+ if (credentials.transport === "openrouter") {
5520
+ return "OpenRouter (reusing pi login)";
5521
+ }
5522
+ switch (credentials.source) {
5523
+ case "advisor-json":
5524
+ return "TypeSafe (key: advisor.json — plaintext, not recommended)";
5525
+ case "bun-secrets":
5526
+ return "TypeSafe (key: Bun.secrets)";
5527
+ case "file":
5528
+ return "TypeSafe (key: stored file, mode 0600)";
5529
+ default:
5530
+ return "TypeSafe (key: TYPESAFE_API_KEY)";
5531
+ }
5532
+ };
5533
+ var defaultVerify = async (credentials) => {
5534
+ const client = jevClientFromCredentials(credentials);
5535
+ try {
5536
+ await client.ask({ purpose: "pi-advisor setup verification" }, { verified: noul("Answer yes.") });
5537
+ return { ok: true };
5538
+ } catch (error) {
5539
+ return {
5540
+ message: error instanceof Error ? error.message : String(error),
5541
+ ok: false
5542
+ };
5543
+ }
5544
+ };
5545
+
5546
+ class JevSetupSubmenu {
5547
+ options;
5548
+ deps;
5549
+ maskedInput;
5550
+ credentials;
5551
+ mode = "menu";
5552
+ notice;
5553
+ selectedIndex = 0;
5554
+ _focused = true;
5555
+ constructor(options, deps = {}) {
5556
+ this.options = options;
5557
+ this.deps = {
5558
+ clearStoredKey: deps.clearStoredKey ?? clearKeyTypeSafeKey,
5559
+ removePlaintextKey: deps.removePlaintextKey ?? removeTypeSafeKeyFromAdvisorJson,
5560
+ resolveTransport: deps.resolveTransport ?? (() => resolveJevTransport()),
5561
+ verify: deps.verify ?? defaultVerify,
5562
+ writeKey: deps.writeKey ?? writeKeyTypeSafeKey
5563
+ };
5564
+ this.maskedInput = new MaskedInput({
5565
+ onEscape: () => this.options.done(),
5566
+ onSubmit: (value) => this.submitEnteredKey(value),
5567
+ placeholder: "Paste a TypeSafe API key"
5568
+ });
5569
+ this.refresh().catch(() => {
5570
+ return;
5571
+ });
5572
+ }
5573
+ get focused() {
5574
+ return this._focused;
5575
+ }
5576
+ set focused(value) {
5577
+ this._focused = value;
5578
+ this.maskedInput.focused = value;
5579
+ }
5580
+ invalidate() {
5581
+ this.maskedInput.invalidate();
5582
+ }
5583
+ handleInput(keyData) {
5584
+ if (this.mode === "key-entry") {
5585
+ this.maskedInput.handleInput(keyData);
5586
+ this.options.tui.requestRender();
5587
+ return;
5588
+ }
5589
+ if (this.mode !== "menu") {
5590
+ return;
5591
+ }
5592
+ const actions = this.actions();
5593
+ if (matchesKey2(keyData, Key2.down) || keyData === "\x1B[B" || keyData === "\x1BOB") {
5594
+ this.selectedIndex = (this.selectedIndex + 1) % actions.length;
5595
+ } else if (matchesKey2(keyData, Key2.up) || keyData === "\x1B[A" || keyData === "\x1BOA") {
5596
+ this.selectedIndex = (this.selectedIndex - 1 + actions.length) % actions.length;
5597
+ } else if (matchesKey2(keyData, Key2.enter) || keyData === "\r") {
5598
+ this.activate(actions[this.selectedIndex]);
5599
+ } else {
5600
+ return;
5601
+ }
5602
+ this.options.tui.requestRender();
5603
+ }
5604
+ render(width) {
5605
+ const { theme } = this.options;
5606
+ const enabled = this.options.currentValue === "On";
5607
+ const lines = [
5608
+ theme.fg("accent", theme.bold(" Jev consultation filter")),
5609
+ ""
5610
+ ];
5611
+ if (this.credentials) {
5612
+ lines.push(` Transport: ${transportLabel(this.credentials)}`);
5613
+ lines.push(` Filter: ${enabled ? "On" : "Off"}`);
5614
+ } else if (this.mode === "verifying") {
5615
+ lines.push(" Checking available Jev credentials…");
5616
+ } 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.");
5619
+ }
5620
+ if (this.notice) {
5621
+ lines.push("");
5622
+ lines.push(theme.fg("warning", ` ${this.notice}`));
5623
+ }
5624
+ lines.push("");
5625
+ if (this.mode === "verifying") {
5626
+ lines.push(" Verifying with a live Jev call…");
5627
+ } 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"));
5630
+ } else {
5631
+ for (const [index, label] of this.labels().entries()) {
5632
+ const prefix = index === this.selectedIndex ? "→ " : " ";
5633
+ lines.push(`${prefix}${label}`);
5634
+ }
5635
+ }
5636
+ return lines.map((line) => truncateToWidth5(line, width));
5637
+ }
5638
+ actions() {
5639
+ if (!this.credentials) {
5640
+ return ["enter-key", "done"];
5641
+ }
5642
+ const enabled = this.options.currentValue === "On";
5643
+ const actions = [enabled ? "verify-again" : "verify-enable"];
5644
+ actions.push("disable");
5645
+ if (this.credentials.source === "bun-secrets" || this.credentials.source === "file") {
5646
+ actions.push("disable-clear");
5647
+ }
5648
+ actions.push("done");
5649
+ return actions;
5650
+ }
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
+ labels() {
5662
+ const labels = this.actionLabels();
5663
+ return this.actions().map((action) => labels[action]);
5664
+ }
5665
+ async refresh() {
5666
+ const wasVerifying = this.mode === "verifying";
5667
+ this.mode = "verifying";
5668
+ if (!wasVerifying) {
5669
+ this.options.tui.requestRender();
5670
+ }
5671
+ this.credentials = await this.deps.resolveTransport?.();
5672
+ this.mode = "menu";
5673
+ this.selectedIndex = 0;
5674
+ if (this.credentials?.source === "advisor-json") {
5675
+ this.notice ??= consumePlaintextKeyWarning();
5676
+ }
5677
+ this.options.tui.requestRender();
5678
+ }
5679
+ 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;
5704
+ }
5705
+ }
5706
+ async disableAndClear() {
5707
+ const clear = this.deps.clearStoredKey;
5708
+ if (!clear) {
5709
+ return;
5710
+ }
5711
+ const result = await clear();
5712
+ this.credentials = undefined;
5713
+ this.notice = result.message;
5714
+ this.options.done("Off");
5715
+ }
5716
+ async verifyAndEnable() {
5717
+ const { credentials } = this;
5718
+ if (!(credentials && this.deps.verify)) {
5719
+ return;
5720
+ }
5721
+ this.mode = "verifying";
5722
+ this.notice = undefined;
5723
+ this.options.tui.requestRender();
5724
+ const outcome = await this.deps.verify(credentials);
5725
+ this.mode = "menu";
5726
+ if (!outcome.ok) {
5727
+ this.notice = `Verification failed: ${outcome.message ?? "unknown error"}`;
5728
+ this.options.tui.requestRender();
5729
+ return;
5730
+ }
5731
+ if (credentials.transport === "typesafe" && credentials.source === "advisor-json") {
5732
+ const stored = await this.deps.writeKey?.(credentials.apiKey);
5733
+ if (!stored?.ok) {
5734
+ this.notice = stored?.message ?? "Storing the key failed; the plaintext advisor.json key keeps working.";
5735
+ this.options.tui.requestRender();
5736
+ return;
5737
+ }
5738
+ const removed = this.deps.removePlaintextKey?.();
5739
+ this.notice = removed?.ok ? "Key moved from advisor.json into the secure store." : `Stored securely, but ${removed?.message ?? "removing the plaintext copy failed; remove it yourself."}`;
5740
+ }
5741
+ this.options.done("On");
5742
+ }
5743
+ async submitEnteredKey(value) {
5744
+ const key = value.trim();
5745
+ if (!key) {
5746
+ return;
5747
+ }
5748
+ if (!this.deps.verify) {
5749
+ return;
5750
+ }
5751
+ this.mode = "verifying";
5752
+ this.options.tui.requestRender();
5753
+ const outcome = await this.deps.verify({
5754
+ apiKey: key,
5755
+ transport: "typesafe"
5756
+ });
5757
+ this.mode = "menu";
5758
+ if (!outcome.ok) {
5759
+ this.notice = `Verification failed: ${outcome.message ?? "unknown error"}`;
5760
+ this.options.tui.requestRender();
5761
+ return;
5762
+ }
5763
+ const stored = await this.deps.writeKey?.(key);
5764
+ if (!stored?.ok) {
5765
+ this.notice = stored?.message ?? "Storing the key failed.";
5766
+ this.options.tui.requestRender();
5767
+ return;
5768
+ }
5769
+ this.notice = `${stored.message} Verification succeeded.`;
5770
+ await this.refresh();
5771
+ this.options.done("On");
5772
+ }
5773
+ }
5774
+
5775
+ // src/ui/text-setting-submenu.ts
5776
+ import {
5777
+ Input as Input3,
5778
+ truncateToWidth as truncateToWidth6
5779
+ } from "@earendil-works/pi-tui";
5780
+
4424
5781
  class TextSettingSubmenu {
4425
- input = new Input2;
5782
+ input = new Input3;
4426
5783
  options;
4427
5784
  _focused = true;
4428
5785
  error;
@@ -4466,7 +5823,7 @@ class TextSettingSubmenu {
4466
5823
  lines.push(theme.fg("error", ` ${this.error}`));
4467
5824
  }
4468
5825
  lines.push("", theme.fg("dim", " Enter: apply · Esc: cancel"));
4469
- return lines.map((line) => truncateToWidth4(line, width));
5826
+ return lines.map((line) => truncateToWidth6(line, width));
4470
5827
  }
4471
5828
  handleInput(keyData) {
4472
5829
  const before = this.input.getValue();
@@ -4485,6 +5842,103 @@ var toggle = (id, label, description, value, defaultValue) => ({
4485
5842
  label,
4486
5843
  values: TOGGLE_VALUES
4487
5844
  });
5845
+ var jevItems = (settings, theme, tui) => [
5846
+ {
5847
+ currentValue: settingValue(settings.jevFilterEnabled, false),
5848
+ description: "Screen low-stakes ask_advisor consultations with Jev; guided setup verifies credentials.",
5849
+ id: "jevFilter",
5850
+ label: "Jev consultation filter",
5851
+ submenu: (currentValue, done) => new JevSetupSubmenu({ currentValue, done, theme, tui })
5852
+ },
5853
+ {
5854
+ currentValue: String(settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE),
5855
+ description: "Required probability on negligible stakes before a consultation is skipped.",
5856
+ id: "jevFilterSkipConfidence",
5857
+ label: "Jev skip confidence",
5858
+ values: numericValues(settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE, [0.6, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95])
5859
+ },
5860
+ {
5861
+ currentValue: String(settings.jevFilterNoulMargin ?? DEFAULT_JEV_FILTER_NOUL_MARGIN),
5862
+ description: "Extra margin over a coin flip required on self-answerability before skipping.",
5863
+ id: "jevFilterNoulMargin",
5864
+ label: "Jev Noul margin",
5865
+ values: numericValues(settings.jevFilterNoulMargin ?? DEFAULT_JEV_FILTER_NOUL_MARGIN, [0.1, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45])
5866
+ },
5867
+ {
5868
+ currentValue: `${settings.jevFilterOverrideWindow ?? DEFAULT_JEV_FILTER_OVERRIDE_WINDOW} turns`,
5869
+ description: "Turns after a skip during which the same question passes automatically.",
5870
+ id: "jevFilterOverrideWindow",
5871
+ label: "Jev override window",
5872
+ values: numericValues(settings.jevFilterOverrideWindow ?? DEFAULT_JEV_FILTER_OVERRIDE_WINDOW, [0, 3, 5, 10, 20, 50]).map((value) => `${value} turns`)
5873
+ },
5874
+ {
5875
+ currentValue: settings.jevModel ?? DEFAULT_JEV_MODEL,
5876
+ description: "TypeSafe Jev model used for screening and turn-gate checks.",
5877
+ id: "jevModel",
5878
+ label: "Jev model",
5879
+ submenu: (_currentValue, done) => new TextSettingSubmenu({
5880
+ description: "Enter a TypeSafe model name (for example jev-latest or jev-1.13.0).",
5881
+ initial: settings.jevModel ?? DEFAULT_JEV_MODEL,
5882
+ onCancel: done,
5883
+ onSubmit: (value) => ({ value: value.trim() || DEFAULT_JEV_MODEL }),
5884
+ theme,
5885
+ title: "Jev model",
5886
+ tui
5887
+ })
5888
+ },
5889
+ {
5890
+ currentValue: String(settings.jevTimeoutMs ?? DEFAULT_JEV_TIMEOUT_MS),
5891
+ description: "Total wall-time budget for one Jev call, including retries.",
5892
+ id: "jevTimeoutMs",
5893
+ label: "Jev timeout ms",
5894
+ values: numericValues(settings.jevTimeoutMs ?? DEFAULT_JEV_TIMEOUT_MS, [1000, 2000, 5000, 8000, 15000, 30000])
5895
+ },
5896
+ {
5897
+ currentValue: String(settings.jevDigestMaxChars ?? DEFAULT_JEV_DIGEST_MAX_CHARS),
5898
+ description: "Conversation characters sent to Jev as screening evidence.",
5899
+ id: "jevDigestMaxChars",
5900
+ label: "Jev digest chars",
5901
+ values: numericValues(settings.jevDigestMaxChars ?? DEFAULT_JEV_DIGEST_MAX_CHARS, [0, 1000, 2000, 4000, 8000, 15000])
5902
+ },
5903
+ {
5904
+ currentValue: String(settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK),
5905
+ description: "Assumed TypeSafe price per million input tokens for cost lines.",
5906
+ id: "jevPricePerMtok",
5907
+ label: "Jev price/Mtok",
5908
+ values: numericValues(settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK, [0.01, 0.02, 0.042, 0.05, 0.1])
5909
+ },
5910
+ {
5911
+ currentValue: settings.jevTurnGateEveryTurns === 0 || settings.jevTurnGateEveryTurns === undefined ? "Off" : `every ${settings.jevTurnGateEveryTurns} turns`,
5912
+ description: "Proactively consult the Advisor every N turns without a consultation (0 = off).",
5913
+ id: "jevTurnGateEveryTurns",
5914
+ label: "Jev turn gate",
5915
+ values: [
5916
+ "Off",
5917
+ "every 3 turns",
5918
+ "every 5 turns",
5919
+ "every 10 turns",
5920
+ "every 20 turns"
5921
+ ]
5922
+ },
5923
+ {
5924
+ currentValue: String(settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD),
5925
+ description: "Jev confidence required before the turn gate interrupts with advice.",
5926
+ id: "jevTurnGateNoulThreshold",
5927
+ label: "Jev turn-gate threshold",
5928
+ values: numericValues(settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD, [0.6, 0.7, 0.8, 0.85, 0.9, 0.95])
5929
+ },
5930
+ {
5931
+ currentValue: settings.jevTransport ?? DEFAULT_JEV_TRANSPORT,
5932
+ description: "How Jev calls travel: auto reuses an OpenRouter login when no TypeSafe key is set.",
5933
+ id: "jevTransport",
5934
+ label: "Jev transport",
5935
+ values: withCurrentValue(settings.jevTransport ?? DEFAULT_JEV_TRANSPORT, [
5936
+ "auto",
5937
+ "typesafe",
5938
+ "openrouter"
5939
+ ])
5940
+ }
5941
+ ];
4488
5942
  var createSettingsItems = ({
4489
5943
  effortLevels,
4490
5944
  presets,
@@ -4621,14 +6075,14 @@ var createSettingsItems = ({
4621
6075
  title: "Tool disclosure policies",
4622
6076
  tui
4623
6077
  })
4624
- }, toggle("trackedFileContent", "Tracked file content", "Allow tracked file contents to be sent with Advisor context.", settings.trackedFileContent, false), toggle("untrackedContent", "Untracked file content", "Allow untracked file contents to be sent with Advisor context.", settings.untrackedContent, false), toggle("outcomeLogging", "Outcome logging (global)", "Allow anonymized Advisor outcomes to be logged globally.", settings.outcomeLogging, false));
6078
+ }, toggle("trackedFileContent", "Tracked file content", "Allow tracked file contents to be sent with Advisor context.", settings.trackedFileContent, false), toggle("untrackedContent", "Untracked file content", "Allow untracked file contents to be sent with Advisor context.", settings.untrackedContent, false), toggle("outcomeLogging", "Outcome logging (global)", "Allow anonymized Advisor outcomes to be logged globally.", settings.outcomeLogging, false), ...jevItems(settings, theme, tui));
4625
6079
  return items;
4626
6080
  };
4627
6081
 
4628
6082
  // src/ui/settings-list-adapter.ts
4629
6083
  import {
4630
- Key as Key2,
4631
- matchesKey as matchesKey2
6084
+ Key as Key3,
6085
+ matchesKey as matchesKey3
4632
6086
  } from "@earendil-works/pi-tui";
4633
6087
 
4634
6088
  class SettingsListAdapter {
@@ -4663,9 +6117,9 @@ class SettingsListAdapter {
4663
6117
  }
4664
6118
  changeWithArrow(keyData, onChange) {
4665
6119
  let direction = 0;
4666
- if (matchesKey2(keyData, Key2.left) || keyData === "\x1B[D") {
6120
+ if (matchesKey3(keyData, Key3.left) || keyData === "\x1B[D") {
4667
6121
  direction = -1;
4668
- } else if (matchesKey2(keyData, Key2.right) || keyData === "\x1B[C") {
6122
+ } else if (matchesKey3(keyData, Key3.right) || keyData === "\x1B[C") {
4669
6123
  direction = 1;
4670
6124
  }
4671
6125
  if (direction === 0) {
@@ -4758,6 +6212,39 @@ var mutateAdvisorSettings = (settings, id, value, presets) => {
4758
6212
  case "gitContextMaxChars":
4759
6213
  settings.gitContextMaxChars = Number(value);
4760
6214
  break;
6215
+ case "jevFilter":
6216
+ settings.jevFilterEnabled = value === "On";
6217
+ break;
6218
+ case "jevFilterSkipConfidence":
6219
+ settings.jevFilterSkipConfidence = Number(value);
6220
+ break;
6221
+ case "jevFilterNoulMargin":
6222
+ settings.jevFilterNoulMargin = Number(value);
6223
+ break;
6224
+ case "jevFilterOverrideWindow":
6225
+ 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":
6231
+ settings.jevTurnGateNoulThreshold = Number(value);
6232
+ break;
6233
+ case "jevModel":
6234
+ settings.jevModel = value.trim() || DEFAULT_JEV_MODEL;
6235
+ break;
6236
+ case "jevTimeoutMs":
6237
+ settings.jevTimeoutMs = Number(value);
6238
+ break;
6239
+ case "jevDigestMaxChars":
6240
+ settings.jevDigestMaxChars = Number(value);
6241
+ break;
6242
+ case "jevPricePerMtok":
6243
+ settings.jevPricePerMtok = Number(value);
6244
+ break;
6245
+ case "jevTransport":
6246
+ settings.jevTransport = value;
6247
+ break;
4761
6248
  default:
4762
6249
  if (BOOLEAN_SETTING_IDS.has(id)) {
4763
6250
  settings[id] = value === "On";
@@ -4807,7 +6294,7 @@ class AdvisorSettingsSelector {
4807
6294
  }
4808
6295
  render(width) {
4809
6296
  const border = this.options.theme.fg("border", "─".repeat(Math.max(1, width)));
4810
- return [border, ...this.settingsList.render(width), border].map((line) => truncateToWidth5(line, width));
6297
+ return [border, ...this.settingsList.render(width), border].map((line) => truncateToWidth7(line, width));
4811
6298
  }
4812
6299
  handleInput(keyData) {
4813
6300
  if (!this.settingsList.changeWithArrow(keyData, (id, value) => this.change(id, value))) {
@@ -4894,6 +6381,17 @@ var applyAdvisorSettings = (settings) => {
4894
6381
  setAlwaysOnRef(settings.alwaysOn ?? false);
4895
6382
  setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
4896
6383
  setAdvisorHerdrIntegrationRef(settings.herdrIntegration ?? true);
6384
+ setAdvisorJevFilterEnabledRef(settings.jevFilterEnabled ?? false);
6385
+ setAdvisorJevFilterSkipConfidenceRef(settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE);
6386
+ setAdvisorJevFilterNoulMarginRef(settings.jevFilterNoulMargin ?? DEFAULT_JEV_FILTER_NOUL_MARGIN);
6387
+ setAdvisorJevFilterOverrideWindowRef(settings.jevFilterOverrideWindow ?? DEFAULT_JEV_FILTER_OVERRIDE_WINDOW);
6388
+ setAdvisorJevModelRef(settings.jevModel ?? DEFAULT_JEV_MODEL);
6389
+ setAdvisorJevTimeoutMsRef(settings.jevTimeoutMs ?? DEFAULT_JEV_TIMEOUT_MS);
6390
+ setAdvisorJevDigestMaxCharsRef(settings.jevDigestMaxChars ?? DEFAULT_JEV_DIGEST_MAX_CHARS);
6391
+ setAdvisorJevPricePerMtokRef(settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK);
6392
+ setAdvisorJevTransportRef(settings.jevTransport ?? DEFAULT_JEV_TRANSPORT);
6393
+ setAdvisorJevTurnGateEveryTurnsRef(settings.jevTurnGateEveryTurns ?? DEFAULT_JEV_TURN_GATE_EVERY_TURNS);
6394
+ setAdvisorJevTurnGateNoulThresholdRef(settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD);
4897
6395
  setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
4898
6396
  setAdvisorToolResultMaxBytesRef(settings.toolResultMaxBytes ?? 50 * 1024);
4899
6397
  setAdvisorRedactSecretsRef(settings.redactSecrets ?? false);
@@ -4985,8 +6483,8 @@ import {
4985
6483
  unlink,
4986
6484
  writeFile
4987
6485
  } from "node:fs/promises";
4988
- import { join as join4 } from "node:path";
4989
- import { getAgentDir as getAgentDir3 } from "@earendil-works/pi-coding-agent";
6486
+ import { join as join5 } from "node:path";
6487
+ import { getAgentDir as getAgentDir4 } from "@earendil-works/pi-coding-agent";
4990
6488
  var ADOPTIONS = [
4991
6489
  "followed",
4992
6490
  "not-followed",
@@ -4999,11 +6497,11 @@ var VALIDATIONS = [
4999
6497
  "unknown"
5000
6498
  ];
5001
6499
  var MAX_LOG_BYTES = 1024 * 1024;
5002
- var statePath = () => join4(getAgentDir3(), "advisor-outcomes-salt");
5003
- var outcomeLogPath = () => join4(getAgentDir3(), "advisor-outcomes.jsonl");
6500
+ var statePath = () => join5(getAgentDir4(), "advisor-outcomes-salt");
6501
+ var outcomeLogPath = () => join5(getAgentDir4(), "advisor-outcomes.jsonl");
5004
6502
  var salt = async () => {
5005
6503
  const path = statePath();
5006
- await mkdir(getAgentDir3(), { mode: 448, recursive: true });
6504
+ await mkdir(getAgentDir4(), { mode: 448, recursive: true });
5007
6505
  for (let attempt = 0;attempt < 20; attempt += 1) {
5008
6506
  try {
5009
6507
  const existing = await readFile(path);
@@ -5080,7 +6578,7 @@ var withOutcomeLock = async (run) => {
5080
6578
  var adviceDigest = (advice, key) => createHmac("sha256", key).update(advice).digest("hex").slice(0, 16);
5081
6579
  var appendOutcome = async (record) => {
5082
6580
  const path = outcomeLogPath();
5083
- await mkdir(getAgentDir3(), { mode: 448, recursive: true });
6581
+ await mkdir(getAgentDir4(), { mode: 448, recursive: true });
5084
6582
  return withOutcomeLock(async () => {
5085
6583
  const next = {
5086
6584
  adoption: record.adoption,
@@ -5108,9 +6606,6 @@ var appendOutcome = async (record) => {
5108
6606
  });
5109
6607
  };
5110
6608
 
5111
- // src/tools/register-ask-advisor.ts
5112
- import { Type } from "typebox";
5113
-
5114
6609
  // src/tools/gate-policy.ts
5115
6610
  var updateAdvisorUsageStatus = (ctx, session) => {
5116
6611
  if (ctx.hasUI) {
@@ -5184,6 +6679,99 @@ var reserveAdvisorCall = (event, ctx, session, reservedCalls) => {
5184
6679
  return {};
5185
6680
  };
5186
6681
 
6682
+ // src/tools/jev-turn-gate.ts
6683
+ var turnGateQuestion = {
6684
+ criteria: {
6685
+ false: "The executor is progressing soundly on work that matches the user's intent; interrupting would add nothing material.",
6686
+ true: "The executor is approaching a material decision, repeating a failure, about to claim success without validation, or drifting from the user's intent; a second opinion now would change what happens next."
6687
+ },
6688
+ instructions: "Should a senior engineering advisor be consulted right now, before the executor continues? Judge from `recent_conversation`.",
6689
+ type: "noul"
6690
+ };
6691
+ var outageNotifier2 = createOutageNotifier((category, message) => `Advisor Jev turn gate failed (${category}); continuing without a proactive consultation. ${message}`);
6692
+ var notifyFailureOnce = outageNotifier2.notify;
6693
+ var resetJevTurnGateNotification = outageNotifier2.reset;
6694
+ var handleJevTurnEnd = async (registration, ctx) => {
6695
+ const { session } = registration;
6696
+ session.recordCompletedTurn();
6697
+ const interval = advisorJevTurnGateEveryTurnsRef;
6698
+ if (interval <= 0 || session.turnsSinceConsultation <= 0 || session.turnsSinceConsultation % interval !== 0) {
6699
+ return;
6700
+ }
6701
+ if (isSimpleMode() || session.blocked || !registration.activeTools().includes("ask_advisor") || !session.canConsult(getAdvisorMaxCallsPerSession())) {
6702
+ return;
6703
+ }
6704
+ const consult = registration.deps?.consult ?? registration.consult;
6705
+ const deps = registration.deps ?? {};
6706
+ try {
6707
+ const credentials = await (deps.resolveTransport ?? resolveJevTransport)(ctx);
6708
+ if (!credentials) {
6709
+ session.recordJevGateFailure();
6710
+ notifyFailureOnce(ctx, "missing-key", "No Jev credentials resolved (no TypeSafe key and no OpenRouter login).");
6711
+ return;
6712
+ }
6713
+ const client = jevClientFromCredentials(credentials, deps.fetch);
6714
+ const result = await client.ask(buildJevState(ctx, {}), {
6715
+ should_consult: turnGateQuestion
6716
+ });
6717
+ session.recordJevGateCheck(result.usage);
6718
+ const shouldConsult = composeTurnGateVerdict(result.answers, advisorJevTurnGateNoulThresholdRef);
6719
+ if (!shouldConsult) {
6720
+ return;
6721
+ }
6722
+ session.consumeCall();
6723
+ herdrAdvisorActivity.start();
6724
+ registration.send({
6725
+ content: "Proactive Advisor turn review",
6726
+ customType: "advisor-turn-gate-call",
6727
+ details: {
6728
+ question: `Turn gate: ${session.turnsSinceConsultation} turns without a consultation`,
6729
+ turn: session.sessionTurnOrdinal
6730
+ },
6731
+ display: true
6732
+ });
6733
+ try {
6734
+ const consulted = await consult(ctx, undefined, ctx.signal, undefined, "turn-gate");
6735
+ session.recordJevGateConsultation();
6736
+ session.resetTurnsSinceConsultation();
6737
+ session.recordInvocation({
6738
+ cost: advisorUsageCost(consulted.usage),
6739
+ executionEffect: "continued",
6740
+ kind: "markdown",
6741
+ model: consulted.model,
6742
+ trigger: "turn-gate",
6743
+ usage: consulted.usage
6744
+ });
6745
+ updateAdvisorUsageStatus(ctx, session);
6746
+ registration.send({
6747
+ content: consulted.markdown,
6748
+ customType: "advisor-turn-gate-result",
6749
+ details: {
6750
+ advisor: consulted.model,
6751
+ text: consulted.markdown,
6752
+ usage: consulted.usage
6753
+ },
6754
+ display: true
6755
+ });
6756
+ } finally {
6757
+ herdrAdvisorActivity.finish();
6758
+ }
6759
+ } catch (error) {
6760
+ session.recordJevGateFailure();
6761
+ if (error instanceof JevFailure) {
6762
+ notifyFailureOnce(ctx, error.category, error.message);
6763
+ } else if (!ctx.signal?.aborted) {
6764
+ notifyFailureOnce(ctx, "error", error instanceof Error ? error.message : String(error));
6765
+ }
6766
+ }
6767
+ };
6768
+ var registerJevTurnGate = (on, registration) => {
6769
+ on("turn_end", (_event, ctx) => handleJevTurnEnd(registration, ctx));
6770
+ };
6771
+
6772
+ // src/tools/register-ask-advisor.ts
6773
+ import { Type } from "typebox";
6774
+
5187
6775
  // src/tools/render-advisor-result.ts
5188
6776
  import {
5189
6777
  getMarkdownTheme as getMarkdownTheme4
@@ -5197,6 +6785,22 @@ var syncRenderPhase = (context, phase) => {
5197
6785
  }
5198
6786
  context.state.phase = phase;
5199
6787
  };
6788
+ var attachmentLabels = (details) => [
6789
+ details?.draftBytes ? `Draft attached · ${details.draftBytes} B` : undefined,
6790
+ details?.preferenceBytes ? `Project preferences attached · ${details.preferenceBytes} B` : undefined,
6791
+ details?.trackedBytes ? `Tracked files attached · ${details.trackedBytes} B` : undefined,
6792
+ details?.untrackedBytes ? `Untracked files attached · ${details.untrackedBytes} B` : undefined
6793
+ ].filter((label) => label !== undefined);
6794
+ var renderJevSkipBox = (box, result, expanded, theme) => {
6795
+ const details = advisorResultDetails(result);
6796
+ const lines = [
6797
+ theme.fg("dim", theme.bold("◆ ADVISOR · SKIPPED")),
6798
+ theme.fg("dim", ` ${details?.jev?.reason ?? ""}`)
6799
+ ];
6800
+ box.addChild(new Text5(lines.join(`
6801
+ `), 0, 0));
6802
+ box.addChild(new Markdown4(adviceForDisplay(textFrom(result.content), expanded), 0, 0, getMarkdownTheme4()));
6803
+ };
5200
6804
  var renderPartialAdvisorResult = (box, result, expanded, theme, context) => {
5201
6805
  const details = advisorResultDetails(result);
5202
6806
  if (details?.scout) {
@@ -5235,6 +6839,10 @@ var renderFinalAdvisorResult = (box, result, expanded, theme, context) => {
5235
6839
  context.state.timerId = undefined;
5236
6840
  }
5237
6841
  const details = advisorResultDetails(result);
6842
+ if (details?.jev?.skipped) {
6843
+ renderJevSkipBox(box, result, expanded, theme);
6844
+ return;
6845
+ }
5238
6846
  if (details?.scout) {
5239
6847
  context.state.scout = details.scout;
5240
6848
  }
@@ -5256,12 +6864,7 @@ var renderFinalAdvisorResult = (box, result, expanded, theme, context) => {
5256
6864
  lines.push(theme.fg("dim", ` Usage: ${usage}`));
5257
6865
  }
5258
6866
  }
5259
- const attachments = [
5260
- details?.draftBytes ? `Draft attached · ${details.draftBytes} B` : undefined,
5261
- details?.preferenceBytes ? `Project preferences attached · ${details.preferenceBytes} B` : undefined,
5262
- details?.trackedBytes ? `Tracked files attached · ${details.trackedBytes} B` : undefined,
5263
- details?.untrackedBytes ? `Untracked files attached · ${details.untrackedBytes} B` : undefined
5264
- ].filter(Boolean);
6867
+ const attachments = attachmentLabels(details);
5265
6868
  if (attachments.length) {
5266
6869
  lines.push(theme.fg("dim", ` ${attachments.join(" · ")}`));
5267
6870
  }
@@ -5303,6 +6906,7 @@ var registerAskAdvisorTool = ({
5303
6906
  consult: requestAdvisor,
5304
6907
  pi,
5305
6908
  reservedCalls,
6909
+ screen,
5306
6910
  session
5307
6911
  }) => {
5308
6912
  pi.registerTool({
@@ -5312,9 +6916,31 @@ var registerAskAdvisorTool = ({
5312
6916
  if (!(isSimpleMode() || session.canConsult(getAdvisorMaxCallsPerSession()))) {
5313
6917
  throw new Error("Advisor call budget exhausted for this session.");
5314
6918
  }
6919
+ const normalizedQuestion = normalizeScreeningQuestion(resolveAdvisorRequest(params.question));
6920
+ const screening = await screen(ctx, session, {
6921
+ draft: params.draft,
6922
+ force: params.force,
6923
+ question: resolveAdvisorRequest(params.question),
6924
+ signal
6925
+ });
6926
+ if (screening.decision === "skip") {
6927
+ const skipText = screeningSkipText(screening);
6928
+ return {
6929
+ content: [{ text: skipText, type: "text" }],
6930
+ details: {
6931
+ jev: {
6932
+ kind: screening.kind,
6933
+ reason: screening.reason,
6934
+ skipped: true
6935
+ },
6936
+ text: skipText
6937
+ }
6938
+ };
6939
+ }
5315
6940
  claimTrackedHandoff(session, params.includeTrackedFiles);
5316
6941
  if (!isSimpleMode()) {
5317
6942
  session.consumeCall();
6943
+ session.resetTurnsSinceConsultation();
5318
6944
  }
5319
6945
  herdrAdvisorActivity.start();
5320
6946
  let scoutDetails;
@@ -5347,7 +6973,7 @@ var registerAskAdvisorTool = ({
5347
6973
  });
5348
6974
  }, _id);
5349
6975
  flushUpdate();
5350
- session.issueAdvice(result.adviceId, result.markdown, result.trigger, Boolean(result.draftBytes));
6976
+ session.issueAdvice(result.adviceId, result.markdown, result.trigger, Boolean(result.draftBytes), normalizedQuestion);
5351
6977
  session.recordInvocation({
5352
6978
  cost: advisorUsageCost(result.usage),
5353
6979
  executionEffect: "continued",
@@ -5408,6 +7034,9 @@ ${result.markdown}`,
5408
7034
  draft: Type.Optional(Type.String({
5409
7035
  description: "Concise untrusted draft for plan or completion review; claims are not verification evidence."
5410
7036
  })),
7037
+ force: Type.Optional(Type.Boolean({
7038
+ description: "Set true only when you judge a decision genuinely material after a consultation was screened out; bypasses screening."
7039
+ })),
5411
7040
  gitContext: Type.Optional(Type.Union([Type.Literal("none"), Type.Literal("summary"), Type.Literal("full")], {
5412
7041
  description: "How much of the working tree to include. Use full when the review depends on the exact code changes, such as a completion review. Use summary for changed file names only, or none when the question is not about the current changes. The user's configured allowance is the ceiling and a larger request is narrowed to it."
5413
7042
  })),
@@ -5520,6 +7149,7 @@ var handleAutomaticGate = async (pi, event, ctx, session, runGate, scoutStatus)
5520
7149
  return failure.block ? { block: true, reason: failure.reason } : undefined;
5521
7150
  }
5522
7151
  session.consumeCall();
7152
+ session.resetTurnsSinceConsultation();
5523
7153
  herdrAdvisorActivity.start();
5524
7154
  let scoutDetails;
5525
7155
  const scoutStatusToken = Symbol("automatic-gate-scout");
@@ -5719,6 +7349,30 @@ var registerToolRenderers = (pi) => {
5719
7349
  renderScoutDetails(box, scout, Boolean(expanded), theme);
5720
7350
  return box;
5721
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
+ });
5722
7376
  pi.registerMessageRenderer?.("advisor-loop-call", (message, _options, theme) => {
5723
7377
  const details = message.details;
5724
7378
  return renderAdvisorCallBox(details?.question, theme);
@@ -5754,12 +7408,20 @@ var registerAdvisorTool = (pi, session = advisorSessionState, dependencies = {})
5754
7408
  reservedCalls: new Set,
5755
7409
  runGate: dependencies.runGate ?? runAdvisorGate,
5756
7410
  scoutStatus: dependencies.statusManager ?? new ScoutStatusManager,
7411
+ screen: dependencies.screen ?? screenConsultation,
5757
7412
  session
5758
7413
  };
5759
7414
  registerToolRenderers(pi);
5760
7415
  registerToolLifecycle(registration);
5761
7416
  registerAskAdvisorTool(registration);
5762
7417
  registerOutcomeTool(registration);
7418
+ registerJevTurnGate((event, handler) => pi.on(event, handler), {
7419
+ activeTools: () => pi.getActiveTools(),
7420
+ consult: registration.consult,
7421
+ ...dependencies.turnGateDeps ? { deps: dependencies.turnGateDeps } : {},
7422
+ send: (message) => pi.sendMessage(message, { deliverAs: "steer" }),
7423
+ session
7424
+ });
5763
7425
  };
5764
7426
 
5765
7427
  // extensions/index.ts