thumbgate 1.27.17 → 1.27.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/.claude-plugin/marketplace.json +85 -0
  2. package/.claude-plugin/plugin.json +3 -4
  3. package/.well-known/llms.txt +12 -33
  4. package/.well-known/mcp/server-card.json +8 -8
  5. package/README.md +30 -221
  6. package/adapters/chatgpt/openapi.yaml +1695 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/gemini/function-declarations.json +0 -1
  9. package/adapters/mcp/server-stdio.js +11 -263
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bench/thumbgate-bench.json +2 -2
  12. package/bin/cli.js +122 -1421
  13. package/bin/postinstall.js +27 -11
  14. package/config/gate-templates.json +0 -216
  15. package/config/gates/claim-verification.json +0 -12
  16. package/config/gates/default.json +2 -31
  17. package/config/github-about.json +2 -2
  18. package/config/mcp-allowlists.json +13 -23
  19. package/config/merge-quality-checks.json +1 -0
  20. package/config/model-candidates.json +6 -121
  21. package/openapi/openapi.yaml +0 -12
  22. package/package.json +99 -229
  23. package/public/blog.html +4 -4
  24. package/public/codex-plugin.html +20 -72
  25. package/public/compare.html +8 -31
  26. package/public/dashboard.html +166 -930
  27. package/public/federal.html +2 -2
  28. package/public/guide.html +9 -33
  29. package/public/index.html +116 -495
  30. package/public/learn.html +18 -191
  31. package/public/lessons.html +10 -168
  32. package/public/numbers.html +7 -7
  33. package/public/pro.html +11 -34
  34. package/scripts/agent-memory-lifecycle.js +0 -211
  35. package/scripts/agent-readiness.js +3 -167
  36. package/scripts/agent-reward-model.js +1 -53
  37. package/scripts/auto-promote-gates.js +10 -82
  38. package/scripts/auto-wire-hooks.js +0 -14
  39. package/scripts/billing.js +1 -93
  40. package/scripts/build-metadata.js +10 -50
  41. package/scripts/cli-feedback.js +6 -18
  42. package/scripts/cli-schema.js +0 -90
  43. package/scripts/cli-telemetry.js +1 -6
  44. package/scripts/commercial-offer.js +2 -82
  45. package/scripts/context-manager.js +6 -74
  46. package/scripts/dashboard.js +2 -68
  47. package/scripts/durability/step.js +3 -3
  48. package/scripts/export-databricks-bundle.js +1 -5
  49. package/scripts/export-dpo-pairs.js +2 -7
  50. package/scripts/feedback-loop.js +1 -145
  51. package/scripts/feedback-quality.js +0 -87
  52. package/scripts/filesystem-search.js +10 -35
  53. package/scripts/gate-stats.js +0 -89
  54. package/scripts/gates-engine.js +85 -1105
  55. package/scripts/gemini-embedding-policy.js +1 -2
  56. package/scripts/hook-runtime.js +14 -20
  57. package/scripts/hook-thumbgate-cache-updater.js +2 -18
  58. package/scripts/hybrid-feedback-context.js +7 -142
  59. package/scripts/lesson-inference.js +3 -8
  60. package/scripts/lesson-search.js +1 -17
  61. package/scripts/license.js +1 -0
  62. package/scripts/llm-client.js +9 -390
  63. package/scripts/local-model-profile.js +8 -15
  64. package/scripts/memory-scope-readiness.js +0 -159
  65. package/scripts/meta-agent-loop.js +0 -36
  66. package/scripts/operational-integrity.js +5 -39
  67. package/scripts/oss-pr-opportunity-scout.js +5 -35
  68. package/scripts/plausible-server-events.js +6 -9
  69. package/scripts/pro-local-dashboard.js +4 -4
  70. package/scripts/proxy-pointer-rag-guardrails.js +1 -42
  71. package/scripts/rate-limiter.js +13 -64
  72. package/scripts/secret-scanner.js +5 -44
  73. package/scripts/security-scanner.js +10 -260
  74. package/scripts/self-distill-agent.js +1 -3
  75. package/scripts/seo-gsd.js +2 -916
  76. package/scripts/statusline-cache-path.js +2 -17
  77. package/scripts/statusline-local-stats.js +1 -9
  78. package/scripts/statusline-meta.js +2 -28
  79. package/scripts/statusline.sh +4 -20
  80. package/scripts/telemetry-analytics.js +0 -357
  81. package/scripts/thompson-sampling.js +10 -31
  82. package/scripts/thumbgate-bench.js +1 -16
  83. package/scripts/thumbgate-search.js +19 -85
  84. package/scripts/tool-registry.js +1 -169
  85. package/scripts/vector-store.js +0 -45
  86. package/scripts/workflow-sentinel.js +53 -286
  87. package/scripts/workspace-evolver.js +2 -62
  88. package/src/api/server.js +290 -2578
  89. package/.claude/commands/dashboard.md +0 -15
  90. package/.claude/commands/thumbgate-blocked.md +0 -27
  91. package/.claude/commands/thumbgate-dashboard.md +0 -15
  92. package/.claude/commands/thumbgate-doctor.md +0 -30
  93. package/.claude/commands/thumbgate-guard.md +0 -36
  94. package/.claude/commands/thumbgate-protect.md +0 -30
  95. package/.claude/commands/thumbgate-rules.md +0 -30
  96. package/.well-known/agentic-verify.txt +0 -1
  97. package/adapters/gcp/dfcx-webhook-gate.js +0 -295
  98. package/adapters/letta/README.md +0 -41
  99. package/adapters/letta/thumbgate-letta-adapter.js +0 -133
  100. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  101. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  102. package/bench/observability-eval-suite.json +0 -26
  103. package/bin/dashboard-cli.js +0 -7
  104. package/commands/dashboard.md +0 -15
  105. package/commands/thumbgate-dashboard.md +0 -15
  106. package/config/builtin-lessons.json +0 -23
  107. package/config/gate-classifier-routing.json +0 -98
  108. package/config/post-deploy-marketing-pages.json +0 -61
  109. package/public/about.html +0 -162
  110. package/public/agent-manager.html +0 -179
  111. package/public/agents-cost-savings.html +0 -153
  112. package/public/ai-malpractice-prevention.html +0 -818
  113. package/public/assets/brand/github-social-preview.png +0 -0
  114. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  115. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  116. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  117. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  118. package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
  119. package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
  120. package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
  121. package/public/assets/brand/thumbgate-mark-team.svg +0 -26
  122. package/public/assets/brand/thumbgate-mark.svg +0 -21
  123. package/public/assets/brand/thumbgate-wordmark.svg +0 -20
  124. package/public/assets/claude-thumbgate-statusbar.svg +0 -8
  125. package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
  126. package/public/assets/legal-intake-control-flow.svg +0 -66
  127. package/public/brand/thumbgate-mark.svg +0 -19
  128. package/public/brand/thumbgate-og.svg +0 -16
  129. package/public/chatgpt-app.html +0 -330
  130. package/public/codex-enterprise.html +0 -123
  131. package/public/js/buyer-intent.js +0 -672
  132. package/public/pricing.html +0 -399
  133. package/scripts/action-receipts.js +0 -324
  134. package/scripts/activation-quickstart.js +0 -187
  135. package/scripts/agent-operations-planner.js +0 -621
  136. package/scripts/ai-component-inventory.js +0 -367
  137. package/scripts/async-eval-observability.js +0 -236
  138. package/scripts/audit.js +0 -65
  139. package/scripts/aws-blocks-guardrails.js +0 -228
  140. package/scripts/classifier-routing.js +0 -130
  141. package/scripts/dashboard-chat.js +0 -332
  142. package/scripts/feedback-aggregate.js +0 -281
  143. package/scripts/feedback-sanitizer.js +0 -105
  144. package/scripts/hob-pack.js +0 -591
  145. package/scripts/hook-stop-anti-claim.js +0 -301
  146. package/scripts/install-shim.js +0 -87
  147. package/scripts/mcp-oauth.js +0 -293
  148. package/scripts/noop-detect.js +0 -285
  149. package/scripts/omlx-smoke.js +0 -192
  150. package/scripts/parallel-workflow-orchestrator.js +0 -293
  151. package/scripts/plan-gate.js +0 -243
  152. package/scripts/plausible-domain-config.js +0 -99
  153. package/scripts/qa-scenario-planner.js +0 -136
  154. package/scripts/repeat-metric.js +0 -137
  155. package/scripts/secret-fixture-tokens.js +0 -61
  156. package/scripts/secret-redaction.js +0 -166
  157. package/scripts/self-harness-optimizer.js +0 -141
  158. package/scripts/self-healing-check.js +0 -193
  159. package/scripts/silent-failure-cluster.js +0 -531
  160. package/scripts/statusline-cache-read.js +0 -57
  161. package/scripts/sync-telemetry-from-prod.js +0 -374
  162. package/scripts/tool-contract-validator.js +0 -76
  163. package/scripts/trajectory-scorer.js +0 -63
  164. package/scripts/verify-marketing-pages-deployed.js +0 -195
  165. package/scripts/visitor-journey.js +0 -172
@@ -3,12 +3,11 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const os = require('os');
7
6
  const crypto = require('crypto');
8
- const { execFileSync } = require('child_process');
7
+ const { execSync, execFileSync } = require('child_process');
9
8
  const { loadOptionalModule } = require('./private-core-boundary');
10
9
 
