thumbgate 1.29.2 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/README.md +54 -16
  4. package/adapters/claude/.mcp.json +2 -2
  5. package/adapters/forge/forge.yaml +3 -3
  6. package/adapters/mcp/server-stdio.js +143 -14
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/bench/observability-eval-suite.json +2 -2
  9. package/bin/cli.js +154 -36
  10. package/config/evals/generation-quality-golden.json +95 -0
  11. package/config/evals/rag-answer-quality-golden.json +91 -0
  12. package/config/evals/retrieval-hybrid-ablation.json +66 -0
  13. package/config/evals/retrieval-ranking-golden.json +522 -0
  14. package/config/gates/default.json +217 -50
  15. package/config/mcp-allowlists.json +215 -185
  16. package/config/model-tiers.json +7 -2
  17. package/config/post-deploy-marketing-pages.json +26 -1
  18. package/glama.json +6 -0
  19. package/package.json +94 -11
  20. package/public/architecture.html +130 -0
  21. package/public/assets/diagrams/agent-integration.png +0 -0
  22. package/public/assets/diagrams/before-after.svg +22 -0
  23. package/public/assets/diagrams/decision.svg +36 -0
  24. package/public/assets/diagrams/feedback-pipeline.png +0 -0
  25. package/public/assets/diagrams/hero-thumbs.svg +68 -0
  26. package/public/assets/diagrams/loop.svg +40 -0
  27. package/public/assets/diagrams/plugin-topology.png +0 -0
  28. package/public/assets/diagrams/pre-action-gate-loop.svg +59 -0
  29. package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
  30. package/public/assets/diagrams/stack.svg +18 -0
  31. package/public/assets/diagrams/thumbgate-architecture.png +0 -0
  32. package/public/case-studies.html +151 -0
  33. package/public/compare.html +1 -0
  34. package/public/dashboard.html +126 -28
  35. package/public/eval-scorecard.html +195 -0
  36. package/public/eval-scorecard.json +18 -0
  37. package/public/evaluations.html +168 -0
  38. package/public/index.html +143 -13
  39. package/public/numbers.html +3 -2
  40. package/public/pricing.html +143 -30
  41. package/public/whitepaper.html +189 -0
  42. package/scripts/a-plus-evidence-scorecard.js +303 -0
  43. package/scripts/activation-quickstart.js +1 -0
  44. package/scripts/agent-outcome-monitor.js +71 -1
  45. package/scripts/async-eval-observability.js +36 -11
  46. package/scripts/audit-trail.js +37 -1
  47. package/scripts/auto-promote-gates.js +149 -34
  48. package/scripts/billing.js +3 -1
  49. package/scripts/claude-feedback-sync.js +3 -2
  50. package/scripts/cli-feedback.js +13 -7
  51. package/scripts/colbert-style-maxsim.js +236 -0
  52. package/scripts/cross-encoder-reranker.js +359 -126
  53. package/scripts/dashboard-chat.js +350 -17
  54. package/scripts/document-intake.js +283 -7
  55. package/scripts/eval-quality-suite.js +204 -0
  56. package/scripts/feedback-aggregate.js +5 -2
  57. package/scripts/feedback-loop.js +359 -189
  58. package/scripts/feedback-paths.js +32 -13
  59. package/scripts/feedback-quality.js +53 -0
  60. package/scripts/filesystem-search.js +17 -7
  61. package/scripts/gates-engine.js +98 -4
  62. package/scripts/generate-case-study-outreach.js +253 -0
  63. package/scripts/generate-eval-scorecard.js +276 -0
  64. package/scripts/growth-campaigns.js +183 -0
  65. package/scripts/harness-tool-names.js +70 -0
  66. package/scripts/hook-runtime.js +10 -3
  67. package/scripts/jsonl-watcher.js +1 -0
  68. package/scripts/lesson-db.js +16 -5
  69. package/scripts/lesson-embedding-index.js +67 -20
  70. package/scripts/lesson-embedding-maintenance.js +177 -0
  71. package/scripts/lesson-inference.js +23 -4
  72. package/scripts/lesson-reranker.js +55 -9
  73. package/scripts/lesson-retrieval.js +375 -32
  74. package/scripts/lesson-search.js +48 -11
  75. package/scripts/llm-client.js +304 -15
  76. package/scripts/mcp-config.js +26 -5
  77. package/scripts/mcp-oauth.js +37 -2
  78. package/scripts/model-eval.js +308 -0
  79. package/scripts/model-tier-router.js +593 -0
  80. package/scripts/parallel-workflow-orchestrator.js +86 -22
  81. package/scripts/pragmatic-hybrid-search.js +379 -0
  82. package/scripts/published-cli.js +11 -1
  83. package/scripts/rag-document-pipeline.js +461 -0
  84. package/scripts/rag-structured-output.js +441 -0
  85. package/scripts/ragas-style-metrics.js +351 -0
  86. package/scripts/refresh-proof-pack.js +261 -0
  87. package/scripts/request-envelope.js +178 -0
  88. package/scripts/rerank-pipeline.js +370 -0
  89. package/scripts/rerank-quality-eval.js +155 -0
  90. package/scripts/retrieval-hybrid-ablation.js +120 -0
  91. package/scripts/retrieval-quality-tier.js +118 -0
  92. package/scripts/risk-scorer.js +144 -15
  93. package/scripts/secret-scanner.js +395 -4
  94. package/scripts/self-distill-agent.js +7 -1
  95. package/scripts/self-healing-check.js +25 -0
  96. package/scripts/skill-packs.js +183 -0
  97. package/scripts/slow-loop.js +72 -0
  98. package/scripts/statusline-links.js +1 -1
  99. package/scripts/statusline-local-stats.js +1 -1
  100. package/scripts/statusline.sh +8 -1
  101. package/scripts/telemetry-analytics.js +13 -1
  102. package/scripts/thumbgate-bench.js +13 -0
  103. package/scripts/thumbgate-search.js +98 -6
  104. package/scripts/tier-budget-guard.js +186 -0
  105. package/scripts/tool-kpi-tracker.js +124 -0
  106. package/scripts/tool-registry.js +95 -1
  107. package/scripts/vector-store.js +108 -4
  108. package/scripts/verify-marketing-pages-deployed.js +85 -3
  109. package/server.json +44 -0
  110. package/smithery.yaml +17 -0
  111. package/src/api/server.js +424 -99
