thumbgate 1.27.18 → 1.27.19
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 +6 -6
- package/.claude-plugin/plugin.json +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +249 -30
- package/adapters/chatgpt/openapi.yaml +12 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1429 -121
- package/bin/postinstall.js +1 -8
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +1 -1
- package/public/blog.html +4 -4
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pro.html +34 -11
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/rate-limiter.js +64 -13
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/seo-gsd.js +916 -7
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-registry.js +169 -1
- package/scripts/vector-store.js +45 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/scripts/bot-detector.js +0 -50
package/public/pro.html
CHANGED
|
@@ -11,11 +11,12 @@ __GOOGLE_SITE_VERIFICATION_META__
|
|
|
11
11
|
<meta property="og:type" content="website">
|
|
12
12
|
<meta property="og:url" content="__APP_ORIGIN__/pro">
|
|
13
13
|
<link rel="canonical" href="__APP_ORIGIN__/pro">
|
|
14
|
+
<link rel="alternate" type="text/markdown" title="ThumbGate LLM context" href="__APP_ORIGIN__/llm-context.md">
|
|
14
15
|
<link rel="icon" type="image/png" href="/thumbgate-icon.png">
|
|
15
16
|
<link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg">
|
|
16
17
|
<meta property="og:image" content="/og.png">
|
|
17
18
|
|
|
18
|
-
<script defer data-domain="thumbgate
|
|
19
|
+
<script defer data-domain="thumbgate.ai" src="https://plausible.io/js/script.js"></script>
|
|
19
20
|
__GA_BOOTSTRAP__
|
|
20
21
|
|
|
21
22
|
<script>
|
|
@@ -36,7 +37,7 @@ __GA_BOOTSTRAP__
|
|
|
36
37
|
<script type="application/ld+json">
|
|
37
38
|
{
|
|
38
39
|
"@context": "https://schema.org",
|
|
39
|
-
"@type": "FAQPage", "mainEntity": [{ "@type": "Question", "name": "How is Pro different from the free install?", "acceptedAnswer": { "@type": "Answer", "text": "Free keeps local recall, checks, and MCP. Pro adds the personal dashboard, DPO export, auto-connect, and founder support." } }, { "@type": "Question", "name": "Does Pro require a cloud account?", "acceptedAnswer": { "@type": "Answer", "text": "No. Pro stays local-first;
|
|
40
|
+
"@type": "FAQPage", "mainEntity": [{ "@type": "Question", "name": "How is Pro different from the free install?", "acceptedAnswer": { "@type": "Answer", "text": "Free keeps local recall, checks, and MCP. Pro adds the personal dashboard, DPO export, auto-connect, and founder support." } }, { "@type": "Question", "name": "Does Pro require a cloud account?", "acceptedAnswer": { "@type": "Answer", "text": "No. Pro stays local-first; Enterprise is the hosted rollout lane for shared lessons, org visibility, and reviews." } }, { "@type": "Question", "name": "What happens after checkout?", "acceptedAnswer": { "@type": "Answer", "text": "You activate Pro, connect the local dashboard, and inspect blocked actions, lessons, and exports." } }, { "@type": "Question", "name": "When should I choose Enterprise instead of Pro?", "acceptedAnswer": { "@type": "Answer", "text": "Choose Enterprise when one correction needs to protect multiple developers or agents across shared repositories." } }]
|
|
40
41
|
}
|
|
41
42
|
</script>
|
|
42
43
|
|
|
@@ -711,7 +712,7 @@ __GA_BOOTSTRAP__
|
|
|
711
712
|
<body>
|
|
712
713
|
<nav>
|
|
713
714
|
<div class="container">
|
|
714
|
-
<a href="/" class="nav-logo"><img src="/assets/brand/thumbgate-mark-inline.svg" alt="ThumbGate" class="logo-mark" width="28" height="28"><span class="logo-text">ThumbGate Pro</span></a>
|
|
715
|
+
<a href="/" class="nav-logo"><img src="/assets/brand/thumbgate-mark-inline-v3.svg" alt="ThumbGate" class="logo-mark" width="28" height="28"><span class="logo-text">ThumbGate Pro</span></a>
|
|
715
716
|
<div class="nav-links">
|
|
716
717
|
<a href="#why-pay">Why Pro</a>
|
|
717
718
|
<a href="#proof">Proof</a>
|
|
@@ -730,7 +731,7 @@ __GA_BOOTSTRAP__
|
|
|
730
731
|
<h1>Buy the operator loop that proves your AI agent stopped repeating the mistake.</h1>
|
|
731
732
|
<p style="font-size:13px;opacity:0.8;margin-bottom:0.5rem;">Updated: <time datetime="2026-04-20">2026-04-20</time> · by <a href="https://github.com/IgorGanapolsky" style="color:inherit;">Igor Ganapolsky</a></p>
|
|
732
733
|
<p>ThumbGate Pro is for one operator who already hit a repeated AI-agent failure and now needs proof: what was blocked, why it was blocked, and what changed before the next risky run.</p>
|
|
733
|
-
<p>Start Pro when you want the local dashboard, DPO export, and a single proof lane for the repeated mistake you need to stop.
|
|
734
|
+
<p>Start Pro when you want the local dashboard, DPO export, and a single proof lane for the repeated mistake you need to stop. Enterprise diagnostics and custom services are handled through intake, not this buyer path.</p>
|
|
734
735
|
<div class="hero-proof">
|
|
735
736
|
<div class="proof-pill">Personal local dashboard</div>
|
|
736
737
|
<div class="proof-pill">DPO export from real corrections</div>
|
|
@@ -815,6 +816,28 @@ __GA_BOOTSTRAP__
|
|
|
815
816
|
</div>
|
|
816
817
|
</section>
|
|
817
818
|
|
|
819
|
+
<section class="section" id="deterministic-loop">
|
|
820
|
+
<div class="container">
|
|
821
|
+
<div class="section-label">Why Pro now</div>
|
|
822
|
+
<h2 class="section-title">Black-box thumbs do not prove prevention. Pro gives the operator an audit loop.</h2>
|
|
823
|
+
<p class="section-intro">Native rating buttons can tell a vendor that an answer felt wrong. ThumbGate Pro gives you the operational record: the correction, the lesson, the rule, the blocked tool call, and the export path.</p>
|
|
824
|
+
<div class="grid-3">
|
|
825
|
+
<div class="feature-card">
|
|
826
|
+
<h3>Inspectable memory</h3>
|
|
827
|
+
<p>Search the exact lesson that came from a thumbs-down and see whether it is still active, warning-only, or blocking.</p>
|
|
828
|
+
</div>
|
|
829
|
+
<div class="feature-card">
|
|
830
|
+
<h3>Deterministic checks</h3>
|
|
831
|
+
<p>The enforcement layer evaluates tool name, arguments, working directory, command shape, confidence, and required evidence before the action runs.</p>
|
|
832
|
+
</div>
|
|
833
|
+
<div class="feature-card">
|
|
834
|
+
<h3>Exportable proof</h3>
|
|
835
|
+
<p>Take the same correction history into JSONL, DPO export, review packets, and team rollout conversations instead of trusting hidden memory.</p>
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
838
|
+
</div>
|
|
839
|
+
</section>
|
|
840
|
+
|
|
818
841
|
<section class="section" id="why-pay">
|
|
819
842
|
<div class="container">
|
|
820
843
|
<div class="section-label">Why operators pay</div>
|
|
@@ -901,11 +924,11 @@ __GA_BOOTSTRAP__
|
|
|
901
924
|
|
|
902
925
|
<div class="pricing-sidebar">
|
|
903
926
|
<div class="team-card">
|
|
904
|
-
<div class="section-label" style="text-align:left;margin-bottom:8px;">When
|
|
927
|
+
<div class="section-label" style="text-align:left;margin-bottom:8px;">When Enterprise is better</div>
|
|
905
928
|
<h3>Need shared enforcement?</h3>
|
|
906
|
-
<p>Choose
|
|
929
|
+
<p>Choose Enterprise when one correction must protect multiple developers or agents across shared repositories, CI, approval policies, and audit trails. Enterprise is custom pricing, scoped after intake.</p>
|
|
907
930
|
<div class="hero-actions" style="margin-top:18px;">
|
|
908
|
-
<a class="btn-secondary" href="/#workflow-sprint-intake">Book
|
|
931
|
+
<a class="btn-secondary" href="/#workflow-sprint-intake">Book an Enterprise Pilot Call</a>
|
|
909
932
|
</div>
|
|
910
933
|
</div>
|
|
911
934
|
<div class="team-card">
|
|
@@ -928,15 +951,15 @@ __GA_BOOTSTRAP__
|
|
|
928
951
|
</div>
|
|
929
952
|
<div class="faq-item">
|
|
930
953
|
<button class="faq-q" type="button" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)" aria-expanded="false">Does Pro require a cloud account?</button>
|
|
931
|
-
<div class="faq-a">No. Pro is still local-first for the individual operator lane.
|
|
954
|
+
<div class="faq-a">No. Pro is still local-first for the individual operator lane. Enterprise is the hosted rollout lane for shared lessons, org visibility, and hosted review views.</div>
|
|
932
955
|
</div>
|
|
933
956
|
<div class="faq-item">
|
|
934
957
|
<button class="faq-q" type="button" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)" aria-expanded="false">What happens after checkout?</button>
|
|
935
958
|
<div class="faq-a">You activate Pro, connect the personal local dashboard, and your running agents can appear automatically so you can inspect blocked actions, active lessons, and exportable DPO pairs without adding a separate cloud dashboard dependency.</div>
|
|
936
959
|
</div>
|
|
937
960
|
<div class="faq-item">
|
|
938
|
-
<button class="faq-q" type="button" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)" aria-expanded="false">When should I choose
|
|
939
|
-
<div class="faq-a">Choose
|
|
961
|
+
<button class="faq-q" type="button" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)" aria-expanded="false">When should I choose Enterprise instead of Pro?</button>
|
|
962
|
+
<div class="faq-a">Choose Enterprise when one thumbs-down should protect multiple people or agents across shared repositories, or when you need shared hosted lessons, org dashboard visibility, and a workflow hardening pilot with rollout review views.</div>
|
|
940
963
|
</div>
|
|
941
964
|
</div>
|
|
942
965
|
</div>
|
|
@@ -964,7 +987,7 @@ __GA_BOOTSTRAP__
|
|
|
964
987
|
<a href="__VERIFICATION_URL__" target="_blank" rel="noopener">Verification Evidence</a>
|
|
965
988
|
<a href="https://github.com/IgorGanapolsky/ThumbGate" target="_blank" rel="noopener">GitHub</a>
|
|
966
989
|
</div>
|
|
967
|
-
<div class="footer-copy">ThumbGate Pro for individual operators.
|
|
990
|
+
<div class="footer-copy">ThumbGate Pro for individual operators. Enterprise stays intake-first.</div>
|
|
968
991
|
</div>
|
|
969
992
|
</footer>
|
|
970
993
|
|
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const MEMORY_TYPES = new Set(['episodic', 'semantic', 'procedural', 'preference', 'working']);
|
|
5
|
+
const MEMORY_SCOPES = new Set(['task', 'session', 'user', 'project', 'org']);
|
|
6
|
+
const HIGH_RISK_TERMS = new Set([
|
|
7
|
+
'billing',
|
|
8
|
+
'checkout',
|
|
9
|
+
'compliance',
|
|
10
|
+
'credential',
|
|
11
|
+
'data-loss',
|
|
12
|
+
'deploy',
|
|
13
|
+
'deployment',
|
|
14
|
+
'git',
|
|
15
|
+
'payment',
|
|
16
|
+
'production',
|
|
17
|
+
'release',
|
|
18
|
+
'secret',
|
|
19
|
+
'security',
|
|
20
|
+
'stripe',
|
|
21
|
+
'verification',
|
|
22
|
+
]);
|
|
23
|
+
const KNOWN_ENTITY_PATTERNS = [
|
|
24
|
+
['Claude Code', /\bclaude\s+code\b/i, 'agent'],
|
|
25
|
+
['Codex', /\bcodex\b/i, 'agent'],
|
|
26
|
+
['Cursor', /\bcursor\b/i, 'agent'],
|
|
27
|
+
['Gemini CLI', /\bgemini\s+cli\b/i, 'agent'],
|
|
28
|
+
['MCP', /\bmcp\b/i, 'protocol'],
|
|
29
|
+
['Stripe', /\bstripe\b/i, 'service'],
|
|
30
|
+
['GitHub', /\bgithub\b|\bgh\s+/i, 'service'],
|
|
31
|
+
['Railway', /\brailway\b/i, 'service'],
|
|
32
|
+
['Plausible', /\bplausible\b/i, 'service'],
|
|
33
|
+
['PostHog', /\bposthog\b/i, 'service'],
|
|
34
|
+
['SQLite', /\bsqlite\b|\bfts5\b/i, 'storage'],
|
|
35
|
+
['LanceDB', /\blancedb\b/i, 'storage'],
|
|
36
|
+
['Docker', /\bdocker\b/i, 'runtime'],
|
|
37
|
+
['npm', /\bnpm\b|\bnpx\b/i, 'runtime'],
|
|
38
|
+
];
|
|
5
39
|
|
|
6
40
|
function normalizeText(value) {
|
|
7
41
|
if (value === undefined || value === null) return '';
|
|
@@ -13,6 +47,178 @@ function normalizeMemoryType(value) {
|
|
|
13
47
|
return MEMORY_TYPES.has(normalized) ? normalized : 'episodic';
|
|
14
48
|
}
|
|
15
49
|
|
|
50
|
+
function tokenize(value) {
|
|
51
|
+
return normalizeText(value)
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
.split(/[^a-z0-9_.:/-]+/)
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function uniqueByName(entities) {
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
return entities.filter((entity) => {
|
|
60
|
+
const key = normalizeText(entity.name).toLowerCase();
|
|
61
|
+
if (!key || seen.has(key)) return false;
|
|
62
|
+
seen.add(key);
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function collectMemoryText(memory = {}) {
|
|
68
|
+
return [
|
|
69
|
+
memory.title,
|
|
70
|
+
memory.content,
|
|
71
|
+
memory.context,
|
|
72
|
+
memory.whatWentWrong,
|
|
73
|
+
memory.whatToChange,
|
|
74
|
+
memory.whatWorked,
|
|
75
|
+
memory.domain,
|
|
76
|
+
memory.skill,
|
|
77
|
+
Array.isArray(memory.tags) ? memory.tags.join(' ') : memory.tags,
|
|
78
|
+
].filter(Boolean).join(' ');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractMemoryEntities(memory = {}) {
|
|
82
|
+
const text = collectMemoryText(memory);
|
|
83
|
+
const entities = [];
|
|
84
|
+
|
|
85
|
+
for (const [name, pattern, type] of KNOWN_ENTITY_PATTERNS) {
|
|
86
|
+
if (pattern.test(text)) entities.push({ name, type });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const commandMatches = text.match(/`([^`]+)`/g) || [];
|
|
90
|
+
for (const match of commandMatches) {
|
|
91
|
+
const command = match.slice(1, -1).trim();
|
|
92
|
+
if (/^(git|npm|npx|node|gh|curl|docker|python|pytest|stripe)\b/i.test(command)) {
|
|
93
|
+
entities.push({ name: command, type: 'command' });
|
|
94
|
+
} else if (/[./-]/.test(command)) {
|
|
95
|
+
entities.push({ name: command, type: 'path' });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const pathMatches = text.match(/\b(?:[a-z0-9_-]+\/)+[a-z0-9_.-]+\b/gi) || [];
|
|
100
|
+
for (const filePath of pathMatches.slice(0, 8)) {
|
|
101
|
+
entities.push({ name: filePath, type: 'path' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return uniqueByName(entities).slice(0, 16);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function inferMemoryScope(memory = {}) {
|
|
108
|
+
const explicit = normalizeText(memory.scope || memory.memoryScope).toLowerCase();
|
|
109
|
+
if (MEMORY_SCOPES.has(explicit)) return explicit;
|
|
110
|
+
|
|
111
|
+
const text = collectMemoryText(memory).toLowerCase();
|
|
112
|
+
const tags = new Set(Array.isArray(memory.tags) ? memory.tags.map((tag) => normalizeText(tag).toLowerCase()) : []);
|
|
113
|
+
|
|
114
|
+
if (tags.has('preference') || /\b(prefer|style|tone|my preference|user preference)\b/.test(text)) return 'user';
|
|
115
|
+
if (tags.has('org') || tags.has('team') || /\b(enterprise|seat|team|shared|org|compliance|policy|approval)\b/.test(text)) return 'org';
|
|
116
|
+
if (tags.has('repo') || tags.has('project') || tags.has('release') || tags.has('deployment')
|
|
117
|
+
|| /\b(repo|repository|branch|ci|pull request|github|deploy|production|release|publish)\b/.test(text)) return 'project';
|
|
118
|
+
if (tags.has('session') || /\b(this session|current session|today|right now)\b/.test(text)) return 'session';
|
|
119
|
+
return 'task';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function scoreMemoryDecay(memory = {}, options = {}) {
|
|
123
|
+
const nowMs = options.now ? new Date(options.now).getTime() : Date.now();
|
|
124
|
+
const timestampMs = memory.timestamp ? new Date(memory.timestamp).getTime() : NaN;
|
|
125
|
+
const ageDays = Number.isFinite(timestampMs)
|
|
126
|
+
? Math.max(0, (nowMs - timestampMs) / (1000 * 60 * 60 * 24))
|
|
127
|
+
: null;
|
|
128
|
+
const textTokens = new Set(tokenize(collectMemoryText(memory)));
|
|
129
|
+
const tags = Array.isArray(memory.tags) ? memory.tags.map((tag) => normalizeText(tag).toLowerCase()) : [];
|
|
130
|
+
const highRisk = tags.some((tag) => HIGH_RISK_TERMS.has(tag))
|
|
131
|
+
|| [...textTokens].some((token) => HIGH_RISK_TERMS.has(token))
|
|
132
|
+
|| ['critical', 'high'].includes(normalizeText(memory.importance).toLowerCase());
|
|
133
|
+
|
|
134
|
+
if (highRisk) {
|
|
135
|
+
return {
|
|
136
|
+
state: 'sticky',
|
|
137
|
+
ageDays,
|
|
138
|
+
score: 1,
|
|
139
|
+
reason: 'high-risk memories stay retrievable until explicitly retired',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (ageDays === null) {
|
|
143
|
+
return {
|
|
144
|
+
state: 'review',
|
|
145
|
+
ageDays,
|
|
146
|
+
score: 0.6,
|
|
147
|
+
reason: 'memory has no timestamp, so it needs review before durable promotion',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (ageDays > 180) {
|
|
151
|
+
return {
|
|
152
|
+
state: 'archive_candidate',
|
|
153
|
+
ageDays,
|
|
154
|
+
score: 0.2,
|
|
155
|
+
reason: 'old low-risk memory should be consolidated or archived',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (ageDays > 60) {
|
|
159
|
+
return {
|
|
160
|
+
state: 'review',
|
|
161
|
+
ageDays,
|
|
162
|
+
score: 0.55,
|
|
163
|
+
reason: 'older low-risk memory should be refreshed before it dominates recall',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
state: 'active',
|
|
168
|
+
ageDays,
|
|
169
|
+
score: 0.85,
|
|
170
|
+
reason: 'recent memory remains eligible for recall',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function scoreHybridMemoryMatch(query, memory = {}, options = {}) {
|
|
175
|
+
const queryTokens = new Set(tokenize(query));
|
|
176
|
+
const memoryTokens = new Set(tokenize(collectMemoryText(memory)));
|
|
177
|
+
const queryText = normalizeText(query).toLowerCase();
|
|
178
|
+
const memoryText = collectMemoryText(memory).toLowerCase();
|
|
179
|
+
const memoryEntities = extractMemoryEntities(memory);
|
|
180
|
+
const queryEntityNames = extractMemoryEntities({ content: query }).map((entity) => entity.name.toLowerCase());
|
|
181
|
+
|
|
182
|
+
let lexicalMatches = 0;
|
|
183
|
+
for (const token of queryTokens) {
|
|
184
|
+
if (memoryTokens.has(token)) lexicalMatches++;
|
|
185
|
+
}
|
|
186
|
+
const lexicalScore = queryTokens.size > 0 ? lexicalMatches / queryTokens.size : 0;
|
|
187
|
+
const phraseScore = queryText && memoryText.includes(queryText) ? 0.35 : 0;
|
|
188
|
+
const entityMatches = memoryEntities.filter((entity) => queryEntityNames.includes(entity.name.toLowerCase()));
|
|
189
|
+
const entityScore = queryEntityNames.length > 0 ? entityMatches.length / queryEntityNames.length : 0;
|
|
190
|
+
const decay = scoreMemoryDecay(memory, options);
|
|
191
|
+
const lifecycleScore = decay.state === 'archive_candidate' ? -0.15 : decay.state === 'sticky' ? 0.12 : 0;
|
|
192
|
+
const score = lexicalScore + phraseScore + (entityScore * 0.45) + lifecycleScore;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
score: Number(Math.max(0, score).toFixed(4)),
|
|
196
|
+
lexicalScore: Number(lexicalScore.toFixed(4)),
|
|
197
|
+
entityScore: Number(entityScore.toFixed(4)),
|
|
198
|
+
matchedEntities: entityMatches,
|
|
199
|
+
decayState: decay.state,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildMemoryLifecycleView(memory = {}, options = {}) {
|
|
204
|
+
const scope = inferMemoryScope(memory);
|
|
205
|
+
const entities = extractMemoryEntities(memory);
|
|
206
|
+
const decay = scoreMemoryDecay(memory, options);
|
|
207
|
+
const retrieval = scoreHybridMemoryMatch(options.query || '', memory, options);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
scope,
|
|
211
|
+
entities,
|
|
212
|
+
decay,
|
|
213
|
+
retrievalHints: {
|
|
214
|
+
hybridScore: retrieval.score,
|
|
215
|
+
lexicalScore: retrieval.lexicalScore,
|
|
216
|
+
entityScore: retrieval.entityScore,
|
|
217
|
+
matchedEntities: retrieval.matchedEntities,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
16
222
|
function buildMemoryLifecyclePolicy(input = {}) {
|
|
17
223
|
return {
|
|
18
224
|
generatedAt: normalizeText(input.generatedAt) || new Date().toISOString(),
|
|
@@ -91,6 +297,11 @@ function evaluateMemoryPromotion(memory = {}, policy = buildMemoryLifecyclePolic
|
|
|
91
297
|
|
|
92
298
|
module.exports = {
|
|
93
299
|
buildMemoryLifecyclePolicy,
|
|
300
|
+
buildMemoryLifecycleView,
|
|
94
301
|
evaluateMemoryPromotion,
|
|
302
|
+
extractMemoryEntities,
|
|
303
|
+
inferMemoryScope,
|
|
95
304
|
normalizeMemoryType,
|
|
305
|
+
scoreHybridMemoryMatch,
|
|
306
|
+
scoreMemoryDecay,
|
|
96
307
|
};
|
|
@@ -94,9 +94,26 @@ function detectRuntimeIsolation() {
|
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function
|
|
97
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
98
|
+
try {
|
|
99
|
+
let curr = path.resolve(startDir);
|
|
100
|
+
while (true) {
|
|
101
|
+
const indicators = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.mcp.json', '.git'];
|
|
102
|
+
if (indicators.some((f) => fs.existsSync(path.join(curr, f)))) {
|
|
103
|
+
return curr;
|
|
104
|
+
}
|
|
105
|
+
const parent = path.dirname(curr);
|
|
106
|
+
if (parent === curr) break;
|
|
107
|
+
curr = parent;
|
|
108
|
+
}
|
|
109
|
+
} catch (_) { /* fallback to startDir */ }
|
|
110
|
+
return startDir;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function collectBootstrapFiles(projectRoot) {
|
|
114
|
+
const effectiveRoot = projectRoot || findProjectRoot();
|
|
98
115
|
const files = BOOTSTRAP_FILES.map((file) => {
|
|
99
|
-
const absolutePath = path.join(
|
|
116
|
+
const absolutePath = path.join(effectiveRoot, file.path);
|
|
100
117
|
return {
|
|
101
118
|
id: file.id,
|
|
102
119
|
path: file.path,
|
|
@@ -118,7 +135,7 @@ function collectBootstrapFiles(projectRoot = PROJECT_ROOT) {
|
|
|
118
135
|
missingRequired,
|
|
119
136
|
recommendation: missingRequired.length === 0
|
|
120
137
|
? 'Bootstrap context is present.'
|
|
121
|
-
: `Add missing bootstrap files: ${missingRequired.join(', ')}`,
|
|
138
|
+
: `Add missing bootstrap files to project root (${effectiveRoot}): ${missingRequired.join(', ')}`,
|
|
122
139
|
};
|
|
123
140
|
}
|
|
124
141
|
|
|
@@ -331,6 +331,55 @@ function buildRewardReport(episodes = [], options = {}) {
|
|
|
331
331
|
deep: 'destructive, public, or production-adjacent work',
|
|
332
332
|
xhigh: 'payments, secrets, deploy-prod, data-loss, force-push-main',
|
|
333
333
|
},
|
|
334
|
+
continualAdapterPlan: buildContinualAdapterTrainingPlan(episodes, options),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function buildContinualAdapterTrainingPlan(episodes = [], options = {}) {
|
|
339
|
+
const maxAdapters = Math.max(1, Number(options.maxAdapters || 4));
|
|
340
|
+
const minExamples = Math.max(1, Number(options.minExamples || 2));
|
|
341
|
+
const pairs = buildPreferencePairs(episodes, { ...options, maxPairs: Number(options.maxPairs || 24) });
|
|
342
|
+
const candidates = rankGateCandidatesByReward(episodes, {
|
|
343
|
+
...options,
|
|
344
|
+
minOccurrences: minExamples,
|
|
345
|
+
maxGateCandidates: maxAdapters,
|
|
346
|
+
}).slice(0, maxAdapters);
|
|
347
|
+
|
|
348
|
+
const adapters = candidates.map((candidate, index) => ({
|
|
349
|
+
id: `lora_${candidate.gateId || `adapter_${index + 1}`}`.slice(0, 96),
|
|
350
|
+
source: candidate.key,
|
|
351
|
+
trainingMode: 'concurrent-lora',
|
|
352
|
+
examples: candidate.occurrences,
|
|
353
|
+
priorityScore: candidate.priorityScore,
|
|
354
|
+
targetBehavior: candidate.recommendation,
|
|
355
|
+
retentionChecks: [
|
|
356
|
+
'baseline gate pass-rate must not regress',
|
|
357
|
+
'previous adapter reward must stay within tolerance',
|
|
358
|
+
'new adapter must improve blocked-repeat or verification reward on held-out episodes',
|
|
359
|
+
],
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
const enoughData = pairs.length >= minExamples && adapters.length > 0;
|
|
363
|
+
return {
|
|
364
|
+
status: enoughData ? 'ready' : 'needs_more_feedback',
|
|
365
|
+
trainingStack: 'multi-lora-continual-learning',
|
|
366
|
+
baseModelPolicy: 'freeze base model; train small adapters from reward-ranked DPO pairs',
|
|
367
|
+
scheduling: adapters.length > 1 ? 'batch adapters concurrently when they share the same frozen base' : 'train serially until at least two adapter candidates exist',
|
|
368
|
+
adapters,
|
|
369
|
+
dpoPairsAvailable: pairs.length,
|
|
370
|
+
retentionGate: 'ship no adapter unless reward, regression, and prior-domain retention checks pass',
|
|
371
|
+
nextActions: enoughData
|
|
372
|
+
? [
|
|
373
|
+
'Export reward-ranked DPO pairs.',
|
|
374
|
+
'Train candidate LoRA adapters concurrently on the same frozen base.',
|
|
375
|
+
'Evaluate each adapter against held-out negative episodes and prior positive episodes.',
|
|
376
|
+
'Promote only the adapter that improves reward without retention regression.',
|
|
377
|
+
]
|
|
378
|
+
: [
|
|
379
|
+
'Capture more thumbs-up/down outcomes before training.',
|
|
380
|
+
'Promote deterministic gates first when examples are sparse.',
|
|
381
|
+
'Use local semantic recall until enough reward-ranked pairs exist for adapter training.',
|
|
382
|
+
],
|
|
334
383
|
};
|
|
335
384
|
}
|
|
336
385
|
|
|
@@ -419,8 +468,10 @@ if (isCliInvocation()) {
|
|
|
419
468
|
console.log(JSON.stringify(buildPreferencePairs(episodes), null, 2));
|
|
420
469
|
} else if (args.command === 'gates') {
|
|
421
470
|
console.log(JSON.stringify(rankGateCandidatesByReward(episodes), null, 2));
|
|
471
|
+
} else if (args.command === 'adapters') {
|
|
472
|
+
console.log(JSON.stringify(buildContinualAdapterTrainingPlan(episodes), null, 2));
|
|
422
473
|
} else {
|
|
423
|
-
console.error(`Unknown command: ${args.command}. Use: report, pairs, gates`);
|
|
474
|
+
console.error(`Unknown command: ${args.command}. Use: report, pairs, gates, adapters`);
|
|
424
475
|
process.exit(1);
|
|
425
476
|
}
|
|
426
477
|
}
|
|
@@ -428,6 +479,7 @@ if (isCliInvocation()) {
|
|
|
428
479
|
module.exports = {
|
|
429
480
|
HIGH_RISK_TAGS,
|
|
430
481
|
allocateTestTimeCompute,
|
|
482
|
+
buildContinualAdapterTrainingPlan,
|
|
431
483
|
buildPreferencePairFromEpisodes,
|
|
432
484
|
buildPreferencePairs,
|
|
433
485
|
buildRewardReport,
|
|
@@ -58,6 +58,47 @@ function readJSONL(filePath) {
|
|
|
58
58
|
}).filter(Boolean);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// --- Self-Harness stage 3: regression-gated promotion -----------------------
|
|
62
|
+
// Inspired by "Self-Harness: Harnesses That Improve Themselves" (arXiv 2606.09498).
|
|
63
|
+
// Stages 1-2 (weakness mining -> rule extraction) already exist via lesson
|
|
64
|
+
// inference + this promoter. Stage 3 — accept a harness change only after
|
|
65
|
+
// regression-testing it does not degrade behavior — was missing: a noisy 3x
|
|
66
|
+
// capture could hard-block an over-broad pattern with no check that it wouldn't
|
|
67
|
+
// have wrongly blocked actions that were previously ALLOWED. This replays a
|
|
68
|
+
// candidate BLOCK rule against the audit trail's prior `allow` decisions; if it
|
|
69
|
+
// would have blocked safe actions, the caller quarantines it to `warn` instead.
|
|
70
|
+
const REGRESSION_FALSE_BLOCK_LIMIT = 0; // any prior safe action it would block => quarantine
|
|
71
|
+
|
|
72
|
+
function getAuditTrailPath() {
|
|
73
|
+
return path.join(path.dirname(getFeedbackLogPath()), 'audit-trail.jsonl');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Returns { falseBlocks, allowSampleSize } or null when there is no history /
|
|
77
|
+
// matcher available — in which case the caller promotes as usual (fail-open to
|
|
78
|
+
// existing behavior, since regression gating is an enhancement, not a hard gate).
|
|
79
|
+
function regressionCheck(gate, options = {}) {
|
|
80
|
+
const auditPath = options.auditTrailPath || getAuditTrailPath();
|
|
81
|
+
const entries = readJSONL(auditPath);
|
|
82
|
+
if (!entries.length) return null;
|
|
83
|
+
// Lazy-require to avoid the gates-engine <-> auto-promote-gates require cycle.
|
|
84
|
+
let matchesGate;
|
|
85
|
+
try { ({ matchesGate } = require('./gates-engine')); } catch { return null; }
|
|
86
|
+
if (typeof matchesGate !== 'function') return null;
|
|
87
|
+
const allowed = entries.filter((e) => e && e.decision === 'allow' && e.toolName);
|
|
88
|
+
if (!allowed.length) return null;
|
|
89
|
+
let falseBlocks = 0;
|
|
90
|
+
for (const e of allowed) {
|
|
91
|
+
try {
|
|
92
|
+
if (matchesGate(gate, e.toolName, e.toolInput || {})) falseBlocks += 1;
|
|
93
|
+
} catch { /* a bad pattern/entry never counts as a false block */ }
|
|
94
|
+
}
|
|
95
|
+
return { falseBlocks, allowSampleSize: allowed.length };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function safeRegressionCheck(gate, options) {
|
|
99
|
+
try { return regressionCheck(gate, options); } catch { return null; }
|
|
100
|
+
}
|
|
101
|
+
|
|
61
102
|
function loadAutoGates() {
|
|
62
103
|
const autoGatesPath = getAutoGatesPath();
|
|
63
104
|
if (!fs.existsSync(autoGatesPath)) {
|
|
@@ -193,9 +234,9 @@ function patternToGateId(key) {
|
|
|
193
234
|
return 'auto-' + key.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').slice(0, 50).toLowerCase();
|
|
194
235
|
}
|
|
195
236
|
|
|
196
|
-
function buildGateRule(group) {
|
|
197
|
-
const action = group.count === 'MANUAL' ? group.manualAction || 'block' : (group.count >= BLOCK_THRESHOLD ? 'block' : 'warn');
|
|
198
|
-
const severity = action === 'block' ? 'critical' : 'medium';
|
|
237
|
+
function buildGateRule(group, actionOverride) {
|
|
238
|
+
const action = actionOverride || (group.count === 'MANUAL' ? group.manualAction || 'block' : (group.count >= BLOCK_THRESHOLD ? 'block' : 'warn'));
|
|
239
|
+
const severity = action === 'block' ? 'critical' : action === 'approve' ? 'high' : 'medium';
|
|
199
240
|
const context = group.latestContext.slice(0, 120);
|
|
200
241
|
const kind = group.key.startsWith('diagnosis:')
|
|
201
242
|
? 'repeated diagnosis'
|
|
@@ -332,7 +373,8 @@ function forcePromote(context, action = 'block') {
|
|
|
332
373
|
return { gateId, action, totalGates: data.gates.length };
|
|
333
374
|
}
|
|
334
375
|
|
|
335
|
-
function promote(feedbackLogPath) {
|
|
376
|
+
function promote(feedbackLogPath, options) {
|
|
377
|
+
const opts = options || {};
|
|
336
378
|
const logPath = feedbackLogPath || getFeedbackLogPath();
|
|
337
379
|
const entries = readJSONL(logPath);
|
|
338
380
|
const groups = groupNegativeFeedback(entries, WINDOW_DAYS);
|
|
@@ -357,17 +399,38 @@ function promote(feedbackLogPath) {
|
|
|
357
399
|
const existing = data.gates[existingIdx];
|
|
358
400
|
const newAction = group.count >= BLOCK_THRESHOLD ? 'block' : 'warn';
|
|
359
401
|
if (existing.action !== newAction && newAction === 'block') {
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
402
|
+
// Self-Harness stage 3: regression-test before upgrading warn -> block.
|
|
403
|
+
const regression = opts.skipRegression ? null : safeRegressionCheck(buildGateRule(group, 'block'), opts);
|
|
404
|
+
if (regression && regression.falseBlocks > REGRESSION_FALSE_BLOCK_LIMIT) {
|
|
405
|
+
// Would block prior safe actions — hold at warn instead of upgrading.
|
|
406
|
+
promotions.push({ type: 'upgrade-quarantined', gateId, from: existing.action, occurrences: group.count, falseBlocks: regression.falseBlocks });
|
|
407
|
+
} else {
|
|
408
|
+
// Upgrade from warn to block
|
|
409
|
+
data.gates[existingIdx] = { ...existing, action: 'block', severity: 'critical', occurrences: group.count, upgradedAt: new Date().toISOString() };
|
|
410
|
+
promotions.push({ type: 'upgrade', gateId, from: existing.action, to: 'block', occurrences: group.count });
|
|
411
|
+
}
|
|
363
412
|
}
|
|
364
413
|
// Update occurrence count even if no action change
|
|
365
414
|
data.gates[existingIdx].occurrences = group.count;
|
|
366
415
|
continue;
|
|
367
416
|
}
|
|
368
417
|
|
|
369
|
-
// New gate
|
|
370
|
-
const gate = buildGateRule(group);
|
|
418
|
+
// New gate — respect explicit gateAction override (e.g. 'approve' for human-approval rules)
|
|
419
|
+
const gate = buildGateRule(group, opts.gateAction);
|
|
420
|
+
|
|
421
|
+
// Self-Harness stage 3: before a feedback rule goes live as a hard block,
|
|
422
|
+
// regression-test it against prior allowed actions. If it would have blocked
|
|
423
|
+
// safe actions, quarantine it to `warn` instead of `block`.
|
|
424
|
+
let regression = null;
|
|
425
|
+
if (gate.action === 'block' && !opts.gateAction && !opts.skipRegression) {
|
|
426
|
+
regression = safeRegressionCheck(gate, opts);
|
|
427
|
+
if (regression && regression.falseBlocks > REGRESSION_FALSE_BLOCK_LIMIT) {
|
|
428
|
+
gate.action = 'warn';
|
|
429
|
+
gate.severity = 'medium';
|
|
430
|
+
gate.quarantined = true;
|
|
431
|
+
gate.regression = regression;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
371
434
|
|
|
372
435
|
// Enforce max limit — rotate oldest
|
|
373
436
|
if (data.gates.length >= MAX_AUTO_GATES) {
|
|
@@ -376,7 +439,13 @@ function promote(feedbackLogPath) {
|
|
|
376
439
|
}
|
|
377
440
|
|
|
378
441
|
data.gates.push(gate);
|
|
379
|
-
promotions.push({
|
|
442
|
+
promotions.push({
|
|
443
|
+
type: gate.quarantined ? 'new-quarantined' : 'new',
|
|
444
|
+
gateId: gate.id,
|
|
445
|
+
action: gate.action,
|
|
446
|
+
occurrences: group.count,
|
|
447
|
+
...(gate.quarantined ? { falseBlocks: regression.falseBlocks, allowSampleSize: regression.allowSampleSize } : {}),
|
|
448
|
+
});
|
|
380
449
|
}
|
|
381
450
|
|
|
382
451
|
// Log promotions
|
|
@@ -437,6 +506,9 @@ module.exports = {
|
|
|
437
506
|
groupNegativeFeedback,
|
|
438
507
|
patternToGateId,
|
|
439
508
|
buildGateRule,
|
|
509
|
+
regressionCheck,
|
|
510
|
+
getAuditTrailPath,
|
|
511
|
+
REGRESSION_FALSE_BLOCK_LIMIT,
|
|
440
512
|
extractPatternKey,
|
|
441
513
|
normalizeCommandSignature,
|
|
442
514
|
isNegative,
|
|
@@ -27,6 +27,7 @@ const {
|
|
|
27
27
|
statuslineCommand,
|
|
28
28
|
userPromptHookCommand,
|
|
29
29
|
} = require('./hook-runtime');
|
|
30
|
+
const { installShim } = require('./install-shim');
|
|
30
31
|
|
|
31
32
|
function getHome() {
|
|
32
33
|
return process.env.HOME || process.env.USERPROFILE || '';
|
|
@@ -338,6 +339,19 @@ function wireClaudeHooks(options) {
|
|
|
338
339
|
options.projectSettingsPath || claudeProjectSettingsPath(options.projectDir);
|
|
339
340
|
const dryRun = options.dryRun || false;
|
|
340
341
|
const projectDir = options.projectDir || process.cwd();
|
|
342
|
+
|
|
343
|
+
// --- Install stable shim before resolving hook commands ---
|
|
344
|
+
// The shim at ~/.thumbgate/bin/thumbgate-hook always resolves @latest,
|
|
345
|
+
// so hooks never go stale across version bumps (Volta-style pattern).
|
|
346
|
+
// Skip in source-checkout mode — developers use direct node commands.
|
|
347
|
+
if (!dryRun && !require('./mcp-config').isSourceCheckout(path.join(__dirname, '..'))) {
|
|
348
|
+
try {
|
|
349
|
+
installShim();
|
|
350
|
+
} catch {
|
|
351
|
+
// Non-fatal: fall back to version-pinned commands
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
341
355
|
const desiredStatusLine = statuslineCommand();
|
|
342
356
|
|
|
343
357
|
// --- Step 0: clean up stale hooks from BOTH settings locations ---
|