thumbgate 1.27.6 → 1.27.8

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 (96) hide show
  1. package/.claude/commands/thumbgate-blocked.md +27 -0
  2. package/.claude/commands/thumbgate-doctor.md +30 -0
  3. package/.claude/commands/thumbgate-guard.md +36 -0
  4. package/.claude/commands/thumbgate-protect.md +30 -0
  5. package/.claude/commands/thumbgate-rules.md +30 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +6 -2
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/README.md +49 -5
  10. package/adapters/claude/.mcp.json +2 -2
  11. package/adapters/letta/README.md +41 -0
  12. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  13. package/adapters/mcp/server-stdio.js +16 -1
  14. package/adapters/opencode/opencode.json +1 -1
  15. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  16. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  17. package/bench/observability-eval-suite.json +26 -0
  18. package/bin/cli.js +180 -2
  19. package/bin/postinstall.js +1 -1
  20. package/config/gate-templates.json +84 -0
  21. package/config/gates/claim-verification.json +6 -0
  22. package/config/gates/default.json +20 -0
  23. package/config/github-about.json +1 -1
  24. package/config/model-candidates.json +50 -0
  25. package/package.json +66 -25
  26. package/public/agent-manager.html +41 -1
  27. package/public/agents-cost-savings.html +1 -1
  28. package/public/ai-malpractice-prevention.html +2 -1
  29. package/public/assets/brand/github-social-preview.png +0 -0
  30. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  31. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  32. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  33. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  34. package/public/assets/brand/thumbgate-mark-inline.svg +15 -0
  35. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  36. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  37. package/public/assets/brand/thumbgate-mark.svg +15 -0
  38. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  39. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  40. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  41. package/public/assets/legal-intake-control-flow.svg +66 -0
  42. package/public/blog.html +1 -1
  43. package/public/brand/thumbgate-mark.svg +15 -0
  44. package/public/brand/thumbgate-og.svg +16 -0
  45. package/public/codex-enterprise.html +1 -1
  46. package/public/codex-plugin.html +1 -1
  47. package/public/compare.html +23 -3
  48. package/public/dashboard.html +312 -30
  49. package/public/federal.html +1 -1
  50. package/public/guide.html +5 -4
  51. package/public/index.html +167 -49
  52. package/public/js/buyer-intent.js +672 -0
  53. package/public/learn.html +74 -7
  54. package/public/lessons.html +2 -1
  55. package/public/numbers.html +3 -3
  56. package/public/pricing.html +63 -15
  57. package/public/pro.html +7 -7
  58. package/scripts/activation-quickstart.js +187 -0
  59. package/scripts/agent-memory-lifecycle.js +211 -0
  60. package/scripts/async-eval-observability.js +236 -0
  61. package/scripts/auto-promote-gates.js +75 -4
  62. package/scripts/build-metadata.js +24 -3
  63. package/scripts/cli-schema.js +22 -0
  64. package/scripts/dashboard-chat.js +2 -1
  65. package/scripts/dashboard.js +8 -0
  66. package/scripts/export-databricks-bundle.js +5 -1
  67. package/scripts/export-dpo-pairs.js +7 -2
  68. package/scripts/feedback-aggregate.js +281 -0
  69. package/scripts/feedback-loop.js +34 -0
  70. package/scripts/filesystem-search.js +35 -10
  71. package/scripts/gates-engine.js +198 -6
  72. package/scripts/gemini-embedding-policy.js +2 -1
  73. package/scripts/hook-stop-anti-claim.js +227 -0
  74. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  75. package/scripts/lesson-inference.js +8 -3
  76. package/scripts/lesson-search.js +17 -1
  77. package/scripts/operational-integrity.js +39 -5
  78. package/scripts/plausible-domain-config.js +4 -2
  79. package/scripts/rate-limiter.js +12 -6
  80. package/scripts/secret-redaction.js +166 -0
  81. package/scripts/security-scanner.js +100 -0
  82. package/scripts/self-distill-agent.js +3 -1
  83. package/scripts/self-harness-optimizer.js +141 -0
  84. package/scripts/seo-gsd.js +635 -0
  85. package/scripts/statusline-cache-path.js +17 -2
  86. package/scripts/statusline-cache-read.js +57 -0
  87. package/scripts/statusline-local-stats.js +9 -1
  88. package/scripts/statusline-meta.js +5 -2
  89. package/scripts/statusline.sh +13 -1
  90. package/scripts/sync-telemetry-from-prod.js +374 -0
  91. package/scripts/telemetry-analytics.js +9 -0
  92. package/scripts/thumbgate-search.js +85 -19
  93. package/scripts/tool-contract-validator.js +76 -0
  94. package/scripts/vector-store.js +44 -0
  95. package/scripts/workspace-evolver.js +62 -2
  96. package/src/api/server.js +715 -86
@@ -119,8 +119,16 @@ const MAX_COMMAND_SCAN_CHARS = 20000;
119
119
  const BOOSTED_RISK_BLOCK_SCORE = 0.8;
120
120
  const BOOSTED_RISK_MIN_EXAMPLES = 3;
121
121
  const PR_THREAD_RESOLUTION_ACTION = 'pr_thread_resolution_verified_after_commit';