11
- const { isProTier, isInTrialPeriod, FREE_TIER_MAX_GATES, FREE_TIER_DAILY_BLOCKS, todayKey } = require('./rate-limiter');
10
+ const { isProTier, FREE_TIER_MAX_GATES } = require('./rate-limiter');
12
11
  const {
13
12
  DEFAULT_BASE_BRANCH,
14
13
  evaluateOperationalIntegrity,
@@ -20,10 +19,6 @@ const {
20
19
  recordDecisionEvaluation,
21
20
  recordDecisionOutcome,
22
21
  } = require('./decision-journal');
23
- const {
24
- actionFingerprint,
25
- sanitizeFeedbackText,
26
- } = require('./feedback-sanitizer');
27
22
 
28
23
  /**
29
24
  * Computes the SHA-256 hash of an executable binary to prevent path-based bypasses.
@@ -35,12 +30,10 @@ function computeExecutableHash(command) {
35
30
  const firstWord = command.trim().split(/\s+/)[0];
36
31
  if (!firstWord) return null;
37
32
 
38
- // Resolve absolute path using 'which'. Use execFileSync (no shell) and pass
39
- // firstWord as an argv element, never interpolated into a command string, so
40
- // a hostile `command` value cannot inject shell metacharacters here.
33
+ // Resolve absolute path using 'which'
41
34
  let fullPath;
42
35
  try {
43
- fullPath = execFileSync('which', [firstWord], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
36
+ fullPath = execSync(`which ${firstWord}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
44
37
  } catch (e) {
45
38
  // If 'which' fails, it might be an absolute path or a non-existent command
46
39
  fullPath = path.isAbsolute(firstWord) ? firstWord : null;
@@ -58,44 +51,21 @@ const {
58
51
  scanHookInput,
59
52
  buildSafeSummary,
60
53
  redactText,
61
- isSafeSecretStorageWrite,
62
- SAFE_SECRET_STORAGE_DIRS,
63
54
  } = require('./secret-scanner');
64
55
  const {
65
56
  evaluateSecurityScan,
66
57
  } = require('./security-scanner');
67
- const { evaluatePlanGate } = require('./plan-gate');
68
- const { getTrajectoryScore } = require('./trajectory-scorer');
69
- const { evaluateSequenceState } = loadOptionalModule('./sequence-guard', () => ({
70
- evaluateSequenceState: () => null,
71
- }));
72
58
  const { getAutoGatesPath } = require('./auto-promote-gates');
73
59
  const { recordAuditEvent, auditToFeedback } = require('./audit-trail');
74
60
 
75
61
  const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', 'config', 'gates', 'default.json');
76
62
  const DEFAULT_CLAIM_GATES_PATH = path.join(__dirname, '..', 'config', 'gates', 'claim-verification.json');
77
-
78
- function resolveThumbgateStateDir() {
79
- if (process.env.THUMBGATE_STATE_DIR) return process.env.THUMBGATE_STATE_DIR;
80
-
81
- if (process.env.XDG_STATE_HOME) {
82
- return path.join(process.env.XDG_STATE_HOME, 'thumbgate');
83
- }
84
-
85
- if (process.env.CODEX_SANDBOX) {
86
- return path.join(os.tmpdir(), 'thumbgate');
87
- }
88
-
89
- return path.join(process.env.HOME || os.tmpdir(), '.thumbgate');
90
- }
91
-
92
- const STATE_DIR = resolveThumbgateStateDir();
93
- const STATE_PATH = path.join(STATE_DIR, 'gate-state.json');
94
- const CONSTRAINTS_PATH = path.join(STATE_DIR, 'session-constraints.json');
95
- const STATS_PATH = path.join(STATE_DIR, 'gate-stats.json');
96
- const SESSION_ACTIONS_PATH = path.join(STATE_DIR, 'session-actions.json');
97
- const CUSTOM_CLAIM_GATES_PATH = path.join(STATE_DIR, 'claim-verification.json');
98
- const GOVERNANCE_STATE_PATH = path.join(STATE_DIR, 'governance-state.json');
63
+ const STATE_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-state.json');
64
+ const CONSTRAINTS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'session-constraints.json');
65
+ const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
66
+ const SESSION_ACTIONS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'session-actions.json');
67
+ const CUSTOM_CLAIM_GATES_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'claim-verification.json');
68
+ const GOVERNANCE_STATE_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'governance-state.json');
99
69
  const TTL_MS = 5 * 60 * 1000; // 5 minutes
100
70
  const SESSION_ACTION_TTL_MS = 60 * 60 * 1000; // 1 hour
101
71
  const PROTECTED_APPROVAL_TTL_MS = 60 * 60 * 1000; // 1 hour
@@ -115,127 +85,12 @@ const DEFAULT_PROTECTED_FILE_GLOBS = [
115
85
  const EDIT_LIKE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
116
86
  const HIGH_RISK_BASH_PATTERN = /\b(?:git\s+(?:add|commit|push)|gh\s+pr\s+(?:create|merge)|npm\s+publish|yarn\s+publish|pnpm\s+publish|rm\s+-rf)\b/i;
117
87
  const REMOTE_SIDE_EFFECT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+(?:create|merge|close|reopen|ready|edit)\b|gh\s+release\s+(?:create|delete|edit|upload)\b|npm\s+publish\b|yarn\s+publish\b|pnpm\s+publish\b)\b/i;
118
- const MAX_COMMAND_SCAN_CHARS = 20000;
119
88
  const BOOSTED_RISK_BLOCK_SCORE = 0.8;
120
89
  const BOOSTED_RISK_MIN_EXAMPLES = 3;
121
90
  const PR_THREAD_RESOLUTION_ACTION = 'pr_thread_resolution_verified_after_commit';
122
- const HELPER_BYPASS_ACTION = 'helper_script_modified';
123
- const KNOWLEDGE_ENTROPY_THRESHOLD = 0.7;
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;
132
-
133
- // ---------------------------------------------------------------------------
134
- // Enforcement posture (CEO decision 2026-06-04): warn-by-default.
135
- // The firewall ALWAYS fires and logs every decision, but most gates WARN rather
136
- // than hard-block — only TRULY CATASTROPHIC, irreversible actions hard-block:
137
- // - secret exfiltration (handled on its own deny path; never downgraded)
138
- // - security-vulnerability / supply-chain denies (own deny path; not downgraded)
139
- // - irreversibly destructive filesystem commands (rm -rf class, mkfs, dd to disk,
140
- // fork bomb) — kept as hard deny via DESTRUCTIVE_FS_PATTERN below.
141
- // Everything else (memory-high-risk, workflow-sequence, off-scope, git push, deploy,
142
- // approval gates) downgrades deny/approve -> warn so legitimate work is never blocked.
143
- // Opt back into full hard enforcement with THUMBGATE_STRICT_ENFORCEMENT=1.
144
- // Enforcement posture (CEO decision 2026-06-04): WARN + AUDIT by default.
145
- // The firewall fires and LOGS every decision, but downgrades deny/approve -> warn so
146
- // legitimate work is never hard-blocked. We deliberately do NOT try to hard-block
147
- // arbitrary destructive commands here: a regex "catastrophic floor" is unwinnable
148
- // (sudo / bash -c / find -exec / eval / base64|sh all evade it) and gives false confidence.
149
- // HARD enforcement is an explicit opt-in via THUMBGATE_STRICT_ENFORCEMENT=1, which keeps
150
- // the engine's FULL gate set (its high-risk-command gates catch prefixed/obfuscated forms
151
- // far better than any single regex). Secret exfiltration and the security-vulnerability
152
- // scan hard-deny on their OWN paths before this runs, so irreversible data-leak / supply
153
- // chain risks stay blocked regardless of posture.
154
- function applyEnforcementPosture(result) {
155
- if (!result || (result.decision !== 'deny' && result.decision !== 'approve')) return result;
156
- // Full hard enforcement opt-in: keep every deny.
157
- if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') return result;
158
- // Honor the explicit strict-knowledge-conflict opt-in for that gate.
159
- if (process.env.THUMBGATE_STRICT_KNOWLEDGE_CONFLICT === '1' && result.gate === 'knowledge-conflict-gate') return result;
160
- // Warn-by-default: the gate still fired and is recorded; the action is allowed through
161
- // with the warning surfaced instead of hard-blocked, so legitimate work is never blocked.
162
- return {
163
- ...result,
164
- decision: 'warn',
165
- warnByDefault: true,
166
- message: `${result.message}\n\n⚠️ ThumbGate is in warn-by-default mode — this was flagged and logged, not blocked. Set THUMBGATE_STRICT_ENFORCEMENT=1 to hard-block, or THUMBGATE_HOTFIX_BYPASS=1 to disable checks entirely.`,
167
- };
168
- }
169
- const BREAK_GLASS_CONDITION = 'thumbgate_break_glass';
170
- const BREAK_GLASS_SETTINGS_GLOBS = [
171
- '.claude/settings.local.json',
172
- '.claude/settings.json',
173
- '**/.claude/settings.local.json',
174
- '**/.claude/settings.json',
175
- '.codex/config.toml',
176
- '**/.codex/config.toml',
177
- ];
178
-
179
- function isRuntimePlanGateEnabled() {
180
- return process.env.THUMBGATE_PLAN_GATE === '1' || process.env.THUMBGATE_PLAN_GATE === 'true';
181
- }
182
91
  const PR_THREAD_RESOLUTION_CLAIM_PATTERN = '(?:thread|review|comment).*?(?:resolved|verified|checked|addressed|fixed)|(?:resolved|verified|checked|addressed|fixed).*?(?:thread|review|comment)';
183
92
  const PR_THREAD_RESOLUTION_REQUIRED_ACTIONS = ['pr_threads_checked', 'thread_resolution_verified'];
184
93
 
185
- function commandScanText(command) {
186
- return String(command || '').slice(0, MAX_COMMAND_SCAN_CHARS);
187
- }
188
-
189
- function commandWords(command) {
190
- return commandScanText(command).toLowerCase().split(/\s+/).filter(Boolean);
191
- }
192
-
193
- function commandContainsSequence(words, sequence) {
194
- if (!Array.isArray(words) || !Array.isArray(sequence) || sequence.length === 0) return false;
195
- for (let i = 0; i <= words.length - sequence.length; i += 1) {
196
- let matched = true;
197
- for (let j = 0; j < sequence.length; j += 1) {
198
- if (words[i + j] !== sequence[j]) {
199
- matched = false;
200
- break;
201
- }
202
- }
203
- if (matched) return true;
204
- }
205
- return false;
206
- }
207
-
208
- function commandHasPostMethod(words) {
209
- for (let i = 0; i < words.length; i += 1) {
210
- const word = words[i];
211
- if ((word === '-x' || word === '--method') && words[i + 1] === 'post') return true;
212
- if (word === '--method=post' || word === '-xpost') return true;
213
- }
214
- return false;
215
- }
216
-
217
- function isGhApiPrCreateCommand(command) {
218
- const words = commandWords(command);
219
- if (!commandContainsSequence(words, ['gh', 'api'])) return false;
220
- const hasPullsEndpoint = words.some((word) => word === '/pulls' || word.endsWith('/pulls'));
221
- if (!hasPullsEndpoint) return false;
222
- const fieldFlags = new Set(['-f', '--field', '--raw-field']);
223
- const hasFieldWrite = words.some((word) => (
224
- fieldFlags.has(word) ||
225
- word.startsWith('-f=') ||
226
- word.startsWith('--field=') ||
227
- word.startsWith('--raw-field=')
228
- ));
229
- return hasFieldWrite || commandHasPostMethod(words);
230
- }
231
-
232
- function isRecursiveChmodCommand(command) {
233
- const words = commandWords(command);
234
- const chmodIndex = words.indexOf('chmod');
235
- if (chmodIndex === -1) return false;
236
- return words.slice(chmodIndex + 1).includes('-r') || words.slice(chmodIndex + 1).some((word) => word.includes('r') && word.startsWith('-'));
237
- }
238
-
239
94
  // ---------------------------------------------------------------------------
240
95
  // Config loading
241
96
  // ---------------------------------------------------------------------------
@@ -332,27 +187,6 @@ function sanitizeGlobList(globs) {
332
187
  return [...new Set(globs.map((glob) => normalizeGlob(glob)).filter(Boolean))];
333
188
  }
334
189
 
