thumbgate 1.27.16 → 1.27.18
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/.claude-plugin/marketplace.json +85 -0
- package/.claude-plugin/plugin.json +3 -4
- package/.well-known/llms.txt +12 -33
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +30 -215
- package/adapters/chatgpt/openapi.yaml +1695 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gemini/function-declarations.json +0 -1
- package/adapters/mcp/server-stdio.js +11 -263
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +122 -1421
- package/bin/postinstall.js +27 -11
- package/config/gate-templates.json +0 -216
- package/config/gates/claim-verification.json +0 -12
- package/config/gates/default.json +2 -31
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +13 -23
- package/config/merge-quality-checks.json +1 -0
- package/config/model-candidates.json +6 -121
- package/openapi/openapi.yaml +0 -12
- package/package.json +98 -228
- package/public/blog.html +4 -4
- package/public/codex-plugin.html +20 -72
- package/public/compare.html +8 -31
- package/public/dashboard.html +166 -930
- package/public/federal.html +2 -2
- package/public/guide.html +9 -33
- package/public/index.html +116 -495
- package/public/learn.html +18 -191
- package/public/lessons.html +10 -168
- package/public/numbers.html +7 -7
- package/public/pro.html +11 -34
- package/scripts/agent-memory-lifecycle.js +0 -211
- package/scripts/agent-readiness.js +3 -167
- package/scripts/agent-reward-model.js +1 -53
- package/scripts/auto-promote-gates.js +10 -82
- package/scripts/auto-wire-hooks.js +0 -14
- package/scripts/billing.js +1 -93
- package/scripts/build-metadata.js +10 -50
- package/scripts/cli-feedback.js +6 -18
- package/scripts/cli-schema.js +0 -90
- package/scripts/cli-telemetry.js +1 -6
- package/scripts/commercial-offer.js +2 -82
- package/scripts/context-manager.js +6 -74
- package/scripts/dashboard.js +2 -68
- package/scripts/durability/step.js +3 -3
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-dpo-pairs.js +2 -7
- package/scripts/feedback-loop.js +1 -123
- package/scripts/feedback-quality.js +0 -87
- package/scripts/filesystem-search.js +10 -35
- package/scripts/gate-stats.js +0 -89
- package/scripts/gates-engine.js +85 -1105
- package/scripts/gemini-embedding-policy.js +1 -2
- package/scripts/hook-runtime.js +14 -20
- package/scripts/hook-thumbgate-cache-updater.js +2 -18
- package/scripts/hybrid-feedback-context.js +7 -142
- package/scripts/lesson-inference.js +3 -8
- package/scripts/lesson-search.js +1 -17
- package/scripts/license.js +1 -0
- package/scripts/llm-client.js +9 -390
- package/scripts/local-model-profile.js +8 -15
- package/scripts/memory-scope-readiness.js +0 -159
- package/scripts/meta-agent-loop.js +0 -36
- package/scripts/operational-integrity.js +5 -39
- package/scripts/oss-pr-opportunity-scout.js +5 -35
- package/scripts/plausible-server-events.js +6 -9
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +1 -42
- package/scripts/rate-limiter.js +13 -64
- package/scripts/secret-scanner.js +5 -44
- package/scripts/security-scanner.js +10 -260
- package/scripts/self-distill-agent.js +1 -3
- package/scripts/seo-gsd.js +2 -916
- package/scripts/statusline-cache-path.js +2 -17
- package/scripts/statusline-local-stats.js +1 -9
- package/scripts/statusline-meta.js +2 -28
- package/scripts/statusline.sh +4 -20
- package/scripts/telemetry-analytics.js +0 -357
- package/scripts/thompson-sampling.js +10 -31
- package/scripts/thumbgate-bench.js +1 -16
- package/scripts/thumbgate-search.js +19 -85
- package/scripts/tool-registry.js +1 -169
- package/scripts/vector-store.js +0 -45
- package/scripts/workflow-sentinel.js +53 -286
- package/scripts/workspace-evolver.js +2 -62
- package/src/api/server.js +290 -2578
- package/.claude/commands/dashboard.md +0 -15
- package/.claude/commands/thumbgate-blocked.md +0 -27
- package/.claude/commands/thumbgate-dashboard.md +0 -15
- package/.claude/commands/thumbgate-doctor.md +0 -30
- package/.claude/commands/thumbgate-guard.md +0 -36
- package/.claude/commands/thumbgate-protect.md +0 -30
- package/.claude/commands/thumbgate-rules.md +0 -30
- package/.well-known/agentic-verify.txt +0 -1
- package/adapters/gcp/dfcx-webhook-gate.js +0 -295
- package/adapters/letta/README.md +0 -41
- package/adapters/letta/thumbgate-letta-adapter.js +0 -133
- package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
- package/bench/observability-eval-suite.json +0 -26
- package/bin/dashboard-cli.js +0 -7
- package/commands/dashboard.md +0 -15
- package/commands/thumbgate-dashboard.md +0 -15
- package/config/builtin-lessons.json +0 -23
- package/config/gate-classifier-routing.json +0 -98
- package/config/post-deploy-marketing-pages.json +0 -61
- package/public/about.html +0 -162
- package/public/agent-manager.html +0 -179
- package/public/agents-cost-savings.html +0 -153
- package/public/ai-malpractice-prevention.html +0 -818
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
- package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
- package/public/assets/brand/thumbgate-mark-team.svg +0 -26
- package/public/assets/brand/thumbgate-mark.svg +0 -21
- package/public/assets/brand/thumbgate-wordmark.svg +0 -20
- package/public/assets/claude-thumbgate-statusbar.svg +0 -8
- package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
- package/public/assets/legal-intake-control-flow.svg +0 -66
- package/public/brand/thumbgate-mark.svg +0 -19
- package/public/brand/thumbgate-og.svg +0 -16
- package/public/chatgpt-app.html +0 -330
- package/public/codex-enterprise.html +0 -123
- package/public/js/buyer-intent.js +0 -672
- package/public/pricing.html +0 -399
- package/scripts/action-receipts.js +0 -324
- package/scripts/activation-quickstart.js +0 -187
- package/scripts/agent-operations-planner.js +0 -621
- package/scripts/ai-component-inventory.js +0 -367
- package/scripts/async-eval-observability.js +0 -236
- package/scripts/audit.js +0 -65
- package/scripts/aws-blocks-guardrails.js +0 -228
- package/scripts/classifier-routing.js +0 -130
- package/scripts/dashboard-chat.js +0 -332
- package/scripts/feedback-aggregate.js +0 -281
- package/scripts/feedback-sanitizer.js +0 -105
- package/scripts/hob-pack.js +0 -591
- package/scripts/hook-stop-anti-claim.js +0 -301
- package/scripts/install-shim.js +0 -87
- package/scripts/mcp-oauth.js +0 -293
- package/scripts/noop-detect.js +0 -285
- package/scripts/omlx-smoke.js +0 -192
- package/scripts/parallel-workflow-orchestrator.js +0 -293
- package/scripts/plan-gate.js +0 -243
- package/scripts/plausible-domain-config.js +0 -99
- package/scripts/qa-scenario-planner.js +0 -136
- package/scripts/repeat-metric.js +0 -137
- package/scripts/secret-fixture-tokens.js +0 -61
- package/scripts/secret-redaction.js +0 -166
- package/scripts/self-harness-optimizer.js +0 -141
- package/scripts/self-healing-check.js +0 -193
- package/scripts/silent-failure-cluster.js +0 -531
- package/scripts/statusline-cache-read.js +0 -57
- package/scripts/sync-telemetry-from-prod.js +0 -374
- package/scripts/tool-contract-validator.js +0 -76
- package/scripts/trajectory-scorer.js +0 -63
- package/scripts/verify-marketing-pages-deployed.js +0 -195
- package/scripts/visitor-journey.js +0 -172
package/scripts/feedback-loop.js
CHANGED
|
@@ -56,90 +56,6 @@ const {
|
|
|
56
56
|
|
|
57
57
|
const AUDIT_TRAIL_TAG = 'audit-trail';
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
* Anonymous fire-and-forget CLI feedback telemetry.
|
|
61
|
-
*
|
|
62
|
-
* Pings the hosted /v1/telemetry/ping endpoint exactly once per successful
|
|
63
|
-
* local feedback capture so the dashboard can measure CLI-side lesson volume.
|
|
64
|
-
*
|
|
65
|
-
* Hard contract (do NOT widen without explicit approval):
|
|
66
|
-
* - ONE event type: `feedback_captured`
|
|
67
|
-
* - Payload: { installId, signal: 'up'|'down', tier, ts } only.
|
|
68
|
-
* No context strings, tags, file paths, or content of any kind.
|
|
69
|
-
* - Opt-out: THUMBGATE_DISABLE_TELEMETRY=1 (or 'true') short-circuits
|
|
70
|
-
* immediately. Legacy THUMBGATE_NO_TELEMETRY=1 / DO_NOT_TRACK=1 are
|
|
71
|
-
* also honored for parity with cli-telemetry.js.
|
|
72
|
-
* - Fire-and-forget: NEVER await this call. Errors are swallowed.
|
|
73
|
-
* - 2-second timeout via AbortSignal.timeout.
|
|
74
|
-
*/
|
|
75
|
-
function emitAnonymousFeedbackPing(signal) {
|
|
76
|
-
try {
|
|
77
|
-
const env = process.env || {};
|
|
78
|
-
if (
|
|
79
|
-
env.THUMBGATE_DISABLE_TELEMETRY === '1' ||
|
|
80
|
-
env.THUMBGATE_DISABLE_TELEMETRY === 'true' ||
|
|
81
|
-
env.THUMBGATE_NO_TELEMETRY === '1' ||
|
|
82
|
-
env.DO_NOT_TRACK === '1'
|
|
83
|
-
) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const normalizedSignal = signal === 'positive' ? 'up' : signal === 'negative' ? 'down' : null;
|
|
88
|
-
if (!normalizedSignal) return;
|
|
89
|
-
|
|
90
|
-
// Reuse the canonical installId from cli-telemetry.js (persisted at
|
|
91
|
-
// ~/.thumbgate/install-id). Falls back to a fresh UUID if that module
|
|
92
|
-
// is unavailable — better to ship an event we can dedup on the server
|
|
93
|
-
// than to drop the ping entirely.
|
|
94
|
-
let installId = null;
|
|
95
|
-
try {
|
|
96
|
-
const { getInstallId } = require('./cli-telemetry');
|
|
97
|
-
installId = getInstallId();
|
|
98
|
-
} catch (_) { /* fall through */ }
|
|
99
|
-
if (!installId) {
|
|
100
|
-
try {
|
|
101
|
-
installId = require('crypto').randomUUID();
|
|
102
|
-
} catch (_) {
|
|
103
|
-
return; // no crypto, no install id → drop silently
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
let tier = 'free';
|
|
108
|
-
try {
|
|
109
|
-
const { getStatuslineMeta } = require('./statusline-meta');
|
|
110
|
-
const meta = getStatuslineMeta({ env });
|
|
111
|
-
const rawTier = String(meta && meta.tier ? meta.tier : 'free').toLowerCase();
|
|
112
|
-
if (rawTier === 'pro' || rawTier === 'enterprise' || rawTier === 'free') {
|
|
113
|
-
tier = rawTier;
|
|
114
|
-
}
|
|
115
|
-
} catch (_) { /* default to 'free' */ }
|
|
116
|
-
|
|
117
|
-
const base = env.THUMBGATE_PUBLIC_APP_ORIGIN
|
|
118
|
-
|| env.THUMBGATE_API_URL
|
|
119
|
-
|| 'https://thumbgate-production.up.railway.app';
|
|
120
|
-
|
|
121
|
-
const body = JSON.stringify({
|
|
122
|
-
eventType: 'feedback_captured',
|
|
123
|
-
clientType: 'cli',
|
|
124
|
-
installId,
|
|
125
|
-
signal: normalizedSignal,
|
|
126
|
-
tier,
|
|
127
|
-
ts: new Date().toISOString(),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Fire-and-forget. No await. AbortSignal.timeout enforces the 2s cap.
|
|
131
|
-
if (typeof fetch !== 'function' || typeof AbortSignal === 'undefined' || typeof AbortSignal.timeout !== 'function') {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
fetch(`${base.replace(/\/+$/, '')}/v1/telemetry/ping`, {
|
|
135
|
-
method: 'POST',
|
|
136
|
-
headers: { 'Content-Type': 'application/json' },
|
|
137
|
-
body,
|
|
138
|
-
signal: AbortSignal.timeout(2000),
|
|
139
|
-
}).catch(() => { /* fire-and-forget */ });
|
|
140
|
-
} catch (_) { /* telemetry must never disrupt CLI */ }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
59
|
function isAuditTrailEntry(entry = {}) {
|
|
144
60
|
return Array.isArray(entry.tags) && entry.tags.includes(AUDIT_TRAIL_TAG);
|
|
145
61
|
}
|
|
@@ -1143,7 +1059,6 @@ function captureFeedback(params) {
|
|
|
1143
1059
|
: null),
|
|
1144
1060
|
structuredRule: structuredRule || null,
|
|
1145
1061
|
...(reflection && { reflection }),
|
|
1146
|
-
gateAction: params.gateAction || null,
|
|
1147
1062
|
timestamp: now,
|
|
1148
1063
|
};
|
|
1149
1064
|
|
|
@@ -1197,7 +1112,6 @@ function captureFeedback(params) {
|
|
|
1197
1112
|
summary.lastUpdated = now;
|
|
1198
1113
|
saveSummary(summary);
|
|
1199
1114
|
appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
|
|
1200
|
-
emitAnonymousFeedbackPing(signal);
|
|
1201
1115
|
try { appendRejectionLedger(feedbackEvent, action.reason); } catch { /* non-critical */ }
|
|
1202
1116
|
try {
|
|
1203
1117
|
appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
|
|
@@ -1239,7 +1153,6 @@ function captureFeedback(params) {
|
|
|
1239
1153
|
...feedbackEvent,
|
|
1240
1154
|
validationIssues: prepared.issues,
|
|
1241
1155
|
});
|
|
1242
|
-
emitAnonymousFeedbackPing(signal);
|
|
1243
1156
|
try { appendRejectionLedger(feedbackEvent, `Schema validation failed: ${prepared.issues.join('; ')}`); } catch { /* non-critical */ }
|
|
1244
1157
|
try {
|
|
1245
1158
|
appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
|
|
@@ -1314,7 +1227,6 @@ function captureFeedback(params) {
|
|
|
1314
1227
|
}
|
|
1315
1228
|
|
|
1316
1229
|
appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
|
|
1317
|
-
emitAnonymousFeedbackPing(signal);
|
|
1318
1230
|
|
|
1319
1231
|
// Synthesis: merge similar lessons instead of creating duplicates
|
|
1320
1232
|
let synthesisResult = null;
|
|
@@ -1486,7 +1398,7 @@ function captureFeedback(params) {
|
|
|
1486
1398
|
if (feedbackEvent.signal === 'negative') {
|
|
1487
1399
|
try {
|
|
1488
1400
|
const autoPromote = require('./auto-promote-gates');
|
|
1489
|
-
const promoteResult = autoPromote.promote(FEEDBACK_LOG_PATH
|
|
1401
|
+
const promoteResult = autoPromote.promote(FEEDBACK_LOG_PATH);
|
|
1490
1402
|
// First-rule activation telemetry: anonymous ping the first time
|
|
1491
1403
|
// a prevention rule auto-promotes for this install. Idempotent —
|
|
1492
1404
|
// see scripts/activation-tracker.js. Critical for activation funnel
|
|
@@ -1499,44 +1411,10 @@ function captureFeedback(params) {
|
|
|
1499
1411
|
totalGates: promoteResult.totalGates,
|
|
1500
1412
|
});
|
|
1501
1413
|
} catch { /* activation telemetry is non-critical */ }
|
|
1502
|
-
|
|
1503
|
-
// Trigger Self-Harness Optimizer to propagate the new rules to prompt files & validate
|
|
1504
|
-
try {
|
|
1505
|
-
const { fork } = require('child_process');
|
|
1506
|
-
const localOptimizerPath = path.join(process.cwd(), 'scripts', 'self-harness-optimizer.js');
|
|
1507
|
-
const packageOptimizerPath = path.join(__dirname, 'self-harness-optimizer.js');
|
|
1508
|
-
|
|
1509
|
-
if (fs.existsSync(localOptimizerPath)) {
|
|
1510
|
-
fork(localOptimizerPath, [], { stdio: 'ignore', detached: true }).unref();
|
|
1511
|
-
} else if (fs.existsSync(packageOptimizerPath)) {
|
|
1512
|
-
fork(packageOptimizerPath, [], { stdio: 'ignore', detached: true }).unref();
|
|
1513
|
-
}
|
|
1514
|
-
} catch (err) {
|
|
1515
|
-
console.error('Failed to trigger self-harness optimizer:', err);
|
|
1516
|
-
}
|
|
1517
1414
|
}
|
|
1518
1415
|
} catch { /* Gate promotion is non-critical */ }
|
|
1519
1416
|
}
|
|
1520
1417
|
|
|
1521
|
-
// Auto-export to Obsidian if configured (deferred but tracked)
|
|
1522
|
-
if (process.env.THUMBGATE_OBSIDIAN_VAULT_PATH) {
|
|
1523
|
-
const exportPromise = new Promise((resolve) => {
|
|
1524
|
-
setImmediate(() => {
|
|
1525
|
-
try {
|
|
1526
|
-
const { exportAll } = require('./obsidian-export');
|
|
1527
|
-
exportAll({
|
|
1528
|
-
feedbackDir: FEEDBACK_DIR,
|
|
1529
|
-
outputDir: process.env.THUMBGATE_OBSIDIAN_VAULT_PATH,
|
|
1530
|
-
});
|
|
1531
|
-
} catch (_err) {
|
|
1532
|
-
// Non-critical, do not crash feedback loop
|
|
1533
|
-
}
|
|
1534
|
-
resolve();
|
|
1535
|
-
});
|
|
1536
|
-
});
|
|
1537
|
-
trackBackgroundSideEffect(exportPromise);
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
1418
|
// --- Deferred side-effects (contextFs, RLAIF — non-critical, potentially slow) ---
|
|
1541
1419
|
setImmediate(() => {
|
|
1542
1420
|
try {
|
|
@@ -62,92 +62,6 @@ function normalizeFeedbackText(value) {
|
|
|
62
62
|
.trim();
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function editDistance(a, b) {
|
|
66
|
-
const left = String(a || '');
|
|
67
|
-
const right = String(b || '');
|
|
68
|
-
const dp = Array.from({ length: left.length + 1 }, () => Array(right.length + 1).fill(0));
|
|
69
|
-
for (let i = 0; i <= left.length; i++) dp[i][0] = i;
|
|
70
|
-
for (let j = 0; j <= right.length; j++) dp[0][j] = j;
|
|
71
|
-
for (let i = 1; i <= left.length; i++) {
|
|
72
|
-
for (let j = 1; j <= right.length; j++) {
|
|
73
|
-
const cost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
74
|
-
dp[i][j] = Math.min(
|
|
75
|
-
dp[i - 1][j] + 1,
|
|
76
|
-
dp[i][j - 1] + 1,
|
|
77
|
-
dp[i - 1][j - 1] + cost,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return dp[left.length][right.length];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function isNearThumbToken(token) {
|
|
85
|
-
const value = String(token || '');
|
|
86
|
-
if (value.length < 4) return false;
|
|
87
|
-
return editDistance(value, 'thumb') <= 1 || editDistance(value, 'thumbs') <= 2;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function isNearUpToken(token) {
|
|
91
|
-
const value = String(token || '');
|
|
92
|
-
return value === 'up' || editDistance(value, 'up') <= 1;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function isNearDownToken(token) {
|
|
96
|
-
const value = String(token || '');
|
|
97
|
-
if (value.length < 2) return false;
|
|
98
|
-
return editDistance(value, 'down') <= 1;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function detectFeedbackSignal(value) {
|
|
102
|
-
const raw = String(value || '');
|
|
103
|
-
if (/[👎👎🏻👎🏼👎🏽👎🏾👎🏿]/u.test(raw)) return { signal: 'down', confidence: 'emoji', match: '👎' };
|
|
104
|
-
if (/[👍👍🏻👍🏼👍🏽👍🏾👍🏿]/u.test(raw)) return { signal: 'up', confidence: 'emoji', match: '👍' };
|
|
105
|
-
|
|
106
|
-
const normalized = normalizeFeedbackText(raw);
|
|
107
|
-
if (!normalized) return null;
|
|
108
|
-
|
|
109
|
-
const exactDown = [
|
|
110
|
-
/\bthumbs?\s*down\b/,
|
|
111
|
-
/\bthumbs?down\b/,
|
|
112
|
-
/\bthat failed\b/,
|
|
113
|
-
/\bit failed\b/,
|
|
114
|
-
/\bthat was wrong\b/,
|
|
115
|
-
/\bfix this\b/,
|
|
116
|
-
];
|
|
117
|
-
if (exactDown.some((pattern) => pattern.test(normalized))) {
|
|
118
|
-
return { signal: 'down', confidence: 'exact', match: normalized };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const exactUp = [
|
|
122
|
-
/\bthumbs?\s*up\b/,
|
|
123
|
-
/\bthumbs?up\b/,
|
|
124
|
-
/\bthat worked\b/,
|
|
125
|
-
/\bit worked\b/,
|
|
126
|
-
/\blooks good\b/,
|
|
127
|
-
/\bgood job\b/,
|
|
128
|
-
/\bgood work\b/,
|
|
129
|
-
/\bnice work\b/,
|
|
130
|
-
/\bperfect\b/,
|
|
131
|
-
/\blgtm\b/,
|
|
132
|
-
];
|
|
133
|
-
if (exactUp.some((pattern) => pattern.test(normalized))) {
|
|
134
|
-
return { signal: 'up', confidence: 'exact', match: normalized };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const words = normalized.split(/\s+/).filter(Boolean);
|
|
138
|
-
for (let i = 0; i < words.length - 1; i++) {
|
|
139
|
-
if (!isNearThumbToken(words[i])) continue;
|
|
140
|
-
if (isNearDownToken(words[i + 1])) {
|
|
141
|
-
return { signal: 'down', confidence: 'fuzzy', match: `${words[i]} ${words[i + 1]}` };
|
|
142
|
-
}
|
|
143
|
-
if (isNearUpToken(words[i + 1])) {
|
|
144
|
-
return { signal: 'up', confidence: 'fuzzy', match: `${words[i]} ${words[i + 1]}` };
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
65
|
function isGenericFeedbackText(value, signal) {
|
|
152
66
|
const normalized = normalizeFeedbackText(value);
|
|
153
67
|
if (!normalized) return false;
|
|
@@ -217,7 +131,6 @@ function buildClarificationMessage(params = {}) {
|
|
|
217
131
|
|
|
218
132
|
module.exports = {
|
|
219
133
|
GENERIC_PHRASE_RULES,
|
|
220
|
-
detectFeedbackSignal,
|
|
221
134
|
normalizeFeedbackSignal,
|
|
222
135
|
normalizeFeedbackText,
|
|
223
136
|
isGenericFeedbackText,
|
|
@@ -53,31 +53,10 @@ function listJsonFiles(dirPath) {
|
|
|
53
53
|
return results;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const STOPWORDS = new Set([
|
|
57
|
-
'a', 'about', 'above', 'after', 'again', 'against', 'all', 'am', 'an', 'and', 'any', 'are', 'arent', 'as', 'at',
|
|
58
|
-
'be', 'because', 'been', 'before', 'being', 'below', 'between', 'both', 'but', 'by',
|
|
59
|
-
'cant', 'cannot', 'could', 'couldnt', 'did', 'didnt', 'do', 'does', 'doesnt', 'doing', 'dont', 'down', 'during',
|
|
60
|
-
'each', 'few', 'for', 'from', 'further', 'had', 'hadnt', 'has', 'hasnt', 'have', 'havent', 'having',
|
|
61
|
-
'he', 'hed', 'hell', 'hes', 'her', 'here', 'heres', 'hers', 'herself', 'him', 'himself', 'his',
|
|
62
|
-
'how', 'hows', 'i', 'id', 'ill', 'im', 'ive', 'if', 'in', 'into', 'is', 'isnt', 'it', 'its', 'itself',
|
|
63
|
-
'lets', 'me', 'more', 'most', 'mustnt', 'my', 'myself',
|
|
64
|
-
'no', 'nor', 'not', 'of', 'off', 'on', 'once', 'only', 'or', 'other', 'ought', 'our', 'ours', 'ourselves', 'out', 'over', 'own',
|
|
65
|
-
'same', 'shant', 'she', 'shed', 'shell', 'shes', 'should', 'shouldnt', 'so', 'some', 'such',
|
|
66
|
-
'than', 'that', 'thats', 'the', 'their', 'theirs', 'them', 'themselves', 'then', 'there', 'theres', 'these', 'they', 'theyd', 'theyll', 'theyre', 'theyve', 'this', 'those', 'through', 'to', 'too', 'under', 'until', 'up', 'very',
|
|
67
|
-
'was', 'wasnt', 'we', 'wed', 'well', 'were', 'weve', 'werent', 'what', 'whats', 'when', 'whens', 'where', 'wheres', 'which', 'while', 'who', 'whos', 'whom', 'why', 'whys',
|
|
68
|
-
'with', 'wont', 'would', 'wouldnt', 'you', 'youd', 'youll', 'youre', 'youve', 'your', 'yours', 'yourself', 'yourselves'
|
|
69
|
-
]);
|
|
70
|
-
|
|
71
56
|
function tokenize(text) {
|
|
72
57
|
return String(text || '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
73
58
|
}
|
|
74
59
|
|
|
75
|
-
function getSearchTokens(queryText) {
|
|
76
|
-
const allTokens = tokenize(queryText);
|
|
77
|
-
const contentTokens = allTokens.filter(t => !STOPWORDS.has(t));
|
|
78
|
-
return contentTokens.length > 0 ? contentTokens : allTokens;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
60
|
function unique(arr) {
|
|
82
61
|
return [...new Set(arr)];
|
|
83
62
|
}
|
|
@@ -103,7 +82,7 @@ function substringBoost(query, text) {
|
|
|
103
82
|
const q = query.toLowerCase();
|
|
104
83
|
const t = text.toLowerCase();
|
|
105
84
|
if (t.includes(q)) return 0.3;
|
|
106
|
-
const words = q.split(/\s+/).filter((w) => w.length > 2
|
|
85
|
+
const words = q.split(/\s+/).filter((w) => w.length > 2);
|
|
107
86
|
const matched = words.filter((w) => t.includes(w)).length;
|
|
108
87
|
return words.length > 0 ? (matched / words.length) * 0.2 : 0;
|
|
109
88
|
}
|
|
@@ -138,12 +117,8 @@ function scoreRecord(queryTokens, queryText, record) {
|
|
|
138
117
|
const recency = recencyScore(record.timestamp);
|
|
139
118
|
const signalBoost = record.signal === 'down' ? 0.05 : 0;
|
|
140
119
|
|
|
141
|
-
const matchScore = jaccard + substr;
|
|
142
|
-
const isWildcard = !queryText || queryText === '*';
|
|
143
|
-
const score = isWildcard ? (recency + signalBoost || 0.01) : (matchScore > 0 ? matchScore + recency + signalBoost : 0);
|
|
144
|
-
|
|
145
120
|
return {
|
|
146
|
-
score:
|
|
121
|
+
score: jaccard + substr + recency + signalBoost,
|
|
147
122
|
record,
|
|
148
123
|
matchedTokens: unique(queryTokens).filter((t) => new Set(recordTokens).has(t)),
|
|
149
124
|
};
|
|
@@ -154,7 +129,7 @@ function scoreRecord(queryTokens, queryText, record) {
|
|
|
154
129
|
// ---------------------------------------------------------------------------
|
|
155
130
|
|
|
156
131
|
function searchFeedbackLog(queryText, limit = 5, options = {}) {
|
|
157
|
-
const logPath = path.join(
|
|
132
|
+
const logPath = path.join(getFeedbackDir(), 'feedback-log.jsonl');
|
|
158
133
|
let records = readJsonl(logPath);
|
|
159
134
|
|
|
160
135
|
// SQLite fallback: if JSONL is empty/tiny, pull records from the lesson DB
|
|
@@ -181,7 +156,7 @@ function searchFeedbackLog(queryText, limit = 5, options = {}) {
|
|
|
181
156
|
|
|
182
157
|
// Wildcard query: return all records sorted by recency
|
|
183
158
|
const isWildcard = queryText === '*' || queryText === '';
|
|
184
|
-
const queryTokens = isWildcard ? [] :
|
|
159
|
+
const queryTokens = isWildcard ? [] : tokenize(queryText);
|
|
185
160
|
|
|
186
161
|
let scored = isWildcard
|
|
187
162
|
? records.map((r) => ({ score: recencyScore(r.timestamp) || 0.01, record: r, matchedTokens: [] }))
|
|
@@ -211,9 +186,9 @@ function searchFeedbackLog(queryText, limit = 5, options = {}) {
|
|
|
211
186
|
}
|
|
212
187
|
|
|
213
188
|
function searchContextFs(queryText, limit = 5, options = {}) {
|
|
214
|
-
const contextDir =
|
|
189
|
+
const contextDir = getContextFsDir();
|
|
215
190
|
const namespaces = options.namespaces || ['memory/error', 'memory/learning', 'rules', 'raw_history'];
|
|
216
|
-
const queryTokens =
|
|
191
|
+
const queryTokens = tokenize(queryText);
|
|
217
192
|
const scored = [];
|
|
218
193
|
|
|
219
194
|
for (const ns of namespaces) {
|
|
@@ -246,12 +221,12 @@ function searchContextFs(queryText, limit = 5, options = {}) {
|
|
|
246
221
|
}));
|
|
247
222
|
}
|
|
248
223
|
|
|
249
|
-
function searchPreventionRules(queryText, limit = 5
|
|
250
|
-
const rulesPath = path.join(
|
|
224
|
+
function searchPreventionRules(queryText, limit = 5) {
|
|
225
|
+
const rulesPath = path.join(getFeedbackDir(), 'prevention-rules.md');
|
|
251
226
|
if (!fs.existsSync(rulesPath)) return [];
|
|
252
227
|
|
|
253
228
|
const content = fs.readFileSync(rulesPath, 'utf-8');
|
|
254
|
-
const queryTokens =
|
|
229
|
+
const queryTokens = tokenize(queryText);
|
|
255
230
|
const blocks = content.split(/^#{1,3}\s+/m).filter(Boolean);
|
|
256
231
|
|
|
257
232
|
return blocks
|
|
@@ -277,7 +252,7 @@ function searchPreventionRules(queryText, limit = 5, options = {}) {
|
|
|
277
252
|
function searchAll(queryText, limit = 10, options = {}) {
|
|
278
253
|
const feedbackResults = searchFeedbackLog(queryText, limit, options);
|
|
279
254
|
const contextResults = searchContextFs(queryText, limit, options);
|
|
280
|
-
const ruleResults = searchPreventionRules(queryText, limit
|
|
255
|
+
const ruleResults = searchPreventionRules(queryText, limit);
|
|
281
256
|
|
|
282
257
|
const merged = [
|
|
283
258
|
...feedbackResults.map((r) => ({ ...r, _source_type: 'feedback' })),
|
package/scripts/gate-stats.js
CHANGED
|
@@ -9,7 +9,6 @@ const { sequencePathFor } = require('./risk-scorer');
|
|
|
9
9
|
|
|
10
10
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
11
11
|
const MANUAL_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
|
|
12
|
-
const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
|
|
13
12
|
|
|
14
13
|
function loadGatesFile(filePath) {
|
|
15
14
|
if (!fs.existsSync(filePath)) return [];
|
|
@@ -66,15 +65,6 @@ function calculateStats() {
|
|
|
66
65
|
// sample can produce a misleading 0.0% floor.
|
|
67
66
|
const bayesErrorRate = tryComputeBayesErrorRate();
|
|
68
67
|
|
|
69
|
-
// Calibration: per-gate assessment of whether block actions are well-supported
|
|
70
|
-
// by negative feedback, or potentially over/under-blocking without confirmation.
|
|
71
|
-
const calibration = computeCalibration(allGates);
|
|
72
|
-
|
|
73
|
-
// First-time fix rate: 1 - (recurringBlocks / totalBlocksAndWarns)
|
|
74
|
-
// Measures how often a single gate fire resolves the issue vs the agent retrying.
|
|
75
|
-
// Returns null when there is no recorded block/warn data yet.
|
|
76
|
-
const firstTimeFixRate = computeFirstTimeFixRate();
|
|
77
|
-
|
|
78
68
|
return {
|
|
79
69
|
totalGates: allGates.length,
|
|
80
70
|
manualGates: manualGates.length,
|
|
@@ -87,70 +77,10 @@ function calculateStats() {
|
|
|
87
77
|
lastPromotion,
|
|
88
78
|
estimatedHoursSaved,
|
|
89
79
|
bayesErrorRate,
|
|
90
|
-
calibration,
|
|
91
|
-
firstTimeFixRate,
|
|
92
80
|
gates: allGates,
|
|
93
81
|
};
|
|
94
82
|
}
|
|
95
83
|
|
|
96
|
-
/**
|
|
97
|
-
* Assess each gate's calibration by comparing block occurrences to confirmed
|
|
98
|
-
* negative feedback counts. A gate with many blocks but no confirming negative
|
|
99
|
-
* feedback may be over-blocking; one with matching feedback is well-calibrated.
|
|
100
|
-
*
|
|
101
|
-
* @param {Array} gates - Combined array of manual + auto-promoted gate objects
|
|
102
|
-
* @returns {Array<{gateId: string, occurrences: number, action: string, calibrationNote: string}>}
|
|
103
|
-
*/
|
|
104
|
-
function computeCalibration(gates) {
|
|
105
|
-
const calibration = [];
|
|
106
|
-
for (const gate of gates || []) {
|
|
107
|
-
if (!gate || !gate.id) continue;
|
|
108
|
-
const occurrences = Number(gate.occurrences || 0);
|
|
109
|
-
const action = gate.action || 'unknown';
|
|
110
|
-
// Only annotate gates with recorded occurrence data
|
|
111
|
-
if (occurrences === 0) continue;
|
|
112
|
-
|
|
113
|
-
if (action === 'block') {
|
|
114
|
-
const confirmedNegative = Number(gate.confirmedNegative || gate.negativeCount || 0);
|
|
115
|
-
let calibrationNote;
|
|
116
|
-
if (occurrences > 10 && confirmedNegative === 0) {
|
|
117
|
-
calibrationNote = `over-blocking (${occurrences} blocks, 0 confirmed)`;
|
|
118
|
-
} else if (confirmedNegative > 0) {
|
|
119
|
-
calibrationNote = `well-calibrated (${occurrences} blocks, ${confirmedNegative} confirmed)`;
|
|
120
|
-
} else {
|
|
121
|
-
// Low occurrence count with no feedback — not enough data yet
|
|
122
|
-
calibrationNote = `insufficient data (${occurrences} blocks, 0 confirmed)`;
|
|
123
|
-
}
|
|
124
|
-
calibration.push({ gateId: gate.id, occurrences, action, calibrationNote });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return calibration;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Compute the first-time fix rate from the persisted gate-stats.json file.
|
|
132
|
-
*
|
|
133
|
-
* firstTimeFixRate = 1 - (recurringBlocks / totalBlocksAndWarns)
|
|
134
|
-
*
|
|
135
|
-
* Returns null when there are no recorded block/warn events yet.
|
|
136
|
-
* Returns a number in [0, 1] otherwise, where 1.0 means every gate fire
|
|
137
|
-
* was a first-time occurrence and 0.0 means every gate fired at least twice.
|
|
138
|
-
*/
|
|
139
|
-
function computeFirstTimeFixRate() {
|
|
140
|
-
try {
|
|
141
|
-
if (!fs.existsSync(STATS_PATH)) return null;
|
|
142
|
-
const raw = fs.readFileSync(STATS_PATH, 'utf8');
|
|
143
|
-
const data = JSON.parse(raw);
|
|
144
|
-
const totalBlocksAndWarns = (data.blocked || 0) + (data.warned || 0);
|
|
145
|
-
if (totalBlocksAndWarns === 0) return null;
|
|
146
|
-
const recurring = data.recurringBlocks || 0;
|
|
147
|
-
const rate = 1 - (recurring / totalBlocksAndWarns);
|
|
148
|
-
return Math.max(0, Math.min(1, rate));
|
|
149
|
-
} catch {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
84
|
function tryComputeBayesErrorRate() {
|
|
155
85
|
try {
|
|
156
86
|
const seqPath = sequencePathFor();
|
|
@@ -212,13 +142,6 @@ function formatStats(stats) {
|
|
|
212
142
|
lines.push(` Last promotion: ${formatLastPromotion(stats.lastPromotion)}`);
|
|
213
143
|
lines.push(` Estimated time saved: ~${stats.estimatedHoursSaved} hours`);
|
|
214
144
|
lines.push(` Bayes error rate: ${formatBayesErrorRate(stats.bayesErrorRate)}`);
|
|
215
|
-
lines.push(` First-time fix rate: ${formatFirstTimeFixRate(stats.firstTimeFixRate)}`);
|
|
216
|
-
if (Array.isArray(stats.calibration) && stats.calibration.length > 0) {
|
|
217
|
-
lines.push('Calibration:');
|
|
218
|
-
for (const entry of stats.calibration) {
|
|
219
|
-
lines.push(` - ${entry.gateId}: ${entry.calibrationNote}`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
145
|
return lines.join('\n');
|
|
223
146
|
}
|
|
224
147
|
|
|
@@ -230,14 +153,6 @@ function formatBayesErrorRate(rate) {
|
|
|
230
153
|
return `${pct}% — high irreducible error; the feature set can't discriminate`;
|
|
231
154
|
}
|
|
232
155
|
|
|
233
|
-
function formatFirstTimeFixRate(rate) {
|
|
234
|
-
if (rate === null || rate === undefined) return 'n/a (no blocks or warns recorded yet)';
|
|
235
|
-
const pct = (rate * 100).toFixed(1);
|
|
236
|
-
if (rate >= 0.95) return `${pct}% — agents fix issues on first block (excellent)`;
|
|
237
|
-
if (rate >= 0.80) return `${pct}% — most blocks resolved first time (good)`;
|
|
238
|
-
return `${pct}% — recurring blocks detected; agents may be ignoring gate feedback`;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
156
|
if (require.main === module) {
|
|
242
157
|
try {
|
|
243
158
|
const stats = calculateStats();
|
|
@@ -253,11 +168,7 @@ module.exports = {
|
|
|
253
168
|
formatStats,
|
|
254
169
|
formatLastPromotion,
|
|
255
170
|
formatBayesErrorRate,
|
|
256
|
-
formatFirstTimeFixRate,
|
|
257
|
-
computeFirstTimeFixRate,
|
|
258
171
|
loadGatesFile,
|
|
259
172
|
tryComputeBayesErrorRate,
|
|
260
|
-
computeCalibration,
|
|
261
173
|
MANUAL_GATES_PATH,
|
|
262
|
-
STATS_PATH,
|
|
263
174
|
};
|