@warmdrift/kgauto-compiler 2.0.0-alpha.77 → 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");
4704
- }
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
- });
4718
- }
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;
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
+ ];
4779
4760
  }
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);
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
+ }
4787
4780
  }
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);
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
+ ];
4795
4793
  }
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);
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
+ ];
4812
4812
  }
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
+ ];
4813
4823
  }
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);
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
+ ];
4833
4851
  }
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);
4852
+ function confidenceRank(c) {
4853
+ if (c === "high") return 3;
4854
+ if (c === "medium") return 2;
4855
+ return 1;
4856
+ }
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) {
@@ -6480,9 +6655,6 @@ function extractErrorMessage(body) {
6480
6655
  if (typeof b.message === "string") return b.message;
6481
6656
  return void 0;
6482
6657
  }
6483
- function retryableError(status, code, message, raw) {
6484
- return { ok: false, status, errorType: "retryable", errorCode: code, message, raw };
6485
- }
6486
6658
 
6487
6659
  // src/execute.ts
6488
6660
  var ANTHROPIC_URL2 = "https://api.anthropic.com/v1/messages";
@@ -6527,7 +6699,9 @@ async function executeAnthropic(request, opts) {
6527
6699
  if (opts.onChunk) {
6528
6700
  return streamAnthropic(request, apiKey, {
6529
6701
  onChunk: opts.onChunk,
6530
- fetchImpl: opts.fetchImpl
6702
+ fetchImpl: opts.fetchImpl,
6703
+ signal: opts.signal,
6704
+ stallTimeoutMs: opts.stallTimeoutMs
6531
6705
  });
6532
6706
  }
6533
6707
  const { provider: _provider, ...body } = request;
@@ -6542,11 +6716,13 @@ async function executeAnthropic(request, opts) {
6542
6716
  "anthropic-version": "2023-06-01",
6543
6717
  "content-type": "application/json"
6544
6718
  },
6545
- body: JSON.stringify(body)
6719
+ body: JSON.stringify(body),
6720
+ signal: opts.signal
6546
6721
  });
6547
6722
  json = await res.json().catch(() => ({}));
6548
6723
  } catch (err) {
6549
- 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 };
6550
6726
  }
6551
6727
  if (!res.ok) return classifyHttpError2(res.status, json);
6552
6728
  return { ok: true, status: res.status, response: normalizeAnthropic(json) };
@@ -6578,11 +6754,13 @@ async function executeGoogle(request, opts) {
6578
6754
  res = await fetchFn(url, {
6579
6755
  method: "POST",
6580
6756
  headers: { "content-type": "application/json" },
6581
- body: JSON.stringify(body)
6757
+ body: JSON.stringify(body),
6758
+ signal: opts.signal
6582
6759
  });
6583
6760
  json = await res.json().catch(() => ({}));
6584
6761
  } catch (err) {
6585
- 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 };
6586
6764
  }
6587
6765
  if (!res.ok) return classifyHttpError2(res.status, json);
6588
6766
  return { ok: true, status: res.status, response: normalizeGoogle(json) };
@@ -6614,7 +6792,9 @@ async function executeOpenAI(request, opts) {
6614
6792
  if (opts.onChunk) {
6615
6793
  return streamOpenAILike(OPENAI_URL, request, apiKey, "openai", {
6616
6794
  onChunk: opts.onChunk,
6617
- fetchImpl: opts.fetchImpl
6795
+ fetchImpl: opts.fetchImpl,
6796
+ signal: opts.signal,
6797
+ stallTimeoutMs: opts.stallTimeoutMs
6618
6798
  });
6619
6799
  }
6620
6800
  const { provider: _provider, ...body } = request;
@@ -6625,11 +6805,13 @@ async function executeOpenAI(request, opts) {
6625
6805
  res = await fetchFn(OPENAI_URL, {
6626
6806
  method: "POST",
6627
6807
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
6628
- body: JSON.stringify(body)
6808
+ body: JSON.stringify(body),
6809
+ signal: opts.signal
6629
6810
  });
6630
6811
  json = await res.json().catch(() => ({}));
6631
6812
  } catch (err) {
6632
- 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 };
6633
6815
  }
6634
6816
  if (!res.ok) return classifyHttpError2(res.status, json);