122
+ const HELPER_BYPASS_ACTION = 'helper_script_modified';
122
123
  const KNOWLEDGE_ENTROPY_THRESHOLD = 0.7;
123
- const KNOWLEDGE_CONFLICT_STRICT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+merge\b|gh\s+release\s+(?:create|delete|edit|upload)\b|(?:npm|yarn|pnpm)\s+publish\b|rm\s+-rf\b|git\s+reset\s+--hard\b|git\s+clean\s+-f|railway\s+(?:deploy|up)\b|gcloud\s+(?:run\s+deploy|app\s+deploy)\b|firebase\s+deploy\b|vercel\s+--prod\b|kubectl\s+(?:apply|delete)\b|terraform\s+(?:apply|destroy)\b)\b/i;
124
+ const KNOWLEDGE_CONFLICT_STRICT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+merge\b|gh\s+release\s+(?:create|delete|edit|upload)\b|(?:npm|yarn|pnpm)\s+publish\b|rm\s+-rf\b|git\s+reset\s+--hard\b|git\s+clean\s+-f[a-z]*|railway\s+(?:deploy|up)\b|gcloud\s+(?:run\s+deploy|app\s+deploy)\b|firebase\s+deploy\b|vercel\s+--prod\b|kubectl\s+(?:apply|delete)\b|terraform\s+(?:apply|destroy)\b)\b/i;
125
+ const HELPER_SCRIPT_FILE_PATTERN = /(?:^|\/)(?:scripts|bin|tools|tasks|\.githooks|\.github\/workflows)\/|(?:^|\/)(?:package\.json|Makefile|justfile|Taskfile\.ya?ml)$|\.(?:sh|bash|zsh|fish|js|mjs|cjs|ts|tsx|py|rb|pl|ps1|yml|yaml)$/i;
126
+ const PACKAGE_RUN_PATTERN = /\b(?:npm|yarn|pnpm)\s+run\s+([:@./\w-]+)\b/i;
127
+ const HELPER_EXEC_PATTERN = /(?:^|[;&|]\s*|\b(?:bash|sh|zsh|node|python3?|ruby|perl|tsx|ts-node)\s+)(?:(?:\.?\.?\/)?(?:scripts|bin|tools|tasks|tmp|build|dist|\.tmp|\.cache)\/[^\s;&|]+|\.\/[^\s;&|]+\.(?:sh|bash|zsh|js|mjs|cjs|ts|py|rb|pl|ps1))\b/i;
128
+ const HELPER_WRITE_PATTERN = /\b(?:cat|printf|echo|tee|npm\s+pkg\s+set|jq|node\s+-e|python3?\s+-c)\b[\s\S]{0,300}(?:>|--field|scripts\.|package\.json|(?:scripts|bin|tools|tasks|tmp|build|dist|\.tmp|\.cache)\/[^\s;&|]+|\.(?:sh|js|mjs|cjs|ts|py|rb|pl|ps1))\b/i;
129
+ const NETWORK_OR_PROCESS_BOUNDARY_PATTERN = /\b(?:curl|wget|nc|ncat|socat|ssh|scp|rsync|ftp|python3?\s+-m\s+http\.server|node\s+-e|python3?\s+-c|perl\s+-e|ruby\s+-e|bash\s+-c|sh\s+-c|osascript|open)\b/i;
130
+ const DOWNLOAD_EXEC_CHAIN_PATTERN = /\b(?:curl|wget)\b[\s\S]{0,400}(?:\|\s*(?:bash|sh|zsh)|&&[\s\S]{0,200}\bchmod\s+\+x\b[\s\S]{0,200}&&[\s\S]{0,120}(?:\.\/|bash|sh|node|python3?))/i;
131
+ const DESTRUCTIVE_OR_PRIVILEGE_BOUNDARY_PATTERN = /\b(?:rm\s+-rf|chmod\s+(?:\+x|777)|chown\b|sudo\b|dd\s+if=|mkfs|git\s+reset\s+--hard|git\s+clean\s+-f[a-z]*|kubectl\s+(?:apply|delete)|terraform\s+(?:apply|destroy)|railway\s+(?:deploy|up)|gcloud\s+(?:run\s+deploy|app\s+deploy)|vercel\s+--prod|firebase\s+deploy)\b/i;
124
132
 
125
133
  // ---------------------------------------------------------------------------
126
134
  // Enforcement posture (CEO decision 2026-06-04): warn-by-default.
@@ -834,9 +842,26 @@ function toRepoRelativePath(filePath, repoRoot) {
834
842
  const value = String(filePath || '').trim();
835
843
  if (!value) return '';
836
844
  if (repoRoot && path.isAbsolute(value)) {
837
- const relative = path.relative(repoRoot, value);
838
- if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
839
- return normalizePosix(relative);
845
+ const candidates = [[path.resolve(repoRoot), path.resolve(value)]];
846
+ try {
847
+ const rootReal = fs.realpathSync.native(repoRoot);
848
+ let valueReal = null;
849
+ try {
850
+ valueReal = fs.realpathSync.native(value);
851
+ } catch {
852
+ const parentReal = fs.realpathSync.native(path.dirname(value));
853
+ valueReal = path.join(parentReal, path.basename(value));
854
+ }
855
+ candidates.push([rootReal, valueReal]);
856
+ } catch {
857
+ // Fall back to lexical path comparison below.
858
+ }
859
+
860
+ for (const [rootCandidate, valueCandidate] of candidates) {
861
+ const relative = path.relative(rootCandidate, valueCandidate);
862
+ if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
863
+ return normalizePosix(relative);
864
+ }
840
865
  }
841
866
  }
