@warmdrift/kgauto-compiler 2.0.0-alpha.77 → 2.0.0-alpha.79

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-GB7VJQ6C.mjs";
19
+ } from "./chunk-DRYCOR6G.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
- };
2518
+ ];
2519
+ }
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 [];
2386
2525
  }
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
- };
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"
2393
2533
  }
2394
- if (profile.provider === "deepseek") {
2395
- return {
2396
- id: RULE_NARRATION_THINKING_LEAK_DEEPSEEK,
2397
- preamble: NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE
2398
- };
2534
+ ];
2535
+ }
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"
2399
2547
  }
2400
- return null;
2401
- }
2402
- return null;
2548
+ ];
2403
2549
  }
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: [] };
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;
2572
+ }
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 };
2587
+ }
2437
2588
  }
2438
- const rewrittenIR = { ...ir, sections: newSections };
2439
- return { rewrittenIR, rewrites };
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
+ ];
2440
2604
  }
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";
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
+ ];
2447
2620
  }
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;
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
+ }
2453
2640
  }
2454
- return null;
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
+ ];
2455
2653
  }
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
- });
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
+ ];
2475
2672
  }
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;
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
+ ];
2494
2683
  }
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);
2510
- }
2511
- return snap.data.find(
2512
- (p) => p.archetype === opts.archetype && p.mode === opts.mode
2513
- );
2514
- }
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);
2524
- }
2525
- }
2526
- }
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}`);
2538
- }
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);
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"
2552
2709
  }
2553
- }
2554
- }
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;
2710
+ ];
2566
2711
  }
2567
- async function _testWaitForPromotionsRefresh() {
2568
- const pending = Array.from(pendingRefreshes4.values());
2569
- if (pending.length > 0) await Promise.all(pending);
2712
+ function confidenceRank(c) {
2713
+ if (c === "high") return 3;
2714
+ if (c === "medium") return 2;
2715
+ return 1;
2570
2716
  }
2571
2717
 
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");
2585
- }
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
- });
2599
- }
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;
2660
- }
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);
2668
- }
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);
2676
- }
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
- }
2694
- }
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);
2714
- }
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);
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) {
@@ -4276,9 +4419,6 @@ function extractErrorMessage(body) {
4276
4419
  if (typeof b.message === "string") return b.message;
4277
4420
  return void 0;
4278
4421
  }
4279
- function retryableError(status, code, message, raw) {
4280
- return { ok: false, status, errorType: "retryable", errorCode: code, message, raw };
4281
- }
4282
4422
 
4283
4423
  // src/execute.ts
4284
4424
  var ANTHROPIC_URL2 = "https://api.anthropic.com/v1/messages";
@@ -4323,7 +4463,9 @@ async function executeAnthropic(request, opts) {
4323
4463
  if (opts.onChunk) {
4324
4464
  return streamAnthropic(request, apiKey, {
4325
4465
  onChunk: opts.onChunk,
4326
- fetchImpl: opts.fetchImpl
4466
+ fetchImpl: opts.fetchImpl,
4467
+ signal: opts.signal,
4468
+ stallTimeoutMs: opts.stallTimeoutMs
4327
4469
  });
4328
4470
  }
4329
4471
  const { provider: _provider, ...body } = request;
@@ -4338,11 +4480,13 @@ async function executeAnthropic(request, opts) {
4338
4480
  "anthropic-version": "2023-06-01",
4339
4481
  "content-type": "application/json"
4340
4482
  },
4341
- body: JSON.stringify(body)
4483
+ body: JSON.stringify(body),
4484
+ signal: opts.signal
4342
4485
  });
4343
4486
  json = await res.json().catch(() => ({}));
4344
4487
  } catch (err) {
4345
- 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 };
4346
4490
  }
4347
4491
  if (!res.ok) return classifyHttpError2(res.status, json);
4348
4492
  return { ok: true, status: res.status, response: normalizeAnthropic(json) };
@@ -4374,11 +4518,13 @@ async function executeGoogle(request, opts) {
4374
4518
  res = await fetchFn(url, {
4375
4519
  method: "POST",
4376
4520
  headers: { "content-type": "application/json" },
4377
- body: JSON.stringify(body)
4521
+ body: JSON.stringify(body),
4522
+ signal: opts.signal
4378
4523
  });
4379
4524
  json = await res.json().catch(() => ({}));
4380
4525
  } catch (err) {
4381
- 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 };
4382
4528
  }
4383
4529
  if (!res.ok) return classifyHttpError2(res.status, json);
4384
4530
  return { ok: true, status: res.status, response: normalizeGoogle(json) };
@@ -4410,7 +4556,9 @@ async function executeOpenAI(request, opts) {
4410
4556
  if (opts.onChunk) {
4411
4557
  return streamOpenAILike(OPENAI_URL, request, apiKey, "openai", {
4412
4558
  onChunk: opts.onChunk,
4413
- fetchImpl: opts.fetchImpl
4559
+ fetchImpl: opts.fetchImpl,
4560
+ signal: opts.signal,
4561
+ stallTimeoutMs: opts.stallTimeoutMs
4414
4562
  });
4415
4563
  }
4416
4564
  const { provider: _provider, ...body } = request;
@@ -4421,11 +4569,13 @@ async function executeOpenAI(request, opts) {
4421
4569
  res = await fetchFn(OPENAI_URL, {
4422
4570
  method: "POST",
4423
4571
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
4424
- body: JSON.stringify(body)
4572
+ body: JSON.stringify(body),
4573
+ signal: opts.signal
4425
4574
  });
4426
4575
  json = await res.json().catch(() => ({}));
4427
4576
  } catch (err) {
4428
- 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 };
4429
4579
  }
4430
4580
  if (!res.ok) return classifyHttpError2(res.status, json);
4431
4581
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -4438,7 +4588,9 @@ async function executeDeepSeek(request, opts) {
4438
4588
  if (opts.onChunk) {
4439
4589
  return streamOpenAILike(DEEPSEEK_URL, request, apiKey, "deepseek", {
4440
4590
  onChunk: opts.onChunk,
4441
- fetchImpl: opts.fetchImpl
4591
+ fetchImpl: opts.fetchImpl,
4592
+ signal: opts.signal,
4593
+ stallTimeoutMs: opts.stallTimeoutMs
4442
4594
  });
4443
4595
  }
4444
4596
  const { provider: _provider, ...body } = request;
@@ -4449,11 +4601,13 @@ async function executeDeepSeek(request, opts) {
4449
4601
  res = await fetchFn(DEEPSEEK_URL, {
4450
4602
  method: "POST",
4451
4603
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
4452
- body: JSON.stringify(body)
4604
+ body: JSON.stringify(body),
4605
+ signal: opts.signal
4453
4606
  });
4454
4607
  json = await res.json().catch(() => ({}));
4455
4608
  } catch (err) {
4456
- 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 };
4457
4611
  }
4458
4612
  if (!res.ok) return classifyHttpError2(res.status, json);
4459
4613
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -4466,7 +4620,9 @@ async function executeOpenAICompatible(request, opts, spec) {
4466
4620
  if (opts.onChunk) {
4467
4621
  return streamOpenAILike(spec.url, request, apiKey, spec.provider, {
4468
4622
  onChunk: opts.onChunk,
4469
- fetchImpl: opts.fetchImpl
4623
+ fetchImpl: opts.fetchImpl,
4624
+ signal: opts.signal,
4625
+ stallTimeoutMs: opts.stallTimeoutMs
4470
4626
  });
4471
4627
  }
4472
4628
  const { provider: _provider, ...body } = request;
@@ -4477,11 +4633,13 @@ async function executeOpenAICompatible(request, opts, spec) {
4477
4633
  res = await fetchFn(spec.url, {
4478
4634
  method: "POST",
4479
4635
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
4480
- body: JSON.stringify(body)
4636
+ body: JSON.stringify(body),
4637
+ signal: opts.signal
4481
4638
  });
4482
4639
  json = await res.json().catch(() => ({}));
4483
4640
  } catch (err) {
4484
- 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 };
4485
4643
  }
4486
4644
  if (!res.ok) return classifyHttpError2(res.status, json);
4487
4645
  return { ok: true, status: res.status, response: normalizeOpenAILike(json) };
@@ -4551,9 +4709,6 @@ function extractErrorMessage2(body) {
4551
4709
  function terminalError(status, code, message) {
4552
4710
  return { ok: false, status, errorType: "terminal", errorCode: code, message, raw: null };
4553
4711
  }
4554
- function retryableError2(status, code, message, raw) {
4555
- return { ok: false, status, errorType: "retryable", errorCode: code, message, raw };
4556
- }
4557
4712
  function tryParseJson2(s) {
4558
4713
  if (typeof s !== "string" || s.length === 0) return void 0;
4559
4714
  try {
@@ -4708,6 +4863,15 @@ async function call(ir, opts = {}) {
4708
4863
  let retriedSameModel = false;
4709
4864
  for (let i = 0; i < targetsToTry.length; i++) {
4710
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
+ }
4711
4875
  const targetProfile = tryGetProfile(targetModel);
4712
4876
  const providerFailReason = targetProfile ? failedProviders.get(targetProfile.provider) : void 0;
4713
4877
  if (targetProfile && providerFailReason && !opts.noFallback) {
@@ -4744,16 +4908,29 @@ async function call(ir, opts = {}) {
4744
4908
  );
4745
4909
  const targetSupportsStreaming = targetProfile?.streaming === true;
4746
4910
  const streamingOnChunk = opts.onChunk && !opts.noStream && targetSupportsStreaming ? opts.onChunk : void 0;
4747
- const execOpts = {
4911
+ const mkExecOpts = () => ({
4748
4912
  apiKeys: opts.apiKeys,
4749
4913
  fetchImpl: opts.fetchImpl,
4750
4914
  providerOverrides: opts.providerOverrides,
4751
- onChunk: streamingOnChunk
4752
- };
4753
- 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());
4754
4923
  let validated = exec.ok ? validateStructuredContract(exec, ir) : exec;
4755
4924
  let servedByRetry = false;
4756
- 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) {
4757
4934
  retriedSameModel = true;
4758
4935
  attempts.push({
4759
4936
  model: targetModel,
@@ -4765,7 +4942,7 @@ async function call(ir, opts = {}) {
4765
4942
  safeEmit(
4766
4943
  () => emitExecuteAttempt(traceId, ir.appId, { model: targetModel, attemptIndex: i })
4767
4944
  );
4768
- const retryExec = await execute(retryRequest, execOpts);
4945
+ const retryExec = await execute(retryRequest, mkExecOpts());
4769
4946
  validated = retryExec.ok ? validateStructuredContract(retryExec, ir) : retryExec;
4770
4947
  servedByRetry = true;
4771
4948
  }
@@ -4892,7 +5069,7 @@ async function call(ir, opts = {}) {
4892
5069
  model: targetModel,
4893
5070
  status: validated.errorType,
4894
5071
  errorCode: validated.errorCode,
4895
- message: validated.message,
5072
+ message: retrySuppressionNote ? validated.message + retrySuppressionNote : validated.message,
4896
5073
  ...servedByRetry ? { sameModelRetry: true } : {}
4897
5074
  });
4898
5075
  lastErr = validated;
@@ -4931,6 +5108,25 @@ async function call(ir, opts = {}) {
4931
5108
  lastErr?.errorCode
4932
5109
  );
4933
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
+ }
4934
5130
  function compileAndRegister(ir, opts) {
4935
5131
  const result = compile(ir, {
4936
5132
  policy: opts.policy,
@@ -5985,6 +6181,12 @@ async function runGoldenEval(opts) {
5985
6181
  );
5986
6182
  }
5987
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
+ }
5988
6190
  const latestRes = await fetchFn(
5989
6191
  rest(
5990
6192
  `compile_outcomes?app_id=eq.${encodeURIComponent(opts.appId)}&select=id&order=id.desc&limit=1`
@@ -6141,6 +6343,171 @@ function clamp(n) {
6141
6343
  return Math.max(0, Math.min(1, n));
6142
6344
  }
6143
6345
 
6346
+ // src/delegate.ts
6347
+ function isDelegateEnabledFromEnv(envSource) {
6348
+ const env = envSource ?? (typeof process !== "undefined" && process.env ? process.env : {});
6349
+ const raw = (env.KGAUTO_DELEGATE ?? "").trim().toLowerCase();
6350
+ return raw === "1" || raw === "true";
6351
+ }
6352
+ var DELEGATE_TOOL_DEFINITION = {
6353
+ name: "delegate",
6354
+ description: "Delegate a self-contained sub-task to a cheaper executor model chosen by kgauto from measured evidence. YOU decide what to delegate and supply a complete input; kgauto decides which model runs it (never outside the declared pool). The call may be refused (budget exhausted, no qualified executor) \u2014 on refusal, continue and compose with what you have. Verify sub-results before composing them into your answer; do not present unverified delegated content as checked.",
6355
+ inputSchema: {
6356
+ type: "object",
6357
+ properties: {
6358
+ sub_archetype: {
6359
+ type: "string",
6360
+ enum: ALL_ARCHETYPES,
6361
+ description: "What kind of work the sub-task is (its routing archetype)."
6362
+ },
6363
+ input: {
6364
+ type: "string",
6365
+ description: "Complete, self-contained sub-task input. Include everything the executor needs \u2014 it sees nothing else."
6366
+ },
6367
+ quality_floor: {
6368
+ type: "number",
6369
+ minimum: 0,
6370
+ maximum: 10,
6371
+ description: "Optional minimum executor quality score (0-10) for this archetype. Omit to accept the evidence-ranked default."
6372
+ }
6373
+ },
6374
+ required: ["sub_archetype", "input"],
6375
+ additionalProperties: false
6376
+ }
6377
+ };
6378
+ function createDelegate(opts) {
6379
+ const { parentIr, parentHandle } = opts;
6380
+ const enabled = opts.enabled ?? isDelegateEnabledFromEnv();
6381
+ const budget = opts.callOpts?.policy?.maxCostPerTraceUsd;
6382
+ let spentUsd = 0;
6383
+ async function handler(args) {
6384
+ if (!enabled) {
6385
+ return {
6386
+ ok: false,
6387
+ reason: "delegate_not_enabled",
6388
+ detail: "Delegation is not enabled for this consumer (KGAUTO_DELEGATE / CreateDelegateOpts.enabled). Do the sub-task yourself."
6389
+ };
6390
+ }
6391
+ const archetype = args.sub_archetype;
6392
+ if (!ALL_ARCHETYPES.includes(archetype)) {
6393
+ return {
6394
+ ok: false,
6395
+ reason: "invalid_sub_archetype",
6396
+ detail: `Unknown sub_archetype '${String(args.sub_archetype)}'. Valid: ${ALL_ARCHETYPES.join(", ")}. Re-classify or do the sub-task yourself.`
6397
+ };
6398
+ }
6399
+ const pool = parentIr.models;
6400
+ const concreteIds = pool.filter((m) => typeof m === "string");
6401
+ let blockedByFloor = [];
6402
+ if (typeof args.quality_floor === "number") {
6403
+ blockedByFloor = concreteIds.filter(
6404
+ (m) => getArchetypePerfScore(m, archetype).score < args.quality_floor
6405
+ );
6406
+ if (blockedByFloor.length === pool.length) {
6407
+ const scores = concreteIds.map((m) => `${m}=${getArchetypePerfScore(m, archetype).score}`).join(", ");
6408
+ return {
6409
+ ok: false,
6410
+ reason: "no_qualified_executor",
6411
+ detail: `No model in the declared pool clears quality_floor=${args.quality_floor} for '${archetype}' (${scores}). Lower the floor or do the sub-task yourself.`
6412
+ };
6413
+ }
6414
+ }
6415
+ const subIr = {
6416
+ appId: parentIr.appId,
6417
+ intent: { name: `delegate:${archetype}`, archetype },
6418
+ sections: [{ id: "delegated-task", text: args.input }],
6419
+ currentTurn: { role: "user", content: args.input },
6420
+ models: pool
6421
+ };
6422
+ const mergedPolicy = {
6423
+ ...opts.callOpts?.policy ?? {},
6424
+ blockedModels: [
6425
+ ...opts.callOpts?.policy?.blockedModels ?? [],
6426
+ ...blockedByFloor
6427
+ ]
6428
+ };
6429
+ if (typeof budget === "number" && budget > 0) {
6430
+ let estimate = 0;
6431
+ try {
6432
+ estimate = compile(subIr, { policy: mergedPolicy }).estimatedCostUsd;
6433
+ } catch {
6434
+ estimate = 0;
6435
+ }
6436
+ if (spentUsd + estimate > budget) {
6437
+ return {
6438
+ ok: false,
6439
+ reason: "trace_budget_exhausted",
6440
+ detail: `Trace budget $${budget.toFixed(4)} would be exceeded (spent $${spentUsd.toFixed(4)} + estimated $${estimate.toFixed(4)}). Compose your answer from the sub-results you already have.`
6441
+ };
6442
+ }
6443
+ }
6444
+ let result;
6445
+ try {
6446
+ result = await call(subIr, {
6447
+ ...opts.callOpts ?? {},
6448
+ policy: mergedPolicy,
6449
+ parentHandle
6450
+ // R0 linkage — branch row, trace_id = parent
6451
+ });
6452
+ } catch (err) {
6453
+ const detail = err instanceof CallError ? `Sub-call failed after ${err.attempts.length} attempt(s): ${err.attempts.map((a) => a.errorCode).join(" \u2192 ")}.` : String(err);
6454
+ return {
6455
+ ok: false,
6456
+ reason: "call_failed",
6457
+ detail: `${detail} Compose without this sub-result or retry with different input.`
6458
+ };
6459
+ }
6460
+ const profile = tryGetProfile(result.actualModel);
6461
+ const costUsd2 = profile ? result.response.tokens.input / 1e6 * profile.costInputPer1m + result.response.tokens.output / 1e6 * profile.costOutputPer1m : 0;
6462
+ spentUsd += costUsd2;
6463
+ return {
6464
+ ok: true,
6465
+ output: result.response.structuredOutput !== null ? JSON.stringify(result.response.structuredOutput) : result.response.text,
6466
+ subHandle: result.handle,
6467
+ executorModel: result.actualModel,
6468
+ costUsd: costUsd2,
6469
+ latencyMs: result.latencyMs,
6470
+ verification: "trusted"
6471
+ };
6472
+ }
6473
+ async function reportComposition(report) {
6474
+ const env = readBrainReadEnv();
6475
+ if (!env.endpoint || !env.jwt || !env.anonKey) {
6476
+ return { ok: false, reason: `brain_read_not_configured:${env.missingEnv.join(",")}` };
6477
+ }
6478
+ try {
6479
+ const res = await fetch(
6480
+ `${env.endpoint.replace(/\/$/, "")}/rest/v1/kgauto_composition_reports`,
6481
+ {
6482
+ method: "POST",
6483
+ headers: {
6484
+ Authorization: `Bearer ${env.jwt}`,
6485
+ apikey: env.anonKey,
6486
+ "Content-Type": "application/json",
6487
+ Prefer: "return=minimal"
6488
+ },
6489
+ body: JSON.stringify({
6490
+ app_id: parentIr.appId,
6491
+ sub_handle: report.subHandle,
6492
+ disposition: report.disposition,
6493
+ ...report.note ? { note: report.note } : {}
6494
+ })
6495
+ }
6496
+ );
6497
+ if (!res.ok) return { ok: false, reason: `write_failed:${res.status}` };
6498
+ return { ok: true };
6499
+ } catch (err) {
6500
+ return { ok: false, reason: `network_error:${err instanceof Error ? err.message : String(err)}` };
6501
+ }
6502
+ }
6503
+ return {
6504
+ toolDefinition: DELEGATE_TOOL_DEFINITION,
6505
+ handler,
6506
+ reportComposition,
6507
+ traceSpendUsd: () => spentUsd
6508
+ };
6509
+ }
6510
+
6144
6511
  // src/advisories-api.ts
6145
6512
  var SEVERITY_SET = /* @__PURE__ */ new Set(["info", "warn", "critical"]);
6146
6513
  var STATUS_SET = /* @__PURE__ */ new Set(["open", "snoozed", "resolved"]);
@@ -6212,19 +6579,32 @@ function resolveFetch(injected) {
6212
6579
  function normalizeEndpoint2(endpoint) {
6213
6580
  return endpoint.replace(/\/+$/, "");
6214
6581
  }
6582
+ function resolveBrainReadTrio(opts) {
6583
+ const env = readBrainReadEnv();
6584
+ const brainEndpoint = opts.brainEndpoint ?? env.endpoint;
6585
+ const brainJwt = opts.brainJwt ?? env.jwt;
6586
+ const brainAnonKey = opts.brainAnonKey ?? env.anonKey;
6587
+ if (brainEndpoint && brainJwt && brainAnonKey) {
6588
+ return { ok: true, brainEndpoint, brainJwt, brainAnonKey };
6589
+ }
6590
+ const missingEnv = [];
6591
+ if (!brainEndpoint) missingEnv.push("KGAUTO_V2_BRAIN_SUPABASE_URL");
6592
+ if (!brainJwt) missingEnv.push("KGAUTO_V2_BRAIN_JWT");
6593
+ if (!brainAnonKey) missingEnv.push("KGAUTO_V2_BRAIN_ANON_KEY");
6594
+ return { ok: false, missingEnv };
6595
+ }
6215
6596
  async function getActionableAdvisories(opts) {
6216
- const {
6217
- appId,
6218
- severity,
6219
- status,
6220
- brainEndpoint,
6221
- brainJwt,
6222
- brainAnonKey,
6223
- fetch: injectedFetch
6224
- } = opts;
6597
+ const { appId, severity, status, fetch: injectedFetch } = opts;
6225
6598
  if (!appId) {
6226
6599
  throw new Error("getActionableAdvisories: appId is required");
6227
6600
  }
6601
+ const trio = resolveBrainReadTrio(opts);
6602
+ if (!trio.ok) {
6603
+ throw new Error(
6604
+ `getActionableAdvisories: brain-read-not-configured \u2014 missing ${trio.missingEnv.join(", ")} (pass brainEndpoint/brainJwt/brainAnonKey explicitly or set the canonical env vars)`
6605
+ );
6606
+ }
6607
+ const { brainEndpoint, brainJwt, brainAnonKey } = trio;
6228
6608
  const doFetch = resolveFetch(injectedFetch);
6229
6609
  const base = normalizeEndpoint2(brainEndpoint);
6230
6610
  const qs = new URLSearchParams();
@@ -6277,17 +6657,15 @@ async function getActionableAdvisories(opts) {
6277
6657
  return out;
6278
6658
  }
6279
6659
  async function markAdvisoryResolved(opts) {
6280
- const {
6281
- id,
6282
- resolutionNote,
6283
- brainEndpoint,
6284
- brainJwt,
6285
- brainAnonKey,
6286
- fetch: injectedFetch
6287
- } = opts;
6660
+ const { id, resolutionNote, fetch: injectedFetch } = opts;
6288
6661
  if (!id) {
6289
6662
  return { ok: false, reason: "id_required" };
6290
6663
  }
6664
+ const trio = resolveBrainReadTrio(opts);
6665
+ if (!trio.ok) {
6666
+ return { ok: false, reason: `brain_read_not_configured:${trio.missingEnv.join(",")}` };
6667
+ }
6668
+ const { brainEndpoint, brainJwt, brainAnonKey } = trio;
6291
6669
  const doFetch = resolveFetch(injectedFetch);
6292
6670
  const base = normalizeEndpoint2(brainEndpoint);
6293
6671
  const lookupUrl = `${base}/rest/v1/actionable_advisories_v?id=eq.${encodeURIComponent(id)}&select=app_id,rule`;
@@ -6372,7 +6750,7 @@ async function markAdvisoryResolved(opts) {
6372
6750
  }
6373
6751
  }
6374
6752
  if (outcomeIds.length === 0) {
6375
- return { ok: true };
6753
+ return { ok: true, firingsResolved: 0, status: "unknown" };
6376
6754
  }
6377
6755
  const inList = outcomeIds.join(",");
6378
6756
  const patchUrl = `${base}/rest/v1/compile_outcome_advisories?outcome_id=in.(${inList})&code=eq.${encodeURIComponent(code)}&resolved_at=is.null`;
@@ -6392,8 +6770,10 @@ async function markAdvisoryResolved(opts) {
6392
6770
  apikey: brainAnonKey,
6393
6771
  "Content-Type": "application/json",
6394
6772
  Accept: "application/json",
6395
- // PostgREST default is no return; we don't need the row back.
6396
- Prefer: "return=minimal"
6773
+ // alpha.78 count what actually changed. An RLS-filtered PATCH
6774
+ // returns 2xx with zero rows; return=representation makes that
6775
+ // visible instead of success-shaped.
6776
+ Prefer: "return=representation"
6397
6777
  },
6398
6778
  body: JSON.stringify(patchBody)
6399
6779
  });
@@ -6410,7 +6790,33 @@ async function markAdvisoryResolved(opts) {
6410
6790
  if (!patchRes.ok) {
6411
6791
  return { ok: false, reason: `patch_failed:${patchRes.status}` };
6412
6792
  }
6413
- return { ok: true };
6793
+ let patchedRows = [];
6794
+ try {
6795
+ patchedRows = await patchRes.json();
6796
+ } catch {
6797
+ }
6798
+ const firingsResolved = Array.isArray(patchedRows) ? patchedRows.length : 0;
6799
+ let status = "unknown";
6800
+ try {
6801
+ const statusRes = await doFetch(
6802
+ `${base}/rest/v1/actionable_advisories_v?id=eq.${encodeURIComponent(id)}&select=status`,
6803
+ {
6804
+ method: "GET",
6805
+ headers: {
6806
+ Authorization: `Bearer ${brainJwt}`,
6807
+ apikey: brainAnonKey,
6808
+ Accept: "application/json"
6809
+ }
6810
+ }
6811
+ );
6812
+ if (statusRes.ok) {
6813
+ const rows = await statusRes.json();
6814
+ const s = Array.isArray(rows) ? rows[0]?.status : void 0;
6815
+ if (s === "open" || s === "resolved") status = s;
6816
+ }
6817
+ } catch {
6818
+ }
6819
+ return { ok: true, firingsResolved, status };
6414
6820
  }
6415
6821
  async function markExclusionFindingHandled(opts) {
6416
6822
  const {
@@ -6419,12 +6825,14 @@ async function markExclusionFindingHandled(opts) {
6419
6825
  excludedModel,
6420
6826
  resolution,
6421
6827
  resolutionNote,
6422
- brainEndpoint,
6423
- brainJwt,
6424
- brainAnonKey,
6425
6828
  fetch: injectedFetch
6426
6829
  } = opts;
6427
6830
  if (!appId) return { ok: false, reason: "app_id_required" };
6831
+ const trio = resolveBrainReadTrio(opts);
6832
+ if (!trio.ok) {
6833
+ return { ok: false, reason: `brain_read_not_configured:${trio.missingEnv.join(",")}` };
6834
+ }
6835
+ const { brainEndpoint, brainJwt, brainAnonKey } = trio;
6428
6836
  if (!archetype) return { ok: false, reason: "archetype_required" };
6429
6837
  if (!excludedModel) {
6430
6838
  return { ok: false, reason: "excluded_model_required" };
@@ -6690,6 +7098,7 @@ export {
6690
7098
  ALL_ARCHETYPES,
6691
7099
  ARCHETYPE_FAMILY_FITS,
6692
7100
  ARCHETYPE_FLOOR_DEFAULT,
7101
+ BRAIN_READ_ENV_NAMES,
6693
7102
  COACH_CFG,
6694
7103
  CallError,
6695
7104
  DECOMPOSITION_TEMPLATES,
@@ -6697,6 +7106,7 @@ export {
6697
7106
  DEFAULT_FINDINGS_ENDPOINT,
6698
7107
  DEFAULT_MEASURED_FAILURE_ENDPOINT,
6699
7108
  DEFAULT_PROMOTIONS_ENDPOINT,
7109
+ DELEGATE_TOOL_DEFINITION,
6700
7110
  DIALECT_VERSION,
6701
7111
  DISCIPLINE_GATES_V1_ALT_HEADER,
6702
7112
  FamilyResolutionError,
@@ -6708,6 +7118,7 @@ export {
6708
7118
  MEASURED_GROUNDING_MIN_N,
6709
7119
  PRODUCER_OWNED_RULE_CODES,
6710
7120
  PROVIDER_ENV_KEYS,
7121
+ ROLLBACK_SUPPRESSION_WINDOW_DAYS,
6711
7122
  RULE_DISCIPLINE_GATES_V1,
6712
7123
  RULE_DISCIPLINE_GATES_V1_STRUCTURED,
6713
7124
  RULE_SEQUENTIAL_TOOL_CLIFF,
@@ -6743,6 +7154,7 @@ export {
6743
7154
  configurePromotionsBrain,
6744
7155
  countTokens,
6745
7156
  createBrainForwardRoutes,
7157
+ createDelegate,
6746
7158
  createKeyHealthRoute,
6747
7159
  deriveFamilyFromModelId,
6748
7160
  deriveOwnership,
@@ -6761,17 +7173,20 @@ export {
6761
7173
  getPerAxisMetrics,
6762
7174
  getProfile,
6763
7175
  getReachabilityDiagnostic,
7176
+ getRecentRollback,
6764
7177
  getRecommendedPrimary,
6765
7178
  getSequentialStarterChain,
6766
7179
  getSequentialStarterChainWithGrounding,
6767
7180
  getStaleExclusionFindings,
6768
7181
  getStarterChain,
6769
7182
  getStarterChainWithGrounding,
7183
+ hasMutation,
6770
7184
  hashShape,
6771
7185
  isArchetype,
6772
7186
  isAutoPromoteEnabledFromEnv,
6773
7187
  isBrainQueryActiveFor,
6774
7188
  isBrainSync,
7189
+ isDelegateEnabledFromEnv,
6775
7190
  isExclusionFindingsBrainActive,
6776
7191
  isMeasuredFailureBrainActive,
6777
7192
  isMeasuredFailureGateEnabledFromEnv,
@@ -6787,9 +7202,11 @@ export {
6787
7202
  loadChainsFromBrain,
6788
7203
  loadModelsFromBrain,
6789
7204
  loadPricingFromBrain,
7205
+ mapMeasuredFailureRows,
6790
7206
  markAdvisoryResolved,
6791
7207
  markExclusionFindingHandled,
6792
7208
  markPromoteReadyHandled,
7209
+ mutationId,
6793
7210
  parseGoldenCaptureRate,
6794
7211
  parseJudgeVerdict,
6795
7212
  peekBrainDeadLetter,