6635
6817
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -6642,7 +6824,9 @@ async function executeDeepSeek(request, opts) {
6642
6824
  if (opts.onChunk) {
6643
6825
  return streamOpenAILike(DEEPSEEK_URL, request, apiKey, "deepseek", {
6644
6826
  onChunk: opts.onChunk,
6645
- fetchImpl: opts.fetchImpl
6827
+ fetchImpl: opts.fetchImpl,
6828
+ signal: opts.signal,
6829
+ stallTimeoutMs: opts.stallTimeoutMs
6646
6830
  });
6647
6831
  }
6648
6832
  const { provider: _provider, ...body } = request;
@@ -6653,11 +6837,13 @@ async function executeDeepSeek(request, opts) {
6653
6837
  res = await fetchFn(DEEPSEEK_URL, {
6654
6838
  method: "POST",
6655
6839
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
6656
- body: JSON.stringify(body)
6840
+ body: JSON.stringify(body),
6841
+ signal: opts.signal
6657
6842
  });
6658
6843
  json = await res.json().catch(() => ({}));
6659
6844
  } catch (err) {
6660
- 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 };
6661
6847
  }
6662
6848
  if (!res.ok) return classifyHttpError2(res.status, json);
6663
6849
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -6670,7 +6856,9 @@ async function executeOpenAICompatible(request, opts, spec) {
6670
6856
  if (opts.onChunk) {
6671
6857
  return streamOpenAILike(spec.url, request, apiKey, spec.provider, {
6672
6858
  onChunk: opts.onChunk,
6673
- fetchImpl: opts.fetchImpl
6859
+ fetchImpl: opts.fetchImpl,
6860
+ signal: opts.signal,
6861
+ stallTimeoutMs: opts.stallTimeoutMs
6674
6862
  });
6675
6863
  }
6676
6864
  const { provider: _provider, ...body } = request;
@@ -6681,11 +6869,13 @@ async function executeOpenAICompatible(request, opts, spec) {
6681
6869
  res = await fetchFn(spec.url, {
6682
6870
  method: "POST",
6683
6871
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
6684
- body: JSON.stringify(body)
6872
+ body: JSON.stringify(body),
6873
+ signal: opts.signal
6685
6874
  });
6686
6875
  json = await res.json().catch(() => ({}));
6687
6876
  } catch (err) {
6688
- 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 };
6689
6879
  }
6690
6880
  if (!res.ok) return classifyHttpError2(res.status, json);
