thumbgate 1.27.12 → 1.27.13
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/plugin.json +1 -1
- package/.well-known/llms.txt +2 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +2 -4
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bin/cli.js +78 -259
- package/config/gate-templates.json +0 -228
- package/config/gates/claim-verification.json +0 -18
- package/package.json +35 -25
- package/public/assets/brand/thumbgate-logo-transparent.svg +22 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +19 -0
- package/public/assets/brand/thumbgate-mark.svg +11 -5
- package/public/blog.html +0 -30
- package/public/brand/thumbgate-mark.svg +9 -5
- package/public/chatgpt-app.html +2 -2
- package/public/compare.html +2 -1
- package/public/dashboard.html +1 -1
- package/public/federal.html +1 -1
- package/public/index.html +95 -216
- package/public/learn.html +59 -35
- package/public/lessons.html +1 -1
- package/public/numbers.html +2 -2
- package/public/pro.html +7 -7
- package/scripts/aws-blocks-guardrails.js +228 -0
- package/scripts/cli-schema.js +22 -10
- package/scripts/dashboard-chat.js +2 -1
- package/scripts/document-intake.js +1 -49
- package/scripts/durability/step.js +3 -3
- package/scripts/gate-stats.js +5 -11
- package/scripts/gates-engine.js +0 -49
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-stop-anti-claim.js +116 -184
- package/scripts/hosted-config.js +0 -12
- package/scripts/lesson-search.js +1 -15
- package/scripts/llm-client.js +187 -5
- package/scripts/plausible-domain-config.js +3 -1
- package/scripts/seo-gsd.js +240 -1
- package/scripts/tool-registry.js +2 -2
- package/scripts/vector-store.js +44 -0
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +340 -131
- package/public/assets/brand/thumbgate-mark-inline.svg +0 -15
- package/public/compare/adopt-ai.html +0 -219
- package/public/compare/agentix-labs.html +0 -197
- package/public/compare/ai-experience-orchestration.html +0 -216
- package/public/compare/anthropic-claude-for-legal.html +0 -260
- package/public/compare/anthropic-containment.html +0 -280
- package/public/compare/arcade.html +0 -175
- package/public/compare/arcjet.html +0 -239
- package/public/compare/bumblebee.html +0 -307
- package/public/compare/claude-code-hooks.html +0 -294
- package/public/compare/databricks-unity-ai-gateway.html +0 -215
- package/public/compare/fallow.html +0 -351
- package/public/compare/heidi.html +0 -233
- package/public/compare/mem0.html +0 -342
- package/public/compare/oak-and-sparrow-gatekeeper.html +0 -289
- package/public/compare/rein.html +0 -236
- package/public/compare/sigmashake.html +0 -256
- package/public/compare/speclock.html +0 -342
- package/public/guides/agent-harness-optimization.html +0 -342
- package/public/guides/agentic-web-governance.html +0 -406
- package/public/guides/ai-agent-governance-sprint.html +0 -415
- package/public/guides/ai-agent-pre-action-approval-gates.html +0 -401
- package/public/guides/ai-agent-workflow-migration-checklist.html +0 -392
- package/public/guides/ai-deployment-readiness.html +0 -415
- package/public/guides/ai-mode-ads-agent-governance.html +0 -401
- package/public/guides/ai-search-topical-presence.html +0 -342
- package/public/guides/autoresearch-agent-safety.html +0 -342
- package/public/guides/background-agent-governance.html +0 -358
- package/public/guides/best-tools-stop-ai-agents-breaking-production.html +0 -363
- package/public/guides/browser-automation-safety.html +0 -342
- package/public/guides/chatgpt-ads-trust.html +0 -353
- package/public/guides/claude-code-feedback.html +0 -339
- package/public/guides/claude-code-prevent-repeated-mistakes.html +0 -161
- package/public/guides/claude-code-skills-guardrails.html +0 -343
- package/public/guides/claude-desktop.html +0 -356
- package/public/guides/code-knowledge-graph-guardrails.html +0 -365
- package/public/guides/codex-cli-guardrails.html +0 -339
- package/public/guides/cursor-agent-guardrails.html +0 -339
- package/public/guides/cursor-prevent-repeated-mistakes.html +0 -161
- package/public/guides/database-agent-safety.html +0 -406
- package/public/guides/deepseek-v4-runtime-guardrails.html +0 -346
- package/public/guides/developer-machine-supply-chain-guardrails.html +0 -358
- package/public/guides/gcp-mcp-guardrails.html +0 -147
- package/public/guides/gemini-cli-feedback-memory.html +0 -339
- package/public/guides/gpt-5-5-model-evaluation.html +0 -358
- package/public/guides/internal-ai-engineering-stack-guardrails.html +0 -348
- package/public/guides/long-running-agent-context-management.html +0 -346
- package/public/guides/mcp-tool-governance.html +0 -401
- package/public/guides/multica-thumbgate-setup.html +0 -134
- package/public/guides/native-messaging-host-security.html +0 -342
- package/public/guides/policy-engine-pre-action-gates.html +0 -346
- package/public/guides/pre-action-checks.html +0 -342
- package/public/guides/pretooluse-hooks-vs-advisory-prompt-rules.html +0 -342
- package/public/guides/prompt-tricks-to-workflow-rules.html +0 -365
- package/public/guides/proxy-pointer-rag-guardrails.html +0 -352
- package/public/guides/rag-precision-tuning-guardrails.html +0 -352
- package/public/guides/reasoning-compression-guardrails.html +0 -346
- package/public/guides/relational-knowledge-ai-recommendations.html +0 -342
- package/public/guides/roo-code-alternative-cline.html +0 -339
- package/public/guides/semantic-programmatic-seo-guardrails.html +0 -352
- package/public/guides/seo-agent-skills-guardrails.html +0 -344
- package/public/guides/stop-repeated-ai-agent-mistakes.html +0 -342
- package/public/learn/ac-dc-runtime-enforcement.html +0 -277
- package/public/learn/agent-harness-pattern.html +0 -181
- package/public/learn/agent-identity-connector-governance.html +0 -146
- package/public/learn/agent-swarms-shared-gates.html +0 -173
- package/public/learn/agentic-enterprise-context-brain.html +0 -117
- package/public/learn/agentic-os-team-governance.html +0 -146
- package/public/learn/ai-agent-governance.html +0 -158
- package/public/learn/ai-agent-persistent-memory.html +0 -211
- package/public/learn/anthropomorphic-claim-gates.html +0 -180
- package/public/learn/background-agent-control-layer.html +0 -184
- package/public/learn/claude-code-goal-with-rubrics.html +0 -205
- package/public/learn/codex-role-plugins-need-governance.html +0 -125
- package/public/learn/cost-aware-agent-gate-routing.html +0 -173
- package/public/learn/databricks-unity-ai-gateway-runtime-governance.html +0 -157
- package/public/learn/deterministic-agent-workflows.html +0 -185
- package/public/learn/feedback-loop-vs-decision-layer.html +0 -283
- package/public/learn/from-prototype-to-production.html +0 -223
- package/public/learn/learn.css +0 -51
- package/public/learn/mcp-pre-action-checks-explained.html +0 -172
- package/public/learn/pretix-stripe-connect-marketplaces.html +0 -161
- package/public/learn/regulated-agent-execution-boundary.html +0 -196
- package/public/learn/spec-driven-development.html +0 -168
- package/public/learn/stop-ai-agent-force-push.html +0 -134
- package/public/learn/vibe-coding-safety-net.html +0 -142
- package/scripts/reddit-browser-notification-watch.js +0 -230
package/scripts/gate-stats.js
CHANGED
|
@@ -11,11 +11,6 @@ const PROJECT_ROOT = path.join(__dirname, '..');
|
|
|
11
11
|
const MANUAL_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
|
|
12
12
|
const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
|
|
13
13
|
|
|
14
|
-
function safeOccurrenceCount(value) {
|
|
15
|
-
const n = Number(value);
|
|
16
|
-
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
14
|
function loadGatesFile(filePath) {
|
|
20
15
|
if (!fs.existsSync(filePath)) return [];
|
|
21
16
|
try {
|
|
@@ -44,16 +39,16 @@ function calculateStats() {
|
|
|
44
39
|
// Count total blocks/warns from occurrences in auto-promoted gates
|
|
45
40
|
const totalBlocked = autoGates
|
|
46
41
|
.filter((g) => g.action === 'block')
|
|
47
|
-
.reduce((sum, g) => sum +
|
|
42
|
+
.reduce((sum, g) => sum + (g.occurrences || 0), 0);
|
|
48
43
|
const totalWarned = autoGates
|
|
49
44
|
.filter((g) => g.action === 'warn')
|
|
50
|
-
.reduce((sum, g) => sum +
|
|
45
|
+
.reduce((sum, g) => sum + (g.occurrences || 0), 0);
|
|
51
46
|
|
|
52
47
|
// Top blocked gate. A configured block rule with zero occurrences is not a
|
|
53
48
|
// "top blocker"; only recorded block events should appear here.
|
|
54
49
|
const topBlocked = [...allGates]
|
|
55
|
-
.filter((g) => g.action === 'block' &&
|
|
56
|
-
.sort((a, b) =>
|
|
50
|
+
.filter((g) => g.action === 'block' && Number(g.occurrences || 0) > 0)
|
|
51
|
+
.sort((a, b) => (b.occurrences || 0) - (a.occurrences || 0))
|
|
57
52
|
.at(0) || null;
|
|
58
53
|
|
|
59
54
|
// Last promotion event
|
|
@@ -110,7 +105,7 @@ function computeCalibration(gates) {
|
|
|
110
105
|
const calibration = [];
|
|
111
106
|
for (const gate of gates || []) {
|
|
112
107
|
if (!gate || !gate.id) continue;
|
|
113
|
-
const occurrences =
|
|
108
|
+
const occurrences = Number(gate.occurrences || 0);
|
|
114
109
|
const action = gate.action || 'unknown';
|
|
115
110
|
// Only annotate gates with recorded occurrence data
|
|
116
111
|
if (occurrences === 0) continue;
|
|
@@ -263,7 +258,6 @@ module.exports = {
|
|
|
263
258
|
loadGatesFile,
|
|
264
259
|
tryComputeBayesErrorRate,
|
|
265
260
|
computeCalibration,
|
|
266
|
-
safeOccurrenceCount,
|
|
267
261
|
MANUAL_GATES_PATH,
|
|
268
262
|
STATS_PATH,
|
|
269
263
|
};
|
package/scripts/gates-engine.js
CHANGED
|
@@ -16,13 +16,6 @@ const {
|
|
|
16
16
|
const {
|
|
17
17
|
evaluateWorkflowSentinel,
|
|
18
18
|
} = require('./workflow-sentinel');
|
|
19
|
-
const {
|
|
20
|
-
extractPayloadText,
|
|
21
|
-
extractPayloadPreviousUserText,
|
|
22
|
-
hasPositiveFeedback,
|
|
23
|
-
isLowValueCloseout,
|
|
24
|
-
buildResponseQualityReason,
|
|
25
|
-
} = require('./hook-stop-anti-claim');
|
|
26
19
|
const {
|
|
27
20
|
recordDecisionEvaluation,
|
|
28
21
|
recordDecisionOutcome,
|
|
@@ -2592,39 +2585,6 @@ function buildReminderOutput(context) {
|
|
|
2592
2585
|
});
|
|
2593
2586
|
}
|
|
2594
2587
|
|
|
2595
|
-
function inferHookEventName(input = {}) {
|
|
2596
|
-
const explicit = input.hook_event_name || input.hookEventName || input.event || input.lifecycle;
|
|
2597
|
-
if (explicit) return String(explicit);
|
|
2598
|
-
return extractPayloadText(input) ? 'Stop' : 'PreToolUse';
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
function buildResponseQualityBlockOutput(reason, input = {}) {
|
|
2602
|
-
return JSON.stringify({
|
|
2603
|
-
decision: 'block',
|
|
2604
|
-
reason,
|
|
2605
|
-
hookSpecificOutput: {
|
|
2606
|
-
hookEventName: inferHookEventName(input),
|
|
2607
|
-
permissionDecision: 'deny',
|
|
2608
|
-
permissionDecisionReason: reason,
|
|
2609
|
-
},
|
|
2610
|
-
});
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
function evaluateFinalResponseQualityGate(input = {}) {
|
|
2614
|
-
const finalText = extractPayloadText(input) || process.env.CLAUDE_RESPONSE || '';
|
|
2615
|
-
const previousUserText = extractPayloadPreviousUserText(input)
|
|
2616
|
-
|| process.env.CLAUDE_PREVIOUS_USER_TEXT
|
|
2617
|
-
|| process.env.CLAUDE_PREVIOUS_USER
|
|
2618
|
-
|| '';
|
|
2619
|
-
|
|
2620
|
-
if (!finalText || !hasPositiveFeedback(previousUserText)) return null;
|
|
2621
|
-
if (!isLowValueCloseout(finalText, '')) return null;
|
|
2622
|
-
recordStat('response-quality-shallow-closeout', 'block', null, {
|
|
2623
|
-
hookEventName: inferHookEventName(input),
|
|
2624
|
-
});
|
|
2625
|
-
return buildResponseQualityBlockOutput(buildResponseQualityReason(), input);
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
2588
|
// ---------------------------------------------------------------------------
|
|
2629
2589
|
// Upgrade nudge: surfaces Pro value at usage milestones and trial expiry.
|
|
2630
2590
|
// Block-action Pro CTA: brief upgrade mention after a deny/warn decision.
|
|
@@ -2955,9 +2915,6 @@ function mergeContextStrings(...ctxs) {
|
|
|
2955
2915
|
}
|
|
2956
2916
|
|
|
2957
2917
|
async function runAsync(input) {
|
|
2958
|
-
const responseQualityGate = evaluateFinalResponseQualityGate(input);
|
|
2959
|
-
if (responseQualityGate) return responseQualityGate;
|
|
2960
|
-
|
|
2961
2918
|
const secretGuard = evaluateSecretGuard(input);
|
|
2962
2919
|
if (secretGuard) {
|
|
2963
2920
|
return formatOutput(secretGuard);
|
|
@@ -3005,9 +2962,6 @@ async function runAsync(input) {
|
|
|
3005
2962
|
}
|
|
3006
2963
|
|
|
3007
2964
|
function run(input) {
|
|
3008
|
-
const responseQualityGate = evaluateFinalResponseQualityGate(input);
|
|
3009
|
-
if (responseQualityGate) return responseQualityGate;
|
|
3010
|
-
|
|
3011
2965
|
const secretGuard = evaluateSecretGuard(input);
|
|
3012
2966
|
if (secretGuard) {
|
|
3013
2967
|
return formatOutput(secretGuard);
|
|
@@ -3342,9 +3296,6 @@ module.exports = {
|
|
|
3342
3296
|
evaluateGatesAsync,
|
|
3343
3297
|
computeExecutableHash,
|
|
3344
3298
|
formatOutput,
|
|
3345
|
-
inferHookEventName,
|
|
3346
|
-
buildResponseQualityBlockOutput,
|
|
3347
|
-
evaluateFinalResponseQualityGate,
|
|
3348
3299
|
isApprovalGatesEnabled,
|
|
3349
3300
|
run,
|
|
3350
3301
|
runAsync,
|
|
@@ -122,7 +122,7 @@ function resolveGeminiEmbeddingConfig(env = process.env) {
|
|
|
122
122
|
|
|
123
123
|
return {
|
|
124
124
|
enabled,
|
|
125
|
-
provider: enabled ? 'gemini' : 'local',
|
|
125
|
+
provider: provider === 'coreai' ? 'coreai' : (enabled ? 'gemini' : 'local'),
|
|
126
126
|
model: String(env.THUMBGATE_GEMINI_EMBED_MODEL || GEMINI_EMBEDDING_2_MODEL).trim() || GEMINI_EMBEDDING_2_MODEL,
|
|
127
127
|
apiKey,
|
|
128
128
|
apiBaseUrl: trimTrailingSlashes(env.THUMBGATE_GEMINI_API_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta'),
|
|
@@ -171,6 +171,7 @@ function buildGeminiEmbeddingRolloutPlan(args = {}) {
|
|
|
171
171
|
},
|
|
172
172
|
rolloutSteps: [
|
|
173
173
|
'Keep local embeddings as the default offline path.',
|
|
174
|
+
'For Apple Silicon developers, route local queries through Core AI (AOT compiled models) to bypass CPU overhead.',
|
|
174
175
|
'Enable Gemini Embedding 2 only when a Gemini API key is present.',
|
|
175
176
|
'Use task-specific query/document prefixes at index and retrieval time.',
|
|
176
177
|
'Start at 768 dimensions, then benchmark 1536 only if recall misses show up.',
|
|
@@ -16,11 +16,10 @@
|
|
|
16
16
|
* ThumbGate dogfood — we are the prevention-rule generator and a perfect
|
|
17
17
|
* customer for our own gate.
|
|
18
18
|
*
|
|
19
|
-
* Wires through .claude/settings.json Stop hooks list. Always exits 0
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* written the assistant response.
|
|
19
|
+
* Wires through .claude/settings.json Stop hooks list. Always exits 0
|
|
20
|
+
* (informational): the goal is to surface a system reminder in the next
|
|
21
|
+
* turn so the agent corrects mid-conversation rather than to hard-block
|
|
22
|
+
* the turn that already happened.
|
|
24
23
|
*
|
|
25
24
|
* Stdin: Claude Code passes the hook payload as JSON on stdin. We read
|
|
26
25
|
* `transcript_path` to locate the JSONL session log and scan the last
|
|
@@ -44,8 +43,20 @@ const CLAIM_PATTERNS = [
|
|
|
44
43
|
/\beverything\s+(?:is\s+)?(?:done|working|ready)\b/i,
|
|
45
44
|
/\b(?:github|repo|repository)\s+(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b/i,
|
|
46
45
|
/\b(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b.*\b(?:github|repo|repository)\b/i,
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
// Added 2026-06-11 after a cross-project failure analysis: these completion
|
|
47
|
+
// claims ("all green / stable / verified / race over / tests pass") were
|
|
48
|
+
// asserted without proof and slipped past the original set. The proof-gate
|
|
49
|
+
// below suppresses them whenever the SAME turn ran a verification tool, so a
|
|
50
|
+
// "verified" claim backed by a test/curl/Read stays silent.
|
|
51
|
+
/\b(?:all\s+)?(?:tests?|checks?|ci)\s+(?:are\s+)?(?:now\s+)?passing\b/i,
|
|
52
|
+
/\ball\s+(?:tests?|checks?)\s+pass(?:ed)?\b/i,
|
|
53
|
+
/\bverified\b/i,
|
|
54
|
+
/\bconfirmed\b/i,
|
|
55
|
+
/\b(?:is|are|it'?s|now)\s+stable\b/i,
|
|
56
|
+
/\ball\s+clear\b/i,
|
|
57
|
+
/\bgood\s+to\s+go\b/i,
|
|
58
|
+
/\brace\s+(?:is\s+)?over\b/i,
|
|
59
|
+
/\bno\s+longer\s+racing\b/i,
|
|
49
60
|
];
|
|
50
61
|
|
|
51
62
|
// Proof-of-verification patterns. If the SAME turn included one of these
|
|
@@ -69,63 +80,97 @@ const PROOF_PATTERNS = [
|
|
|
69
80
|
/\bshopify\b/,
|
|
70
81
|
/\bsquare\b/,
|
|
71
82
|
/\bquickbooks\b/,
|
|
72
|
-
/\bgh\s+api\b/,
|
|
73
|
-
/\bls\b/,
|
|
74
|
-
/\bcat\b/,
|
|
75
83
|
/Read\s*\(/, // Claude Code Read tool call
|
|
76
84
|
/Bash\s*\(/, // Claude Code Bash tool call
|
|
77
85
|
];
|
|
78
86
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
const COMMERCIAL_CLAIM_SUBJECTS = [
|
|
88
|
+
'money',
|
|
89
|
+
'payment',
|
|
90
|
+
'charge',
|
|
91
|
+
'checkout',
|
|
92
|
+
'revenue',
|
|
93
|
+
'price',
|
|
94
|
+
'pricing',
|
|
95
|
+
'invoice',
|
|
96
|
+
'billing',
|
|
97
|
+
'tax',
|
|
98
|
+
'sales tax',
|
|
99
|
+
'inventory',
|
|
100
|
+
'stock',
|
|
101
|
+
'permission',
|
|
102
|
+
'access',
|
|
103
|
+
'customer facing',
|
|
87
104
|
];
|
|
88
105
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
const COMMERCIAL_CLAIM_STATES = [
|
|
107
|
+
'correct',
|
|
108
|
+
'accurate',
|
|
109
|
+
'verified',
|
|
110
|
+
'valid',
|
|
111
|
+
'matches',
|
|
112
|
+
'working',
|
|
113
|
+
'fixed',
|
|
114
|
+
'resolved',
|
|
115
|
+
'calculated',
|
|
116
|
+
'configured',
|
|
93
117
|
];
|
|
94
118
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
const GREEN_CLAIM_PHRASES = [
|
|
120
|
+
'all green',
|
|
121
|
+
'all tests green',
|
|
122
|
+
'all checks green',
|
|
123
|
+
'all the tests green',
|
|
124
|
+
'all the checks green',
|
|
125
|
+
'all tests are green',
|
|
126
|
+
'all checks are green',
|
|
101
127
|
];
|
|
102
128
|
|
|
103
|
-
function
|
|
104
|
-
|
|
129
|
+
function normalizeClaimText(text) {
|
|
130
|
+
return String(text || '')
|
|
131
|
+
.toLowerCase()
|
|
132
|
+
.replace(/[-_]+/g, ' ')
|
|
133
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
134
|
+
.replace(/\s+/g, ' ')
|
|
135
|
+
.trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function containsPhrase(normalizedText, phrase) {
|
|
139
|
+
return ` ${normalizedText} `.includes(` ${phrase} `);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function findTokenListClaim(text, subjects, states, label) {
|
|
143
|
+
const normalized = normalizeClaimText(text);
|
|
144
|
+
if (!normalized) return null;
|
|
145
|
+
const subject = subjects.find((candidate) => containsPhrase(normalized, candidate));
|
|
146
|
+
const state = states.find((candidate) => containsPhrase(normalized, candidate));
|
|
147
|
+
return subject && state ? `${label}: ${subject} ${state}` : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function findGreenClaim(text) {
|
|
151
|
+
const normalized = normalizeClaimText(text);
|
|
152
|
+
return GREEN_CLAIM_PHRASES.find((phrase) => containsPhrase(normalized, phrase)) || null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readLastAssistantTurn(transcriptPath) {
|
|
156
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) return null;
|
|
105
157
|
let content;
|
|
106
158
|
try {
|
|
107
159
|
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
108
160
|
} catch {
|
|
109
|
-
return
|
|
161
|
+
return null;
|
|
110
162
|
}
|
|
111
|
-
|
|
112
|
-
.trim()
|
|
113
|
-
.split('\n')
|
|
114
|
-
.map((raw) => {
|
|
115
|
-
try {
|
|
116
|
-
return JSON.parse(raw);
|
|
117
|
-
} catch {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
.filter(Boolean);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function readLastAssistantTurn(transcriptPath) {
|
|
125
|
-
const entries = readTranscriptEntries(transcriptPath);
|
|
163
|
+
const lines = content.trim().split('\n');
|
|
126
164
|
// Walk backwards to find the last assistant message
|
|
127
|
-
for (let i =
|
|
128
|
-
const
|
|
165
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
166
|
+
const raw = lines[i].trim();
|
|
167
|
+
if (!raw) continue;
|
|
168
|
+
let entry;
|
|
169
|
+
try {
|
|
170
|
+
entry = JSON.parse(raw);
|
|
171
|
+
} catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
129
174
|
if (entry.type === 'assistant' && entry.message) {
|
|
130
175
|
return entry.message;
|
|
131
176
|
}
|
|
@@ -133,22 +178,6 @@ function readLastAssistantTurn(transcriptPath) {
|
|
|
133
178
|
return null;
|
|
134
179
|
}
|
|
135
180
|
|
|
136
|
-
function readPreviousUserText(transcriptPath) {
|
|
137
|
-
const entries = readTranscriptEntries(transcriptPath);
|
|
138
|
-
let seenAssistant = false;
|
|
139
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
140
|
-
const entry = entries[i];
|
|
141
|
-
if (!seenAssistant && entry.type === 'assistant') {
|
|
142
|
-
seenAssistant = true;
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if (seenAssistant && entry.type === 'user' && entry.message) {
|
|
146
|
-
return extractText(entry.message);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return '';
|
|
150
|
-
}
|
|
151
|
-
|
|
152
181
|
function extractText(message) {
|
|
153
182
|
if (!message || !Array.isArray(message.content)) return '';
|
|
154
183
|
return message.content
|
|
@@ -175,24 +204,16 @@ function extractToolUseSummary(message) {
|
|
|
175
204
|
.join('\n');
|
|
176
205
|
}
|
|
177
206
|
|
|
178
|
-
function wordCount(text) {
|
|
179
|
-
return String(text || '').trim().split(/\s+/).filter(Boolean).length;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function hasPositiveFeedback(text) {
|
|
183
|
-
return POSITIVE_FEEDBACK_PATTERNS.some((p) => p.test(text || ''));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function isLowValueCloseout(text, toolUseSummary = '') {
|
|
187
|
-
const normalized = String(text || '').trim();
|
|
188
|
-
if (!normalized) return false;
|
|
189
|
-
if (toolUseSummary.trim()) return false;
|
|
190
|
-
if (wordCount(normalized) > 45) return false;
|
|
191
|
-
if (SUBSTANTIVE_CLOSEOUT_PATTERNS.some((p) => p.test(normalized))) return false;
|
|
192
|
-
return LOW_VALUE_CLOSEOUT_PATTERNS.some((p) => p.test(normalized));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
207
|
function findClaim(text) {
|
|
208
|
+
const commercialClaim = findTokenListClaim(
|
|
209
|
+
text,
|
|
210
|
+
COMMERCIAL_CLAIM_SUBJECTS,
|
|
211
|
+
COMMERCIAL_CLAIM_STATES,
|
|
212
|
+
'commercial truth',
|
|
213
|
+
);
|
|
214
|
+
if (commercialClaim) return commercialClaim;
|
|
215
|
+
const greenClaim = findGreenClaim(text);
|
|
216
|
+
if (greenClaim) return greenClaim;
|
|
196
217
|
for (const p of CLAIM_PATTERNS) {
|
|
197
218
|
const m = text.match(p);
|
|
198
219
|
if (m) return m[0];
|
|
@@ -204,79 +225,6 @@ function hasProof(combined) {
|
|
|
204
225
|
return PROOF_PATTERNS.some((p) => p.test(combined));
|
|
205
226
|
}
|
|
206
227
|
|
|
207
|
-
function extractPayloadText(payload) {
|
|
208
|
-
if (!payload || typeof payload !== 'object') return '';
|
|
209
|
-
const candidates = [
|
|
210
|
-
payload.response,
|
|
211
|
-
payload.assistant_response,
|
|
212
|
-
payload.assistantResponse,
|
|
213
|
-
payload.final_response,
|
|
214
|
-
payload.finalResponse,
|
|
215
|
-
payload.text,
|
|
216
|
-
payload.output,
|
|
217
|
-
payload.message,
|
|
218
|
-
];
|
|
219
|
-
for (const candidate of candidates) {
|
|
220
|
-
if (typeof candidate === 'string' && candidate.trim()) return candidate;
|
|
221
|
-
if (candidate && typeof candidate === 'object') {
|
|
222
|
-
const extracted = extractText(candidate);
|
|
223
|
-
if (extracted.trim()) return extracted;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return '';
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function extractPayloadPreviousUserText(payload) {
|
|
230
|
-
if (!payload || typeof payload !== 'object') return '';
|
|
231
|
-
const candidates = [
|
|
232
|
-
payload.previous_user_text,
|
|
233
|
-
payload.previousUserText,
|
|
234
|
-
payload.user_prompt,
|
|
235
|
-
payload.userPrompt,
|
|
236
|
-
payload.prompt,
|
|
237
|
-
];
|
|
238
|
-
for (const candidate of candidates) {
|
|
239
|
-
if (typeof candidate === 'string' && candidate.trim()) return candidate;
|
|
240
|
-
if (candidate && typeof candidate === 'object') {
|
|
241
|
-
const extracted = extractText(candidate);
|
|
242
|
-
if (extracted.trim()) return extracted;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return '';
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function buildResponseQualityReason() {
|
|
249
|
-
return [
|
|
250
|
-
'ThumbGate response-quality gate: final response answered positive feedback',
|
|
251
|
-
'with a low-value social closeout instead of silence-level brevity or an evidence checkpoint.',
|
|
252
|
-
'Positive feedback after operational work should trigger either no extra noise,',
|
|
253
|
-
'a compact evidence checkpoint, or a concrete next-state update.',
|
|
254
|
-
'Do not reply with generic "Good / Great / Use X/Y" filler.',
|
|
255
|
-
].join(' ');
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function buildResponseQualityReminder() {
|
|
259
|
-
return [
|
|
260
|
-
'⚠️ ThumbGate response-quality gate: previous turn answered positive feedback',
|
|
261
|
-
' with a low-value social closeout instead of silence-level brevity or an evidence checkpoint.',
|
|
262
|
-
' Positive feedback after operational work should trigger either no extra noise,',
|
|
263
|
-
' a compact evidence checkpoint, or a concrete next-state update.',
|
|
264
|
-
' Do not reply with generic "Good / Great / Use X/Y" filler.',
|
|
265
|
-
].join('\n');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function writeBlock(reason) {
|
|
269
|
-
process.stdout.write(JSON.stringify({
|
|
270
|
-
decision: 'block',
|
|
271
|
-
reason,
|
|
272
|
-
hookSpecificOutput: {
|
|
273
|
-
hookEventName: 'Stop',
|
|
274
|
-
permissionDecision: 'deny',
|
|
275
|
-
permissionDecisionReason: reason,
|
|
276
|
-
},
|
|
277
|
-
}) + '\n');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
228
|
function readStdinSync() {
|
|
281
229
|
try {
|
|
282
230
|
return fs.readFileSync(0, 'utf8');
|
|
@@ -295,42 +243,34 @@ function main() {
|
|
|
295
243
|
}
|
|
296
244
|
|
|
297
245
|
const transcriptPath = payload.transcript_path || process.env.CLAUDE_TRANSCRIPT_PATH;
|
|
298
|
-
const directText = extractPayloadText(payload) || process.env.CLAUDE_RESPONSE || '';
|
|
299
|
-
const directPreviousUserText = extractPayloadPreviousUserText(payload)
|
|
300
|
-
|| process.env.CLAUDE_PREVIOUS_USER_TEXT
|
|
301
|
-
|| process.env.CLAUDE_PREVIOUS_USER
|
|
302
|
-
|| '';
|
|
303
|
-
|
|
304
|
-
if (directText && hasPositiveFeedback(directPreviousUserText) && isLowValueCloseout(directText, '')) {
|
|
305
|
-
writeBlock(buildResponseQualityReason());
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
246
|
const message = readLastAssistantTurn(transcriptPath);
|
|
310
247
|
if (!message) return; // no transcript visible; nothing to check
|
|
311
248
|
|
|
312
249
|
const text = extractText(message);
|
|
313
250
|
const toolUseSummary = extractToolUseSummary(message);
|
|
314
|
-
const previousUserText = readPreviousUserText(transcriptPath);
|
|
315
|
-
|
|
316
|
-
if (hasPositiveFeedback(previousUserText) && isLowValueCloseout(text, toolUseSummary)) {
|
|
317
|
-
process.stdout.write(buildResponseQualityReminder() + '\n');
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
251
|
const claim = findClaim(text);
|
|
322
252
|
if (!claim) return; // no completion claim made; silent
|
|
323
253
|
|
|
324
254
|
const proofText = `${text}\n${toolUseSummary}`;
|
|
325
255
|
if (hasProof(proofText)) return; // claim backed by proof in same turn
|
|
326
256
|
|
|
327
|
-
//
|
|
257
|
+
// Strict mode (THUMBGATE_STRICT_ENFORCEMENT=1): hard-block the stop. Emit a
|
|
258
|
+
// Stop-hook block decision so Claude Code does NOT end the turn — the agent
|
|
259
|
+
// must run the verification (or retract) before it can stop. Default mode
|
|
260
|
+
// stays soft (a reminder for the next turn) so we don't break existing wiring.
|
|
261
|
+
if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') {
|
|
262
|
+
const reason = `ThumbGate anti-claim gate (strict): you claimed completion ("${claim}") without a proof tool call in the same message. Run the verification (curl / grep / test / Read) and restate with the proof, or retract — do not end the turn on an unverified claim.`;
|
|
263
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason }) + '\n');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Default (soft): surface a system reminder for the NEXT turn. Do not hard-block.
|
|
328
268
|
const reminder = [
|
|
329
269
|
'⚠️ ThumbGate anti-claim gate: previous turn claimed completion',
|
|
330
270
|
` ("${claim}") without a proof tool call in the same message.`,
|
|
331
|
-
' Per CLAUDE.md anti-lying: never claim "done / live / deployed / fixed
|
|
332
|
-
' or commercial truth (money / tax / inventory /
|
|
333
|
-
' without curl / grep / test / source-of-truth output in the SAME turn.',
|
|
271
|
+
' Per CLAUDE.md anti-lying: never claim "done / live / deployed / fixed /',
|
|
272
|
+
' verified / all green / stable" or commercial truth (money / tax / inventory /',
|
|
273
|
+
' permissions / customer-facing state) without curl / grep / test / source-of-truth output in the SAME turn.',
|
|
334
274
|
' If the work really is verified, re-state the claim with the proof.',
|
|
335
275
|
' If not, retract and run the verification before re-asserting.',
|
|
336
276
|
].join('\n');
|
|
@@ -354,16 +294,8 @@ if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
|
|
|
354
294
|
module.exports = {
|
|
355
295
|
CLAIM_PATTERNS,
|
|
356
296
|
PROOF_PATTERNS,
|
|
357
|
-
POSITIVE_FEEDBACK_PATTERNS,
|
|
358
|
-
LOW_VALUE_CLOSEOUT_PATTERNS,
|
|
359
297
|
findClaim,
|
|
360
298
|
hasProof,
|
|
361
|
-
hasPositiveFeedback,
|
|
362
|
-
isLowValueCloseout,
|
|
363
|
-
extractPayloadText,
|
|
364
|
-
extractPayloadPreviousUserText,
|
|
365
|
-
buildResponseQualityReason,
|
|
366
299
|
extractText,
|
|
367
300
|
extractToolUseSummary,
|
|
368
|
-
readPreviousUserText,
|
|
369
301
|
};
|
package/scripts/hosted-config.js
CHANGED
|
@@ -13,7 +13,6 @@ const DEFAULT_PRO_PRICE_DOLLARS = PRO_MONTHLY_PRICE_DOLLARS;
|
|
|
13
13
|
const DEFAULT_PRO_PRICE_LABEL = PRO_PRICE_LABEL;
|
|
14
14
|
const DEFAULT_SPRINT_DIAGNOSTIC_PRICE_DOLLARS = 499;
|
|
15
15
|
const DEFAULT_WORKFLOW_SPRINT_PRICE_DOLLARS = 1500;
|
|
16
|
-
const DEFAULT_SNAPSHOT_PRICE_DOLLARS = 97;
|
|
17
16
|
const GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]+$/i;
|
|
18
17
|
|
|
19
18
|
function normalizeOrigin(value) {
|
|
@@ -130,10 +129,6 @@ function resolveHostedBillingConfig({ requestOrigin } = {}, env = process.env) {
|
|
|
130
129
|
const googleSiteVerification = normalizeTrackingId(env.THUMBGATE_GOOGLE_SITE_VERIFICATION);
|
|
131
130
|
const sprintDiagnosticCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_SPRINT_DIAGNOSTIC_CHECKOUT_URL);
|
|
132
131
|
const workflowSprintCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_WORKFLOW_SPRINT_CHECKOUT_URL);
|
|
133
|
-
const paypalDiagnosticCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_PAYPAL_DIAGNOSTIC_CHECKOUT_URL);
|
|
134
|
-
const paypalWorkflowSprintCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_PAYPAL_WORKFLOW_SPRINT_CHECKOUT_URL);
|
|
135
|
-
const morSnapshotCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_MOR_SNAPSHOT_CHECKOUT_URL);
|
|
136
|
-
const morProvider = String(env.THUMBGATE_MOR_PROVIDER || '').trim();
|
|
137
132
|
|
|
138
133
|
return {
|
|
139
134
|
appOrigin,
|
|
@@ -147,16 +142,10 @@ function resolveHostedBillingConfig({ requestOrigin } = {}, env = process.env) {
|
|
|
147
142
|
proPriceLabel,
|
|
148
143
|
sprintDiagnosticCheckoutUrl,
|
|
149
144
|
workflowSprintCheckoutUrl,
|
|
150
|
-
paypalDiagnosticCheckoutUrl,
|
|
151
|
-
paypalWorkflowSprintCheckoutUrl,
|
|
152
|
-
morSnapshotCheckoutUrl,
|
|
153
|
-
morProvider,
|
|
154
145
|
sprintDiagnosticPriceDollars: normalizePriceDollars(env.THUMBGATE_SPRINT_DIAGNOSTIC_PRICE_DOLLARS)
|
|
155
146
|
|| DEFAULT_SPRINT_DIAGNOSTIC_PRICE_DOLLARS,
|
|
156
147
|
workflowSprintPriceDollars: normalizePriceDollars(env.THUMBGATE_WORKFLOW_SPRINT_PRICE_DOLLARS)
|
|
157
148
|
|| DEFAULT_WORKFLOW_SPRINT_PRICE_DOLLARS,
|
|
158
|
-
snapshotPriceDollars: normalizePriceDollars(env.THUMBGATE_SNAPSHOT_PRICE_DOLLARS)
|
|
159
|
-
|| DEFAULT_SNAPSHOT_PRICE_DOLLARS,
|
|
160
149
|
gaMeasurementId,
|
|
161
150
|
googleSiteVerification,
|
|
162
151
|
posthogApiKey,
|
|
@@ -170,7 +159,6 @@ module.exports = {
|
|
|
170
159
|
DEFAULT_PRO_PRICE_LABEL,
|
|
171
160
|
DEFAULT_SPRINT_DIAGNOSTIC_PRICE_DOLLARS,
|
|
172
161
|
DEFAULT_WORKFLOW_SPRINT_PRICE_DOLLARS,
|
|
173
|
-
DEFAULT_SNAPSHOT_PRICE_DOLLARS,
|
|
174
162
|
GA_MEASUREMENT_ID_PATTERN,
|
|
175
163
|
normalizeAbsoluteUrl,
|
|
176
164
|
normalizeOrigin,
|
package/scripts/lesson-search.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
4
|
const { readJSONL, getFeedbackPaths } = require('./feedback-loop');
|
|
6
5
|
const { buildMemoryLifecycleView, scoreHybridMemoryMatch } = require('./agent-memory-lifecycle');
|
|
7
6
|
const { loadOptionalModule } = require('./private-core-boundary');
|
|
@@ -167,17 +166,6 @@ function resolveLessonPaths(options = {}) {
|
|
|
167
166
|
};
|
|
168
167
|
}
|
|
169
168
|
|
|
170
|
-
function readPackagedBuiltinLessons() {
|
|
171
|
-
if (process.env.THUMBGATE_DISABLE_BUILTIN_LESSONS === '1') return [];
|
|
172
|
-
const builtinPath = path.resolve(__dirname, '..', 'config', 'builtin-lessons.json');
|
|
173
|
-
try {
|
|
174
|
-
const parsed = JSON.parse(fs.readFileSync(builtinPath, 'utf8'));
|
|
175
|
-
return Array.isArray(parsed.lessons) ? parsed.lessons : [];
|
|
176
|
-
} catch {
|
|
177
|
-
return [];
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
169
|
function readPreventionRuleMatches(queryText, limit = 3, options = {}) {
|
|
182
170
|
const { PREVENTION_RULES_PATH } = resolveLessonPaths(options);
|
|
183
171
|
if (!PREVENTION_RULES_PATH) return [];
|
|
@@ -493,9 +481,7 @@ function searchLessons(query = '', options = {}) {
|
|
|
493
481
|
const sqliteResults = tryFts5Search(query, options);
|
|
494
482
|
if (sqliteResults) return sqliteResults;
|
|
495
483
|
|
|
496
|
-
const
|
|
497
|
-
const builtinMemories = options.includeBuiltinLessons === false ? [] : readPackagedBuiltinLessons();
|
|
498
|
-
const memories = [...builtinMemories, ...localMemories];
|
|
484
|
+
const memories = readJSONL(MEMORY_LOG_PATH);
|
|
499
485
|
const feedbackEntries = readJSONL(FEEDBACK_LOG_PATH);
|
|
500
486
|
const feedbackById = new Map(feedbackEntries.map((entry) => [entry.id, entry]));
|
|
501
487
|
const parsedLimit = Number(options.limit || 10);
|