842
867
  return normalizePosix(value);
@@ -1195,6 +1220,125 @@ function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governa
1195
1220
  };
1196
1221
  }
1197
1222
 
1223
+ function helperRiskReasons(text) {
1224
+ const value = String(text || '');
1225
+ const reasons = [];
1226
+ if (DOWNLOAD_EXEC_CHAIN_PATTERN.test(value)) reasons.push('download-then-execute chain');
1227
+ if (NETWORK_OR_PROCESS_BOUNDARY_PATTERN.test(value)) reasons.push('network/process boundary');
1228
+ if (DESTRUCTIVE_OR_PRIVILEGE_BOUNDARY_PATTERN.test(value)) reasons.push('destructive/privileged side effect');
1229
+ if (/\b(?:child_process|spawn\(|exec\(|execFile\(|subprocess|ProcessBuilder)\b/i.test(value)) {
1230
+ reasons.push('process spawn');
1231
+ }
1232
+ return [...new Set(reasons)];
1233
+ }
1234
+
1235
+ function readPackageScript(repoRoot, scriptName) {
1236
+ if (!repoRoot || !scriptName) return '';
1237
+ try {
1238
+ const packagePath = path.join(repoRoot, 'package.json');
1239
+ if (!fs.existsSync(packagePath)) return '';
1240
+ const { scripts = {} } = JSON.parse(fs.readFileSync(packagePath, 'utf8')) || {};
1241
+ return typeof scripts[scriptName] === 'string' ? scripts[scriptName] : '';
1242
+ } catch {
1243
+ return '';
1244
+ }
1245
+ }
1246
+
1247
+ function recordHelperScriptWrite(toolName, toolInput = {}) {
1248
+ if (process.env.THUMBGATE_HELPER_BYPASS_GUARD === '0') return null;
1249
+
1250
+ const affected = extractAffectedFiles(toolName, toolInput);
1251
+ const affectedFiles = affected.files || [];
1252
+ const command = String(toolInput.command || '');
1253
+ const actionContext = extractActionContext(toolName, toolInput);
1254
+ const helperFiles = affectedFiles.filter((filePath) => HELPER_SCRIPT_FILE_PATTERN.test(normalizePosix(filePath)));
1255
+ const packageScriptTouched = affectedFiles.some((filePath) => normalizePosix(filePath) === 'package.json') || /\bpackage\.json\b|\bscripts\./i.test(actionContext);
1256
+ const commandWrite = toolName === 'Bash' && HELPER_WRITE_PATTERN.test(command);
1257
+
1258
+ if (helperFiles.length === 0 && !packageScriptTouched && !commandWrite) return null;
1259
+
1260
+ const riskText = [
1261
+ actionContext,
1262
+ command,
1263
+ helperFiles.join(' '),
1264
+ packageScriptTouched ? 'package.json scripts' : '',
1265
+ ].filter(Boolean).join('\n');
1266
+ const reasons = helperRiskReasons(riskText);
1267
+
1268
+ const metadata = {
1269
+ repoRoot: affected.repoRoot || resolveRepoRoot(toolInput) || null,
1270
+ helperFiles,
1271
+ packageScriptTouched,
1272
+ reasons,
1273
+ };
1274
+ trackAction(HELPER_BYPASS_ACTION, metadata);
1275
+ return metadata;
1276
+ }
1277
+
1278
+ function evaluateStatefulHelperBypassGate(toolName, toolInput = {}) {
1279
+ if (process.env.THUMBGATE_HELPER_BYPASS_GUARD === '0') return null;
1280
+ if (toolName !== 'Bash') {
1281
+ recordHelperScriptWrite(toolName, toolInput);
1282
+ return null;
1283
+ }
1284
+
1285
+ const command = String(toolInput.command || '').trim();
1286
+ if (!command) return null;
1287
+
1288
+ if (DOWNLOAD_EXEC_CHAIN_PATTERN.test(command)) {
1289
+ return {
1290
+ decision: 'deny',
1291
+ gate: 'stateful-helper-script-bypass',
1292
+ message: 'Download-then-execute chains are blocked before the real action moves below the visible tool call.',
1293
+ severity: 'critical',
1294
+ reasoning: [
1295
+ `Command matched download/execute chain: ${command.slice(0, 180)}`,
1296
+ 'PreToolUse must review the whole action chain, not only the first low-risk command',
1297
+ ],
1298
+ };
1299
+ }
1300
+
1301
+ const writeMetadata = recordHelperScriptWrite(toolName, toolInput);
1302
+ const actions = listSessionActions();
1303
+ const recentWrite = actions[HELPER_BYPASS_ACTION];
1304
+ const recentMetadata = recentWrite && recentWrite.metadata && typeof recentWrite.metadata === 'object'
1305
+ ? recentWrite.metadata
1306
+ : null;
1307
+
1308
+ const scriptMatch = command.match(PACKAGE_RUN_PATTERN);
1309
+ const scriptName = scriptMatch ? scriptMatch[1] : '';
1310
+ const repoRoot = resolveRepoRoot(toolInput);
1311
+ const packageScript = scriptName ? readPackageScript(repoRoot, scriptName) : '';
1312
+ const commandBoundaryReasons = helperRiskReasons(`${command}\n${packageScript}`);
1313
+ const executesRecentHelper = HELPER_EXEC_PATTERN.test(command);
1314
+ const runsRecentPackageScript = Boolean(scriptName && recentMetadata && recentMetadata.packageScriptTouched);
1315
+ const recentReasons = recentMetadata && Array.isArray(recentMetadata.reasons) ? recentMetadata.reasons : [];
1316
+ const writeRiskReasons = writeMetadata && Array.isArray(writeMetadata.reasons) ? writeMetadata.reasons : [];
1317
+ const correlatedReasons = [...new Set([...recentReasons, ...writeRiskReasons, ...commandBoundaryReasons])];
1318
+
1319
+ if (
1320
+ (executesRecentHelper || runsRecentPackageScript) &&
1321
+ (recentMetadata || writeMetadata) &&
1322
+ correlatedReasons.length > 0
1323
+ ) {
1324
+ const target = scriptName ? `package script "${scriptName}"` : 'recent helper script';
1325
+ return {
1326
+ decision: 'deny',
1327
+ gate: 'stateful-helper-script-bypass',
1328
+ message: `A recently modified ${target} now crosses a risky boundary. Review it or constrain cwd, network, process, and writable paths before execution.`,
1329
+ severity: 'critical',
1330
+ reasoning: [
1331
+ `Recent helper/package modification: ${(recentMetadata && recentMetadata.helperFiles || []).join(', ') || (recentMetadata && recentMetadata.packageScriptTouched ? 'package.json scripts' : 'current command write')}`,
1332
+ `Execution command: ${command.slice(0, 180)}`,
1333
+ `Risk reasons: ${correlatedReasons.join(', ')}`,
1334
+ 'This blocks the helper-script/package-script bypass class raised in external review',
1335
+ ],
1336
+ };
1337
+ }
1338
+
1339
+ return null;
1340
+ }
1341
+
1198
1342
  function recordStructuralGateBlock(toolName, toolInput, result) {
1199
1343
  recordStat(result.gate, 'block', null, { toolName, toolInput });
1200
1344
  const auditRecord = recordAuditEvent({
@@ -1454,8 +1598,19 @@ function checkWhenClause(when, constraints) {
1454
1598
  if (!when || !when.constraints) return true;
1455
1599
 
1456
1600
  for (const [key, expectedValue] of Object.entries(when.constraints)) {
1457
- const constraint = constraints[key];
1458
- if (!constraint || constraint.value !== expectedValue) {
1601
+ let value;
1602
+ if (key === 'careful_mode') {
1603
+ const isEnvTrue = process.env.THUMBGATE_CAREFUL_MODE === '1' ||
1604
+ String(process.env.THUMBGATE_CAREFUL_MODE).toLowerCase() === 'true';
1605
+ value = isEnvTrue || (constraints[key] && constraints[key].value === expectedValue);
1606
+ } else if (key === 'freeze_mode') {
1607
+ value = Boolean(process.env.THUMBGATE_FREEZE_PATHS) ||
1608
+ (constraints[key] && constraints[key].value !== false && constraints[key].value !== null && constraints[key].value !== undefined);
1609
+ } else {
1610
+ const constraint = constraints[key];
1611
+ value = constraint && constraint.value === expectedValue;
1612
+ }
1613
+ if (!value) {
1459
1614
  return false;
1460
1615
  }
1461
1616
  }
@@ -1503,6 +1658,29 @@ function matchGate(gate, toolName, toolInput = {}) {
1503
1658
  const affectedFiles = affected.files;
1504
1659
  const repoRoot = affected.repoRoot;
1505
1660
  const governanceState = loadGovernanceState();
1661
+ const constraints = loadConstraints();
1662
+
1663
+ if (gate.id === 'on-demand-freeze-mode' || (gate.when && gate.when.constraints && gate.when.constraints.freeze_mode)) {
1664
+ let freezePaths = [];
1665
+ if (process.env.THUMBGATE_FREEZE_PATHS) {
1666
+ freezePaths = process.env.THUMBGATE_FREEZE_PATHS.split(',').map(p => p.trim()).filter(Boolean);
1667
+ } else if (constraints.freeze_mode && typeof constraints.freeze_mode.value === 'string') {
1668
+ freezePaths = constraints.freeze_mode.value.split(',').map(p => p.trim()).filter(Boolean);
1669
+ } else if (constraints.freeze_mode && Array.isArray(constraints.freeze_mode.value)) {
1670
+ freezePaths = constraints.freeze_mode.value;
1671
+ } else if (governanceState.taskScope && Array.isArray(governanceState.taskScope.allowedPaths)) {
1672
+ freezePaths = governanceState.taskScope.allowedPaths;
1673
+ }
1674
+
1675
+ if (freezePaths.length > 0) {
1676
+ const outsideFiles = affectedFiles.filter((filePath) => !matchesAnyGlob(filePath, freezePaths));
1677
+ if (outsideFiles.length > 0) {
1678
+ return { matched: true, matchText, affectedFiles };
1679
+ } else {
1680
+ return { matched: false, matchText, affectedFiles };
1681
+ }
1682
+ }
1683
+ }
1506
1684
 
1507
1685
  if (Array.isArray(gate.toolNames) && gate.toolNames.length > 0 && !gate.toolNames.includes(toolName)) {
1508
1686
  return { matched: false, matchText, affectedFiles };
@@ -1838,6 +2016,10 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1838
2016
  if (localOnlyRemoteSideEffectGate) {
1839
2017
  return recordStructuralGateBlock(toolName, toolInput, localOnlyRemoteSideEffectGate);
1840
2018
  }
2019
+ const statefulHelperBypassGate = evaluateStatefulHelperBypassGate(toolName, toolInput);
2020
+ if (statefulHelperBypassGate) {
2021
+ return recordStructuralGateBlock(toolName, toolInput, statefulHelperBypassGate);
2022
+ }
1841
2023
  if (isBreakGlassSettingsRecoveryAction(toolName, toolInput)) {
1842
2024
  recordAuditEvent({
1843
2025
  toolName,
@@ -2065,6 +2247,10 @@ function evaluateGates(toolName, toolInput, configPath) {
2065
2247
  if (localOnlyRemoteSideEffectGate) {
2066
2248
  return recordStructuralGateBlock(toolName, toolInput, localOnlyRemoteSideEffectGate);
2067
2249
  }
2250
+ const statefulHelperBypassGate = evaluateStatefulHelperBypassGate(toolName, toolInput);
2251
+ if (statefulHelperBypassGate) {
2252
+ return recordStructuralGateBlock(toolName, toolInput, statefulHelperBypassGate);
2253
+ }
2068
2254
  if (isBreakGlassSettingsRecoveryAction(toolName, toolInput)) {
2069
2255
  recordAuditEvent({
2070
2256
  toolName,
@@ -2707,6 +2893,9 @@ function extractActionContext(toolName, toolInput) {
2707
2893
  const parts = [toolName];
2708
2894
  if (toolInput.command) parts.push(String(toolInput.command).slice(0, 400));
2709
2895
  if (toolInput.file_path) parts.push(String(toolInput.file_path));
2896
+ if (toolInput.content) parts.push(String(toolInput.content).slice(0, 600));
2897
+ if (toolInput.new_string) parts.push(String(toolInput.new_string).slice(0, 600));
2898
+ if (toolInput.old_string) parts.push(String(toolInput.old_string).slice(0, 240));
2710
2899
  if (toolInput.description) parts.push(String(toolInput.description).slice(0, 200));
2711
2900
  if (toolInput.prompt) parts.push(String(toolInput.prompt).slice(0, 400));
2712
2901
  if (toolInput.pattern) parts.push(String(toolInput.pattern).slice(0, 200));
@@ -3149,9 +3338,12 @@ module.exports = {
3149
3338
  getLocalOnlyScopeSources,
3150
3339
  isRemoteSideEffectCommand,
3151
3340
  evaluateLocalOnlyRemoteSideEffectGate,
3341
+ recordHelperScriptWrite,
3342
+ evaluateStatefulHelperBypassGate,
3152
3343
  isAgentHookSettingsFile,
3153
3344
  isBreakGlassSettingsRecoveryAction,
3154
3345
  PR_THREAD_RESOLUTION_ACTION,
3346
+ HELPER_BYPASS_ACTION,
3155
3347
  buildBlockActionProCta,
3156
3348
  applyDailyBlockCap,
3157
3349
  getTodayBlockCount,
@@ -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.',
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Stop hook: anti-claim enforcement.
6
+ *
7
+ * Scans the assistant's most recent turn (assistant text + same-turn tool_use
8
+ * blocks) and blocks the "deployed / live / done / fixed / ready" claim
9
+ * unless that same turn included a proof tool call (curl / grep / test).
10
+ *
11
+ * Why: CLAUDE.md anti-lying directive ("Never claim fix done until
12
+ * committed+pushed. Never claim 'ready' without running e2e.") was
13
+ * aspirational, not enforced. Per CEO 2026-05-13 feedback after a session in
14
+ * which 5+ unverified claims slipped through, this is the harness-level
15
+ * enforcement that ends the recurring trust-burn pattern. ThumbGate-on-
16
+ * ThumbGate dogfood — we are the prevention-rule generator and a perfect
17
+ * customer for our own gate.
18
+ *
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.
23
+ *
24
+ * Stdin: Claude Code passes the hook payload as JSON on stdin. We read
25
+ * `transcript_path` to locate the JSONL session log and scan the last
26
+ * assistant message.
27
+ *
28
+ * Stdout: any text printed is shown to the agent on the next turn.
29
+ */
30
+
31
+ const fs = require('node:fs');
32
+
33
+ // Lie-phrase patterns. These match common "claim of completion" wording
34
+ // the agent emits without verification. Word-boundary anchored to avoid
35
+ // false positives ("ready-made", "live-streaming", etc).
36
+ const CLAIM_PATTERNS = [
37
+ /\bis\s+live\b/i,
38
+ /\bnow\s+live\b/i,
39
+ /\bgoing\s+live\b/i,
40
+ /\bdeployed\b(?!\s*(yet|to\s+staging|on\s+a\s+branch))/i,
41
+ /\b(?:is|are|it'?s)\s+(?:now\s+)?(?:fully\s+)?(?:fixed|resolved|merged|shipped)\b/i,
42
+ /\bproduction[-\s]ready\b/i,
43
+ /\beverything\s+(?:is\s+)?(?:done|working|ready)\b/i,
44
+ /\b(?:github|repo|repository)\s+(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b/i,
45
+ /\b(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b.*\b(?:github|repo|repository)\b/i,
46
+ /\b(?:money|payment|charge|checkout|revenue|price|pricing|invoice|billing|tax|sales tax|inventory|stock|permission|access|customer[-\s]facing)\b.*\b(?:correct|accurate|verified|valid|matches|working|fixed|resolved|calculated|configured)\b/i,
47
+ /\b(?:correct|accurate|verified|valid|matches|working|fixed|resolved|calculated|configured)\b.*\b(?:money|payment|charge|checkout|revenue|price|pricing|invoice|billing|tax|sales tax|inventory|stock|permission|access|customer[-\s]facing)\b/i,
48
+ // Added 2026-06-11 after a cross-project failure analysis: these completion
49
+ // claims ("all green / stable / verified / race over / tests pass") were
50
+ // asserted without proof and slipped past the original set. The proof-gate
51
+ // below suppresses them whenever the SAME turn ran a verification tool, so a
52
+ // "verified" claim backed by a test/curl/Read stays silent.
53
+ /\ball\s+(?:the\s+)?(?:tests?\s+|checks?\s+)?(?:are\s+)?green\b/i,
54
+ /\b(?:all\s+)?(?:tests?|checks?|ci)\s+(?:are\s+)?(?:now\s+)?passing\b/i,
55
+ /\ball\s+(?:tests?|checks?)\s+pass(?:ed)?\b/i,
56
+ /\bverified\b/i,
57
+ /\bconfirmed\b/i,
58
+ /\b(?:is|are|it'?s|now)\s+stable\b/i,
59
+ /\ball\s+clear\b/i,
60
+ /\bgood\s+to\s+go\b/i,
61
+ /\brace\s+(?:is\s+)?over\b/i,
62
+ /\bno\s+longer\s+racing\b/i,
63
+ ];
64
+
65
+ // Proof-of-verification patterns. If the SAME turn included one of these
66
+ // tool calls or shell command tokens, the claim is considered backed and
67
+ // the hook stays silent.
68
+ const PROOF_PATTERNS = [
69
+ /\bcurl\b/,
70
+ /\bgh\s+pr\s+(?:view|checks|status)\b/,
71
+ /\bgh\s+run\s+view\b/,
72
+ /\bgh\s+api\b/,
73
+ /\bnode\s+--test\b/,
74
+ /\bnpm\s+(?:run\s+)?test\b/,
75
+ /\bnpm\s+pack\b/,
76
+ /\bjest\b/,
77
+ /\bmocha\b/,
78
+ /\bpytest\b/,
79
+ /\bplaywright\b/,
80
+ /\bgrep\b/,
81
+ /\bstripe\b/,
82
+ /\bplaid\b/,
83
+ /\bshopify\b/,
84
+ /\bsquare\b/,
85
+ /\bquickbooks\b/,
86
+ /Read\s*\(/, // Claude Code Read tool call
87
+ /Bash\s*\(/, // Claude Code Bash tool call
88
+ ];
89
+
90
+ function readLastAssistantTurn(transcriptPath) {
91
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return null;
92
+ let content;
93
+ try {
94
+ content = fs.readFileSync(transcriptPath, 'utf8');
95
+ } catch {
96
+ return null;
97
+ }
98
+ const lines = content.trim().split('\n');
99
+ // Walk backwards to find the last assistant message
100
+ for (let i = lines.length - 1; i >= 0; i--) {
101
+ const raw = lines[i].trim();
102
+ if (!raw) continue;
103
+ let entry;
104
+ try {
105
+ entry = JSON.parse(raw);
106
+ } catch {
107
+ continue;
108
+ }
109
+ if (entry.type === 'assistant' && entry.message) {
110
+ return entry.message;
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+
116
+ function extractText(message) {
117
+ if (!message || !Array.isArray(message.content)) return '';
118
+ return message.content
119
+ .filter((b) => b && typeof b.text === 'string')
120
+ .map((b) => b.text)
121
+ .join('\n');
122
+ }
123
+
124
+ function extractToolUseSummary(message) {
125
+ if (!message || !Array.isArray(message.content)) return '';
126
+ return message.content
127
+ .filter((b) => b?.type === 'tool_use')
128
+ .map((b) => {
129
+ const name = b.name || 'tool';
130
+ let summary = '';
131
+ if (b.input && typeof b.input === 'object') {
132
+ if (typeof b.input.command === 'string') summary = b.input.command;
133
+ else if (typeof b.input.file_path === 'string') summary = b.input.file_path;
134
+ else if (typeof b.input.query === 'string') summary = b.input.query;
135
+ else summary = JSON.stringify(b.input).slice(0, 200);
136
+ }
137
+ return `${name}: ${summary}`;
138
+ })
139
+ .join('\n');
140
+ }
141
+
142
+ function findClaim(text) {
143
+ for (const p of CLAIM_PATTERNS) {
144
+ const m = text.match(p);
145
+ if (m) return m[0];
146
+ }
147
+ return null;
148
+ }
149
+
150
+ function hasProof(combined) {
151
+ return PROOF_PATTERNS.some((p) => p.test(combined));
152
+ }
153
+
154
+ function readStdinSync() {
155
+ try {
156
+ return fs.readFileSync(0, 'utf8');
157
+ } catch {
158
+ return '';
159
+ }
160
+ }
161
+
162
+ function main() {
163
+ const raw = readStdinSync();
164
+ let payload = {};
165
+ try {
166
+ payload = raw ? JSON.parse(raw) : {};
167
+ } catch {
168
+ payload = {};
169
+ }
170
+
171
+ const transcriptPath = payload.transcript_path || process.env.CLAUDE_TRANSCRIPT_PATH;
172
+ const message = readLastAssistantTurn(transcriptPath);
173
+ if (!message) return; // no transcript visible; nothing to check
174
+
175
+ const text = extractText(message);
176
+ const toolUseSummary = extractToolUseSummary(message);
177
+ const claim = findClaim(text);
178
+ if (!claim) return; // no completion claim made; silent
179
+
180
+ const proofText = `${text}\n${toolUseSummary}`;
181
+ if (hasProof(proofText)) return; // claim backed by proof in same turn
182
+
183
+ // Strict mode (THUMBGATE_STRICT_ENFORCEMENT=1): hard-block the stop. Emit a
184
+ // Stop-hook block decision so Claude Code does NOT end the turn — the agent
185
+ // must run the verification (or retract) before it can stop. Default mode
186
+ // stays soft (a reminder for the next turn) so we don't break existing wiring.
187
+ if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') {
188
+ 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.`;
189
+ process.stdout.write(JSON.stringify({ decision: 'block', reason }) + '\n');
190
+ return;
191
+ }
192
+
193
+ // Default (soft): surface a system reminder for the NEXT turn. Do not hard-block.
194
+ const reminder = [
195
+ '⚠️ ThumbGate anti-claim gate: previous turn claimed completion',
196
+ ` ("${claim}") without a proof tool call in the same message.`,
197
+ ' Per CLAUDE.md anti-lying: never claim "done / live / deployed / fixed /',
198
+ ' verified / all green / stable" or commercial truth (money / tax / inventory /',
199
+ ' permissions / customer-facing state) without curl / grep / test / source-of-truth output in the SAME turn.',
200
+ ' If the work really is verified, re-state the claim with the proof.',
201
+ ' If not, retract and run the verification before re-asserting.',
202
+ ].join('\n');
203
+ process.stdout.write(reminder + '\n');
204
+ }
205
+
206
+ // Path-resolve check instead of `require.main === module`. SonarCloud's
207
+ // strict type inference (rule S3403) flags the === form as always-false
208
+ // in CommonJS, and CLAUDE.md "Hard-Won Lessons" pins the path-based form
209
+ // as the portable fix (incident 2026-04-21 / PR #1115). Resolve BOTH sides
210
+ // so the comparison is between two normalized absolute paths.
211
+ const path = require('node:path');
212
+ if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
213
+ try {
214
+ main();
215
+ } catch {
216
+ // never crash the agent
217
+ }
218
+ }
219
+
220
+ module.exports = {
221
+ CLAIM_PATTERNS,
222
+ PROOF_PATTERNS,
223
+ findClaim,
224
+ hasProof,
225
+ extractText,
226
+ extractToolUseSummary,
227
+ };
@@ -6,11 +6,27 @@
6
6
  * Also used directly by the CLI to refresh statusline counters after feedback capture.
7
7
  */
8
8
 
9
- const fs = require('fs');
10
- const path = require('path');
9
+ const fs = require('node:fs');
10
+ const os = require('node:os');
11
+ const path = require('node:path');
11
12
  const { resolveFeedbackDir } = require('./feedback-paths');
13
+ const {
14
+ getAggregateStatuslineCachePath,
15
+ shouldAggregateFeedback,
16
+ } = require('./feedback-aggregate');
12
17
 
13
18
  function getCachePath() {
19
+ const explicitFeedbackDir = process.env.THUMBGATE_FEEDBACK_DIR;
20
+ if (shouldAggregateFeedback() && explicitFeedbackDir) {
21
+ try {
22
+ if (path.resolve(explicitFeedbackDir).startsWith(path.resolve(os.tmpdir()) + path.sep)) {
23
+ return path.join(resolveFeedbackDir(), 'statusline_cache.json');
24
+ }
25
+ } catch {
26
+ return path.join(resolveFeedbackDir(), 'statusline_cache.json');
27
+ }
28
+ }
29
+ if (shouldAggregateFeedback()) return getAggregateStatuslineCachePath();
14
30
  return path.join(resolveFeedbackDir(), 'statusline_cache.json');
15
31
  }
16
32
 
@@ -18,6 +18,7 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { resolveFeedbackDir } = require('./feedback-paths');
20
20
  const { ensureParentDir, readJsonl } = require('./fs-utils');
21
+ const { redactSecretsDeep } = require('./secret-redaction');
21
22
  const {
22
23
  buildStableId,
23
24
  extractFilePaths,
@@ -137,15 +138,19 @@ function createLesson({ feedbackId, signal, inferredLesson, triggerMessage, prio
137
138
  // Stable link: dashboard deep-link to this lesson
138
139
  lesson.link = `${getLessonBaseUrl()}/lessons#${lesson.id}`;
139
140
 
141
+ // Redact secrets before persisting — lesson text is derived from conversation content that may
142
+ // have captured a pasted credential. See scripts/secret-redaction.js.
143
+ const redactedLesson = redactSecretsDeep(lesson);
144
+
140
145
  const lessonsPath = getLessonsPath();
141
146
  ensureParentDir(lessonsPath);
142
- fs.appendFileSync(lessonsPath, JSON.stringify(lesson) + '\n');
147
+ fs.appendFileSync(lessonsPath, JSON.stringify(redactedLesson) + '\n');
143
148
 
144
149
  // Update recent lesson for statusbar
145
150
  const recentPath = getRecentLessonPath();
146
- fs.writeFileSync(recentPath, JSON.stringify(lesson, null, 2) + '\n');
151
+ fs.writeFileSync(recentPath, JSON.stringify(redactedLesson, null, 2) + '\n');
147
152
 
148
- return lesson;
153
+ return redactedLesson;
149
154
  }
150
155
 
151
156
  /**
@@ -2,6 +2,7 @@
2
2
 
3
3
  const path = require('node:path');
4
4
  const { readJSONL, getFeedbackPaths } = require('./feedback-loop');
5
+ const { buildMemoryLifecycleView, scoreHybridMemoryMatch } = require('./agent-memory-lifecycle');
5
6
  const { loadOptionalModule } = require('./private-core-boundary');
6
7
 
7
8
  const HIGH_RISK_TAGS = new Set([
@@ -411,7 +412,8 @@ function scoreLesson(queryText, memory, parsed, sourceFeedback) {
411
412
  const score = jaccardSimilarity(queryTokens, lessonTokens)
412
413
  + substringBoost(queryText, lessonText)
413
414
  + recencyScore(memory.timestamp)
414
- + (memory.category === 'error' ? 0.05 : 0);
415
+ + (memory.category === 'error' ? 0.05 : 0)
416
+ + Math.min(0.2, scoreHybridMemoryMatch(queryText, memory).score * 0.1);
415
417
 
416
418
  return {
417
419
  score,
@@ -427,6 +429,7 @@ function buildLessonResult(memory, sourceFeedback, options = {}) {
427
429
  const { score, matchedTokens } = scoreLesson(options.query || '', memory, parsed, sourceFeedback);
428
430
  const harnessRecommendations = buildHarnessRecommendations(memory, parsed, sourceFeedback, ruleMatches, gateMatches);
429
431
  const lifecycle = buildLifecycle(memory, parsed, sourceFeedback, ruleMatches, gateMatches, harnessRecommendations);
432
+ const memoryLifecycle = buildMemoryLifecycleView(memory, { query: options.query || '' });
430
433
 
431
434
  return {
432
435
  id: memory.id,
@@ -452,6 +455,7 @@ function buildLessonResult(memory, sourceFeedback, options = {}) {
452
455
  systemResponse: {
453
456
  promotedToMemory: true,
454
457
  lifecycle,
458
+ memoryLifecycle,
455
459
  diagnosis: memory.diagnosis || null,
456
460
  sourceFeedback: sourceFeedback
457
461
  ? {
@@ -589,6 +593,14 @@ function tryFts5Search(query, options) {
589
593
  tags: row.tags,
590
594
  importance: row.importance,
591
595
  timestamp: row.timestamp,
596
+ memoryLifecycle: buildMemoryLifecycleView({
597
+ title: row.context,
598
+ content: [row.whatWentWrong, row.whatToChange, row.whatWorked].filter(Boolean).join('\n'),
599
+ domain: row.domain,
600
+ tags: row.tags,
601
+ importance: row.importance,
602
+ timestamp: row.timestamp,
603
+ }, { query: query || '' }),
592
604
  })),
593
605
  backend: 'sqlite-fts5',
594
606
  };
@@ -618,6 +630,10 @@ function formatLessonSearchResults(payload) {
618
630
  payload.results.forEach((result, index) => {
619
631
  lines.push(`${index + 1}. ${result.title}`);
620
632
  lines.push(` Category: ${result.category} | Tags: ${result.tags.join(', ') || 'none'} | Score: ${result.score}`);
633
+ if (result.systemResponse.memoryLifecycle) {
634
+ const memoryLifecycle = result.systemResponse.memoryLifecycle;
635
+ lines.push(` Memory: scope=${memoryLifecycle.scope} | decay=${memoryLifecycle.decay.state} | hybrid=${memoryLifecycle.retrievalHints.hybridScore}`);
636
+ }
621
637
  if (result.lesson.summary) {
622
638
  lines.push(` Lesson: ${result.lesson.summary}`);
623
639
  }