thumbgate 1.30.0 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/README.md +54 -16
  4. package/adapters/claude/.mcp.json +2 -2
  5. package/adapters/forge/forge.yaml +3 -3
  6. package/adapters/mcp/server-stdio.js +105 -10
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/bench/observability-eval-suite.json +2 -2
  9. package/bin/cli.js +168 -31
  10. package/config/evals/generation-quality-golden.json +95 -0
  11. package/config/evals/rag-answer-quality-golden.json +91 -0
  12. package/config/evals/retrieval-hybrid-ablation.json +66 -0
  13. package/config/evals/retrieval-ranking-golden.json +522 -0
  14. package/config/gates/claim-verifiers.example.json +42 -0
  15. package/config/gates/claim-verifiers.json +25 -0
  16. package/config/gates/default.json +217 -50
  17. package/config/mcp-allowlists.json +233 -206
  18. package/config/model-tiers.json +7 -2
  19. package/glama.json +6 -0
  20. package/hooks/hooks.json +1 -1
  21. package/package.json +69 -12
  22. package/public/assets/diagrams/before-after.svg +17 -16
  23. package/public/assets/diagrams/hero-thumbs.svg +68 -0
  24. package/public/assets/diagrams/loop.svg +19 -13
  25. package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
  26. package/public/compare.html +1 -0
  27. package/public/dashboard.html +126 -28
  28. package/public/evaluations.html +1 -1
  29. package/public/index.html +142 -13
  30. package/public/numbers.html +3 -2
  31. package/public/pricing.html +143 -30
  32. package/scripts/a-plus-evidence-scorecard.js +303 -0
  33. package/scripts/agent-readiness.js +110 -0
  34. package/scripts/async-eval-observability.js +36 -11
  35. package/scripts/audit-trail.js +37 -1
  36. package/scripts/auto-promote-gates.js +149 -34
  37. package/scripts/auto-wire-hooks.js +20 -8
  38. package/scripts/cli-schema.js +14 -0
  39. package/scripts/colbert-style-maxsim.js +236 -0
  40. package/scripts/cross-encoder-reranker.js +356 -126
  41. package/scripts/dashboard-chat.js +350 -17
  42. package/scripts/document-intake.js +283 -7
  43. package/scripts/eval-quality-suite.js +204 -0
  44. package/scripts/feedback-loop.js +115 -7
  45. package/scripts/feedback-paths.js +32 -13
  46. package/scripts/feedback-quality.js +53 -0
  47. package/scripts/feedback-schema.js +3 -0
  48. package/scripts/file-ledger-lock.js +130 -0
  49. package/scripts/filesystem-search.js +17 -7
  50. package/scripts/financial-control-plane.js +1514 -0
  51. package/scripts/gates-engine.js +202 -7
  52. package/scripts/gemini-embedding-policy.js +1 -0
  53. package/scripts/harness-tool-names.js +70 -0
  54. package/scripts/hook-runtime.js +15 -3
  55. package/scripts/hook-stop-anti-claim.js +63 -3
  56. package/scripts/human-escalation.js +353 -41
  57. package/scripts/lesson-db.js +16 -5
  58. package/scripts/lesson-embedding-index.js +67 -20
  59. package/scripts/lesson-embedding-maintenance.js +177 -0
  60. package/scripts/lesson-reranker.js +55 -9
  61. package/scripts/lesson-retrieval.js +305 -29
  62. package/scripts/lesson-search.js +22 -8
  63. package/scripts/llm-client.js +304 -15
  64. package/scripts/model-tier-router.js +593 -0
  65. package/scripts/pragmatic-hybrid-search.js +379 -0
  66. package/scripts/provider-action-normalizer.js +11 -4
  67. package/scripts/rag-document-pipeline.js +461 -0
  68. package/scripts/rag-structured-output.js +441 -0
  69. package/scripts/ragas-style-metrics.js +351 -0
  70. package/scripts/request-envelope.js +178 -0
  71. package/scripts/rerank-pipeline.js +370 -0
  72. package/scripts/rerank-quality-eval.js +155 -0
  73. package/scripts/retrieval-hybrid-ablation.js +120 -0
  74. package/scripts/retrieval-quality-tier.js +118 -0
  75. package/scripts/secret-scanner.js +395 -4
  76. package/scripts/self-distill-agent.js +7 -1
  77. package/scripts/self-healing-check.js +25 -0
  78. package/scripts/skill-packs.js +183 -0
  79. package/scripts/slow-loop.js +72 -0
  80. package/scripts/statusline-links.js +1 -1
  81. package/scripts/statusline.sh +8 -1
  82. package/scripts/telemetry-analytics.js +13 -1
  83. package/scripts/thumbgate-search.js +98 -6
  84. package/scripts/tier-budget-guard.js +186 -0
  85. package/scripts/tool-registry.js +141 -5
  86. package/scripts/universal-claim-evaluator.js +767 -0
  87. package/scripts/vector-store.js +154 -17
  88. package/scripts/verify-marketing-pages-deployed.js +85 -3
  89. package/scripts/workflow-sentinel.js +77 -11
  90. package/server.json +44 -0
  91. package/smithery.yaml +17 -0
  92. package/src/api/server.js +196 -13
