thumbgate 0.9.10

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 (364) hide show
  1. package/.claude-plugin/README.md +134 -0
  2. package/.claude-plugin/bundle/icon.png +0 -0
  3. package/.claude-plugin/bundle/icon.svg +18 -0
  4. package/.claude-plugin/bundle/server/index.js +24 -0
  5. package/.claude-plugin/marketplace.json +36 -0
  6. package/.claude-plugin/plugin.json +21 -0
  7. package/.well-known/mcp/server-card.json +231 -0
  8. package/LICENSE +21 -0
  9. package/README.md +375 -0
  10. package/adapters/README.md +9 -0
  11. package/adapters/amp/skills/thumbgate-feedback/SKILL.md +22 -0
  12. package/adapters/chatgpt/INSTALL.md +83 -0
  13. package/adapters/chatgpt/openapi.yaml +1281 -0
  14. package/adapters/claude/.mcp.json +14 -0
  15. package/adapters/codex/config.toml +9 -0
  16. package/adapters/gemini/function-declarations.json +224 -0
  17. package/adapters/mcp/server-stdio.js +788 -0
  18. package/adapters/opencode/opencode.json +15 -0
  19. package/bin/cli.js +1484 -0
  20. package/bin/memory.sh +64 -0
  21. package/bin/obsidian-sync.sh +20 -0
  22. package/bin/postinstall.js +37 -0
  23. package/config/build-metadata.json +4 -0
  24. package/config/e2e-critical-flows.json +45 -0
  25. package/config/gate-templates.json +77 -0
  26. package/config/gates/claim-verification.json +29 -0
  27. package/config/gates/computer-use.json +39 -0
  28. package/config/gates/default.json +117 -0
  29. package/config/github-about.json +25 -0
  30. package/config/mcp-allowlists.json +135 -0
  31. package/config/model-tiers.json +33 -0
  32. package/config/partner-routing.json +132 -0
  33. package/config/policy-bundles/constrained-v1.json +64 -0
  34. package/config/policy-bundles/default-v1.json +91 -0
  35. package/config/rubrics/default-v1.json +52 -0
  36. package/config/skill-packs/react-testing.json +23 -0
  37. package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
  38. package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
  39. package/config/skill-specs/pr-reviewer.json +9 -0
  40. package/config/skill-specs/release-status.json +9 -0
  41. package/config/skill-specs/ticket-triage.json +9 -0
  42. package/config/subagent-profiles.json +32 -0
  43. package/config/tessl-tiles.json +29 -0
  44. package/config/thumbgate-settings.managed.json +12 -0
  45. package/openapi/openapi.yaml +1281 -0
  46. package/package.json +283 -0
  47. package/plugins/amp-skill/INSTALL.md +52 -0
  48. package/plugins/amp-skill/SKILL.md +64 -0
  49. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
  50. package/plugins/claude-codex-bridge/.mcp.json +12 -0
  51. package/plugins/claude-codex-bridge/INSTALL.md +43 -0
  52. package/plugins/claude-codex-bridge/README.md +46 -0
  53. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
  54. package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
  55. package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
  56. package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
  57. package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
  58. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
  59. package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
  60. package/plugins/claude-skill/INSTALL.md +55 -0
  61. package/plugins/claude-skill/SKILL.md +46 -0
  62. package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
  63. package/plugins/codex-profile/.mcp.json +12 -0
  64. package/plugins/codex-profile/AGENTS.md +20 -0
  65. package/plugins/codex-profile/INSTALL.md +66 -0
  66. package/plugins/codex-profile/README.md +37 -0
  67. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
  68. package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
  69. package/plugins/cursor-marketplace/LICENSE +21 -0
  70. package/plugins/cursor-marketplace/README.md +124 -0
  71. package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
  72. package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
  73. package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
  74. package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
  75. package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
  76. package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
  77. package/plugins/cursor-marketplace/mcp.json +12 -0
  78. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
  79. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
  80. package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
  81. package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
  82. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
  83. package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
  84. package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
  85. package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
  86. package/plugins/gemini-extension/INSTALL.md +92 -0
  87. package/plugins/gemini-extension/gemini_prompt.txt +14 -0
  88. package/plugins/gemini-extension/tool_contract.json +45 -0
  89. package/plugins/opencode-profile/INSTALL.md +57 -0
  90. package/public/assets/instagram-card.png +0 -0
  91. package/public/assets/tiktok-agent-memory.mp4 +0 -0
  92. package/public/blog.html +400 -0
  93. package/public/dashboard.html +1093 -0
  94. package/public/guide.html +317 -0
  95. package/public/index.html +1014 -0
  96. package/public/learn/agent-harness-pattern.html +180 -0
  97. package/public/learn/ai-agent-persistent-memory.html +202 -0
  98. package/public/learn/learn.css +45 -0
  99. package/public/learn/mcp-pre-action-gates-explained.html +172 -0
  100. package/public/learn/stop-ai-agent-force-push.html +134 -0
  101. package/public/learn/vibe-coding-safety-net.html +142 -0
  102. package/public/learn.html +213 -0
  103. package/public/lessons.html +650 -0
  104. package/public/vercel.json +8 -0
  105. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  106. package/scripts/a2ui-engine.js +73 -0
  107. package/scripts/access-anomaly-detector.js +12 -0
  108. package/scripts/adk-consolidator.js +266 -0
  109. package/scripts/agent-readiness.js +220 -0
  110. package/scripts/agent-security-hardening.js +227 -0
  111. package/scripts/agentic-data-pipeline.js +847 -0
  112. package/scripts/analytics-report.js +328 -0
  113. package/scripts/analytics-window.js +158 -0
  114. package/scripts/async-job-runner.js +1001 -0
  115. package/scripts/audit-trail.js +398 -0
  116. package/scripts/auto-promote-gates.js +299 -0
  117. package/scripts/auto-wire-hooks.js +312 -0
  118. package/scripts/autonomous-sales-agent.js +39 -0
  119. package/scripts/autoresearch-runner.js +216 -0
  120. package/scripts/background-agent-governance.js +237 -0
  121. package/scripts/behavioral-extraction.js +97 -0
  122. package/scripts/belief-update.js +84 -0
  123. package/scripts/billing.js +2438 -0
  124. package/scripts/bot-detector.js +50 -0
  125. package/scripts/budget-guard.js +173 -0
  126. package/scripts/build-claude-mcpb.js +189 -0
  127. package/scripts/build-metadata.js +97 -0
  128. package/scripts/check-congruence.js +322 -0
  129. package/scripts/cli-feedback.js +135 -0
  130. package/scripts/cli-telemetry.js +87 -0
  131. package/scripts/cloudflare-dynamic-sandbox.js +315 -0
  132. package/scripts/code-reasoning.js +350 -0
  133. package/scripts/codegraph-context.js +466 -0
  134. package/scripts/commercial-offer.js +56 -0
  135. package/scripts/computer-use-firewall.js +250 -0
  136. package/scripts/context-engine.js +694 -0
  137. package/scripts/contextfs.js +1287 -0
  138. package/scripts/conversation-context.js +119 -0
  139. package/scripts/creator-campaigns.js +239 -0
  140. package/scripts/daemon-manager.js +108 -0
  141. package/scripts/daily-digest.js +11 -0
  142. package/scripts/dashboard-render-spec.js +395 -0
  143. package/scripts/dashboard.js +1058 -0
  144. package/scripts/data-governance.js +173 -0
  145. package/scripts/delegation-runtime.js +900 -0
  146. package/scripts/deploy-gcp.sh +44 -0
  147. package/scripts/deploy-policy.js +263 -0
  148. package/scripts/disagreement-mining.js +315 -0
  149. package/scripts/dispatch-brief.js +159 -0
  150. package/scripts/distribution-surfaces.js +44 -0
  151. package/scripts/dpo-optimizer.js +209 -0
  152. package/scripts/ephemeral-agent-store.js +219 -0
  153. package/scripts/eval-harness.js +56 -0
  154. package/scripts/evolution-state.js +241 -0
  155. package/scripts/experiment-tracker.js +267 -0
  156. package/scripts/export-databricks-bundle.js +242 -0
  157. package/scripts/export-dpo-pairs.js +345 -0
  158. package/scripts/export-kto-pairs.js +310 -0
  159. package/scripts/export-training.js +448 -0
  160. package/scripts/failure-diagnostics.js +558 -0
  161. package/scripts/feedback-attribution.js +313 -0
  162. package/scripts/feedback-fallback.js +111 -0
  163. package/scripts/feedback-history-distiller.js +391 -0
  164. package/scripts/feedback-inbox-read.js +162 -0
  165. package/scripts/feedback-loop.js +1887 -0
  166. package/scripts/feedback-paths.js +145 -0
  167. package/scripts/feedback-quality.js +139 -0
  168. package/scripts/feedback-root-consolidator.js +238 -0
  169. package/scripts/feedback-schema.js +426 -0
  170. package/scripts/feedback-session.js +286 -0
  171. package/scripts/feedback-to-memory.js +185 -0
  172. package/scripts/feedback-to-rules.js +163 -0
  173. package/scripts/filesystem-search.js +404 -0
  174. package/scripts/funnel-analytics.js +35 -0
  175. package/scripts/gate-satisfy.js +42 -0
  176. package/scripts/gate-stats.js +116 -0
  177. package/scripts/gate-templates.js +70 -0
  178. package/scripts/gates-engine.js +816 -0
  179. package/scripts/generate-paperbanana-diagrams.sh +99 -0
  180. package/scripts/generate-pretool-hook.sh +40 -0
  181. package/scripts/github-about.js +350 -0
  182. package/scripts/github-outreach.js +65 -0
  183. package/scripts/gtm-revenue-loop.js +520 -0
  184. package/scripts/hallucination-detector.js +226 -0
  185. package/scripts/hf-papers.js +317 -0
  186. package/scripts/history-distiller.js +200 -0
  187. package/scripts/hook-auto-capture.sh +95 -0
  188. package/scripts/hook-stop-pr-thread-check.sh +68 -0
  189. package/scripts/hook-stop-self-score.sh +51 -0
  190. package/scripts/hook-stop-verify-deploy.sh +31 -0
  191. package/scripts/hook-thumbgate-cache-updater.js +48 -0
  192. package/scripts/hook-verify-before-done.sh +20 -0
  193. package/scripts/hosted-config.js +170 -0
  194. package/scripts/hybrid-feedback-context.js +676 -0
  195. package/scripts/install-mcp.js +159 -0
  196. package/scripts/intent-router.js +392 -0
  197. package/scripts/internal-agent-bootstrap.js +490 -0
  198. package/scripts/jsonl-watcher.js +155 -0
  199. package/scripts/lesson-db.js +613 -0
  200. package/scripts/lesson-inference.js +315 -0
  201. package/scripts/lesson-retrieval.js +95 -0
  202. package/scripts/lesson-rotation.js +137 -0
  203. package/scripts/lesson-search.js +644 -0
  204. package/scripts/lesson-synthesis.js +196 -0
  205. package/scripts/license.js +50 -0
  206. package/scripts/local-model-profile.js +383 -0
  207. package/scripts/markdown-escape.js +12 -0
  208. package/scripts/marketing-experiment.js +671 -0
  209. package/scripts/mcp-config.js +149 -0
  210. package/scripts/mcp-policy.js +99 -0
  211. package/scripts/memalign-recall.js +111 -0
  212. package/scripts/memory-firewall.js +222 -0
  213. package/scripts/memory-migration.js +296 -0
  214. package/scripts/meta-policy.js +194 -0
  215. package/scripts/metered-billing.js +16 -0
  216. package/scripts/model-tier-router.js +301 -0
  217. package/scripts/money-watcher.js +71 -0
  218. package/scripts/multi-hop-recall.js +240 -0
  219. package/scripts/natural-language-harness.js +330 -0
  220. package/scripts/obsidian-export.js +712 -0
  221. package/scripts/operational-dashboard.js +103 -0
  222. package/scripts/operational-summary.js +93 -0
  223. package/scripts/optimize-context.js +17 -0
  224. package/scripts/org-dashboard.js +201 -0
  225. package/scripts/partner-orchestration.js +146 -0
  226. package/scripts/per-step-scoring.js +165 -0
  227. package/scripts/perplexity-marketing.js +466 -0
  228. package/scripts/pii-scanner.js +153 -0
  229. package/scripts/plan-gate.js +154 -0
  230. package/scripts/post-everywhere.js +308 -0
  231. package/scripts/post-to-x-retry.sh +22 -0
  232. package/scripts/post-to-x.js +369 -0
  233. package/scripts/pr-manager.js +236 -0
  234. package/scripts/predictive-insights.js +356 -0
  235. package/scripts/principle-extractor.js +162 -0
  236. package/scripts/pro-features.js +40 -0
  237. package/scripts/pro-local-dashboard.js +174 -0
  238. package/scripts/problem-detail.js +53 -0
  239. package/scripts/product-feedback.js +134 -0
  240. package/scripts/profile-router.js +245 -0
  241. package/scripts/prompt-dlp.js +221 -0
  242. package/scripts/prompt-guard.js +83 -0
  243. package/scripts/prove-adapters.js +863 -0
  244. package/scripts/prove-attribution.js +365 -0
  245. package/scripts/prove-automation.js +653 -0
  246. package/scripts/prove-autoresearch.js +304 -0
  247. package/scripts/prove-claim-verification.js +277 -0
  248. package/scripts/prove-cloudflare-sandbox.js +163 -0
  249. package/scripts/prove-data-pipeline.js +410 -0
  250. package/scripts/prove-data-quality.js +227 -0
  251. package/scripts/prove-evolution.js +352 -0
  252. package/scripts/prove-harnesses.js +287 -0
  253. package/scripts/prove-intelligence.js +259 -0
  254. package/scripts/prove-lancedb.js +371 -0
  255. package/scripts/prove-local-intelligence.js +342 -0
  256. package/scripts/prove-loop-closure.js +263 -0
  257. package/scripts/prove-predictive-insights.js +357 -0
  258. package/scripts/prove-runtime.js +350 -0
  259. package/scripts/prove-seo-gsd.js +234 -0
  260. package/scripts/prove-settings.js +279 -0
  261. package/scripts/prove-subway-upgrades.js +277 -0
  262. package/scripts/prove-tessl.js +229 -0
  263. package/scripts/prove-training-export.js +327 -0
  264. package/scripts/prove-workflow-contract.js +116 -0
  265. package/scripts/prove-xmemory.js +332 -0
  266. package/scripts/publish-decision.js +133 -0
  267. package/scripts/pulse.js +80 -0
  268. package/scripts/rate-limiter.js +125 -0
  269. package/scripts/reddit-dm-outreach.js +182 -0
  270. package/scripts/reddit-monitor-cron.sh +26 -0
  271. package/scripts/reflector-agent.js +221 -0
  272. package/scripts/reminder-engine.js +132 -0
  273. package/scripts/revenue-status.js +472 -0
  274. package/scripts/risk-scorer.js +458 -0
  275. package/scripts/rlaif-self-audit.js +129 -0
  276. package/scripts/rubric-engine.js +230 -0
  277. package/scripts/schedule-manager.js +251 -0
  278. package/scripts/secret-scanner.js +414 -0
  279. package/scripts/self-heal.js +147 -0
  280. package/scripts/self-healing-check.js +188 -0
  281. package/scripts/semantic-layer.js +98 -0
  282. package/scripts/seo-gsd.js +1153 -0
  283. package/scripts/settings-hierarchy.js +214 -0
  284. package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
  285. package/scripts/skill-exporter.js +262 -0
  286. package/scripts/skill-generator.js +446 -0
  287. package/scripts/skill-materializer.js +134 -0
  288. package/scripts/skill-packs.js +136 -0
  289. package/scripts/skill-proposer.js +99 -0
  290. package/scripts/skill-quality-tracker.js +284 -0
  291. package/scripts/slo-alert-engine.js +14 -0
  292. package/scripts/slow-loop.js +72 -0
  293. package/scripts/social-analytics/db/schema.sql +32 -0
  294. package/scripts/social-analytics/digest.js +256 -0
  295. package/scripts/social-analytics/generate-instagram-card.js +97 -0
  296. package/scripts/social-analytics/instagram-thumbgate-post.js +73 -0
  297. package/scripts/social-analytics/mcp-server.js +289 -0
  298. package/scripts/social-analytics/normalizer.js +580 -0
  299. package/scripts/social-analytics/notify.js +162 -0
  300. package/scripts/social-analytics/poll-all.js +107 -0
  301. package/scripts/social-analytics/pollers/github.js +195 -0
  302. package/scripts/social-analytics/pollers/instagram.js +253 -0
  303. package/scripts/social-analytics/pollers/linkedin.js +330 -0
  304. package/scripts/social-analytics/pollers/plausible.js +247 -0
  305. package/scripts/social-analytics/pollers/reddit.js +306 -0
  306. package/scripts/social-analytics/pollers/threads.js +233 -0
  307. package/scripts/social-analytics/pollers/tiktok.js +203 -0
  308. package/scripts/social-analytics/pollers/x.js +227 -0
  309. package/scripts/social-analytics/pollers/youtube.js +304 -0
  310. package/scripts/social-analytics/pollers/zernio.js +180 -0
  311. package/scripts/social-analytics/publish-instagram-thumbgate.js +85 -0
  312. package/scripts/social-analytics/publishers/devto.js +122 -0
  313. package/scripts/social-analytics/publishers/instagram.js +317 -0
  314. package/scripts/social-analytics/publishers/linkedin.js +294 -0
  315. package/scripts/social-analytics/publishers/reddit.js +390 -0
  316. package/scripts/social-analytics/publishers/threads.js +275 -0
  317. package/scripts/social-analytics/publishers/tiktok.js +217 -0
  318. package/scripts/social-analytics/publishers/x.js +259 -0
  319. package/scripts/social-analytics/publishers/youtube.js +223 -0
  320. package/scripts/social-analytics/publishers/zernio.js +209 -0
  321. package/scripts/social-analytics/run-digest.js +34 -0
  322. package/scripts/social-analytics/store.js +257 -0
  323. package/scripts/social-analytics/utm.js +143 -0
  324. package/scripts/social-pipeline.js +2628 -0
  325. package/scripts/social-quality-gate.js +18 -0
  326. package/scripts/social-reply-monitor.js +445 -0
  327. package/scripts/status-dashboard.js +155 -0
  328. package/scripts/statusline-lesson.js +16 -0
  329. package/scripts/statusline-tower.js +8 -0
  330. package/scripts/statusline.sh +116 -0
  331. package/scripts/stripe-live-status.js +115 -0
  332. package/scripts/subagent-profiles.js +79 -0
  333. package/scripts/sync-gh-secrets-from-env.sh +70 -0
  334. package/scripts/sync-github-about.js +52 -0
  335. package/scripts/sync-version.js +451 -0
  336. package/scripts/synthetic-dpo.js +234 -0
  337. package/scripts/telemetry-analytics.js +821 -0
  338. package/scripts/tessl-export.js +371 -0
  339. package/scripts/test-coverage.js +120 -0
  340. package/scripts/thompson-sampling.js +417 -0
  341. package/scripts/thumbgate-search.js +189 -0
  342. package/scripts/tool-kpi-tracker.js +12 -0
  343. package/scripts/tool-registry.js +811 -0
  344. package/scripts/train_from_feedback.py +910 -0
  345. package/scripts/user-profile.js +78 -0
  346. package/scripts/validate-feedback.js +580 -0
  347. package/scripts/validate-workflow-contract.js +287 -0
  348. package/scripts/vector-store.js +198 -0
  349. package/scripts/verification-loop.js +291 -0
  350. package/scripts/verify-obsidian-setup.sh +269 -0
  351. package/scripts/verify-run.js +269 -0
  352. package/scripts/webhook-delivery.js +62 -0
  353. package/scripts/weekly-auto-post.js +124 -0
  354. package/scripts/workflow-runs.js +154 -0
  355. package/scripts/workflow-sprint-intake.js +475 -0
  356. package/scripts/workspace-evolver.js +374 -0
  357. package/scripts/x-autonomous-marketing.js +139 -0
  358. package/scripts/xmemory-lite.js +405 -0
  359. package/skills/agent-memory/SKILL.md +97 -0
  360. package/skills/solve-architecture-autonomy/SKILL.md +17 -0
  361. package/skills/solve-architecture-autonomy/tool.js +33 -0
  362. package/skills/thumbgate/SKILL.md +114 -0
  363. package/skills/thumbgate-feedback/SKILL.md +49 -0
  364. package/src/api/server.js +4208 -0