@@ -21,6 +21,7 @@ const {
21
21
  buildClarificationMessage,
22
22
  isGenericFeedbackText,
23
23
  normalizeFeedbackText,
24
+ scoreFeedbackReward,
24
25
  } = require('./feedback-quality');
25
26
  const {
26
27
  buildRubricEvaluation,
@@ -62,6 +63,11 @@ const FEEDBACK_EVENT_CLAIM_STALE_MS = 60 * 1000;
62
63
  const FEEDBACK_EVENT_CLAIM_WAIT_MS = 15 * 1000;
63
64
  const FEEDBACK_EVENT_CLAIM_POLL_MS = 25;
64
65
 
66
+ function normalizeReviewOrigin(value) {
67
+ const origin = String(value || '').trim().toLowerCase();
68
+ return ['human', 'automated', 'imported'].includes(origin) ? origin : 'unverified';
69
+ }
70
+
65
71
  function isSelfHarnessOptimizerEnabled(env = process.env) {
66
72
  return /^(?:1|true)$/i.test(String(env.THUMBGATE_SELF_HARNESS_OPTIMIZER || '').trim());
67
73
  }
@@ -387,6 +393,8 @@ function normalizeAnalysisShape(analysis = {}) {
387
393
  byImportance: Array.isArray(analysis.byImportance) ? analysis.byImportance : [],
388
394
  recentLessons: Array.isArray(analysis.recentLessons) ? analysis.recentLessons : [],
389
395
  sessionCount: Number.isFinite(analysis.sessionCount) ? analysis.sessionCount : 0,
396
+ rawTotal: analysis.rawTotal ?? total,
397
+ excludedTotal: analysis.excludedTotal ?? 0,
390
398
  };
391
399
  }
392
400
 
@@ -1431,6 +1439,7 @@ function captureFeedback(params) {
1431
1439
  structuredRule: structuredRule || null,
1432
1440
  ...(reflection && { reflection }),
1433
1441
  gateAction: params.gateAction || null,
1442
+ reviewOrigin: normalizeReviewOrigin(params.reviewOrigin),
1434
1443
  sourceEvent: publicFeedbackSourceMetadata(params.sourceEvent),
1435
1444
  timestamp: now,
1436
1445
  };
@@ -1562,6 +1571,7 @@ function captureFeedback(params) {
1562
1571
  diagnosis: storedDiagnosis,
1563
1572
  structuredRule: structuredRule || null,
1564
1573
  sourceFeedbackId: feedbackEvent.id,
1574
+ reviewOrigin: feedbackEvent.reviewOrigin,
1565
1575
  timestamp: now,
1566
1576
  };
1567
1577
 
@@ -1604,6 +1614,14 @@ function captureFeedback(params) {
1604
1614
  return firewallBlocked;
1605
1615
  }
1606
1616
 
1617
+ // Grade the correction the operator actually wrote, and persist it with the
1618
+ // entry. assessFeedbackActionability already answered the binary question
1619
+ // (promotable at all); this is the graded one, and without computing it here
1620
+ // the scorer would be reachable-but-never-called — the same defect class it
1621
+ // was added to help detect.
1622
+ const rewardScore = scoreFeedbackReward(feedbackEvent);
1623
+ if (rewardScore) feedbackEvent.rewardScore = rewardScore;
1624
+
1607
1625
  appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
1608
1626
  emitAnonymousFeedbackPing(signal);
1609
1627
 
@@ -1885,6 +1903,9 @@ function captureFeedback(params) {
1885
1903
  inferredLesson: memoryRecord ? memoryRecord.title : (feedbackEvent.context || '').slice(0, 200),
1886
1904
  confidence: memoryRecord ? 70 : 40,
1887
1905
  tags: feedbackEvent.tags || [],
1906
+ metadata: {
1907
+ reviewOrigin: feedbackEvent.reviewOrigin,
1908
+ },
1888
1909
  });
1889
1910
  } catch { /* non-critical — lesson creation should never block feedback */ }
1890
1911
  });
@@ -1910,143 +1931,146 @@ function captureFeedbackIdempotent(params = {}) {
1910
1931
  }
1911
1932
  }
1912
1933
 
