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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -16,10 +16,11 @@ import {
16
16
  import {
17
17
  LIBRARY_VERSION,
18
18
  createKeyHealthRoute
19
- } from "./chunk-WZZCW6NA.mjs";
19
+ } from "./chunk-QVD2QWST.mjs";
20
20
  import {
21
21
  ABSOLUTE_FLOOR,
22
22
  ARCHETYPE_FLOOR_DEFAULT,
23
+ BRAIN_READ_ENV_NAMES,
23
24
  PROVIDER_ENV_KEYS,
24
25
  configureBrainQuery,
25
26
  createBrainQueryCache,
@@ -42,7 +43,7 @@ import {
42
43
  loadChainsFromBrain,
43
44
  readBrainReadEnv,
44
45
  resolveProviderKey
45
- } from "./chunk-46U2NVOL.mjs";
46
+ } from "./chunk-FT2FN6ZP.mjs";
46
47
  import {
47
48
  ALIASES,
48
49
  LATENCY_TIER_MS,
@@ -53,7 +54,7 @@ import {
53
54
  latencyTierOf,
54
55
  profilesByProvider,
55
56
  tryGetProfile
56
- } from "./chunk-N36LE3MK.mjs";
57
+ } from "./chunk-VVRDFE6T.mjs";
57
58
  import {
58
59
  emitAdvisoryFired,
59
60
  emitCompileDone,
@@ -820,11 +821,18 @@ function passScoreTargets(ir, opts) {
820
821
  }
821
822
  }
822
823
  let qualityGatePenalty = 0;
824
+ let structuredCliffGate;
823
825
  if (constraints.structuredOutput) {
824
826
  const schemaWeak = effectiveConventions(profile).some(
825
827
  (c) => c.archetype === ir.intent.archetype && c.structuredOutputHint === "avoid"
826
828
  );
827
829
  if (schemaWeak) qualityGatePenalty = QUALITY_GATE_PENALTY;
830
+ if (!schemaWeak) {
831
+ structuredCliffGate = profile.cliffs.find(
832
+ (c) => c.action === "quality_gate_structured" && (!c.whenIntent || c.whenIntent === ir.intent.archetype) && c.metric === "input_tokens" && opts.estimatedInputTokens >= c.threshold
833
+ );
834
+ if (structuredCliffGate) qualityGatePenalty = QUALITY_GATE_PENALTY;
835
+ }
828
836
  }
829
837
  const measuredGate = opts.measuredFailureGates?.get(modelId);
830
838
  if (measuredGate) qualityGatePenalty = QUALITY_GATE_PENALTY;