6691
6881
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -6755,9 +6945,6 @@ function extractErrorMessage2(body) {
6755
6945
  function terminalError(status, code, message) {
6756
6946
  return { ok: false, status, errorType: "terminal", errorCode: code, message, raw: null };
6757
6947
  }
6758
- function retryableError2(status, code, message, raw) {
6759
- return { ok: false, status, errorType: "retryable", errorCode: code, message, raw };
6760
- }
6761
6948
  function tryParseJson2(s) {
6762
6949
  if (typeof s !== "string" || s.length === 0) return void 0;
6763
6950
  try {
@@ -7566,6 +7753,15 @@ async function call(ir, opts = {}) {
7566
7753
  let retriedSameModel = false;
7567
7754
  for (let i = 0; i < targetsToTry.length; i++) {
7568
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
+ }
7569
7765
  const targetProfile = tryGetProfile(targetModel);
7570
7766
  const providerFailReason = targetProfile ? failedProviders.get(targetProfile.provider) : void 0;
7571
7767
  if (targetProfile && providerFailReason && !opts.noFallback) {
@@ -7602,16 +7798,29 @@ async function call(ir, opts = {}) {
7602
7798
  );
7603
7799
  const targetSupportsStreaming = targetProfile?.streaming === true;
7604
7800
  const streamingOnChunk = opts.onChunk && !opts.noStream && targetSupportsStreaming ? opts.onChunk : void 0;
7605
- const execOpts = {
7801
+ const mkExecOpts = () => ({
7606
7802
  apiKeys: opts.apiKeys,
7607
7803
  fetchImpl: opts.fetchImpl,
7608
7804
  providerOverrides: opts.providerOverrides,
7609
- onChunk: streamingOnChunk
7610
- };
7611
- 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());
7612
7813
  let validated = exec.ok ? validateStructuredContract(exec, ir) : exec;
7613
7814
  let servedByRetry = false;
7614
- 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) {
7615
7824
  retriedSameModel = true;
7616
7825
  attempts.push({
7617
7826
  model: targetModel,
@@ -7623,7 +7832,7 @@ async function call(ir, opts = {}) {
7623
7832
  safeEmit(
7624
7833
  () => emitExecuteAttempt(traceId, ir.appId, { model: targetModel, attemptIndex: i })
7625
7834
  );
7626
- const retryExec = await execute(retryRequest, execOpts);
7835
+ const retryExec = await execute(retryRequest, mkExecOpts());
7627
7836
  validated = retryExec.ok ? validateStructuredContract(retryExec, ir) : retryExec;
7628
7837
  servedByRetry = true;
7629
7838
  }
@@ -7750,7 +7959,7 @@ async function call(ir, opts = {}) {
7750
7959
  model: targetModel,
7751
7960
  status: validated.errorType,
7752
7961
  errorCode: validated.errorCode,
7753
- message: validated.message,
7962
+ message: retrySuppressionNote ? validated.message + retrySuppressionNote : validated.message,
7754
7963
  ...servedByRetry ? { sameModelRetry: true } : {}
7755
7964
  });
7756
7965
  lastErr = validated;
@@ -7789,6 +7998,25 @@ async function call(ir, opts = {}) {
7789
7998
  lastErr?.errorCode
7790
7999
  );
7791
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
+ }
7792
8020
  function compileAndRegister(ir, opts) {
7793
8021
  const result = compile(ir, {
7794
8022
  policy: opts.policy,
@@ -8843,6 +9071,12 @@ async function runGoldenEval(opts) {
8843
9071
  );
8844
9072
  }
8845
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
+ }
8846
9080
  const latestRes = await fetchFn(
8847
9081
  rest(
8848
9082
  `compile_outcomes?app_id=eq.${encodeURIComponent(opts.appId)}&select=id&order=id.desc&limit=1`
@@ -9078,7 +9312,7 @@ function createBrainForwardRoutes(config) {
9078
9312
  }
9079
9313
 
9080
9314
  // src/version.ts
9081
- var LIBRARY_VERSION = "2.0.0-alpha.77";
9315
+ var LIBRARY_VERSION = "2.0.0-alpha.78";
9082
9316
 
9083
9317
  // src/key-health.ts
9084
9318
  var JSON_HEADERS2 = { "Content-Type": "application/json" };
@@ -9435,19 +9669,32 @@ function resolveFetch(injected) {
9435
9669
  function normalizeEndpoint2(endpoint) {
9436
9670
  return endpoint.replace(/\/+$/, "");
9437
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
+ }
9438
9686
  async function getActionableAdvisories(opts) {
9439
- const {
9440
- appId,
9441
- severity,
9442
- status,
9443
- brainEndpoint,
9444
- brainJwt,
9445
- brainAnonKey,
9446
- fetch: injectedFetch
9447
- } = opts;
9687
+ const { appId, severity, status, fetch: injectedFetch } = opts;
9448
9688
  if (!appId) {
9449
9689
  throw new Error("getActionableAdvisories: appId is required");
9450
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;
9451
9698
  const doFetch = resolveFetch(injectedFetch);
9452
9699
  const base = normalizeEndpoint2(brainEndpoint);
9453
9700
  const qs = new URLSearchParams();
@@ -9500,17 +9747,15 @@ async function getActionableAdvisories(opts) {
9500
9747
  return out;
9501
9748
  }
9502
9749
  async function markAdvisoryResolved(opts) {
9503
- const {
9504
- id,
9505
- resolutionNote,
9506
- brainEndpoint,
9507
- brainJwt,
9508
- brainAnonKey,
9509
- fetch: injectedFetch
9510
- } = opts;
9750
+ const { id, resolutionNote, fetch: injectedFetch } = opts;
9511
9751
  if (!id) {
9512
9752
  return { ok: false, reason: "id_required" };
9513
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;
9514
9759
  const doFetch = resolveFetch(injectedFetch);
9515
9760
  const base = normalizeEndpoint2(brainEndpoint);
9516
9761
  const lookupUrl = `${base}/rest/v1/actionable_advisories_v?id=eq.${encodeURIComponent(id)}&select=app_id,rule`;
@@ -9595,7 +9840,7 @@ async function markAdvisoryResolved(opts) {
9595
9840
  }
9596
9841
  }
9597
9842
  if (outcomeIds.length === 0) {
9598
- return { ok: true };
9843
+ return { ok: true, firingsResolved: 0, status: "unknown" };
9599
9844
  }
9600
9845
  const inList = outcomeIds.join(",");
9601
9846
  const patchUrl = `${base}/rest/v1/compile_outcome_advisories?outcome_id=in.(${inList})&code=eq.${encodeURIComponent(code)}&resolved_at=is.null`;
@@ -9615,8 +9860,10 @@ async function markAdvisoryResolved(opts) {
9615
9860
  apikey: brainAnonKey,
9616
9861
  "Content-Type": "application/json",
9617
9862
  Accept: "application/json",
9618
- // PostgREST default is no return; we don't need the row back.
9619
- 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"
9620
9867
  },
9621
9868
  body: JSON.stringify(patchBody)
9622
9869
  });
@@ -9633,7 +9880,33 @@ async function markAdvisoryResolved(opts) {
9633
9880
  if (!patchRes.ok) {
9634
9881
  return { ok: false, reason: `patch_failed:${patchRes.status}` };
9635
9882
  }
9636
- 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 };
9637
9910
  }
9638
9911
  async function markExclusionFindingHandled(opts) {
9639
9912
  const {
@@ -9642,12 +9915,14 @@ async function markExclusionFindingHandled(opts) {
9642
9915
  excludedModel,
9643
9916
  resolution,
9644
9917
  resolutionNote,
9645
- brainEndpoint,
9646
- brainJwt,
9647
- brainAnonKey,
9648
9918
  fetch: injectedFetch
9649
9919
  } = opts;
9650
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;
9651
9926
  if (!archetype) return { ok: false, reason: "archetype_required" };
9652
9927
  if (!excludedModel) {
9653
9928
  return { ok: false, reason: "excluded_model_required" };
@@ -9914,6 +10189,7 @@ function compile2(ir, opts) {
9914
10189
  ALL_ARCHETYPES,
9915
10190
  ARCHETYPE_FAMILY_FITS,
9916
10191
  ARCHETYPE_FLOOR_DEFAULT,
10192
+ BRAIN_READ_ENV_NAMES,
9917
10193
  COACH_CFG,
9918
10194
  CallError,
9919
10195
  DECOMPOSITION_TEMPLATES,
@@ -9932,6 +10208,7 @@ function compile2(ir, opts) {
9932
10208
  MEASURED_GROUNDING_MIN_N,
9933
10209
  PRODUCER_OWNED_RULE_CODES,
9934
10210
  PROVIDER_ENV_KEYS,
10211
+ ROLLBACK_SUPPRESSION_WINDOW_DAYS,
9935
10212
  RULE_DISCIPLINE_GATES_V1,
9936
10213
  RULE_DISCIPLINE_GATES_V1_STRUCTURED,
9937
10214
  RULE_SEQUENTIAL_TOOL_CLIFF,
@@ -9985,12 +10262,14 @@ function compile2(ir, opts) {
9985
10262
  getPerAxisMetrics,
9986
10263
  getProfile,
9987
10264
  getReachabilityDiagnostic,
10265
+ getRecentRollback,
9988
10266
  getRecommendedPrimary,
9989
10267
  getSequentialStarterChain,
9990
10268
  getSequentialStarterChainWithGrounding,
9991
10269
  getStaleExclusionFindings,
9992
10270
  getStarterChain,
9993
10271
  getStarterChainWithGrounding,
10272
+ hasMutation,
9994
10273
  hashShape,
9995
10274
  isArchetype,
9996
10275
  isAutoPromoteEnabledFromEnv,
@@ -10011,9 +10290,11 @@ function compile2(ir, opts) {
10011
10290
  loadChainsFromBrain,
10012
10291
  loadModelsFromBrain,
10013
10292
  loadPricingFromBrain,
10293
+ mapMeasuredFailureRows,
10014
10294
  markAdvisoryResolved,
10015
10295
  markExclusionFindingHandled,
10016
10296
  markPromoteReadyHandled,
10297
+ mutationId,
10017
10298
  parseGoldenCaptureRate,
10018
10299
  parseJudgeVerdict,
10019
10300
  peekBrainDeadLetter,