@warmdrift/kgauto-compiler 2.0.0-alpha.76 → 2.0.0-alpha.78

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
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  ALL_ARCHETYPES: () => ALL_ARCHETYPES,
26
26
  ARCHETYPE_FAMILY_FITS: () => ARCHETYPE_FAMILY_FITS,
27
27
  ARCHETYPE_FLOOR_DEFAULT: () => ARCHETYPE_FLOOR_DEFAULT,
28
+ BRAIN_READ_ENV_NAMES: () => BRAIN_READ_ENV_NAMES,
28
29
  COACH_CFG: () => COACH_CFG,
29
30
  CallError: () => CallError,
30
31
  DECOMPOSITION_TEMPLATES: () => DECOMPOSITION_TEMPLATES,
@@ -43,6 +44,7 @@ __export(index_exports, {
43
44
  MEASURED_GROUNDING_MIN_N: () => MEASURED_GROUNDING_MIN_N,
44
45
  PRODUCER_OWNED_RULE_CODES: () => PRODUCER_OWNED_RULE_CODES,
45
46
  PROVIDER_ENV_KEYS: () => PROVIDER_ENV_KEYS,
47
+ ROLLBACK_SUPPRESSION_WINDOW_DAYS: () => ROLLBACK_SUPPRESSION_WINDOW_DAYS,
46
48
  RULE_DISCIPLINE_GATES_V1: () => RULE_DISCIPLINE_GATES_V1,
47
49
  RULE_DISCIPLINE_GATES_V1_STRUCTURED: () => RULE_DISCIPLINE_GATES_V1_STRUCTURED,
48
50
  RULE_SEQUENTIAL_TOOL_CLIFF: () => RULE_SEQUENTIAL_TOOL_CLIFF,
@@ -96,12 +98,14 @@ __export(index_exports, {
96
98
  getPerAxisMetrics: () => getPerAxisMetrics,
97
99
  getProfile: () => getProfile,
98
100
  getReachabilityDiagnostic: () => getReachabilityDiagnostic,
101
+ getRecentRollback: () => getRecentRollback,
99
102
  getRecommendedPrimary: () => getRecommendedPrimary,
100
103
  getSequentialStarterChain: () => getSequentialStarterChain,
101
104
  getSequentialStarterChainWithGrounding: () => getSequentialStarterChainWithGrounding,
102
105
  getStaleExclusionFindings: () => getStaleExclusionFindings,
103
106
  getStarterChain: () => getStarterChain,
104
107
  getStarterChainWithGrounding: () => getStarterChainWithGrounding,
108
+ hasMutation: () => hasMutation,
105
109
  hashShape: () => hashShape,
106
110
  isArchetype: () => isArchetype,
107
111
  isAutoPromoteEnabledFromEnv: () => isAutoPromoteEnabledFromEnv,
@@ -122,9 +126,11 @@ __export(index_exports, {
122
126
  loadChainsFromBrain: () => loadChainsFromBrain,
123
127
  loadModelsFromBrain: () => loadModelsFromBrain,
124
128
  loadPricingFromBrain: () => loadPricingFromBrain,
129
+ mapMeasuredFailureRows: () => mapMeasuredFailureRows,
125
130
  markAdvisoryResolved: () => markAdvisoryResolved,
126
131
  markExclusionFindingHandled: () => markExclusionFindingHandled,
127
132
  markPromoteReadyHandled: () => markPromoteReadyHandled,
133
+ mutationId: () => mutationId,
128
134
  parseGoldenCaptureRate: () => parseGoldenCaptureRate,
129
135
  parseJudgeVerdict: () => parseJudgeVerdict,
130
136
  peekBrainDeadLetter: () => peekBrainDeadLetter,
@@ -693,6 +699,22 @@ var PROFILES_RAW = [
693
699
  threshold: 16,
694
700
  action: "drop_to_top_relevant",
695
701
  reason: "Haiku reliability degrades above ~16 tools"
702
+ },
703
+ {
704
+ // alpha.78 — the declared `structuredOutput: 'grammar'` does NOT
705
+ // hold on long-input summarize. MEASURED (brain, playbacksam):
706
+ // 21 disambiguated `structured_output_parse_failed` fallover rows
707
+ // 2026-07-22..27, tokens_in 12,280–31,450; PB's gate counted 20/20
708
+ // in-window failures. Clean traffic p50 sits at ~9K tokens_in, so
709
+ // 12K gates the failing band without touching the working one.
710
+ // Short-input summarize carries no failure evidence and stays
711
+ // ungated — this is why it's a cliff, not an archetype-wide
712
+ // `structuredOutputHint: 'avoid'`.
713
+ metric: "input_tokens",
714
+ threshold: 12e3,
715
+ action: "quality_gate_structured",
716
+ whenIntent: "summarize",
717
+ reason: "Structured-output parse failures at 100% in-window on long-input summarize (measured on playbacksam, 2026-07-25..27; haiku only led when input size made price dominate, then failed every time)."
696
718
  }
697
719
  ],
698
720
  costInputPer1m: 1,
@@ -2939,11 +2961,18 @@ function passScoreTargets(ir, opts) {
2939
2961
  }
2940
2962
  }
2941
2963
  let qualityGatePenalty = 0;
2964
+ let structuredCliffGate;
2942
2965
  if (constraints.structuredOutput) {
2943
2966
  const schemaWeak = effectiveConventions(profile).some(
2944
2967
  (c) => c.archetype === ir.intent.archetype && c.structuredOutputHint === "avoid"
2945
2968
  );
2946
2969
  if (schemaWeak) qualityGatePenalty = QUALITY_GATE_PENALTY;
2970
+ if (!schemaWeak) {
2971
+ structuredCliffGate = profile.cliffs.find(
2972
+ (c) => c.action === "quality_gate_structured" && (!c.whenIntent || c.whenIntent === ir.intent.archetype) && c.metric === "input_tokens" && opts.estimatedInputTokens >= c.threshold
2973
+ );
2974
+ if (structuredCliffGate) qualityGatePenalty = QUALITY_GATE_PENALTY;
2975
+ }
2947
2976
  }
2948
2977
  const measuredGate = opts.measuredFailureGates?.get(modelId);
2949
2978
  if (measuredGate) qualityGatePenalty = QUALITY_GATE_PENALTY;
@@ -3008,6 +3037,16 @@ function passScoreTargets(ir, opts) {
3008
3037
  rankAfter: rank,
3009
3038
  description: `Model ${modelId} gated below the quality floor for archetype '${ir.intent.archetype}' by MEASURED evidence from this app's own outcomes \u2014 ${measuredGate.nFail} of ${measuredGate.n} attempts failed on the quality axis in the trailing window (${pct(measuredGate.rate)}; 95% lower bound ${pct(measuredGate.lowerBound)} > 50%). Rank ${rankBefore.toFixed(2)} \u2192 ${rank.toFixed(2)} (\u2212${qualityGatePenalty.toFixed(2)}). Down-ranked out of leadership; retained as graceful fallback only. The gate is derived, not stored \u2014 it lifts on its own once the failures age out of the window.`
3010
3039
  });
3040
+ } else if (structuredCliffGate) {
3041
+ policyMutations.push({
3042
+ id: `quality-gate-structured-cliff-${modelId}`,
3043
+ source: "quality_gate",
3044
+ passName: "score_targets",
3045
+ rankDelta: -qualityGatePenalty,
3046
+ rankBefore,
3047
+ rankAfter: rank,
3048
+ description: `Model ${modelId} gated below the quality floor for archetype '${ir.intent.archetype}' \u2014 declared structuredOutput + input_tokens \u2265 ${structuredCliffGate.threshold} trips a measured cliff: ${structuredCliffGate.reason} Rank ${rankBefore.toFixed(2)} \u2192 ${rank.toFixed(2)} (\u2212${qualityGatePenalty.toFixed(2)}). Down-ranked out of leadership; retained as graceful fallback only. Bundled knowledge \u2014 active on cold isolates with no brain.`
3049
+ });
3011
3050
  } else {
3012
3051
  policyMutations.push({
3013
3052
  id: `quality-gate-structured-${modelId}`,
@@ -3691,13 +3730,8 @@ function getArchetypePerfScore(modelId, archetype) {
3691
3730
  return { score, n, grounding };
3692
3731
  }
3693
3732
 
3694
- // src/promote-ready-brain.ts
3695
- function isRawPromoteReadyRow(x) {
3696
- if (!x || typeof x !== "object") return false;
3697
- const r = x;
3698
- return typeof r.intent_archetype === "string" && typeof r.family === "string" && typeof r.candidate_model === "string" && typeof r.current_model === "string" && typeof r.detected_at === "string";
3699
- }
3700
- function coerceNumber(v) {
3733
+ // src/measured-failure-brain.ts
3734
+ function coerceCount(v) {
3701
3735
  if (typeof v === "number") return Number.isFinite(v) ? v : null;
3702
3736
  if (typeof v === "string") {
3703
3737
  const n = Number(v);
@@ -3705,58 +3739,144 @@ function coerceNumber(v) {
3705
3739
  }
3706
3740
  return null;
3707
3741
  }
3708
- function mapRowsToFindings2(rows) {
3742
+ function isRawFailureRow(x) {
3743
+ if (!x || typeof x !== "object") return false;
3744
+ const r = x;
3745
+ return typeof r.intent_archetype === "string" && typeof r.model === "string" && (typeof r.n === "number" || typeof r.n === "string");
3746
+ }
3747
+ function mapRows(rows) {
3709
3748
  const out = [];
3710
3749
  for (const row of rows) {
3711
- if (!isRawPromoteReadyRow(row)) continue;
3712
- const sampleN = coerceNumber(row.sample_n);
3713
- const passRate = coerceNumber(row.judge_pass_rate);
3714
- const avgScore = coerceNumber(row.judge_avg_score);
3715
- if (sampleN === null || passRate === null || avgScore === null) continue;
3750
+ if (!isRawFailureRow(row)) continue;
3751
+ const n = coerceCount(row.n);
3752
+ const nFail = coerceCount(row.n_fail) ?? 0;
3753
+ if (n === null || n <= 0) continue;
3716
3754
  out.push({
3717
3755
  archetype: row.intent_archetype,
3718
- family: row.family,
3719
- candidateModel: row.candidate_model,
3720
- currentModel: row.current_model,
3721
- sampleN,
3722
- judgePassRate: passRate,
3723
- judgeAvgScore: avgScore,
3724
- costDeltaPct: coerceNumber(row.cost_delta_pct),
3725
- detectedAt: row.detected_at
3756
+ model: row.model,
3757
+ n,
3758
+ nFail
3726
3759
  });
3727
3760
  }
3728
3761
  return out;
3729
3762
  }
3763
+ var MEASURED_FAILURE_CFG = {
3764
+ /**
3765
+ * Hard minimum attempts before ANY gate may be created. Guards against
3766
+ * pathological tiny samples that the confidence bound alone would let
3767
+ * through in edge cases. At 5-for-5 the bound clears the threshold; at
3768
+ * 3-for-3 it does not, which is the behaviour we want (three failures is
3769
+ * a bad day, five in a row is a pattern).
3770
+ */
3771
+ minSample: 5,
3772
+ /**
3773
+ * Gate when we are 95% confident the model fails MORE OFTEN THAN IT
3774
+ * SUCCEEDS on this surface. Deliberately unarguable rather than tuned —
3775
+ * a model that probably fails the majority of the time has no business
3776
+ * leading a surface, whatever its declared scores say.
3777
+ */
3778
+ lowerBoundThreshold: 0.5,
3779
+ /** 95% one-sided-ish confidence (standard two-sided z at α=0.05). */
3780
+ z: 1.96,
3781
+ /** Must match the view's window. Documented here for the advisory text. */
3782
+ windowDays: 28
3783
+ };
3784
+ function wilsonLowerBound(failures, n, z = MEASURED_FAILURE_CFG.z) {
3785
+ if (n <= 0) return 0;
3786
+ const p = failures / n;
3787
+ const z2 = z * z;
3788
+ const denom = 1 + z2 / n;
3789
+ const centre = p + z2 / (2 * n);
3790
+ const margin = z * Math.sqrt(p * (1 - p) / n + z2 / (4 * n * n));
3791
+ const lower2 = (centre - margin) / denom;
3792
+ return lower2 < 0 ? 0 : lower2;
3793
+ }
3794
+ function mapMeasuredFailureRows(rows) {
3795
+ return mapRows(rows);
3796
+ }
3797
+ function judgeMeasuredFailure(row, cfg = MEASURED_FAILURE_CFG) {
3798
+ if (!row) return void 0;
3799
+ const normalized = "nFail" in row && typeof row.n === "number" ? row : mapRows([row])[0];
3800
+ if (!normalized || normalized.n < cfg.minSample) return void 0;
3801
+ const lowerBound = wilsonLowerBound(normalized.nFail, normalized.n, cfg.z);
3802
+ return {
3803
+ gated: lowerBound > cfg.lowerBoundThreshold,
3804
+ rate: normalized.nFail / normalized.n,
3805
+ lowerBound,
3806
+ n: normalized.n,
3807
+ nFail: normalized.nFail
3808
+ };
3809
+ }
3730
3810
  var snapshots2 = /* @__PURE__ */ new Map();
3731
3811
  var runtime3;
3732
3812
  var warnedOnce2 = false;
3733
- function isPromoteReadyBrainActive() {
3813
+ var DEFAULT_MEASURED_FAILURE_ENDPOINT = "https://kgauto-dashboard.vercel.app/api/kgauto-v2/measured-failure";
3814
+ function isMeasuredFailureGateEnabledFromEnv(envSource) {
3815
+ const env = envSource ?? (typeof process !== "undefined" && process.env ? process.env : {});
3816
+ const raw = (env.KGAUTO_MEASURED_FAILURE_GATE ?? "").trim().toLowerCase();
3817
+ return !(raw === "0" || raw === "false");
3818
+ }
3819
+ function configureMeasuredFailureBrain(rt) {
3820
+ runtime3 = rt;
3821
+ snapshots2.clear();
3822
+ warnedOnce2 = false;
3823
+ }
3824
+ function isMeasuredFailureBrainActive() {
3734
3825
  return runtime3 !== void 0;
3735
3826
  }
3736
- function loadPromoteReadyFindings(opts) {
3827
+ function prefetchMeasuredFailure(appId) {
3737
3828
  const rt = runtime3;
3738
- if (!rt) return [];
3739
- const appId = opts.appId;
3740
- if (!appId) return [];
3829
+ if (!rt || !appId) return void 0;
3830
+ let snap = snapshots2.get(appId);
3831
+ if (!snap) {
3832
+ snap = { data: [], expiresAt: 0, refreshing: false };
3833
+ snapshots2.set(appId, snap);
3834
+ }
3835
+ if (snap.expiresAt > Date.now()) return void 0;
3836
+ const inflight = pendingRefreshes2.get(appId);
3837
+ if (inflight) return inflight;
3838
+ if (snap.refreshing) return void 0;
3839
+ snap.refreshing = true;
3840
+ void asyncRefresh3(rt, appId);
3841
+ return pendingRefreshes2.get(appId);
3842
+ }
3843
+ async function awaitMeasuredFailureReady(appId, timeoutMs) {
3844
+ if (!runtime3 || !appId) return;
3845
+ const pending = prefetchMeasuredFailure(appId) ?? pendingRefreshes2.get(appId);
3846
+ if (!(timeoutMs > 0)) return;
3847
+ if (!pending) return;
3848
+ let timer;
3849
+ try {
3850
+ await Promise.race([
3851
+ pending,
3852
+ new Promise((resolve) => {
3853
+ timer = setTimeout(resolve, timeoutMs);
3854
+ })
3855
+ ]);
3856
+ } catch {
3857
+ } finally {
3858
+ if (timer) clearTimeout(timer);
3859
+ }
3860
+ }
3861
+ function getMeasuredFailureVerdict(opts) {
3862
+ const rt = runtime3;
3863
+ if (!rt) return void 0;
3864
+ const { appId, archetype, model } = opts;
3865
+ if (!appId || !archetype || !model) return void 0;
3741
3866
  let snap = snapshots2.get(appId);
3742
3867
  if (!snap) {
3743
3868
  snap = { data: [], expiresAt: 0, refreshing: false };
3744
3869
  snapshots2.set(appId, snap);
3745
3870
  }
3746
3871
  const now = Date.now();
3747
- const stale = snap.expiresAt <= now;
3748
- if (stale && !snap.refreshing) {
3872
+ if (snap.expiresAt <= now && !snap.refreshing) {
3749
3873
  snap.refreshing = true;
3750
3874
  void asyncRefresh3(rt, appId);
3751
3875
  }
3752
- let rows = snap.data;
3753
- if (opts.archetype) {
3754
- rows = rows.filter((f) => f.archetype === opts.archetype);
3755
- }
3756
- if (opts.family) {
3757
- rows = rows.filter((f) => f.family === opts.family);
3758
- }
3759
- return rows;
3876
+ const row = snap.data.find(
3877
+ (r) => r.archetype === archetype && r.model === model
3878
+ );
3879
+ return judgeMeasuredFailure(row);
3760
3880
  }
3761
3881
  var pendingRefreshes2 = /* @__PURE__ */ new Map();
3762
3882
  async function asyncRefresh3(rt, appId) {
@@ -3780,12 +3900,11 @@ async function doRefresh3(rt, appId) {
3780
3900
  try {
3781
3901
  const res = await rt.fetchImpl(url, { method: "GET" });
3782
3902
  if (!res.ok) {
3783
- throw new Error(`promote-ready ${res.status}: ${res.statusText}`);
3903
+ throw new Error(`measured-failure ${res.status}: ${res.statusText}`);
3784
3904
  }
3785
3905
  const body = await res.json();
3786
3906
  if (runtime3 !== rt) return;
3787
- const rows = Array.isArray(body) ? mapRowsToFindings2(body) : [];
3788
- snap.data = rows;
3907
+ snap.data = Array.isArray(body) ? mapRows(body) : [];
3789
3908
  snap.expiresAt = Date.now() + rt.ttlMs;
3790
3909
  snap.refreshing = false;
3791
3910
  } catch (err) {
@@ -3800,193 +3919,84 @@ async function doRefresh3(rt, appId) {
3800
3919
  }
3801
3920
  function defaultOnError3(err) {
3802
3921
  console.warn(
3803
- "[kgauto] promote-ready fetch failed (using empty fallback):",
3922
+ "[kgauto] measured-failure fetch failed (gate inactive until next refresh):",
3804
3923
  err
3805
3924
  );
3806
3925
  }
3807
- function resolveFetchImpl(injected) {
3808
- return injected ?? ((...args) => globalThis.fetch(...args));
3809
- }
3810
- function normalizeEndpoint(endpoint) {
3811
- return endpoint.replace(/\/+$/, "");
3926
+ function _testResetMeasuredFailure() {
3927
+ runtime3 = void 0;
3928
+ snapshots2.clear();
3929
+ pendingRefreshes2 = /* @__PURE__ */ new Map();
3930
+ warnedOnce2 = false;
3812
3931
  }
3813
- async function markPromoteReadyHandled(opts) {
3814
- const {
3815
- appId,
3816
- archetype,
3817
- family,
3818
- resolution,
3819
- resolutionNote,
3820
- brainEndpoint,
3821
- brainJwt,
3822
- brainAnonKey,
3823
- fetch: injectedFetch
3824
- } = opts;
3825
- if (!appId) return { ok: false, reason: "app_id_required" };
3826
- if (!archetype) return { ok: false, reason: "archetype_required" };
3827
- if (!family) return { ok: false, reason: "family_required" };
3828
- if (resolution !== "promoted" && resolution !== "declined" && resolution !== "still-evaluating") {
3829
- return { ok: false, reason: "resolution_invalid" };
3830
- }
3831
- const doFetch = resolveFetchImpl(injectedFetch);
3832
- const base = normalizeEndpoint(brainEndpoint);
3833
- const url = `${base}/rest/v1/promote_ready_findings?app_id=eq.${encodeURIComponent(appId)}&intent_archetype=eq.${encodeURIComponent(archetype)}&family=eq.${encodeURIComponent(family)}&resolved_at=is.null`;
3834
- const patchBody = {
3835
- resolved_at: (/* @__PURE__ */ new Date()).toISOString(),
3836
- resolution
3837
- };
3838
- if (resolutionNote !== void 0) {
3839
- patchBody.resolution_note = resolutionNote;
3840
- }
3841
- let res;
3842
- try {
3843
- res = await doFetch(url, {
3844
- method: "PATCH",
3845
- headers: {
3846
- Authorization: `Bearer ${brainJwt}`,
3847
- apikey: brainAnonKey,
3848
- "Content-Type": "application/json",
3849
- Accept: "application/json",
3850
- Prefer: "return=minimal"
3851
- },
3852
- body: JSON.stringify(patchBody)
3853
- });
3854
- } catch (err) {
3855
- const msg = err instanceof Error ? err.message : String(err);
3856
- return { ok: false, reason: `network_error:${msg}` };
3857
- }
3858
- if (res.status === 401 || res.status === 403) {
3859
- return { ok: false, reason: "brain_auth_misconfig" };
3860
- }
3861
- if (res.status >= 500) {
3862
- return { ok: false, reason: "brain_unavailable" };
3863
- }
3864
- if (!res.ok) {
3865
- return { ok: false, reason: `patch_failed:${res.status}` };
3866
- }
3867
- return { ok: true };
3932
+ async function _testWaitForMeasuredFailureRefresh() {
3933
+ const pending = Array.from(pendingRefreshes2.values());
3934
+ if (pending.length > 0) await Promise.all(pending);
3868
3935
  }
3869
3936
 
3870
- // src/advisor-rules/promote-ready.ts
3871
- var PROMOTE_READY_THRESHOLDS = {
3872
- minPassRate: 0.8,
3873
- minAvgScore: 4
3874
- };
3875
- function shouldFirePromoteReady(finding, resolvedPrimary) {
3876
- if (finding.currentModel !== resolvedPrimary) return false;
3877
- if (finding.judgePassRate < PROMOTE_READY_THRESHOLDS.minPassRate) return false;
3878
- if (finding.judgeAvgScore < PROMOTE_READY_THRESHOLDS.minAvgScore) return false;
3879
- return true;
3937
+ // src/promotions-brain.ts
3938
+ function isRawPromotionRow(x) {
3939
+ if (!x || typeof x !== "object") return false;
3940
+ const r = x;
3941
+ return (typeof r.id === "number" || typeof r.id === "string") && typeof r.intent_archetype === "string" && typeof r.promoted_model === "string" && typeof r.incumbent_model === "string";
3880
3942
  }
3881
- function deriveFamilyLocal(modelId) {
3882
- if (modelId.startsWith("claude-opus-")) return "claude-opus";
3883
- if (modelId.startsWith("claude-sonnet-")) return "claude-sonnet";
3884
- if (modelId.startsWith("claude-haiku-")) return "claude-haiku";
3885
- if (/^gemini-.*-flash-lite/.test(modelId)) return "gemini-flash-lite";
3886
- if (/^gemini-.*-flash/.test(modelId)) return "gemini-flash";
3887
- if (/^gemini-.*-pro/.test(modelId)) return "gemini-pro";
3888
- if (/^deepseek-.*-pro/.test(modelId)) return "deepseek-reasoner";
3889
- if (modelId.startsWith("deepseek-")) return "deepseek-chat";
3890
- if (modelId.startsWith("gpt-")) return "openai-gpt";
3943
+ function coerceId(v) {
3944
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
3945
+ if (typeof v === "string") {
3946
+ const n = Number(v);
3947
+ return Number.isFinite(n) ? n : null;
3948
+ }
3891
3949
  return null;
3892
3950
  }
3893
- function advisorRulePromoteReady(ctx) {
3894
- if (!isPromoteReadyBrainActive()) return [];
3895
- if (!ctx.appId) return [];
3896
- if (!ctx.resolvedPrimary) return [];
3897
- const family = deriveFamilyLocal(ctx.resolvedPrimary);
3898
- if (!family) return [];
3899
- const findings = loadPromoteReadyFindings({
3900
- appId: ctx.appId,
3901
- archetype: ctx.archetype,
3902
- family
3903
- });
3904
- if (findings.length === 0) return [];
3905
- const qualifying = findings.filter(
3906
- (f) => shouldFirePromoteReady(f, ctx.resolvedPrimary)
3907
- );
3908
- if (qualifying.length === 0) return [];
3909
- qualifying.sort((a, b) => {
3910
- if (a.judgeAvgScore !== b.judgeAvgScore) {
3911
- return b.judgeAvgScore - a.judgeAvgScore;
3912
- }
3913
- return b.judgePassRate - a.judgePassRate;
3914
- });
3915
- const top = qualifying[0];
3916
- const pctPass = Math.round(top.judgePassRate * 100);
3917
- const score = top.judgeAvgScore.toFixed(2);
3918
- let costClause = "";
3919
- if (top.costDeltaPct !== null) {
3920
- const sign = top.costDeltaPct < 0 ? "cheaper" : "more expensive";
3921
- const magnitude = Math.abs(top.costDeltaPct * 100).toFixed(1);
3922
- costClause = `, cost ${magnitude}% ${sign}`;
3923
- }
3924
- const message = `Probe found ${top.candidateModel} produces equivalent-or-better outputs vs ${top.currentModel} on ${top.sampleN} recent ${top.archetype} prompts (pass rate ${pctPass}%, avg score ${score}/5${costClause}). Consider promoting via markPromoteReadyHandled.`;
3925
- return [
3926
- {
3927
- level: "info",
3928
- code: "promote-ready",
3929
- message,
3930
- suggestion: `Migrate ${top.archetype} traffic from ${top.currentModel} to ${top.candidateModel}, then call markPromoteReadyHandled({ appId, archetype: '${top.archetype}', family: '${top.family}', resolution: 'promoted' }) to silence this advisory.`,
3931
- // alpha.36 architectural field — not a no-ai-needed case.
3932
- recommendedArchitecture: void 0,
3933
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
3934
- }
3935
- ];
3936
- }
3937
-
3938
- // src/advisor-rules/consumer-on-stale-model.ts
3939
- function isStaleStatus(v) {
3940
- return v === "legacy" || v === "deprecated";
3941
- }
3942
- function asString(v) {
3943
- return typeof v === "string" && v.length > 0 ? v : void 0;
3944
- }
3945
- function mapRowsToFindings3(rows) {
3951
+ function mapRowsToPromotions(rows) {
3946
3952
  const out = [];
3947
- for (const raw of rows) {
3948
- if (!raw || typeof raw !== "object") continue;
3949
- const r = raw;
3950
- const archetype = asString(r.intent_archetype) ?? asString(r.applies_to_archetype);
3951
- const staleModel = asString(r.stale_model) ?? asString(r.applies_to_model);
3952
- const staleProvider = asString(r.stale_provider);
3953
- const recommendedModel = asString(r.recommended_model);
3954
- const family = asString(r.family);
3955
- const message = asString(r.message);
3956
- if (!archetype || !staleModel || !recommendedModel || !family || !message) {
3957
- continue;
3958
- }
3959
- if (!isStaleStatus(r.stale_status)) continue;
3960
- const row = {
3961
- archetype,
3962
- staleModel,
3963
- staleProvider: staleProvider ?? "unknown",
3964
- staleStatus: r.stale_status,
3965
- recommendedModel,
3966
- family,
3967
- message
3968
- };
3969
- const suggestion = asString(r.suggestion);
3970
- if (suggestion) row.suggestion = suggestion;
3971
- if (typeof r.observation_count === "number" && Number.isFinite(r.observation_count)) {
3972
- row.observationCount = r.observation_count;
3973
- }
3974
- out.push(row);
3953
+ for (const row of rows) {
3954
+ if (!isRawPromotionRow(row)) continue;
3955
+ const id = coerceId(row.id);
3956
+ if (id === null) continue;
3957
+ const mode = row.mode === "strategy" ? "strategy" : row.mode === "downswap" || row.mode === void 0 ? "downswap" : null;
3958
+ if (mode === null) continue;
3959
+ out.push({
3960
+ id,
3961
+ archetype: row.intent_archetype,
3962
+ mode,
3963
+ strategy: typeof row.strategy === "string" ? row.strategy : null,
3964
+ promotedModel: row.promoted_model,
3965
+ incumbentModel: row.incumbent_model,
3966
+ evalRunId: coerceId(row.eval_run_id ?? null),
3967
+ suppressQualityGate: row.suppress_quality_gate === true,
3968
+ promotedAt: typeof row.promoted_at === "string" ? row.promoted_at : "",
3969
+ // Pre-.78 endpoints serve no status column and only active rows —
3970
+ // defaulting to 'active' is exact, not optimistic.
3971
+ status: row.status === "rolled_back" ? "rolled_back" : "active",
3972
+ ...typeof row.rolled_back_at === "string" ? { rolledBackAt: row.rolled_back_at } : {},
3973
+ ...typeof row.rollback_class === "string" ? { rollbackClass: row.rollback_class } : {}
3974
+ });
3975
3975
  }
3976
3976
  return out;
3977
3977
  }
3978
3978
  var snapshots3 = /* @__PURE__ */ new Map();
3979
3979
  var runtime4;
3980
3980
  var warnedOnce3 = false;
3981
- var pendingRefreshes3 = /* @__PURE__ */ new Map();
3982
- function isStaleModelFindingsBrainActive() {
3981
+ var DEFAULT_PROMOTIONS_ENDPOINT = "https://kgauto-dashboard.vercel.app/api/kgauto-v2/promotions";
3982
+ function isAutoPromoteEnabledFromEnv(envSource) {
3983
+ const env = envSource ?? (typeof process !== "undefined" && process.env ? process.env : {});
3984
+ const raw = (env.KGAUTO_AUTO_PROMOTE ?? "").trim().toLowerCase();
3985
+ return raw === "1" || raw === "true";
3986
+ }
3987
+ function configurePromotionsBrain(rt) {
3988
+ runtime4 = rt;
3989
+ snapshots3.clear();
3990
+ warnedOnce3 = false;
3991
+ }
3992
+ function isPromotionsBrainActive() {
3983
3993
  return runtime4 !== void 0;
3984
3994
  }
3985
- function getStaleModelFindings(opts) {
3995
+ function getApplicablePromotion(opts) {
3986
3996
  const rt = runtime4;
3987
- if (!rt) return [];
3997
+ if (!rt) return void 0;
3988
3998
  const appId = opts.appId;
3989
- if (!appId) return [];
3999
+ if (!appId || !opts.archetype || !opts.mode) return void 0;
3990
4000
  let snap = snapshots3.get(appId);
3991
4001
  if (!snap) {
3992
4002
  snap = { data: [], expiresAt: 0, refreshing: false };
@@ -3998,11 +4008,31 @@ function getStaleModelFindings(opts) {
3998
4008
  snap.refreshing = true;
3999
4009
  void asyncRefresh4(rt, appId);
4000
4010
  }
4001
- if (opts.archetype) {
4002
- return snap.data.filter((f) => f.archetype === opts.archetype);
4011
+ return snap.data.find(
4012
+ (p) => p.status === "active" && p.archetype === opts.archetype && p.mode === opts.mode
4013
+ );
4014
+ }
4015
+ var ROLLBACK_SUPPRESSION_WINDOW_DAYS = 28;
4016
+ function getRecentRollback(opts) {
4017
+ const rt = runtime4;
4018
+ if (!rt) return void 0;
4019
+ if (!opts.appId || !opts.archetype || !opts.model) return void 0;
4020
+ let snap = snapshots3.get(opts.appId);
4021
+ if (!snap) {
4022
+ snap = { data: [], expiresAt: 0, refreshing: false };
4023
+ snapshots3.set(opts.appId, snap);
4003
4024
  }
4004
- return snap.data;
4025
+ const now = opts.nowMs ?? Date.now();
4026
+ if (snap.expiresAt <= now && !snap.refreshing) {
4027
+ snap.refreshing = true;
4028
+ void asyncRefresh4(rt, opts.appId);
4029
+ }
4030
+ const windowMs = (opts.windowDays ?? ROLLBACK_SUPPRESSION_WINDOW_DAYS) * 864e5;
4031
+ return snap.data.find(
4032
+ (p) => p.status === "rolled_back" && p.archetype === opts.archetype && p.promotedModel === opts.model && typeof p.rolledBackAt === "string" && now - Date.parse(p.rolledBackAt) <= windowMs
4033
+ );
4005
4034
  }
4035
+ var pendingRefreshes3 = /* @__PURE__ */ new Map();
4006
4036
  async function asyncRefresh4(rt, appId) {
4007
4037
  const promise = doRefresh4(rt, appId);
4008
4038
  pendingRefreshes3.set(appId, promise);
@@ -4015,7 +4045,7 @@ async function asyncRefresh4(rt, appId) {
4015
4045
  }
4016
4046
  }
4017
4047
  async function doRefresh4(rt, appId) {
4018
- const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
4048
+ const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}&with_rollbacks=1`;
4019
4049
  let snap = snapshots3.get(appId);
4020
4050
  if (!snap) {
4021
4051
  snap = { data: [], expiresAt: 0, refreshing: false };
@@ -4024,11 +4054,11 @@ async function doRefresh4(rt, appId) {
4024
4054
  try {
4025
4055
  const res = await rt.fetchImpl(url, { method: "GET" });
4026
4056
  if (!res.ok) {
4027
- throw new Error(`stale-model findings ${res.status}: ${res.statusText}`);
4057
+ throw new Error(`promotions ${res.status}: ${res.statusText}`);
4028
4058
  }
4029
4059
  const body = await res.json();
4030
4060
  if (runtime4 !== rt) return;
4031
- const rows = Array.isArray(body) ? mapRowsToFindings3(body) : [];
4061
+ const rows = Array.isArray(body) ? mapRowsToPromotions(body) : [];
4032
4062
  snap.data = rows;
4033
4063
  snap.expiresAt = Date.now() + rt.ttlMs;
4034
4064
  snap.refreshing = false;
@@ -4044,847 +4074,905 @@ async function doRefresh4(rt, appId) {
4044
4074
  }
4045
4075
  function defaultOnError4(err) {
4046
4076
  console.warn(
4047
- "[kgauto] stale-model findings fetch failed (using empty fallback):",
4077
+ "[kgauto] promotions fetch failed (promotion boost inactive until next refresh):",
4048
4078
  err
4049
4079
  );
4050
4080
  }
4051
- var CONSUMER_ON_STALE_MODEL_RULE_CODE = "consumer-on-stale-model";
4052
- function advisorRuleConsumerOnStaleModel(ir) {
4053
- if (!isStaleModelFindingsBrainActive()) return [];
4054
- if (!ir.appId) return [];
4055
- const findings = getStaleModelFindings({
4056
- appId: ir.appId,
4057
- archetype: ir.intent.archetype
4058
- });
4059
- if (findings.length === 0) return [];
4060
- const ranked = [...findings].sort((a, b) => {
4061
- if (a.staleStatus !== b.staleStatus) {
4062
- return a.staleStatus === "deprecated" ? -1 : 1;
4063
- }
4064
- return a.staleModel.localeCompare(b.staleModel);
4065
- });
4066
- const top = ranked[0];
4067
- const extraCount = findings.length - 1;
4068
- const extraNote = extraCount > 0 ? ` (+ ${extraCount} more stale model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
4069
- return [
4070
- {
4071
- level: "warn",
4072
- code: CONSUMER_ON_STALE_MODEL_RULE_CODE,
4073
- message: `${top.message}${extraNote}`,
4074
- suggestion: top.suggestion ?? `Migrate ${top.staleModel} \u2192 ${top.recommendedModel} for archetype "${top.archetype}". The newer model is the current latest in the "${top.family}" family; the stale one is ${top.staleStatus}.`,
4075
- recommendationType: "model-swap",
4076
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4077
- }
4078
- ];
4081
+ function _testResetPromotions() {
4082
+ runtime4 = void 0;
4083
+ snapshots3.clear();
4084
+ pendingRefreshes3 = /* @__PURE__ */ new Map();
4085
+ warnedOnce3 = false;
4086
+ }
4087
+ async function _testWaitForPromotionsRefresh() {
4088
+ const pending = Array.from(pendingRefreshes3.values());
4089
+ if (pending.length > 0) await Promise.all(pending);
4079
4090
  }
4080
4091
 
4081
- // src/archetype-fits.ts
4082
- var ARCHETYPE_FAMILY_FITS = Object.freeze([
4083
- {
4084
- archetype: "plan",
4085
- betterFitFamily: "deepseek-reasoner",
4086
- reason: "Plan archetype is reasoning-shaped (multi-step chains, hypothesis-and-check, sub-goal decomposition) \u2014 exactly where reasoner-family models excel. Sonnet/Opus produce plans but at higher cost; reasoners produce equivalent-or-better plans at 7-17x lower cost at current promo pricing (deepseek-v4-pro $0.435/$0.87 per 1M promo through 2026-05-31 vs sonnet $3/$15).",
4087
- costGuidance: "substantially cheaper at current pricing (deepseek-v4-pro promo: ~7-17x cheaper than sonnet)"
4088
- },
4089
- {
4090
- archetype: "critique",
4091
- betterFitFamily: "deepseek-reasoner",
4092
- reason: "Critique archetype rewards epistemic humility and explicit reasoning \u2014 reasoner-family default behavior. Sonnet/Opus over-confident on critique tasks; reasoners surface uncertainty productively.",
4093
- costGuidance: "comparable or cheaper at current pricing"
4094
- }
4095
- ]);
4096
- function findBetterFit(archetype, currentFamily) {
4097
- for (const fit of ARCHETYPE_FAMILY_FITS) {
4098
- if (fit.archetype !== archetype) continue;
4099
- if (fit.betterFitFamily === currentFamily) return null;
4100
- return fit;
4092
+ // src/promote-ready-brain.ts
4093
+ function isRawPromoteReadyRow(x) {
4094
+ if (!x || typeof x !== "object") return false;
4095
+ const r = x;
4096
+ return typeof r.intent_archetype === "string" && typeof r.family === "string" && typeof r.candidate_model === "string" && typeof r.current_model === "string" && typeof r.detected_at === "string";
4097
+ }
4098
+ function coerceNumber(v) {
4099
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
4100
+ if (typeof v === "string") {
4101
+ const n = Number(v);
4102
+ return Number.isFinite(n) ? n : null;
4101
4103
  }
4102
4104
  return null;
4103
4105
  }
4104
-
4105
- // src/advisor-rules/cross-family-fit.ts
4106
- function familyHasCurrentActiveModel(family) {
4107
- for (const profile of allProfiles()) {
4108
- const profileFamily = profile.family ?? deriveFamilyFromModelId(profile.id);
4109
- if (profileFamily !== family) continue;
4110
- if (profile.status !== "current") continue;
4111
- if (profile.active === false) continue;
4112
- return true;
4106
+ function mapRowsToFindings2(rows) {
4107
+ const out = [];
4108
+ for (const row of rows) {
4109
+ if (!isRawPromoteReadyRow(row)) continue;
4110
+ const sampleN = coerceNumber(row.sample_n);
4111
+ const passRate = coerceNumber(row.judge_pass_rate);
4112
+ const avgScore = coerceNumber(row.judge_avg_score);
4113
+ if (sampleN === null || passRate === null || avgScore === null) continue;
4114
+ out.push({
4115
+ archetype: row.intent_archetype,
4116
+ family: row.family,
4117
+ candidateModel: row.candidate_model,
4118
+ currentModel: row.current_model,
4119
+ sampleN,
4120
+ judgePassRate: passRate,
4121
+ judgeAvgScore: avgScore,
4122
+ costDeltaPct: coerceNumber(row.cost_delta_pct),
4123
+ detectedAt: row.detected_at
4124
+ });
4113
4125
  }
4114
- return false;
4126
+ return out;
4115
4127
  }
4116
- function listCandidatesInFamily(family) {
4117
- const candidates = [];
4118
- for (const profile of allProfiles()) {
4119
- const profileFamily = profile.family ?? deriveFamilyFromModelId(profile.id);
4120
- if (profileFamily !== family) continue;
4121
- if (profile.status !== "current") continue;
4122
- if (profile.active === false) continue;
4123
- candidates.push(profile.id);
4124
- if (candidates.length >= 3) break;
4125
- }
4126
- return candidates;
4128
+ var snapshots4 = /* @__PURE__ */ new Map();
4129
+ var runtime5;
4130
+ var warnedOnce4 = false;
4131
+ function isPromoteReadyBrainActive() {
4132
+ return runtime5 !== void 0;
4127
4133
  }
4128
- function advisorRuleCrossFamilyFit(ctx) {
4129
- if (!ctx.resolvedPrimary) return [];
4130
- const currentFamily = deriveFamilyFromModelId(ctx.resolvedPrimary);
4131
- if (!currentFamily) return [];
4132
- const fit = findBetterFit(ctx.archetype, currentFamily);
4133
- if (!fit) return [];
4134
- if (!familyHasCurrentActiveModel(fit.betterFitFamily)) return [];
4135
- const candidates = listCandidatesInFamily(fit.betterFitFamily);
4136
- if (candidates.length === 0) return [];
4137
- const candidateStr = candidates.join(", ");
4138
- const message = `Your ${currentFamily} call on ${ctx.archetype} could shift to ${fit.betterFitFamily} \u2014 typically better quality + ${fit.costGuidance}. Suggested candidates: ${candidateStr}.`;
4139
- return [
4140
- {
4141
- level: "info",
4142
- code: "cross-family-fit-candidate",
4143
- ownership: "consumer-actionable",
4144
- message,
4145
- suggestion: `Swap the model literal in \`ir.models\` to one of: ${candidateStr}. Or call \`getRecommendedPrimary({ family: '${fit.betterFitFamily}', archetype: '${ctx.archetype}', fallback: { id: '${candidates[0]}', reason: 'cross-family-fit-recommendation' } })\` to let kgauto resolve to the current+active family member.`,
4146
- recommendationType: "model-swap",
4147
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4134
+ function loadPromoteReadyFindings(opts) {
4135
+ const rt = runtime5;
4136
+ if (!rt) return [];
4137
+ const appId = opts.appId;
4138
+ if (!appId) return [];
4139
+ let snap = snapshots4.get(appId);
4140
+ if (!snap) {
4141
+ snap = { data: [], expiresAt: 0, refreshing: false };
4142
+ snapshots4.set(appId, snap);
4143
+ }
4144
+ const now = Date.now();
4145
+ const stale = snap.expiresAt <= now;
4146
+ if (stale && !snap.refreshing) {
4147
+ snap.refreshing = true;
4148
+ void asyncRefresh5(rt, appId);
4149
+ }
4150
+ let rows = snap.data;
4151
+ if (opts.archetype) {
4152
+ rows = rows.filter((f) => f.archetype === opts.archetype);
4153
+ }
4154
+ if (opts.family) {
4155
+ rows = rows.filter((f) => f.family === opts.family);
4156
+ }
4157
+ return rows;
4158
+ }
4159
+ var pendingRefreshes4 = /* @__PURE__ */ new Map();
4160
+ async function asyncRefresh5(rt, appId) {
4161
+ const promise = doRefresh5(rt, appId);
4162
+ pendingRefreshes4.set(appId, promise);
4163
+ try {
4164
+ await promise;
4165
+ } finally {
4166
+ if (pendingRefreshes4.get(appId) === promise) {
4167
+ pendingRefreshes4.delete(appId);
4148
4168
  }
4149
- ];
4169
+ }
4150
4170
  }
4151
-
4152
- // src/advisor.ts
4153
- var QUALITY_FLOOR_FOR_RECOMMENDATION = 6;
4154
- var TIER_DOWN_COST_RATIO = 0.5;
4155
- var COST_MISMATCHED_CHOSEN_SCORE_CEILING = 7;
4156
- var PRODUCER_OWNED_RULE_CODES = Object.freeze(
4157
- /* @__PURE__ */ new Set(["model-stale-evidence", "promote-ready"])
4158
- );
4159
- function deriveOwnership(code, selfDeclared) {
4160
- if (selfDeclared) return selfDeclared;
4161
- return PRODUCER_OWNED_RULE_CODES.has(code) ? "producer-owned" : "consumer-actionable";
4171
+ async function doRefresh5(rt, appId) {
4172
+ const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
4173
+ let snap = snapshots4.get(appId);
4174
+ if (!snap) {
4175
+ snap = { data: [], expiresAt: 0, refreshing: false };
4176
+ snapshots4.set(appId, snap);
4177
+ }
4178
+ try {
4179
+ const res = await rt.fetchImpl(url, { method: "GET" });
4180
+ if (!res.ok) {
4181
+ throw new Error(`promote-ready ${res.status}: ${res.statusText}`);
4182
+ }
4183
+ const body = await res.json();
4184
+ if (runtime5 !== rt) return;
4185
+ const rows = Array.isArray(body) ? mapRowsToFindings2(body) : [];
4186
+ snap.data = rows;
4187
+ snap.expiresAt = Date.now() + rt.ttlMs;
4188
+ snap.refreshing = false;
4189
+ } catch (err) {
4190
+ if (runtime5 !== rt) return;
4191
+ snap.refreshing = false;
4192
+ snap.expiresAt = Date.now() + rt.ttlMs;
4193
+ if (!warnedOnce4) {
4194
+ warnedOnce4 = true;
4195
+ (rt.onError ?? defaultOnError5)(err);
4196
+ }
4197
+ }
4162
4198
  }
4163
- function runAdvisor(ir, result, profile, policy, phase2) {
4164
- const out = [];
4165
- out.push(...detectCachingOff(ir, profile));
4166
- out.push(...detectSingleChunkSystem(ir, profile));
4167
- out.push(...detectToolBloat(ir, result));
4168
- out.push(...detectHistoryUncached(ir, profile));
4169
- out.push(...detectSingleModelArray(ir, policy));
4170
- if (policy?.posture !== "locked") {
4171
- out.push(...detectCostMismatchedArchetype(ir, profile, phase2));
4172
- out.push(...detectModelStaleEvidence(ir, profile));
4173
- out.push(...detectTierDown(ir, profile, phase2));
4199
+ function defaultOnError5(err) {
4200
+ console.warn(
4201
+ "[kgauto] promote-ready fetch failed (using empty fallback):",
4202
+ err
4203
+ );
4204
+ }
4205
+ function resolveFetchImpl(injected) {
4206
+ return injected ?? ((...args) => globalThis.fetch(...args));
4207
+ }
4208
+ function normalizeEndpoint(endpoint) {
4209
+ return endpoint.replace(/\/+$/, "");
4210
+ }
4211
+ async function markPromoteReadyHandled(opts) {
4212
+ const {
4213
+ appId,
4214
+ archetype,
4215
+ family,
4216
+ resolution,
4217
+ resolutionNote,
4218
+ brainEndpoint,
4219
+ brainJwt,
4220
+ brainAnonKey,
4221
+ fetch: injectedFetch
4222
+ } = opts;
4223
+ if (!appId) return { ok: false, reason: "app_id_required" };
4224
+ if (!archetype) return { ok: false, reason: "archetype_required" };
4225
+ if (!family) return { ok: false, reason: "family_required" };
4226
+ if (resolution !== "promoted" && resolution !== "declined" && resolution !== "still-evaluating") {
4227
+ return { ok: false, reason: "resolution_invalid" };
4174
4228
  }
4175
- if (!translatorClearedToolCallCliff(phase2)) {
4176
- out.push(...detectArchetypePerfFloorBreach(ir, profile));
4229
+ const doFetch = resolveFetchImpl(injectedFetch);
4230
+ const base = normalizeEndpoint(brainEndpoint);
4231
+ const url = `${base}/rest/v1/promote_ready_findings?app_id=eq.${encodeURIComponent(appId)}&intent_archetype=eq.${encodeURIComponent(archetype)}&family=eq.${encodeURIComponent(family)}&resolved_at=is.null`;
4232
+ const patchBody = {
4233
+ resolved_at: (/* @__PURE__ */ new Date()).toISOString(),
4234
+ resolution
4235
+ };
4236
+ if (resolutionNote !== void 0) {
4237
+ patchBody.resolution_note = resolutionNote;
4177
4238
  }
4178
- if (policy?.posture !== "locked") {
4179
- out.push(...detectStaleExclusionCandidate(ir));
4239
+ let res;
4240
+ try {
4241
+ res = await doFetch(url, {
4242
+ method: "PATCH",
4243
+ headers: {
4244
+ Authorization: `Bearer ${brainJwt}`,
4245
+ apikey: brainAnonKey,
4246
+ "Content-Type": "application/json",
4247
+ Accept: "application/json",
4248
+ Prefer: "return=minimal"
4249
+ },
4250
+ body: JSON.stringify(patchBody)
4251
+ });
4252
+ } catch (err) {
4253
+ const msg = err instanceof Error ? err.message : String(err);
4254
+ return { ok: false, reason: `network_error:${msg}` };
4180
4255
  }
4181
- if (policy?.posture !== "locked" && ir.appId) {
4182
- out.push(
4183
- ...advisorRulePromoteReady({
4184
- appId: ir.appId,
4185
- archetype: ir.intent.archetype,
4186
- resolvedPrimary: profile.id
4187
- })
4188
- );
4189
- out.push(...advisorRuleConsumerOnStaleModel(ir));
4256
+ if (res.status === 401 || res.status === 403) {
4257
+ return { ok: false, reason: "brain_auth_misconfig" };
4190
4258
  }
4191
- if (policy?.posture !== "locked") {
4192
- out.push(
4193
- ...advisorRuleCrossFamilyFit({
4194
- archetype: ir.intent.archetype,
4195
- resolvedPrimary: profile.id
4196
- })
4197
- );
4259
+ if (res.status >= 500) {
4260
+ return { ok: false, reason: "brain_unavailable" };
4198
4261
  }
4199
- return out;
4200
- }
4201
- function translatorClearedToolCallCliff(phase2) {
4202
- const rewrites = phase2?.sectionRewritesApplied;
4203
- if (!rewrites || rewrites.length === 0) return false;
4204
- for (const rw of rewrites) {
4205
- if (rw.kind === "tool_call_contract") return true;
4262
+ if (!res.ok) {
4263
+ return { ok: false, reason: `patch_failed:${res.status}` };
4206
4264
  }
4207
- return false;
4265
+ return { ok: true };
4208
4266
  }
4209
- function detectCachingOff(ir, profile) {
4210
- if (profile.provider !== "anthropic") return [];
4211
- const totalChars = ir.sections.reduce((s, sec) => s + sec.text.length, 0);
4212
- if (totalChars < 2e3) return [];
4213
- const anyCacheable = ir.sections.some((s) => s.cacheable === true);
4214
- if (anyCacheable) return [];
4267
+
4268
+ // src/advisor-rules/promote-ready.ts
4269
+ var PROMOTE_READY_THRESHOLDS = {
4270
+ minPassRate: 0.8,
4271
+ minAvgScore: 4
4272
+ };
4273
+ function shouldFirePromoteReady(finding, resolvedPrimary) {
4274
+ if (finding.currentModel !== resolvedPrimary) return false;
4275
+ if (finding.judgePassRate < PROMOTE_READY_THRESHOLDS.minPassRate) return false;
4276
+ if (finding.judgeAvgScore < PROMOTE_READY_THRESHOLDS.minAvgScore) return false;
4277
+ return true;
4278
+ }
4279
+ function deriveFamilyLocal(modelId) {
4280
+ if (modelId.startsWith("claude-opus-")) return "claude-opus";
4281
+ if (modelId.startsWith("claude-sonnet-")) return "claude-sonnet";
4282
+ if (modelId.startsWith("claude-haiku-")) return "claude-haiku";
4283
+ if (/^gemini-.*-flash-lite/.test(modelId)) return "gemini-flash-lite";
4284
+ if (/^gemini-.*-flash/.test(modelId)) return "gemini-flash";
4285
+ if (/^gemini-.*-pro/.test(modelId)) return "gemini-pro";
4286
+ if (/^deepseek-.*-pro/.test(modelId)) return "deepseek-reasoner";
4287
+ if (modelId.startsWith("deepseek-")) return "deepseek-chat";
4288
+ if (modelId.startsWith("gpt-")) return "openai-gpt";
4289
+ return null;
4290
+ }
4291
+ function advisorRulePromoteReady(ctx) {
4292
+ if (!isPromoteReadyBrainActive()) return [];
4293
+ if (!ctx.appId) return [];
4294
+ if (!ctx.resolvedPrimary) return [];
4295
+ const family = deriveFamilyLocal(ctx.resolvedPrimary);
4296
+ if (!family) return [];
4297
+ const findings = loadPromoteReadyFindings({
4298
+ appId: ctx.appId,
4299
+ archetype: ctx.archetype,
4300
+ family
4301
+ });
4302
+ if (findings.length === 0) return [];
4303
+ const qualifying = findings.filter(
4304
+ (f) => shouldFirePromoteReady(f, ctx.resolvedPrimary)
4305
+ );
4306
+ if (qualifying.length === 0) return [];
4307
+ qualifying.sort((a, b) => {
4308
+ if (a.judgeAvgScore !== b.judgeAvgScore) {
4309
+ return b.judgeAvgScore - a.judgeAvgScore;
4310
+ }
4311
+ return b.judgePassRate - a.judgePassRate;
4312
+ });
4313
+ const top = qualifying[0];
4314
+ const pctPass = Math.round(top.judgePassRate * 100);
4315
+ const score = top.judgeAvgScore.toFixed(2);
4316
+ let costClause = "";
4317
+ if (top.costDeltaPct !== null) {
4318
+ const sign = top.costDeltaPct < 0 ? "cheaper" : "more expensive";
4319
+ const magnitude = Math.abs(top.costDeltaPct * 100).toFixed(1);
4320
+ costClause = `, cost ${magnitude}% ${sign}`;
4321
+ }
4322
+ const message = `Probe found ${top.candidateModel} produces equivalent-or-better outputs vs ${top.currentModel} on ${top.sampleN} recent ${top.archetype} prompts (pass rate ${pctPass}%, avg score ${score}/5${costClause}). Consider promoting via markPromoteReadyHandled.`;
4215
4323
  return [
4216
4324
  {
4217
- level: "warn",
4218
- code: "caching-off-on-claude",
4219
- message: `System prompt is ${totalChars} chars on Anthropic but no PromptSection has cacheable=true. Anthropic prompt caching cuts cached-prefix input cost by ~90% on subsequent calls; without it, every turn re-pays full price for the static system context.`,
4220
- suggestion: "Mark stable system sections (role, persona, tool policy) with `cacheable: true`. The lowering pass concatenates cacheable sections into a single cache-controlled block before the dynamic ones.",
4325
+ level: "info",
4326
+ code: "promote-ready",
4327
+ message,
4328
+ suggestion: `Migrate ${top.archetype} traffic from ${top.currentModel} to ${top.candidateModel}, then call markPromoteReadyHandled({ appId, archetype: '${top.archetype}', family: '${top.family}', resolution: 'promoted' }) to silence this advisory.`,
4329
+ // alpha.36 architectural field — not a no-ai-needed case.
4330
+ recommendedArchitecture: void 0,
4221
4331
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4222
4332
  }
4223
4333
  ];
4224
4334
  }
4225
- function detectSingleChunkSystem(ir, profile) {
4226
- if (profile.provider !== "anthropic") return [];
4227
- if (ir.sections.length !== 1) return [];
4228
- const only = ir.sections[0];
4229
- if (!only || only.text.length <= 1e3) return [];
4230
- return [
4231
- {
4232
- level: "info",
4233
- code: "single-chunk-system",
4234
- message: `System prompt is a single ${only.text.length}-char chunk. Splitting into NamedChunks (static role/persona vs dynamic context) gives the lowering pass a finer cache-marker boundary \u2014 only the static portion needs to be byte-stable for the cache to hit.`,
4235
- suggestion: "Refactor the system builder to return an array of `PromptSection` shaped { id, text, cacheable?: boolean }. Static chunks (role, persona, tool policy) get `cacheable: true`; dynamic ones (current context, today's date) don't.",
4236
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4237
- }
4238
- ];
4335
+
4336
+ // src/advisor-rules/consumer-on-stale-model.ts
4337
+ function isStaleStatus(v) {
4338
+ return v === "legacy" || v === "deprecated";
4239
4339
  }
4240
- function detectToolBloat(ir, result) {
4241
- const SHORT_OUTPUT = /* @__PURE__ */ new Set([
4242
- "classify",
4243
- "extract",
4244
- "summarize",
4245
- "transform",
4246
- "critique"
4247
- ]);
4248
- if (!ir.tools || ir.tools.length === 0) return [];
4249
- const toolsKept = result.diagnostics.toolsKept;
4250
- if (toolsKept <= 10) return [];
4251
- if (!SHORT_OUTPUT.has(ir.intent.archetype)) return [];
4252
- return [
4253
- {
4254
- level: "warn",
4255
- code: "tool-bloat",
4256
- message: `${toolsKept} tools kept after the relevance pass for archetype="${ir.intent.archetype}" (consumer declared ${ir.tools.length}). This archetype is short-output and rarely needs more than 3 tools; each tool definition eats ~350 tokens of context budget.`,
4257
- suggestion: "Tighten `relevanceByIntent: { [archetype]: 0..1 }` per ToolDefinition. Tools below `toolRelevanceThreshold` (default 0.2) get dropped. Without `relevanceByIntent`, every tool defaults to neutral (0.5) and stays.",
4258
- docsUrl: "https://github.com/stue/kgauto/blob/main/v2/README.md#tools"
4340
+ function asString(v) {
4341
+ return typeof v === "string" && v.length > 0 ? v : void 0;
4342
+ }
4343
+ function mapRowsToFindings3(rows) {
4344
+ const out = [];
4345
+ for (const raw of rows) {
4346
+ if (!raw || typeof raw !== "object") continue;
4347
+ const r = raw;
4348
+ const archetype = asString(r.intent_archetype) ?? asString(r.applies_to_archetype);
4349
+ const staleModel = asString(r.stale_model) ?? asString(r.applies_to_model);
4350
+ const staleProvider = asString(r.stale_provider);
4351
+ const recommendedModel = asString(r.recommended_model);
4352
+ const family = asString(r.family);
4353
+ const message = asString(r.message);
4354
+ if (!archetype || !staleModel || !recommendedModel || !family || !message) {
4355
+ continue;
4259
4356
  }
4260
- ];
4357
+ if (!isStaleStatus(r.stale_status)) continue;
4358
+ const row = {
4359
+ archetype,
4360
+ staleModel,
4361
+ staleProvider: staleProvider ?? "unknown",
4362
+ staleStatus: r.stale_status,
4363
+ recommendedModel,
4364
+ family,
4365
+ message
4366
+ };
4367
+ const suggestion = asString(r.suggestion);
4368
+ if (suggestion) row.suggestion = suggestion;
4369
+ if (typeof r.observation_count === "number" && Number.isFinite(r.observation_count)) {
4370
+ row.observationCount = r.observation_count;
4371
+ }
4372
+ out.push(row);
4373
+ }
4374
+ return out;
4261
4375
  }
4262
- function detectHistoryUncached(ir, profile) {
4263
- if (profile.provider !== "anthropic") return [];
4264
- if (!ir.history || ir.history.length < 2) return [];
4265
- if (ir.historyCachePolicy && ir.historyCachePolicy.strategy !== "none") {
4266
- return [];
4376
+ var snapshots5 = /* @__PURE__ */ new Map();
4377
+ var runtime6;
4378
+ var warnedOnce5 = false;
4379
+ var pendingRefreshes5 = /* @__PURE__ */ new Map();
4380
+ function isStaleModelFindingsBrainActive() {
4381
+ return runtime6 !== void 0;
4382
+ }
4383
+ function getStaleModelFindings(opts) {
4384
+ const rt = runtime6;
4385
+ if (!rt) return [];
4386
+ const appId = opts.appId;
4387
+ if (!appId) return [];
4388
+ let snap = snapshots5.get(appId);
4389
+ if (!snap) {
4390
+ snap = { data: [], expiresAt: 0, refreshing: false };
4391
+ snapshots5.set(appId, snap);
4267
4392
  }
4268
- return [
4269
- {
4270
- level: "warn",
4271
- code: "history-uncached-on-claude",
4272
- message: `${ir.history.length} history messages on Anthropic with no historyCachePolicy. Every turn re-pays for the full conversation context; with caching, subsequent turns hit the cache at ~10% the input cost.`,
4273
- suggestion: "Set `historyCachePolicy: { strategy: 'all-but-latest' }` on this IR. The lowering pass marks the message immediately preceding currentTurn with cache_control; subsequent turns whose history prefix matches byte-for-byte hit the cache.",
4274
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4275
- }
4276
- ];
4393
+ const now = Date.now();
4394
+ const stale = snap.expiresAt <= now;
4395
+ if (stale && !snap.refreshing) {
4396
+ snap.refreshing = true;
4397
+ void asyncRefresh6(rt, appId);
4398
+ }
4399
+ if (opts.archetype) {
4400
+ return snap.data.filter((f) => f.archetype === opts.archetype);
4401
+ }
4402
+ return snap.data;
4277
4403
  }
4278
- function detectSingleModelArray(ir, policy) {
4279
- if (ir.models.length !== 1) return [];
4280
- if (policy?.posture === "locked") return [];
4281
- const only = ir.models[0];
4282
- return [
4283
- {
4284
- level: "warn",
4285
- code: "single-model-array",
4286
- message: `\`ir.models\` has length 1 (only "${only}") and posture is not 'locked'. A single-model chain has no safety net \u2014 the first 429 / 5xx / cliff hits the user as a failure. Master plan \xA71.2 closes the reliability gap with a 2-step minimum.`,
4287
- suggestion: "Use `getDefaultFallbackChain({ archetype: ir.intent.archetype, primary: '" + only + "', posture: 'preferred' })` for a user-anchored chain, or `getDefaultFallbackChain({ archetype, posture: 'open' })` for library-picked. If single-model is intentional (compliance/brand promise), set `policy.posture = 'locked'` to silence this rule.",
4288
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#single-model-array"
4404
+ async function asyncRefresh6(rt, appId) {
4405
+ const promise = doRefresh6(rt, appId);
4406
+ pendingRefreshes5.set(appId, promise);
4407
+ try {
4408
+ await promise;
4409
+ } finally {
4410
+ if (pendingRefreshes5.get(appId) === promise) {
4411
+ pendingRefreshes5.delete(appId);
4289
4412
  }
4290
- ];
4413
+ }
4291
4414
  }
4292
- function detectCostMismatchedArchetype(ir, profile, phase2) {
4293
- if (!phase2 || phase2.fallbackChain.length === 0) return [];
4294
- if (!phase2.profileResolver) return [];
4295
- const archetype = ir.intent.archetype;
4296
- const chosenScore = getArchetypePerfScore(profile.id, archetype);
4297
- const chosenHasRoomToGrow = chosenScore.grounding === "judgment" || chosenScore.score < COST_MISMATCHED_CHOSEN_SCORE_CEILING;
4298
- if (!chosenHasRoomToGrow) return [];
4299
- let bestAlt = null;
4300
- for (const altId of phase2.fallbackChain) {
4301
- const altProfile = phase2.profileResolver(altId);
4302
- if (!altProfile) continue;
4303
- if (altProfile.id === profile.id) continue;
4304
- const altScore = getArchetypePerfScore(altProfile.id, archetype);
4305
- if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
4306
- if (altScore.score < chosenScore.score) continue;
4307
- if (altProfile.costInputPer1m >= profile.costInputPer1m) continue;
4308
- if (!bestAlt || altScore.score > bestAlt.score.score || altScore.score === bestAlt.score.score && altProfile.costInputPer1m < bestAlt.profile.costInputPer1m) {
4309
- bestAlt = { id: altId, profile: altProfile, score: altScore };
4310
- }
4415
+ async function doRefresh6(rt, appId) {
4416
+ const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
4417
+ let snap = snapshots5.get(appId);
4418
+ if (!snap) {
4419
+ snap = { data: [], expiresAt: 0, refreshing: false };
4420
+ snapshots5.set(appId, snap);
4311
4421
  }
4312
- if (!bestAlt) return [];
4313
- const tierDownWouldFire = bestAlt.score.grounding === "measured" && bestAlt.profile.costInputPer1m <= profile.costInputPer1m * TIER_DOWN_COST_RATIO;
4314
- if (tierDownWouldFire) return [];
4315
- const chosenGrounding = chosenScore.grounding === "judgment" ? `archetypePerf.${archetype}=judgment` : `archetypePerf.${archetype}=${chosenScore.score}`;
4316
- const altGrounding = bestAlt.score.grounding === "measured" ? `archetypePerf.${archetype}=${bestAlt.score.score}, measured, n=${bestAlt.score.n}` : `archetypePerf.${archetype}=${bestAlt.score.score}, judgment`;
4317
- return [
4318
- {
4319
- level: "warn",
4320
- code: "cost-mismatched-archetype",
4321
- message: `Cost-mismatched-archetype: target=${profile.id} (${chosenGrounding}) selected for ${archetype}. Alternative ${bestAlt.id} (${altGrounding}) is cheaper ($${bestAlt.profile.costInputPer1m}/$${bestAlt.profile.costOutputPer1m} vs $${profile.costInputPer1m}/$${profile.costOutputPer1m} per 1M) at equal-or-better quality.`,
4322
- suggestion: `Consider declaring \`${bestAlt.id}\` as the primary model for this archetype, or relax to posture='open' to let kgauto select among the chain. If the chosen model is required for compliance/brand reasons, set \`policy.posture = 'locked'\` to silence this rule.`,
4323
- recommendationType: profile.provider === bestAlt.profile.provider ? "tier-down" : "model-swap",
4324
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4422
+ try {
4423
+ const res = await rt.fetchImpl(url, { method: "GET" });
4424
+ if (!res.ok) {
4425
+ throw new Error(`stale-model findings ${res.status}: ${res.statusText}`);
4325
4426
  }
4326
- ];
4327
- }
4328
- function detectModelStaleEvidence(ir, profile) {
4329
- if (!isBrainQueryActiveFor("kgauto_archetype_perf")) return [];
4330
- const archetype = ir.intent.archetype;
4331
- const chosen = getArchetypePerfScore(profile.id, archetype);
4332
- if (chosen.grounding !== "judgment") return [];
4333
- return [
4334
- {
4335
- level: "info",
4336
- code: "model-stale-evidence",
4337
- message: `Model-stale-evidence: target=${profile.id} archetype=${archetype} is judgment-grounded (n=${chosen.n}) despite brain-query mode being active. Measurement substrate is wired but the brain hasn't accumulated >=10 outcomes for this (model, archetype) tuple yet \u2014 routing decisions remain pre-measured for this slot.`,
4338
- suggestion: "Verify that `record()` is being called on every call() outcome with the appropriate `actualModel` and `mutationsApplied` fields. Once the brain accumulates n>=10 rows on this tuple, the score promotes from judgment to measured automatically (5-min SWR cache). No code change required from your side \u2014 this is the substrate signaling the gap.",
4339
- recommendationType: "prompt-fix",
4340
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4427
+ const body = await res.json();
4428
+ if (runtime6 !== rt) return;
4429
+ const rows = Array.isArray(body) ? mapRowsToFindings3(body) : [];
4430
+ snap.data = rows;
4431
+ snap.expiresAt = Date.now() + rt.ttlMs;
4432
+ snap.refreshing = false;
4433
+ } catch (err) {
4434
+ if (runtime6 !== rt) return;
4435
+ snap.refreshing = false;
4436
+ snap.expiresAt = Date.now() + rt.ttlMs;
4437
+ if (!warnedOnce5) {
4438
+ warnedOnce5 = true;
4439
+ (rt.onError ?? defaultOnError6)(err);
4341
4440
  }
4342
- ];
4441
+ }
4343
4442
  }
4344
- function detectTierDown(ir, profile, phase2) {
4345
- if (!phase2 || phase2.fallbackChain.length === 0) return [];
4346
- if (!phase2.profileResolver) return [];
4347
- const archetype = ir.intent.archetype;
4348
- const chosenScore = getArchetypePerfScore(profile.id, archetype);
4349
- const chosenCost = profile.costInputPer1m;
4350
- let bestAlt = null;
4351
- for (const altId of phase2.fallbackChain) {
4352
- const altProfile = phase2.profileResolver(altId);
4353
- if (!altProfile) continue;
4354
- if (altProfile.id === profile.id) continue;
4355
- const altScore = getArchetypePerfScore(altProfile.id, archetype);
4356
- if (altScore.grounding !== "measured") continue;
4357
- if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
4358
- if (altScore.score < chosenScore.score) continue;
4359
- if (altProfile.costInputPer1m > chosenCost * TIER_DOWN_COST_RATIO) continue;
4360
- if (!bestAlt || altProfile.costInputPer1m < bestAlt.profile.costInputPer1m || altProfile.costInputPer1m === bestAlt.profile.costInputPer1m && altScore.score > bestAlt.score.score) {
4361
- bestAlt = { id: altId, profile: altProfile, score: altScore };
4443
+ function defaultOnError6(err) {
4444
+ console.warn(
4445
+ "[kgauto] stale-model findings fetch failed (using empty fallback):",
4446
+ err
4447
+ );
4448
+ }
4449
+ var CONSUMER_ON_STALE_MODEL_RULE_CODE = "consumer-on-stale-model";
4450
+ function advisorRuleConsumerOnStaleModel(ir) {
4451
+ if (!isStaleModelFindingsBrainActive()) return [];
4452
+ if (!ir.appId) return [];
4453
+ const findings = getStaleModelFindings({
4454
+ appId: ir.appId,
4455
+ archetype: ir.intent.archetype
4456
+ });
4457
+ if (findings.length === 0) return [];
4458
+ const ranked = [...findings].sort((a, b) => {
4459
+ if (a.staleStatus !== b.staleStatus) {
4460
+ return a.staleStatus === "deprecated" ? -1 : 1;
4362
4461
  }
4363
- }
4364
- if (!bestAlt) return [];
4365
- const chosenDesc = chosenScore.grounding === "measured" ? `archetypePerf.${archetype}=${chosenScore.score} (measured, n=${chosenScore.n})` : `archetypePerf.${archetype}=${chosenScore.score} (${chosenScore.grounding})`;
4462
+ return a.staleModel.localeCompare(b.staleModel);
4463
+ });
4464
+ const top = ranked[0];
4465
+ const extraCount = findings.length - 1;
4466
+ const extraNote = extraCount > 0 ? ` (+ ${extraCount} more stale model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
4366
4467
  return [
4367
4468
  {
4368
4469
  level: "warn",
4369
- code: "tier-down",
4370
- message: `Tier-down: target=${profile.id} (${chosenDesc}) selected for ${archetype}. Brain shows ${bestAlt.id} delivers equal-or-better quality (archetypePerf.${archetype}=${bestAlt.score.score}, measured, n=${bestAlt.score.n}) at $${bestAlt.profile.costInputPer1m}/$${bestAlt.profile.costOutputPer1m} per 1M vs $${profile.costInputPer1m}/$${profile.costOutputPer1m} \u2014 a measured tier-down opportunity.`,
4371
- suggestion: `Move \`${bestAlt.id}\` to primary for this archetype. The brain has n=${bestAlt.score.n} measured outcomes backing the recommendation; this is data, not opinion. If posture='locked' is required (compliance/brand promise), set it explicitly to silence this rule.`,
4372
- recommendationType: "tier-down",
4470
+ code: CONSUMER_ON_STALE_MODEL_RULE_CODE,
4471
+ message: `${top.message}${extraNote}`,
4472
+ suggestion: top.suggestion ?? `Migrate ${top.staleModel} \u2192 ${top.recommendedModel} for archetype "${top.archetype}". The newer model is the current latest in the "${top.family}" family; the stale one is ${top.staleStatus}.`,
4473
+ recommendationType: "model-swap",
4373
4474
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4374
4475
  }
4375
4476
  ];
4376
4477
  }
4377
- function detectArchetypePerfFloorBreach(ir, profile) {
4378
- const compat = getModelCompatibility(profile.id, {
4379
- archetype: ir.intent.archetype,
4380
- toolOrchestration: ir.constraints?.toolOrchestration
4381
- });
4382
- if (compat.status === "compatible") return [];
4383
- if (compat.status === "requires-adapter") {
4384
- return [
4385
- {
4386
- level: "warn",
4387
- code: "archetype-perf-floor-breach",
4388
- message: `${profile.id} sits below the archetype floor for ${ir.intent.archetype} (score ${compat.archetypePerf}/10, floor ${6}). A known adapter would lift it: ${compat.adapter.parameter}=${compat.adapter.value}. ${compat.adapter.consequence}`,
4389
- suggestion: `Pass \`ir.constraints.${compat.adapter.parameter} = '${compat.adapter.value}'\` for this call, OR pick a model whose archetypePerf for ${ir.intent.archetype} already clears the floor (call \`getModelCompatibility(modelId, { archetype: '${ir.intent.archetype}' })\` to check). Estimated post-adapter score: ${compat.archetypePerfWithAdapter}/10.`,
4390
- recommendationType: "prompt-fix",
4391
- suggestedAdaptation: compat.adapter,
4392
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4393
- }
4394
- ];
4478
+
4479
+ // src/archetype-fits.ts
4480
+ var ARCHETYPE_FAMILY_FITS = Object.freeze([
4481
+ {
4482
+ archetype: "plan",
4483
+ betterFitFamily: "deepseek-reasoner",
4484
+ reason: "Plan archetype is reasoning-shaped (multi-step chains, hypothesis-and-check, sub-goal decomposition) \u2014 exactly where reasoner-family models excel. Sonnet/Opus produce plans but at higher cost; reasoners produce equivalent-or-better plans at 7-17x lower cost at current promo pricing (deepseek-v4-pro $0.435/$0.87 per 1M promo through 2026-05-31 vs sonnet $3/$15).",
4485
+ costGuidance: "substantially cheaper at current pricing (deepseek-v4-pro promo: ~7-17x cheaper than sonnet)"
4486
+ },
4487
+ {
4488
+ archetype: "critique",
4489
+ betterFitFamily: "deepseek-reasoner",
4490
+ reason: "Critique archetype rewards epistemic humility and explicit reasoning \u2014 reasoner-family default behavior. Sonnet/Opus over-confident on critique tasks; reasoners surface uncertainty productively.",
4491
+ costGuidance: "comparable or cheaper at current pricing"
4492
+ }
4493
+ ]);
4494
+ function findBetterFit(archetype, currentFamily) {
4495
+ for (const fit of ARCHETYPE_FAMILY_FITS) {
4496
+ if (fit.archetype !== archetype) continue;
4497
+ if (fit.betterFitFamily === currentFamily) return null;
4498
+ return fit;
4395
4499
  }
4500
+ return null;
4501
+ }
4502
+
4503
+ // src/advisor-rules/cross-family-fit.ts
4504
+ function familyHasCurrentActiveModel(family) {
4505
+ for (const profile of allProfiles()) {
4506
+ const profileFamily = profile.family ?? deriveFamilyFromModelId(profile.id);
4507
+ if (profileFamily !== family) continue;
4508
+ if (profile.status !== "current") continue;
4509
+ if (profile.active === false) continue;
4510
+ return true;
4511
+ }
4512
+ return false;
4513
+ }
4514
+ function listCandidatesInFamily(family) {
4515
+ const candidates = [];
4516
+ for (const profile of allProfiles()) {
4517
+ const profileFamily = profile.family ?? deriveFamilyFromModelId(profile.id);
4518
+ if (profileFamily !== family) continue;
4519
+ if (profile.status !== "current") continue;
4520
+ if (profile.active === false) continue;
4521
+ candidates.push(profile.id);
4522
+ if (candidates.length >= 3) break;
4523
+ }
4524
+ return candidates;
4525
+ }
4526
+ function advisorRuleCrossFamilyFit(ctx) {
4527
+ if (!ctx.resolvedPrimary) return [];
4528
+ const currentFamily = deriveFamilyFromModelId(ctx.resolvedPrimary);
4529
+ if (!currentFamily) return [];
4530
+ const fit = findBetterFit(ctx.archetype, currentFamily);
4531
+ if (!fit) return [];
4532
+ if (!familyHasCurrentActiveModel(fit.betterFitFamily)) return [];
4533
+ const candidates = listCandidatesInFamily(fit.betterFitFamily);
4534
+ if (candidates.length === 0) return [];
4535
+ const candidateStr = candidates.join(", ");
4536
+ const message = `Your ${currentFamily} call on ${ctx.archetype} could shift to ${fit.betterFitFamily} \u2014 typically better quality + ${fit.costGuidance}. Suggested candidates: ${candidateStr}.`;
4396
4537
  return [
4397
4538
  {
4398
- level: "critical",
4399
- code: "archetype-perf-floor-breach",
4400
- message: `${profile.id} sits below the archetype floor for ${ir.intent.archetype} (score ${compat.archetypePerf}/10, floor ${6}) and no known adapter would lift it. ${compat.reason}`,
4401
- suggestion: `Swap to a model whose archetypePerf for ${ir.intent.archetype} clears the floor. Use \`getModelCompatibility(candidateId, { archetype: '${ir.intent.archetype}' })\` to vet candidates, or \`getDefaultFallbackChain({ archetype: '${ir.intent.archetype}', posture: 'open' })\` for a library-picked chain that respects the floor by construction.`,
4539
+ level: "info",
4540
+ code: "cross-family-fit-candidate",
4541
+ ownership: "consumer-actionable",
4542
+ message,
4543
+ suggestion: `Swap the model literal in \`ir.models\` to one of: ${candidateStr}. Or call \`getRecommendedPrimary({ family: '${fit.betterFitFamily}', archetype: '${ctx.archetype}', fallback: { id: '${candidates[0]}', reason: 'cross-family-fit-recommendation' } })\` to let kgauto resolve to the current+active family member.`,
4402
4544
  recommendationType: "model-swap",
4403
4545
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4404
4546
  }
4405
4547
  ];
4406
4548
  }
4407
- function detectStaleExclusionCandidate(ir) {
4408
- if (!isExclusionFindingsBrainActive()) return [];
4409
- if (!ir.appId) return [];
4410
- const findings = getStaleExclusionFindings({
4411
- appId: ir.appId,
4412
- archetype: ir.intent.archetype
4413
- });
4414
- if (findings.length === 0) return [];
4415
- const ranked = [...findings].sort((a, b) => {
4416
- const sa = a.estimatedSavingsUsd30d ?? -Infinity;
4417
- const sb = b.estimatedSavingsUsd30d ?? -Infinity;
4418
- if (sa !== sb) return sb - sa;
4419
- return confidenceRank(b.confidence) - confidenceRank(a.confidence);
4420
- });
4421
- const top = ranked[0];
4422
- const extraCount = findings.length - 1;
4423
- const extraNote = extraCount > 0 ? ` (+ ${extraCount} more excluded model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
4549
+
4550
+ // src/advisor.ts
4551
+ var QUALITY_FLOOR_FOR_RECOMMENDATION = 6;
4552
+ var TIER_DOWN_COST_RATIO = 0.5;
4553
+ var COST_MISMATCHED_CHOSEN_SCORE_CEILING = 7;
4554
+ var PRODUCER_OWNED_RULE_CODES = Object.freeze(
4555
+ /* @__PURE__ */ new Set(["model-stale-evidence", "promote-ready"])
4556
+ );
4557
+ function deriveOwnership(code, selfDeclared) {
4558
+ if (selfDeclared) return selfDeclared;
4559
+ return PRODUCER_OWNED_RULE_CODES.has(code) ? "producer-owned" : "consumer-actionable";
4560
+ }
4561
+ function runAdvisor(ir, result, profile, policy, phase2) {
4562
+ const out = [];
4563
+ out.push(...detectCachingOff(ir, profile));
4564
+ out.push(...detectSingleChunkSystem(ir, profile));
4565
+ out.push(...detectToolBloat(ir, result));
4566
+ out.push(...detectHistoryUncached(ir, profile));
4567
+ out.push(...detectSingleModelArray(ir, policy));
4568
+ if (policy?.posture !== "locked") {
4569
+ out.push(...detectCostMismatchedArchetype(ir, profile, phase2));
4570
+ out.push(...detectModelStaleEvidence(ir, profile));
4571
+ out.push(...detectTierDown(ir, profile, phase2));
4572
+ }
4573
+ if (!translatorClearedToolCallCliff(phase2)) {
4574
+ out.push(...detectArchetypePerfFloorBreach(ir, profile));
4575
+ }
4576
+ if (policy?.posture !== "locked") {
4577
+ out.push(...detectStaleExclusionCandidate(ir));
4578
+ }
4579
+ if (policy?.posture !== "locked" && ir.appId) {
4580
+ out.push(
4581
+ ...advisorRulePromoteReady({
4582
+ appId: ir.appId,
4583
+ archetype: ir.intent.archetype,
4584
+ resolvedPrimary: profile.id
4585
+ })
4586
+ );
4587
+ out.push(...advisorRuleConsumerOnStaleModel(ir));
4588
+ }
4589
+ if (policy?.posture !== "locked") {
4590
+ out.push(
4591
+ ...advisorRuleCrossFamilyFit({
4592
+ archetype: ir.intent.archetype,
4593
+ resolvedPrimary: profile.id
4594
+ })
4595
+ );
4596
+ }
4597
+ return out;
4598
+ }
4599
+ function translatorClearedToolCallCliff(phase2) {
4600
+ const rewrites = phase2?.sectionRewritesApplied;
4601
+ if (!rewrites || rewrites.length === 0) return false;
4602
+ for (const rw of rewrites) {
4603
+ if (rw.kind === "tool_call_contract") return true;
4604
+ }
4605
+ return false;
4606
+ }
4607
+ function detectCachingOff(ir, profile) {
4608
+ if (profile.provider !== "anthropic") return [];
4609
+ const totalChars = ir.sections.reduce((s, sec) => s + sec.text.length, 0);
4610
+ if (totalChars < 2e3) return [];
4611
+ const anyCacheable = ir.sections.some((s) => s.cacheable === true);
4612
+ if (anyCacheable) return [];
4424
4613
  return [
4425
4614
  {
4426
- level: "info",
4427
- code: "stale-exclusion-candidate",
4428
- message: `${top.message}${extraNote}`,
4429
- suggestion: top.suggestion,
4430
- recommendationType: "tier-down",
4615
+ level: "warn",
4616
+ code: "caching-off-on-claude",
4617
+ message: `System prompt is ${totalChars} chars on Anthropic but no PromptSection has cacheable=true. Anthropic prompt caching cuts cached-prefix input cost by ~90% on subsequent calls; without it, every turn re-pays full price for the static system context.`,
4618
+ suggestion: "Mark stable system sections (role, persona, tool policy) with `cacheable: true`. The lowering pass concatenates cacheable sections into a single cache-controlled block before the dynamic ones.",
4431
4619
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4432
4620
  }
4433
4621
  ];
4434
4622
  }
4435
- function confidenceRank(c) {
4436
- if (c === "high") return 3;
4437
- if (c === "medium") return 2;
4438
- return 1;
4623
+ function detectSingleChunkSystem(ir, profile) {
4624
+ if (profile.provider !== "anthropic") return [];
4625
+ if (ir.sections.length !== 1) return [];
4626
+ const only = ir.sections[0];
4627
+ if (!only || only.text.length <= 1e3) return [];
4628
+ return [
4629
+ {
4630
+ level: "info",
4631
+ code: "single-chunk-system",
4632
+ message: `System prompt is a single ${only.text.length}-char chunk. Splitting into NamedChunks (static role/persona vs dynamic context) gives the lowering pass a finer cache-marker boundary \u2014 only the static portion needs to be byte-stable for the cache to hit.`,
4633
+ suggestion: "Refactor the system builder to return an array of `PromptSection` shaped { id, text, cacheable?: boolean }. Static chunks (role, persona, tool policy) get `cacheable: true`; dynamic ones (current context, today's date) don't. NOTE: the lowering pass HOISTS cacheable sections ahead of dynamic ones on the Anthropic wire (prefix caching requires it) \u2014 if your prompt has a protected ordering (e.g. a voice/persona block that must precede boilerplate), splitting will reorder the compiled output; declining this advisory is then correct. Also: a cacheable block under ~1024 tokens gets NO cache_control marker (provider minimum), so marking small sections is inert, not harmful.",
4634
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4635
+ }
4636
+ ];
4439
4637
  }
4440
-
4441
- // src/translator.ts
4442
- var TRANSLATOR_FLOOR = ARCHETYPE_FLOOR_DEFAULT;
4443
- var RULE_SEQUENTIAL_TOOL_CLIFF = "sequential-tool-cliff-below-floor";
4444
- var RULE_NARRATION_DRIFT_ANTHROPIC = "narration-drift-anthropic";
4445
- var RULE_NARRATION_THINKING_LEAK_DEEPSEEK = "narration-thinking-leak-deepseek";
4446
- var SEQUENTIAL_TOOL_PREAMBLE = "IMPORTANT: Use one tool call per response. Wait for the tool result before deciding the next tool. Do NOT batch tool calls in parallel.";
4447
- var NARRATION_DRIFT_ANTHROPIC_PREAMBLE = "Output ONLY the requested content. Do not narrate your thought process. Each line \u2264 12 words.";
4448
- var NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE = "Reasoning is internal. Output ONLY the requested content; do not emit <thinking> blocks or internal monologue as user-facing text.";
4449
- var RULE_DISCIPLINE_GATES_V1 = "discipline-gates-v1";
4450
- var DISCIPLINE_GATES_V1_WITH_TOOLS = `Work through these gates at every judgment point, explicitly:
4451
- 1. Evidence before reasoning: cite what you observed before concluding from it.
4452
- 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
4453
- 3. Expand, don't guess: resolve a compressed or referenced item by looking it up rather than inferring its contents.
4454
- 4. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.
4455
- 5. Label each claim: mark it observed, inferred, or assumed.
4456
- 6. A surfaced gap beats a guessed answer: flag what you cannot determine rather than fabricating past it.`;
4457
- var DISCIPLINE_GATES_V1_NO_TOOLS = `Work through these gates at every judgment point, explicitly:
4458
- 1. Evidence before reasoning: cite what you observed before concluding from it.
4459
- 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
4460
- 3. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.
4461
- 4. Label each claim: mark it observed, inferred, or assumed.
4462
- 5. A surfaced gap beats a guessed answer: flag what you cannot determine rather than fabricating past it.`;
4463
- var RULE_DISCIPLINE_GATES_V1_STRUCTURED = "discipline-gates-v1-structured";
4464
- var DISCIPLINE_GATES_V1_STRUCTURED_WITH_TOOLS = `Work through these gates at every judgment point, explicitly:
4465
- 1. Evidence before reasoning: cite what you observed before concluding from it.
4466
- 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
4467
- 3. Expand, don't guess: resolve a compressed or referenced item by looking it up rather than inferring its contents.
4468
- 4. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.`;
4469
- var DISCIPLINE_GATES_V1_STRUCTURED_NO_TOOLS = `Work through these gates at every judgment point, explicitly:
4470
- 1. Evidence before reasoning: cite what you observed before concluding from it.
4471
- 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
4472
- 3. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.`;
4473
- var DISCIPLINE_ELIGIBLE_ARCHETYPES = /* @__PURE__ */ new Set([
4474
- "hunt",
4475
- "summarize",
4476
- "plan",
4477
- "critique",
4478
- "judge"
4479
- ]);
4480
- function matchRule(kind, profile, archetype, ctx) {
4481
- if (kind === "discipline_contract") {
4482
- if (!DISCIPLINE_ELIGIBLE_ARCHETYPES.has(archetype)) return null;
4483
- if (ctx.outputMode !== "text") {
4484
- return {
4485
- id: RULE_DISCIPLINE_GATES_V1_STRUCTURED,
4486
- preamble: ctx.hasTools ? DISCIPLINE_GATES_V1_STRUCTURED_WITH_TOOLS : DISCIPLINE_GATES_V1_STRUCTURED_NO_TOOLS
4487
- };
4638
+ function detectToolBloat(ir, result) {
4639
+ const SHORT_OUTPUT = /* @__PURE__ */ new Set([
4640
+ "classify",
4641
+ "extract",
4642
+ "summarize",
4643
+ "transform",
4644
+ "critique"
4645
+ ]);
4646
+ if (!ir.tools || ir.tools.length === 0) return [];
4647
+ const toolsKept = result.diagnostics.toolsKept;
4648
+ if (toolsKept <= 10) return [];
4649
+ if (!SHORT_OUTPUT.has(ir.intent.archetype)) return [];
4650
+ return [
4651
+ {
4652
+ level: "warn",
4653
+ code: "tool-bloat",
4654
+ message: `${toolsKept} tools kept after the relevance pass for archetype="${ir.intent.archetype}" (consumer declared ${ir.tools.length}). This archetype is short-output and rarely needs more than 3 tools; each tool definition eats ~350 tokens of context budget.`,
4655
+ suggestion: "Tighten `relevanceByIntent: { [archetype]: 0..1 }` per ToolDefinition. Tools below `toolRelevanceThreshold` (default 0.2) get dropped. Without `relevanceByIntent`, every tool defaults to neutral (0.5) and stays.",
4656
+ docsUrl: "https://github.com/stue/kgauto/blob/main/v2/README.md#tools"
4488
4657
  }
4489
- return {
4490
- id: RULE_DISCIPLINE_GATES_V1,
4491
- preamble: ctx.hasTools ? DISCIPLINE_GATES_V1_WITH_TOOLS : DISCIPLINE_GATES_V1_NO_TOOLS
4492
- };
4493
- }
4494
- if (kind === "tool_call_contract") {
4495
- if (!profile.archetypePerf) return null;
4496
- const archetypeScore = profile.archetypePerf[archetype];
4497
- if (typeof archetypeScore !== "number" || archetypeScore >= TRANSLATOR_FLOOR) {
4498
- return null;
4499
- }
4500
- return {
4501
- id: RULE_SEQUENTIAL_TOOL_CLIFF,
4502
- preamble: SEQUENTIAL_TOOL_PREAMBLE,
4503
- wireOverrides: { parallelToolCalls: false }
4504
- };
4505
- }
4506
- if (kind === "narration_contract") {
4507
- if (profile.provider === "anthropic") {
4508
- return {
4509
- id: RULE_NARRATION_DRIFT_ANTHROPIC,
4510
- preamble: NARRATION_DRIFT_ANTHROPIC_PREAMBLE
4511
- };
4512
- }
4513
- if (profile.provider === "deepseek") {
4514
- return {
4515
- id: RULE_NARRATION_THINKING_LEAK_DEEPSEEK,
4516
- preamble: NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE
4517
- };
4518
- }
4519
- return null;
4520
- }
4521
- return null;
4522
- }
4523
- function applySectionRewrites(args) {
4524
- const { ir, profile, archetype } = args;
4525
- if (!Array.isArray(ir.sections) || ir.sections.length === 0) {
4526
- return { rewrittenIR: ir, rewrites: [] };
4527
- }
4528
- const outputMode = args.outputMode ?? resolveOutputMode({
4529
- declared: ir.constraints?.outputMode,
4530
- structuredOutput: ir.constraints?.structuredOutput,
4531
- toolCount: ir.tools?.length ?? 0
4532
- });
4533
- const hasTools = (ir.tools?.length ?? 0) > 0;
4534
- const ctx = { outputMode, hasTools };
4535
- const rewrites = [];
4536
- const newSections = ir.sections.map((section) => {
4537
- if (!section.kind || section.kind === "arbitrary") return section;
4538
- const rule = matchRule(section.kind, profile, archetype, ctx);
4539
- if (!rule) return section;
4540
- const originalText = section.text;
4541
- const transformedText = `${rule.preamble}
4542
-
4543
- ${originalText}`;
4544
- rewrites.push({
4545
- sectionId: section.id,
4546
- kind: section.kind,
4547
- rule: rule.id,
4548
- originalText,
4549
- transformedText,
4550
- ...rule.wireOverrides ? { wireOverrides: rule.wireOverrides } : {}
4551
- });
4552
- return { ...section, text: transformedText };
4553
- });
4554
- if (rewrites.length === 0) {
4555
- return { rewrittenIR: ir, rewrites: [] };
4556
- }
4557
- const rewrittenIR = { ...ir, sections: newSections };
4558
- return { rewrittenIR, rewrites };
4559
- }
4560
-
4561
- // src/promotions-brain.ts
4562
- function isRawPromotionRow(x) {
4563
- if (!x || typeof x !== "object") return false;
4564
- const r = x;
4565
- return (typeof r.id === "number" || typeof r.id === "string") && typeof r.intent_archetype === "string" && typeof r.promoted_model === "string" && typeof r.incumbent_model === "string";
4566
- }
4567
- function coerceId(v) {
4568
- if (typeof v === "number") return Number.isFinite(v) ? v : null;
4569
- if (typeof v === "string") {
4570
- const n = Number(v);
4571
- return Number.isFinite(n) ? n : null;
4572
- }
4573
- return null;
4574
- }
4575
- function mapRowsToPromotions(rows) {
4576
- const out = [];
4577
- for (const row of rows) {
4578
- if (!isRawPromotionRow(row)) continue;
4579
- const id = coerceId(row.id);
4580
- if (id === null) continue;
4581
- const mode = row.mode === "strategy" ? "strategy" : row.mode === "downswap" || row.mode === void 0 ? "downswap" : null;
4582
- if (mode === null) continue;
4583
- out.push({
4584
- id,
4585
- archetype: row.intent_archetype,
4586
- mode,
4587
- strategy: typeof row.strategy === "string" ? row.strategy : null,
4588
- promotedModel: row.promoted_model,
4589
- incumbentModel: row.incumbent_model,
4590
- evalRunId: coerceId(row.eval_run_id ?? null),
4591
- suppressQualityGate: row.suppress_quality_gate === true,
4592
- promotedAt: typeof row.promoted_at === "string" ? row.promoted_at : ""
4593
- });
4594
- }
4595
- return out;
4596
- }
4597
- var snapshots4 = /* @__PURE__ */ new Map();
4598
- var runtime5;
4599
- var warnedOnce4 = false;
4600
- var DEFAULT_PROMOTIONS_ENDPOINT = "https://kgauto-dashboard.vercel.app/api/kgauto-v2/promotions";
4601
- function isAutoPromoteEnabledFromEnv(envSource) {
4602
- const env = envSource ?? (typeof process !== "undefined" && process.env ? process.env : {});
4603
- const raw = (env.KGAUTO_AUTO_PROMOTE ?? "").trim().toLowerCase();
4604
- return raw === "1" || raw === "true";
4605
- }
4606
- function configurePromotionsBrain(rt) {
4607
- runtime5 = rt;
4608
- snapshots4.clear();
4609
- warnedOnce4 = false;
4610
- }
4611
- function isPromotionsBrainActive() {
4612
- return runtime5 !== void 0;
4658
+ ];
4613
4659
  }
4614
- function getApplicablePromotion(opts) {
4615
- const rt = runtime5;
4616
- if (!rt) return void 0;
4617
- const appId = opts.appId;
4618
- if (!appId || !opts.archetype || !opts.mode) return void 0;
4619
- let snap = snapshots4.get(appId);
4620
- if (!snap) {
4621
- snap = { data: [], expiresAt: 0, refreshing: false };
4622
- snapshots4.set(appId, snap);
4623
- }
4624
- const now = Date.now();
4625
- const stale = snap.expiresAt <= now;
4626
- if (stale && !snap.refreshing) {
4627
- snap.refreshing = true;
4628
- void asyncRefresh5(rt, appId);
4660
+ function detectHistoryUncached(ir, profile) {
4661
+ if (profile.provider !== "anthropic") return [];
4662
+ if (!ir.history || ir.history.length < 2) return [];
4663
+ if (ir.historyCachePolicy && ir.historyCachePolicy.strategy !== "none") {
4664
+ return [];
4629
4665
  }
4630
- return snap.data.find(
4631
- (p) => p.archetype === opts.archetype && p.mode === opts.mode
4632
- );
4666
+ return [
4667
+ {
4668
+ level: "warn",
4669
+ code: "history-uncached-on-claude",
4670
+ message: `${ir.history.length} history messages on Anthropic with no historyCachePolicy. Every turn re-pays for the full conversation context; with caching, subsequent turns hit the cache at ~10% the input cost.`,
4671
+ suggestion: "Set `historyCachePolicy: { strategy: 'all-but-latest' }` on this IR. The lowering pass marks the message immediately preceding currentTurn with cache_control; subsequent turns whose history prefix matches byte-for-byte hit the cache.",
4672
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4673
+ }
4674
+ ];
4633
4675
  }
4634
- var pendingRefreshes4 = /* @__PURE__ */ new Map();
4635
- async function asyncRefresh5(rt, appId) {
4636
- const promise = doRefresh5(rt, appId);
4637
- pendingRefreshes4.set(appId, promise);
4638
- try {
4639
- await promise;
4640
- } finally {
4641
- if (pendingRefreshes4.get(appId) === promise) {
4642
- pendingRefreshes4.delete(appId);
4676
+ function detectSingleModelArray(ir, policy) {
4677
+ if (ir.models.length !== 1) return [];
4678
+ if (policy?.posture === "locked") return [];
4679
+ const only = ir.models[0];
4680
+ return [
4681
+ {
4682
+ level: "warn",
4683
+ code: "single-model-array",
4684
+ message: `\`ir.models\` has length 1 (only "${only}") and posture is not 'locked'. A single-model chain has no safety net \u2014 the first 429 / 5xx / cliff hits the user as a failure. Master plan \xA71.2 closes the reliability gap with a 2-step minimum.`,
4685
+ suggestion: "Use `getDefaultFallbackChain({ archetype: ir.intent.archetype, primary: '" + only + "', posture: 'preferred' })` for a user-anchored chain, or `getDefaultFallbackChain({ archetype, posture: 'open' })` for library-picked. If single-model is intentional (compliance/brand promise), set `policy.posture = 'locked'` to silence this rule.",
4686
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#single-model-array"
4643
4687
  }
4644
- }
4688
+ ];
4645
4689
  }
4646
- async function doRefresh5(rt, appId) {
4647
- const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
4648
- let snap = snapshots4.get(appId);
4649
- if (!snap) {
4650
- snap = { data: [], expiresAt: 0, refreshing: false };
4651
- snapshots4.set(appId, snap);
4652
- }
4653
- try {
4654
- const res = await rt.fetchImpl(url, { method: "GET" });
4655
- if (!res.ok) {
4656
- throw new Error(`promotions ${res.status}: ${res.statusText}`);
4690
+ function detectCostMismatchedArchetype(ir, profile, phase2) {
4691
+ if (!phase2 || phase2.fallbackChain.length === 0) return [];
4692
+ if (!phase2.profileResolver) return [];
4693
+ const archetype = ir.intent.archetype;
4694
+ const chosenScore = getArchetypePerfScore(profile.id, archetype);
4695
+ const chosenHasRoomToGrow = chosenScore.grounding === "judgment" || chosenScore.score < COST_MISMATCHED_CHOSEN_SCORE_CEILING;
4696
+ if (!chosenHasRoomToGrow) return [];
4697
+ let bestAlt = null;
4698
+ for (const altId of phase2.fallbackChain) {
4699
+ const altProfile = phase2.profileResolver(altId);
4700
+ if (!altProfile) continue;
4701
+ if (altProfile.id === profile.id) continue;
4702
+ const altScore = getArchetypePerfScore(altProfile.id, archetype);
4703
+ if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
4704
+ if (altScore.score < chosenScore.score) continue;
4705
+ if (altProfile.costInputPer1m >= profile.costInputPer1m) continue;
4706
+ if (getMeasuredFailureVerdict({
4707
+ appId: ir.appId,
4708
+ archetype,
4709
+ model: altProfile.id
4710
+ })?.gated === true) {
4711
+ continue;
4657
4712
  }
4658
- const body = await res.json();
4659
- if (runtime5 !== rt) return;
4660
- const rows = Array.isArray(body) ? mapRowsToPromotions(body) : [];
4661
- snap.data = rows;
4662
- snap.expiresAt = Date.now() + rt.ttlMs;
4663
- snap.refreshing = false;
4664
- } catch (err) {
4665
- if (runtime5 !== rt) return;
4666
- snap.refreshing = false;
4667
- snap.expiresAt = Date.now() + rt.ttlMs;
4668
- if (!warnedOnce4) {
4669
- warnedOnce4 = true;
4670
- (rt.onError ?? defaultOnError5)(err);
4713
+ if (ir.constraints?.structuredOutput && effectiveConventions(altProfile).some(
4714
+ (c) => c.archetype === archetype && c.structuredOutputHint === "avoid"
4715
+ )) {
4716
+ continue;
4717
+ }
4718
+ if (getRecentRollback({
4719
+ appId: ir.appId,
4720
+ archetype,
4721
+ model: altProfile.id
4722
+ }) !== void 0) {
4723
+ continue;
4724
+ }
4725
+ if (!bestAlt || altScore.score > bestAlt.score.score || altScore.score === bestAlt.score.score && altProfile.costInputPer1m < bestAlt.profile.costInputPer1m) {
4726
+ bestAlt = { id: altId, profile: altProfile, score: altScore };
4671
4727
  }
4672
4728
  }
4729
+ if (!bestAlt) return [];
4730
+ const tierDownWouldFire = bestAlt.score.grounding === "measured" && bestAlt.profile.costInputPer1m <= profile.costInputPer1m * TIER_DOWN_COST_RATIO;
4731
+ if (tierDownWouldFire) return [];
4732
+ const chosenGrounding = chosenScore.grounding === "judgment" ? `archetypePerf.${archetype}=judgment` : `archetypePerf.${archetype}=${chosenScore.score}`;
4733
+ const altGrounding = bestAlt.score.grounding === "measured" ? `archetypePerf.${archetype}=${bestAlt.score.score}, measured, n=${bestAlt.score.n}` : `archetypePerf.${archetype}=${bestAlt.score.score}, judgment`;
4734
+ return [
4735
+ {
4736
+ level: "warn",
4737
+ code: "cost-mismatched-archetype",
4738
+ message: `Cost-mismatched-archetype: target=${profile.id} (${chosenGrounding}) selected for ${archetype}. Alternative ${bestAlt.id} (${altGrounding}) is cheaper ($${bestAlt.profile.costInputPer1m}/$${bestAlt.profile.costOutputPer1m} vs $${profile.costInputPer1m}/$${profile.costOutputPer1m} per 1M) at equal-or-better quality.`,
4739
+ suggestion: `Consider declaring \`${bestAlt.id}\` as the primary model for this archetype, or relax to posture='open' to let kgauto select among the chain. If the chosen model is required for compliance/brand reasons, set \`policy.posture = 'locked'\` to silence this rule.`,
4740
+ recommendationType: profile.provider === bestAlt.profile.provider ? "tier-down" : "model-swap",
4741
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4742
+ }
4743
+ ];
4673
4744
  }
4674
- function defaultOnError5(err) {
4675
- console.warn(
4676
- "[kgauto] promotions fetch failed (promotion boost inactive until next refresh):",
4677
- err
4678
- );
4679
- }
4680
- function _testResetPromotions() {
4681
- runtime5 = void 0;
4682
- snapshots4.clear();
4683
- pendingRefreshes4 = /* @__PURE__ */ new Map();
4684
- warnedOnce4 = false;
4685
- }
4686
- async function _testWaitForPromotionsRefresh() {
4687
- const pending = Array.from(pendingRefreshes4.values());
4688
- if (pending.length > 0) await Promise.all(pending);
4689
- }
4690
-
4691
- // src/measured-failure-brain.ts
4692
- function coerceCount(v) {
4693
- if (typeof v === "number") return Number.isFinite(v) ? v : null;
4694
- if (typeof v === "string") {
4695
- const n = Number(v);
4696
- return Number.isFinite(n) ? n : null;
4697
- }
4698
- return null;
4699
- }
4700
- function isRawFailureRow(x) {
4701
- if (!x || typeof x !== "object") return false;
4702
- const r = x;
4703
- return typeof r.intent_archetype === "string" && typeof r.model === "string" && (typeof r.n === "number" || typeof r.n === "string");
4745
+ function detectModelStaleEvidence(ir, profile) {
4746
+ if (!isBrainQueryActiveFor("kgauto_archetype_perf")) return [];
4747
+ const archetype = ir.intent.archetype;
4748
+ const chosen = getArchetypePerfScore(profile.id, archetype);
4749
+ if (chosen.grounding !== "judgment") return [];
4750
+ return [
4751
+ {
4752
+ level: "info",
4753
+ code: "model-stale-evidence",
4754
+ message: `Model-stale-evidence: target=${profile.id} archetype=${archetype} is judgment-grounded (n=${chosen.n}, cross-app 90d window) despite brain-query mode being active. Fewer than 10 outcomes back this (model, archetype) tuple across ALL consumers \u2014 routing decisions remain pre-measured for this slot.`,
4755
+ suggestion: "Verify that `record()` is being called on every call() outcome. Counts are cross-app (migration 050 view): once ANY consumers accumulate n>=10 rows on this tuple, the score promotes from judgment to measured automatically within the 5-min SWR window. (Before alpha.78 this promotion was advertised but had no implementing mechanism \u2014 n was never populated; if this advisory has been firing for weeks at n=0 despite real traffic, bump to >=alpha.78 and it will clear on its own.)",
4756
+ recommendationType: "prompt-fix",
4757
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4758
+ }
4759
+ ];
4704
4760
  }
4705
- function mapRows(rows) {
4706
- const out = [];
4707
- for (const row of rows) {
4708
- if (!isRawFailureRow(row)) continue;
4709
- const n = coerceCount(row.n);
4710
- const nFail = coerceCount(row.n_fail) ?? 0;
4711
- if (n === null || n <= 0) continue;
4712
- out.push({
4713
- archetype: row.intent_archetype,
4714
- model: row.model,
4715
- n,
4716
- nFail
4717
- });
4761
+ function detectTierDown(ir, profile, phase2) {
4762
+ if (!phase2 || phase2.fallbackChain.length === 0) return [];
4763
+ if (!phase2.profileResolver) return [];
4764
+ const archetype = ir.intent.archetype;
4765
+ const chosenScore = getArchetypePerfScore(profile.id, archetype);
4766
+ const chosenCost = profile.costInputPer1m;
4767
+ let bestAlt = null;
4768
+ for (const altId of phase2.fallbackChain) {
4769
+ const altProfile = phase2.profileResolver(altId);
4770
+ if (!altProfile) continue;
4771
+ if (altProfile.id === profile.id) continue;
4772
+ const altScore = getArchetypePerfScore(altProfile.id, archetype);
4773
+ if (altScore.grounding !== "measured") continue;
4774
+ if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
4775
+ if (altScore.score < chosenScore.score) continue;
4776
+ if (altProfile.costInputPer1m > chosenCost * TIER_DOWN_COST_RATIO) continue;
4777
+ if (!bestAlt || altProfile.costInputPer1m < bestAlt.profile.costInputPer1m || altProfile.costInputPer1m === bestAlt.profile.costInputPer1m && altScore.score > bestAlt.score.score) {
4778
+ bestAlt = { id: altId, profile: altProfile, score: altScore };
4779
+ }
4718
4780
  }
4719
- return out;
4720
- }
4721
- var MEASURED_FAILURE_CFG = {
4722
- /**
4723
- * Hard minimum attempts before ANY gate may be created. Guards against
4724
- * pathological tiny samples that the confidence bound alone would let
4725
- * through in edge cases. At 5-for-5 the bound clears the threshold; at
4726
- * 3-for-3 it does not, which is the behaviour we want (three failures is
4727
- * a bad day, five in a row is a pattern).
4728
- */
4729
- minSample: 5,
4730
- /**
4731
- * Gate when we are 95% confident the model fails MORE OFTEN THAN IT
4732
- * SUCCEEDS on this surface. Deliberately unarguable rather than tuned —
4733
- * a model that probably fails the majority of the time has no business
4734
- * leading a surface, whatever its declared scores say.
4735
- */
4736
- lowerBoundThreshold: 0.5,
4737
- /** 95% one-sided-ish confidence (standard two-sided z at α=0.05). */
4738
- z: 1.96,
4739
- /** Must match the view's window. Documented here for the advisory text. */
4740
- windowDays: 28
4741
- };
4742
- function wilsonLowerBound(failures, n, z = MEASURED_FAILURE_CFG.z) {
4743
- if (n <= 0) return 0;
4744
- const p = failures / n;
4745
- const z2 = z * z;
4746
- const denom = 1 + z2 / n;
4747
- const centre = p + z2 / (2 * n);
4748
- const margin = z * Math.sqrt(p * (1 - p) / n + z2 / (4 * n * n));
4749
- const lower2 = (centre - margin) / denom;
4750
- return lower2 < 0 ? 0 : lower2;
4751
- }
4752
- function judgeMeasuredFailure(row, cfg = MEASURED_FAILURE_CFG) {
4753
- if (!row || row.n < cfg.minSample) return void 0;
4754
- const lowerBound = wilsonLowerBound(row.nFail, row.n, cfg.z);
4755
- return {
4756
- gated: lowerBound > cfg.lowerBoundThreshold,
4757
- rate: row.nFail / row.n,
4758
- lowerBound,
4759
- n: row.n,
4760
- nFail: row.nFail
4761
- };
4762
- }
4763
- var snapshots5 = /* @__PURE__ */ new Map();
4764
- var runtime6;
4765
- var warnedOnce5 = false;
4766
- var DEFAULT_MEASURED_FAILURE_ENDPOINT = "https://kgauto-dashboard.vercel.app/api/kgauto-v2/measured-failure";
4767
- function isMeasuredFailureGateEnabledFromEnv(envSource) {
4768
- const env = envSource ?? (typeof process !== "undefined" && process.env ? process.env : {});
4769
- const raw = (env.KGAUTO_MEASURED_FAILURE_GATE ?? "").trim().toLowerCase();
4770
- return !(raw === "0" || raw === "false");
4771
- }
4772
- function configureMeasuredFailureBrain(rt) {
4773
- runtime6 = rt;
4774
- snapshots5.clear();
4775
- warnedOnce5 = false;
4776
- }
4777
- function isMeasuredFailureBrainActive() {
4778
- return runtime6 !== void 0;
4781
+ if (!bestAlt) return [];
4782
+ const chosenDesc = chosenScore.grounding === "measured" ? `archetypePerf.${archetype}=${chosenScore.score} (measured, n=${chosenScore.n})` : `archetypePerf.${archetype}=${chosenScore.score} (${chosenScore.grounding})`;
4783
+ return [
4784
+ {
4785
+ level: "warn",
4786
+ code: "tier-down",
4787
+ message: `Tier-down: target=${profile.id} (${chosenDesc}) selected for ${archetype}. Brain shows ${bestAlt.id} delivers equal-or-better quality (archetypePerf.${archetype}=${bestAlt.score.score}, measured, n=${bestAlt.score.n}) at $${bestAlt.profile.costInputPer1m}/$${bestAlt.profile.costOutputPer1m} per 1M vs $${profile.costInputPer1m}/$${profile.costOutputPer1m} \u2014 a measured tier-down opportunity.`,
4788
+ suggestion: `Move \`${bestAlt.id}\` to primary for this archetype. The brain has n=${bestAlt.score.n} measured outcomes backing the recommendation; this is data, not opinion. If posture='locked' is required (compliance/brand promise), set it explicitly to silence this rule.`,
4789
+ recommendationType: "tier-down",
4790
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4791
+ }
4792
+ ];
4779
4793
  }
4780
- function prefetchMeasuredFailure(appId) {
4781
- const rt = runtime6;
4782
- if (!rt || !appId) return void 0;
4783
- let snap = snapshots5.get(appId);
4784
- if (!snap) {
4785
- snap = { data: [], expiresAt: 0, refreshing: false };
4786
- snapshots5.set(appId, snap);
4794
+ function detectArchetypePerfFloorBreach(ir, profile) {
4795
+ const compat = getModelCompatibility(profile.id, {
4796
+ archetype: ir.intent.archetype,
4797
+ toolOrchestration: ir.constraints?.toolOrchestration
4798
+ });
4799
+ if (compat.status === "compatible") return [];
4800
+ if (compat.status === "requires-adapter") {
4801
+ return [
4802
+ {
4803
+ level: "warn",
4804
+ code: "archetype-perf-floor-breach",
4805
+ message: `${profile.id} sits below the archetype floor for ${ir.intent.archetype} (score ${compat.archetypePerf}/10, floor ${6}). A known adapter would lift it: ${compat.adapter.parameter}=${compat.adapter.value}. ${compat.adapter.consequence}`,
4806
+ suggestion: `Pass \`ir.constraints.${compat.adapter.parameter} = '${compat.adapter.value}'\` for this call, OR pick a model whose archetypePerf for ${ir.intent.archetype} already clears the floor (call \`getModelCompatibility(modelId, { archetype: '${ir.intent.archetype}' })\` to check). Estimated post-adapter score: ${compat.archetypePerfWithAdapter}/10.`,
4807
+ recommendationType: "prompt-fix",
4808
+ suggestedAdaptation: compat.adapter,
4809
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4810
+ }
4811
+ ];
4787
4812
  }
4788
- if (snap.expiresAt > Date.now()) return void 0;
4789
- const inflight = pendingRefreshes5.get(appId);
4790
- if (inflight) return inflight;
4791
- if (snap.refreshing) return void 0;
4792
- snap.refreshing = true;
4793
- void asyncRefresh6(rt, appId);
4794
- return pendingRefreshes5.get(appId);
4813
+ return [
4814
+ {
4815
+ level: "critical",
4816
+ code: "archetype-perf-floor-breach",
4817
+ message: `${profile.id} sits below the archetype floor for ${ir.intent.archetype} (score ${compat.archetypePerf}/10, floor ${6}) and no known adapter would lift it. ${compat.reason}`,
4818
+ suggestion: `Swap to a model whose archetypePerf for ${ir.intent.archetype} clears the floor. Use \`getModelCompatibility(candidateId, { archetype: '${ir.intent.archetype}' })\` to vet candidates, or \`getDefaultFallbackChain({ archetype: '${ir.intent.archetype}', posture: 'open' })\` for a library-picked chain that respects the floor by construction.`,
4819
+ recommendationType: "model-swap",
4820
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4821
+ }
4822
+ ];
4795
4823
  }
4796
- async function awaitMeasuredFailureReady(appId, timeoutMs) {
4797
- if (!runtime6 || !appId) return;
4798
- const pending = prefetchMeasuredFailure(appId) ?? pendingRefreshes5.get(appId);
4799
- if (!(timeoutMs > 0)) return;
4800
- if (!pending) return;
4801
- let timer;
4802
- try {
4803
- await Promise.race([
4804
- pending,
4805
- new Promise((resolve) => {
4806
- timer = setTimeout(resolve, timeoutMs);
4807
- })
4808
- ]);
4809
- } catch {
4810
- } finally {
4811
- if (timer) clearTimeout(timer);
4812
- }
4824
+ function detectStaleExclusionCandidate(ir) {
4825
+ if (!isExclusionFindingsBrainActive()) return [];
4826
+ if (!ir.appId) return [];
4827
+ const findings = getStaleExclusionFindings({
4828
+ appId: ir.appId,
4829
+ archetype: ir.intent.archetype
4830
+ });
4831
+ if (findings.length === 0) return [];
4832
+ const ranked = [...findings].sort((a, b) => {
4833
+ const sa = a.estimatedSavingsUsd30d ?? -Infinity;
4834
+ const sb = b.estimatedSavingsUsd30d ?? -Infinity;
4835
+ if (sa !== sb) return sb - sa;
4836
+ return confidenceRank(b.confidence) - confidenceRank(a.confidence);
4837
+ });
4838
+ const top = ranked[0];
4839
+ const extraCount = findings.length - 1;
4840
+ const extraNote = extraCount > 0 ? ` (+ ${extraCount} more excluded model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
4841
+ return [
4842
+ {
4843
+ level: "info",
4844
+ code: "stale-exclusion-candidate",
4845
+ message: `${top.message}${extraNote}`,
4846
+ suggestion: top.suggestion,
4847
+ recommendationType: "tier-down",
4848
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
4849
+ }
4850
+ ];
4813
4851
  }
4814
- function getMeasuredFailureVerdict(opts) {
4815
- const rt = runtime6;
4816
- if (!rt) return void 0;
4817
- const { appId, archetype, model } = opts;
4818
- if (!appId || !archetype || !model) return void 0;
4819
- let snap = snapshots5.get(appId);
4820
- if (!snap) {
4821
- snap = { data: [], expiresAt: 0, refreshing: false };
4822
- snapshots5.set(appId, snap);
4823
- }
4824
- const now = Date.now();
4825
- if (snap.expiresAt <= now && !snap.refreshing) {
4826
- snap.refreshing = true;
4827
- void asyncRefresh6(rt, appId);
4828
- }
4829
- const row = snap.data.find(
4830
- (r) => r.archetype === archetype && r.model === model
4831
- );
4832
- return judgeMeasuredFailure(row);
4852
+ function confidenceRank(c) {
4853
+ if (c === "high") return 3;
4854
+ if (c === "medium") return 2;
4855
+ return 1;
4833
4856
  }
4834
- var pendingRefreshes5 = /* @__PURE__ */ new Map();
4835
- async function asyncRefresh6(rt, appId) {
4836
- const promise = doRefresh6(rt, appId);
4837
- pendingRefreshes5.set(appId, promise);
4838
- try {
4839
- await promise;
4840
- } finally {
4841
- if (pendingRefreshes5.get(appId) === promise) {
4842
- pendingRefreshes5.delete(appId);
4857
+
4858
+ // src/translator.ts
4859
+ var TRANSLATOR_FLOOR = ARCHETYPE_FLOOR_DEFAULT;
4860
+ var RULE_SEQUENTIAL_TOOL_CLIFF = "sequential-tool-cliff-below-floor";
4861
+ var RULE_NARRATION_DRIFT_ANTHROPIC = "narration-drift-anthropic";
4862
+ var RULE_NARRATION_THINKING_LEAK_DEEPSEEK = "narration-thinking-leak-deepseek";
4863
+ var SEQUENTIAL_TOOL_PREAMBLE = "IMPORTANT: Use one tool call per response. Wait for the tool result before deciding the next tool. Do NOT batch tool calls in parallel.";
4864
+ var NARRATION_DRIFT_ANTHROPIC_PREAMBLE = "Output ONLY the requested content. Do not narrate your thought process. Each line \u2264 12 words.";
4865
+ var NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE = "Reasoning is internal. Output ONLY the requested content; do not emit <thinking> blocks or internal monologue as user-facing text.";
4866
+ var RULE_DISCIPLINE_GATES_V1 = "discipline-gates-v1";
4867
+ var DISCIPLINE_GATES_V1_WITH_TOOLS = `Work through these gates at every judgment point, explicitly:
4868
+ 1. Evidence before reasoning: cite what you observed before concluding from it.
4869
+ 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
4870
+ 3. Expand, don't guess: resolve a compressed or referenced item by looking it up rather than inferring its contents.
4871
+ 4. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.
4872
+ 5. Label each claim: mark it observed, inferred, or assumed.
4873
+ 6. A surfaced gap beats a guessed answer: flag what you cannot determine rather than fabricating past it.`;
4874
+ var DISCIPLINE_GATES_V1_NO_TOOLS = `Work through these gates at every judgment point, explicitly:
4875
+ 1. Evidence before reasoning: cite what you observed before concluding from it.
4876
+ 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
4877
+ 3. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.
4878
+ 4. Label each claim: mark it observed, inferred, or assumed.
4879
+ 5. A surfaced gap beats a guessed answer: flag what you cannot determine rather than fabricating past it.`;
4880
+ var RULE_DISCIPLINE_GATES_V1_STRUCTURED = "discipline-gates-v1-structured";
4881
+ var DISCIPLINE_GATES_V1_STRUCTURED_WITH_TOOLS = `Work through these gates at every judgment point, explicitly:
4882
+ 1. Evidence before reasoning: cite what you observed before concluding from it.
4883
+ 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
4884
+ 3. Expand, don't guess: resolve a compressed or referenced item by looking it up rather than inferring its contents.
4885
+ 4. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.`;
4886
+ var DISCIPLINE_GATES_V1_STRUCTURED_NO_TOOLS = `Work through these gates at every judgment point, explicitly:
4887
+ 1. Evidence before reasoning: cite what you observed before concluding from it.
4888
+ 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
4889
+ 3. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.`;
4890
+ var DISCIPLINE_ELIGIBLE_ARCHETYPES = /* @__PURE__ */ new Set([
4891
+ "hunt",
4892
+ "summarize",
4893
+ "plan",
4894
+ "critique",
4895
+ "judge"
4896
+ ]);
4897
+ function matchRule(kind, profile, archetype, ctx) {
4898
+ if (kind === "discipline_contract") {
4899
+ if (!DISCIPLINE_ELIGIBLE_ARCHETYPES.has(archetype)) return null;
4900
+ if (ctx.outputMode !== "text") {
4901
+ return {
4902
+ id: RULE_DISCIPLINE_GATES_V1_STRUCTURED,
4903
+ preamble: ctx.hasTools ? DISCIPLINE_GATES_V1_STRUCTURED_WITH_TOOLS : DISCIPLINE_GATES_V1_STRUCTURED_NO_TOOLS
4904
+ };
4843
4905
  }
4906
+ return {
4907
+ id: RULE_DISCIPLINE_GATES_V1,
4908
+ preamble: ctx.hasTools ? DISCIPLINE_GATES_V1_WITH_TOOLS : DISCIPLINE_GATES_V1_NO_TOOLS
4909
+ };
4844
4910
  }
4845
- }
4846
- async function doRefresh6(rt, appId) {
4847
- const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
4848
- let snap = snapshots5.get(appId);
4849
- if (!snap) {
4850
- snap = { data: [], expiresAt: 0, refreshing: false };
4851
- snapshots5.set(appId, snap);
4911
+ if (kind === "tool_call_contract") {
4912
+ if (!profile.archetypePerf) return null;
4913
+ const archetypeScore = profile.archetypePerf[archetype];
4914
+ if (typeof archetypeScore !== "number" || archetypeScore >= TRANSLATOR_FLOOR) {
4915
+ return null;
4916
+ }
4917
+ return {
4918
+ id: RULE_SEQUENTIAL_TOOL_CLIFF,
4919
+ preamble: SEQUENTIAL_TOOL_PREAMBLE,
4920
+ wireOverrides: { parallelToolCalls: false }
4921
+ };
4852
4922
  }
4853
- try {
4854
- const res = await rt.fetchImpl(url, { method: "GET" });
4855
- if (!res.ok) {
4856
- throw new Error(`measured-failure ${res.status}: ${res.statusText}`);
4923
+ if (kind === "narration_contract") {
4924
+ if (profile.provider === "anthropic") {
4925
+ return {
4926
+ id: RULE_NARRATION_DRIFT_ANTHROPIC,
4927
+ preamble: NARRATION_DRIFT_ANTHROPIC_PREAMBLE
4928
+ };
4857
4929
  }
4858
- const body = await res.json();
4859
- if (runtime6 !== rt) return;
4860
- snap.data = Array.isArray(body) ? mapRows(body) : [];
4861
- snap.expiresAt = Date.now() + rt.ttlMs;
4862
- snap.refreshing = false;
4863
- } catch (err) {
4864
- if (runtime6 !== rt) return;
4865
- snap.refreshing = false;
4866
- snap.expiresAt = Date.now() + rt.ttlMs;
4867
- if (!warnedOnce5) {
4868
- warnedOnce5 = true;
4869
- (rt.onError ?? defaultOnError6)(err);
4930
+ if (profile.provider === "deepseek") {
4931
+ return {
4932
+ id: RULE_NARRATION_THINKING_LEAK_DEEPSEEK,
4933
+ preamble: NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE
4934
+ };
4870
4935
  }
4936
+ return null;
4871
4937
  }
4938
+ return null;
4872
4939
  }
4873
- function defaultOnError6(err) {
4874
- console.warn(
4875
- "[kgauto] measured-failure fetch failed (gate inactive until next refresh):",
4876
- err
4877
- );
4878
- }
4879
- function _testResetMeasuredFailure() {
4880
- runtime6 = void 0;
4881
- snapshots5.clear();
4882
- pendingRefreshes5 = /* @__PURE__ */ new Map();
4883
- warnedOnce5 = false;
4884
- }
4885
- async function _testWaitForMeasuredFailureRefresh() {
4886
- const pending = Array.from(pendingRefreshes5.values());
4887
- if (pending.length > 0) await Promise.all(pending);
4940
+ function applySectionRewrites(args) {
4941
+ const { ir, profile, archetype } = args;
4942
+ if (!Array.isArray(ir.sections) || ir.sections.length === 0) {
4943
+ return { rewrittenIR: ir, rewrites: [] };
4944
+ }
4945
+ const outputMode = args.outputMode ?? resolveOutputMode({
4946
+ declared: ir.constraints?.outputMode,
4947
+ structuredOutput: ir.constraints?.structuredOutput,
4948
+ toolCount: ir.tools?.length ?? 0
4949
+ });
4950
+ const hasTools = (ir.tools?.length ?? 0) > 0;
4951
+ const ctx = { outputMode, hasTools };
4952
+ const rewrites = [];
4953
+ const newSections = ir.sections.map((section) => {
4954
+ if (!section.kind || section.kind === "arbitrary") return section;
4955
+ const rule = matchRule(section.kind, profile, archetype, ctx);
4956
+ if (!rule) return section;
4957
+ const originalText = section.text;
4958
+ const transformedText = `${rule.preamble}
4959
+
4960
+ ${originalText}`;
4961
+ rewrites.push({
4962
+ sectionId: section.id,
4963
+ kind: section.kind,
4964
+ rule: rule.id,
4965
+ originalText,
4966
+ transformedText,
4967
+ ...rule.wireOverrides ? { wireOverrides: rule.wireOverrides } : {}
4968
+ });
4969
+ return { ...section, text: transformedText };
4970
+ });
4971
+ if (rewrites.length === 0) {
4972
+ return { rewrittenIR: ir, rewrites: [] };
4973
+ }
4974
+ const rewrittenIR = { ...ir, sections: newSections };
4975
+ return { rewrittenIR, rewrites };
4888
4976
  }
4889
4977
 
4890
4978
  // src/compile.ts
@@ -6014,6 +6102,17 @@ function parseJsonLoose(raw) {
6014
6102
  var FAILED = /* @__PURE__ */ Symbol("parse-failed");
6015
6103
 
6016
6104
  // src/ir.ts
6105
+ function mutationId(m) {
6106
+ return typeof m === "string" ? m : m.id;
6107
+ }
6108
+ function hasMutation(list, idOrPrefix) {
6109
+ if (!Array.isArray(list)) return false;
6110
+ const prefix = idOrPrefix.endsWith("*") ? idOrPrefix.slice(0, -1) : void 0;
6111
+ return list.some((m) => {
6112
+ const id = mutationId(m);
6113
+ return prefix !== void 0 ? id.startsWith(prefix) : id === idOrPrefix;
6114
+ });
6115
+ }
6017
6116
  var CallError = class extends Error {
6018
6117
  attempts;
6019
6118
  lastErrorCode;
@@ -6136,6 +6235,17 @@ function getReachabilityDiagnostic(opts = {}) {
6136
6235
  }
6137
6236
  return out;
6138
6237
  }
6238
+ var BRAIN_READ_ENV_NAMES = {
6239
+ endpoint: "KGAUTO_V2_BRAIN_SUPABASE_URL",
6240
+ jwt: "KGAUTO_V2_BRAIN_JWT",
6241
+ anonKey: "KGAUTO_V2_BRAIN_ANON_KEY",
6242
+ /** Accepted fallbacks, in resolution order after the canonical name. */
6243
+ fallbacks: {
6244
+ endpoint: ["KGAUTO_V2_BRAIN_URL"],
6245
+ jwt: ["GLASSBOX_BRAIN_JWT"],
6246
+ anonKey: []
6247
+ }
6248
+ };
6139
6249
  function readBrainReadEnv(envSource = defaultEnv()) {
6140
6250
  const endpoint = readKeyValue(envSource.KGAUTO_V2_BRAIN_SUPABASE_URL) ?? readKeyValue(envSource.KGAUTO_V2_BRAIN_URL);
6141
6251
  const jwt = readKeyValue(envSource.KGAUTO_V2_BRAIN_JWT) ?? readKeyValue(envSource.GLASSBOX_BRAIN_JWT);
@@ -6152,12 +6262,46 @@ function isSameModelRetryEnabledFromEnv(envSource = defaultEnv()) {
6152
6262
  }
6153
6263
 
6154
6264
  // src/streaming.ts
6265
+ function classifyThrownFetchError(err) {
6266
+ const name = err?.name;
6267
+ if (name === "TimeoutError") {
6268
+ return { errorType: "retryable", errorCode: "timeout", message: String(err) };
6269
+ }
6270
+ if (name === "AbortError") {
6271
+ return { errorType: "terminal", errorCode: "aborted", message: "aborted by caller signal" };
6272
+ }
6273
+ return { errorType: "retryable", errorCode: "network_error", message: String(err) };
6274
+ }
6275
+ function createStallGuard(external, stallTimeoutMs) {
6276
+ const ctl = new AbortController();
6277
+ if (external) {
6278
+ if (external.aborted) ctl.abort(external.reason);
6279
+ else external.addEventListener("abort", () => ctl.abort(external.reason), { once: true });
6280
+ }
6281
+ let timer;
6282
+ const arm = () => {
6283
+ if (!stallTimeoutMs || stallTimeoutMs <= 0) return;
6284
+ if (timer) clearTimeout(timer);
6285
+ timer = setTimeout(() => {
6286
+ ctl.abort(
6287
+ new DOMException(`stream stalled: no bytes for ${stallTimeoutMs}ms`, "TimeoutError")
6288
+ );
6289
+ }, stallTimeoutMs);
6290
+ };
6291
+ const clear = () => {
6292
+ if (timer) clearTimeout(timer);
6293
+ timer = void 0;
6294
+ };
6295
+ return { signal: ctl.signal, arm, clear };
6296
+ }
6155
6297
  var ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
6156
6298
  async function streamAnthropic(request, apiKey, opts) {
6157
6299
  const { provider: _provider, ...body } = request;
6158
6300
  const fetchFn = opts.fetchImpl ?? fetch;
6301
+ const guard = createStallGuard(opts.signal, opts.stallTimeoutMs);
6159
6302
  let res;
6160
6303
  try {
6304
+ guard.arm();
6161
6305
  res = await fetchFn(ANTHROPIC_URL, {
6162
6306
  method: "POST",
6163
6307
  headers: {
@@ -6165,12 +6309,16 @@ async function streamAnthropic(request, apiKey, opts) {
6165
6309
  "anthropic-version": "2023-06-01",
6166
6310
  "content-type": "application/json"
6167
6311
  },
6168
- body: JSON.stringify({ ...body, stream: true })
6312
+ body: JSON.stringify({ ...body, stream: true }),
6313
+ signal: guard.signal
6169
6314
  });
6170
6315
  } catch (err) {
6171
- return retryableError(0, "network_error", String(err), null);
6316
+ guard.clear();
6317
+ const c = classifyThrownFetchError(err);
6318
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
6172
6319
  }
6173
6320
  if (!res.ok) {
6321
+ guard.clear();
6174
6322
  const errBody = await res.json().catch(() => ({}));
6175
6323
  return classifyHttpError(res.status, errBody);
6176
6324
  }
@@ -6233,9 +6381,13 @@ async function streamAnthropic(request, apiKey, opts) {
6233
6381
  if (typeof p.usage?.output_tokens === "number") outputTokens = p.usage.output_tokens;
6234
6382
  return;
6235
6383
  }
6236
- });
6384
+ }, guard.arm, guard.signal);
6237
6385
  } catch (err) {
6238
- return retryableError(0, "stream_interrupted", String(err), null);
6386
+ const c = classifyThrownFetchError(err);
6387
+ const code = c.errorCode === "network_error" ? "stream_interrupted" : c.errorCode;
6388
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: code, message: c.message, raw: null };
6389
+ } finally {
6390
+ guard.clear();
6239
6391
  }
6240
6392
  const toolCalls = Array.from(toolBlocks.values()).map((b) => ({
6241
6393
  id: b.id,
@@ -6262,6 +6414,7 @@ async function streamAnthropic(request, apiKey, opts) {
6262
6414
  async function streamOpenAILike(url, request, apiKey, providerLabel, opts) {
6263
6415
  const { provider: _provider, ...body } = request;
6264
6416
  const fetchFn = opts.fetchImpl ?? fetch;
6417
+ const guard = createStallGuard(opts.signal, opts.stallTimeoutMs);
6265
6418
  const reqBody = {
6266
6419
  ...body,
6267
6420
  stream: true,
@@ -6272,18 +6425,23 @@ async function streamOpenAILike(url, request, apiKey, providerLabel, opts) {
6272
6425
  };
6273
6426
  let res;
6274
6427
  try {
6428
+ guard.arm();
6275
6429
  res = await fetchFn(url, {
6276
6430
  method: "POST",
6277
6431
  headers: {
6278
6432
  authorization: `Bearer ${apiKey}`,
6279
6433
  "content-type": "application/json"
6280
6434
  },
6281
- body: JSON.stringify(reqBody)
6435
+ body: JSON.stringify(reqBody),
6436
+ signal: guard.signal
6282
6437
  });
6283
6438
  } catch (err) {
6284
- return retryableError(0, "network_error", String(err), null);
6439
+ guard.clear();
6440
+ const c = classifyThrownFetchError(err);
6441
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
6285
6442
  }
6286
6443
  if (!res.ok) {
6444
+ guard.clear();
6287
6445
  const errBody = await res.json().catch(() => ({}));
6288
6446
  return classifyHttpError(res.status, errBody);
6289
6447
  }
@@ -6336,9 +6494,13 @@ async function streamOpenAILike(url, request, apiKey, providerLabel, opts) {
6336
6494
  const details = usage.prompt_tokens_details;
6337
6495
  if (typeof details?.cached_tokens === "number") cachedTokens = details.cached_tokens;
6338
6496
  }
6339
- });
6497
+ }, guard.arm, guard.signal);
6340
6498
  } catch (err) {
6341
- return retryableError(0, "stream_interrupted", String(err), null);
6499
+ const c = classifyThrownFetchError(err);
6500
+ const code = c.errorCode === "network_error" ? "stream_interrupted" : c.errorCode;
6501
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: code, message: c.message, raw: null };
6502
+ } finally {
6503
+ guard.clear();
6342
6504
  }
6343
6505
  const toolCalls = Array.from(toolBuffers.values()).filter((b) => b.name.length > 0).map((b) => ({
6344
6506
  id: b.id,
@@ -6361,15 +6523,28 @@ async function streamOpenAILike(url, request, apiKey, providerLabel, opts) {
6361
6523
  };
6362
6524
  return { ok: true, status: res.status, response };
6363
6525
  }
6364
- async function parseSSEStream(response, handler) {
6526
+ async function parseSSEStream(response, handler, onRead, signal) {
6365
6527
  const body = response.body;
6366
6528
  if (!body) throw new Error("Response has no body for SSE parse");
6367
6529
  const reader = body.getReader();
6368
6530
  const decoder = new TextDecoder("utf-8");
6369
6531
  let buffer = "";
6532
+ const abortRace = signal ? new Promise((_resolve, reject) => {
6533
+ if (signal.aborted) return reject(signal.reason);
6534
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
6535
+ }) : void 0;
6370
6536
  for (; ; ) {
6371
- const { value, done } = await reader.read();
6537
+ let readResult;
6538
+ try {
6539
+ readResult = abortRace ? await Promise.race([reader.read(), abortRace]) : await reader.read();
6540
+ } catch (err) {
6541
+ void reader.cancel().catch(() => {
6542
+ });
6543
+ throw err;
6544
+ }
6545
+ const { value, done } = readResult;
6372
6546
  if (done) break;
6547
+ onRead?.();
6373
6548
  buffer += decoder.decode(value, { stream: true });
6374
6549
  let sep;
6375
6550
  while (sep = buffer.indexOf("\n\n"), sep !== -1) {
@@ -6426,8 +6601,33 @@ function isAuthSignatureBody(body, message) {
6426
6601
  const m = message.toLowerCase();
6427
6602
  return m.includes("api key not valid") || m.includes("invalid api key") || m.includes("invalid x-api-key") || m.includes("incorrect api key");
6428
6603
  }
6604
+ function isBillingExhaustedBody(status, body, message) {
6605
+ if (status === 402) return true;
6606
+ const m = message.toLowerCase();
6607
+ if (m.includes("free_tier") || m.includes("free tier")) return false;
6608
+ if (m.includes("credit balance is too low")) return true;
6609
+ if (m.includes("insufficient balance")) return true;
6610
+ if (m.includes("billingnotenabled") || m.includes("billing not enabled")) return true;
6611
+ if (body && typeof body === "object") {
6612
+ const err = body.error;
6613
+ if (err && typeof err === "object") {
6614
+ const e = err;
6615
+ if (e.code === "insufficient_quota" || e.type === "insufficient_quota") return true;
6616
+ if (Array.isArray(e.details)) {
6617
+ for (const d of e.details) {
6618
+ if (d && typeof d === "object" && d.reason === "BILLING_DISABLED") {
6619
+ return true;
6620
+ }
6621
+ }
6622
+ }
6623
+ }
6624
+ }
6625
+ return false;
6626
+ }
6429
6627
  function classifyHttpError(status, body) {
6430
6628
  const message = extractErrorMessage(body) ?? `HTTP ${status}`;
6629
+ if (isBillingExhaustedBody(status, body, message))
6630
+ return { ok: false, status, errorType: "terminal", errorCode: "billing_exhausted", message, raw: body };
6431
6631
  if (status === 429)
6432
6632
  return { ok: false, status, errorType: "retryable", errorCode: "rate_limit", message, raw: body };
6433
6633
  if (status === 408)
@@ -6455,9 +6655,6 @@ function extractErrorMessage(body) {
6455
6655
  if (typeof b.message === "string") return b.message;
6456
6656
  return void 0;
6457
6657
  }
6458
- function retryableError(status, code, message, raw) {
6459
- return { ok: false, status, errorType: "retryable", errorCode: code, message, raw };
6460
- }
6461
6658
 
6462
6659
  // src/execute.ts
6463
6660
  var ANTHROPIC_URL2 = "https://api.anthropic.com/v1/messages";
@@ -6502,7 +6699,9 @@ async function executeAnthropic(request, opts) {
6502
6699
  if (opts.onChunk) {
6503
6700
  return streamAnthropic(request, apiKey, {
6504
6701
  onChunk: opts.onChunk,
6505
- fetchImpl: opts.fetchImpl
6702
+ fetchImpl: opts.fetchImpl,
6703
+ signal: opts.signal,
6704
+ stallTimeoutMs: opts.stallTimeoutMs
6506
6705
  });
6507
6706
  }
6508
6707
  const { provider: _provider, ...body } = request;
@@ -6517,11 +6716,13 @@ async function executeAnthropic(request, opts) {
6517
6716
  "anthropic-version": "2023-06-01",
6518
6717
  "content-type": "application/json"
6519
6718
  },
6520
- body: JSON.stringify(body)
6719
+ body: JSON.stringify(body),
6720
+ signal: opts.signal
6521
6721
  });
6522
6722
  json = await res.json().catch(() => ({}));
6523
6723
  } catch (err) {
6524
- return retryableError2(0, "network_error", String(err), null);
6724
+ const c = classifyThrownFetchError(err);
6725
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
6525
6726
  }
6526
6727
  if (!res.ok) return classifyHttpError2(res.status, json);
6527
6728
  return { ok: true, status: res.status, response: normalizeAnthropic(json) };
@@ -6553,11 +6754,13 @@ async function executeGoogle(request, opts) {
6553
6754
  res = await fetchFn(url, {
6554
6755
  method: "POST",
6555
6756
  headers: { "content-type": "application/json" },
6556
- body: JSON.stringify(body)
6757
+ body: JSON.stringify(body),
6758
+ signal: opts.signal
6557
6759
  });
6558
6760
  json = await res.json().catch(() => ({}));
6559
6761
  } catch (err) {
6560
- return retryableError2(0, "network_error", String(err), null);
6762
+ const c = classifyThrownFetchError(err);
6763
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
6561
6764
  }
6562
6765
  if (!res.ok) return classifyHttpError2(res.status, json);
6563
6766
  return { ok: true, status: res.status, response: normalizeGoogle(json) };
@@ -6589,7 +6792,9 @@ async function executeOpenAI(request, opts) {
6589
6792
  if (opts.onChunk) {
6590
6793
  return streamOpenAILike(OPENAI_URL, request, apiKey, "openai", {
6591
6794
  onChunk: opts.onChunk,
6592
- fetchImpl: opts.fetchImpl
6795
+ fetchImpl: opts.fetchImpl,
6796
+ signal: opts.signal,
6797
+ stallTimeoutMs: opts.stallTimeoutMs
6593
6798
  });
6594
6799
  }
6595
6800
  const { provider: _provider, ...body } = request;
@@ -6600,11 +6805,13 @@ async function executeOpenAI(request, opts) {
6600
6805
  res = await fetchFn(OPENAI_URL, {
6601
6806
  method: "POST",
6602
6807
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
6603
- body: JSON.stringify(body)
6808
+ body: JSON.stringify(body),
6809
+ signal: opts.signal
6604
6810
  });
6605
6811
  json = await res.json().catch(() => ({}));
6606
6812
  } catch (err) {
6607
- return retryableError2(0, "network_error", String(err), null);
6813
+ const c = classifyThrownFetchError(err);
6814
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
6608
6815
  }
6609
6816
  if (!res.ok) return classifyHttpError2(res.status, json);
6610
6817
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -6617,7 +6824,9 @@ async function executeDeepSeek(request, opts) {
6617
6824
  if (opts.onChunk) {
6618
6825
  return streamOpenAILike(DEEPSEEK_URL, request, apiKey, "deepseek", {
6619
6826
  onChunk: opts.onChunk,
6620
- fetchImpl: opts.fetchImpl
6827
+ fetchImpl: opts.fetchImpl,
6828
+ signal: opts.signal,
6829
+ stallTimeoutMs: opts.stallTimeoutMs
6621
6830
  });
6622
6831
  }
6623
6832
  const { provider: _provider, ...body } = request;
@@ -6628,11 +6837,13 @@ async function executeDeepSeek(request, opts) {
6628
6837
  res = await fetchFn(DEEPSEEK_URL, {
6629
6838
  method: "POST",
6630
6839
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
6631
- body: JSON.stringify(body)
6840
+ body: JSON.stringify(body),
6841
+ signal: opts.signal
6632
6842
  });
6633
6843
  json = await res.json().catch(() => ({}));
6634
6844
  } catch (err) {
6635
- return retryableError2(0, "network_error", String(err), null);
6845
+ const c = classifyThrownFetchError(err);
6846
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
6636
6847
  }
6637
6848
  if (!res.ok) return classifyHttpError2(res.status, json);
6638
6849
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -6645,7 +6856,9 @@ async function executeOpenAICompatible(request, opts, spec) {
6645
6856
  if (opts.onChunk) {
6646
6857
  return streamOpenAILike(spec.url, request, apiKey, spec.provider, {
6647
6858
  onChunk: opts.onChunk,
6648
- fetchImpl: opts.fetchImpl
6859
+ fetchImpl: opts.fetchImpl,
6860
+ signal: opts.signal,
6861
+ stallTimeoutMs: opts.stallTimeoutMs
6649
6862
  });
6650
6863
  }
6651
6864
  const { provider: _provider, ...body } = request;
@@ -6656,11 +6869,13 @@ async function executeOpenAICompatible(request, opts, spec) {
6656
6869
  res = await fetchFn(spec.url, {
6657
6870
  method: "POST",
6658
6871
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
6659
- body: JSON.stringify(body)
6872
+ body: JSON.stringify(body),
6873
+ signal: opts.signal
6660
6874
  });
6661
6875
  json = await res.json().catch(() => ({}));
6662
6876
  } catch (err) {
6663
- return retryableError2(0, "network_error", String(err), null);
6877
+ const c = classifyThrownFetchError(err);
6878
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
6664
6879
  }
6665
6880
  if (!res.ok) return classifyHttpError2(res.status, json);
6666
6881
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -6691,6 +6906,9 @@ function applyOverrides(request, overrides) {
6691
6906
  }
6692
6907
  function classifyHttpError2(status, body) {
6693
6908
  const message = extractErrorMessage2(body) ?? `HTTP ${status}`;
6909
+ if (isBillingExhaustedBody(status, body, message)) {
6910
+ return { ok: false, status, errorType: "terminal", errorCode: "billing_exhausted", message, raw: body };
6911
+ }
6694
6912
  if (status === 429) {
6695
6913
  return { ok: false, status, errorType: "retryable", errorCode: "rate_limit", message, raw: body };
6696
6914
  }
@@ -6727,9 +6945,6 @@ function extractErrorMessage2(body) {
6727
6945
  function terminalError(status, code, message) {
6728
6946
  return { ok: false, status, errorType: "terminal", errorCode: code, message, raw: null };
6729
6947
  }
6730
- function retryableError2(status, code, message, raw) {
6731
- return { ok: false, status, errorType: "retryable", errorCode: code, message, raw };
6732
- }
6733
6948
  function tryParseJson2(s) {
6734
6949
  if (typeof s !== "string" || s.length === 0) return void 0;
6735
6950
  try {
@@ -7533,18 +7748,28 @@ async function call(ir, opts = {}) {
7533
7748
  }
7534
7749
  let activeCompile = initial;
7535
7750
  let lastErr;
7536
- const failedProviders = /* @__PURE__ */ new Set();
7751
+ const failedProviders = /* @__PURE__ */ new Map();
7537
7752
  const sameModelRetryEnabled = opts.sameModelRetry ?? isSameModelRetryEnabledFromEnv();
7538
7753
  let retriedSameModel = false;
7539
7754
  for (let i = 0; i < targetsToTry.length; i++) {
7540
7755
  const targetModel = targetsToTry[i];
7756
+ if (opts.abortSignal?.aborted) {
7757
+ attempts.push({
7758
+ model: targetModel,
7759
+ status: "terminal",
7760
+ errorCode: "aborted",
7761
+ message: "Skipped \u2014 caller abortSignal fired before this attempt started"
7762
+ });
7763
+ break;
7764
+ }
7541
7765
  const targetProfile = tryGetProfile(targetModel);
7542
- if (targetProfile && failedProviders.has(targetProfile.provider) && !opts.noFallback) {
7766
+ const providerFailReason = targetProfile ? failedProviders.get(targetProfile.provider) : void 0;
7767
+ if (targetProfile && providerFailReason && !opts.noFallback) {
7543
7768
  attempts.push({
7544
7769
  model: targetModel,
7545
7770
  status: "terminal",
7546
- errorCode: "auth_inferred",
7547
- message: `Skipped \u2014 provider ${targetProfile.provider} returned 401/403 earlier in this call; same key inferred to fail`
7771
+ errorCode: `${providerFailReason}_inferred`,
7772
+ message: providerFailReason === "billing_exhausted" ? `Skipped \u2014 provider ${targetProfile.provider} is out of credits (seen earlier in this call); a billing failure is account-wide, so this attempt cannot succeed until a human funds the account` : `Skipped \u2014 provider ${targetProfile.provider} returned 401/403 earlier in this call; same key inferred to fail`
7548
7773
  });
7549
7774
  continue;
7550
7775
  }
@@ -7573,16 +7798,29 @@ async function call(ir, opts = {}) {
7573
7798
  );
7574
7799
  const targetSupportsStreaming = targetProfile?.streaming === true;
7575
7800
  const streamingOnChunk = opts.onChunk && !opts.noStream && targetSupportsStreaming ? opts.onChunk : void 0;
7576
- const execOpts = {
7801
+ const mkExecOpts = () => ({
7577
7802
  apiKeys: opts.apiKeys,
7578
7803
  fetchImpl: opts.fetchImpl,
7579
7804
  providerOverrides: opts.providerOverrides,
7580
- onChunk: streamingOnChunk
7581
- };
7582
- const exec = await execute(activeCompile.request, execOpts);
7805
+ onChunk: streamingOnChunk,
7806
+ signal: composeAttemptSignal(
7807
+ streamingOnChunk ? void 0 : opts.attemptTimeoutMs,
7808
+ opts.abortSignal
7809
+ ),
7810
+ stallTimeoutMs: streamingOnChunk ? opts.attemptTimeoutMs : void 0
7811
+ });
7812
+ const exec = await execute(activeCompile.request, mkExecOpts());
7583
7813
  let validated = exec.ok ? validateStructuredContract(exec, ir) : exec;
7584
7814
  let servedByRetry = false;
7585
- if (!validated.ok && isStructuredContractViolation(validated.errorCode) && sameModelRetryEnabled && !retriedSameModel) {
7815
+ let retrySuppressionNote;
7816
+ if (!validated.ok && isStructuredContractViolation(validated.errorCode) && sameModelRetryEnabled && !retriedSameModel && getMeasuredFailureVerdict({
7817
+ appId: ir.appId,
7818
+ archetype: ir.intent.archetype,
7819
+ model: targetModel
7820
+ })?.gated === true) {
7821
+ retrySuppressionNote = " [sameModelRetry suppressed: this model carries an active measured-failure gate for this archetype \u2014 retrying it would near-certainly bill a second doomed inference; walking the chain instead]";
7822
+ }
7823
+ if (!validated.ok && isStructuredContractViolation(validated.errorCode) && sameModelRetryEnabled && !retriedSameModel && !retrySuppressionNote) {
7586
7824
  retriedSameModel = true;
7587
7825
  attempts.push({
7588
7826
  model: targetModel,
@@ -7594,7 +7832,7 @@ async function call(ir, opts = {}) {
7594
7832
  safeEmit(
7595
7833
  () => emitExecuteAttempt(traceId, ir.appId, { model: targetModel, attemptIndex: i })
7596
7834
  );
7597
- const retryExec = await execute(retryRequest, execOpts);
7835
+ const retryExec = await execute(retryRequest, mkExecOpts());
7598
7836
  validated = retryExec.ok ? validateStructuredContract(retryExec, ir) : retryExec;
7599
7837
  servedByRetry = true;
7600
7838
  }
@@ -7721,20 +7959,23 @@ async function call(ir, opts = {}) {
7721
7959
  model: targetModel,
7722
7960
  status: validated.errorType,
7723
7961
  errorCode: validated.errorCode,
7724
- message: validated.message,
7962
+ message: retrySuppressionNote ? validated.message + retrySuppressionNote : validated.message,
7725
7963
  ...servedByRetry ? { sameModelRetry: true } : {}
7726
7964
  });
7727
7965
  lastErr = validated;
7728
7966
  if (validated.errorType === "terminal" || opts.noFallback) {
7729
- if (validated.errorCode === "auth" && !opts.noFallback && activeCompile.provider) {
7730
- failedProviders.add(activeCompile.provider);
7967
+ if ((validated.errorCode === "auth" || validated.errorCode === "billing_exhausted") && !opts.noFallback && activeCompile.provider) {
7968
+ failedProviders.set(
7969
+ activeCompile.provider,
7970
+ validated.errorCode === "billing_exhausted" ? "billing_exhausted" : "auth"
7971
+ );
7731
7972
  continue;
7732
7973
  }
7733
7974
  break;
7734
7975
  }
7735
7976
  }
7736
7977
  const lastAttempted = [...attempts].reverse().find(
7737
- (a) => a.status !== "success" && a.errorCode !== "auth_inferred" && a.errorCode !== "compile_error"
7978
+ (a) => a.status !== "success" && a.errorCode !== "auth_inferred" && a.errorCode !== "billing_exhausted_inferred" && a.errorCode !== "compile_error"
7738
7979
  );
7739
7980
  const latencyMs = Date.now() - start;
7740
7981
  await record({
@@ -7757,6 +7998,25 @@ async function call(ir, opts = {}) {
7757
7998
  lastErr?.errorCode
7758
7999
  );
7759
8000
  }
8001
+ function composeAttemptSignal(attemptTimeoutMs, callerSignal) {
8002
+ const signals = [];
8003
+ if (callerSignal) signals.push(callerSignal);
8004
+ if (typeof attemptTimeoutMs === "number" && attemptTimeoutMs > 0) {
8005
+ signals.push(AbortSignal.timeout(attemptTimeoutMs));
8006
+ }
8007
+ if (signals.length === 0) return void 0;
8008
+ if (signals.length === 1) return signals[0];
8009
+ if (typeof AbortSignal.any === "function") return AbortSignal.any(signals);
8010
+ const ctl = new AbortController();
8011
+ for (const s of signals) {
8012
+ if (s.aborted) {
8013
+ ctl.abort(s.reason);
8014
+ break;
8015
+ }
8016
+ s.addEventListener("abort", () => ctl.abort(s.reason), { once: true });
8017
+ }
8018
+ return ctl.signal;
8019
+ }
7760
8020
  function compileAndRegister(ir, opts) {
7761
8021
  const result = compile(ir, {
7762
8022
  policy: opts.policy,
@@ -8056,6 +8316,9 @@ function normalizeFallbackReason(attempts) {
8056
8316
  }
8057
8317
  if (code === "cost_cap_exceeded") return "cost_cap";
8058
8318
  if (code === "auth" || code === "auth_inferred") return "provider_auth_failed";
8319
+ if (code === "billing_exhausted" || code === "billing_exhausted_inferred") {
8320
+ return "provider_billing_exhausted";
8321
+ }
8059
8322
  return "provider_error";
8060
8323
  }
8061
8324
  function generateTraceId() {
@@ -8808,6 +9071,12 @@ async function runGoldenEval(opts) {
8808
9071
  );
8809
9072
  }
8810
9073
  }
9074
+ if (wins + ties + losses === 0) {
9075
+ notes.push(
9076
+ "zero cases judged \u2014 run row persisted, evidence advisory deliberately NOT written (a verdict on n=0 is not evidence)"
9077
+ );
9078
+ return result;
9079
+ }
8811
9080
  const latestRes = await fetchFn(
8812
9081
  rest(
8813
9082
  `compile_outcomes?app_id=eq.${encodeURIComponent(opts.appId)}&select=id&order=id.desc&limit=1`
@@ -9043,7 +9312,7 @@ function createBrainForwardRoutes(config) {
9043
9312
  }
9044
9313
 
9045
9314
  // src/version.ts
9046
- var LIBRARY_VERSION = "2.0.0-alpha.76";
9315
+ var LIBRARY_VERSION = "2.0.0-alpha.78";
9047
9316
 
9048
9317
  // src/key-health.ts
9049
9318
  var JSON_HEADERS2 = { "Content-Type": "application/json" };
@@ -9400,19 +9669,32 @@ function resolveFetch(injected) {
9400
9669
  function normalizeEndpoint2(endpoint) {
9401
9670
  return endpoint.replace(/\/+$/, "");
9402
9671
  }
9672
+ function resolveBrainReadTrio(opts) {
9673
+ const env = readBrainReadEnv();
9674
+ const brainEndpoint = opts.brainEndpoint ?? env.endpoint;
9675
+ const brainJwt = opts.brainJwt ?? env.jwt;
9676
+ const brainAnonKey = opts.brainAnonKey ?? env.anonKey;
9677
+ if (brainEndpoint && brainJwt && brainAnonKey) {
9678
+ return { ok: true, brainEndpoint, brainJwt, brainAnonKey };
9679
+ }
9680
+ const missingEnv = [];
9681
+ if (!brainEndpoint) missingEnv.push("KGAUTO_V2_BRAIN_SUPABASE_URL");
9682
+ if (!brainJwt) missingEnv.push("KGAUTO_V2_BRAIN_JWT");
9683
+ if (!brainAnonKey) missingEnv.push("KGAUTO_V2_BRAIN_ANON_KEY");
9684
+ return { ok: false, missingEnv };
9685
+ }
9403
9686
  async function getActionableAdvisories(opts) {
9404
- const {
9405
- appId,
9406
- severity,
9407
- status,
9408
- brainEndpoint,
9409
- brainJwt,
9410
- brainAnonKey,
9411
- fetch: injectedFetch
9412
- } = opts;
9687
+ const { appId, severity, status, fetch: injectedFetch } = opts;
9413
9688
  if (!appId) {
9414
9689
  throw new Error("getActionableAdvisories: appId is required");
9415
9690
  }
9691
+ const trio = resolveBrainReadTrio(opts);
9692
+ if (!trio.ok) {
9693
+ throw new Error(
9694
+ `getActionableAdvisories: brain-read-not-configured \u2014 missing ${trio.missingEnv.join(", ")} (pass brainEndpoint/brainJwt/brainAnonKey explicitly or set the canonical env vars)`
9695
+ );
9696
+ }
9697
+ const { brainEndpoint, brainJwt, brainAnonKey } = trio;
9416
9698
  const doFetch = resolveFetch(injectedFetch);
9417
9699
  const base = normalizeEndpoint2(brainEndpoint);
9418
9700
  const qs = new URLSearchParams();
@@ -9465,17 +9747,15 @@ async function getActionableAdvisories(opts) {
9465
9747
  return out;
9466
9748
  }
9467
9749
  async function markAdvisoryResolved(opts) {
9468
- const {
9469
- id,
9470
- resolutionNote,
9471
- brainEndpoint,
9472
- brainJwt,
9473
- brainAnonKey,
9474
- fetch: injectedFetch
9475
- } = opts;
9750
+ const { id, resolutionNote, fetch: injectedFetch } = opts;
9476
9751
  if (!id) {
9477
9752
  return { ok: false, reason: "id_required" };
9478
9753
  }
9754
+ const trio = resolveBrainReadTrio(opts);
9755
+ if (!trio.ok) {
9756
+ return { ok: false, reason: `brain_read_not_configured:${trio.missingEnv.join(",")}` };
9757
+ }
9758
+ const { brainEndpoint, brainJwt, brainAnonKey } = trio;
9479
9759
  const doFetch = resolveFetch(injectedFetch);
9480
9760
  const base = normalizeEndpoint2(brainEndpoint);
9481
9761
  const lookupUrl = `${base}/rest/v1/actionable_advisories_v?id=eq.${encodeURIComponent(id)}&select=app_id,rule`;
@@ -9560,7 +9840,7 @@ async function markAdvisoryResolved(opts) {
9560
9840
  }
9561
9841
  }
9562
9842
  if (outcomeIds.length === 0) {
9563
- return { ok: true };
9843
+ return { ok: true, firingsResolved: 0, status: "unknown" };
9564
9844
  }
9565
9845
  const inList = outcomeIds.join(",");
9566
9846
  const patchUrl = `${base}/rest/v1/compile_outcome_advisories?outcome_id=in.(${inList})&code=eq.${encodeURIComponent(code)}&resolved_at=is.null`;
@@ -9580,8 +9860,10 @@ async function markAdvisoryResolved(opts) {
9580
9860
  apikey: brainAnonKey,
9581
9861
  "Content-Type": "application/json",
9582
9862
  Accept: "application/json",
9583
- // PostgREST default is no return; we don't need the row back.
9584
- Prefer: "return=minimal"
9863
+ // alpha.78 count what actually changed. An RLS-filtered PATCH
9864
+ // returns 2xx with zero rows; return=representation makes that
9865
+ // visible instead of success-shaped.
9866
+ Prefer: "return=representation"
9585
9867
  },
9586
9868
  body: JSON.stringify(patchBody)
9587
9869
  });
@@ -9598,7 +9880,33 @@ async function markAdvisoryResolved(opts) {
9598
9880
  if (!patchRes.ok) {
9599
9881
  return { ok: false, reason: `patch_failed:${patchRes.status}` };
9600
9882
  }
9601
- return { ok: true };
9883
+ let patchedRows = [];
9884
+ try {
9885
+ patchedRows = await patchRes.json();
9886
+ } catch {
9887
+ }
9888
+ const firingsResolved = Array.isArray(patchedRows) ? patchedRows.length : 0;
9889
+ let status = "unknown";
9890
+ try {
9891
+ const statusRes = await doFetch(
9892
+ `${base}/rest/v1/actionable_advisories_v?id=eq.${encodeURIComponent(id)}&select=status`,
9893
+ {
9894
+ method: "GET",
9895
+ headers: {
9896
+ Authorization: `Bearer ${brainJwt}`,
9897
+ apikey: brainAnonKey,
9898
+ Accept: "application/json"
9899
+ }
9900
+ }
9901
+ );
9902
+ if (statusRes.ok) {
9903
+ const rows = await statusRes.json();
9904
+ const s = Array.isArray(rows) ? rows[0]?.status : void 0;
9905
+ if (s === "open" || s === "resolved") status = s;
9906
+ }
9907
+ } catch {
9908
+ }
9909
+ return { ok: true, firingsResolved, status };
9602
9910
  }
9603
9911
  async function markExclusionFindingHandled(opts) {
9604
9912
  const {
@@ -9607,12 +9915,14 @@ async function markExclusionFindingHandled(opts) {
9607
9915
  excludedModel,
9608
9916
  resolution,
9609
9917
  resolutionNote,
9610
- brainEndpoint,
9611
- brainJwt,
9612
- brainAnonKey,
9613
9918
  fetch: injectedFetch
9614
9919
  } = opts;
9615
9920
  if (!appId) return { ok: false, reason: "app_id_required" };
9921
+ const trio = resolveBrainReadTrio(opts);
9922
+ if (!trio.ok) {
9923
+ return { ok: false, reason: `brain_read_not_configured:${trio.missingEnv.join(",")}` };
9924
+ }
9925
+ const { brainEndpoint, brainJwt, brainAnonKey } = trio;
9616
9926
  if (!archetype) return { ok: false, reason: "archetype_required" };
9617
9927
  if (!excludedModel) {
9618
9928
  return { ok: false, reason: "excluded_model_required" };
@@ -9879,6 +10189,7 @@ function compile2(ir, opts) {
9879
10189
  ALL_ARCHETYPES,
9880
10190
  ARCHETYPE_FAMILY_FITS,
9881
10191
  ARCHETYPE_FLOOR_DEFAULT,
10192
+ BRAIN_READ_ENV_NAMES,
9882
10193
  COACH_CFG,
9883
10194
  CallError,
9884
10195
  DECOMPOSITION_TEMPLATES,
@@ -9897,6 +10208,7 @@ function compile2(ir, opts) {
9897
10208
  MEASURED_GROUNDING_MIN_N,
9898
10209
  PRODUCER_OWNED_RULE_CODES,
9899
10210
  PROVIDER_ENV_KEYS,
10211
+ ROLLBACK_SUPPRESSION_WINDOW_DAYS,
9900
10212
  RULE_DISCIPLINE_GATES_V1,
9901
10213
  RULE_DISCIPLINE_GATES_V1_STRUCTURED,
9902
10214
  RULE_SEQUENTIAL_TOOL_CLIFF,
@@ -9950,12 +10262,14 @@ function compile2(ir, opts) {
9950
10262
  getPerAxisMetrics,
9951
10263
  getProfile,
9952
10264
  getReachabilityDiagnostic,
10265
+ getRecentRollback,
9953
10266
  getRecommendedPrimary,
9954
10267
  getSequentialStarterChain,
9955
10268
  getSequentialStarterChainWithGrounding,
9956
10269
  getStaleExclusionFindings,
9957
10270
  getStarterChain,
9958
10271
  getStarterChainWithGrounding,
10272
+ hasMutation,
9959
10273
  hashShape,
9960
10274
  isArchetype,
9961
10275
  isAutoPromoteEnabledFromEnv,
@@ -9976,9 +10290,11 @@ function compile2(ir, opts) {
9976
10290
  loadChainsFromBrain,
9977
10291
  loadModelsFromBrain,
9978
10292
  loadPricingFromBrain,
10293
+ mapMeasuredFailureRows,
9979
10294
  markAdvisoryResolved,
9980
10295
  markExclusionFindingHandled,
9981
10296
  markPromoteReadyHandled,
10297
+ mutationId,
9982
10298
  parseGoldenCaptureRate,
9983
10299
  parseJudgeVerdict,
9984
10300
  peekBrainDeadLetter,