@@ -889,6 +897,16 @@ function passScoreTargets(ir, opts) {
889
897
  rankAfter: rank,
890
898
  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.`
891
899
  });
900
+ } else if (structuredCliffGate) {
901
+ policyMutations.push({
902
+ id: `quality-gate-structured-cliff-${modelId}`,
903
+ source: "quality_gate",
904
+ passName: "score_targets",
905
+ rankDelta: -qualityGatePenalty,
906
+ rankBefore,
907
+ rankAfter: rank,
908
+ 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.`
909
+ });
892
910
  } else {
893
911
  policyMutations.push({
894
912
  id: `quality-gate-structured-${modelId}`,
@@ -1572,13 +1590,8 @@ function getArchetypePerfScore(modelId, archetype) {
1572
1590
  return { score, n, grounding };
1573
1591
  }
1574
1592
 
1575
- // src/promote-ready-brain.ts
1576
- function isRawPromoteReadyRow(x) {
1577
- if (!x || typeof x !== "object") return false;
1578
- const r = x;
1579
- 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";
1580
- }
1581
- function coerceNumber(v) {
1593
+ // src/measured-failure-brain.ts
1594
+ function coerceCount(v) {
1582
1595
  if (typeof v === "number") return Number.isFinite(v) ? v : null;
1583
1596
  if (typeof v === "string") {
1584
1597
  const n = Number(v);
@@ -1586,58 +1599,144 @@ function coerceNumber(v) {
1586
1599
  }
1587
1600
  return null;
1588
1601
  }
1589
- function mapRowsToFindings2(rows) {
1602
+ function isRawFailureRow(x) {
1603
+ if (!x || typeof x !== "object") return false;
1604
+ const r = x;
1605
+ return typeof r.intent_archetype === "string" && typeof r.model === "string" && (typeof r.n === "number" || typeof r.n === "string");
1606
+ }
1607
+ function mapRows(rows) {
1590
1608
  const out = [];
1591
1609
  for (const row of rows) {
1592
- if (!isRawPromoteReadyRow(row)) continue;
1593
- const sampleN = coerceNumber(row.sample_n);
1594
- const passRate = coerceNumber(row.judge_pass_rate);
1595
- const avgScore = coerceNumber(row.judge_avg_score);
1596
- if (sampleN === null || passRate === null || avgScore === null) continue;
1610
+ if (!isRawFailureRow(row)) continue;
1611
+ const n = coerceCount(row.n);
1612
+ const nFail = coerceCount(row.n_fail) ?? 0;
1613
+ if (n === null || n <= 0) continue;
1597
1614
  out.push({
1598
1615
  archetype: row.intent_archetype,
1599
- family: row.family,
1600
- candidateModel: row.candidate_model,
1601
- currentModel: row.current_model,
1602
- sampleN,
1603
- judgePassRate: passRate,
1604
- judgeAvgScore: avgScore,
1605
- costDeltaPct: coerceNumber(row.cost_delta_pct),
1606
- detectedAt: row.detected_at
1616
+ model: row.model,
1617
+ n,
1618
+ nFail
1607
1619
  });
1608
1620
  }
1609
1621
  return out;
1610
1622
  }
1623
+ var MEASURED_FAILURE_CFG = {
1624
+ /**
1625
+ * Hard minimum attempts before ANY gate may be created. Guards against
1626
+ * pathological tiny samples that the confidence bound alone would let
1627
+ * through in edge cases. At 5-for-5 the bound clears the threshold; at
1628
+ * 3-for-3 it does not, which is the behaviour we want (three failures is
1629
+ * a bad day, five in a row is a pattern).
1630
+ */
1631
+ minSample: 5,
1632
+ /**
1633
+ * Gate when we are 95% confident the model fails MORE OFTEN THAN IT
1634
+ * SUCCEEDS on this surface. Deliberately unarguable rather than tuned —
1635
+ * a model that probably fails the majority of the time has no business
1636
+ * leading a surface, whatever its declared scores say.
1637
+ */
1638
+ lowerBoundThreshold: 0.5,
1639
+ /** 95% one-sided-ish confidence (standard two-sided z at α=0.05). */
1640
+ z: 1.96,
1641
+ /** Must match the view's window. Documented here for the advisory text. */
1642
+ windowDays: 28
1643
+ };
1644
+ function wilsonLowerBound(failures, n, z = MEASURED_FAILURE_CFG.z) {
1645
+ if (n <= 0) return 0;
1646
+ const p = failures / n;
1647
+ const z2 = z * z;
1648
+ const denom = 1 + z2 / n;
1649
+ const centre = p + z2 / (2 * n);
1650
+ const margin = z * Math.sqrt(p * (1 - p) / n + z2 / (4 * n * n));
1651
+ const lower2 = (centre - margin) / denom;
1652
+ return lower2 < 0 ? 0 : lower2;
1653
+ }
1654
+ function mapMeasuredFailureRows(rows) {
1655
+ return mapRows(rows);
1656
+ }
1657
+ function judgeMeasuredFailure(row, cfg = MEASURED_FAILURE_CFG) {
1658
+ if (!row) return void 0;
1659
+ const normalized = "nFail" in row && typeof row.n === "number" ? row : mapRows([row])[0];
1660
+ if (!normalized || normalized.n < cfg.minSample) return void 0;
1661
+ const lowerBound = wilsonLowerBound(normalized.nFail, normalized.n, cfg.z);
1662
+ return {
1663
+ gated: lowerBound > cfg.lowerBoundThreshold,
1664
+ rate: normalized.nFail / normalized.n,
1665
+ lowerBound,
1666
+ n: normalized.n,
1667
+ nFail: normalized.nFail
1668
+ };
1669
+ }
1611
1670
  var snapshots2 = /* @__PURE__ */ new Map();
1612
1671
  var runtime2;
1613
1672
  var warnedOnce2 = false;
1614
- function isPromoteReadyBrainActive() {
1673
+ var DEFAULT_MEASURED_FAILURE_ENDPOINT = "https://kgauto-dashboard.vercel.app/api/kgauto-v2/measured-failure";
1674
+ function isMeasuredFailureGateEnabledFromEnv(envSource) {
1675
+ const env = envSource ?? (typeof process !== "undefined" && process.env ? process.env : {});
1676
+ const raw = (env.KGAUTO_MEASURED_FAILURE_GATE ?? "").trim().toLowerCase();
1677
+ return !(raw === "0" || raw === "false");
1678
+ }
1679
+ function configureMeasuredFailureBrain(rt) {
1680
+ runtime2 = rt;
1681
+ snapshots2.clear();
1682
+ warnedOnce2 = false;
1683
+ }
1684
+ function isMeasuredFailureBrainActive() {
1615
1685
  return runtime2 !== void 0;
1616
1686
  }
1617
- function loadPromoteReadyFindings(opts) {
1687
+ function prefetchMeasuredFailure(appId) {
1618
1688
  const rt = runtime2;
1619
- if (!rt) return [];
1620
- const appId = opts.appId;
1621
- if (!appId) return [];
1689
+ if (!rt || !appId) return void 0;
1690
+ let snap = snapshots2.get(appId);
1691
+ if (!snap) {
1692
+ snap = { data: [], expiresAt: 0, refreshing: false };
1693
+ snapshots2.set(appId, snap);
1694
+ }
1695
+ if (snap.expiresAt > Date.now()) return void 0;
1696
+ const inflight = pendingRefreshes2.get(appId);
1697
+ if (inflight) return inflight;
1698
+ if (snap.refreshing) return void 0;
1699
+ snap.refreshing = true;
1700
+ void asyncRefresh2(rt, appId);
1701
+ return pendingRefreshes2.get(appId);
1702
+ }
1703
+ async function awaitMeasuredFailureReady(appId, timeoutMs) {
1704
+ if (!runtime2 || !appId) return;
1705
+ const pending = prefetchMeasuredFailure(appId) ?? pendingRefreshes2.get(appId);
1706
+ if (!(timeoutMs > 0)) return;
1707
+ if (!pending) return;
1708
+ let timer;
1709
+ try {
1710
+ await Promise.race([
1711
+ pending,
1712
+ new Promise((resolve) => {
1713
+ timer = setTimeout(resolve, timeoutMs);
1714
+ })
1715
+ ]);
1716
+ } catch {
1717
+ } finally {
1718
+ if (timer) clearTimeout(timer);
1719
+ }
1720
+ }
1721
+ function getMeasuredFailureVerdict(opts) {
1722
+ const rt = runtime2;
1723
+ if (!rt) return void 0;
1724
+ const { appId, archetype, model } = opts;
1725
+ if (!appId || !archetype || !model) return void 0;
1622
1726
  let snap = snapshots2.get(appId);
1623
1727
  if (!snap) {
1624
1728
  snap = { data: [], expiresAt: 0, refreshing: false };
1625
1729
  snapshots2.set(appId, snap);
1626
1730
  }
1627
1731
  const now = Date.now();
1628
- const stale = snap.expiresAt <= now;
1629
- if (stale && !snap.refreshing) {
1732
+ if (snap.expiresAt <= now && !snap.refreshing) {
1630
1733
  snap.refreshing = true;
1631
1734
  void asyncRefresh2(rt, appId);
1632
1735
  }
1633
- let rows = snap.data;
1634
- if (opts.archetype) {
1635
- rows = rows.filter((f) => f.archetype === opts.archetype);
1636
- }
1637
- if (opts.family) {
1638
- rows = rows.filter((f) => f.family === opts.family);
1639
- }
1640
- return rows;
1736
+ const row = snap.data.find(
1737
+ (r) => r.archetype === archetype && r.model === model
1738
+ );
1739
+ return judgeMeasuredFailure(row);
1641
1740
  }
1642
1741
  var pendingRefreshes2 = /* @__PURE__ */ new Map();
1643
1742
  async function asyncRefresh2(rt, appId) {
@@ -1661,12 +1760,11 @@ async function doRefresh2(rt, appId) {
1661
1760
  try {
1662
1761
  const res = await rt.fetchImpl(url, { method: "GET" });
1663
1762
  if (!res.ok) {
1664
- throw new Error(`promote-ready ${res.status}: ${res.statusText}`);
1763
+ throw new Error(`measured-failure ${res.status}: ${res.statusText}`);
1665
1764
  }
1666
1765
  const body = await res.json();
1667
1766
  if (runtime2 !== rt) return;
1668
- const rows = Array.isArray(body) ? mapRowsToFindings2(body) : [];
1669
- snap.data = rows;
1767
+ snap.data = Array.isArray(body) ? mapRows(body) : [];
1670
1768
  snap.expiresAt = Date.now() + rt.ttlMs;
1671
1769
  snap.refreshing = false;
1672
1770
  } catch (err) {
@@ -1681,193 +1779,84 @@ async function doRefresh2(rt, appId) {
1681
1779
  }
1682
1780
  function defaultOnError2(err) {
1683
1781
  console.warn(
1684
- "[kgauto] promote-ready fetch failed (using empty fallback):",
1782
+ "[kgauto] measured-failure fetch failed (gate inactive until next refresh):",
1685
1783
  err
1686
1784
  );
1687
1785
  }
1688
- function resolveFetchImpl(injected) {
1689
- return injected ?? ((...args) => globalThis.fetch(...args));
1690
- }
1691
- function normalizeEndpoint(endpoint) {
1692
- return endpoint.replace(/\/+$/, "");
1786
+ function _testResetMeasuredFailure() {
1787
+ runtime2 = void 0;
1788
+ snapshots2.clear();
1789
+ pendingRefreshes2 = /* @__PURE__ */ new Map();
1790
+ warnedOnce2 = false;
1693
1791
  }
1694
- async function markPromoteReadyHandled(opts) {
1695
- const {
1696
- appId,
1697
- archetype,
1698
- family,
1699
- resolution,
1700
- resolutionNote,
1701
- brainEndpoint,
1702
- brainJwt,
1703
- brainAnonKey,
1704
- fetch: injectedFetch
1705
- } = opts;
1706
- if (!appId) return { ok: false, reason: "app_id_required" };
1707
- if (!archetype) return { ok: false, reason: "archetype_required" };
1708
- if (!family) return { ok: false, reason: "family_required" };
1709
- if (resolution !== "promoted" && resolution !== "declined" && resolution !== "still-evaluating") {
1710
- return { ok: false, reason: "resolution_invalid" };
1711
- }
1712
- const doFetch = resolveFetchImpl(injectedFetch);
1713
- const base = normalizeEndpoint(brainEndpoint);
1714
- 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`;
1715
- const patchBody = {
1716
- resolved_at: (/* @__PURE__ */ new Date()).toISOString(),
1717
- resolution
1718
- };
1719
- if (resolutionNote !== void 0) {
1720
- patchBody.resolution_note = resolutionNote;
1721
- }
1722
- let res;
1723
- try {
1724
- res = await doFetch(url, {
1725
- method: "PATCH",
1726
- headers: {
1727
- Authorization: `Bearer ${brainJwt}`,
1728
- apikey: brainAnonKey,
1729
- "Content-Type": "application/json",
1730
- Accept: "application/json",
1731
- Prefer: "return=minimal"
1732
- },
1733
- body: JSON.stringify(patchBody)
1734
- });
1735
- } catch (err) {
1736
- const msg = err instanceof Error ? err.message : String(err);
1737
- return { ok: false, reason: `network_error:${msg}` };
1738
- }
1739
- if (res.status === 401 || res.status === 403) {
1740
- return { ok: false, reason: "brain_auth_misconfig" };
1741
- }
1742
- if (res.status >= 500) {
1743
- return { ok: false, reason: "brain_unavailable" };
1744
- }
1745
- if (!res.ok) {
1746
- return { ok: false, reason: `patch_failed:${res.status}` };
1747
- }
1748
- return { ok: true };
1792
+ async function _testWaitForMeasuredFailureRefresh() {
1793
+ const pending = Array.from(pendingRefreshes2.values());
1794
+ if (pending.length > 0) await Promise.all(pending);
1749
1795
  }
1750
1796
 
1751
- // src/advisor-rules/promote-ready.ts
1752
- var PROMOTE_READY_THRESHOLDS = {
1753
- minPassRate: 0.8,
1754
- minAvgScore: 4
1755
- };
1756
- function shouldFirePromoteReady(finding, resolvedPrimary) {
1757
- if (finding.currentModel !== resolvedPrimary) return false;
1758
- if (finding.judgePassRate < PROMOTE_READY_THRESHOLDS.minPassRate) return false;
1759
- if (finding.judgeAvgScore < PROMOTE_READY_THRESHOLDS.minAvgScore) return false;
1760
- return true;
1797
+ // src/promotions-brain.ts
1798
+ function isRawPromotionRow(x) {
1799
+ if (!x || typeof x !== "object") return false;
1800
+ const r = x;
1801
+ return (typeof r.id === "number" || typeof r.id === "string") && typeof r.intent_archetype === "string" && typeof r.promoted_model === "string" && typeof r.incumbent_model === "string";
1761
1802
  }
1762
- function deriveFamilyLocal(modelId) {
1763
- if (modelId.startsWith("claude-opus-")) return "claude-opus";
1764
- if (modelId.startsWith("claude-sonnet-")) return "claude-sonnet";
1765
- if (modelId.startsWith("claude-haiku-")) return "claude-haiku";
1766
- if (/^gemini-.*-flash-lite/.test(modelId)) return "gemini-flash-lite";
1767
- if (/^gemini-.*-flash/.test(modelId)) return "gemini-flash";
1768
- if (/^gemini-.*-pro/.test(modelId)) return "gemini-pro";
1769
- if (/^deepseek-.*-pro/.test(modelId)) return "deepseek-reasoner";
1770
- if (modelId.startsWith("deepseek-")) return "deepseek-chat";
1771
- if (modelId.startsWith("gpt-")) return "openai-gpt";
1803
+ function coerceId(v) {
1804
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
1805
+ if (typeof v === "string") {
1806
+ const n = Number(v);
1807
+ return Number.isFinite(n) ? n : null;
1808
+ }
1772
1809
  return null;
1773
1810
  }
1774
- function advisorRulePromoteReady(ctx) {
1775
- if (!isPromoteReadyBrainActive()) return [];
1776
- if (!ctx.appId) return [];
1777
- if (!ctx.resolvedPrimary) return [];
1778
- const family = deriveFamilyLocal(ctx.resolvedPrimary);
1779
- if (!family) return [];
1780
- const findings = loadPromoteReadyFindings({
1781
- appId: ctx.appId,
1782
- archetype: ctx.archetype,
1783
- family
1784
- });
1785
- if (findings.length === 0) return [];
1786
- const qualifying = findings.filter(
1787
- (f) => shouldFirePromoteReady(f, ctx.resolvedPrimary)
1788
- );
1789
- if (qualifying.length === 0) return [];
1790
- qualifying.sort((a, b) => {
1791
- if (a.judgeAvgScore !== b.judgeAvgScore) {
1792
- return b.judgeAvgScore - a.judgeAvgScore;
1793
- }
1794
- return b.judgePassRate - a.judgePassRate;
1795
- });
1796
- const top = qualifying[0];
1797
- const pctPass = Math.round(top.judgePassRate * 100);
1798
- const score = top.judgeAvgScore.toFixed(2);
1799
- let costClause = "";
1800
- if (top.costDeltaPct !== null) {
1801
- const sign = top.costDeltaPct < 0 ? "cheaper" : "more expensive";
1802
- const magnitude = Math.abs(top.costDeltaPct * 100).toFixed(1);
1803
- costClause = `, cost ${magnitude}% ${sign}`;
1804
- }
1805
- 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.`;
1806
- return [
1807
- {
1808
- level: "info",
1809
- code: "promote-ready",
1810
- message,
1811
- 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.`,
1812
- // alpha.36 architectural field — not a no-ai-needed case.
1813
- recommendedArchitecture: void 0,
1814
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1815
- }
1816
- ];
1817
- }
1818
-
1819
- // src/advisor-rules/consumer-on-stale-model.ts
1820
- function isStaleStatus(v) {
1821
- return v === "legacy" || v === "deprecated";
1822
- }
1823
- function asString(v) {
1824
- return typeof v === "string" && v.length > 0 ? v : void 0;
1825
- }
1826
- function mapRowsToFindings3(rows) {
1811
+ function mapRowsToPromotions(rows) {
1827
1812
  const out = [];
1828
- for (const raw of rows) {
1829
- if (!raw || typeof raw !== "object") continue;
1830
- const r = raw;
1831
- const archetype = asString(r.intent_archetype) ?? asString(r.applies_to_archetype);
1832
- const staleModel = asString(r.stale_model) ?? asString(r.applies_to_model);
1833
- const staleProvider = asString(r.stale_provider);
1834
- const recommendedModel = asString(r.recommended_model);
1835
- const family = asString(r.family);
1836
- const message = asString(r.message);
1837
- if (!archetype || !staleModel || !recommendedModel || !family || !message) {
1838
- continue;
1839
- }
1840
- if (!isStaleStatus(r.stale_status)) continue;
1841
- const row = {
1842
- archetype,
1843
- staleModel,
1844
- staleProvider: staleProvider ?? "unknown",
1845
- staleStatus: r.stale_status,
1846
- recommendedModel,
1847
- family,
1848
- message
1849
- };
1850
- const suggestion = asString(r.suggestion);
1851
- if (suggestion) row.suggestion = suggestion;
1852
- if (typeof r.observation_count === "number" && Number.isFinite(r.observation_count)) {
1853
- row.observationCount = r.observation_count;
1854
- }
1855
- out.push(row);
1813
+ for (const row of rows) {
1814
+ if (!isRawPromotionRow(row)) continue;
1815
+ const id = coerceId(row.id);
1816
+ if (id === null) continue;
1817
+ const mode = row.mode === "strategy" ? "strategy" : row.mode === "downswap" || row.mode === void 0 ? "downswap" : null;
1818
+ if (mode === null) continue;
1819
+ out.push({
1820
+ id,
1821
+ archetype: row.intent_archetype,
1822
+ mode,
1823
+ strategy: typeof row.strategy === "string" ? row.strategy : null,
1824
+ promotedModel: row.promoted_model,
1825
+ incumbentModel: row.incumbent_model,
1826
+ evalRunId: coerceId(row.eval_run_id ?? null),
1827
+ suppressQualityGate: row.suppress_quality_gate === true,
1828
+ promotedAt: typeof row.promoted_at === "string" ? row.promoted_at : "",
1829
+ // Pre-.78 endpoints serve no status column and only active rows —
1830
+ // defaulting to 'active' is exact, not optimistic.
1831
+ status: row.status === "rolled_back" ? "rolled_back" : "active",
1832
+ ...typeof row.rolled_back_at === "string" ? { rolledBackAt: row.rolled_back_at } : {},
1833
+ ...typeof row.rollback_class === "string" ? { rollbackClass: row.rollback_class } : {}
1834
+ });
1856
1835
  }
1857
1836
  return out;
1858
1837
  }
1859
1838
  var snapshots3 = /* @__PURE__ */ new Map();
1860
1839
  var runtime3;
1861
1840
  var warnedOnce3 = false;
1862
- var pendingRefreshes3 = /* @__PURE__ */ new Map();
1863
- function isStaleModelFindingsBrainActive() {
1841
+ var DEFAULT_PROMOTIONS_ENDPOINT = "https://kgauto-dashboard.vercel.app/api/kgauto-v2/promotions";
1842
+ function isAutoPromoteEnabledFromEnv(envSource) {
1843
+ const env = envSource ?? (typeof process !== "undefined" && process.env ? process.env : {});
1844
+ const raw = (env.KGAUTO_AUTO_PROMOTE ?? "").trim().toLowerCase();
1845
+ return raw === "1" || raw === "true";
1846
+ }
1847
+ function configurePromotionsBrain(rt) {
1848
+ runtime3 = rt;
1849
+ snapshots3.clear();
1850
+ warnedOnce3 = false;
1851
+ }
1852
+ function isPromotionsBrainActive() {
1864
1853
  return runtime3 !== void 0;
1865
1854
  }
1866
- function getStaleModelFindings(opts) {
1855
+ function getApplicablePromotion(opts) {
1867
1856
  const rt = runtime3;
1868
- if (!rt) return [];
1857
+ if (!rt) return void 0;
1869
1858
  const appId = opts.appId;
1870
- if (!appId) return [];
1859
+ if (!appId || !opts.archetype || !opts.mode) return void 0;
1871
1860
  let snap = snapshots3.get(appId);
1872
1861
  if (!snap) {
1873
1862
  snap = { data: [], expiresAt: 0, refreshing: false };
@@ -1879,11 +1868,31 @@ function getStaleModelFindings(opts) {
1879
1868
  snap.refreshing = true;
1880
1869
  void asyncRefresh3(rt, appId);
1881
1870
  }
1882
- if (opts.archetype) {
1883
- return snap.data.filter((f) => f.archetype === opts.archetype);
1871
+ return snap.data.find(
1872
+ (p) => p.status === "active" && p.archetype === opts.archetype && p.mode === opts.mode
1873
+ );
1874
+ }
1875
+ var ROLLBACK_SUPPRESSION_WINDOW_DAYS = 28;
1876
+ function getRecentRollback(opts) {
1877
+ const rt = runtime3;
1878
+ if (!rt) return void 0;
1879
+ if (!opts.appId || !opts.archetype || !opts.model) return void 0;
1880
+ let snap = snapshots3.get(opts.appId);
1881
+ if (!snap) {
1882
+ snap = { data: [], expiresAt: 0, refreshing: false };
1883
+ snapshots3.set(opts.appId, snap);
1884
1884
  }
1885
- return snap.data;
1885
+ const now = opts.nowMs ?? Date.now();
1886
+ if (snap.expiresAt <= now && !snap.refreshing) {
1887
+ snap.refreshing = true;
1888
+ void asyncRefresh3(rt, opts.appId);
1889
+ }
1890
+ const windowMs = (opts.windowDays ?? ROLLBACK_SUPPRESSION_WINDOW_DAYS) * 864e5;
1891
+ return snap.data.find(
1892
+ (p) => p.status === "rolled_back" && p.archetype === opts.archetype && p.promotedModel === opts.model && typeof p.rolledBackAt === "string" && now - Date.parse(p.rolledBackAt) <= windowMs
1893
+ );
1886
1894
  }
1895
+ var pendingRefreshes3 = /* @__PURE__ */ new Map();
1887
1896
  async function asyncRefresh3(rt, appId) {
1888
1897
  const promise = doRefresh3(rt, appId);
1889
1898
  pendingRefreshes3.set(appId, promise);
@@ -1896,7 +1905,7 @@ async function asyncRefresh3(rt, appId) {
1896
1905
  }
1897
1906
  }
1898
1907
  async function doRefresh3(rt, appId) {
1899
- const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
1908
+ const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}&with_rollbacks=1`;
1900
1909
  let snap = snapshots3.get(appId);
1901
1910
  if (!snap) {
1902
1911
  snap = { data: [], expiresAt: 0, refreshing: false };
@@ -1905,11 +1914,11 @@ async function doRefresh3(rt, appId) {
1905
1914
  try {
1906
1915
  const res = await rt.fetchImpl(url, { method: "GET" });
1907
1916
  if (!res.ok) {
1908
- throw new Error(`stale-model findings ${res.status}: ${res.statusText}`);
1917
+ throw new Error(`promotions ${res.status}: ${res.statusText}`);
1909
1918
  }
1910
1919
  const body = await res.json();
1911
1920
  if (runtime3 !== rt) return;
1912
- const rows = Array.isArray(body) ? mapRowsToFindings3(body) : [];
1921
+ const rows = Array.isArray(body) ? mapRowsToPromotions(body) : [];
1913
1922
  snap.data = rows;
1914
1923
  snap.expiresAt = Date.now() + rt.ttlMs;
1915
1924
  snap.refreshing = false;
@@ -1925,847 +1934,905 @@ async function doRefresh3(rt, appId) {
1925
1934
  }
1926
1935
  function defaultOnError3(err) {
1927
1936
  console.warn(
1928
- "[kgauto] stale-model findings fetch failed (using empty fallback):",
1937
+ "[kgauto] promotions fetch failed (promotion boost inactive until next refresh):",
1929
1938
  err
1930
1939
  );
1931
1940
  }
1932
- var CONSUMER_ON_STALE_MODEL_RULE_CODE = "consumer-on-stale-model";
1933
- function advisorRuleConsumerOnStaleModel(ir) {
1934
- if (!isStaleModelFindingsBrainActive()) return [];
1935
- if (!ir.appId) return [];
1936
- const findings = getStaleModelFindings({
1937
- appId: ir.appId,
1938
- archetype: ir.intent.archetype
1939
- });
1940
- if (findings.length === 0) return [];
1941
- const ranked = [...findings].sort((a, b) => {
1942
- if (a.staleStatus !== b.staleStatus) {
1943
- return a.staleStatus === "deprecated" ? -1 : 1;
1944
- }
1945
- return a.staleModel.localeCompare(b.staleModel);
1946
- });
1947
- const top = ranked[0];
1948
- const extraCount = findings.length - 1;
1949
- const extraNote = extraCount > 0 ? ` (+ ${extraCount} more stale model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
1950
- return [
1951
- {
1952
- level: "warn",
1953
- code: CONSUMER_ON_STALE_MODEL_RULE_CODE,
1954
- message: `${top.message}${extraNote}`,
1955
- 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}.`,
1956
- recommendationType: "model-swap",
1957
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1958
- }
1959
- ];
1941
+ function _testResetPromotions() {
1942
+ runtime3 = void 0;
1943
+ snapshots3.clear();
1944
+ pendingRefreshes3 = /* @__PURE__ */ new Map();
1945
+ warnedOnce3 = false;
1946
+ }
1947
+ async function _testWaitForPromotionsRefresh() {
1948
+ const pending = Array.from(pendingRefreshes3.values());
1949
+ if (pending.length > 0) await Promise.all(pending);
1960
1950
  }
1961
1951
 
1962
- // src/archetype-fits.ts
1963
- var ARCHETYPE_FAMILY_FITS = Object.freeze([
1964
- {
1965
- archetype: "plan",
1966
- betterFitFamily: "deepseek-reasoner",
1967
- 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).",
1968
- costGuidance: "substantially cheaper at current pricing (deepseek-v4-pro promo: ~7-17x cheaper than sonnet)"
1969
- },
1970
- {
1971
- archetype: "critique",
1972
- betterFitFamily: "deepseek-reasoner",
1973
- 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.",
1974
- costGuidance: "comparable or cheaper at current pricing"
1975
- }
1976
- ]);
1977
- function findBetterFit(archetype, currentFamily) {
1978
- for (const fit of ARCHETYPE_FAMILY_FITS) {
1979
- if (fit.archetype !== archetype) continue;
1980
- if (fit.betterFitFamily === currentFamily) return null;
1981
- return fit;
1952
+ // src/promote-ready-brain.ts
1953
+ function isRawPromoteReadyRow(x) {
1954
+ if (!x || typeof x !== "object") return false;
1955
+ const r = x;
1956
+ 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";
1957
+ }
1958
+ function coerceNumber(v) {
1959
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
1960
+ if (typeof v === "string") {
1961
+ const n = Number(v);
1962
+ return Number.isFinite(n) ? n : null;
1982
1963
  }
1983
1964
  return null;
1984
1965
  }
1985
-
1986
- // src/advisor-rules/cross-family-fit.ts
1987
- function familyHasCurrentActiveModel(family) {
1988
- for (const profile of allProfiles()) {
1989
- const profileFamily = profile.family ?? deriveFamilyFromModelId(profile.id);
1990
- if (profileFamily !== family) continue;
1991
- if (profile.status !== "current") continue;
1992
- if (profile.active === false) continue;
1993
- return true;
1966
+ function mapRowsToFindings2(rows) {
1967
+ const out = [];
1968
+ for (const row of rows) {
1969
+ if (!isRawPromoteReadyRow(row)) continue;
1970
+ const sampleN = coerceNumber(row.sample_n);
1971
+ const passRate = coerceNumber(row.judge_pass_rate);
1972
+ const avgScore = coerceNumber(row.judge_avg_score);
1973
+ if (sampleN === null || passRate === null || avgScore === null) continue;
1974
+ out.push({
1975
+ archetype: row.intent_archetype,
1976
+ family: row.family,
1977
+ candidateModel: row.candidate_model,
1978
+ currentModel: row.current_model,
1979
+ sampleN,
1980
+ judgePassRate: passRate,
1981
+ judgeAvgScore: avgScore,
1982
+ costDeltaPct: coerceNumber(row.cost_delta_pct),
1983
+ detectedAt: row.detected_at
1984
+ });
1994
1985
  }
1995
- return false;
1986
+ return out;
1996
1987
  }
1997
- function listCandidatesInFamily(family) {
1998
- const candidates = [];
1999
- for (const profile of allProfiles()) {
2000
- const profileFamily = profile.family ?? deriveFamilyFromModelId(profile.id);
2001
- if (profileFamily !== family) continue;
2002
- if (profile.status !== "current") continue;
2003
- if (profile.active === false) continue;
2004
- candidates.push(profile.id);
2005
- if (candidates.length >= 3) break;
2006
- }
2007
- return candidates;
1988
+ var snapshots4 = /* @__PURE__ */ new Map();
1989
+ var runtime4;
1990
+ var warnedOnce4 = false;
1991
+ function isPromoteReadyBrainActive() {
1992
+ return runtime4 !== void 0;
2008
1993
  }
2009
- function advisorRuleCrossFamilyFit(ctx) {
2010
- if (!ctx.resolvedPrimary) return [];
2011
- const currentFamily = deriveFamilyFromModelId(ctx.resolvedPrimary);
2012
- if (!currentFamily) return [];
2013
- const fit = findBetterFit(ctx.archetype, currentFamily);
2014
- if (!fit) return [];
2015
- if (!familyHasCurrentActiveModel(fit.betterFitFamily)) return [];
2016
- const candidates = listCandidatesInFamily(fit.betterFitFamily);
2017
- if (candidates.length === 0) return [];
2018
- const candidateStr = candidates.join(", ");
2019
- const message = `Your ${currentFamily} call on ${ctx.archetype} could shift to ${fit.betterFitFamily} \u2014 typically better quality + ${fit.costGuidance}. Suggested candidates: ${candidateStr}.`;
2020
- return [
2021
- {
2022
- level: "info",
2023
- code: "cross-family-fit-candidate",
2024
- ownership: "consumer-actionable",
2025
- message,
2026
- 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.`,
2027
- recommendationType: "model-swap",
2028
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1994
+ function loadPromoteReadyFindings(opts) {
1995
+ const rt = runtime4;
1996
+ if (!rt) return [];
1997
+ const appId = opts.appId;
1998
+ if (!appId) return [];
1999
+ let snap = snapshots4.get(appId);
2000
+ if (!snap) {
2001
+ snap = { data: [], expiresAt: 0, refreshing: false };
2002
+ snapshots4.set(appId, snap);
2003
+ }
2004
+ const now = Date.now();
2005
+ const stale = snap.expiresAt <= now;
2006
+ if (stale && !snap.refreshing) {
2007
+ snap.refreshing = true;
2008
+ void asyncRefresh4(rt, appId);
2009
+ }
2010
+ let rows = snap.data;
2011
+ if (opts.archetype) {
2012
+ rows = rows.filter((f) => f.archetype === opts.archetype);
2013
+ }
2014
+ if (opts.family) {
2015
+ rows = rows.filter((f) => f.family === opts.family);
2016
+ }
2017
+ return rows;
2018
+ }
2019
+ var pendingRefreshes4 = /* @__PURE__ */ new Map();
2020
+ async function asyncRefresh4(rt, appId) {
2021
+ const promise = doRefresh4(rt, appId);
2022
+ pendingRefreshes4.set(appId, promise);
2023
+ try {
2024
+ await promise;
2025
+ } finally {
2026
+ if (pendingRefreshes4.get(appId) === promise) {
2027
+ pendingRefreshes4.delete(appId);
2029
2028
  }
2030
- ];
2029
+ }
2031
2030
  }
2032
-
2033
- // src/advisor.ts
2034
- var QUALITY_FLOOR_FOR_RECOMMENDATION = 6;
2035
- var TIER_DOWN_COST_RATIO = 0.5;
2036
- var COST_MISMATCHED_CHOSEN_SCORE_CEILING = 7;
2037
- var PRODUCER_OWNED_RULE_CODES = Object.freeze(
2038
- /* @__PURE__ */ new Set(["model-stale-evidence", "promote-ready"])
2039
- );
2040
- function deriveOwnership(code, selfDeclared) {
2041
- if (selfDeclared) return selfDeclared;
2042
- return PRODUCER_OWNED_RULE_CODES.has(code) ? "producer-owned" : "consumer-actionable";
2031
+ async function doRefresh4(rt, appId) {
2032
+ const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
2033
+ let snap = snapshots4.get(appId);
2034
+ if (!snap) {
2035
+ snap = { data: [], expiresAt: 0, refreshing: false };
2036
+ snapshots4.set(appId, snap);
2037
+ }
2038
+ try {
2039
+ const res = await rt.fetchImpl(url, { method: "GET" });
2040
+ if (!res.ok) {
2041
+ throw new Error(`promote-ready ${res.status}: ${res.statusText}`);
2042
+ }
2043
+ const body = await res.json();
2044
+ if (runtime4 !== rt) return;
2045
+ const rows = Array.isArray(body) ? mapRowsToFindings2(body) : [];
2046
+ snap.data = rows;
2047
+ snap.expiresAt = Date.now() + rt.ttlMs;
2048
+ snap.refreshing = false;
2049
+ } catch (err) {
2050
+ if (runtime4 !== rt) return;
2051
+ snap.refreshing = false;
2052
+ snap.expiresAt = Date.now() + rt.ttlMs;
2053
+ if (!warnedOnce4) {
2054
+ warnedOnce4 = true;
2055
+ (rt.onError ?? defaultOnError4)(err);
2056
+ }
2057
+ }
2043
2058
  }
2044
- function runAdvisor(ir, result, profile, policy, phase2) {
2045
- const out = [];
2046
- out.push(...detectCachingOff(ir, profile));
2047
- out.push(...detectSingleChunkSystem(ir, profile));
2048
- out.push(...detectToolBloat(ir, result));
2049
- out.push(...detectHistoryUncached(ir, profile));
2050
- out.push(...detectSingleModelArray(ir, policy));
2051
- if (policy?.posture !== "locked") {
2052
- out.push(...detectCostMismatchedArchetype(ir, profile, phase2));
2053
- out.push(...detectModelStaleEvidence(ir, profile));
2054
- out.push(...detectTierDown(ir, profile, phase2));
2059
+ function defaultOnError4(err) {
2060
+ console.warn(
2061
+ "[kgauto] promote-ready fetch failed (using empty fallback):",
2062
+ err
2063
+ );
2064
+ }
2065
+ function resolveFetchImpl(injected) {
2066
+ return injected ?? ((...args) => globalThis.fetch(...args));
2067
+ }
2068
+ function normalizeEndpoint(endpoint) {
2069
+ return endpoint.replace(/\/+$/, "");
2070
+ }
2071
+ async function markPromoteReadyHandled(opts) {
2072
+ const {
2073
+ appId,
2074
+ archetype,
2075
+ family,
2076
+ resolution,
2077
+ resolutionNote,
2078
+ brainEndpoint,
2079
+ brainJwt,
2080
+ brainAnonKey,
2081
+ fetch: injectedFetch
2082
+ } = opts;
2083
+ if (!appId) return { ok: false, reason: "app_id_required" };
2084
+ if (!archetype) return { ok: false, reason: "archetype_required" };
2085
+ if (!family) return { ok: false, reason: "family_required" };
2086
+ if (resolution !== "promoted" && resolution !== "declined" && resolution !== "still-evaluating") {
2087
+ return { ok: false, reason: "resolution_invalid" };
2055
2088
  }
2056
- if (!translatorClearedToolCallCliff(phase2)) {
2057
- out.push(...detectArchetypePerfFloorBreach(ir, profile));
2089
+ const doFetch = resolveFetchImpl(injectedFetch);
2090
+ const base = normalizeEndpoint(brainEndpoint);
2091
+ 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`;
2092
+ const patchBody = {
2093
+ resolved_at: (/* @__PURE__ */ new Date()).toISOString(),
2094
+ resolution
2095
+ };
2096
+ if (resolutionNote !== void 0) {
2097
+ patchBody.resolution_note = resolutionNote;
2058
2098
  }
2059
- if (policy?.posture !== "locked") {
2060
- out.push(...detectStaleExclusionCandidate(ir));
2099
+ let res;
2100
+ try {
2101
+ res = await doFetch(url, {
2102
+ method: "PATCH",
2103
+ headers: {
2104
+ Authorization: `Bearer ${brainJwt}`,
2105
+ apikey: brainAnonKey,
2106
+ "Content-Type": "application/json",
2107
+ Accept: "application/json",
2108
+ Prefer: "return=minimal"
2109
+ },
2110
+ body: JSON.stringify(patchBody)
2111
+ });
2112
+ } catch (err) {
2113
+ const msg = err instanceof Error ? err.message : String(err);
2114
+ return { ok: false, reason: `network_error:${msg}` };
2061
2115
  }
2062
- if (policy?.posture !== "locked" && ir.appId) {
2063
- out.push(
2064
- ...advisorRulePromoteReady({
2065
- appId: ir.appId,
2066
- archetype: ir.intent.archetype,
2067
- resolvedPrimary: profile.id
2068
- })
2069
- );
2070
- out.push(...advisorRuleConsumerOnStaleModel(ir));
2116
+ if (res.status === 401 || res.status === 403) {
2117
+ return { ok: false, reason: "brain_auth_misconfig" };
2071
2118
  }
2072
- if (policy?.posture !== "locked") {
2073
- out.push(
2074
- ...advisorRuleCrossFamilyFit({
2075
- archetype: ir.intent.archetype,
2076
- resolvedPrimary: profile.id
2077
- })
2078
- );
2119
+ if (res.status >= 500) {
2120
+ return { ok: false, reason: "brain_unavailable" };
2079
2121
  }
2080
- return out;
2081
- }
2082
- function translatorClearedToolCallCliff(phase2) {
2083
- const rewrites = phase2?.sectionRewritesApplied;
2084
- if (!rewrites || rewrites.length === 0) return false;
2085
- for (const rw of rewrites) {
2086
- if (rw.kind === "tool_call_contract") return true;
2122
+ if (!res.ok) {
2123
+ return { ok: false, reason: `patch_failed:${res.status}` };
2087
2124
  }
2088
- return false;
2125
+ return { ok: true };
2089
2126
  }
2090
- function detectCachingOff(ir, profile) {
2091
- if (profile.provider !== "anthropic") return [];
2092
- const totalChars = ir.sections.reduce((s, sec) => s + sec.text.length, 0);
2093
- if (totalChars < 2e3) return [];
2094
- const anyCacheable = ir.sections.some((s) => s.cacheable === true);
2095
- if (anyCacheable) return [];
2127
+
2128
+ // src/advisor-rules/promote-ready.ts
2129
+ var PROMOTE_READY_THRESHOLDS = {
2130
+ minPassRate: 0.8,
2131
+ minAvgScore: 4
2132
+ };
2133
+ function shouldFirePromoteReady(finding, resolvedPrimary) {
2134
+ if (finding.currentModel !== resolvedPrimary) return false;
2135
+ if (finding.judgePassRate < PROMOTE_READY_THRESHOLDS.minPassRate) return false;
2136
+ if (finding.judgeAvgScore < PROMOTE_READY_THRESHOLDS.minAvgScore) return false;
2137
+ return true;
2138
+ }
2139
+ function deriveFamilyLocal(modelId) {
2140
+ if (modelId.startsWith("claude-opus-")) return "claude-opus";
2141
+ if (modelId.startsWith("claude-sonnet-")) return "claude-sonnet";
2142
+ if (modelId.startsWith("claude-haiku-")) return "claude-haiku";
2143
+ if (/^gemini-.*-flash-lite/.test(modelId)) return "gemini-flash-lite";
2144
+ if (/^gemini-.*-flash/.test(modelId)) return "gemini-flash";
2145
+ if (/^gemini-.*-pro/.test(modelId)) return "gemini-pro";
2146
+ if (/^deepseek-.*-pro/.test(modelId)) return "deepseek-reasoner";
2147
+ if (modelId.startsWith("deepseek-")) return "deepseek-chat";
2148
+ if (modelId.startsWith("gpt-")) return "openai-gpt";
2149
+ return null;
2150
+ }
2151
+ function advisorRulePromoteReady(ctx) {
2152
+ if (!isPromoteReadyBrainActive()) return [];
2153
+ if (!ctx.appId) return [];
2154
+ if (!ctx.resolvedPrimary) return [];
2155
+ const family = deriveFamilyLocal(ctx.resolvedPrimary);
2156
+ if (!family) return [];
2157
+ const findings = loadPromoteReadyFindings({
2158
+ appId: ctx.appId,
2159
+ archetype: ctx.archetype,
2160
+ family
2161
+ });
2162
+ if (findings.length === 0) return [];
2163
+ const qualifying = findings.filter(
2164
+ (f) => shouldFirePromoteReady(f, ctx.resolvedPrimary)
2165
+ );
2166
+ if (qualifying.length === 0) return [];
2167
+ qualifying.sort((a, b) => {
2168
+ if (a.judgeAvgScore !== b.judgeAvgScore) {
2169
+ return b.judgeAvgScore - a.judgeAvgScore;
2170
+ }
2171
+ return b.judgePassRate - a.judgePassRate;
2172
+ });
2173
+ const top = qualifying[0];
2174
+ const pctPass = Math.round(top.judgePassRate * 100);
2175
+ const score = top.judgeAvgScore.toFixed(2);
2176
+ let costClause = "";
2177
+ if (top.costDeltaPct !== null) {
2178
+ const sign = top.costDeltaPct < 0 ? "cheaper" : "more expensive";
2179
+ const magnitude = Math.abs(top.costDeltaPct * 100).toFixed(1);
2180
+ costClause = `, cost ${magnitude}% ${sign}`;
2181
+ }
2182
+ 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.`;
2096
2183
  return [
2097
2184
  {
2098
- level: "warn",
2099
- code: "caching-off-on-claude",
2100
- 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.`,
2101
- 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.",
2185
+ level: "info",
2186
+ code: "promote-ready",
2187
+ message,
2188
+ 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.`,
2189
+ // alpha.36 architectural field — not a no-ai-needed case.
2190
+ recommendedArchitecture: void 0,
2102
2191
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2103
2192
  }
2104
2193
  ];
2105
2194
  }
2106
- function detectSingleChunkSystem(ir, profile) {
2107
- if (profile.provider !== "anthropic") return [];
2108
- if (ir.sections.length !== 1) return [];
2109
- const only = ir.sections[0];
2110
- if (!only || only.text.length <= 1e3) return [];
2111
- return [
2112
- {
2113
- level: "info",
2114
- code: "single-chunk-system",
2115
- 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.`,
2116
- 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.",
2117
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2118
- }
2119
- ];
2195
+
2196
+ // src/advisor-rules/consumer-on-stale-model.ts
2197
+ function isStaleStatus(v) {
2198
+ return v === "legacy" || v === "deprecated";
2120
2199
  }
2121
- function detectToolBloat(ir, result) {
2122
- const SHORT_OUTPUT = /* @__PURE__ */ new Set([
2123
- "classify",
2124
- "extract",
2125
- "summarize",
2126
- "transform",
2127
- "critique"
2128
- ]);
2129
- if (!ir.tools || ir.tools.length === 0) return [];
2130
- const toolsKept = result.diagnostics.toolsKept;
2131
- if (toolsKept <= 10) return [];
2132
- if (!SHORT_OUTPUT.has(ir.intent.archetype)) return [];
2133
- return [
2134
- {
2135
- level: "warn",
2136
- code: "tool-bloat",
2137
- 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.`,
2138
- 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.",
2139
- docsUrl: "https://github.com/stue/kgauto/blob/main/v2/README.md#tools"
2200
+ function asString(v) {
2201
+ return typeof v === "string" && v.length > 0 ? v : void 0;
2202
+ }
2203
+ function mapRowsToFindings3(rows) {
2204
+ const out = [];
2205
+ for (const raw of rows) {
2206
+ if (!raw || typeof raw !== "object") continue;
2207
+ const r = raw;
2208
+ const archetype = asString(r.intent_archetype) ?? asString(r.applies_to_archetype);
2209
+ const staleModel = asString(r.stale_model) ?? asString(r.applies_to_model);
2210
+ const staleProvider = asString(r.stale_provider);
2211
+ const recommendedModel = asString(r.recommended_model);
2212
+ const family = asString(r.family);
2213
+ const message = asString(r.message);
2214
+ if (!archetype || !staleModel || !recommendedModel || !family || !message) {
2215
+ continue;
2140
2216
  }
2141
- ];
2217
+ if (!isStaleStatus(r.stale_status)) continue;
2218
+ const row = {
2219
+ archetype,
2220
+ staleModel,
2221
+ staleProvider: staleProvider ?? "unknown",
2222
+ staleStatus: r.stale_status,
2223
+ recommendedModel,
2224
+ family,
2225
+ message
2226
+ };
2227
+ const suggestion = asString(r.suggestion);
2228
+ if (suggestion) row.suggestion = suggestion;
2229
+ if (typeof r.observation_count === "number" && Number.isFinite(r.observation_count)) {
2230
+ row.observationCount = r.observation_count;
2231
+ }
2232
+ out.push(row);
2233
+ }
2234
+ return out;
2142
2235
  }
2143
- function detectHistoryUncached(ir, profile) {
2144
- if (profile.provider !== "anthropic") return [];
2145
- if (!ir.history || ir.history.length < 2) return [];
2146
- if (ir.historyCachePolicy && ir.historyCachePolicy.strategy !== "none") {
2147
- return [];
2236
+ var snapshots5 = /* @__PURE__ */ new Map();
2237
+ var runtime5;
2238
+ var warnedOnce5 = false;
2239
+ var pendingRefreshes5 = /* @__PURE__ */ new Map();
2240
+ function isStaleModelFindingsBrainActive() {
2241
+ return runtime5 !== void 0;
2242
+ }
2243
+ function getStaleModelFindings(opts) {
2244
+ const rt = runtime5;
2245
+ if (!rt) return [];
2246
+ const appId = opts.appId;
2247
+ if (!appId) return [];
2248
+ let snap = snapshots5.get(appId);
2249
+ if (!snap) {
2250
+ snap = { data: [], expiresAt: 0, refreshing: false };
2251
+ snapshots5.set(appId, snap);
2148
2252
  }
2149
- return [
2150
- {
2151
- level: "warn",
2152
- code: "history-uncached-on-claude",
2153
- 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.`,
2154
- 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.",
2155
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2156
- }
2157
- ];
2253
+ const now = Date.now();
2254
+ const stale = snap.expiresAt <= now;
2255
+ if (stale && !snap.refreshing) {
2256
+ snap.refreshing = true;
2257
+ void asyncRefresh5(rt, appId);
2258
+ }
2259
+ if (opts.archetype) {
2260
+ return snap.data.filter((f) => f.archetype === opts.archetype);
2261
+ }
2262
+ return snap.data;
2158
2263
  }
2159
- function detectSingleModelArray(ir, policy) {
2160
- if (ir.models.length !== 1) return [];
2161
- if (policy?.posture === "locked") return [];
2162
- const only = ir.models[0];
2163
- return [
2164
- {
2165
- level: "warn",
2166
- code: "single-model-array",
2167
- 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.`,
2168
- 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.",
2169
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#single-model-array"
2264
+ async function asyncRefresh5(rt, appId) {
2265
+ const promise = doRefresh5(rt, appId);
2266
+ pendingRefreshes5.set(appId, promise);
2267
+ try {
2268
+ await promise;
2269
+ } finally {
2270
+ if (pendingRefreshes5.get(appId) === promise) {
2271
+ pendingRefreshes5.delete(appId);
2170
2272
  }
2171
- ];
2273
+ }
2172
2274
  }
2173
- function detectCostMismatchedArchetype(ir, profile, phase2) {
2174
- if (!phase2 || phase2.fallbackChain.length === 0) return [];
2175
- if (!phase2.profileResolver) return [];
2176
- const archetype = ir.intent.archetype;
2177
- const chosenScore = getArchetypePerfScore(profile.id, archetype);
2178
- const chosenHasRoomToGrow = chosenScore.grounding === "judgment" || chosenScore.score < COST_MISMATCHED_CHOSEN_SCORE_CEILING;
2179
- if (!chosenHasRoomToGrow) return [];
2180
- let bestAlt = null;
2181
- for (const altId of phase2.fallbackChain) {
2182
- const altProfile = phase2.profileResolver(altId);
2183
- if (!altProfile) continue;
2184
- if (altProfile.id === profile.id) continue;
2185
- const altScore = getArchetypePerfScore(altProfile.id, archetype);
2186
- if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
2187
- if (altScore.score < chosenScore.score) continue;
2188
- if (altProfile.costInputPer1m >= profile.costInputPer1m) continue;
2189
- if (!bestAlt || altScore.score > bestAlt.score.score || altScore.score === bestAlt.score.score && altProfile.costInputPer1m < bestAlt.profile.costInputPer1m) {
2190
- bestAlt = { id: altId, profile: altProfile, score: altScore };
2191
- }
2275
+ async function doRefresh5(rt, appId) {
2276
+ const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
2277
+ let snap = snapshots5.get(appId);
2278
+ if (!snap) {
2279
+ snap = { data: [], expiresAt: 0, refreshing: false };
2280
+ snapshots5.set(appId, snap);
2192
2281
  }
2193
- if (!bestAlt) return [];
2194
- const tierDownWouldFire = bestAlt.score.grounding === "measured" && bestAlt.profile.costInputPer1m <= profile.costInputPer1m * TIER_DOWN_COST_RATIO;
2195
- if (tierDownWouldFire) return [];
2196
- const chosenGrounding = chosenScore.grounding === "judgment" ? `archetypePerf.${archetype}=judgment` : `archetypePerf.${archetype}=${chosenScore.score}`;
2197
- const altGrounding = bestAlt.score.grounding === "measured" ? `archetypePerf.${archetype}=${bestAlt.score.score}, measured, n=${bestAlt.score.n}` : `archetypePerf.${archetype}=${bestAlt.score.score}, judgment`;
2198
- return [
2199
- {
2200
- level: "warn",
2201
- code: "cost-mismatched-archetype",
2202
- 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.`,
2203
- 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.`,
2204
- recommendationType: profile.provider === bestAlt.profile.provider ? "tier-down" : "model-swap",
2205
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2282
+ try {
2283
+ const res = await rt.fetchImpl(url, { method: "GET" });
2284
+ if (!res.ok) {
2285
+ throw new Error(`stale-model findings ${res.status}: ${res.statusText}`);
2206
2286
  }
2207
- ];
2208
- }
2209
- function detectModelStaleEvidence(ir, profile) {
2210
- if (!isBrainQueryActiveFor("kgauto_archetype_perf")) return [];
2211
- const archetype = ir.intent.archetype;
2212
- const chosen = getArchetypePerfScore(profile.id, archetype);
2213
- if (chosen.grounding !== "judgment") return [];
2214
- return [
2215
- {
2216
- level: "info",
2217
- code: "model-stale-evidence",
2218
- 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.`,
2219
- 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.",
2220
- recommendationType: "prompt-fix",
2221
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2287
+ const body = await res.json();
2288
+ if (runtime5 !== rt) return;
2289
+ const rows = Array.isArray(body) ? mapRowsToFindings3(body) : [];
2290
+ snap.data = rows;
2291
+ snap.expiresAt = Date.now() + rt.ttlMs;
2292
+ snap.refreshing = false;
2293
+ } catch (err) {
2294
+ if (runtime5 !== rt) return;
2295
+ snap.refreshing = false;
2296
+ snap.expiresAt = Date.now() + rt.ttlMs;
2297
+ if (!warnedOnce5) {
2298
+ warnedOnce5 = true;
2299
+ (rt.onError ?? defaultOnError5)(err);
2222
2300
  }
2223
- ];
2301
+ }
2224
2302
  }
2225
- function detectTierDown(ir, profile, phase2) {
2226
- if (!phase2 || phase2.fallbackChain.length === 0) return [];
2227
- if (!phase2.profileResolver) return [];
2228
- const archetype = ir.intent.archetype;
2229
- const chosenScore = getArchetypePerfScore(profile.id, archetype);
2230
- const chosenCost = profile.costInputPer1m;
2231
- let bestAlt = null;
2232
- for (const altId of phase2.fallbackChain) {
2233
- const altProfile = phase2.profileResolver(altId);
2234
- if (!altProfile) continue;
2235
- if (altProfile.id === profile.id) continue;
2236
- const altScore = getArchetypePerfScore(altProfile.id, archetype);
2237
- if (altScore.grounding !== "measured") continue;
2238
- if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
2239
- if (altScore.score < chosenScore.score) continue;
2240
- if (altProfile.costInputPer1m > chosenCost * TIER_DOWN_COST_RATIO) continue;
2241
- if (!bestAlt || altProfile.costInputPer1m < bestAlt.profile.costInputPer1m || altProfile.costInputPer1m === bestAlt.profile.costInputPer1m && altScore.score > bestAlt.score.score) {
2242
- bestAlt = { id: altId, profile: altProfile, score: altScore };
2303
+ function defaultOnError5(err) {
2304
+ console.warn(
2305
+ "[kgauto] stale-model findings fetch failed (using empty fallback):",
2306
+ err
2307
+ );
2308
+ }
2309
+ var CONSUMER_ON_STALE_MODEL_RULE_CODE = "consumer-on-stale-model";
2310
+ function advisorRuleConsumerOnStaleModel(ir) {
2311
+ if (!isStaleModelFindingsBrainActive()) return [];
2312
+ if (!ir.appId) return [];
2313
+ const findings = getStaleModelFindings({
2314
+ appId: ir.appId,
2315
+ archetype: ir.intent.archetype
2316
+ });
2317
+ if (findings.length === 0) return [];
2318
+ const ranked = [...findings].sort((a, b) => {
2319
+ if (a.staleStatus !== b.staleStatus) {
2320
+ return a.staleStatus === "deprecated" ? -1 : 1;
2243
2321
  }
2244
- }
2245
- if (!bestAlt) return [];
2246
- const chosenDesc = chosenScore.grounding === "measured" ? `archetypePerf.${archetype}=${chosenScore.score} (measured, n=${chosenScore.n})` : `archetypePerf.${archetype}=${chosenScore.score} (${chosenScore.grounding})`;
2322
+ return a.staleModel.localeCompare(b.staleModel);
2323
+ });
2324
+ const top = ranked[0];
2325
+ const extraCount = findings.length - 1;
2326
+ const extraNote = extraCount > 0 ? ` (+ ${extraCount} more stale model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
2247
2327
  return [
2248
2328
  {
2249
2329
  level: "warn",
2250
- code: "tier-down",
2251
- 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.`,
2252
- 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.`,
2253
- recommendationType: "tier-down",
2330
+ code: CONSUMER_ON_STALE_MODEL_RULE_CODE,
2331
+ message: `${top.message}${extraNote}`,
2332
+ 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}.`,
2333
+ recommendationType: "model-swap",
2254
2334
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2255
2335
  }
2256
2336
  ];
2257
2337
  }
2258
- function detectArchetypePerfFloorBreach(ir, profile) {
2259
- const compat = getModelCompatibility(profile.id, {
2260
- archetype: ir.intent.archetype,
2261
- toolOrchestration: ir.constraints?.toolOrchestration
2262
- });
2263
- if (compat.status === "compatible") return [];
2264
- if (compat.status === "requires-adapter") {
2265
- return [
2266
- {
2267
- level: "warn",
2268
- code: "archetype-perf-floor-breach",
2269
- 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}`,
2270
- 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.`,
2271
- recommendationType: "prompt-fix",
2272
- suggestedAdaptation: compat.adapter,
2273
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2274
- }
2275
- ];
2338
+
2339
+ // src/archetype-fits.ts
2340
+ var ARCHETYPE_FAMILY_FITS = Object.freeze([
2341
+ {
2342
+ archetype: "plan",
2343
+ betterFitFamily: "deepseek-reasoner",
2344
+ 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).",
2345
+ costGuidance: "substantially cheaper at current pricing (deepseek-v4-pro promo: ~7-17x cheaper than sonnet)"
2346
+ },
2347
+ {
2348
+ archetype: "critique",
2349
+ betterFitFamily: "deepseek-reasoner",
2350
+ 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.",
2351
+ costGuidance: "comparable or cheaper at current pricing"
2352
+ }
2353
+ ]);
2354
+ function findBetterFit(archetype, currentFamily) {
2355
+ for (const fit of ARCHETYPE_FAMILY_FITS) {
2356
+ if (fit.archetype !== archetype) continue;
2357
+ if (fit.betterFitFamily === currentFamily) return null;
2358
+ return fit;
2359
+ }
2360
+ return null;
2361
+ }
2362
+
2363
+ // src/advisor-rules/cross-family-fit.ts
2364
+ function familyHasCurrentActiveModel(family) {
2365
+ for (const profile of allProfiles()) {
2366
+ const profileFamily = profile.family ?? deriveFamilyFromModelId(profile.id);
2367
+ if (profileFamily !== family) continue;
2368
+ if (profile.status !== "current") continue;
2369
+ if (profile.active === false) continue;
2370
+ return true;
2371
+ }
2372
+ return false;
2373
+ }
2374
+ function listCandidatesInFamily(family) {
2375
+ const candidates = [];
2376
+ for (const profile of allProfiles()) {
2377
+ const profileFamily = profile.family ?? deriveFamilyFromModelId(profile.id);
2378
+ if (profileFamily !== family) continue;
2379
+ if (profile.status !== "current") continue;
2380
+ if (profile.active === false) continue;
2381
+ candidates.push(profile.id);
2382
+ if (candidates.length >= 3) break;
2276
2383
  }
2384
+ return candidates;
2385
+ }
2386
+ function advisorRuleCrossFamilyFit(ctx) {
2387
+ if (!ctx.resolvedPrimary) return [];
2388
+ const currentFamily = deriveFamilyFromModelId(ctx.resolvedPrimary);
2389
+ if (!currentFamily) return [];
2390
+ const fit = findBetterFit(ctx.archetype, currentFamily);
2391
+ if (!fit) return [];
2392
+ if (!familyHasCurrentActiveModel(fit.betterFitFamily)) return [];
2393
+ const candidates = listCandidatesInFamily(fit.betterFitFamily);
2394
+ if (candidates.length === 0) return [];
2395
+ const candidateStr = candidates.join(", ");
2396
+ const message = `Your ${currentFamily} call on ${ctx.archetype} could shift to ${fit.betterFitFamily} \u2014 typically better quality + ${fit.costGuidance}. Suggested candidates: ${candidateStr}.`;
2277
2397
  return [
2278
2398
  {
2279
- level: "critical",
2280
- code: "archetype-perf-floor-breach",
2281
- 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}`,
2282
- 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.`,
2399
+ level: "info",
2400
+ code: "cross-family-fit-candidate",
2401
+ ownership: "consumer-actionable",
2402
+ message,
2403
+ 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.`,
2283
2404
  recommendationType: "model-swap",
2284
2405
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2285
2406
  }
2286
2407
  ];
2287
2408
  }
2288
- function detectStaleExclusionCandidate(ir) {
2289
- if (!isExclusionFindingsBrainActive()) return [];
2290
- if (!ir.appId) return [];
2291
- const findings = getStaleExclusionFindings({
2292
- appId: ir.appId,
2293
- archetype: ir.intent.archetype
2294
- });
2295
- if (findings.length === 0) return [];
2296
- const ranked = [...findings].sort((a, b) => {
2297
- const sa = a.estimatedSavingsUsd30d ?? -Infinity;
2298
- const sb = b.estimatedSavingsUsd30d ?? -Infinity;
2299
- if (sa !== sb) return sb - sa;
2300
- return confidenceRank(b.confidence) - confidenceRank(a.confidence);
2301
- });
2302
- const top = ranked[0];
2303
- const extraCount = findings.length - 1;
2304
- const extraNote = extraCount > 0 ? ` (+ ${extraCount} more excluded model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
2409
+
2410
+ // src/advisor.ts
2411
+ var QUALITY_FLOOR_FOR_RECOMMENDATION = 6;
2412
+ var TIER_DOWN_COST_RATIO = 0.5;
2413
+ var COST_MISMATCHED_CHOSEN_SCORE_CEILING = 7;
2414
+ var PRODUCER_OWNED_RULE_CODES = Object.freeze(
2415
+ /* @__PURE__ */ new Set(["model-stale-evidence", "promote-ready"])
2416
+ );
2417
+ function deriveOwnership(code, selfDeclared) {
2418
+ if (selfDeclared) return selfDeclared;
2419
+ return PRODUCER_OWNED_RULE_CODES.has(code) ? "producer-owned" : "consumer-actionable";
2420
+ }
2421
+ function runAdvisor(ir, result, profile, policy, phase2) {
2422
+ const out = [];
2423
+ out.push(...detectCachingOff(ir, profile));
2424
+ out.push(...detectSingleChunkSystem(ir, profile));
2425
+ out.push(...detectToolBloat(ir, result));
2426
+ out.push(...detectHistoryUncached(ir, profile));
2427
+ out.push(...detectSingleModelArray(ir, policy));
2428
+ if (policy?.posture !== "locked") {
2429
+ out.push(...detectCostMismatchedArchetype(ir, profile, phase2));
2430
+ out.push(...detectModelStaleEvidence(ir, profile));
2431
+ out.push(...detectTierDown(ir, profile, phase2));
2432
+ }
2433
+ if (!translatorClearedToolCallCliff(phase2)) {
2434
+ out.push(...detectArchetypePerfFloorBreach(ir, profile));
2435
+ }
2436
+ if (policy?.posture !== "locked") {
2437
+ out.push(...detectStaleExclusionCandidate(ir));
2438
+ }
2439
+ if (policy?.posture !== "locked" && ir.appId) {
2440
+ out.push(
2441
+ ...advisorRulePromoteReady({
2442
+ appId: ir.appId,
2443
+ archetype: ir.intent.archetype,
2444
+ resolvedPrimary: profile.id
2445
+ })
2446
+ );
2447
+ out.push(...advisorRuleConsumerOnStaleModel(ir));
2448
+ }
2449
+ if (policy?.posture !== "locked") {
2450
+ out.push(
2451
+ ...advisorRuleCrossFamilyFit({
2452
+ archetype: ir.intent.archetype,
2453
+ resolvedPrimary: profile.id
2454
+ })
2455
+ );
2456
+ }
2457
+ return out;
2458
+ }
2459
+ function translatorClearedToolCallCliff(phase2) {
2460
+ const rewrites = phase2?.sectionRewritesApplied;
2461
+ if (!rewrites || rewrites.length === 0) return false;
2462
+ for (const rw of rewrites) {
2463
+ if (rw.kind === "tool_call_contract") return true;
2464
+ }
2465
+ return false;
2466
+ }
2467
+ function detectCachingOff(ir, profile) {
2468
+ if (profile.provider !== "anthropic") return [];
2469
+ const totalChars = ir.sections.reduce((s, sec) => s + sec.text.length, 0);
2470
+ if (totalChars < 2e3) return [];
2471
+ const anyCacheable = ir.sections.some((s) => s.cacheable === true);
2472
+ if (anyCacheable) return [];
2305
2473
  return [
2306
2474
  {
2307
- level: "info",
2308
- code: "stale-exclusion-candidate",
2309
- message: `${top.message}${extraNote}`,
2310
- suggestion: top.suggestion,
2311
- recommendationType: "tier-down",
2475
+ level: "warn",
2476
+ code: "caching-off-on-claude",
2477
+ 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.`,
2478
+ 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.",
2312
2479
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2313
2480
  }
2314
2481
  ];
2315
2482
  }
2316
- function confidenceRank(c) {
2317
- if (c === "high") return 3;
2318
- if (c === "medium") return 2;
2319
- return 1;
2483
+ function detectSingleChunkSystem(ir, profile) {
2484
+ if (profile.provider !== "anthropic") return [];
2485
+ if (ir.sections.length !== 1) return [];
2486
+ const only = ir.sections[0];
2487
+ if (!only || only.text.length <= 1e3) return [];
2488
+ return [
2489
+ {
2490
+ level: "info",
2491
+ code: "single-chunk-system",
2492
+ 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.`,
2493
+ 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.",
2494
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2495
+ }
2496
+ ];
2320
2497
  }
2321
-
2322
- // src/translator.ts
2323
- var TRANSLATOR_FLOOR = ARCHETYPE_FLOOR_DEFAULT;
2324
- var RULE_SEQUENTIAL_TOOL_CLIFF = "sequential-tool-cliff-below-floor";
2325
- var RULE_NARRATION_DRIFT_ANTHROPIC = "narration-drift-anthropic";
2326
- var RULE_NARRATION_THINKING_LEAK_DEEPSEEK = "narration-thinking-leak-deepseek";
2327
- 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.";
2328
- var NARRATION_DRIFT_ANTHROPIC_PREAMBLE = "Output ONLY the requested content. Do not narrate your thought process. Each line \u2264 12 words.";
2329
- 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.";
2330
- var RULE_DISCIPLINE_GATES_V1 = "discipline-gates-v1";
2331
- var DISCIPLINE_GATES_V1_WITH_TOOLS = `Work through these gates at every judgment point, explicitly:
2332
- 1. Evidence before reasoning: cite what you observed before concluding from it.
2333
- 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
2334
- 3. Expand, don't guess: resolve a compressed or referenced item by looking it up rather than inferring its contents.
2335
- 4. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.
2336
- 5. Label each claim: mark it observed, inferred, or assumed.
2337
- 6. A surfaced gap beats a guessed answer: flag what you cannot determine rather than fabricating past it.`;
2338
- var DISCIPLINE_GATES_V1_NO_TOOLS = `Work through these gates at every judgment point, explicitly:
2339
- 1. Evidence before reasoning: cite what you observed before concluding from it.
2340
- 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
2341
- 3. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.
2342
- 4. Label each claim: mark it observed, inferred, or assumed.
2343
- 5. A surfaced gap beats a guessed answer: flag what you cannot determine rather than fabricating past it.`;
2344
- var RULE_DISCIPLINE_GATES_V1_STRUCTURED = "discipline-gates-v1-structured";
2345
- var DISCIPLINE_GATES_V1_STRUCTURED_WITH_TOOLS = `Work through these gates at every judgment point, explicitly:
2346
- 1. Evidence before reasoning: cite what you observed before concluding from it.
2347
- 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
2348
- 3. Expand, don't guess: resolve a compressed or referenced item by looking it up rather than inferring its contents.
2349
- 4. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.`;
2350
- var DISCIPLINE_GATES_V1_STRUCTURED_NO_TOOLS = `Work through these gates at every judgment point, explicitly:
2351
- 1. Evidence before reasoning: cite what you observed before concluding from it.
2352
- 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
2353
- 3. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.`;
2354
- var DISCIPLINE_ELIGIBLE_ARCHETYPES = /* @__PURE__ */ new Set([
2355
- "hunt",
2356
- "summarize",
2357
- "plan",
2358
- "critique",
2359
- "judge"
2360
- ]);
2361
- function matchRule(kind, profile, archetype, ctx) {
2362
- if (kind === "discipline_contract") {
2363
- if (!DISCIPLINE_ELIGIBLE_ARCHETYPES.has(archetype)) return null;
2364
- if (ctx.outputMode !== "text") {
2365
- return {
2366
- id: RULE_DISCIPLINE_GATES_V1_STRUCTURED,
2367
- preamble: ctx.hasTools ? DISCIPLINE_GATES_V1_STRUCTURED_WITH_TOOLS : DISCIPLINE_GATES_V1_STRUCTURED_NO_TOOLS
2368
- };
2498
+ function detectToolBloat(ir, result) {
2499
+ const SHORT_OUTPUT = /* @__PURE__ */ new Set([
2500
+ "classify",
2501
+ "extract",
2502
+ "summarize",
2503
+ "transform",
2504
+ "critique"
2505
+ ]);
2506
+ if (!ir.tools || ir.tools.length === 0) return [];
2507
+ const toolsKept = result.diagnostics.toolsKept;
2508
+ if (toolsKept <= 10) return [];
2509
+ if (!SHORT_OUTPUT.has(ir.intent.archetype)) return [];
2510
+ return [
2511
+ {
2512
+ level: "warn",
2513
+ code: "tool-bloat",
2514
+ 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.`,
2515
+ 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.",
2516
+ docsUrl: "https://github.com/stue/kgauto/blob/main/v2/README.md#tools"
2369
2517
  }
2370
- return {
2371
- id: RULE_DISCIPLINE_GATES_V1,
2372
- preamble: ctx.hasTools ? DISCIPLINE_GATES_V1_WITH_TOOLS : DISCIPLINE_GATES_V1_NO_TOOLS
2373
- };
2374
- }
2375
- if (kind === "tool_call_contract") {
2376
- if (!profile.archetypePerf) return null;
2377
- const archetypeScore = profile.archetypePerf[archetype];
2378
- if (typeof archetypeScore !== "number" || archetypeScore >= TRANSLATOR_FLOOR) {
2379
- return null;
2380
- }
2381
- return {
2382
- id: RULE_SEQUENTIAL_TOOL_CLIFF,
2383
- preamble: SEQUENTIAL_TOOL_PREAMBLE,
2384
- wireOverrides: { parallelToolCalls: false }
2385
- };
2386
- }
2387
- if (kind === "narration_contract") {
2388
- if (profile.provider === "anthropic") {
2389
- return {
2390
- id: RULE_NARRATION_DRIFT_ANTHROPIC,
2391
- preamble: NARRATION_DRIFT_ANTHROPIC_PREAMBLE
2392
- };
2393
- }
2394
- if (profile.provider === "deepseek") {
2395
- return {
2396
- id: RULE_NARRATION_THINKING_LEAK_DEEPSEEK,
2397
- preamble: NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE
2398
- };
2399
- }
2400
- return null;
2401
- }
2402
- return null;
2403
- }
2404
- function applySectionRewrites(args) {
2405
- const { ir, profile, archetype } = args;
2406
- if (!Array.isArray(ir.sections) || ir.sections.length === 0) {
2407
- return { rewrittenIR: ir, rewrites: [] };
2408
- }
2409
- const outputMode = args.outputMode ?? resolveOutputMode({
2410
- declared: ir.constraints?.outputMode,
2411
- structuredOutput: ir.constraints?.structuredOutput,
2412
- toolCount: ir.tools?.length ?? 0
2413
- });
2414
- const hasTools = (ir.tools?.length ?? 0) > 0;
2415
- const ctx = { outputMode, hasTools };
2416
- const rewrites = [];
2417
- const newSections = ir.sections.map((section) => {
2418
- if (!section.kind || section.kind === "arbitrary") return section;
2419
- const rule = matchRule(section.kind, profile, archetype, ctx);
2420
- if (!rule) return section;
2421
- const originalText = section.text;
2422
- const transformedText = `${rule.preamble}
2423
-
2424
- ${originalText}`;
2425
- rewrites.push({
2426
- sectionId: section.id,
2427
- kind: section.kind,
2428
- rule: rule.id,
2429
- originalText,
2430
- transformedText,
2431
- ...rule.wireOverrides ? { wireOverrides: rule.wireOverrides } : {}
2432
- });
2433
- return { ...section, text: transformedText };
2434
- });
2435
- if (rewrites.length === 0) {
2436
- return { rewrittenIR: ir, rewrites: [] };
2437
- }
2438
- const rewrittenIR = { ...ir, sections: newSections };
2439
- return { rewrittenIR, rewrites };
2440
- }
2441
-
2442
- // src/promotions-brain.ts
2443
- function isRawPromotionRow(x) {
2444
- if (!x || typeof x !== "object") return false;
2445
- const r = x;
2446
- return (typeof r.id === "number" || typeof r.id === "string") && typeof r.intent_archetype === "string" && typeof r.promoted_model === "string" && typeof r.incumbent_model === "string";
2447
- }
2448
- function coerceId(v) {
2449
- if (typeof v === "number") return Number.isFinite(v) ? v : null;
2450
- if (typeof v === "string") {
2451
- const n = Number(v);
2452
- return Number.isFinite(n) ? n : null;
2453
- }
2454
- return null;
2455
- }
2456
- function mapRowsToPromotions(rows) {
2457
- const out = [];
2458
- for (const row of rows) {
2459
- if (!isRawPromotionRow(row)) continue;
2460
- const id = coerceId(row.id);
2461
- if (id === null) continue;
2462
- const mode = row.mode === "strategy" ? "strategy" : row.mode === "downswap" || row.mode === void 0 ? "downswap" : null;
2463
- if (mode === null) continue;
2464
- out.push({
2465
- id,
2466
- archetype: row.intent_archetype,
2467
- mode,
2468
- strategy: typeof row.strategy === "string" ? row.strategy : null,
2469
- promotedModel: row.promoted_model,
2470
- incumbentModel: row.incumbent_model,
2471
- evalRunId: coerceId(row.eval_run_id ?? null),
2472
- suppressQualityGate: row.suppress_quality_gate === true,
2473
- promotedAt: typeof row.promoted_at === "string" ? row.promoted_at : ""
2474
- });
2475
- }
2476
- return out;
2477
- }
2478
- var snapshots4 = /* @__PURE__ */ new Map();
2479
- var runtime4;
2480
- var warnedOnce4 = false;
2481
- var DEFAULT_PROMOTIONS_ENDPOINT = "https://kgauto-dashboard.vercel.app/api/kgauto-v2/promotions";
2482
- function isAutoPromoteEnabledFromEnv(envSource) {
2483
- const env = envSource ?? (typeof process !== "undefined" && process.env ? process.env : {});
2484
- const raw = (env.KGAUTO_AUTO_PROMOTE ?? "").trim().toLowerCase();
2485
- return raw === "1" || raw === "true";
2486
- }
2487
- function configurePromotionsBrain(rt) {
2488
- runtime4 = rt;
2489
- snapshots4.clear();
2490
- warnedOnce4 = false;
2491
- }
2492
- function isPromotionsBrainActive() {
2493
- return runtime4 !== void 0;
2518
+ ];
2494
2519
  }
2495
- function getApplicablePromotion(opts) {
2496
- const rt = runtime4;
2497
- if (!rt) return void 0;
2498
- const appId = opts.appId;
2499
- if (!appId || !opts.archetype || !opts.mode) return void 0;
2500
- let snap = snapshots4.get(appId);
2501
- if (!snap) {
2502
- snap = { data: [], expiresAt: 0, refreshing: false };
2503
- snapshots4.set(appId, snap);
2504
- }
2505
- const now = Date.now();
2506
- const stale = snap.expiresAt <= now;
2507
- if (stale && !snap.refreshing) {
2508
- snap.refreshing = true;
2509
- void asyncRefresh4(rt, appId);
2520
+ function detectHistoryUncached(ir, profile) {
2521
+ if (profile.provider !== "anthropic") return [];
2522
+ if (!ir.history || ir.history.length < 2) return [];
2523
+ if (ir.historyCachePolicy && ir.historyCachePolicy.strategy !== "none") {
2524
+ return [];
2510
2525
  }
2511
- return snap.data.find(
2512
- (p) => p.archetype === opts.archetype && p.mode === opts.mode
2513
- );
2526
+ return [
2527
+ {
2528
+ level: "warn",
2529
+ code: "history-uncached-on-claude",
2530
+ 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.`,
2531
+ 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.",
2532
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2533
+ }
2534
+ ];
2514
2535
  }
2515
- var pendingRefreshes4 = /* @__PURE__ */ new Map();
2516
- async function asyncRefresh4(rt, appId) {
2517
- const promise = doRefresh4(rt, appId);
2518
- pendingRefreshes4.set(appId, promise);
2519
- try {
2520
- await promise;
2521
- } finally {
2522
- if (pendingRefreshes4.get(appId) === promise) {
2523
- pendingRefreshes4.delete(appId);
2536
+ function detectSingleModelArray(ir, policy) {
2537
+ if (ir.models.length !== 1) return [];
2538
+ if (policy?.posture === "locked") return [];
2539
+ const only = ir.models[0];
2540
+ return [
2541
+ {
2542
+ level: "warn",
2543
+ code: "single-model-array",
2544
+ 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.`,
2545
+ 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.",
2546
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#single-model-array"
2524
2547
  }
2525
- }
2548
+ ];
2526
2549
  }
2527
- async function doRefresh4(rt, appId) {
2528
- const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
2529
- let snap = snapshots4.get(appId);
2530
- if (!snap) {
2531
- snap = { data: [], expiresAt: 0, refreshing: false };
2532
- snapshots4.set(appId, snap);
2533
- }
2534
- try {
2535
- const res = await rt.fetchImpl(url, { method: "GET" });
2536
- if (!res.ok) {
2537
- throw new Error(`promotions ${res.status}: ${res.statusText}`);
2550
+ function detectCostMismatchedArchetype(ir, profile, phase2) {
2551
+ if (!phase2 || phase2.fallbackChain.length === 0) return [];
2552
+ if (!phase2.profileResolver) return [];
2553
+ const archetype = ir.intent.archetype;
2554
+ const chosenScore = getArchetypePerfScore(profile.id, archetype);
2555
+ const chosenHasRoomToGrow = chosenScore.grounding === "judgment" || chosenScore.score < COST_MISMATCHED_CHOSEN_SCORE_CEILING;
2556
+ if (!chosenHasRoomToGrow) return [];
2557
+ let bestAlt = null;
2558
+ for (const altId of phase2.fallbackChain) {
2559
+ const altProfile = phase2.profileResolver(altId);
2560
+ if (!altProfile) continue;
2561
+ if (altProfile.id === profile.id) continue;
2562
+ const altScore = getArchetypePerfScore(altProfile.id, archetype);
2563
+ if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
2564
+ if (altScore.score < chosenScore.score) continue;
2565
+ if (altProfile.costInputPer1m >= profile.costInputPer1m) continue;
2566
+ if (getMeasuredFailureVerdict({
2567
+ appId: ir.appId,
2568
+ archetype,
2569
+ model: altProfile.id
2570
+ })?.gated === true) {
2571
+ continue;
2538
2572
  }
2539
- const body = await res.json();
2540
- if (runtime4 !== rt) return;
2541
- const rows = Array.isArray(body) ? mapRowsToPromotions(body) : [];
2542
- snap.data = rows;
2543
- snap.expiresAt = Date.now() + rt.ttlMs;
2544
- snap.refreshing = false;
2545
- } catch (err) {
2546
- if (runtime4 !== rt) return;
2547
- snap.refreshing = false;
2548
- snap.expiresAt = Date.now() + rt.ttlMs;
2549
- if (!warnedOnce4) {
2550
- warnedOnce4 = true;
2551
- (rt.onError ?? defaultOnError4)(err);
2573
+ if (ir.constraints?.structuredOutput && effectiveConventions(altProfile).some(
2574
+ (c) => c.archetype === archetype && c.structuredOutputHint === "avoid"
2575
+ )) {
2576
+ continue;
2577
+ }
2578
+ if (getRecentRollback({
2579
+ appId: ir.appId,
2580
+ archetype,
2581
+ model: altProfile.id
2582
+ }) !== void 0) {
2583
+ continue;
2584
+ }
2585
+ if (!bestAlt || altScore.score > bestAlt.score.score || altScore.score === bestAlt.score.score && altProfile.costInputPer1m < bestAlt.profile.costInputPer1m) {
2586
+ bestAlt = { id: altId, profile: altProfile, score: altScore };
2552
2587
  }
2553
2588
  }
2589
+ if (!bestAlt) return [];
2590
+ const tierDownWouldFire = bestAlt.score.grounding === "measured" && bestAlt.profile.costInputPer1m <= profile.costInputPer1m * TIER_DOWN_COST_RATIO;
2591
+ if (tierDownWouldFire) return [];
2592
+ const chosenGrounding = chosenScore.grounding === "judgment" ? `archetypePerf.${archetype}=judgment` : `archetypePerf.${archetype}=${chosenScore.score}`;
2593
+ const altGrounding = bestAlt.score.grounding === "measured" ? `archetypePerf.${archetype}=${bestAlt.score.score}, measured, n=${bestAlt.score.n}` : `archetypePerf.${archetype}=${bestAlt.score.score}, judgment`;
2594
+ return [
2595
+ {
2596
+ level: "warn",
2597
+ code: "cost-mismatched-archetype",
2598
+ 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.`,
2599
+ 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.`,
2600
+ recommendationType: profile.provider === bestAlt.profile.provider ? "tier-down" : "model-swap",
2601
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2602
+ }
2603
+ ];
2554
2604
  }
2555
- function defaultOnError4(err) {
2556
- console.warn(
2557
- "[kgauto] promotions fetch failed (promotion boost inactive until next refresh):",
2558
- err
2559
- );
2560
- }
2561
- function _testResetPromotions() {
2562
- runtime4 = void 0;
2563
- snapshots4.clear();
2564
- pendingRefreshes4 = /* @__PURE__ */ new Map();
2565
- warnedOnce4 = false;
2566
- }
2567
- async function _testWaitForPromotionsRefresh() {
2568
- const pending = Array.from(pendingRefreshes4.values());
2569
- if (pending.length > 0) await Promise.all(pending);
2570
- }
2571
-
2572
- // src/measured-failure-brain.ts
2573
- function coerceCount(v) {
2574
- if (typeof v === "number") return Number.isFinite(v) ? v : null;
2575
- if (typeof v === "string") {
2576
- const n = Number(v);
2577
- return Number.isFinite(n) ? n : null;
2578
- }
2579
- return null;
2580
- }
2581
- function isRawFailureRow(x) {
2582
- if (!x || typeof x !== "object") return false;
2583
- const r = x;
2584
- return typeof r.intent_archetype === "string" && typeof r.model === "string" && (typeof r.n === "number" || typeof r.n === "string");
2605
+ function detectModelStaleEvidence(ir, profile) {
2606
+ if (!isBrainQueryActiveFor("kgauto_archetype_perf")) return [];
2607
+ const archetype = ir.intent.archetype;
2608
+ const chosen = getArchetypePerfScore(profile.id, archetype);
2609
+ if (chosen.grounding !== "judgment") return [];
2610
+ return [
2611
+ {
2612
+ level: "info",
2613
+ code: "model-stale-evidence",
2614
+ 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.`,
2615
+ 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.)",
2616
+ recommendationType: "prompt-fix",
2617
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2618
+ }
2619
+ ];
2585
2620
  }
2586
- function mapRows(rows) {
2587
- const out = [];
2588
- for (const row of rows) {
2589
- if (!isRawFailureRow(row)) continue;
2590
- const n = coerceCount(row.n);
2591
- const nFail = coerceCount(row.n_fail) ?? 0;
2592
- if (n === null || n <= 0) continue;
2593
- out.push({
2594
- archetype: row.intent_archetype,
2595
- model: row.model,
2596
- n,
2597
- nFail
2598
- });
2621
+ function detectTierDown(ir, profile, phase2) {
2622
+ if (!phase2 || phase2.fallbackChain.length === 0) return [];
2623
+ if (!phase2.profileResolver) return [];
2624
+ const archetype = ir.intent.archetype;
2625
+ const chosenScore = getArchetypePerfScore(profile.id, archetype);
2626
+ const chosenCost = profile.costInputPer1m;
2627
+ let bestAlt = null;
2628
+ for (const altId of phase2.fallbackChain) {
2629
+ const altProfile = phase2.profileResolver(altId);
2630
+ if (!altProfile) continue;
2631
+ if (altProfile.id === profile.id) continue;
2632
+ const altScore = getArchetypePerfScore(altProfile.id, archetype);
2633
+ if (altScore.grounding !== "measured") continue;
2634
+ if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
2635
+ if (altScore.score < chosenScore.score) continue;
2636
+ if (altProfile.costInputPer1m > chosenCost * TIER_DOWN_COST_RATIO) continue;
2637
+ if (!bestAlt || altProfile.costInputPer1m < bestAlt.profile.costInputPer1m || altProfile.costInputPer1m === bestAlt.profile.costInputPer1m && altScore.score > bestAlt.score.score) {
2638
+ bestAlt = { id: altId, profile: altProfile, score: altScore };
2639
+ }
2599
2640
  }
2600
- return out;
2601
- }
2602
- var MEASURED_FAILURE_CFG = {
2603
- /**
2604
- * Hard minimum attempts before ANY gate may be created. Guards against
2605
- * pathological tiny samples that the confidence bound alone would let
2606
- * through in edge cases. At 5-for-5 the bound clears the threshold; at
2607
- * 3-for-3 it does not, which is the behaviour we want (three failures is
2608
- * a bad day, five in a row is a pattern).
2609
- */
2610
- minSample: 5,
2611
- /**
2612
- * Gate when we are 95% confident the model fails MORE OFTEN THAN IT
2613
- * SUCCEEDS on this surface. Deliberately unarguable rather than tuned —
2614
- * a model that probably fails the majority of the time has no business
2615
- * leading a surface, whatever its declared scores say.
2616
- */
2617
- lowerBoundThreshold: 0.5,
2618
- /** 95% one-sided-ish confidence (standard two-sided z at α=0.05). */
2619
- z: 1.96,
2620
- /** Must match the view's window. Documented here for the advisory text. */
2621
- windowDays: 28
2622
- };
2623
- function wilsonLowerBound(failures, n, z = MEASURED_FAILURE_CFG.z) {
2624
- if (n <= 0) return 0;
2625
- const p = failures / n;
2626
- const z2 = z * z;
2627
- const denom = 1 + z2 / n;
2628
- const centre = p + z2 / (2 * n);
2629
- const margin = z * Math.sqrt(p * (1 - p) / n + z2 / (4 * n * n));
2630
- const lower2 = (centre - margin) / denom;
2631
- return lower2 < 0 ? 0 : lower2;
2632
- }
2633
- function judgeMeasuredFailure(row, cfg = MEASURED_FAILURE_CFG) {
2634
- if (!row || row.n < cfg.minSample) return void 0;
2635
- const lowerBound = wilsonLowerBound(row.nFail, row.n, cfg.z);
2636
- return {
2637
- gated: lowerBound > cfg.lowerBoundThreshold,
2638
- rate: row.nFail / row.n,
2639
- lowerBound,
2640
- n: row.n,
2641
- nFail: row.nFail
2642
- };
2643
- }
2644
- var snapshots5 = /* @__PURE__ */ new Map();
2645
- var runtime5;
2646
- var warnedOnce5 = false;
2647
- var DEFAULT_MEASURED_FAILURE_ENDPOINT = "https://kgauto-dashboard.vercel.app/api/kgauto-v2/measured-failure";
2648
- function isMeasuredFailureGateEnabledFromEnv(envSource) {
2649
- const env = envSource ?? (typeof process !== "undefined" && process.env ? process.env : {});
2650
- const raw = (env.KGAUTO_MEASURED_FAILURE_GATE ?? "").trim().toLowerCase();
2651
- return !(raw === "0" || raw === "false");
2652
- }
2653
- function configureMeasuredFailureBrain(rt) {
2654
- runtime5 = rt;
2655
- snapshots5.clear();
2656
- warnedOnce5 = false;
2657
- }
2658
- function isMeasuredFailureBrainActive() {
2659
- return runtime5 !== void 0;
2641
+ if (!bestAlt) return [];
2642
+ const chosenDesc = chosenScore.grounding === "measured" ? `archetypePerf.${archetype}=${chosenScore.score} (measured, n=${chosenScore.n})` : `archetypePerf.${archetype}=${chosenScore.score} (${chosenScore.grounding})`;
2643
+ return [
2644
+ {
2645
+ level: "warn",
2646
+ code: "tier-down",
2647
+ 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.`,
2648
+ 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.`,
2649
+ recommendationType: "tier-down",
2650
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2651
+ }
2652
+ ];
2660
2653
  }
2661
- function prefetchMeasuredFailure(appId) {
2662
- const rt = runtime5;
2663
- if (!rt || !appId) return void 0;
2664
- let snap = snapshots5.get(appId);
2665
- if (!snap) {
2666
- snap = { data: [], expiresAt: 0, refreshing: false };
2667
- snapshots5.set(appId, snap);
2654
+ function detectArchetypePerfFloorBreach(ir, profile) {
2655
+ const compat = getModelCompatibility(profile.id, {
2656
+ archetype: ir.intent.archetype,
2657
+ toolOrchestration: ir.constraints?.toolOrchestration
2658
+ });
2659
+ if (compat.status === "compatible") return [];
2660
+ if (compat.status === "requires-adapter") {
2661
+ return [
2662
+ {
2663
+ level: "warn",
2664
+ code: "archetype-perf-floor-breach",
2665
+ 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}`,
2666
+ 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.`,
2667
+ recommendationType: "prompt-fix",
2668
+ suggestedAdaptation: compat.adapter,
2669
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2670
+ }
2671
+ ];
2668
2672
  }
2669
- if (snap.expiresAt > Date.now()) return void 0;
2670
- const inflight = pendingRefreshes5.get(appId);
2671
- if (inflight) return inflight;
2672
- if (snap.refreshing) return void 0;
2673
- snap.refreshing = true;
2674
- void asyncRefresh5(rt, appId);
2675
- return pendingRefreshes5.get(appId);
2673
+ return [
2674
+ {
2675
+ level: "critical",
2676
+ code: "archetype-perf-floor-breach",
2677
+ 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}`,
2678
+ 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.`,
2679
+ recommendationType: "model-swap",
2680
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2681
+ }
2682
+ ];
2676
2683
  }
2677
- async function awaitMeasuredFailureReady(appId, timeoutMs) {
2678
- if (!runtime5 || !appId) return;
2679
- const pending = prefetchMeasuredFailure(appId) ?? pendingRefreshes5.get(appId);
2680
- if (!(timeoutMs > 0)) return;
2681
- if (!pending) return;
2682
- let timer;
2683
- try {
2684
- await Promise.race([
2685
- pending,
2686
- new Promise((resolve) => {
2687
- timer = setTimeout(resolve, timeoutMs);
2688
- })
2689
- ]);
2690
- } catch {
2691
- } finally {
2692
- if (timer) clearTimeout(timer);
2693
- }
2684
+ function detectStaleExclusionCandidate(ir) {
2685
+ if (!isExclusionFindingsBrainActive()) return [];
2686
+ if (!ir.appId) return [];
2687
+ const findings = getStaleExclusionFindings({
2688
+ appId: ir.appId,
2689
+ archetype: ir.intent.archetype
2690
+ });
2691
+ if (findings.length === 0) return [];
2692
+ const ranked = [...findings].sort((a, b) => {
2693
+ const sa = a.estimatedSavingsUsd30d ?? -Infinity;
2694
+ const sb = b.estimatedSavingsUsd30d ?? -Infinity;
2695
+ if (sa !== sb) return sb - sa;
2696
+ return confidenceRank(b.confidence) - confidenceRank(a.confidence);
2697
+ });
2698
+ const top = ranked[0];
2699
+ const extraCount = findings.length - 1;
2700
+ const extraNote = extraCount > 0 ? ` (+ ${extraCount} more excluded model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
2701
+ return [
2702
+ {
2703
+ level: "info",
2704
+ code: "stale-exclusion-candidate",
2705
+ message: `${top.message}${extraNote}`,
2706
+ suggestion: top.suggestion,
2707
+ recommendationType: "tier-down",
2708
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2709
+ }
2710
+ ];
2694
2711
  }
2695
- function getMeasuredFailureVerdict(opts) {
2696
- const rt = runtime5;
2697
- if (!rt) return void 0;
2698
- const { appId, archetype, model } = opts;
2699
- if (!appId || !archetype || !model) return void 0;
2700
- let snap = snapshots5.get(appId);
2701
- if (!snap) {
2702
- snap = { data: [], expiresAt: 0, refreshing: false };
2703
- snapshots5.set(appId, snap);
2704
- }
2705
- const now = Date.now();
2706
- if (snap.expiresAt <= now && !snap.refreshing) {
2707
- snap.refreshing = true;
2708
- void asyncRefresh5(rt, appId);
2709
- }
2710
- const row = snap.data.find(
2711
- (r) => r.archetype === archetype && r.model === model
2712
- );
2713
- return judgeMeasuredFailure(row);
2712
+ function confidenceRank(c) {
2713
+ if (c === "high") return 3;
2714
+ if (c === "medium") return 2;
2715
+ return 1;
2714
2716
  }
2715
- var pendingRefreshes5 = /* @__PURE__ */ new Map();
2716
- async function asyncRefresh5(rt, appId) {
2717
- const promise = doRefresh5(rt, appId);
2718
- pendingRefreshes5.set(appId, promise);
2719
- try {
2720
- await promise;
2721
- } finally {
2722
- if (pendingRefreshes5.get(appId) === promise) {
2723
- pendingRefreshes5.delete(appId);
2717
+
2718
+ // src/translator.ts
2719
+ var TRANSLATOR_FLOOR = ARCHETYPE_FLOOR_DEFAULT;
2720
+ var RULE_SEQUENTIAL_TOOL_CLIFF = "sequential-tool-cliff-below-floor";
2721
+ var RULE_NARRATION_DRIFT_ANTHROPIC = "narration-drift-anthropic";
2722
+ var RULE_NARRATION_THINKING_LEAK_DEEPSEEK = "narration-thinking-leak-deepseek";
2723
+ 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.";
2724
+ var NARRATION_DRIFT_ANTHROPIC_PREAMBLE = "Output ONLY the requested content. Do not narrate your thought process. Each line \u2264 12 words.";
2725
+ 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.";
2726
+ var RULE_DISCIPLINE_GATES_V1 = "discipline-gates-v1";
2727
+ var DISCIPLINE_GATES_V1_WITH_TOOLS = `Work through these gates at every judgment point, explicitly:
2728
+ 1. Evidence before reasoning: cite what you observed before concluding from it.
2729
+ 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
2730
+ 3. Expand, don't guess: resolve a compressed or referenced item by looking it up rather than inferring its contents.
2731
+ 4. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.
2732
+ 5. Label each claim: mark it observed, inferred, or assumed.
2733
+ 6. A surfaced gap beats a guessed answer: flag what you cannot determine rather than fabricating past it.`;
2734
+ var DISCIPLINE_GATES_V1_NO_TOOLS = `Work through these gates at every judgment point, explicitly:
2735
+ 1. Evidence before reasoning: cite what you observed before concluding from it.
2736
+ 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
2737
+ 3. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.
2738
+ 4. Label each claim: mark it observed, inferred, or assumed.
2739
+ 5. A surfaced gap beats a guessed answer: flag what you cannot determine rather than fabricating past it.`;
2740
+ var RULE_DISCIPLINE_GATES_V1_STRUCTURED = "discipline-gates-v1-structured";
2741
+ var DISCIPLINE_GATES_V1_STRUCTURED_WITH_TOOLS = `Work through these gates at every judgment point, explicitly:
2742
+ 1. Evidence before reasoning: cite what you observed before concluding from it.
2743
+ 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
2744
+ 3. Expand, don't guess: resolve a compressed or referenced item by looking it up rather than inferring its contents.
2745
+ 4. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.`;
2746
+ var DISCIPLINE_GATES_V1_STRUCTURED_NO_TOOLS = `Work through these gates at every judgment point, explicitly:
2747
+ 1. Evidence before reasoning: cite what you observed before concluding from it.
2748
+ 2. One extra signal: when a finding feels conclusive, check one more adjacent signal before stating it.
2749
+ 3. Innocent explanation first: state the most plausible benign reading before alleging the alarming one.`;
2750
+ var DISCIPLINE_ELIGIBLE_ARCHETYPES = /* @__PURE__ */ new Set([
2751
+ "hunt",
2752
+ "summarize",
2753
+ "plan",
2754
+ "critique",
2755
+ "judge"
2756
+ ]);
2757
+ function matchRule(kind, profile, archetype, ctx) {
2758
+ if (kind === "discipline_contract") {
2759
+ if (!DISCIPLINE_ELIGIBLE_ARCHETYPES.has(archetype)) return null;
2760
+ if (ctx.outputMode !== "text") {
2761
+ return {
2762
+ id: RULE_DISCIPLINE_GATES_V1_STRUCTURED,
2763
+ preamble: ctx.hasTools ? DISCIPLINE_GATES_V1_STRUCTURED_WITH_TOOLS : DISCIPLINE_GATES_V1_STRUCTURED_NO_TOOLS
2764
+ };
2724
2765
  }
2766
+ return {
2767
+ id: RULE_DISCIPLINE_GATES_V1,
2768
+ preamble: ctx.hasTools ? DISCIPLINE_GATES_V1_WITH_TOOLS : DISCIPLINE_GATES_V1_NO_TOOLS
2769
+ };
2725
2770
  }
2726
- }
2727
- async function doRefresh5(rt, appId) {
2728
- const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
2729
- let snap = snapshots5.get(appId);
2730
- if (!snap) {
2731
- snap = { data: [], expiresAt: 0, refreshing: false };
2732
- snapshots5.set(appId, snap);
2771
+ if (kind === "tool_call_contract") {
2772
+ if (!profile.archetypePerf) return null;
2773
+ const archetypeScore = profile.archetypePerf[archetype];
2774
+ if (typeof archetypeScore !== "number" || archetypeScore >= TRANSLATOR_FLOOR) {
2775
+ return null;
2776
+ }
2777
+ return {
2778
+ id: RULE_SEQUENTIAL_TOOL_CLIFF,
2779
+ preamble: SEQUENTIAL_TOOL_PREAMBLE,
2780
+ wireOverrides: { parallelToolCalls: false }
2781
+ };
2733
2782
  }
2734
- try {
2735
- const res = await rt.fetchImpl(url, { method: "GET" });
2736
- if (!res.ok) {
2737
- throw new Error(`measured-failure ${res.status}: ${res.statusText}`);
2783
+ if (kind === "narration_contract") {
2784
+ if (profile.provider === "anthropic") {
2785
+ return {
2786
+ id: RULE_NARRATION_DRIFT_ANTHROPIC,
2787
+ preamble: NARRATION_DRIFT_ANTHROPIC_PREAMBLE
2788
+ };
2738
2789
  }
2739
- const body = await res.json();
2740
- if (runtime5 !== rt) return;
2741
- snap.data = Array.isArray(body) ? mapRows(body) : [];
2742
- snap.expiresAt = Date.now() + rt.ttlMs;
2743
- snap.refreshing = false;
2744
- } catch (err) {
2745
- if (runtime5 !== rt) return;
2746
- snap.refreshing = false;
2747
- snap.expiresAt = Date.now() + rt.ttlMs;
2748
- if (!warnedOnce5) {
2749
- warnedOnce5 = true;
2750
- (rt.onError ?? defaultOnError5)(err);
2790
+ if (profile.provider === "deepseek") {
2791
+ return {
2792
+ id: RULE_NARRATION_THINKING_LEAK_DEEPSEEK,
2793
+ preamble: NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE
2794
+ };
2751
2795
  }
2796
+ return null;
2752
2797
  }
2798
+ return null;
2753
2799
  }
2754
- function defaultOnError5(err) {
2755
- console.warn(
2756
- "[kgauto] measured-failure fetch failed (gate inactive until next refresh):",
2757
- err
2758
- );
2759
- }
2760
- function _testResetMeasuredFailure() {
2761
- runtime5 = void 0;
2762
- snapshots5.clear();
2763
- pendingRefreshes5 = /* @__PURE__ */ new Map();
2764
- warnedOnce5 = false;
2765
- }
2766
- async function _testWaitForMeasuredFailureRefresh() {
2767
- const pending = Array.from(pendingRefreshes5.values());
2768
- if (pending.length > 0) await Promise.all(pending);
2800
+ function applySectionRewrites(args) {
2801
+ const { ir, profile, archetype } = args;
2802
+ if (!Array.isArray(ir.sections) || ir.sections.length === 0) {
2803
+ return { rewrittenIR: ir, rewrites: [] };
2804
+ }
2805
+ const outputMode = args.outputMode ?? resolveOutputMode({
2806
+ declared: ir.constraints?.outputMode,
2807
+ structuredOutput: ir.constraints?.structuredOutput,
2808
+ toolCount: ir.tools?.length ?? 0
2809
+ });
2810
+ const hasTools = (ir.tools?.length ?? 0) > 0;
2811
+ const ctx = { outputMode, hasTools };
2812
+ const rewrites = [];
2813
+ const newSections = ir.sections.map((section) => {
2814
+ if (!section.kind || section.kind === "arbitrary") return section;
2815
+ const rule = matchRule(section.kind, profile, archetype, ctx);
2816
+ if (!rule) return section;
2817
+ const originalText = section.text;
2818
+ const transformedText = `${rule.preamble}
2819
+
2820
+ ${originalText}`;
2821
+ rewrites.push({
2822
+ sectionId: section.id,
2823
+ kind: section.kind,
2824
+ rule: rule.id,
2825
+ originalText,
2826
+ transformedText,
2827
+ ...rule.wireOverrides ? { wireOverrides: rule.wireOverrides } : {}
2828
+ });
2829
+ return { ...section, text: transformedText };
2830
+ });
2831
+ if (rewrites.length === 0) {
2832
+ return { rewrittenIR: ir, rewrites: [] };
2833
+ }
2834
+ const rewrittenIR = { ...ir, sections: newSections };
2835
+ return { rewrittenIR, rewrites };
2769
2836
  }
2770
2837
 
2771
2838
  // src/compile.ts
@@ -3895,6 +3962,17 @@ function parseJsonLoose(raw) {
3895
3962
  var FAILED = /* @__PURE__ */ Symbol("parse-failed");
3896
3963
 
3897
3964
  // src/ir.ts
3965
+ function mutationId(m) {
3966
+ return typeof m === "string" ? m : m.id;
3967
+ }
3968
+ function hasMutation(list, idOrPrefix) {
3969
+ if (!Array.isArray(list)) return false;
3970
+ const prefix = idOrPrefix.endsWith("*") ? idOrPrefix.slice(0, -1) : void 0;
3971
+ return list.some((m) => {
3972
+ const id = mutationId(m);
3973
+ return prefix !== void 0 ? id.startsWith(prefix) : id === idOrPrefix;
3974
+ });
3975
+ }
3898
3976
  var CallError = class extends Error {
3899
3977
  attempts;
3900
3978
  lastErrorCode;
@@ -3948,12 +4026,46 @@ async function captureGoldenIr(ctx) {
3948
4026
  }
3949
4027
 
3950
4028
  // src/streaming.ts
4029
+ function classifyThrownFetchError(err) {
4030
+ const name = err?.name;
4031
+ if (name === "TimeoutError") {
4032
+ return { errorType: "retryable", errorCode: "timeout", message: String(err) };
4033
+ }
4034
+ if (name === "AbortError") {
4035
+ return { errorType: "terminal", errorCode: "aborted", message: "aborted by caller signal" };
4036
+ }
4037
+ return { errorType: "retryable", errorCode: "network_error", message: String(err) };
4038
+ }
4039
+ function createStallGuard(external, stallTimeoutMs) {
4040
+ const ctl = new AbortController();
4041
+ if (external) {
4042
+ if (external.aborted) ctl.abort(external.reason);
4043
+ else external.addEventListener("abort", () => ctl.abort(external.reason), { once: true });
4044
+ }
4045
+ let timer;
4046
+ const arm = () => {
4047
+ if (!stallTimeoutMs || stallTimeoutMs <= 0) return;
4048
+ if (timer) clearTimeout(timer);
4049
+ timer = setTimeout(() => {
4050
+ ctl.abort(
4051
+ new DOMException(`stream stalled: no bytes for ${stallTimeoutMs}ms`, "TimeoutError")
4052
+ );
4053
+ }, stallTimeoutMs);
4054
+ };
4055
+ const clear = () => {
4056
+ if (timer) clearTimeout(timer);
4057
+ timer = void 0;
4058
+ };
4059
+ return { signal: ctl.signal, arm, clear };
4060
+ }
3951
4061
  var ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
3952
4062
  async function streamAnthropic(request, apiKey, opts) {
3953
4063
  const { provider: _provider, ...body } = request;
3954
4064
  const fetchFn = opts.fetchImpl ?? fetch;
4065
+ const guard = createStallGuard(opts.signal, opts.stallTimeoutMs);
3955
4066
  let res;
3956
4067
  try {
4068
+ guard.arm();
3957
4069
  res = await fetchFn(ANTHROPIC_URL, {
3958
4070
  method: "POST",
3959
4071
  headers: {
@@ -3961,12 +4073,16 @@ async function streamAnthropic(request, apiKey, opts) {
3961
4073
  "anthropic-version": "2023-06-01",
3962
4074
  "content-type": "application/json"
3963
4075
  },
3964
- body: JSON.stringify({ ...body, stream: true })
4076
+ body: JSON.stringify({ ...body, stream: true }),
4077
+ signal: guard.signal
3965
4078
  });
3966
4079
  } catch (err) {
3967
- return retryableError(0, "network_error", String(err), null);
4080
+ guard.clear();
4081
+ const c = classifyThrownFetchError(err);
4082
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
3968
4083
  }
3969
4084
  if (!res.ok) {
4085
+ guard.clear();
3970
4086
  const errBody = await res.json().catch(() => ({}));
3971
4087
  return classifyHttpError(res.status, errBody);
3972
4088
  }
@@ -4029,9 +4145,13 @@ async function streamAnthropic(request, apiKey, opts) {
4029
4145
  if (typeof p.usage?.output_tokens === "number") outputTokens = p.usage.output_tokens;
4030
4146
  return;
4031
4147
  }
4032
- });
4148
+ }, guard.arm, guard.signal);
4033
4149
  } catch (err) {
4034
- return retryableError(0, "stream_interrupted", String(err), null);
4150
+ const c = classifyThrownFetchError(err);
4151
+ const code = c.errorCode === "network_error" ? "stream_interrupted" : c.errorCode;
4152
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: code, message: c.message, raw: null };
4153
+ } finally {
4154
+ guard.clear();
4035
4155
  }
4036
4156
  const toolCalls = Array.from(toolBlocks.values()).map((b) => ({
4037
4157
  id: b.id,
@@ -4058,6 +4178,7 @@ async function streamAnthropic(request, apiKey, opts) {
4058
4178
  async function streamOpenAILike(url, request, apiKey, providerLabel, opts) {
4059
4179
  const { provider: _provider, ...body } = request;
4060
4180
  const fetchFn = opts.fetchImpl ?? fetch;
4181
+ const guard = createStallGuard(opts.signal, opts.stallTimeoutMs);
4061
4182
  const reqBody = {
4062
4183
  ...body,
4063
4184
  stream: true,
@@ -4068,18 +4189,23 @@ async function streamOpenAILike(url, request, apiKey, providerLabel, opts) {
4068
4189
  };
4069
4190
  let res;
4070
4191
  try {
4192
+ guard.arm();
4071
4193
  res = await fetchFn(url, {
4072
4194
  method: "POST",
4073
4195
  headers: {
4074
4196
  authorization: `Bearer ${apiKey}`,
4075
4197
  "content-type": "application/json"
4076
4198
  },
4077
- body: JSON.stringify(reqBody)
4199
+ body: JSON.stringify(reqBody),
4200
+ signal: guard.signal
4078
4201
  });
4079
4202
  } catch (err) {
4080
- return retryableError(0, "network_error", String(err), null);
4203
+ guard.clear();
4204
+ const c = classifyThrownFetchError(err);
4205
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
4081
4206
  }
4082
4207
  if (!res.ok) {
4208
+ guard.clear();
4083
4209
  const errBody = await res.json().catch(() => ({}));
4084
4210
  return classifyHttpError(res.status, errBody);
4085
4211
  }
@@ -4132,9 +4258,13 @@ async function streamOpenAILike(url, request, apiKey, providerLabel, opts) {
4132
4258
  const details = usage.prompt_tokens_details;
4133
4259
  if (typeof details?.cached_tokens === "number") cachedTokens = details.cached_tokens;
4134
4260
  }
4135
- });
4261
+ }, guard.arm, guard.signal);
4136
4262
  } catch (err) {
4137
- return retryableError(0, "stream_interrupted", String(err), null);
4263
+ const c = classifyThrownFetchError(err);
4264
+ const code = c.errorCode === "network_error" ? "stream_interrupted" : c.errorCode;
4265
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: code, message: c.message, raw: null };
4266
+ } finally {
4267
+ guard.clear();
4138
4268
  }
4139
4269
  const toolCalls = Array.from(toolBuffers.values()).filter((b) => b.name.length > 0).map((b) => ({
4140
4270
  id: b.id,
@@ -4157,15 +4287,28 @@ async function streamOpenAILike(url, request, apiKey, providerLabel, opts) {
4157
4287
  };
4158
4288
  return { ok: true, status: res.status, response };
4159
4289
  }
4160
- async function parseSSEStream(response, handler) {
4290
+ async function parseSSEStream(response, handler, onRead, signal) {
4161
4291
  const body = response.body;
4162
4292
  if (!body) throw new Error("Response has no body for SSE parse");
4163
4293
  const reader = body.getReader();
4164
4294
  const decoder = new TextDecoder("utf-8");
4165
4295
  let buffer = "";
4296
+ const abortRace = signal ? new Promise((_resolve, reject) => {
4297
+ if (signal.aborted) return reject(signal.reason);
4298
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
4299
+ }) : void 0;
4166
4300
  for (; ; ) {
4167
- const { value, done } = await reader.read();
4301
+ let readResult;
4302
+ try {
4303
+ readResult = abortRace ? await Promise.race([reader.read(), abortRace]) : await reader.read();
4304
+ } catch (err) {
4305
+ void reader.cancel().catch(() => {
4306
+ });
4307
+ throw err;
4308
+ }
4309
+ const { value, done } = readResult;
4168
4310
  if (done) break;
4311
+ onRead?.();
4169
4312
  buffer += decoder.decode(value, { stream: true });
4170
4313
  let sep;
4171
4314
  while (sep = buffer.indexOf("\n\n"), sep !== -1) {
@@ -4222,8 +4365,33 @@ function isAuthSignatureBody(body, message) {
4222
4365
  const m = message.toLowerCase();
4223
4366
  return m.includes("api key not valid") || m.includes("invalid api key") || m.includes("invalid x-api-key") || m.includes("incorrect api key");
4224
4367
  }
4368
+ function isBillingExhaustedBody(status, body, message) {
4369
+ if (status === 402) return true;
4370
+ const m = message.toLowerCase();
4371
+ if (m.includes("free_tier") || m.includes("free tier")) return false;
4372
+ if (m.includes("credit balance is too low")) return true;
4373
+ if (m.includes("insufficient balance")) return true;
4374
+ if (m.includes("billingnotenabled") || m.includes("billing not enabled")) return true;
4375
+ if (body && typeof body === "object") {
4376
+ const err = body.error;
4377
+ if (err && typeof err === "object") {
4378
+ const e = err;
4379
+ if (e.code === "insufficient_quota" || e.type === "insufficient_quota") return true;
4380
+ if (Array.isArray(e.details)) {
4381
+ for (const d of e.details) {
4382
+ if (d && typeof d === "object" && d.reason === "BILLING_DISABLED") {
4383
+ return true;
4384
+ }
4385
+ }
4386
+ }
4387
+ }
4388
+ }
4389
+ return false;
4390
+ }
4225
4391
  function classifyHttpError(status, body) {
4226
4392
  const message = extractErrorMessage(body) ?? `HTTP ${status}`;
4393
+ if (isBillingExhaustedBody(status, body, message))
4394
+ return { ok: false, status, errorType: "terminal", errorCode: "billing_exhausted", message, raw: body };
4227
4395
  if (status === 429)
4228
4396
  return { ok: false, status, errorType: "retryable", errorCode: "rate_limit", message, raw: body };
4229
4397
  if (status === 408)
@@ -4251,9 +4419,6 @@ function extractErrorMessage(body) {
4251
4419
  if (typeof b.message === "string") return b.message;
4252
4420
  return void 0;
4253
4421
  }
4254
- function retryableError(status, code, message, raw) {
4255
- return { ok: false, status, errorType: "retryable", errorCode: code, message, raw };
4256
- }
4257
4422
 
4258
4423
  // src/execute.ts
4259
4424
  var ANTHROPIC_URL2 = "https://api.anthropic.com/v1/messages";
@@ -4298,7 +4463,9 @@ async function executeAnthropic(request, opts) {
4298
4463
  if (opts.onChunk) {
4299
4464
  return streamAnthropic(request, apiKey, {
4300
4465
  onChunk: opts.onChunk,
4301
- fetchImpl: opts.fetchImpl
4466
+ fetchImpl: opts.fetchImpl,
4467
+ signal: opts.signal,
4468
+ stallTimeoutMs: opts.stallTimeoutMs
4302
4469
  });
4303
4470
  }
4304
4471
  const { provider: _provider, ...body } = request;
@@ -4313,11 +4480,13 @@ async function executeAnthropic(request, opts) {
4313
4480
  "anthropic-version": "2023-06-01",
4314
4481
  "content-type": "application/json"
4315
4482
  },
4316
- body: JSON.stringify(body)
4483
+ body: JSON.stringify(body),
4484
+ signal: opts.signal
4317
4485
  });
4318
4486
  json = await res.json().catch(() => ({}));
4319
4487
  } catch (err) {
4320
- return retryableError2(0, "network_error", String(err), null);
4488
+ const c = classifyThrownFetchError(err);
4489
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
4321
4490
  }
4322
4491
  if (!res.ok) return classifyHttpError2(res.status, json);
4323
4492
  return { ok: true, status: res.status, response: normalizeAnthropic(json) };
@@ -4349,11 +4518,13 @@ async function executeGoogle(request, opts) {
4349
4518
  res = await fetchFn(url, {
4350
4519
  method: "POST",
4351
4520
  headers: { "content-type": "application/json" },
4352
- body: JSON.stringify(body)
4521
+ body: JSON.stringify(body),
4522
+ signal: opts.signal
4353
4523
  });
4354
4524
  json = await res.json().catch(() => ({}));
4355
4525
  } catch (err) {
4356
- return retryableError2(0, "network_error", String(err), null);
4526
+ const c = classifyThrownFetchError(err);
4527
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
4357
4528
  }
4358
4529
  if (!res.ok) return classifyHttpError2(res.status, json);
4359
4530
  return { ok: true, status: res.status, response: normalizeGoogle(json) };
@@ -4385,7 +4556,9 @@ async function executeOpenAI(request, opts) {
4385
4556
  if (opts.onChunk) {
4386
4557
  return streamOpenAILike(OPENAI_URL, request, apiKey, "openai", {
4387
4558
  onChunk: opts.onChunk,
4388
- fetchImpl: opts.fetchImpl
4559
+ fetchImpl: opts.fetchImpl,
4560
+ signal: opts.signal,
4561
+ stallTimeoutMs: opts.stallTimeoutMs
4389
4562
  });
4390
4563
  }
4391
4564
  const { provider: _provider, ...body } = request;
@@ -4396,11 +4569,13 @@ async function executeOpenAI(request, opts) {
4396
4569
  res = await fetchFn(OPENAI_URL, {
4397
4570
  method: "POST",
4398
4571
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
4399
- body: JSON.stringify(body)
4572
+ body: JSON.stringify(body),
4573
+ signal: opts.signal
4400
4574
  });
4401
4575
  json = await res.json().catch(() => ({}));
4402
4576
  } catch (err) {
4403
- return retryableError2(0, "network_error", String(err), null);
4577
+ const c = classifyThrownFetchError(err);
4578
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
4404
4579
  }
4405
4580
  if (!res.ok) return classifyHttpError2(res.status, json);
4406
4581
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -4413,7 +4588,9 @@ async function executeDeepSeek(request, opts) {
4413
4588
  if (opts.onChunk) {
4414
4589
  return streamOpenAILike(DEEPSEEK_URL, request, apiKey, "deepseek", {
4415
4590
  onChunk: opts.onChunk,
4416
- fetchImpl: opts.fetchImpl
4591
+ fetchImpl: opts.fetchImpl,
4592
+ signal: opts.signal,
4593
+ stallTimeoutMs: opts.stallTimeoutMs
4417
4594
  });
4418
4595
  }
4419
4596
  const { provider: _provider, ...body } = request;
@@ -4424,11 +4601,13 @@ async function executeDeepSeek(request, opts) {
4424
4601
  res = await fetchFn(DEEPSEEK_URL, {
4425
4602
  method: "POST",
4426
4603
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
4427
- body: JSON.stringify(body)
4604
+ body: JSON.stringify(body),
4605
+ signal: opts.signal
4428
4606
  });
4429
4607
  json = await res.json().catch(() => ({}));
4430
4608
  } catch (err) {
4431
- return retryableError2(0, "network_error", String(err), null);
4609
+ const c = classifyThrownFetchError(err);
4610
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
4432
4611
  }
4433
4612
  if (!res.ok) return classifyHttpError2(res.status, json);
4434
4613
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -4441,7 +4620,9 @@ async function executeOpenAICompatible(request, opts, spec) {
4441
4620
  if (opts.onChunk) {
4442
4621
  return streamOpenAILike(spec.url, request, apiKey, spec.provider, {
4443
4622
  onChunk: opts.onChunk,
4444
- fetchImpl: opts.fetchImpl
4623
+ fetchImpl: opts.fetchImpl,
4624
+ signal: opts.signal,
4625
+ stallTimeoutMs: opts.stallTimeoutMs
4445
4626
  });
4446
4627
  }
4447
4628
  const { provider: _provider, ...body } = request;
@@ -4452,11 +4633,13 @@ async function executeOpenAICompatible(request, opts, spec) {
4452
4633
  res = await fetchFn(spec.url, {
4453
4634
  method: "POST",
4454
4635
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
4455
- body: JSON.stringify(body)
4636
+ body: JSON.stringify(body),
4637
+ signal: opts.signal
4456
4638
  });
4457
4639
  json = await res.json().catch(() => ({}));
4458
4640
  } catch (err) {
4459
- return retryableError2(0, "network_error", String(err), null);
4641
+ const c = classifyThrownFetchError(err);
4642
+ return { ok: false, status: 0, errorType: c.errorType, errorCode: c.errorCode, message: c.message, raw: null };
4460
4643
  }
4461
4644
  if (!res.ok) return classifyHttpError2(res.status, json);
4462
4645
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -4487,6 +4670,9 @@ function applyOverrides(request, overrides) {
4487
4670
  }
4488
4671
  function classifyHttpError2(status, body) {
4489
4672
  const message = extractErrorMessage2(body) ?? `HTTP ${status}`;
4673
+ if (isBillingExhaustedBody(status, body, message)) {
4674
+ return { ok: false, status, errorType: "terminal", errorCode: "billing_exhausted", message, raw: body };
4675
+ }
4490
4676
  if (status === 429) {
4491
4677
  return { ok: false, status, errorType: "retryable", errorCode: "rate_limit", message, raw: body };
4492
4678
  }
@@ -4523,9 +4709,6 @@ function extractErrorMessage2(body) {
4523
4709
  function terminalError(status, code, message) {
4524
4710
  return { ok: false, status, errorType: "terminal", errorCode: code, message, raw: null };
4525
4711
  }
4526
- function retryableError2(status, code, message, raw) {
4527
- return { ok: false, status, errorType: "retryable", errorCode: code, message, raw };
4528
- }
4529
4712
  function tryParseJson2(s) {
4530
4713
  if (typeof s !== "string" || s.length === 0) return void 0;
4531
4714
  try {
@@ -4675,18 +4858,28 @@ async function call(ir, opts = {}) {
4675
4858
  }
4676
4859
  let activeCompile = initial;
4677
4860
  let lastErr;
4678
- const failedProviders = /* @__PURE__ */ new Set();
4861
+ const failedProviders = /* @__PURE__ */ new Map();
4679
4862
  const sameModelRetryEnabled = opts.sameModelRetry ?? isSameModelRetryEnabledFromEnv();
4680
4863
  let retriedSameModel = false;
4681
4864
  for (let i = 0; i < targetsToTry.length; i++) {
4682
4865
  const targetModel = targetsToTry[i];
4866
+ if (opts.abortSignal?.aborted) {
4867
+ attempts.push({
4868
+ model: targetModel,
4869
+ status: "terminal",
4870
+ errorCode: "aborted",
4871
+ message: "Skipped \u2014 caller abortSignal fired before this attempt started"
4872
+ });
4873
+ break;
4874
+ }
4683
4875
  const targetProfile = tryGetProfile(targetModel);
4684
- if (targetProfile && failedProviders.has(targetProfile.provider) && !opts.noFallback) {
4876
+ const providerFailReason = targetProfile ? failedProviders.get(targetProfile.provider) : void 0;
4877
+ if (targetProfile && providerFailReason && !opts.noFallback) {
4685
4878
  attempts.push({
4686
4879
  model: targetModel,
4687
4880
  status: "terminal",
4688
- errorCode: "auth_inferred",
4689
- message: `Skipped \u2014 provider ${targetProfile.provider} returned 401/403 earlier in this call; same key inferred to fail`
4881
+ errorCode: `${providerFailReason}_inferred`,
4882
+ message: providerFailReason === "billing_exhausted" ? `Skipped \u2014 provider ${targetProfile.provider} is out of credits (seen earlier in this call); a billing failure is account-wide, so this attempt cannot succeed until a human funds the account` : `Skipped \u2014 provider ${targetProfile.provider} returned 401/403 earlier in this call; same key inferred to fail`
4690
4883
  });
4691
4884
  continue;
4692
4885
  }
@@ -4715,16 +4908,29 @@ async function call(ir, opts = {}) {
4715
4908
  );
4716
4909
  const targetSupportsStreaming = targetProfile?.streaming === true;
4717
4910
  const streamingOnChunk = opts.onChunk && !opts.noStream && targetSupportsStreaming ? opts.onChunk : void 0;
4718
- const execOpts = {
4911
+ const mkExecOpts = () => ({
4719
4912
  apiKeys: opts.apiKeys,
4720
4913
  fetchImpl: opts.fetchImpl,
4721
4914
  providerOverrides: opts.providerOverrides,
4722
- onChunk: streamingOnChunk
4723
- };
4724
- const exec = await execute(activeCompile.request, execOpts);
4915
+ onChunk: streamingOnChunk,
4916
+ signal: composeAttemptSignal(
4917
+ streamingOnChunk ? void 0 : opts.attemptTimeoutMs,
4918
+ opts.abortSignal
4919
+ ),
4920
+ stallTimeoutMs: streamingOnChunk ? opts.attemptTimeoutMs : void 0
4921
+ });
4922
+ const exec = await execute(activeCompile.request, mkExecOpts());
4725
4923
  let validated = exec.ok ? validateStructuredContract(exec, ir) : exec;
4726
4924
  let servedByRetry = false;
4727
- if (!validated.ok && isStructuredContractViolation(validated.errorCode) && sameModelRetryEnabled && !retriedSameModel) {
4925
+ let retrySuppressionNote;
4926
+ if (!validated.ok && isStructuredContractViolation(validated.errorCode) && sameModelRetryEnabled && !retriedSameModel && getMeasuredFailureVerdict({
4927
+ appId: ir.appId,
4928
+ archetype: ir.intent.archetype,
4929
+ model: targetModel
4930
+ })?.gated === true) {
4931
+ 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]";
4932
+ }
4933
+ if (!validated.ok && isStructuredContractViolation(validated.errorCode) && sameModelRetryEnabled && !retriedSameModel && !retrySuppressionNote) {
4728
4934
  retriedSameModel = true;
4729
4935
  attempts.push({
4730
4936
  model: targetModel,
@@ -4736,7 +4942,7 @@ async function call(ir, opts = {}) {
4736
4942
  safeEmit(
4737
4943
  () => emitExecuteAttempt(traceId, ir.appId, { model: targetModel, attemptIndex: i })
4738
4944
  );
4739
- const retryExec = await execute(retryRequest, execOpts);
4945
+ const retryExec = await execute(retryRequest, mkExecOpts());
4740
4946
  validated = retryExec.ok ? validateStructuredContract(retryExec, ir) : retryExec;
4741
4947
  servedByRetry = true;
4742
4948
  }
@@ -4863,20 +5069,23 @@ async function call(ir, opts = {}) {
4863
5069
  model: targetModel,
4864
5070
  status: validated.errorType,
4865
5071
  errorCode: validated.errorCode,
4866
- message: validated.message,
5072
+ message: retrySuppressionNote ? validated.message + retrySuppressionNote : validated.message,
4867
5073
  ...servedByRetry ? { sameModelRetry: true } : {}
4868
5074
  });
4869
5075
  lastErr = validated;
4870
5076
  if (validated.errorType === "terminal" || opts.noFallback) {
4871
- if (validated.errorCode === "auth" && !opts.noFallback && activeCompile.provider) {
4872
- failedProviders.add(activeCompile.provider);
5077
+ if ((validated.errorCode === "auth" || validated.errorCode === "billing_exhausted") && !opts.noFallback && activeCompile.provider) {
5078
+ failedProviders.set(
5079
+ activeCompile.provider,
5080
+ validated.errorCode === "billing_exhausted" ? "billing_exhausted" : "auth"
5081
+ );
4873
5082
  continue;
4874
5083
  }
4875
5084
  break;
4876
5085
  }
4877
5086
  }
4878
5087
  const lastAttempted = [...attempts].reverse().find(
4879
- (a) => a.status !== "success" && a.errorCode !== "auth_inferred" && a.errorCode !== "compile_error"
5088
+ (a) => a.status !== "success" && a.errorCode !== "auth_inferred" && a.errorCode !== "billing_exhausted_inferred" && a.errorCode !== "compile_error"
4880
5089
  );
4881
5090
  const latencyMs = Date.now() - start;
4882
5091
  await record({
@@ -4899,6 +5108,25 @@ async function call(ir, opts = {}) {
4899
5108
  lastErr?.errorCode
4900
5109
  );
4901
5110
  }
5111
+ function composeAttemptSignal(attemptTimeoutMs, callerSignal) {
5112
+ const signals = [];
5113
+ if (callerSignal) signals.push(callerSignal);
5114
+ if (typeof attemptTimeoutMs === "number" && attemptTimeoutMs > 0) {
5115
+ signals.push(AbortSignal.timeout(attemptTimeoutMs));
5116
+ }
5117
+ if (signals.length === 0) return void 0;
5118
+ if (signals.length === 1) return signals[0];
5119
+ if (typeof AbortSignal.any === "function") return AbortSignal.any(signals);
5120
+ const ctl = new AbortController();
5121
+ for (const s of signals) {
5122
+ if (s.aborted) {
5123
+ ctl.abort(s.reason);
5124
+ break;
5125
+ }
5126
+ s.addEventListener("abort", () => ctl.abort(s.reason), { once: true });
5127
+ }
5128
+ return ctl.signal;
5129
+ }
4902
5130
  function compileAndRegister(ir, opts) {
4903
5131
  const result = compile(ir, {
4904
5132
  policy: opts.policy,
@@ -5198,6 +5426,9 @@ function normalizeFallbackReason(attempts) {
5198
5426
  }
5199
5427
  if (code === "cost_cap_exceeded") return "cost_cap";
5200
5428
  if (code === "auth" || code === "auth_inferred") return "provider_auth_failed";
5429
+ if (code === "billing_exhausted" || code === "billing_exhausted_inferred") {
5430
+ return "provider_billing_exhausted";
5431
+ }
5201
5432
  return "provider_error";
5202
5433
  }
5203
5434
  function generateTraceId() {
@@ -5950,6 +6181,12 @@ async function runGoldenEval(opts) {
5950
6181
  );
5951
6182
  }
5952
6183
  }
6184
+ if (wins + ties + losses === 0) {
6185
+ notes.push(
6186
+ "zero cases judged \u2014 run row persisted, evidence advisory deliberately NOT written (a verdict on n=0 is not evidence)"
6187
+ );
6188
+ return result;
6189
+ }
5953
6190
  const latestRes = await fetchFn(
5954
6191
  rest(
5955
6192
  `compile_outcomes?app_id=eq.${encodeURIComponent(opts.appId)}&select=id&order=id.desc&limit=1`
@@ -6177,19 +6414,32 @@ function resolveFetch(injected) {
6177
6414
  function normalizeEndpoint2(endpoint) {
6178
6415
  return endpoint.replace(/\/+$/, "");
6179
6416
  }
6417
+ function resolveBrainReadTrio(opts) {
6418
+ const env = readBrainReadEnv();
6419
+ const brainEndpoint = opts.brainEndpoint ?? env.endpoint;
6420
+ const brainJwt = opts.brainJwt ?? env.jwt;
6421
+ const brainAnonKey = opts.brainAnonKey ?? env.anonKey;
6422
+ if (brainEndpoint && brainJwt && brainAnonKey) {
6423
+ return { ok: true, brainEndpoint, brainJwt, brainAnonKey };
6424
+ }
6425
+ const missingEnv = [];
6426
+ if (!brainEndpoint) missingEnv.push("KGAUTO_V2_BRAIN_SUPABASE_URL");
6427
+ if (!brainJwt) missingEnv.push("KGAUTO_V2_BRAIN_JWT");
6428
+ if (!brainAnonKey) missingEnv.push("KGAUTO_V2_BRAIN_ANON_KEY");
6429
+ return { ok: false, missingEnv };
6430
+ }
6180
6431
  async function getActionableAdvisories(opts) {
6181
- const {
6182
- appId,
6183
- severity,
6184
- status,
6185
- brainEndpoint,
6186
- brainJwt,
6187
- brainAnonKey,
6188
- fetch: injectedFetch
6189
- } = opts;
6432
+ const { appId, severity, status, fetch: injectedFetch } = opts;
6190
6433
  if (!appId) {
6191
6434
  throw new Error("getActionableAdvisories: appId is required");
6192
6435
  }
6436
+ const trio = resolveBrainReadTrio(opts);
6437
+ if (!trio.ok) {
6438
+ throw new Error(
6439
+ `getActionableAdvisories: brain-read-not-configured \u2014 missing ${trio.missingEnv.join(", ")} (pass brainEndpoint/brainJwt/brainAnonKey explicitly or set the canonical env vars)`
6440
+ );
6441
+ }
6442
+ const { brainEndpoint, brainJwt, brainAnonKey } = trio;
6193
6443
  const doFetch = resolveFetch(injectedFetch);
6194
6444
  const base = normalizeEndpoint2(brainEndpoint);
6195
6445
  const qs = new URLSearchParams();
@@ -6242,17 +6492,15 @@ async function getActionableAdvisories(opts) {
6242
6492
  return out;
6243
6493
  }
6244
6494
  async function markAdvisoryResolved(opts) {
6245
- const {
6246
- id,
6247
- resolutionNote,
6248
- brainEndpoint,
6249
- brainJwt,
6250
- brainAnonKey,
6251
- fetch: injectedFetch
6252
- } = opts;
6495
+ const { id, resolutionNote, fetch: injectedFetch } = opts;
6253
6496
  if (!id) {
6254
6497
  return { ok: false, reason: "id_required" };
6255
6498
  }
6499
+ const trio = resolveBrainReadTrio(opts);
6500
+ if (!trio.ok) {
6501
+ return { ok: false, reason: `brain_read_not_configured:${trio.missingEnv.join(",")}` };
6502
+ }
6503
+ const { brainEndpoint, brainJwt, brainAnonKey } = trio;
6256
6504
  const doFetch = resolveFetch(injectedFetch);
6257
6505
  const base = normalizeEndpoint2(brainEndpoint);
6258
6506
  const lookupUrl = `${base}/rest/v1/actionable_advisories_v?id=eq.${encodeURIComponent(id)}&select=app_id,rule`;
@@ -6337,7 +6585,7 @@ async function markAdvisoryResolved(opts) {
6337
6585
  }
6338
6586
  }
6339
6587
  if (outcomeIds.length === 0) {
6340
- return { ok: true };
6588
+ return { ok: true, firingsResolved: 0, status: "unknown" };
6341
6589
  }
6342
6590
  const inList = outcomeIds.join(",");
6343
6591
  const patchUrl = `${base}/rest/v1/compile_outcome_advisories?outcome_id=in.(${inList})&code=eq.${encodeURIComponent(code)}&resolved_at=is.null`;
@@ -6357,8 +6605,10 @@ async function markAdvisoryResolved(opts) {
6357
6605
  apikey: brainAnonKey,
6358
6606
  "Content-Type": "application/json",
6359
6607
  Accept: "application/json",
6360
- // PostgREST default is no return; we don't need the row back.
6361
- Prefer: "return=minimal"
6608
+ // alpha.78 count what actually changed. An RLS-filtered PATCH
6609
+ // returns 2xx with zero rows; return=representation makes that
6610
+ // visible instead of success-shaped.
6611
+ Prefer: "return=representation"
6362
6612
  },
6363
6613
  body: JSON.stringify(patchBody)
6364
6614
  });
@@ -6375,7 +6625,33 @@ async function markAdvisoryResolved(opts) {
6375
6625
  if (!patchRes.ok) {
6376
6626
  return { ok: false, reason: `patch_failed:${patchRes.status}` };
6377
6627
  }
6378
- return { ok: true };
6628
+ let patchedRows = [];
6629
+ try {
6630
+ patchedRows = await patchRes.json();
6631
+ } catch {
6632
+ }
6633
+ const firingsResolved = Array.isArray(patchedRows) ? patchedRows.length : 0;
6634
+ let status = "unknown";
6635
+ try {
6636
+ const statusRes = await doFetch(
6637
+ `${base}/rest/v1/actionable_advisories_v?id=eq.${encodeURIComponent(id)}&select=status`,
6638
+ {
6639
+ method: "GET",
6640
+ headers: {
6641
+ Authorization: `Bearer ${brainJwt}`,
6642
+ apikey: brainAnonKey,
6643
+ Accept: "application/json"
6644
+ }
6645
+ }
6646
+ );
6647
+ if (statusRes.ok) {
6648
+ const rows = await statusRes.json();
6649
+ const s = Array.isArray(rows) ? rows[0]?.status : void 0;
6650
+ if (s === "open" || s === "resolved") status = s;
6651
+ }
6652
+ } catch {
6653
+ }
6654
+ return { ok: true, firingsResolved, status };
6379
6655
  }
6380
6656
  async function markExclusionFindingHandled(opts) {
6381
6657
  const {
@@ -6384,12 +6660,14 @@ async function markExclusionFindingHandled(opts) {
6384
6660
  excludedModel,
6385
6661
  resolution,
6386
6662
  resolutionNote,
6387
- brainEndpoint,
6388
- brainJwt,
6389
- brainAnonKey,
6390
6663
  fetch: injectedFetch
6391
6664
  } = opts;
6392
6665
  if (!appId) return { ok: false, reason: "app_id_required" };
6666
+ const trio = resolveBrainReadTrio(opts);
6667
+ if (!trio.ok) {
6668
+ return { ok: false, reason: `brain_read_not_configured:${trio.missingEnv.join(",")}` };
6669
+ }
6670
+ const { brainEndpoint, brainJwt, brainAnonKey } = trio;
6393
6671
  if (!archetype) return { ok: false, reason: "archetype_required" };
6394
6672
  if (!excludedModel) {
6395
6673
  return { ok: false, reason: "excluded_model_required" };
@@ -6655,6 +6933,7 @@ export {
6655
6933
  ALL_ARCHETYPES,
6656
6934
  ARCHETYPE_FAMILY_FITS,
6657
6935
  ARCHETYPE_FLOOR_DEFAULT,
6936
+ BRAIN_READ_ENV_NAMES,
6658
6937
  COACH_CFG,
6659
6938
  CallError,
6660
6939
  DECOMPOSITION_TEMPLATES,
@@ -6673,6 +6952,7 @@ export {
6673
6952
  MEASURED_GROUNDING_MIN_N,
6674
6953
  PRODUCER_OWNED_RULE_CODES,
6675
6954
  PROVIDER_ENV_KEYS,
6955
+ ROLLBACK_SUPPRESSION_WINDOW_DAYS,
6676
6956
  RULE_DISCIPLINE_GATES_V1,
6677
6957
  RULE_DISCIPLINE_GATES_V1_STRUCTURED,
6678
6958
  RULE_SEQUENTIAL_TOOL_CLIFF,
@@ -6726,12 +7006,14 @@ export {
6726
7006
  getPerAxisMetrics,
6727
7007
  getProfile,
6728
7008
  getReachabilityDiagnostic,
7009
+ getRecentRollback,
6729
7010
  getRecommendedPrimary,
6730
7011
  getSequentialStarterChain,
6731
7012
  getSequentialStarterChainWithGrounding,
6732
7013
  getStaleExclusionFindings,
6733
7014
  getStarterChain,
6734
7015
  getStarterChainWithGrounding,
7016
+ hasMutation,
6735
7017
  hashShape,
6736
7018
  isArchetype,
6737
7019
  isAutoPromoteEnabledFromEnv,
@@ -6752,9 +7034,11 @@ export {
6752
7034
  loadChainsFromBrain,
6753
7035
  loadModelsFromBrain,
6754
7036
  loadPricingFromBrain,
7037
+ mapMeasuredFailureRows,
6755
7038
  markAdvisoryResolved,
6756
7039
  markExclusionFindingHandled,
6757
7040
  markPromoteReadyHandled,
7041
+ mutationId,
6758
7042
  parseGoldenCaptureRate,
6759
7043
  parseJudgeVerdict,
6760
7044
  peekBrainDeadLetter,