@@ -16,6 +16,13 @@ const {
16
16
  const {
17
17
  evaluateWorkflowSentinel,
18
18
  } = require('./workflow-sentinel');
19
+ const {
20
+ evaluateFinancialControl,
21
+ } = require('./financial-control-plane');
22
+ const {
23
+ buildCostControl,
24
+ normalizeProviderAction,
25
+ } = require('./provider-action-normalizer');
19
26
  const {
20
27
  recordDecisionEvaluation,
21
28
  recordDecisionOutcome,
@@ -1236,11 +1243,28 @@ const COMMAND_WRAPPERS = new Set([
1236
1243
  ]);
1237
1244
  const ENV_ASSIGNMENT_PREFIX = /^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]*)\s+/;
1238
1245
  const WRAPPER_HEAD = /^([A-Za-z_][\w.-]*)\s+/;
1246
+ const LITERAL_COMMAND_SUBSTITUTION_HEADS = [
1247
+ /^\$\(\s*printf\s+(?:(?:['"]?%s['"]?)\s+)?(['"]?)([A-Za-z_][\w.-]*)\1\s*\)\s+/,
1248
+ /^\$\(\s*echo\s+(['"]?)([A-Za-z_][\w.-]*)\1\s*\)\s+/,
1249
+ /^\$\(\s*(?:command\s+-v|which)\s+(['"]?)([A-Za-z_][\w.-]*)\1\s*\)\s+/,
1250
+ ];
1251
+
1252
+ function canonicalizeLiteralCommandSubstitutionHead(segment) {
1253
+ for (const pattern of LITERAL_COMMAND_SUBSTITUTION_HEADS) {
1254
+ const match = String(segment || '').match(pattern);
1255
+ if (match) return `${match[2]} ${String(segment).slice(match[0].length)}`;
1256
+ }
1257
+ return segment;
1258
+ }
1239
1259
 
1240
1260
  function canonicalizeSegmentHead(segment) {
1241
1261
  let text = String(segment || '').trim();
1242
1262
  for (let i = 0; i < 12; i += 1) {
1243
1263
  const before = text;
1264
+ // Resolve only literal, side-effect-free command-position substitutions.
1265
+ // Never execute or guess arbitrary shell; this closes common `$(printf git)`
1266
+ // and `$(command -v git)` spellings while keeping matching deterministic.
1267
+ text = canonicalizeLiteralCommandSubstitutionHead(text);
1244
1268
  text = text.replace(ENV_ASSIGNMENT_PREFIX, '');
1245
1269
  const wrapper = text.match(WRAPPER_HEAD);
1246
1270
  if (wrapper && COMMAND_WRAPPERS.has(wrapper[1].toLowerCase())) {
@@ -1919,6 +1943,43 @@ function recordStructuralGateBlock(toolName, toolInput, result) {
1919
1943
  return result;
1920
1944
  }
1921
1945
 
1946
+ /**
1947
+ * Resolve catastrophic declarative gates before the ordinary first-match loop.
1948
+ *
1949
+ * Config order is useful for normal policy routing, but it must not let a broad
1950
+ * rule mask a narrower irreversible-action rule. For example, the generic
1951
+ * `push-without-thread-check` gate also matches `git push --force`; selecting it
1952
+ * first allowed the free-tier daily cap to downgrade the action to a warning
1953
+ * before the exempt `force-push` gate was ever evaluated.
1954
+ *
1955
+ * Catastrophic gates are deliberately limited to the audited allowlist above.
1956
+ * Metric-backed gates are excluded because their condition is asynchronous and
1957
+ * none of the catastrophic command boundaries may depend on a remote metric.
1958
+ */
1959
+ function evaluateCatastrophicDeclarativeGate(config, constraints, toolName, toolInput) {
1960
+ if (!config || !Array.isArray(config.gates)) return null;
1961
+
1962
+ for (const gate of config.gates) {
1963
+ if (!CATASTROPHIC_DECLARATIVE_GATE_IDS.has(gate.id)) continue;
1964
+ if (gate.action !== 'block' || gate.metrics) continue;
1965
+
1966
+ const matchDetails = matchGate(gate, toolName, toolInput);
1967
+ if (!matchDetails.matched) continue;
1968
+ if (gate.when && !checkWhenClause(gate.when, constraints)) continue;
1969
+ if (gate.unless && isConditionSatisfied(gate.unless)) continue;
1970
+
1971
+ return {
1972
+ decision: 'deny',
1973
+ gate: gate.id,
1974
+ message: buildGateMessage(gate, matchDetails),
1975
+ severity: gate.severity,
1976
+ reasoning: buildReasoning(gate, toolName, toolInput, matchDetails),
1977
+ };
1978
+ }
1979
+
1980
+ return null;
1981
+ }
1982
+
1922
1983
  function isScopeEnforcedAction(toolName, toolInput = {}, affectedFiles = []) {
1923
1984
  if (EDIT_LIKE_TOOLS.has(toolName) && affectedFiles.length > 0) return true;
1924
1985
  if (toolName !== 'Bash') return false;
@@ -2759,6 +2820,16 @@ async function evaluateGatesAsyncInner(toolName, toolInput, configPath) {
2759
2820
  return boostedRiskGuard;
2760
2821
  }
2761
2822
 
2823
+ const catastrophicDeclarativeGate = evaluateCatastrophicDeclarativeGate(
2824
+ config,
2825
+ constraints,
2826
+ toolName,
2827
+ toolInput,
2828
+ );
2829
+ if (catastrophicDeclarativeGate) {
2830
+ return recordStructuralGateBlock(toolName, toolInput, catastrophicDeclarativeGate);
2831
+ }
2832
+
2762
2833
  // Tier 1b: Planning and Trajectory (v1.26.0 - CodeRabbit Pattern).
2763
2834
  // Keep runtime enforcement explicit so advisory planning checks do not mask
2764
2835
  // higher-priority deny/approve gates in established workflows.
@@ -3005,6 +3076,16 @@ function evaluateGatesInner(toolName, toolInput, configPath) {
3005
3076
  return boostedRiskGuard;
3006
3077
  }
3007
3078
 
3079
+ const catastrophicDeclarativeGate = evaluateCatastrophicDeclarativeGate(
3080
+ config,
3081
+ constraints,
3082
+ toolName,
3083
+ toolInput,
3084
+ );
3085
+ if (catastrophicDeclarativeGate) {
3086
+ return recordStructuralGateBlock(toolName, toolInput, catastrophicDeclarativeGate);
3087
+ }
3088
+
3008
3089
  // Tier 1b: Planning and Trajectory (v1.26.0 - CodeRabbit Pattern).
3009
3090
  // Keep runtime enforcement explicit so advisory planning checks do not mask
3010
3091
  // higher-priority deny/approve gates in established workflows.
@@ -3286,7 +3367,7 @@ function evaluateSecretGuard(input = {}) {
3286
3367
  return result;
3287
3368
  }
3288
3369
 
3289
- function evaluateUnconditionalHardFloor(input = {}) {
3370
+ function evaluateUnconditionalHardFloor(input = {}, options = {}) {
3290
3371
  const secretGuard = evaluateSecretGuard(input);
3291
3372
  if (secretGuard) return { hardFloor: secretGuard, securityScan: null };
3292
3373
 
@@ -3295,14 +3376,81 @@ function evaluateUnconditionalHardFloor(input = {}) {
3295
3376
  return { hardFloor: securityScan, securityScan };
3296
3377
  }
3297
3378
 
3379
+ const financialHardFloor = evaluateFinancialHardFloor(input, false, options);
3380
+ if (financialHardFloor) return { hardFloor: financialHardFloor, securityScan };
3381
+
3298
3382
  return {
3299
3383
  hardFloor: evaluateSelfProtectHardFloor(input),
3300
3384
  securityScan,
3301
3385
  };
3302
3386
  }
3303
3387
 
3304
- function runHardFloor(input) {
3305
- const { hardFloor } = evaluateUnconditionalHardFloor(input);
3388
+ function evaluateFinancialHardFloor(input = {}, consumeReservation = false, options = {}) {
3389
+ const toolName = input.tool_name || input.toolName || 'unknown';
3390
+ const toolInput = input.tool_input && typeof input.tool_input === 'object'
3391
+ ? input.tool_input
3392
+ : {};
3393
+ const normalizedAction = normalizeProviderAction({
3394
+ toolName,
3395
+ toolInput,
3396
+ usage: input.usage || toolInput.usage,
3397
+ costUsd: input.costUsd ?? toolInput.costUsd,
3398
+ budget: input.budget || toolInput.budget,
3399
+ });
3400
+ const costControl = buildCostControl(
3401
+ normalizedAction,
3402
+ input.budget || toolInput.budget || {}
3403
+ );
3404
+ const financialControl = evaluateFinancialControl({
3405
+ toolName,
3406
+ toolInput,
3407
+ actionProfile: {
3408
+ economicAction: undefined,
3409
+ },
3410
+ costControl,
3411
+ }, { ...options, consumeReservation });
3412
+ if (financialControl.mode === 'block') {
3413
+ const result = {
3414
+ decision: 'deny',
3415
+ gate: 'financial-control',
3416
+ message: financialControl.reasons.join(' '),
3417
+ severity: 'critical',
3418
+ financialControl,
3419
+ reasoning: [
3420
+ 'Economic actions default to deny at the pre-tool boundary.',
3421
+ 'Learned policy and advisory memories cannot override this deterministic control.',
3422
+ ],
3423
+ };
3424
+ recordStat('financial-control', 'block', null, { toolName, toolInput });
3425
+ const auditRecord = recordAuditEvent({
3426
+ toolName,
3427
+ toolInput,
3428
+ decision: 'deny',
3429
+ gateId: 'financial-control',
3430
+ message: result.message,
3431
+ severity: result.severity,
3432
+ source: 'financial-control',
3433
+ });
3434
+ auditToFeedback(auditRecord);
3435
+ return result;
3436
+ }
3437
+ return null;
3438
+ }
3439
+
3440
+ // Reservations are single-use. They are consumed only after every other gate
3441
+ // has reached its final allow/warn boundary, never during the preliminary hard
3442
+ // floor preview. This prevents a later workflow or learned-risk denial from
3443
+ // burning an approval for an action that did not execute.
3444
+ function finalizeFinancialAuthorization(input = {}, options = {}) {
3445
+ return evaluateFinancialHardFloor(input, true, options);
3446
+ }
3447
+
3448
+ function isBlockingDecision(result) {
3449
+ return result?.decision === 'deny' || result?.decision === 'approve';
3450
+ }
3451
+
3452
+ function runHardFloor(input, options = {}) {
3453
+ const { hardFloor } = evaluateUnconditionalHardFloor(input, options);
3306
3454
  return hardFloor ? formatOutput(hardFloor) : null;
3307
3455
  }
3308
3456
 
@@ -3376,7 +3524,7 @@ function formatOutput(result, behavioralContext) {
3376
3524
  if (result.decision === 'deny') {
3377
3525
  const reminder = behavioralContext ? buildReminderOutput(behavioralContext) : {};
3378
3526
  const reminderSuffix = behavioralContext ? `\n\nSystem reminder:\n${behavioralContext}` : '';
3379
- const proCta = buildBlockActionProCta() || '';
3527
+ const proCta = result.gate === 'financial-control' ? '' : (buildBlockActionProCta() || '');
3380
3528
  return JSON.stringify({
3381
3529
  hookSpecificOutput: {
3382
3530
  hookEventName: 'PreToolUse',
@@ -3690,10 +3838,18 @@ async function runAsync(input) {
3690
3838
  if (lessonContext && lessonContext.decision === "deny") {
3691
3839
  return formatOutput(applyEnforcementPosture(lessonContext));
3692
3840
  }
3841
+
3842
+ const posturedResult = applyEnforcementPosture(result);
3843
+ if (isBlockingDecision(posturedResult)) {
3844
+ return formatOutput(posturedResult);
3845
+ }
3846
+
3847
+ const financialAuthorization = finalizeFinancialAuthorization(input);
3848
+ if (financialAuthorization) return formatOutput(financialAuthorization);
3693
3849
 
3694
3850
  const recentContext = buildRecentCorrectiveActionsContext();
3695
3851
  const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
3696
- return formatOutput(applyEnforcementPosture(result), combinedContext);
3852
+ return formatOutput(posturedResult, combinedContext);
3697
3853
 
3698
3854
  }
3699
3855
 
@@ -3729,10 +3885,18 @@ function run(input) {
3729
3885
  if (lessonContext && lessonContext.decision === "deny") {
3730
3886
  return formatOutput(applyEnforcementPosture(lessonContext));
3731
3887
  }
3888
+
3889
+ const posturedResult = applyEnforcementPosture(result);
3890
+ if (isBlockingDecision(posturedResult)) {
3891
+ return formatOutput(posturedResult);
3892
+ }
3893
+
3894
+ const financialAuthorization = finalizeFinancialAuthorization(input);
3895
+ if (financialAuthorization) return formatOutput(financialAuthorization);
3732
3896
 
3733
3897
  const recentContext = buildRecentCorrectiveActionsContext();
3734
3898
  const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
3735
- return formatOutput(applyEnforcementPosture(result), combinedContext);
3899
+ return formatOutput(posturedResult, combinedContext);
3736
3900
 
3737
3901
  }
3738
3902
 
@@ -4020,10 +4184,40 @@ function verifyClaimEvidence(claimText, options = {}) {
4020
4184
  });
4021
4185
  }
4022
4186
 
4187
+ // Universal factual claims (row counts, file lines/bytes/existence, version values)
4188
+ // recheck the configured source of truth. Fail-closed on mismatch or missing verifier.
4189
+ let universal = null;
4190
+ if (options.skipUniversal !== true) {
4191
+ try {
4192
+ const {
4193
+ evaluateUniversalClaimsAsGateChecks,
4194
+ } = require('./universal-claim-evaluator');
4195
+ universal = evaluateUniversalClaimsAsGateChecks(normalizedClaimText, {
4196
+ cwd: options.cwd,
4197
+ verifiers: options.verifiers,
4198
+ configPath: options.claimVerifiersPath,
4199
+ config: options.claimVerifiers,
4200
+ feedbackDir: options.feedbackDir,
4201
+ failUnconfigured: options.failUnconfigured,
4202
+ });
4203
+ for (const check of universal.checks) {
4204
+ checks.push(check);
4205
+ }
4206
+ } catch (error) {
4207
+ checks.push({
4208
+ claim: 'universal_evaluator',
4209
+ passed: false,
4210
+ missing: ['universal_claim_evaluator'],
4211
+ message: `Universal claim evaluator failed closed: ${error && error.message ? error.message : 'unknown error'}`,
4212
+ });
4213
+ }
4214
+ }
4215
+
4023
4216
  return {
4024
- verified: checks.every((check) => check.passed),
4217
+ verified: checks.length === 0 ? true : checks.every((check) => check.passed),
4025
4218
  checks,
4026
4219
  goalContract,
4220
+ universal,
4027
4221
  };
4028
4222
  }
4029
4223
 
@@ -4071,6 +4265,7 @@ module.exports = {
4071
4265
  isAutonomousRun,
4072
4266
  computeExecutableHash,
4073
4267
  formatOutput,
4268
+ finalizeFinancialAuthorization,
4074
4269
  isApprovalGatesEnabled,
4075
4270
  runHardFloor,
4076
4271
  run,
@@ -190,6 +190,7 @@ module.exports = {
190
190
  GEMINI_TASK_TYPES,
191
191
  buildGeminiEmbeddingRolloutPlan,
192
192
  isSymmetricTask,
193
+ normalizeEmbeddingKind,
193
194
  normalizeOutputDimensionality,
194
195
  normalizeTask,
195
196
  prepareEmbeddingText,
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Harness tool names -> canonical gate-engine names.
5
+ *
6
+ * Policy gates match Claude Code's vocabulary (Bash, Write, Edit, MultiEdit). Other
7
+ * harnesses use their own: Cline's .clinerules instructs the agent to gate-check
8
+ * `execute_command`, `write_to_file`, `replace_in_file` and `browser_action`.
9
+ * Forwarding those through unchanged meant every gate silently missed — a strict
10
+ * `gate_check` on `{tool_name: "execute_command", command: "rm -rf /"}` returned ALLOW,
11
+ * which is precisely the scenario the adapter advertises as protected.
12
+ *
13
+ * Argument keys differ too: Cline writes use `path`, Claude Code uses `file_path`.
14
+ */
15
+
16
+ const TOOL_NAME_MAP = new Map(Object.entries({
17
+ // Cline / Roo Code
18
+ execute_command: 'Bash',
19
+ write_to_file: 'Write',
20
+ replace_in_file: 'Edit',
21
+ apply_diff: 'Edit',
22
+ insert_content: 'Edit',
23
+ search_and_replace: 'Edit',
24
+ // OpenCode / generic MCP
25
+ bash: 'Bash',
26
+ shell: 'Bash',
27
+ run_command: 'Bash',
28
+ edit_file: 'Edit',
29
+ write_file: 'Write',
30
+ create_file: 'Write',
31
+ str_replace_editor: 'Edit',
32
+ // Cursor
33
+ run_terminal_cmd: 'Bash',
34
+ }));
35
+
36
+ const ARG_KEY_MAP = new Map(Object.entries({
37
+ path: 'file_path',
38
+ filePath: 'file_path',
39
+ target_file: 'file_path',
40
+ cmd: 'command',
41
+ shell_command: 'command',
42
+ diff: 'new_string',
43
+ }));
44
+
45
+ function canonicalizeToolName(toolName) {
46
+ const raw = String(toolName || '').trim();
47
+ if (!raw) return raw;
48
+ return TOOL_NAME_MAP.get(raw) || TOOL_NAME_MAP.get(raw.toLowerCase()) || raw;
49
+ }
50
+
51
+ function canonicalizeToolInput(toolInput) {
52
+ if (!toolInput || typeof toolInput !== 'object' || Array.isArray(toolInput)) return toolInput || {};
53
+ const out = {};
54
+ for (const [key, value] of Object.entries(toolInput)) {
55
+ const canonicalKey = ARG_KEY_MAP.get(key) || key;
56
+ // Never let a mapped key clobber one the caller already supplied canonically.
57
+ if (canonicalKey in out) continue;
58
+ out[canonicalKey] = value;
59
+ }
60
+ return out;
61
+ }
62
+
63
+ function canonicalizeToolCall(toolName, toolInput) {
64
+ return {
65
+ toolName: canonicalizeToolName(toolName),
66
+ toolInput: canonicalizeToolInput(toolInput),
67
+ };
68
+ }
69
+
70
+ module.exports = { canonicalizeToolCall, canonicalizeToolName, canonicalizeToolInput, TOOL_NAME_MAP, ARG_KEY_MAP };
@@ -51,13 +51,20 @@ function resolveCliCommand(subcommand) {
51
51
  }
52
52
 
53
53
  function resolveCodexCliCommand(subcommand) {
54
+ // Codex hooks live in user-global config. Pinning them to a disposable source
55
+ // worktree breaks every hook as soon as normal post-merge cleanup removes it.
56
+ // The stable @latest launcher stays offline at configuration time and survives
57
+ // cleanup. Checkout-pinned hooks remain available as an explicit dev override.
58
+ if (isSourceCheckout(PKG_ROOT) && process.env.THUMBGATE_CODEX_USE_SOURCE_RUNTIME === '1') {
59
+ return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
60
+ }
61
+ if (isSourceCheckout(PKG_ROOT)) {
62
+ return publishedCliShellCommand('latest', [subcommand]);
63
+ }
54
64
  const version = packageVersion();
55
65
  if (publishedHookCommandsAvailable(version)) {
56
66
  return publishedCliShellCommand('latest', [subcommand]);
57
67
  }
58
- if (isSourceCheckout(PKG_ROOT)) {
59
- return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
60
- }
61
68
  return publishedCliShellCommand('latest', [subcommand]);
62
69
  }
63
70
 
@@ -85,6 +92,10 @@ function cacheUpdateHookCommand() {
85
92
  return buildPortableHookCommand('cache-update');
86
93
  }
87
94
 
95
+ function claimStopHookCommand() {
96
+ return buildPortableHookCommand('claim-stop-check');
97
+ }
98
+
88
99
  function statuslineCommand() {
89
100
  return buildPortableHookCommand('statusline-render');
90
101
  }
@@ -113,6 +124,7 @@ module.exports = {
113
124
  buildPortableHookCommand,
114
125
  buildCodexPortableHookCommand,
115
126
  cacheUpdateHookCommand,
127
+ claimStopHookCommand,
116
128
  codexCacheUpdateHookCommand,
117
129
  codexPreToolHookCommand,
118
130
  codexSessionStartHookCommand,
@@ -29,6 +29,9 @@
29
29
  */
30
30
 
31
31
  const fs = require('node:fs');
32
+ const {
33
+ evaluateUniversalClaims,
34
+ } = require('./universal-claim-evaluator');
32
35
 
33
36
  // Lie-phrase patterns. These match common "claim of completion" wording
34
37
  // the agent emits without verification. Word-boundary anchored to avoid
@@ -233,6 +236,45 @@ function readStdinSync() {
233
236
  }
234
237
  }
235
238
 
239
+ function factualClaimBlock(text, options = {}) {
240
+ try {
241
+ const result = evaluateUniversalClaims(text, {
242
+ cwd: options.cwd,
243
+ configPath: options.configPath,
244
+ feedbackDir: options.feedbackDir,
245
+ failUnconfigured: true,
246
+ });
247
+ if (result.parsedCount === 0) return null;
248
+ if (result.verified) return null;
249
+ const failures = result.checks.filter((check) => !check.passed);
250
+ return {
251
+ decision: 'block',
252
+ reason: `ThumbGate factual-claim gate: ${failures.map((check) => check.message).join('; ')}. Recheck the configured source of truth and restate the observed value, or retract the claim.`,
253
+ verification: {
254
+ verified: false,
255
+ parsedCount: result.parsedCount,
256
+ failures: failures.map((check) => ({
257
+ status: check.status,
258
+ kind: check.kind,
259
+ verifierId: check.verifierId || null,
260
+ expected: check.expected,
261
+ actual: Object.hasOwn(check, 'actual') ? check.actual : null,
262
+ })),
263
+ },
264
+ };
265
+ } catch (error) {
266
+ return {
267
+ decision: 'block',
268
+ reason: `ThumbGate factual-claim gate failed closed: ${error.message}`,
269
+ verification: {
270
+ verified: false,
271
+ parsedCount: null,
272
+ failures: [{ status: 'evaluator_error' }],
273
+ },
274
+ };
275
+ }
276
+ }
277
+
236
278
  function main() {
237
279
  const raw = readStdinSync();
238
280
  let payload = {};
@@ -242,12 +284,28 @@ function main() {
242
284
  payload = {};
243
285
  }
244
286
 
287
+ // Claude Code invokes a blocked Stop hook once more with this marker so the
288
+ // agent can correct its response. Re-blocking the same payload would hit the
289
+ // host's block cap instead of giving the agent a correction turn.
290
+ if (payload.stop_hook_active === true) return;
291
+
245
292
  const transcriptPath = payload.transcript_path || process.env.CLAUDE_TRANSCRIPT_PATH;
246
293
  const message = readLastAssistantTurn(transcriptPath);
247
- if (!message) return; // no transcript visible; nothing to check
294
+ const text = message
295
+ ? extractText(message)
296
+ : String(payload.last_assistant_message || process.env.CLAUDE_RESPONSE || '');
297
+ if (!text.trim()) return; // no assistant response visible; nothing to check
298
+
299
+ const toolUseSummary = message ? extractToolUseSummary(message) : '';
300
+ const factualBlock = factualClaimBlock(text, {
301
+ cwd: payload.cwd || payload.workspace_root || process.cwd(),
302
+ configPath: process.env.THUMBGATE_CLAIM_VERIFIERS_PATH,
303
+ });
304
+ if (factualBlock) {
305
+ process.stdout.write(`${JSON.stringify(factualBlock)}\n`);
306
+ return;
307
+ }
248
308
 
249
- const text = extractText(message);
250
- const toolUseSummary = extractToolUseSummary(message);
251
309
  const claim = findClaim(text);
252
310
  if (!claim) return; // no completion claim made; silent
253
311
 
@@ -298,4 +356,6 @@ module.exports = {
298
356
  hasProof,
299
357
  extractText,
300
358
  extractToolUseSummary,
359
+ factualClaimBlock,
360
+ main,
301
361
  };