@@ -0,0 +1,816 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+ const { execSync } = require('child_process');
8
+
9
+ const { isProTier, FREE_TIER_MAX_GATES } = require('./rate-limiter');
10
+
11
+ /**
12
+ * Computes the SHA-256 hash of an executable binary to prevent path-based bypasses.
13
+ * (Layer 5: Supply Chain / Layer 3: Execution)
14
+ */
15
+ function computeExecutableHash(command) {
16
+ try {
17
+ if (!command) return null;
18
+ const firstWord = command.trim().split(/\s+/)[0];
19
+ if (!firstWord) return null;
20
+
21
+ // Resolve absolute path using 'which'
22
+ let fullPath;
23
+ try {
24
+ fullPath = execSync(`which ${firstWord}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
25
+ } catch (e) {
26
+ // If 'which' fails, it might be an absolute path or a non-existent command
27
+ fullPath = path.isAbsolute(firstWord) ? firstWord : null;
28
+ }
29
+
30
+ if (!fullPath || !fs.existsSync(fullPath) || !fs.lstatSync(fullPath).isFile()) return null;
31
+
32
+ const buffer = fs.readFileSync(fullPath);
33
+ return crypto.createHash('sha256').update(buffer).digest('hex');
34
+ } catch (e) {
35
+ return null;
36
+ }
37
+ }
38
+ const {
39
+ scanHookInput,
40
+ buildSafeSummary,
41
+ redactText,
42
+ } = require('./secret-scanner');
43
+ const { getAutoGatesPath } = require('./auto-promote-gates');
44
+ const { recordAuditEvent, auditToFeedback } = require('./audit-trail');
45
+
46
+ const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', 'config', 'gates', 'default.json');
47
+ const DEFAULT_CLAIM_GATES_PATH = path.join(__dirname, '..', 'config', 'gates', 'claim-verification.json');
48
+ const STATE_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-state.json');
49
+ const CONSTRAINTS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'session-constraints.json');
50
+ const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
51
+ const SESSION_ACTIONS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'session-actions.json');
52
+ const CUSTOM_CLAIM_GATES_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'claim-verification.json');
53
+ const TTL_MS = 5 * 60 * 1000; // 5 minutes
54
+ const SESSION_ACTION_TTL_MS = 60 * 60 * 1000; // 1 hour
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Config loading
58
+ // ---------------------------------------------------------------------------
59
+
60
+ function loadGatesConfig(configPath) {
61
+ const primaryPath = configPath || process.env.THUMBGATE_GATES_CONFIG || DEFAULT_CONFIG_PATH;
62
+
63
+ if (!fs.existsSync(primaryPath)) {
64
+ throw new Error(`Gates config not found: ${primaryPath}`);
65
+ }
66
+
67
+ const mergedConfig = { version: 1, gates: [] };
68
+
69
+ const loadOne = (p, isPrimary) => {
70
+ try {
71
+ const raw = fs.readFileSync(p, 'utf8');
72
+ const config = JSON.parse(raw);
73
+ if (!config || !Array.isArray(config.gates)) {
74
+ if (isPrimary) throw new Error('Invalid gates config: missing "gates" array');
75
+ return;
76
+ }
77
+ return config.gates;
78
+ } catch (e) {
79
+ if (isPrimary) throw e;
80
+ console.error(`Warning: failed to load gates from ${p}: ${e.message}`);
81
+ return [];
82
+ }
83
+ };
84
+
85
+ const primaryGates = loadOne(primaryPath, true).map(g => ({ ...g, layer: g.layer || 'Execution' }));
86
+ mergedConfig.gates.push(...primaryGates);
87
+
88
+ // Always preserve the full primary/default safety policy. Free tier limits apply
89
+ // only to auto-promoted add-on gates so core protections never disappear.
90
+ const autoConfigPath = getAutoGatesPath();
91
+ if (!configPath && fs.existsSync(autoConfigPath)) {
92
+ const autoGates = loadOne(autoConfigPath, false).map(g => ({ ...g, layer: g.layer || 'Execution' }));
93
+ const limitedAutoGates = isProTier()
94
+ ? autoGates
95
+ : autoGates.slice(0, FREE_TIER_MAX_GATES);
96
+ mergedConfig.gates.push(...limitedAutoGates);
97
+ }
98
+
99
+ return mergedConfig;
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // State and Constraints management
104
+ // ---------------------------------------------------------------------------
105
+
106
+ function loadJSON(filePath) {
107
+ if (!fs.existsSync(filePath)) return {};
108
+ try {
109
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
110
+ } catch {
111
+ return {};
112
+ }
113
+ }
114
+
115
+ function saveJSON(filePath, data) {
116
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
117
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
118
+ }
119
+
120
+ function loadState() { return loadJSON(module.exports.STATE_PATH); }
121
+ function saveState(state) { saveJSON(module.exports.STATE_PATH, state); }
122
+
123
+ function loadConstraints() { return loadJSON(module.exports.CONSTRAINTS_PATH); }
124
+ function saveConstraints(constraints) { saveJSON(module.exports.CONSTRAINTS_PATH, constraints); }
125
+
126
+ function setConstraint(key, value) {
127
+ const constraints = loadConstraints();
128
+ constraints[key] = {
129
+ value,
130
+ timestamp: Date.now()
131
+ };
132
+ saveConstraints(constraints);
133
+ return constraints[key];
134
+ }
135
+
136
+ function isConditionSatisfied(conditionId) {
137
+ const state = loadState();
138
+ const entry = state[conditionId];
139
+ if (!entry) return false;
140
+ const age = Date.now() - entry.timestamp;
141
+ return age < TTL_MS;
142
+ }
143
+
144
+ function satisfyCondition(conditionId, evidence, structuredReasoning) {
145
+ const state = loadState();
146
+ const entry = {
147
+ timestamp: Date.now(),
148
+ evidence: evidence || '',
149
+ };
150
+ if (structuredReasoning && typeof structuredReasoning === 'object') {
151
+ entry.structuredReasoning = {
152
+ premise: structuredReasoning.premise || null,
153
+ evidence: structuredReasoning.evidence || null,
154
+ risk: structuredReasoning.risk || null,
155
+ conclusion: structuredReasoning.conclusion || null,
156
+ };
157
+ }
158
+ state[conditionId] = entry;
159
+ saveState(state);
160
+ return entry;
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Stats tracking
165
+ // ---------------------------------------------------------------------------
166
+
167
+ function loadStats() {
168
+ const stats = loadJSON(module.exports.STATS_PATH);
169
+ if (Object.keys(stats).length === 0) return { blocked: 0, warned: 0, passed: 0, byGate: {} };
170
+ return stats;
171
+ }
172
+
173
+ function saveStats(stats) { saveJSON(module.exports.STATS_PATH, stats); }
174
+
175
+ function recordStat(gateId, action, gate) {
176
+ const stats = loadStats();
177
+ if (action === 'block') stats.blocked = (stats.blocked || 0) + 1;
178
+ else if (action === 'warn') stats.warned = (stats.warned || 0) + 1;
179
+ else stats.passed = (stats.passed || 0) + 1;
180
+ if (!stats.byGate) stats.byGate = {};
181
+ if (!stats.byGate[gateId]) stats.byGate[gateId] = { blocked: 0, warned: 0 };
182
+ if (action === 'block') stats.byGate[gateId].blocked += 1;
183
+ else if (action === 'warn') stats.byGate[gateId].warned += 1;
184
+ saveStats(stats);
185
+ // Track lesson freshness when an auto-promoted gate fires
186
+ if (gate && gate.sourceLessonId) {
187
+ try {
188
+ const { recordTrigger } = require('./lesson-rotation');
189
+ const { initDB } = require('./lesson-db');
190
+ const db = initDB();
191
+ recordTrigger(db, gate.sourceLessonId);
192
+ db.close();
193
+ } catch (_) { /* lesson DB may not be available */ }
194
+ }
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Reasoning chain builder
199
+ // ---------------------------------------------------------------------------
200
+
201
+ /**
202
+ * Build a human-readable reasoning chain explaining WHY a gate decision was made.
203
+ * Returns an array of evidence steps — each a short sentence a developer can scan.
204
+ *
205
+ * @param {Object} gate - The matched gate definition
206
+ * @param {string} toolName - The tool that was evaluated
207
+ * @param {Object} toolInput - The tool input that was evaluated
208
+ * @param {Object} [extras] - Optional extra context (metrics, constraints)
209
+ * @returns {string[]} Array of reasoning steps
210
+ */
211
+ function buildReasoning(gate, toolName, toolInput, extras = {}) {
212
+ const steps = [];
213
+ const text = toolInput.command || toolInput.file_path || toolInput.path || '';
214
+
215
+ // 1. What matched
216
+ steps.push(`Pattern /${gate.pattern}/ matched "${text.length > 80 ? text.slice(0, 80) + '…' : text}"`);
217
+
218
+ // 2. Gate identity
219
+ steps.push(`Gate ${gate.id} [${gate.action}] — layer: ${gate.layer || 'Execution'}, severity: ${gate.severity || 'medium'}`);
220
+
221
+ // 3. Source (manual vs auto-promoted)
222
+ if (gate.promotedAt || gate.source === 'auto-promote' || gate.source === 'force-promote') {
223
+ const occText = gate.occurrences ? ` after ${gate.occurrences} failures` : '';
224
+ steps.push(`Auto-promoted from feedback${occText} (${gate.promotedAt || 'unknown date'})`);
225
+ } else {
226
+ steps.push('Manual policy rule (default.json)');
227
+ }
228
+
229
+ // 4. Constraint context
230
+ if (gate.when && gate.when.constraints) {
231
+ const keys = Object.entries(gate.when.constraints).map(([k, v]) => `${k}=${v}`).join(', ');
232
+ steps.push(`Active because constraint ${keys} is set`);
233
+ }
234
+
235
+ // 5. Unless condition status
236
+ if (gate.unless) {
237
+ steps.push(`Bypassable via satisfy_gate("${gate.unless}") — not currently satisfied`);
238
+ }
239
+
240
+ // 6. Metric condition
241
+ if (extras.metricFailed) {
242
+ const m = gate.metrics;
243
+ steps.push(`Business metric "${m.name}" outside bounds [${m.min ?? '-∞'}, ${m.max ?? '∞'}]`);
244
+ }
245
+
246
+ // 7. Historical fire count
247
+ const stats = loadStats();
248
+ const gateStats = stats.byGate && stats.byGate[gate.id];
249
+ if (gateStats) {
250
+ steps.push(`History: blocked ${gateStats.blocked || 0}×, warned ${gateStats.warned || 0}×`);
251
+ }
252
+
253
+ return steps;
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Matching engine
258
+ // ---------------------------------------------------------------------------
259
+
260
+ function checkWhenClause(when, constraints) {
261
+ if (!when || !when.constraints) return true;
262
+
263
+ for (const [key, expectedValue] of Object.entries(when.constraints)) {
264
+ const constraint = constraints[key];
265
+ if (!constraint || constraint.value !== expectedValue) {
266
+ return false;
267
+ }
268
+ }
269
+ return true;
270
+ }
271
+
272
+ function matchesGate(gate, _toolName, toolInput) {
273
+ // Build the text to match against: for Bash it's the command, for Edit it's the file path
274
+ const text = toolInput.command || toolInput.file_path || toolInput.path || '';
275
+
276
+ // 1. Check Regex Pattern
277
+ try {
278
+ const regex = new RegExp(gate.pattern);
279
+ if (!regex.test(text)) return false;
280
+ } catch {
281
+ return false;
282
+ }
283
+
284
+ // 2. Check Executable Hash (New: Layer 5 Anti-Bypass)
285
+ // If a hash is specified, we must verify the content of the binary
286
+ if (gate.executable_hash && toolInput.command) {
287
+ const actualHash = computeExecutableHash(toolInput.command);
288
+ if (actualHash !== gate.executable_hash) return false;
289
+ }
290
+
291
+ return true;
292
+ }
293
+
294
+ async function checkMetricCondition(metricCondition) {
295
+ if (!metricCondition) return true;
296
+ const { getBusinessMetrics } = require('./semantic-layer');
297
+ const metrics = await getBusinessMetrics({ window: metricCondition.window || '30d' });
298
+ const value = metrics.metrics[metricCondition.name];
299
+
300
+ if (value === undefined) return true;
301
+
302
+ if (metricCondition.min !== undefined && value < metricCondition.min) return false;
303
+ if (metricCondition.max !== undefined && value > metricCondition.max) return false;
304
+
305
+ return true;
306
+ }
307
+
308
+ async function evaluateGatesAsync(toolName, toolInput, configPath) {
309
+ let config;
310
+ try {
311
+ config = loadGatesConfig(configPath);
312
+ } catch {
313
+ return null;
314
+ }
315
+
316
+ const constraints = loadConstraints();
317
+
318
+ // Fast-path: feedback/recall tools skip metric gates entirely (avoids Stripe API calls)
319
+ const METRIC_SKIP_TOOLS = ['capture_feedback', 'feedback_stats', 'recall', 'feedback_summary', 'prevention_rules'];
320
+ const skipMetrics = METRIC_SKIP_TOOLS.includes(toolName);
321
+
322
+ for (const gate of config.gates) {
323
+ if (!matchesGate(gate, toolName, toolInput)) continue;
324
+
325
+ // EvoSkill Hardening: check contextual 'when' clause
326
+ if (gate.when && !checkWhenClause(gate.when, constraints)) {
327
+ continue;
328
+ }
329
+
330
+ // Metric-aware gates: check business metrics from Semantic Layer
331
+ let metricFailed = false;
332
+ if (gate.metrics) {
333
+ if (skipMetrics) {
334
+ // Fast path: skip metric gates for feedback/recall tools
335
+ continue;
336
+ }
337
+ const metricResult = await Promise.race([
338
+ checkMetricCondition(gate.metrics),
339
+ new Promise(resolve => setTimeout(() => resolve({ pass: true, reason: 'metric-timeout' }), 3000))
340
+ ]);
341
+ // checkMetricCondition returns a boolean; Promise.race timeout returns an object
342
+ const metricsPassed = typeof metricResult === 'object' ? metricResult.pass : metricResult;
343
+ if (!metricsPassed) {
344
+ metricFailed = true;
345
+ } else {
346
+ continue;
347
+ }
348
+ }
349
+
350
+ // Check unless condition
351
+ if (gate.unless && isConditionSatisfied(gate.unless)) {
352
+ continue;
353
+ }
354
+
355
+ const reasoning = buildReasoning(gate, toolName, toolInput, { metricFailed });
356
+
357
+ if (gate.action === 'block') {
358
+ recordStat(gate.id, 'block', gate);
359
+ const result = { decision: 'deny', gate: gate.id, message: gate.message, severity: gate.severity, reasoning };
360
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message: gate.message, severity: gate.severity, source: 'gates-engine' });
361
+ auditToFeedback(auditRecord);
362
+ return result;
363
+ }
364
+
365
+ if (gate.action === 'warn') {
366
+ recordStat(gate.id, 'warn', gate);
367
+ const result = { decision: 'warn', gate: gate.id, message: gate.message, severity: gate.severity, reasoning };
368
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: gate.message, severity: gate.severity, source: 'gates-engine' });
369
+ auditToFeedback(auditRecord);
370
+ return result;
371
+ }
372
+ }
373
+
374
+ // Audit trail: record allow (no gate matched)
375
+ recordAuditEvent({ toolName, toolInput, decision: 'allow', source: 'gates-engine' });
376
+ return null;
377
+ }
378
+
379
+ function evaluateGates(toolName, toolInput, configPath) {
380
+ let config;
381
+ try {
382
+ config = loadGatesConfig(configPath);
383
+ } catch {
384
+ // If config can't be loaded, pass through
385
+ return null;
386
+ }
387
+
388
+ const constraints = loadConstraints();
389
+
390
+ for (const gate of config.gates) {
391
+ if (!matchesGate(gate, toolName, toolInput)) continue;
392
+
393
+ // EvoSkill Hardening: check contextual 'when' clause
394
+ if (gate.when && !checkWhenClause(gate.when, constraints)) {
395
+ continue;
396
+ }
397
+
398
+ // Check unless condition
399
+ if (gate.unless && isConditionSatisfied(gate.unless)) {
400
+ continue;
401
+ }
402
+
403
+ const reasoning = buildReasoning(gate, toolName, toolInput);
404
+
405
+ if (gate.action === 'block') {
406
+ recordStat(gate.id, 'block', gate);
407
+ const result = { decision: 'deny', gate: gate.id, message: gate.message, severity: gate.severity, reasoning };
408
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message: gate.message, severity: gate.severity, source: 'gates-engine' });
409
+ auditToFeedback(auditRecord);
410
+ return result;
411
+ }
412
+
413
+ if (gate.action === 'warn') {
414
+ recordStat(gate.id, 'warn', gate);
415
+ const result = { decision: 'warn', gate: gate.id, message: gate.message, severity: gate.severity, reasoning };
416
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: gate.message, severity: gate.severity, source: 'gates-engine' });
417
+ auditToFeedback(auditRecord);
418
+ return result;
419
+ }
420
+ }
421
+
422
+ // Audit trail: record allow
423
+ recordAuditEvent({ toolName, toolInput, decision: 'allow', source: 'gates-engine' });
424
+ return null;
425
+ }
426
+
427
+ function buildSecretGuardResult(scanResult) {
428
+ return {
429
+ decision: 'deny',
430
+ gate: 'secret-exfiltration',
431
+ message: buildSafeSummary(
432
+ scanResult.findings,
433
+ 'Blocked because the action appears to expose secret material'
434
+ ),
435
+ severity: 'critical',
436
+ secretScan: {
437
+ provider: scanResult.provider,
438
+ findings: scanResult.findings.map((finding) => ({
439
+ id: finding.id,
440
+ label: finding.label,
441
+ line: finding.line || null,
442
+ path: finding.path || null,
443
+ source: finding.source || null,
444
+ reason: finding.reason || null,
445
+ })),
446
+ },
447
+ };
448
+ }
449
+
450
+ function getFeedbackLoopModule() {
451
+ try {
452
+ return require('./feedback-loop');
453
+ } catch {
454
+ return null;
455
+ }
456
+ }
457
+
458
+ function recordSecretViolation(input, scanResult) {
459
+ const feedbackLoop = getFeedbackLoopModule();
460
+ if (!feedbackLoop || typeof feedbackLoop.appendDiagnosticRecord !== 'function') {
461
+ return;
462
+ }
463
+
464
+ const toolName = input.tool_name || input.toolName || 'unknown';
465
+ const toolInput = input.tool_input && typeof input.tool_input === 'object' ? input.tool_input : {};
466
+ const filePath = toolInput.file_path || toolInput.path || toolInput.filePath || null;
467
+ const command = typeof toolInput.command === 'string' ? toolInput.command : '';
468
+ const safeContext = redactText(
469
+ filePath
470
+ ? `${toolName} requested ${filePath}`
471
+ : command
472
+ ? `${toolName} requested command ${command}`
473
+ : `${toolName} requested protected content`
474
+ ).slice(0, 400);
475
+
476
+ feedbackLoop.appendDiagnosticRecord({
477
+ source: 'secret_guard',
478
+ step: 'pre_tool_use',
479
+ context: safeContext,
480
+ metadata: {
481
+ toolName,
482
+ provider: scanResult.provider,
483
+ filePath,
484
+ commandHash: scanResult.commandHash || null,
485
+ fileHashes: scanResult.fileHashes || [],
486
+ },
487
+ diagnosis: {
488
+ diagnosed: true,
489
+ rootCauseCategory: 'guardrail_triggered',
490
+ criticalFailureStep: 'pre_tool_use',
491
+ violations: scanResult.findings.map((finding) => ({
492
+ constraintId: `security:${finding.id || 'secret_exfiltration'}`,
493
+ description: finding.reason || finding.label || 'Secret exposure blocked',
494
+ metadata: {
495
+ label: finding.label || finding.id || 'secret',
496
+ path: finding.path || null,
497
+ line: finding.line || null,
498
+ source: finding.source || null,
499
+ },
500
+ })),
501
+ evidence: scanResult.findings.map((finding) => (
502
+ `${finding.label || finding.id}${finding.path ? ` in ${finding.path}` : ''}${finding.line ? ` line ${finding.line}` : ''}`
503
+ )),
504
+ },
505
+ });
506
+ }
507
+
508
+ function evaluateSecretGuard(input = {}) {
509
+ const scanResult = scanHookInput(input);
510
+ if (!scanResult.detected) {
511
+ return null;
512
+ }
513
+ recordStat('secret-exfiltration', 'block');
514
+ recordSecretViolation(input, scanResult);
515
+ const result = buildSecretGuardResult(scanResult);
516
+ // Audit trail: record secret guard denial
517
+ const auditRecord = recordAuditEvent({
518
+ toolName: input.tool_name || input.toolName || 'unknown',
519
+ toolInput: input.tool_input || {},
520
+ decision: 'deny',
521
+ gateId: 'secret-exfiltration',
522
+ message: 'Secret material detected in tool input',
523
+ severity: 'critical',
524
+ source: 'secret-guard',
525
+ });
526
+ auditToFeedback(auditRecord);
527
+ return result;
528
+ }
529
+
530
+ // ---------------------------------------------------------------------------
531
+ // PreToolUse hook interface (stdin/stdout JSON)
532
+ // ---------------------------------------------------------------------------
533
+
534
+ function formatOutput(result) {
535
+ if (!result) {
536
+ // No gate matched — pass through
537
+ return JSON.stringify({});
538
+ }
539
+
540
+ const reasoningSuffix = Array.isArray(result.reasoning) && result.reasoning.length
541
+ ? '\n Reasoning:\n • ' + result.reasoning.join('\n • ')
542
+ : '';
543
+
544
+ if (result.decision === 'deny') {
545
+ return JSON.stringify({
546
+ hookSpecificOutput: {
547
+ permissionDecision: 'deny',
548
+ permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}`,
549
+ },
550
+ });
551
+ }
552
+
553
+ if (result.decision === 'warn') {
554
+ return JSON.stringify({
555
+ hookSpecificOutput: {
556
+ additionalContext: `[GATE:${result.gate}] WARNING: ${result.message}${reasoningSuffix}`,
557
+ },
558
+ });
559
+ }
560
+
561
+ return JSON.stringify({});
562
+ }
563
+
564
+ async function runAsync(input) {
565
+ const secretGuard = evaluateSecretGuard(input);
566
+ if (secretGuard) {
567
+ return formatOutput(secretGuard);
568
+ }
569
+
570
+ const toolName = input.tool_name || '';
571
+ const toolInput = input.tool_input || {};
572
+ const result = await evaluateGatesAsync(toolName, toolInput);
573
+ return formatOutput(result);
574
+ }
575
+
576
+ function run(input) {
577
+ const secretGuard = evaluateSecretGuard(input);
578
+ if (secretGuard) {
579
+ return formatOutput(secretGuard);
580
+ }
581
+
582
+ const toolName = input.tool_name || '';
583
+ const toolInput = input.tool_input || {};
584
+ const result = evaluateGates(toolName, toolInput);
585
+ return formatOutput(result);
586
+ }
587
+
588
+ // ---------------------------------------------------------------------------
589
+ // Session action tracking and claim verification
590
+ // ---------------------------------------------------------------------------
591
+
592
+ function loadSessionActions() {
593
+ const actions = loadJSON(module.exports.SESSION_ACTIONS_PATH);
594
+ const now = Date.now();
595
+ const valid = {};
596
+
597
+ for (const [key, entry] of Object.entries(actions)) {
598
+ if (!entry || typeof entry !== 'object') continue;
599
+ if (!entry.timestamp || (now - entry.timestamp) >= SESSION_ACTION_TTL_MS) continue;
600
+ valid[key] = entry;
601
+ }
602
+
603
+ if (Object.keys(valid).length !== Object.keys(actions).length) {
604
+ saveSessionActions(valid);
605
+ }
606
+
607
+ return valid;
608
+ }
609
+
610
+ function saveSessionActions(actions) {
611
+ saveJSON(module.exports.SESSION_ACTIONS_PATH, actions);
612
+ }
613
+
614
+ function trackAction(actionId, metadata = {}) {
615
+ const normalizedActionId = String(actionId || '').trim();
616
+ if (!normalizedActionId) {
617
+ throw new Error('actionId is required');
618
+ }
619
+ if (metadata !== null && typeof metadata !== 'object') {
620
+ throw new Error('metadata must be an object when provided');
621
+ }
622
+
623
+ const actions = loadSessionActions();
624
+ actions[normalizedActionId] = {
625
+ timestamp: Date.now(),
626
+ metadata: metadata || {},
627
+ };
628
+ saveSessionActions(actions);
629
+ return actions[normalizedActionId];
630
+ }
631
+
632
+ function hasAction(actionId) {
633
+ const normalizedActionId = String(actionId || '').trim();
634
+ if (!normalizedActionId) return false;
635
+ const actions = loadSessionActions();
636
+ return Boolean(actions[normalizedActionId]);
637
+ }
638
+
639
+ function listSessionActions() {
640
+ return loadSessionActions();
641
+ }
642
+
643
+ function clearSessionActions() {
644
+ saveSessionActions({});
645
+ }
646
+
647
+ function loadClaimGateFile(filePath, { allowMissing = true } = {}) {
648
+ if (!fs.existsSync(filePath)) {
649
+ if (allowMissing) return { claims: [] };
650
+ throw new Error(`Claim gates config not found: ${filePath}`);
651
+ }
652
+
653
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
654
+ if (!parsed || !Array.isArray(parsed.claims)) {
655
+ throw new Error(`Invalid claim gates config: ${filePath}`);
656
+ }
657
+ return parsed;
658
+ }
659
+
660
+ function saveCustomClaimGates(config) {
661
+ fs.mkdirSync(path.dirname(module.exports.CUSTOM_CLAIM_GATES_PATH), { recursive: true });
662
+ fs.writeFileSync(module.exports.CUSTOM_CLAIM_GATES_PATH, JSON.stringify(config, null, 2) + '\n');
663
+ }
664
+
665
+ function loadClaimGates() {
666
+ const defaults = loadClaimGateFile(module.exports.DEFAULT_CLAIM_GATES_PATH, { allowMissing: false });
667
+ const custom = loadClaimGateFile(module.exports.CUSTOM_CLAIM_GATES_PATH);
668
+ const mergedByPattern = new Map();
669
+
670
+ for (const claim of defaults.claims) {
671
+ mergedByPattern.set(claim.pattern, claim);
672
+ }
673
+ for (const claim of custom.claims) {
674
+ mergedByPattern.set(claim.pattern, claim);
675
+ }
676
+
677
+ return {
678
+ version: Math.max(defaults.version || 1, custom.version || 1),
679
+ claims: Array.from(mergedByPattern.values()),
680
+ };
681
+ }
682
+
683
+ function registerClaimGate(claimPattern, requiredActions, blockMessage) {
684
+ const normalizedPattern = String(claimPattern || '').trim();
685
+ if (!normalizedPattern) {
686
+ throw new Error('claimPattern is required');
687
+ }
688
+ if (!Array.isArray(requiredActions) || requiredActions.length === 0) {
689
+ throw new Error('requiredActions must be a non-empty array');
690
+ }
691
+
692
+ const normalizedActions = requiredActions
693
+ .map((actionId) => String(actionId || '').trim())
694
+ .filter(Boolean);
695
+ if (normalizedActions.length === 0) {
696
+ throw new Error('requiredActions must contain at least one non-empty action id');
697
+ }
698
+
699
+ const custom = loadClaimGateFile(module.exports.CUSTOM_CLAIM_GATES_PATH);
700
+ const existingIndex = custom.claims.findIndex((claim) => claim.pattern === normalizedPattern);
701
+ const entry = {
702
+ pattern: normalizedPattern,
703
+ requiredActions: normalizedActions,
704
+ message: blockMessage || `Claim "${normalizedPattern}" requires evidence: ${normalizedActions.join(', ')}`,
705
+ createdAt: Date.now(),
706
+ };
707
+
708
+ if (existingIndex >= 0) {
709
+ custom.claims[existingIndex] = entry;
710
+ } else {
711
+ custom.claims.push(entry);
712
+ }
713
+
714
+ saveCustomClaimGates(custom);
715
+ return entry;
716
+ }
717
+
718
+ function verifyClaimEvidence(claimText) {
719
+ const normalizedClaimText = String(claimText || '').trim();
720
+ if (!normalizedClaimText) {
721
+ throw new Error('claimText is required');
722
+ }
723
+
724
+ const config = loadClaimGates();
725
+ const actions = loadSessionActions();
726
+ const checks = [];
727
+
728
+ for (const claim of config.claims) {
729
+ let regex;
730
+ try {
731
+ regex = new RegExp(claim.pattern, 'i');
732
+ } catch {
733
+ continue;
734
+ }
735
+ if (!regex.test(normalizedClaimText)) continue;
736
+
737
+ const missing = (claim.requiredActions || []).filter((actionId) => !actions[actionId]);
738
+ checks.push({
739
+ claim: claim.pattern,
740
+ passed: missing.length === 0,
741
+ missing,
742
+ message: missing.length > 0 ? claim.message : 'All evidence present',
743
+ });
744
+ }
745
+
746
+ return {
747
+ verified: checks.every((check) => check.passed),
748
+ checks,
749
+ };
750
+ }
751
+
752
+ // ---------------------------------------------------------------------------
753
+ // Exports
754
+ // ---------------------------------------------------------------------------
755
+
756
+ module.exports = {
757
+ loadGatesConfig,
758
+ loadState,
759
+ saveState,
760
+ loadConstraints,
761
+ saveConstraints,
762
+ setConstraint,
763
+ isConditionSatisfied,
764
+ satisfyCondition,
765
+ loadStats,
766
+ saveStats,
767
+ recordStat,
768
+ evaluateSecretGuard,
769
+ buildSecretGuardResult,
770
+ buildReasoning,
771
+ matchesGate,
772
+ evaluateGates,
773
+ evaluateGatesAsync,
774
+ computeExecutableHash,
775
+ formatOutput,
776
+ run,
777
+ runAsync,
778
+ trackAction,
779
+ hasAction,
780
+ listSessionActions,
781
+ clearSessionActions,
782
+ loadClaimGates,
783
+ registerClaimGate,
784
+ verifyClaimEvidence,
785
+ DEFAULT_CONFIG_PATH,
786
+ DEFAULT_CLAIM_GATES_PATH,
787
+ STATE_PATH,
788
+ CONSTRAINTS_PATH,
789
+ STATS_PATH,
790
+ SESSION_ACTIONS_PATH,
791
+ CUSTOM_CLAIM_GATES_PATH,
792
+ TTL_MS,
793
+ SESSION_ACTION_TTL_MS,
794
+ };
795
+
796
+ // ---------------------------------------------------------------------------
797
+ // CLI: reads PreToolUse hook JSON from stdin
798
+ // ---------------------------------------------------------------------------
799
+
800
+ if (require.main === module) {
801
+ let data = '';
802
+ process.stdin.setEncoding('utf8');
803
+ process.stdin.on('data', (chunk) => { data += chunk; });
804
+ process.stdin.on('end', async () => {
805
+ try {
806
+ const input = JSON.parse(data);
807
+ const output = await runAsync(input);
808
+ process.stdout.write(output + '\n');
809
+ process.exit(0);
810
+ } catch (err) {
811
+ process.stderr.write(`gates-engine error: ${err.message}\n`);
812
+ process.stdout.write(JSON.stringify({}) + '\n');
813
+ process.exit(0);
814
+ }
815
+ });
816
+ }