335
- // Affected files are compared as repo-relative paths (git --name-only output and
336
- // inline paths are relative to the repo root). A caller who passes an ABSOLUTE
337
- // allowedPath (e.g. "/Users/me/proj/src/**") therefore declares a glob that can never
338
- // match — a silent no-op scope. When repoPath is known, rebase absolute globs that
339
- // live under it to the repo-relative form so the scope actually applies. Globs already
340
- // relative, or absolute but outside repoPath, are returned unchanged.
341
- function rebaseGlobsToRepoRoot(globs, repoPath) {
342
- // normalizeGlob strips both leading AND trailing slashes, so a repoPath with a
343
- // trailing slash ("/Users/me/proj/") still matches the repo-relative globs.
344
- const repoRel = normalizeGlob(repoPath);
345
- if (!repoRel) return globs;
346
- return globs.map((glob) => {
347
- if (glob === repoRel) return '**';
348
- if (glob.startsWith(`${repoRel}/`)) {
349
- const rebased = glob.slice(repoRel.length + 1);
350
- return rebased || '**';
351
- }
352
- return glob;
353
- });
354
- }
355
-
356
190
  function globToRegExp(glob) {
357
191
  const normalized = normalizeGlob(glob);
358
192
  let pattern = '^';
@@ -406,9 +240,6 @@ function loadGovernanceState() {
406
240
  branchGovernance: raw && raw.branchGovernance && typeof raw.branchGovernance === 'object'
407
241
  ? raw.branchGovernance
408
242
  : null,
409
- workflowContract: raw && raw.workflowContract && typeof raw.workflowContract === 'object'
410
- ? raw.workflowContract
411
- : null,
412
243
  };
413
244
  const now = Date.now();
414
245
  const activeApprovals = state.protectedApprovals.filter((entry) => {
@@ -428,7 +259,6 @@ function saveGovernanceState(state) {
428
259
  taskScope: state && state.taskScope ? state.taskScope : null,
429
260
  protectedApprovals: Array.isArray(state && state.protectedApprovals) ? state.protectedApprovals : [],
430
261
  branchGovernance: state && state.branchGovernance ? state.branchGovernance : null,
431
- workflowContract: state && state.workflowContract ? state.workflowContract : null,
432
262
  };
433
263
  saveJSON(module.exports.GOVERNANCE_STATE_PATH, next);
434
264
  }
@@ -440,39 +270,33 @@ function setTaskScope(scopeInput = {}) {
440
270
  taskScope: null,
441
271
  protectedApprovals: currentState.protectedApprovals,
442
272
  branchGovernance: currentState.branchGovernance,
443
- workflowContract: null,
444
273
  };
445
274
  saveGovernanceState(cleared);
446
- refreshLocalOnlyConstraint(cleared);
447
275
  return null;
448
276
  }
449
277
 
450
- const repoPath = String(scopeInput.repoPath || '').trim() || null;
451
- const allowedPaths = rebaseGlobsToRepoRoot(sanitizeGlobList(scopeInput.allowedPaths), repoPath);
278
+ const allowedPaths = sanitizeGlobList(scopeInput.allowedPaths);
452
279
  if (allowedPaths.length === 0) {
453
280
  throw new Error('allowedPaths must be a non-empty array');
454
281
  }
455
282
 
456
- const protectedPaths = rebaseGlobsToRepoRoot(sanitizeGlobList(
283
+ const protectedPaths = sanitizeGlobList(
457
284
  Array.isArray(scopeInput.protectedPaths) && scopeInput.protectedPaths.length > 0
458
285
  ? scopeInput.protectedPaths
459
286
  : DEFAULT_PROTECTED_FILE_GLOBS
460
- ), repoPath);
287
+ );
461
288
  const taskScope = {
462
289
  taskId: String(scopeInput.taskId || '').trim() || null,
463
290
  summary: String(scopeInput.summary || '').trim() || null,
464
291
  allowedPaths,
465
292
  protectedPaths,
466
293
  localOnly: scopeInput.localOnly === true,
467
- repoPath,
294
+ repoPath: String(scopeInput.repoPath || '').trim() || null,
468
295
  createdAt: new Date().toISOString(),
469
296
  timestamp: Date.now(),
470
297
  };
471
298
  const state = loadGovernanceState();
472
299
  state.taskScope = taskScope;
473
- state.workflowContract = scopeInput.workflowContract && typeof scopeInput.workflowContract === 'object'
474
- ? scopeInput.workflowContract
475
- : null;
476
300
  saveGovernanceState(state);
477
301
  if (taskScope.localOnly) {
478
302
  setConstraint('local_only', true);
@@ -508,44 +332,11 @@ function approveProtectedAction(input = {}) {
508
332
  return entry;
509
333
  }
510
334
 
511
- function breakGlassEmergency(input = {}) {
512
- const reason = String(input.reason || '').trim();
513
- if (!reason) {
514
- throw new Error('reason is required');
515
- }
516
-
517
- const ttlMs = Math.min(clampTtlMs(input.ttlMs, TTL_MS), TTL_MS);
518
- const evidence = `BREAK GLASS: ${reason}`;
519
- const gates = ['pr_create_allowed', 'pr_threads_checked', BREAK_GLASS_CONDITION];
520
- const satisfied = {};
521
- for (const gateId of gates) {
522
- satisfied[gateId] = satisfyCondition(gateId, evidence);
523
- }
524
-
525
- const approval = approveProtectedAction({
526
- pathGlobs: BREAK_GLASS_SETTINGS_GLOBS,
527
- reason: evidence,
528
- evidence,
529
- ttlMs,
530
- });
531
-
532
- return {
533
- ok: true,
534
- reason,
535
- ttlMs,
536
- expiresAt: new Date(Date.now() + ttlMs).toISOString(),
537
- satisfied,
538
- approval,
539
- settingsGlobs: BREAK_GLASS_SETTINGS_GLOBS.slice(),
540
- };
541
- }
542
-
543
335
  function setBranchGovernance(input = {}) {
544
336
  if (input && input.clear === true) {
545
337
  const state = loadGovernanceState();
546
338
  state.branchGovernance = null;
547
339
  saveGovernanceState(state);
548
- refreshLocalOnlyConstraint(state);
549
340
  return null;
550
341
  }
551
342
 
@@ -596,24 +387,6 @@ function setConstraint(key, value) {
596
387
  return constraints[key];
597
388
  }
598
389
 
599
- function clearConstraint(key) {
600
- const constraints = loadConstraints();
601
- delete constraints[key];
602
- saveConstraints(constraints);
603
- }
604
-
605
- function refreshLocalOnlyConstraint(governanceState = loadGovernanceState()) {
606
- const localOnlyActive = Boolean(
607
- (governanceState.taskScope && governanceState.taskScope.localOnly) ||
608
- (governanceState.branchGovernance && governanceState.branchGovernance.localOnly)
609
- );
610
- if (localOnlyActive) {
611
- setConstraint('local_only', true);
612
- } else {
613
- clearConstraint('local_only');
614
- }
615
- }
616
-
617
390
  function isConditionSatisfied(conditionId) {
618
391
  const state = loadState();
619
392
  const entry = state[conditionId];
@@ -653,29 +426,7 @@ function loadStats() {
653
426
 
654
427
  function saveStats(stats) { saveJSON(module.exports.STATS_PATH, stats); }
655
428
 
656
- function buildGateActionFingerprint(gateId, options = {}) {
657
- if (options.actionFingerprint) return String(options.actionFingerprint);
658
- const toolName = options.toolName || options.tool_name || '';
659
- const toolInput = options.toolInput || options.tool_input || {};
660
- const parts = [toolName];
661
- if (typeof toolInput === 'string') {
662
- parts.push(toolInput);
663
- } else if (toolInput && typeof toolInput === 'object') {
664
- parts.push(
665
- toolInput.command || '',
666
- toolInput.cmd || '',
667
- toolInput.file_path || '',
668
- toolInput.path || '',
669
- toolInput.description || '',
670
- toolInput.prompt || '',
671
- toolInput.pattern || '',
672
- );
673
- if (Array.isArray(toolInput.affectedFiles)) parts.push(...toolInput.affectedFiles);
674
- }
675
- return actionFingerprint(parts);
676
- }
677
-
678
- function recordStat(gateId, action, gate, options = {}) {
429
+ function recordStat(gateId, action, gate) {
679
430
  const stats = loadStats();
680
431
  if (action === 'block') stats.blocked = (stats.blocked || 0) + 1;
681
432
  else if (action === 'warn') stats.warned = (stats.warned || 0) + 1;
@@ -688,29 +439,6 @@ function recordStat(gateId, action, gate, options = {}) {
688
439
  else if (action === 'warn') stats.byGate[gateId].warned += 1;
689
440
  else if (action === 'approve') stats.byGate[gateId].pendingApproval = (stats.byGate[gateId].pendingApproval || 0) + 1;
690
441
  else if (action === 'log') stats.byGate[gateId].logged = (stats.byGate[gateId].logged || 0) + 1;
691
-
692
- // Track same-action recurrence within a session for first-time fix rate.
693
- // Gate-only recurrence over-counts noisy gates; repeats require a stable,
694
- // sanitized action fingerprint.
695
- if (action === 'block' || action === 'warn') {
696
- if (!stats.sessionFiredGates) stats.sessionFiredGates = {};
697
- if (!stats.sessionFiredActions) stats.sessionFiredActions = {};
698
- const sessionKey = `session_${Math.floor(Date.now() / SESSION_ACTION_TTL_MS)}`;
699
- if (!stats.sessionFiredGates[sessionKey]) stats.sessionFiredGates[sessionKey] = {};
700
- stats.sessionFiredGates[sessionKey][gateId] = true;
701
-
702
- const fingerprint = buildGateActionFingerprint(gateId, options);
703
- if (fingerprint) {
704
- if (!stats.sessionFiredActions[sessionKey]) stats.sessionFiredActions[sessionKey] = {};
705
- if (!stats.sessionFiredActions[sessionKey][gateId]) stats.sessionFiredActions[sessionKey][gateId] = {};
706
- if (stats.sessionFiredActions[sessionKey][gateId][fingerprint]) {
707
- stats.recurringBlocks = (stats.recurringBlocks || 0) + 1;
708
- } else {
709
- stats.sessionFiredActions[sessionKey][gateId][fingerprint] = true;
710
- }
711
- }
712
- }
713
-
714
442
  saveStats(stats);
715
443
  // Track lesson freshness when an auto-promoted gate fires
716
444
  if (gate && gate.sourceLessonId) {
@@ -724,69 +452,6 @@ function recordStat(gateId, action, gate, options = {}) {
724
452
  }
725
453
  }
726
454
 
727
- // ---------------------------------------------------------------------------
728
- // Free-tier daily block cap
729
- // ---------------------------------------------------------------------------
730
-
731
- /**
732
- * Count today's gate blocks from stats. Free tier gets FREE_TIER_DAILY_BLOCKS
733
- * blocks/day. After the limit, deny → warn + upgrade CTA so the action proceeds
734
- * but the user sees they lost protection.
735
- */
736
- function getTodayBlockCount() {
737
- const stats = loadStats();
738
- const today = todayKey();
739
- if (!stats.dailyBlocks || !stats.dailyBlocks[today]) return 0;
740
- return stats.dailyBlocks[today];
741
- }
742
-
743
- function incrementTodayBlockCount() {
744
- const stats = loadStats();
745
- const today = todayKey();
746
- if (!stats.dailyBlocks) stats.dailyBlocks = {};
747
- // Clean old dates (keep only last 7 days to prevent unbounded growth)
748
- const keys = Object.keys(stats.dailyBlocks);
749
- if (keys.length > 7) {
750
- keys.sort();
751
- for (const k of keys.slice(0, keys.length - 7)) {
752
- delete stats.dailyBlocks[k];
753
- }
754
- }
755
- stats.dailyBlocks[today] = (stats.dailyBlocks[today] || 0) + 1;
756
- saveStats(stats);
757
- return stats.dailyBlocks[today];
758
- }
759
-
760
- /**
761
- * If the user is free-tier and has exceeded daily block limit, downgrade
762
- * a deny result to a warn with an upgrade CTA. Returns null if no cap applies.
763
- */
764
- function applyDailyBlockCap(denyResult) {
765
- // Pro, trial, CI, and THUMBGATE_NO_RATE_LIMIT users are uncapped
766
- if (isProTier()) return null;
767
- if (process.env.CI || process.env.GITHUB_ACTIONS) return null;
768
-
769
- const todayCount = getTodayBlockCount();
770
- if (todayCount < FREE_TIER_DAILY_BLOCKS) {
771
- // Under limit: allow the block, increment counter
772
- incrementTodayBlockCount();
773
- return null;
774
- }
775
-
776
- // Over limit: downgrade deny → warn with upgrade CTA
777
- const remaining = 0;
778
- return {
779
- decision: 'warn',
780
- gate: denyResult.gate,
781
- message: `⚠️ ${denyResult.message}\n\n🔓 Daily protection limit reached (${FREE_TIER_DAILY_BLOCKS}/${FREE_TIER_DAILY_BLOCKS} blocks used). This action was allowed through. Upgrade for unlimited protection: https://thumbgate.ai/go/pro`,
782
- severity: denyResult.severity,
783
- reasoning: (denyResult.reasoning || []).concat([
784
- `Free-tier daily block limit (${FREE_TIER_DAILY_BLOCKS}) exceeded — deny downgraded to warn`,
785
- ]),
786
- dailyBlockCapApplied: true,
787
- };
788
- }
789
-
790
455
  // ---------------------------------------------------------------------------
791
456
  // Reasoning chain builder
792
457
  // ---------------------------------------------------------------------------
@@ -842,26 +507,9 @@ function toRepoRelativePath(filePath, repoRoot) {
842
507
  const value = String(filePath || '').trim();
843
508
  if (!value) return '';
844
509
  if (repoRoot && path.isAbsolute(value)) {
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
- }
510
+ const relative = path.relative(repoRoot, value);
511
+ if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
512
+ return normalizePosix(relative);
865
513
  }
866
514
  }
867
515
  return normalizePosix(value);
@@ -940,7 +588,7 @@ function extractAffectedFiles(toolName, toolInput = {}) {
940
588
  }
941
589
  }
942
590
 
943
- if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command) || isGhApiPrCreateCommand(command)) {
591
+ if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command)) {
944
592
  for (const filePath of getBranchDiffFiles(repoRoot)) {
945
593
  files.add(normalizePosix(filePath));
946
594
  }
@@ -959,7 +607,6 @@ function isHighRiskAction(toolName, toolInput = {}, affectedFiles = []) {
959
607
  const command = String(toolInput.command || '');
960
608
  // Original high-risk pattern (git writes, publishes, destructive ops)
961
609
  if (HIGH_RISK_BASH_PATTERN.test(command)) return true;
962
- if (isGhApiPrCreateCommand(command)) return true;
963
610
  // Broadened: any Bash command that modifies files or has side effects.
964
611
  // Excludes pure read/analysis commands (node --test, cat, ls, echo, etc.)
965
612
  // to avoid false positives on benign operations.
@@ -1197,8 +844,7 @@ function getLocalOnlyScopeSources(governanceState = {}, constraints = {}) {
1197
844
 
1198
845
  function isRemoteSideEffectCommand(toolName, toolInput = {}) {
1199
846
  if (toolName !== 'Bash') return false;
1200
- const command = String(toolInput.command || '');
1201
- return REMOTE_SIDE_EFFECT_BASH_PATTERN.test(command) || isGhApiPrCreateCommand(command);
847
+ return REMOTE_SIDE_EFFECT_BASH_PATTERN.test(String(toolInput.command || ''));
1202
848
  }
1203
849
 
1204
850
  function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governanceState = {}, constraints = {}) {
@@ -1220,127 +866,8 @@ function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governa
1220
866
  };
1221
867
  }
1222
868
 
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
-
1342
869
  function recordStructuralGateBlock(toolName, toolInput, result) {
1343
- recordStat(result.gate, 'block', null, { toolName, toolInput });
870
+ recordStat(result.gate, 'block');
1344
871
  const auditRecord = recordAuditEvent({
1345
872
  toolName,
1346
873
  toolInput,
@@ -1371,25 +898,6 @@ function shouldEnforceTaskScope(gate, governanceState, toolName, toolInput = {},
1371
898
  return isScopeEnforcedAction(toolName, toolInput, affectedFiles);
1372
899
  }
1373
900
 
1374
- function isAgentHookSettingsFile(filePath) {
1375
- return matchesAnyGlob(filePath, BREAK_GLASS_SETTINGS_GLOBS);
1376
- }
1377
-
1378
- function isBreakGlassSettingsBypass(gate, affectedFiles) {
1379
- if (!gate || !['task-scope-edit-boundary', 'protected-file-approval-required'].includes(gate.id)) {
1380
- return false;
1381
- }
1382
- if (!isConditionSatisfied(BREAK_GLASS_CONDITION)) return false;
1383
- return Array.isArray(affectedFiles) && affectedFiles.length > 0 && affectedFiles.every(isAgentHookSettingsFile);
1384
- }
1385
-
1386
- function isBreakGlassSettingsRecoveryAction(toolName, toolInput = {}) {
1387
- if (!EDIT_LIKE_TOOLS.has(toolName)) return false;
1388
- if (!isConditionSatisfied(BREAK_GLASS_CONDITION)) return false;
1389
- const affectedFiles = extractAffectedFiles(toolName, toolInput).files;
1390
- return affectedFiles.length > 0 && affectedFiles.every(isAgentHookSettingsFile);
1391
- }
1392
-
1393
901
  function formatFileList(files, limit = 5) {
1394
902
  const items = Array.isArray(files) ? files.filter(Boolean) : [];
1395
903
  if (items.length === 0) return 'none';
@@ -1598,19 +1106,8 @@ function checkWhenClause(when, constraints) {
1598
1106
  if (!when || !when.constraints) return true;
1599
1107
 
1600
1108
  for (const [key, expectedValue] of Object.entries(when.constraints)) {
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) {
1109
+ const constraint = constraints[key];
1110
+ if (!constraint || constraint.value !== expectedValue) {
1614
1111
  return false;
1615
1112
  }
1616
1113
  }
@@ -1618,69 +1115,11 @@ function checkWhenClause(when, constraints) {
1618
1115
  }
1619
1116
 
1620
1117
  function matchGate(gate, toolName, toolInput = {}) {
1621
- let matchText = toolInput.command || toolInput.file_path || toolInput.path || '';
1622
-
1623
- // Claw/hybrid support: enrich matchText with claw metadata (for EnterpriseClaw/OpenShell/Perplexity hybrid agents)
1624
- const clawCtx = toolInput.clawContext || toolInput._claw || (toolInput.agentId ? {
1625
- actionType: toolInput.actionType || 'unknown',
1626
- agentId: toolInput.agentId || 'unknown',
1627
- hybridRoute: toolInput.hybridRoute || 'unknown',
1628
- screenInteraction: !!toolInput.screenInteraction,
1629
- fileAccess: !!toolInput.fileAccess,
1630
- } : null);
1631
-
1632
- if (clawCtx) {
1633
- const actionType = clawCtx.actionType || clawCtx.claw_action_type || 'unknown';
1634
- const parts = [
1635
- matchText,
1636
- `claw_style: true`,
1637
- `agent_identity: ${clawCtx.agentId || 'unknown'}`,
1638
- `claw_action_type: ${actionType}`,
1639
- `hybrid_route: ${clawCtx.hybridRoute || 'unknown'}`,
1640
- ];
1641
-
1642
- if (clawCtx.screenInteraction || actionType.includes('screen')) {
1643
- parts.push('screen_interaction');
1644
- parts.push('interact screen');
1645
- }
1646
- if (clawCtx.fileAccess || actionType.includes('file') || actionType.includes('fs')) {
1647
- parts.push('file_system_access');
1648
- parts.push('local device file system access');
1649
- }
1650
- if (actionType === 'dynamic-tool-creation' || actionType.includes('create-tool') || actionType.includes('define-tool')) {
1651
- parts.push('create tool');
1652
- }
1653
-
1654
- matchText = parts.filter(Boolean).join(' | ');
1655
- }
1656
-
1118
+ const matchText = toolInput.command || toolInput.file_path || toolInput.path || '';
1657
1119
  const affected = extractAffectedFiles(toolName, toolInput);
1658
1120
  const affectedFiles = affected.files;
1659
1121
  const repoRoot = affected.repoRoot;
1660
1122
  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
- }
1684
1123
 
1685
1124
  if (Array.isArray(gate.toolNames) && gate.toolNames.length > 0 && !gate.toolNames.includes(toolName)) {
1686
1125
  return { matched: false, matchText, affectedFiles };
@@ -1690,12 +1129,6 @@ function matchGate(gate, toolName, toolInput = {}) {
1690
1129
  try {
1691
1130
  const regex = new RegExp(gate.pattern);
1692
1131
  if (!regex.test(matchText)) return { matched: false, matchText, affectedFiles };
1693
- if (gate.id === 'permission-change-approval' && isSafeLocalCredentialHardeningCommand(toolName, toolInput)) {
1694
- return { matched: false, matchText, affectedFiles };
1695
- }
1696
- if (isBreakGlassSettingsBypass(gate, affectedFiles)) {
1697
- return { matched: false, matchText, affectedFiles };
1698
- }
1699
1132
  } catch {
1700
1133
  return { matched: false, matchText, affectedFiles };
1701
1134
  }
@@ -1713,9 +1146,6 @@ function matchGate(gate, toolName, toolInput = {}) {
1713
1146
 
1714
1147
  let taskScopeViolation = null;
1715
1148
  if (gate.requireTaskScope) {
1716
- if (isBreakGlassSettingsBypass(gate, affectedFiles)) {
1717
- return { matched: false, matchText, affectedFiles };
1718
- }
1719
1149
  if (!shouldEnforceTaskScope(gate, governanceState, toolName, toolInput, affectedFiles)) {
1720
1150
  return { matched: false, matchText, affectedFiles };
1721
1151
  }
@@ -1725,9 +1155,6 @@ function matchGate(gate, toolName, toolInput = {}) {
1725
1155
 
1726
1156
  let protectedApprovalViolation = null;
1727
1157
  if (gate.requireProtectedApproval) {
1728
- if (isBreakGlassSettingsBypass(gate, affectedFiles)) {
1729
- return { matched: false, matchText, affectedFiles };
1730
- }
1731
1158
  const protectedGlobs = sanitizeGlobList(
1732
1159
  Array.isArray(gate.protectedGlobs) && gate.protectedGlobs.length > 0
1733
1160
  ? gate.protectedGlobs
@@ -1767,40 +1194,9 @@ function matchesGate(gate, toolName, toolInput) {
1767
1194
  return matchGate(gate, toolName, toolInput).matched;
1768
1195
  }
1769
1196
 
1770
- function isSafeLocalCredentialHardeningCommand(toolName, toolInput = {}) {
1771
- if (toolName !== 'Bash') return false;
1772
- const command = String(toolInput.command || '').trim();
1773
- if (!command || isRecursiveChmodCommand(command)) return false;
1774
- if (/[;&|`$()<>*?[\]{}]/.test(command)) return false;
1775
-
1776
- const match = command.match(/(?:^|\s)chmod\s+(?:-[fv]\s+)?0?([46]00)\s+(['"]?)(\S+)\2\s*$/i);
1777
- if (!match) return false;
1778
-
1779
- const target = match[3];
1780
- if (!target || target === '/' || target === '~') return false;
1781
- if (target.includes('..')) return false;
1782
-
1783
- const normalized = target.replace(/^['"]|['"]$/g, '').toLowerCase();
1784
- const looksLikeCredentialPath = /(?:^|\/)(?:\.config|\.ssh|\.gnupg|\.aws|\.gcloud|\.gemini|\.resume_secrets|\.thumbgate|secrets?|credentials?)(?:\/|$)/.test(normalized)
1785
- || /(?:key|secret|token|credential|gemini|gcloud|google|operator).*\.(?:json|pem|key|env)$/i.test(normalized)
1786
- || /\.(?:pem|key)$/i.test(normalized);
1787
-
1788
- return looksLikeCredentialPath;
1789
- }
1790
-
1791
1197
  function evaluateMemoryGuard(toolName, toolInput = {}) {
1792
1198
  const affected = extractAffectedFiles(toolName, toolInput);
1793
1199
  const affectedFiles = affected.files;
1794
- if (isSafeSecretStorageWrite(toolName, toolInput, process.cwd())) {
1795
- return null;
1796
- }
1797
- // Hardening a credential file's permissions (chmod 600 on a key/secret path) is
1798
- // a safety action, not a risk. The same exemption already guards the
1799
- // permission-change-approval gate; without it here, `chmod 600 ~/.resume_secrets/key`
1800
- // gets hard-denied by recurring-negative-memory matching — the opposite of intent.
1801
- if (isSafeLocalCredentialHardeningCommand(toolName, toolInput)) {
1802
- return null;
1803
- }
1804
1200
  if (!isHighRiskAction(toolName, toolInput, affectedFiles)) {
1805
1201
  return null;
1806
1202
  }
@@ -1814,10 +1210,7 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
1814
1210
  }
1815
1211
 
1816
1212
  const command = String(toolInput.command || '');
1817
- const isPrCreateCommand = toolName === 'Bash' && (
1818
- /\bgh\s+pr\s+create\b/i.test(command) || isGhApiPrCreateCommand(command)
1819
- );
1820
- if (isPrCreateCommand && isConditionSatisfied('pr_create_allowed')) {
1213
+ if (toolName === 'Bash' && /\bgh\s+pr\s+create\b/i.test(command) && isConditionSatisfied('pr_create_allowed')) {
1821
1214
  const branchGovernanceViolation = buildBranchGovernanceViolation(
1822
1215
  governanceState,
1823
1216
  toolInput,
@@ -1830,10 +1223,7 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
1830
1223
  }
1831
1224
  }
1832
1225
 
1833
- if (toolName === 'Bash' && (
1834
- /\b(?:gh\s+pr\s+(?:create|merge)|gh\s+release\s+create|git\s+tag\b|(?:npm|yarn|pnpm)\s+publish\b)\b/i.test(command) ||
1835
- isGhApiPrCreateCommand(command)
1836
- )) {
1226
+ if (toolName === 'Bash' && /\b(?:gh\s+pr\s+(?:create|merge)|gh\s+release\s+create|git\s+tag\b|(?:npm|yarn|pnpm)\s+publish\b)\b/i.test(command)) {
1837
1227
  const branchGovernanceViolation = buildBranchGovernanceViolation(
1838
1228
  governanceState,
1839
1229
  toolInput,
@@ -1871,14 +1261,7 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
1871
1261
  filePath: toolInput.file_path || toolInput.path || null,
1872
1262
  affectedFiles,
1873
1263
  });
1874
- // Claw/hybrid support: pass context if agent provides claw metadata (for EnterpriseClaw/OpenShell/Perplexity hybrid agents)
1875
- let guard;
1876
- if (toolInput && (toolInput.clawContext || toolInput._claw || toolInput.hybridRoute || toolInput.agentId)) {
1877
- const clawCtx = toolInput.clawContext || toolInput._claw || { actionType: toolInput.actionType || 'unknown', agentId: toolInput.agentId || 'unknown', hybridRoute: toolInput.hybridRoute || 'unknown' };
1878
- guard = hybrid.evaluateClawPretool ? hybrid.evaluateClawPretool(toolName, serializedInput, clawCtx) : hybrid.evaluatePretool(toolName, serializedInput);
1879
- } else {
1880
- guard = hybrid.evaluatePretool(toolName, serializedInput);
1881
- }
1264
+ const guard = hybrid.evaluatePretool(toolName, serializedInput);
1882
1265
  if (!guard || guard.mode === 'allow') {
1883
1266
  return null;
1884
1267
  }
@@ -2016,26 +1399,10 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
2016
1399
  if (localOnlyRemoteSideEffectGate) {
2017
1400
  return recordStructuralGateBlock(toolName, toolInput, localOnlyRemoteSideEffectGate);
2018
1401
  }
2019
- const statefulHelperBypassGate = evaluateStatefulHelperBypassGate(toolName, toolInput);
2020
- if (statefulHelperBypassGate) {
2021
- return recordStructuralGateBlock(toolName, toolInput, statefulHelperBypassGate);
2022
- }
2023
- if (isBreakGlassSettingsRecoveryAction(toolName, toolInput)) {
2024
- recordAuditEvent({
2025
- toolName,
2026
- toolInput,
2027
- decision: 'allow',
2028
- gateId: BREAK_GLASS_CONDITION,
2029
- message: 'Break-glass recovery allowed hook settings edit',
2030
- severity: 'high',
2031
- source: 'gates-engine',
2032
- });
2033
- return null;
2034
- }
2035
1402
 
2036
1403
  const pendingThreadResolutionGate = evaluatePendingPrThreadResolutionGate(toolName, toolInput);
2037
1404
  if (pendingThreadResolutionGate) {
2038
- recordStat(pendingThreadResolutionGate.gate, 'block', null, { toolName, toolInput });
1405
+ recordStat(pendingThreadResolutionGate.gate, 'block');
2039
1406
  const auditRecord = recordAuditEvent({
2040
1407
  toolName,
2041
1408
  toolInput,
@@ -2051,7 +1418,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
2051
1418
 
2052
1419
  const boostedRiskGuard = evaluateBoostedRiskTagGuard(toolName, toolInput);
2053
1420
  if (boostedRiskGuard) {
2054
- recordStat(boostedRiskGuard.gate, 'block', null, { toolName, toolInput });
1421
+ recordStat(boostedRiskGuard.gate, 'block');
2055
1422
  const auditRecord = recordAuditEvent({
2056
1423
  toolName,
2057
1424
  toolInput,
@@ -2065,23 +1432,6 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
2065
1432
  return boostedRiskGuard;
2066
1433
  }
2067
1434
 
2068
- // Tier 1b: Planning and Trajectory (v1.26.0 - CodeRabbit Pattern).
2069
- // Keep runtime enforcement explicit so advisory planning checks do not mask
2070
- // higher-priority deny/approve gates in established workflows.
2071
- if (isRuntimePlanGateEnabled()) {
2072
- const planGate = evaluatePlanGate(toolName, toolInput);
2073
- if (planGate) {
2074
- recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
2075
- return planGate;
2076
- }
2077
-
2078
- const trajectory = getTrajectoryScore();
2079
- if (trajectory.isDrifting) {
2080
- recordStat('strategic-drift', 'block', null, { toolName, toolInput });
2081
- return { decision: 'deny', gate: 'strategic-drift', message: trajectory.message, severity: 'high' };
2082
- }
2083
- }
2084
-
2085
1435
  // Fast-path: feedback/recall tools skip metric gates entirely (avoids Stripe API calls)
2086
1436
  const METRIC_SKIP_TOOLS = ['capture_feedback', 'feedback_stats', 'recall', 'feedback_summary', 'prevention_rules'];
2087
1437
  const skipMetrics = METRIC_SKIP_TOOLS.includes(toolName);
@@ -2127,39 +1477,23 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
2127
1477
  });
2128
1478
 
2129
1479
  if (gate.action === 'block') {
2130
- const denyResult = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
2131
- // Free-tier daily block cap: after N blocks/day, deny warn + upgrade CTA
2132
- const cappedResult = applyDailyBlockCap(denyResult);
2133
- if (cappedResult) {
2134
- recordStat(gate.id, 'warn', gate, { toolName, toolInput });
2135
- const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
2136
- auditToFeedback(auditRecord);
2137
- return cappedResult;
2138
- }
2139
- recordStat(gate.id, 'block', gate, { toolName, toolInput });
1480
+ recordStat(gate.id, 'block', gate);
1481
+ const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
2140
1482
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
2141
1483
  auditToFeedback(auditRecord);
2142
- return denyResult;
1484
+ return result;
2143
1485
  }
2144
1486
 
2145
1487
  if (gate.action === 'approve') {
2146
- const approvalEnabled = process.env.THUMBGATE_APPROVAL_GATES !== '0';
2147
- if (approvalEnabled) {
2148
- recordStat(gate.id, 'approve', gate, { toolName, toolInput });
2149
- const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
2150
- const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
2151
- auditToFeedback(auditRecord);
2152
- return result;
2153
- }
2154
- recordStat(gate.id, 'warn', gate, { toolName, toolInput });
2155
- const result = { decision: 'warn', gate: gate.id, message: `[approval gate disabled] ${message}`, severity: gate.severity, reasoning };
2156
- const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1488
+ recordStat(gate.id, 'approve', gate);
1489
+ const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
1490
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
2157
1491
  auditToFeedback(auditRecord);
2158
1492
  return result;
2159
1493
  }
2160
1494
 
2161
1495
  if (gate.action === 'log') {
2162
- recordStat(gate.id, 'log', gate, { toolName, toolInput });
1496
+ recordStat(gate.id, 'log', gate);
2163
1497
  const result = { decision: 'log', gate: gate.id, message, severity: gate.severity, reasoning, logged: true };
2164
1498
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'log', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
2165
1499
  auditToFeedback(auditRecord);
@@ -2168,7 +1502,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
2168
1502
  }
2169
1503
 
2170
1504
  if (gate.action === 'warn') {
2171
- recordStat(gate.id, 'warn', gate, { toolName, toolInput });
1505
+ recordStat(gate.id, 'warn', gate);
2172
1506
  const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
2173
1507
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
2174
1508
  auditToFeedback(auditRecord);
@@ -2176,15 +1510,14 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
2176
1510
  }
2177
1511
  }
2178
1512
 
2179
- const skipAdvisoryGuards = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
2180
- const sentinelReport = skipAdvisoryGuards ? null : evaluateWorkflowSentinel(toolName, toolInput, {
1513
+ const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
2181
1514
  governanceState,
2182
1515
  });
2183
1516
  const sentinelDecision = recordSentinelDecision(sentinelReport, toolName, toolInput);
2184
1517
  const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
2185
1518
  if (memoryGuard) {
2186
1519
  const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
2187
- recordStat(enrichedMemoryGuard.gate, 'block', null, { toolName, toolInput });
1520
+ recordStat(enrichedMemoryGuard.gate, 'block');
2188
1521
  recordMemoryGuardDecision(sentinelDecision, enrichedMemoryGuard);
2189
1522
  const auditRecord = recordAuditEvent({
2190
1523
  toolName,
@@ -2201,7 +1534,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
2201
1534
 
2202
1535
  if (sentinelReport && sentinelReport.decision !== 'allow') {
2203
1536
  const sentinelResult = buildSentinelGateResult(sentinelReport);
2204
- recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
1537
+ recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
2205
1538
  recordSentinelBlockDecision(sentinelDecision, sentinelResult);
2206
1539
  const auditRecord = recordAuditEvent({
2207
1540
  toolName,
@@ -2247,26 +1580,10 @@ function evaluateGates(toolName, toolInput, configPath) {
2247
1580
  if (localOnlyRemoteSideEffectGate) {
2248
1581
  return recordStructuralGateBlock(toolName, toolInput, localOnlyRemoteSideEffectGate);
2249
1582
  }
2250
- const statefulHelperBypassGate = evaluateStatefulHelperBypassGate(toolName, toolInput);
2251
- if (statefulHelperBypassGate) {
2252
- return recordStructuralGateBlock(toolName, toolInput, statefulHelperBypassGate);
2253
- }
2254
- if (isBreakGlassSettingsRecoveryAction(toolName, toolInput)) {
2255
- recordAuditEvent({
2256
- toolName,
2257
- toolInput,
2258
- decision: 'allow',
2259
- gateId: BREAK_GLASS_CONDITION,
2260
- message: 'Break-glass recovery allowed hook settings edit',
2261
- severity: 'high',
2262
- source: 'gates-engine',
2263
- });
2264
- return null;
2265
- }
2266
1583
 
2267
1584
  const pendingThreadResolutionGate = evaluatePendingPrThreadResolutionGate(toolName, toolInput);
2268
1585
  if (pendingThreadResolutionGate) {
2269
- recordStat(pendingThreadResolutionGate.gate, 'block', null, { toolName, toolInput });
1586
+ recordStat(pendingThreadResolutionGate.gate, 'block');
2270
1587
  const auditRecord = recordAuditEvent({
2271
1588
  toolName,
2272
1589
  toolInput,
@@ -2282,7 +1599,7 @@ function evaluateGates(toolName, toolInput, configPath) {
2282
1599
 
2283
1600
  const boostedRiskGuard = evaluateBoostedRiskTagGuard(toolName, toolInput);
2284
1601
  if (boostedRiskGuard) {
2285
- recordStat(boostedRiskGuard.gate, 'block', null, { toolName, toolInput });
1602
+ recordStat(boostedRiskGuard.gate, 'block');
2286
1603
  const auditRecord = recordAuditEvent({
2287
1604
  toolName,
2288
1605
  toolInput,
@@ -2296,23 +1613,6 @@ function evaluateGates(toolName, toolInput, configPath) {
2296
1613
  return boostedRiskGuard;
2297
1614
  }
2298
1615
 
2299
- // Tier 1b: Planning and Trajectory (v1.26.0 - CodeRabbit Pattern).
2300
- // Keep runtime enforcement explicit so advisory planning checks do not mask
2301
- // higher-priority deny/approve gates in established workflows.
2302
- if (isRuntimePlanGateEnabled()) {
2303
- const planGate = evaluatePlanGate(toolName, toolInput);
2304
- if (planGate) {
2305
- recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
2306
- return planGate;
2307
- }
2308
-
2309
- const trajectory = getTrajectoryScore();
2310
- if (trajectory.isDrifting) {
2311
- recordStat('strategic-drift', 'block', null, { toolName, toolInput });
2312
- return { decision: 'deny', gate: 'strategic-drift', message: trajectory.message, severity: 'high' };
2313
- }
2314
- }
2315
-
2316
1616
  for (const gate of config.gates) {
2317
1617
  const matchDetails = matchGate(gate, toolName, toolInput);
2318
1618
  if (!matchDetails.matched) continue;
@@ -2331,39 +1631,23 @@ function evaluateGates(toolName, toolInput, configPath) {
2331
1631
  const reasoning = buildReasoning(gate, toolName, toolInput, matchDetails);
2332
1632
 
2333
1633
  if (gate.action === 'block') {
2334
- const denyResult = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
2335
- // Free-tier daily block cap: after N blocks/day, deny warn + upgrade CTA
2336
- const cappedResult = applyDailyBlockCap(denyResult);
2337
- if (cappedResult) {
2338
- recordStat(gate.id, 'warn', gate, { toolName, toolInput });
2339
- const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
2340
- auditToFeedback(auditRecord);
2341
- return cappedResult;
2342
- }
2343
- recordStat(gate.id, 'block', gate, { toolName, toolInput });
1634
+ recordStat(gate.id, 'block', gate);
1635
+ const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
2344
1636
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
2345
1637
  auditToFeedback(auditRecord);
2346
- return denyResult;
1638
+ return result;
2347
1639
  }
2348
1640
 
2349
1641
  if (gate.action === 'approve') {
2350
- const approvalEnabled = process.env.THUMBGATE_APPROVAL_GATES !== '0';
2351
- if (approvalEnabled) {
2352
- recordStat(gate.id, 'approve', gate, { toolName, toolInput });
2353
- const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
2354
- const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
2355
- auditToFeedback(auditRecord);
2356
- return result;
2357
- }
2358
- recordStat(gate.id, 'warn', gate, { toolName, toolInput });
2359
- const result = { decision: 'warn', gate: gate.id, message: `[approval gate disabled] ${message}`, severity: gate.severity, reasoning };
2360
- const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1642
+ recordStat(gate.id, 'approve', gate);
1643
+ const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
1644
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
2361
1645
  auditToFeedback(auditRecord);
2362
1646
  return result;
2363
1647
  }
2364
1648
 
2365
1649
  if (gate.action === 'log') {
2366
- recordStat(gate.id, 'log', gate, { toolName, toolInput });
1650
+ recordStat(gate.id, 'log', gate);
2367
1651
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'log', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
2368
1652
  auditToFeedback(auditRecord);
2369
1653
  // 'log' action allows the tool call to proceed — continue to next gate
@@ -2371,7 +1655,7 @@ function evaluateGates(toolName, toolInput, configPath) {
2371
1655
  }
2372
1656
 
2373
1657
  if (gate.action === 'warn') {
2374
- recordStat(gate.id, 'warn', gate, { toolName, toolInput });
1658
+ recordStat(gate.id, 'warn', gate);
2375
1659
  const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
2376
1660
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
2377
1661
  auditToFeedback(auditRecord);
@@ -2379,15 +1663,14 @@ function evaluateGates(toolName, toolInput, configPath) {
2379
1663
  }
2380
1664
  }
2381
1665
 
2382
- const skipAdvisoryGuards = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
2383
- const sentinelReport = skipAdvisoryGuards ? null : evaluateWorkflowSentinel(toolName, toolInput, {
1666
+ const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
2384
1667
  governanceState,
2385
1668
  });
2386
1669
  const sentinelDecision = recordSentinelDecision(sentinelReport, toolName, toolInput);
2387
1670
  const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
2388
1671
  if (memoryGuard) {
2389
1672
  const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
2390
- recordStat(enrichedMemoryGuard.gate, 'block', null, { toolName, toolInput });
1673
+ recordStat(enrichedMemoryGuard.gate, 'block');
2391
1674
  recordMemoryGuardDecision(sentinelDecision, enrichedMemoryGuard);
2392
1675
  const auditRecord = recordAuditEvent({
2393
1676
  toolName,
@@ -2404,7 +1687,7 @@ function evaluateGates(toolName, toolInput, configPath) {
2404
1687
 
2405
1688
  if (sentinelReport && sentinelReport.decision !== 'allow') {
2406
1689
  const sentinelResult = buildSentinelGateResult(sentinelReport);
2407
- recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
1690
+ recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
2408
1691
  recordSentinelBlockDecision(sentinelDecision, sentinelResult);
2409
1692
  const auditRecord = recordAuditEvent({
2410
1693
  toolName,
@@ -2424,43 +1707,14 @@ function evaluateGates(toolName, toolInput, configPath) {
2424
1707
  return null;
2425
1708
  }
2426
1709
 
2427
- // Turn a secret-exfiltration block into actionable guidance that names the
2428
- // safe path, instead of a dead-end that drives agents toward brittle
2429
- // workarounds (e.g. writing secrets to /tmp). The vault dirs referenced here
2430
- // are the SAME constant the scanner whitelists, so the hint can never drift
2431
- // from enforcement.
2432
- function buildSecretRemediation(toolName = '', toolInput = {}) {
2433
- const vaultDirs = (SAFE_SECRET_STORAGE_DIRS || []).map((dir) => `~/${dir}`);
2434
- const primaryVault = vaultDirs[0] || '~/.resume_secrets';
2435
- const vaultList = vaultDirs.join(', ') || primaryVault;
2436
-
2437
- if (EDIT_LIKE_TOOLS && EDIT_LIKE_TOOLS.has(toolName)) {
2438
- const target = toolInput.file_path || toolInput.path || toolInput.filePath || toolInput.target_path;
2439
- const where = target ? ` (you targeted ${redactText(String(target))})` : '';
2440
- return `To store this secret safely, write it with the Write/Edit tool to a file under ${vaultList}${where}. `
2441
- + `Those locations are whitelisted for secret storage and will NOT be blocked. `
2442
- + `Do not route around this by writing to /tmp or another path — that leaves the secret in a world-readable location and does not make it safe.`;
2443
- }
2444
-
2445
- if (toolName === 'Bash') {
2446
- return `Do not inline a live secret literal into a shell command — it leaks into shell history and process args. `
2447
- + `Instead, store the value with the Write tool to a file under ${vaultList}, then reference it via an environment variable or by reading that file at runtime.`;
2448
- }
2449
-
2450
- return `Store secrets in the whitelisted vault (${vaultList}) using the Write tool rather than passing the literal through this action.`;
2451
- }
2452
-
2453
- function buildSecretGuardResult(scanResult, context = {}) {
2454
- const remediation = buildSecretRemediation(context.toolName, context.toolInput || {});
2455
- const summary = buildSafeSummary(
2456
- scanResult.findings,
2457
- 'Blocked because the action appears to expose secret material'
2458
- );
1710
+ function buildSecretGuardResult(scanResult) {
2459
1711
  return {
2460
1712
  decision: 'deny',
2461
1713
  gate: 'secret-exfiltration',
2462
- message: `${summary}. ${remediation}`,
2463
- remediation,
1714
+ message: buildSafeSummary(
1715
+ scanResult.findings,
1716
+ 'Blocked because the action appears to expose secret material'
1717
+ ),
2464
1718
  severity: 'critical',
2465
1719
  secretScan: {
2466
1720
  provider: scanResult.provider,
@@ -2539,15 +1793,9 @@ function evaluateSecretGuard(input = {}) {
2539
1793
  if (!scanResult.detected) {
2540
1794
  return null;
2541
1795
  }
2542
- recordStat('secret-exfiltration', 'block', null, {
2543
- toolName: input.tool_name || input.toolName || 'unknown',
2544
- toolInput: input.tool_input || {},
2545
- });
1796
+ recordStat('secret-exfiltration', 'block');
2546
1797
  recordSecretViolation(input, scanResult);
2547
- const result = buildSecretGuardResult(scanResult, {
2548
- toolName: input.tool_name || input.toolName,
2549
- toolInput: input.tool_input || {},
2550
- });
1798
+ const result = buildSecretGuardResult(scanResult);
2551
1799
  // Audit trail: record secret guard denial
2552
1800
  const auditRecord = recordAuditEvent({
2553
1801
  toolName: input.tool_name || input.toolName || 'unknown',
@@ -2563,55 +1811,15 @@ function evaluateSecretGuard(input = {}) {
2563
1811
  }
2564
1812
 
2565
1813
  // ---------------------------------------------------------------------------
2566
- function isApprovalGatesEnabled() {
2567
- return process.env.THUMBGATE_APPROVAL_GATES !== '0';
2568
- }
2569
-
2570
1814
  // PreToolUse hook interface (stdin/stdout JSON)
2571
1815
  // ---------------------------------------------------------------------------
2572
1816
 
2573
- function buildPreToolUseOutput(fields = {}) {
2574
- return {
2575
- hookEventName: 'PreToolUse',
2576
- ...fields,
2577
- };
2578
- }
2579
-
2580
1817
  function buildReminderOutput(context) {
2581
- return buildPreToolUseOutput({
1818
+ return {
2582
1819
  additionalContext: context,
2583
1820
  systemReminder: context,
2584
1821
  thumbgateSystemReminder: context,
2585
- });
2586
- }
2587
-
2588
- // ---------------------------------------------------------------------------
2589
- // Upgrade nudge: surfaces Pro value at usage milestones and trial expiry.
2590
- // Block-action Pro CTA: brief upgrade mention after a deny/warn decision.
2591
- // Highest-intent moment — user just saw ThumbGate save them from a mistake.
2592
- // ---------------------------------------------------------------------------
2593
-
2594
- function buildBlockActionProCta() {
2595
- try {
2596
- if (process.env.THUMBGATE_NO_NUDGE === '1') return null;
2597
- if (process.env.CI || process.env.GITHUB_ACTIONS) return null;
2598
- if (isProTier()) return null;
2599
- if (isInTrialPeriod()) return null; // Already have full access
2600
-
2601
- const stats = loadStats();
2602
- const totalBlocks = stats.blocked || 0;
2603
- if (totalBlocks < 5) return null; // Too early — let them experience the product
2604
-
2605
- if (totalBlocks < 25) {
2606
- return '\n\n💡 Pro: keep this rule synced across laptops, CI, containers, and agent runtimes → thumbgate.ai/go/pro';
2607
- }
2608
- if (totalBlocks < 100) {
2609
- return `\n\n💡 ${totalBlocks} actions blocked. Pro keeps these lessons/rules synced everywhere → thumbgate.ai/go/pro ($19/mo)`;
2610
- }
2611
- return `\n\n💡 ${totalBlocks} mistakes caught. Your team could use shared hosted enforcement → thumbgate.ai/go/pro`;
2612
- } catch (_) {
2613
- return null;
2614
- }
1822
+ };
2615
1823
  }
2616
1824
 
2617
1825
  function formatOutput(result, behavioralContext) {
@@ -2630,28 +1838,13 @@ function formatOutput(result, behavioralContext) {
2630
1838
  : '';
2631
1839
 
2632
1840
  if (result.decision === 'deny') {
2633
- const reminder = behavioralContext ? buildReminderOutput(behavioralContext) : {};
2634
- const reminderSuffix = behavioralContext ? `\n\nSystem reminder:\n${behavioralContext}` : '';
2635
- const proCta = buildBlockActionProCta() || '';
2636
- return JSON.stringify({
2637
- hookSpecificOutput: {
2638
- hookEventName: 'PreToolUse',
2639
- ...reminder,
2640
- permissionDecision: 'deny',
2641
- permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}${reminderSuffix}${proCta}`,
2642
- },
2643
- });
2644
- }
2645
-
2646
- if (result.decision === 'approve') {
2647
1841
  const reminder = behavioralContext ? buildReminderOutput(behavioralContext) : {};
2648
1842
  const reminderSuffix = behavioralContext ? `\n\nSystem reminder:\n${behavioralContext}` : '';
2649
1843
  return JSON.stringify({
2650
1844
  hookSpecificOutput: {
2651
- hookEventName: 'PreToolUse',
2652
1845
  ...reminder,
2653
1846
  permissionDecision: 'deny',
2654
- permissionDecisionReason: `[GATE:${result.gate}] APPROVAL REQUIRED: ${result.message} — Ask the human to confirm this action before proceeding.${reasoningSuffix}${reminderSuffix}`,
1847
+ permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}${reminderSuffix}`,
2655
1848
  },
2656
1849
  });
2657
1850
  }
@@ -2661,7 +1854,6 @@ function formatOutput(result, behavioralContext) {
2661
1854
  const context = `[GATE:${result.gate}] WARNING: ${result.message}${reasoningSuffix}${extra}`;
2662
1855
  return JSON.stringify({
2663
1856
  hookSpecificOutput: {
2664
- hookEventName: 'PreToolUse',
2665
1857
  additionalContext: context,
2666
1858
  ...(behavioralContext ? {
2667
1859
  systemReminder: behavioralContext,
@@ -2785,7 +1977,9 @@ function buildRecentCorrectiveActionsContext(options = {}) {
2785
1977
  function buildRelevantLessonContext(toolName, toolInput) {
2786
1978
  if (!toolName) return null;
2787
1979
 
2788
- const { retrieveRelevantLessons, calculateRetrievalEntropy, filterTopP } = loadOptionalModule("./lesson-retrieval", () => ({ retrieveRelevantLessons: () => [], calculateRetrievalEntropy: () => 0, filterTopP: (l) => l }));
1980
+ const { retrieveRelevantLessons } = loadOptionalModule('./lesson-retrieval', () => ({
1981
+ retrieveRelevantLessons: () => [],
1982
+ }));
2789
1983
 
2790
1984
  // Extract a searchable action context from the tool input
2791
1985
  const actionContext = extractActionContext(toolName, toolInput);
@@ -2793,113 +1987,32 @@ function buildRelevantLessonContext(toolName, toolInput) {
2793
1987
 
2794
1988
  try {
2795
1989
  const lessons = retrieveRelevantLessons(toolName, actionContext, { maxResults: 3 });
1990
+ // retrieveRelevantLessons already filters at relevanceScore > 0.1 internally;
1991
+ // any negative lesson that survives retrieval is relevant enough to surface.
1992
+ const negative = lessons.filter((l) => l.signal === 'negative');
1993
+ if (negative.length === 0) return null;
1994
+
1995
+ const formatted = negative.map((l) => {
1996
+ const title = (l.title || '').replace(/^MISTAKE:\s*/, '').slice(0, 140);
1997
+ const advice = extractAvoidanceAdvice(l.content);
1998
+ return advice ? ` • ${title}\n → ${advice}` : ` • ${title}`;
1999
+ });
2796
2000
 
2797
- const entropy = calculateRetrievalEntropy(lessons);
2798
- if (entropy > KNOWLEDGE_ENTROPY_THRESHOLD) {
2799
- return buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy);
2800
- }
2801
- return formatNegativeLessonContext(lessons);
2802
- } catch {
2803
- return null;
2804
- }
2805
- }
2806
-
2807
- /**
2808
- * Async counterpart of buildRelevantLessonContext: uses HYBRID (dense embeddings +
2809
- * lexical) retrieval so the agent is warned about semantically-related past mistakes
2810
- * even when they share no keywords with the current action. Wired into runAsync.
2811
- * Degrades to the lexical result automatically when no embedder is available.
2812
- */
2813
- async function buildRelevantLessonContextAsync(toolName, toolInput) {
2814
- if (!toolName) return null;
2815
-
2816
- const { retrieveRelevantLessonsAsync, retrieveRelevantLessons, calculateRetrievalEntropy } = loadOptionalModule(
2817
- './lesson-retrieval',
2818
- () => ({ retrieveRelevantLessonsAsync: null, retrieveRelevantLessons: () => [], calculateRetrievalEntropy: () => 0 }),
2819
- );
2820
-
2821
- const actionContext = extractActionContext(toolName, toolInput);
2822
- if (!actionContext) return null;
2823
-
2824
- try {
2825
- const lessons = retrieveRelevantLessonsAsync
2826
- ? await retrieveRelevantLessonsAsync(toolName, actionContext, { maxResults: 3 })
2827
- : retrieveRelevantLessons(toolName, actionContext, { maxResults: 3 });
2828
-
2829
- // Knowledge Conflict Detection: if retrieved lessons have high sentiment entropy,
2830
- // it indicates conflicting past evidence. Warn by default; hard-block only in
2831
- // strict mode for external/destructive side-effect commands.
2832
- const entropy = calculateRetrievalEntropy(lessons);
2833
- if (entropy > KNOWLEDGE_ENTROPY_THRESHOLD) {
2834
- return buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy);
2835
- }
2836
-
2837
- return formatNegativeLessonContext(lessons);
2001
+ return `[ThumbGate] Past mistakes relevant to this action — read before proceeding:\n${formatted.join('\n')}`;
2838
2002
  } catch {
2839
2003
  return null;
2840
2004
  }
2841
2005
  }
2842
2006
 
2843
- /**
2844
- * Shared formatter: render the negative (mistake) lessons that survived retrieval
2845
- * into the PreToolUse warning block. Retrieval already filters by relevance, so any
2846
- * negative lesson present is relevant enough to surface.
2847
- */
2848
- function formatNegativeLessonContext(lessons) {
2849
- const negative = (lessons || []).filter((l) => l.signal === 'negative');
2850
- if (negative.length === 0) return null;
2851
-
2852
- const formatted = negative.map((l) => {
2853
- const title = (l.title || '').replace(/^MISTAKE:\s*/, '').slice(0, 140);
2854
- const advice = extractAvoidanceAdvice(l.content);
2855
- return advice ? ` • ${title}\n → ${advice}` : ` • ${title}`;
2856
- });
2857
-
2858
- return `[ThumbGate] Past mistakes relevant to this action — read before proceeding:\n${formatted.join('\n')}`;
2859
- }
2860
-
2861
- function isStrictKnowledgeConflictMode() {
2862
- return process.env.THUMBGATE_STRICT_KNOWLEDGE_CONFLICT === '1'
2863
- || process.env.THUMBGATE_STRICT_KNOWLEDGE_CONFLICT === 'true';
2864
- }
2865
-
2866
- function isKnowledgeConflictHardBlockAction(toolName, toolInput = {}) {
2867
- if (!isStrictKnowledgeConflictMode()) return false;
2868
- if (EDIT_LIKE_TOOLS.has(toolName)) return true;
2869
- if (toolName !== 'Bash') return false;
2870
- return KNOWLEDGE_CONFLICT_STRICT_BASH_PATTERN.test(String(toolInput.command || ''));
2871
- }
2872
-
2873
- function buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy) {
2874
- const lessonContext = formatNegativeLessonContext(lessons);
2875
- const message = `Knowledge conflict warning: retrieved lessons disagree for this action (entropy ${entropy}). Treat the reminders below as cautionary context, but do not stop unrelated work solely because memory is noisy.`;
2876
-
2877
- if (isKnowledgeConflictHardBlockAction(toolName, toolInput)) {
2878
- recordStat('retrieval_entropy_high', 'block', null, { toolName, toolInput });
2879
- return {
2880
- decision: 'deny',
2881
- gate: 'knowledge-conflict-gate',
2882
- message: `✗ THUMBGATE: ${message} Strict mode is enabled for destructive or external side-effect actions; verify intent or narrow the task before proceeding.`,
2883
- severity: 'high',
2884
- };
2885
- }
2886
-
2887
- recordStat('retrieval_entropy_high', 'warn', null, { toolName, toolInput });
2888
- return mergeContextStrings(`[ThumbGate] ${message}`, lessonContext);
2889
- }
2890
-
2891
2007
  function extractActionContext(toolName, toolInput) {
2892
2008
  if (!toolInput) return toolName;
2893
2009
  const parts = [toolName];
2894
2010
  if (toolInput.command) parts.push(String(toolInput.command).slice(0, 400));
2895
2011
  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));
2899
2012
  if (toolInput.description) parts.push(String(toolInput.description).slice(0, 200));
2900
2013
  if (toolInput.prompt) parts.push(String(toolInput.prompt).slice(0, 400));
2901
2014
  if (toolInput.pattern) parts.push(String(toolInput.pattern).slice(0, 200));
2902
- return sanitizeFeedbackText(parts.filter(Boolean).join(' ')) || toolName;
2015
+ return parts.filter(Boolean).join(' ');
2903
2016
  }
2904
2017
 
2905
2018
  function extractAvoidanceAdvice(content) {
@@ -2928,13 +2041,6 @@ async function runAsync(input) {
2928
2041
 
2929
2042
  const toolName = input.tool_name || '';
2930
2043
  const toolInput = input.tool_input || {};
2931
- const safeSecretStorageWrite = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
2932
-
2933
- const sequenceGuard = evaluateSequenceState(toolName, toolInput);
2934
- if (sequenceGuard && sequenceGuard.decision === 'deny') {
2935
- return formatOutput(applyEnforcementPosture(sequenceGuard));
2936
- }
2937
-
2938
2044
  const result = await evaluateGatesAsync(toolName, toolInput);
2939
2045
 
2940
2046
  // Attach security warnings to allow/warn results
@@ -2947,18 +2053,11 @@ async function runAsync(input) {
2947
2053
  }
2948
2054
  }
2949
2055
 
2950
-
2951
- const behavioralContext = safeSecretStorageWrite ? null : buildBehavioralContext();
2952
- const lessonContext = safeSecretStorageWrite ? null : await buildRelevantLessonContextAsync(toolName, toolInput);
2953
-
2954
- if (lessonContext && lessonContext.decision === "deny") {
2955
- return formatOutput(applyEnforcementPosture(lessonContext));
2956
- }
2957
-
2056
+ const behavioralContext = buildBehavioralContext();
2057
+ const lessonContext = buildRelevantLessonContext(toolName, toolInput);
2958
2058
  const recentContext = buildRecentCorrectiveActionsContext();
2959
2059
  const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
2960
- return formatOutput(applyEnforcementPosture(result), combinedContext);
2961
-
2060
+ return formatOutput(result, combinedContext);
2962
2061
  }
2963
2062
 
2964
2063
  function run(input) {
@@ -2975,13 +2074,6 @@ function run(input) {
2975
2074
 
2976
2075
  const toolName = input.tool_name || '';
2977
2076
  const toolInput = input.tool_input || {};
2978
- const safeSecretStorageWrite = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
2979
-
2980
- const sequenceGuard = evaluateSequenceState(toolName, toolInput);
2981
- if (sequenceGuard && sequenceGuard.decision === 'deny') {
2982
- return formatOutput(applyEnforcementPosture(sequenceGuard));
2983
- }
2984
-
2985
2077
  const result = evaluateGates(toolName, toolInput);
2986
2078
 
2987
2079
  // Attach security warnings to allow/warn results
@@ -2994,18 +2086,11 @@ function run(input) {
2994
2086
  }
2995
2087
  }
2996
2088
 
2997
-
2998
- const behavioralContext = safeSecretStorageWrite ? null : buildBehavioralContext();
2999
- const lessonContext = safeSecretStorageWrite ? null : buildRelevantLessonContext(toolName, toolInput);
3000
-
3001
- if (lessonContext && lessonContext.decision === "deny") {
3002
- return formatOutput(applyEnforcementPosture(lessonContext));
3003
- }
3004
-
2089
+ const behavioralContext = buildBehavioralContext();
2090
+ const lessonContext = buildRelevantLessonContext(toolName, toolInput);
3005
2091
  const recentContext = buildRecentCorrectiveActionsContext();
3006
2092
  const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
3007
- return formatOutput(applyEnforcementPosture(result), combinedContext);
3008
-
2093
+ return formatOutput(result, combinedContext);
3009
2094
  }
3010
2095
 
3011
2096
  // ---------------------------------------------------------------------------
@@ -3138,84 +2223,7 @@ function registerClaimGate(claimPattern, requiredActions, blockMessage) {
3138
2223
  return entry;
3139
2224
  }
3140
2225
 
3141
- function normalizeStringArray(value) {
3142
- if (!Array.isArray(value)) return [];
3143
- return Array.from(new Set(
3144
- value
3145
- .map((item) => String(item || '').trim())
3146
- .filter(Boolean)
3147
- ));
3148
- }
3149
-
3150
- function normalizeGoalContract(goalContract) {
3151
- if (!goalContract || typeof goalContract !== 'object' || Array.isArray(goalContract)) {
3152
- return null;
3153
- }
3154
-
3155
- const goal = String(goalContract.goal || '').trim();
3156
- const doneWhen = normalizeStringArray(goalContract.doneWhen);
3157
- const requiredActions = normalizeStringArray(goalContract.proveBy);
3158
- const mustNotChange = normalizeStringArray(goalContract.mustNotChange);
3159
- const handoff = {
3160
- workerAgent: String(goalContract.workerAgent || '').trim() || null,
3161
- reviewerAgent: String(goalContract.reviewerAgent || '').trim() || null,
3162
- orchestratorAgent: String(goalContract.orchestratorAgent || '').trim() || null,
3163
- };
3164
-
3165
- const matched = Boolean(
3166
- goal ||
3167
- doneWhen.length > 0 ||
3168
- requiredActions.length > 0 ||
3169
- mustNotChange.length > 0 ||
3170
- handoff.workerAgent ||
3171
- handoff.reviewerAgent ||
3172
- handoff.orchestratorAgent
3173
- );
3174
-
3175
- if (!matched) return null;
3176
-
3177
- return {
3178
- goal: goal || null,
3179
- doneWhen,
3180
- requiredActions,
3181
- mustNotChange,
3182
- handoff,
3183
- };
3184
- }
3185
-
3186
- function evaluateGoalContract(goalContract, actions = loadSessionActions()) {
3187
- const normalized = normalizeGoalContract(goalContract);
3188
- if (!normalized) {
3189
- return {
3190
- matched: false,
3191
- passed: true,
3192
- goal: null,
3193
- doneWhen: [],
3194
- requiredActions: [],
3195
- missingActions: [],
3196
- mustNotChange: [],
3197
- handoff: {
3198
- workerAgent: null,
3199
- reviewerAgent: null,
3200
- orchestratorAgent: null,
3201
- },
3202
- };
3203
- }
3204
-
3205
- const missingActions = normalized.requiredActions.filter((actionId) => !actions[actionId]);
3206
- return {
3207
- matched: true,
3208
- passed: missingActions.length === 0,
3209
- goal: normalized.goal,
3210
- doneWhen: normalized.doneWhen,
3211
- requiredActions: normalized.requiredActions,
3212
- missingActions,
3213
- mustNotChange: normalized.mustNotChange,
3214
- handoff: normalized.handoff,
3215
- };
3216
- }
3217
-
3218
- function verifyClaimEvidence(claimText, options = {}) {
2226
+ function verifyClaimEvidence(claimText) {
3219
2227
  const normalizedClaimText = String(claimText || '').trim();
3220
2228
  if (!normalizedClaimText) {
3221
2229
  throw new Error('claimText is required');
@@ -3243,23 +2251,9 @@ function verifyClaimEvidence(claimText, options = {}) {
3243
2251
  });
3244
2252
  }
3245
2253
 
3246
- const goalContract = evaluateGoalContract(options.goalContract, actions);
3247
- if (goalContract.matched) {
3248
- checks.push({
3249
- claim: 'goal_contract',
3250
- passed: goalContract.passed,
3251
- missing: goalContract.missingActions,
3252
- message: goalContract.passed
3253
- ? 'Goal contract evidence present'
3254
- : `Goal contract requires evidence: ${goalContract.missingActions.join(', ')}`,
3255
- goalContract,
3256
- });
3257
- }
3258
-
3259
2254
  return {
3260
2255
  verified: checks.every((check) => check.passed),
3261
2256
  checks,
3262
- goalContract,
3263
2257
  };
3264
2258
  }
3265
2259
 
@@ -3279,7 +2273,6 @@ module.exports = {
3279
2273
  setTaskScope,
3280
2274
  setBranchGovernance,
3281
2275
  approveProtectedAction,
3282
- breakGlassEmergency,
3283
2276
  getScopeState,
3284
2277
  getBranchGovernanceState,
3285
2278
  isConditionSatisfied,
@@ -3296,7 +2289,6 @@ module.exports = {
3296
2289
  evaluateGatesAsync,
3297
2290
  computeExecutableHash,
3298
2291
  formatOutput,
3299
- isApprovalGatesEnabled,
3300
2292
  run,
3301
2293
  runAsync,
3302
2294
  trackAction,
@@ -3305,8 +2297,6 @@ module.exports = {
3305
2297
  clearSessionActions,
3306
2298
  loadClaimGates,
3307
2299
  registerClaimGate,
3308
- normalizeGoalContract,
3309
- evaluateGoalContract,
3310
2300
  verifyClaimEvidence,
3311
2301
  DEFAULT_CONFIG_PATH,
3312
2302
  DEFAULT_CLAIM_GATES_PATH,
@@ -3323,7 +2313,6 @@ module.exports = {
3323
2313
  buildBehavioralContext,
3324
2314
  buildRecentCorrectiveActionsContext,
3325
2315
  buildRelevantLessonContext,
3326
- buildRelevantLessonContextAsync,
3327
2316
  extractActionContext,
3328
2317
  extractAvoidanceAdvice,
3329
2318
  mergeContextStrings,
@@ -3338,16 +2327,7 @@ module.exports = {
3338
2327
  getLocalOnlyScopeSources,
3339
2328
  isRemoteSideEffectCommand,
3340
2329
  evaluateLocalOnlyRemoteSideEffectGate,
3341
- recordHelperScriptWrite,
3342
- evaluateStatefulHelperBypassGate,
3343
- isAgentHookSettingsFile,
3344
- isBreakGlassSettingsRecoveryAction,
3345
2330
  PR_THREAD_RESOLUTION_ACTION,
3346
- HELPER_BYPASS_ACTION,
3347
- buildBlockActionProCta,
3348
- applyDailyBlockCap,
3349
- getTodayBlockCount,
3350
- incrementTodayBlockCount,
3351
2331
  };
3352
2332
 
3353
2333
  // ---------------------------------------------------------------------------