1913
- function analyzeFeedback(logPath) {
1914
- const { FEEDBACK_LOG_PATH } = getFeedbackPaths();
1915
- const resolvedLogPath = logPath || FEEDBACK_LOG_PATH;
1916
- const feedbackDir = path.dirname(resolvedLogPath);
1917
- const paths = buildFeedbackPathsFromDir(feedbackDir);
1918
- const shouldUseSQLite = !logPath || path.resolve(resolvedLogPath) === path.resolve(FEEDBACK_LOG_PATH);
1919
- const entries = readJSONL(resolvedLogPath, { maxLines: 0 });
1920
- const diagnosticLogPath = path.join(feedbackDir, 'diagnostic-log.jsonl');
1921
- const diagnosticEntries = readDiagnosticEntries(diagnosticLogPath);
1922
-
1923
- // Prefer the JSONL mirror for full analytics fidelity. Fall back to SQLite only
1924
- // when the mirror is unavailable so dashboards and proof paths keep their full shape.
1925
- const db = shouldUseSQLite ? getLessonDB() : null;
1926
- if (db && entries.length === 0) {
1927
- try {
1928
- const { getStatsFromDB } = require('./lesson-db');
1929
- const sqliteStats = getStatsFromDB(db);
1930
- if (sqliteStats.total > 0) return normalizeAnalysisShape(sqliteStats);
1931
- } catch { /* fall through to JSONL scan */ }
1934
+ function incrementBucket(buckets, key, signal) {
1935
+ if (!key) return;
1936
+ if (!buckets[key]) buckets[key] = { positive: 0, negative: 0, total: 0 };
1937
+ buckets[key][signal] += 1;
1938
+ buckets[key].total += 1;
1939
+ }
1940
+
1941
+ function summarizeRubric(entry, summary) {
1942
+ if (entry.actionType === 'no-action' && typeof entry.actionReason === 'string' && entry.actionReason.includes('Rubric gate')) {
1943
+ summary.blocked += 1;
1932
1944
  }
1945
+ if (entry.rubric?.weightedScore != null) summary.samples += 1;
1933
1946
 
1934
- const skills = {};
1935
- const tags = {};
1936
- const rubricCriteria = {};
1937
- let rubricSamples = 0;
1938
- let blockedPromotions = 0;
1947
+ for (const criterion of entry.rubric?.failingCriteria || []) {
1948
+ if (!summary.criteria[criterion]) summary.criteria[criterion] = { failures: 0 };
1949
+ summary.criteria[criterion].failures += 1;
1950
+ }
1951
+ }
1939
1952
 
1940
- let totalPositive = 0;
1941
- let totalNegative = 0;
1953
+ function summarizeFeedbackEntries(entries) {
1954
+ const summary = {
1955
+ skills: {},
1956
+ tags: {},
1957
+ criteria: {},
1958
+ samples: 0,
1959
+ blocked: 0,
1960
+ positive: 0,
1961
+ negative: 0,
1962
+ };
1942
1963
 
1943
1964
  for (const entry of entries) {
1944
- if (entry.signal === 'positive') totalPositive++;
1945
- if (entry.signal === 'negative') totalNegative++;
1946
-
1947
- if (entry.skill) {
1948
- if (!skills[entry.skill]) skills[entry.skill] = { positive: 0, negative: 0, total: 0 };
1949
- skills[entry.skill][entry.signal] += 1;
1950
- skills[entry.skill].total += 1;
1951
- }
1965
+ if (entry.signal === 'positive') summary.positive += 1;
1966
+ if (entry.signal === 'negative') summary.negative += 1;
1952
1967
 
1968
+ incrementBucket(summary.skills, entry.skill, entry.signal);
1953
1969
  for (const tag of entry.tags || []) {
1954
- if (!tags[tag]) tags[tag] = { positive: 0, negative: 0, total: 0 };
1955
- tags[tag][entry.signal] += 1;
1956
- tags[tag].total += 1;
1970
+ incrementBucket(summary.tags, tag, entry.signal);
1957
1971
  }
1972
+ summarizeRubric(entry, summary);
1973
+ }
1958
1974
 
1959
- if (entry.actionType === 'no-action' && typeof entry.actionReason === 'string' && entry.actionReason.includes('Rubric gate')) {
1960
- blockedPromotions += 1;
1961
- }
1975
+ return summary;
1976
+ }
1962
1977
 
1963
- if (entry.rubric && entry.rubric.weightedScore != null) {
1964
- rubricSamples += 1;
1965
- }
1978
+ function roundedRate(numerator, denominator) {
1979
+ return denominator > 0 ? Math.round((numerator / denominator) * 1000) / 1000 : 0;
1980
+ }
1966
1981
 
1967
- if (entry.rubric && Array.isArray(entry.rubric.failingCriteria)) {
1968
- for (const criterion of entry.rubric.failingCriteria) {
1969
- if (!rubricCriteria[criterion]) rubricCriteria[criterion] = { failures: 0 };
1970
- rubricCriteria[criterion].failures += 1;
1971
- }
1972
- }
1973
- }
1982
+ function addRec(output, message, remediation) {
1983
+ output.messages.push(message);
1984
+ output.remediations.push(remediation);
1985
+ }
1974
1986
 
1975
- const total = totalPositive + totalNegative;
1976
- const approvalRate = total > 0 ? Math.round((totalPositive / total) * 1000) / 1000 : 0;
1977
- const recent = entries.slice(-20);
1978
- const recentPos = recent.filter((e) => e.signal === 'positive').length;
1979
- const recentRate = recent.length > 0 ? Math.round((recentPos / recent.length) * 1000) / 1000 : 0;
1987
+ function feedbackTrend(windowStats, rate7d, rate30d) {
1988
+ if (windowStats['7d'].total === 0 || windowStats['30d'].total === 0) return 'stable';
1989
+ if (rate7d > rate30d + 0.05) return 'improving';
1990
+ if (rate7d < rate30d - 0.05) return 'degrading';
1991
+ return 'stable';
1992
+ }
1980
1993
 
1981
- // Rolling windows: 7-day, 30-day, lifetime (#204)
1994
+ function feedbackWindows(entries, positive, total) {
1982
1995
  const now = Date.now();
1983
- const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
1984
- const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
1985
- const windowStats = { '7d': { total: 0, positive: 0 }, '30d': { total: 0, positive: 0 } };
1996
+ const windowStats = {
1997
+ '7d': { total: 0, positive: 0 },
1998
+ '30d': { total: 0, positive: 0 },
1999
+ };
2000
+ const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
2001
+ const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
2002
+
1986
2003
  for (const entry of entries) {
1987
- const ts = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
1988
- const age = now - ts;
1989
- if (age <= SEVEN_DAYS_MS) {
1990
- windowStats['7d'].total++;
1991
- if (entry.signal === 'positive') windowStats['7d'].positive++;
2004
+ const timestamp = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
2005
+ const age = now - timestamp;
2006
+ if (age <= sevenDaysMs) {
2007
+ windowStats['7d'].total += 1;
2008
+ if (entry.signal === 'positive') windowStats['7d'].positive += 1;
1992
2009
  }
1993
- if (age <= THIRTY_DAYS_MS) {
1994
- windowStats['30d'].total++;
1995
- if (entry.signal === 'positive') windowStats['30d'].positive++;
2010
+ if (age <= thirtyDaysMs) {
2011
+ windowStats['30d'].total += 1;
2012
+ if (entry.signal === 'positive') windowStats['30d'].positive += 1;
1996
2013
  }
1997
2014
  }
1998
- const rate7d = windowStats['7d'].total > 0
1999
- ? Math.round((windowStats['7d'].positive / windowStats['7d'].total) * 1000) / 1000 : 0;
2000
- const rate30d = windowStats['30d'].total > 0
2001
- ? Math.round((windowStats['30d'].positive / windowStats['30d'].total) * 1000) / 1000 : 0;
2002
- const TREND_THRESHOLD = 0.05;
2003
- const hasTrendData = windowStats['7d'].total > 0 && windowStats['30d'].total > 0;
2004
- const trend = !hasTrendData ? 'stable'
2005
- : rate7d > rate30d + TREND_THRESHOLD ? 'improving'
2006
- : rate7d < rate30d - TREND_THRESHOLD ? 'degrading' : 'stable';
2007
- const windows = {
2008
- '7d': { ...windowStats['7d'], rate: rate7d },
2009
- '30d': { ...windowStats['30d'], rate: rate30d },
2010
- lifetime: { total, positive: totalPositive, rate: approvalRate },
2011
- };
2012
2015
 
2013
- const recommendations = [];
2014
- // Structured counterpart to `recommendations` — machine-actionable shape so
2015
- // hooks/agents can act on each item without regex-parsing prose strings.
2016
- // Each entry: { type, target, evidence, action, rationale }.
2017
- const actionableRemediations = [];
2016
+ const rate7d = roundedRate(windowStats['7d'].positive, windowStats['7d'].total);
2017
+ const rate30d = roundedRate(windowStats['30d'].positive, windowStats['30d'].total);
2018
+ const trend = feedbackTrend(windowStats, rate7d, rate30d);
2019
+
2020
+ return {
2021
+ rate7d,
2022
+ rate30d,
2023
+ trend,
2024
+ windows: {
2025
+ '7d': { ...windowStats['7d'], rate: rate7d },
2026
+ '30d': { ...windowStats['30d'], rate: rate30d },
2027
+ lifetime: { total, positive, rate: roundedRate(positive, total) },
2028
+ },
2029
+ };
2030
+ }
2018
2031
 
2032
+ function addSkillRecs(skills, output) {
2019
2033
  for (const [skill, stat] of Object.entries(skills)) {
2020
2034
  const negRate = stat.total > 0 ? stat.negative / stat.total : 0;
2021
- if (stat.total >= 3 && negRate >= 0.5) {
2022
- recommendations.push(`IMPROVE skill '${skill}' (${stat.negative}/${stat.total} negative)`);
2023
- actionableRemediations.push({
2024
- type: 'skill-improve',
2025
- target: skill,
2026
- evidence: { positive: stat.positive, negative: stat.negative, total: stat.total, negativeRate: Math.round(negRate * 1000) / 1000 },
2027
- action: 'review-and-update-skill',
2028
- rationale: `Skill '${skill}' has ${stat.negative}/${stat.total} negative feedback events (${Math.round(negRate * 100)}% negative rate).`,
2029
- });
2030
- }
2035
+ if (stat.total < 3 || negRate < 0.5) continue;
2036
+ addRec(output, `IMPROVE skill '${skill}' (${stat.negative}/${stat.total} negative)`, {
2037
+ type: 'skill-improve',
2038
+ target: skill,
2039
+ evidence: {
2040
+ positive: stat.positive,
2041
+ negative: stat.negative,
2042
+ total: stat.total,
2043
+ negativeRate: roundedRate(stat.negative, stat.total),
2044
+ },
2045
+ action: 'review-and-update-skill',
2046
+ rationale: `Skill '${skill}' has ${stat.negative}/${stat.total} negative feedback events (${Math.round(negRate * 100)}% negative rate).`,
2047
+ });
2031
2048
  }
2049
+ }
2032
2050
 
2051
+ function addTagRecs(tags, output) {
2033
2052
  for (const [tag, stat] of Object.entries(tags)) {
2034
2053
  const posRate = stat.total > 0 ? stat.positive / stat.total : 0;
2035
- if (stat.total >= 3 && posRate >= 0.8) {
2036
- recommendations.push(`REUSE pattern '${tag}' (${stat.positive}/${stat.total} positive)`);
2037
- actionableRemediations.push({
2038
- type: 'pattern-reuse',
2039
- target: tag,
2040
- evidence: { positive: stat.positive, negative: stat.negative, total: stat.total, positiveRate: Math.round(posRate * 1000) / 1000 },
2041
- action: 'replicate-pattern',
2042
- rationale: `Pattern '${tag}' has ${stat.positive}/${stat.total} positive feedback events (${Math.round(posRate * 100)}% positive rate).`,
2043
- });
2044
- }
2054
+ if (stat.total < 3 || posRate < 0.8) continue;
2055
+ addRec(output, `REUSE pattern '${tag}' (${stat.positive}/${stat.total} positive)`, {
2056
+ type: 'pattern-reuse',
2057
+ target: tag,
2058
+ evidence: {
2059
+ positive: stat.positive,
2060
+ negative: stat.negative,
2061
+ total: stat.total,
2062
+ positiveRate: roundedRate(stat.positive, stat.total),
2063
+ },
2064
+ action: 'replicate-pattern',
2065
+ rationale: `Pattern '${tag}' has ${stat.positive}/${stat.total} positive feedback events (${Math.round(posRate * 100)}% positive rate).`,
2066
+ });
2045
2067
  }
2068
+ }
2046
2069
 
2070
+ function addTrendRecs(metrics, output) {
2071
+ const { recent, recentRate, approvalRate, trend, rate7d, rate30d } = metrics;
2047
2072
  if (recent.length >= 10 && recentRate < approvalRate - 0.1) {
2048
- recommendations.push('DECLINING trend in last 20 signals; tighten verification before response.');
2049
- actionableRemediations.push({
2073
+ addRec(output, 'DECLINING trend in last 20 signals; tighten verification before response.', {
2050
2074
  type: 'trend-declining',
2051
2075
  target: 'recent-signals',
2052
2076
  evidence: { recentRate, approvalRate, sampleSize: recent.length },
@@ -2054,93 +2078,135 @@ function analyzeFeedback(logPath) {
2054
2078
  rationale: `Recent approval rate (${Math.round(recentRate * 100)}%) has dropped ≥10pp below lifetime (${Math.round(approvalRate * 100)}%).`,
2055
2079
  });
2056
2080
  }
2057
- if (trend === 'degrading') {
2058
- recommendations.push(`DEGRADING 7d trend (${rate7d}) vs 30d (${rate30d}); increase prevention rule injection.`);
2059
- actionableRemediations.push({
2060
- type: 'trend-degrading',
2061
- target: '7d-window',
2062
- evidence: { rate7d, rate30d, delta: Math.round((rate7d - rate30d) * 1000) / 1000 },
2063
- action: 'increase-prevention-rule-injection',
2064
- rationale: `7d rate (${rate7d}) is below 30d rate (${rate30d}) by more than threshold.`,
2081
+ if (trend !== 'degrading') return;
2082
+ addRec(output, `DEGRADING 7d trend (${rate7d}) vs 30d (${rate30d}); increase prevention rule injection.`, {
2083
+ type: 'trend-degrading',
2084
+ target: '7d-window',
2085
+ evidence: { rate7d, rate30d, delta: Math.round((rate7d - rate30d) * 1000) / 1000 },
2086
+ action: 'increase-prevention-rule-injection',
2087
+ rationale: `7d rate (${rate7d}) is below 30d rate (${rate30d}) by more than threshold.`,
2088
+ });
2089
+ }
2090
+
2091
+ function addRiskBuckets(buckets, kind, output) {
2092
+ for (const bucket of buckets.slice(0, 2)) {
2093
+ addRec(output, `CHECK high-risk ${kind} '${bucket.key}' (${bucket.highRisk}/${bucket.total} high-risk)`, {
2094
+ type: `high-risk-${kind}`,
2095
+ target: bucket.key,
2096
+ evidence: { highRisk: bucket.highRisk, total: bucket.total, riskRate: bucket.riskRate },
2097
+ action: `audit-${kind}-failures`,
2098
+ rationale: `${kind === 'domain' ? 'Domain' : 'Tag'} '${bucket.key}' has ${bucket.highRisk}/${bucket.total} high-risk events (${Math.round((bucket.riskRate || 0) * 100)}% risk rate).`,
2065
2099
  });
2066
2100
  }
2101
+ }
2067
2102
 
2068
- let boostedRisk = null;
2103
+ function addRiskRecs(feedbackDir, output) {
2069
2104
  try {
2070
2105
  const riskScorer = getRiskScorerModule();
2071
- if (riskScorer) {
2072
- boostedRisk = riskScorer.getRiskSummary(paths.FEEDBACK_DIR);
2073
- if (boostedRisk) {
2074
- boostedRisk.highRiskDomains.slice(0, 2).forEach((bucket) => {
2075
- recommendations.push(`CHECK high-risk domain '${bucket.key}' (${bucket.highRisk}/${bucket.total} high-risk)`);
2076
- actionableRemediations.push({
2077
- type: 'high-risk-domain',
2078
- target: bucket.key,
2079
- evidence: { highRisk: bucket.highRisk, total: bucket.total, riskRate: bucket.riskRate },
2080
- action: 'audit-domain-failures',
2081
- rationale: `Domain '${bucket.key}' has ${bucket.highRisk}/${bucket.total} high-risk events (${Math.round((bucket.riskRate || 0) * 100)}% risk rate).`,
2082
- });
2083
- });
2084
- boostedRisk.highRiskTags.slice(0, 2).forEach((bucket) => {
2085
- recommendations.push(`CHECK high-risk tag '${bucket.key}' (${bucket.highRisk}/${bucket.total} high-risk)`);
2086
- actionableRemediations.push({
2087
- type: 'high-risk-tag',
2088
- target: bucket.key,
2089
- evidence: { highRisk: bucket.highRisk, total: bucket.total, riskRate: bucket.riskRate },
2090
- action: 'audit-tag-failures',
2091
- rationale: `Tag '${bucket.key}' has ${bucket.highRisk}/${bucket.total} high-risk events (${Math.round((bucket.riskRate || 0) * 100)}% risk rate).`,
2092
- });
2093
- });
2094
- }
2095
- }
2106
+ if (!riskScorer) return null;
2107
+ const boostedRisk = riskScorer.getRiskSummary(feedbackDir);
2108
+ if (!boostedRisk) return null;
2109
+ addRiskBuckets(boostedRisk.highRiskDomains, 'domain', output);
2110
+ addRiskBuckets(boostedRisk.highRiskTags, 'tag', output);
2111
+ return boostedRisk;
2096
2112
  } catch {
2097
- boostedRisk = null;
2113
+ return null;
2098
2114
  }
2099
- const diagnostics = aggregateFailureDiagnostics([...entries, ...diagnosticEntries]);
2100
- let delegation = null;
2115
+ }
2116
+
2117
+ function addDelegationRecs(feedbackDir, output) {
2101
2118
  try {
2102
2119
  const delegationRuntime = getDelegationRuntimeModule();
2103
- if (delegationRuntime && typeof delegationRuntime.summarizeDelegation === 'function') {
2104
- delegation = delegationRuntime.summarizeDelegation(paths.FEEDBACK_DIR);
2105
- if (delegation.attemptCount >= 3 && delegation.verificationFailureRate >= 0.5) {
2106
- recommendations.push(`REDUCE delegation: verification failure rate is ${Math.round(delegation.verificationFailureRate * 100)}%`);
2107
- actionableRemediations.push({
2108
- type: 'delegation-reduce',
2109
- target: 'verification-failure-rate',
2110
- evidence: { verificationFailureRate: delegation.verificationFailureRate, attemptCount: delegation.attemptCount },
2111
- action: 'reduce-delegation-use',
2112
- rationale: `Delegation verification failure rate is ${Math.round(delegation.verificationFailureRate * 100)}% across ${delegation.attemptCount} attempts.`,
2113
- });
2114
- }
2115
- if (delegation.avoidedDelegationCount >= 3) {
2116
- recommendations.push(`REVIEW delegation policy: ${delegation.avoidedDelegationCount} handoff starts were blocked before execution`);
2117
- actionableRemediations.push({
2118
- type: 'delegation-policy-review',
2119
- target: 'handoff-blocks',
2120
- evidence: { avoidedDelegationCount: delegation.avoidedDelegationCount },
2121
- action: 'review-delegation-policy',
2122
- rationale: `${delegation.avoidedDelegationCount} handoff starts were blocked before execution.`,
2123
- });
2124
- }
2120
+ if (!delegationRuntime || typeof delegationRuntime.summarizeDelegation !== 'function') return null;
2121
+ const delegation = delegationRuntime.summarizeDelegation(feedbackDir);
2122
+ if (delegation.attemptCount >= 3 && delegation.verificationFailureRate >= 0.5) {
2123
+ addRec(output, `REDUCE delegation: verification failure rate is ${Math.round(delegation.verificationFailureRate * 100)}%`, {
2124
+ type: 'delegation-reduce',
2125
+ target: 'verification-failure-rate',
2126
+ evidence: {
2127
+ verificationFailureRate: delegation.verificationFailureRate,
2128
+ attemptCount: delegation.attemptCount,
2129
+ },
2130
+ action: 'reduce-delegation-use',
2131
+ rationale: `Delegation verification failure rate is ${Math.round(delegation.verificationFailureRate * 100)}% across ${delegation.attemptCount} attempts.`,
2132
+ });
2125
2133
  }
2134
+ if (delegation.avoidedDelegationCount >= 3) {
2135
+ addRec(output, `REVIEW delegation policy: ${delegation.avoidedDelegationCount} handoff starts were blocked before execution`, {
2136
+ type: 'delegation-policy-review',
2137
+ target: 'handoff-blocks',
2138
+ evidence: { avoidedDelegationCount: delegation.avoidedDelegationCount },
2139
+ action: 'review-delegation-policy',
2140
+ rationale: `${delegation.avoidedDelegationCount} handoff starts were blocked before execution.`,
2141
+ });
2142
+ }
2143
+ return delegation;
2126
2144
  } catch {
2127
- delegation = null;
2145
+ return null;
2128
2146
  }
2129
- diagnostics.categories.slice(0, 2).forEach((bucket) => {
2130
- recommendations.push(`DIAGNOSE '${bucket.key}' failures (${bucket.count})`);
2131
- actionableRemediations.push({
2147
+ }
2148
+
2149
+ function addDiagnosticRecs(diagnostics, output) {
2150
+ for (const bucket of diagnostics.categories.slice(0, 2)) {
2151
+ addRec(output, `DIAGNOSE '${bucket.key}' failures (${bucket.count})`, {
2132
2152
  type: 'diagnose-failure-category',
2133
2153
  target: bucket.key,
2134
2154
  evidence: { count: bucket.count },
2135
2155
  action: 'investigate-failure-category',
2136
2156
  rationale: `Failure category '${bucket.key}' has ${bucket.count} diagnosed events.`,
2137
2157
  });
2138
- });
2158
+ }
2159
+ }
2160
+
2161
+ function getSQLiteFallback(useSQLite, entries) {
2162
+ const db = useSQLite ? getLessonDB() : null;
2163
+ if (!db || entries.length > 0) return null;
2164
+ try {
2165
+ const { getStatsFromDB } = require('./lesson-db');
2166
+ const sqliteStats = getStatsFromDB(db);
2167
+ return sqliteStats.total > 0 ? normalizeAnalysisShape(sqliteStats) : null;
2168
+ } catch {
2169
+ return null;
2170
+ }
2171
+ }
2172
+
2173
+ function analyzeFeedback(logPath, options = {}) {
2174
+ const { FEEDBACK_LOG_PATH } = getFeedbackPaths();
2175
+ const resolvedPath = logPath || FEEDBACK_LOG_PATH;
2176
+ const feedbackDir = path.dirname(resolvedPath);
2177
+ const paths = buildFeedbackPathsFromDir(feedbackDir);
2178
+ const useSQLite = !options.humanOnly && (!logPath || path.resolve(resolvedPath) === path.resolve(FEEDBACK_LOG_PATH));
2179
+ let entries = readJSONL(resolvedPath, { maxLines: 0 });
2180
+ const rawTotal = entries.length;
2181
+ if (options.humanOnly) {
2182
+ entries = entries.filter((entry) => normalizeReviewOrigin(entry.reviewOrigin) === 'human');
2183
+ }
2184
+ const fallback = getSQLiteFallback(useSQLite, entries);
2185
+ if (fallback) return fallback;
2186
+
2187
+ const diagnosticEntries = readDiagnosticEntries(path.join(feedbackDir, 'diagnostic-log.jsonl'));
2188
+ const summary = summarizeFeedbackEntries(entries);
2189
+ const { skills, tags, criteria, samples, blocked, positive, negative } = summary;
2190
+ const total = positive + negative;
2191
+ const approvalRate = roundedRate(positive, total);
2192
+ const recent = entries.slice(-20);
2193
+ const recentPos = recent.filter((e) => e.signal === 'positive').length;
2194
+ const recentRate = roundedRate(recentPos, recent.length);
2195
+ const { rate7d, rate30d, trend, windows } = feedbackWindows(entries, positive, total);
2196
+ const recs = { messages: [], remediations: [] };
2197
+
2198
+ addSkillRecs(skills, recs);
2199
+ addTagRecs(tags, recs);
2200
+ addTrendRecs({ recent, recentRate, approvalRate, trend, rate7d, rate30d }, recs);
2201
+ const boostedRisk = addRiskRecs(paths.FEEDBACK_DIR, recs);
2202
+ const diagnostics = aggregateFailureDiagnostics([...entries, ...diagnosticEntries]);
2203
+ const delegation = addDelegationRecs(paths.FEEDBACK_DIR, recs);
2204
+ addDiagnosticRecs(diagnostics, recs);
2139
2205
 
2140
2206
  return normalizeAnalysisShape({
2141
2207
  total,
2142
- totalPositive,
2143
- totalNegative,
2208
+ totalPositive: positive,
2209
+ totalNegative: negative,
2144
2210
  approvalRate,
2145
2211
  recentRate,
2146
2212
  windows,
@@ -2148,15 +2214,17 @@ function analyzeFeedback(logPath) {
2148
2214
  skills,
2149
2215
  tags,
2150
2216
  rubric: {
2151
- samples: rubricSamples,
2152
- blockedPromotions,
2153
- failingCriteria: rubricCriteria,
2217
+ samples,
2218
+ blockedPromotions: blocked,
2219
+ failingCriteria: criteria,
2154
2220
  },
2155
2221
  diagnostics,
2156
2222
  delegation,
2157
2223
  boostedRisk,
2158
- recommendations,
2159
- actionableRemediations,
2224
+ recommendations: recs.messages,
2225
+ actionableRemediations: recs.remediations,
2226
+ rawTotal,
2227
+ excludedTotal: rawTotal - entries.length,
2160
2228
  });
2161
2229
  }
2162
2230
 
@@ -2184,20 +2252,100 @@ function buildPreventionRules(minOccurrences = 2, options = {}) {
2184
2252
  return Math.exp(-lambda * daysSince);
2185
2253
  }
2186
2254
 
2255
+ // CEO contract tags beat generic richContext.domain=general so honesty/overclaim
2256
+ // whatToChange is not drowned by hook noise in the "general" bucket.
2257
+ const PRIORITY_DOMAIN_TAGS = [
2258
+ 'honesty',
2259
+ 'overclaim',
2260
+ 'completion-claim',
2261
+ 'production-truth',
2262
+ 'false-completion',
2263
+ 'pr-hygiene',
2264
+ 'ceo-feedback',
2265
+ ];
2266
+ const GENERIC_TAGS = new Set([
2267
+ 'feedback',
2268
+ 'negative',
2269
+ 'positive',
2270
+ 'entity:Customer',
2271
+ 'thumbs-down',
2272
+ 'thumbs-up',
2273
+ ]);
2274
+
2275
+ function isNoiseTitle(title) {
2276
+ const t = String(title || '');
2277
+ return /hookEventName|user_prompt_submit|"sessionId"/i.test(t);
2278
+ }
2279
+
2280
+ function extractAvoidLine(content) {
2281
+ return String(content || '')
2282
+ .split('\n')
2283
+ .find((l) => l.toLowerCase().startsWith('how to avoid:')) || null;
2284
+ }
2285
+
2286
+ function domainKeyForMemory(m) {
2287
+ const tags = Array.isArray(m.tags) ? m.tags : [];
2288
+ for (const tag of PRIORITY_DOMAIN_TAGS) {
2289
+ if (tags.includes(tag)) return tag;
2290
+ }
2291
+ const rcDomain = m.richContext && m.richContext.domain;
2292
+ if (rcDomain && rcDomain !== 'unknown' && rcDomain !== 'general') {
2293
+ return rcDomain;
2294
+ }
2295
+ return tags.find((t) => !GENERIC_TAGS.has(t)) || 'general';
2296
+ }
2297
+
2298
+ function pickBestMemory(items) {
2299
+ let best = items[items.length - 1];
2300
+ let bestScore = -Infinity;
2301
+ items.forEach((m, index) => {
2302
+ let score = index;
2303
+ if (isNoiseTitle(m.title)) score -= 1000;
2304
+ const avoid = extractAvoidLine(m.content);
2305
+ if (avoid) score += 100;
2306
+ if (avoid && /never/i.test(avoid)) score += 50;
2307
+ if ((m.tags || []).some((t) => PRIORITY_DOMAIN_TAGS.includes(t))) score += 40;
2308
+ if ((m.occurrences || 1) > 1) score += 10;
2309
+ if (score >= bestScore) {
2310
+ bestScore = score;
2311
+ best = m;
2312
+ }
2313
+ });
2314
+ return best;
2315
+ }
2316
+
2187
2317
  const buckets = {};
2188
2318
  const rubricBuckets = {};
2189
2319
  const diagnosisBuckets = {};
2190
2320
  const repeatedViolationBuckets = {};
2321
+ const priorityContracts = [];
2191
2322
  for (const m of memories) {
2192
- const key = (m.richContext && m.richContext.domain && m.richContext.domain !== 'unknown')
2193
- ? m.richContext.domain
2194
- : (m.tags || []).find((t) => !['feedback', 'negative', 'positive'].includes(t)) || 'general';
2323
+ if (isNoiseTitle(m.title) && !extractAvoidLine(m.content)) {
2324
+ // Skip pure hook-noise shells with no actionable avoid line.
2325
+ continue;
2326
+ }
2327
+ const key = domainKeyForMemory(m);
2195
2328
  if (!buckets[key]) buckets[key] = { items: [], weightedCount: 0 };
2196
2329
  const w = decayWeight(m);
2197
2330
  const occ = m.occurrences || 1;
2198
2331
  buckets[key].items.push(m);
2199
2332
  buckets[key].weightedCount += w * occ;
2200
2333
 
2334
+ const tags = Array.isArray(m.tags) ? m.tags : [];
2335
+ const avoid = extractAvoidLine(m.content);
2336
+ if (
2337
+ avoid
2338
+ && tags.some((t) => PRIORITY_DOMAIN_TAGS.includes(t))
2339
+ ) {
2340
+ priorityContracts.push({
2341
+ id: m.id,
2342
+ tags: tags.filter((t) => PRIORITY_DOMAIN_TAGS.includes(t)),
2343
+ rule: avoid.replace(/^How to avoid:\s*/i, ''),
2344
+ title: m.title,
2345
+ occurrences: occ,
2346
+ });
2347
+ }
2348
+
2201
2349
  const failed = m.rubricSummary && Array.isArray(m.rubricSummary.failingCriteria)
2202
2350
  ? m.rubricSummary.failingCriteria
2203
2351
  : [];
@@ -2235,18 +2383,37 @@ function buildPreventionRules(minOccurrences = 2, options = {}) {
2235
2383
 
2236
2384
  const lines = ['# Prevention Rules', '', 'Generated from negative feedback memories (time-weighted, half-life: ' + decayHalfLifeDays + 'd).'];
2237
2385
 
2386
+ // High-priority CEO contracts: always emit actionable whatToChange (threshold 1).
2387
+ if (priorityContracts.length > 0) {
2388
+ lines.push('');
2389
+ lines.push('## High-Priority Contracts');
2390
+ const seen = new Set();
2391
+ priorityContracts
2392
+ .sort((a, b) => (b.occurrences || 1) - (a.occurrences || 1))
2393
+ .forEach((c) => {
2394
+ const dedupe = c.rule.slice(0, 120);
2395
+ if (seen.has(dedupe)) return;
2396
+ seen.add(dedupe);
2397
+ lines.push(`- **[${c.tags.join(', ')}]** ${c.rule}`);
2398
+ lines.push(` - Source: ${c.title}`);
2399
+ });
2400
+ }
2401
+
2238
2402
  Object.entries(buckets)
2239
2403
  .sort((a, b) => b[1].weightedCount - a[1].weightedCount)
2240
2404
  .forEach(([domain, { items, weightedCount }]) => {
2241
2405
  const effectiveOccurrences = Math.round(weightedCount);
2242
- if (effectiveOccurrences < resolvedMinOccurrences) return;
2243
- const latest = items[items.length - 1];
2244
- const avoid = (latest.content || '').split('\n').find((l) => l.toLowerCase().startsWith('how to avoid:')) || 'How to avoid: Investigate and prevent recurrence';
2406
+ // Priority domains promote with a single solid memory (CEO contracts).
2407
+ const threshold = PRIORITY_DOMAIN_TAGS.includes(domain) ? 1 : resolvedMinOccurrences;
2408
+ if (effectiveOccurrences < threshold) return;
2409
+ const best = pickBestMemory(items);
2410
+ const avoid = extractAvoidLine(best.content)
2411
+ || 'How to avoid: Investigate and prevent recurrence';
2245
2412
  lines.push('');
2246
2413
  lines.push(`## ${domain}`);
2247
2414
  lines.push(`- Recurrence count: ${items.length} (weighted: ${weightedCount.toFixed(1)})`);
2248
2415
  lines.push(`- Rule: ${avoid.replace(/^How to avoid:\s*/i, '')}`);
2249
- lines.push(`- Latest mistake: ${latest.title}`);
2416
+ lines.push(`- Latest mistake: ${best.title}`);
2250
2417
  });
2251
2418
 
2252
2419
  const rubricEntries = Object.entries(rubricBuckets)
@@ -2313,7 +2480,10 @@ function writePreventionRules(filePath, minOccurrences = 2) {
2313
2480
 
2314
2481
  function feedbackSummary(recentN = 20, options = {}) {
2315
2482
  const { FEEDBACK_LOG_PATH } = getFeedbackPaths(options);
2316
- const entries = readJSONL(FEEDBACK_LOG_PATH);
2483
+ let entries = readJSONL(FEEDBACK_LOG_PATH);
2484
+ if (options.humanOnly) {
2485
+ entries = entries.filter((entry) => normalizeReviewOrigin(entry.reviewOrigin) === 'human');
2486
+ }
2317
2487
  if (entries.length === 0) {
2318
2488
  return '## Feedback Summary\nNo feedback recorded yet.';
2319
2489
  }
@@ -2323,7 +2493,7 @@ function feedbackSummary(recentN = 20, options = {}) {
2323
2493
  const negative = recent.filter((e) => e.signal === 'negative').length;
2324
2494
  const pct = Math.round((positive / recent.length) * 100);
2325
2495
 
2326
- const analysis = analyzeFeedback(FEEDBACK_LOG_PATH);
2496
+ const analysis = analyzeFeedback(FEEDBACK_LOG_PATH, { humanOnly: options.humanOnly });
2327
2497
 
2328
2498
  const lines = [
2329
2499
  `## Feedback Summary (last ${recent.length})`,
@@ -2396,7 +2566,7 @@ function runCli() {
2396
2566
  }
2397
2567
 
2398
2568
  if (args.summary) {
2399
- console.log(feedbackSummary(Number(args.recent || 20)));
2569
+ console.log(feedbackSummary(Number(args.recent || 20), { humanOnly: true }));
2400
2570
  return;
2401
2571
  }
2402
2572