thumbgate 0.9.9

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 (369) 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/rlhf-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 +1483 -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 +286 -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 +1195 -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-314.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 +293 -0
  117. package/scripts/auto-wire-hooks.js +316 -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 +93 -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 +231 -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 +206 -0
  152. package/scripts/ensure-repo-bootstrap.js +129 -0
  153. package/scripts/ephemeral-agent-store.js +219 -0
  154. package/scripts/eval-harness.js +56 -0
  155. package/scripts/evolution-state.js +241 -0
  156. package/scripts/experiment-tracker.js +267 -0
  157. package/scripts/export-databricks-bundle.js +242 -0
  158. package/scripts/export-dpo-pairs.js +344 -0
  159. package/scripts/export-kto-pairs.js +309 -0
  160. package/scripts/export-training.js +450 -0
  161. package/scripts/failure-diagnostics.js +558 -0
  162. package/scripts/feedback-attribution.js +313 -0
  163. package/scripts/feedback-fallback.js +110 -0
  164. package/scripts/feedback-history-distiller.js +391 -0
  165. package/scripts/feedback-inbox-read.js +162 -0
  166. package/scripts/feedback-loop.js +1887 -0
  167. package/scripts/feedback-paths.js +145 -0
  168. package/scripts/feedback-quality.js +139 -0
  169. package/scripts/feedback-root-consolidator.js +238 -0
  170. package/scripts/feedback-schema.js +426 -0
  171. package/scripts/feedback-session.js +286 -0
  172. package/scripts/feedback-to-memory.js +185 -0
  173. package/scripts/feedback-to-rules.js +164 -0
  174. package/scripts/filesystem-search.js +405 -0
  175. package/scripts/funnel-analytics.js +35 -0
  176. package/scripts/gate-satisfy.js +42 -0
  177. package/scripts/gate-stats.js +116 -0
  178. package/scripts/gate-templates.js +70 -0
  179. package/scripts/gates-engine.js +816 -0
  180. package/scripts/generate-paperbanana-diagrams.sh +99 -0
  181. package/scripts/generate-pretool-hook.sh +40 -0
  182. package/scripts/github-about.js +350 -0
  183. package/scripts/github-outreach.js +65 -0
  184. package/scripts/gtm-revenue-loop.js +520 -0
  185. package/scripts/hallucination-detector.js +226 -0
  186. package/scripts/hf-papers.js +317 -0
  187. package/scripts/history-distiller.js +200 -0
  188. package/scripts/hook-auto-capture.sh +100 -0
  189. package/scripts/hook-stop-pr-thread-check.sh +68 -0
  190. package/scripts/hook-stop-self-score.sh +51 -0
  191. package/scripts/hook-stop-verify-deploy.sh +31 -0
  192. package/scripts/hook-thumbgate-cache-updater.js +48 -0
  193. package/scripts/hook-verify-before-done.sh +20 -0
  194. package/scripts/hosted-config.js +156 -0
  195. package/scripts/hybrid-feedback-context.js +675 -0
  196. package/scripts/install-mcp.js +159 -0
  197. package/scripts/intent-router.js +392 -0
  198. package/scripts/internal-agent-bootstrap.js +490 -0
  199. package/scripts/jsonl-watcher.js +155 -0
  200. package/scripts/lesson-db.js +613 -0
  201. package/scripts/lesson-inference.js +310 -0
  202. package/scripts/lesson-retrieval.js +95 -0
  203. package/scripts/lesson-rotation.js +137 -0
  204. package/scripts/lesson-search.js +644 -0
  205. package/scripts/lesson-synthesis.js +196 -0
  206. package/scripts/license.js +50 -0
  207. package/scripts/local-model-profile.js +384 -0
  208. package/scripts/markdown-escape.js +12 -0
  209. package/scripts/marketing-experiment.js +671 -0
  210. package/scripts/mcp-config.js +149 -0
  211. package/scripts/mcp-policy.js +99 -0
  212. package/scripts/memalign-recall.js +111 -0
  213. package/scripts/memory-firewall.js +222 -0
  214. package/scripts/memory-migration.js +296 -0
  215. package/scripts/meta-policy.js +190 -0
  216. package/scripts/metered-billing.js +16 -0
  217. package/scripts/model-tier-router.js +301 -0
  218. package/scripts/money-watcher.js +71 -0
  219. package/scripts/multi-hop-recall.js +240 -0
  220. package/scripts/natural-language-harness.js +330 -0
  221. package/scripts/obsidian-export.js +713 -0
  222. package/scripts/operational-dashboard.js +103 -0
  223. package/scripts/operational-summary.js +93 -0
  224. package/scripts/optimize-context.js +17 -0
  225. package/scripts/org-dashboard.js +201 -0
  226. package/scripts/partner-orchestration.js +146 -0
  227. package/scripts/per-step-scoring.js +165 -0
  228. package/scripts/perplexity-marketing.js +466 -0
  229. package/scripts/pii-scanner.js +153 -0
  230. package/scripts/plan-gate.js +154 -0
  231. package/scripts/post-everywhere.js +308 -0
  232. package/scripts/post-to-x-retry.sh +22 -0
  233. package/scripts/post-to-x.js +369 -0
  234. package/scripts/pr-manager.js +236 -0
  235. package/scripts/predictive-insights.js +356 -0
  236. package/scripts/principle-extractor.js +162 -0
  237. package/scripts/pro-features.js +40 -0
  238. package/scripts/pro-local-dashboard.js +174 -0
  239. package/scripts/problem-detail.js +53 -0
  240. package/scripts/product-feedback.js +134 -0
  241. package/scripts/profile-router.js +245 -0
  242. package/scripts/prompt-dlp.js +221 -0
  243. package/scripts/prompt-guard.js +83 -0
  244. package/scripts/prove-adapters.js +863 -0
  245. package/scripts/prove-attribution.js +365 -0
  246. package/scripts/prove-automation.js +653 -0
  247. package/scripts/prove-autoresearch.js +304 -0
  248. package/scripts/prove-claim-verification.js +277 -0
  249. package/scripts/prove-cloudflare-sandbox.js +163 -0
  250. package/scripts/prove-data-pipeline.js +410 -0
  251. package/scripts/prove-data-quality.js +227 -0
  252. package/scripts/prove-evolution.js +352 -0
  253. package/scripts/prove-harnesses.js +287 -0
  254. package/scripts/prove-intelligence.js +259 -0
  255. package/scripts/prove-lancedb.js +371 -0
  256. package/scripts/prove-local-intelligence.js +342 -0
  257. package/scripts/prove-loop-closure.js +263 -0
  258. package/scripts/prove-predictive-insights.js +357 -0
  259. package/scripts/prove-runtime.js +350 -0
  260. package/scripts/prove-seo-gsd.js +234 -0
  261. package/scripts/prove-settings.js +279 -0
  262. package/scripts/prove-subway-upgrades.js +277 -0
  263. package/scripts/prove-tessl.js +229 -0
  264. package/scripts/prove-training-export.js +327 -0
  265. package/scripts/prove-workflow-contract.js +116 -0
  266. package/scripts/prove-xmemory.js +332 -0
  267. package/scripts/publish-decision.js +133 -0
  268. package/scripts/pulse.js +80 -0
  269. package/scripts/rate-limiter.js +125 -0
  270. package/scripts/reddit-dm-outreach.js +182 -0
  271. package/scripts/reddit-monitor-cron.sh +26 -0
  272. package/scripts/reflector-agent.js +221 -0
  273. package/scripts/reminder-engine.js +132 -0
  274. package/scripts/revenue-status.js +472 -0
  275. package/scripts/risk-scorer.js +459 -0
  276. package/scripts/rlaif-self-audit.js +129 -0
  277. package/scripts/rlhf_session_start.sh +32 -0
  278. package/scripts/rubric-engine.js +230 -0
  279. package/scripts/schedule-manager.js +251 -0
  280. package/scripts/secret-scanner.js +414 -0
  281. package/scripts/self-heal.js +147 -0
  282. package/scripts/self-healing-check.js +188 -0
  283. package/scripts/semantic-layer.js +98 -0
  284. package/scripts/seo-gsd.js +1153 -0
  285. package/scripts/settings-hierarchy.js +214 -0
  286. package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
  287. package/scripts/skill-exporter.js +262 -0
  288. package/scripts/skill-generator.js +446 -0
  289. package/scripts/skill-materializer.js +134 -0
  290. package/scripts/skill-packs.js +136 -0
  291. package/scripts/skill-proposer.js +99 -0
  292. package/scripts/skill-quality-tracker.js +282 -0
  293. package/scripts/slo-alert-engine.js +14 -0
  294. package/scripts/slow-loop.js +72 -0
  295. package/scripts/social-analytics/db/schema.sql +32 -0
  296. package/scripts/social-analytics/db/social-analytics.db +0 -0
  297. package/scripts/social-analytics/digest.js +256 -0
  298. package/scripts/social-analytics/generate-instagram-card.js +97 -0
  299. package/scripts/social-analytics/instagram-thumbgate-post.js +107 -0
  300. package/scripts/social-analytics/load-env.js +46 -0
  301. package/scripts/social-analytics/mcp-server.js +289 -0
  302. package/scripts/social-analytics/normalizer.js +580 -0
  303. package/scripts/social-analytics/notify.js +162 -0
  304. package/scripts/social-analytics/poll-all.js +92 -0
  305. package/scripts/social-analytics/pollers/github.js +195 -0
  306. package/scripts/social-analytics/pollers/instagram.js +253 -0
  307. package/scripts/social-analytics/pollers/linkedin.js +330 -0
  308. package/scripts/social-analytics/pollers/plausible.js +247 -0
  309. package/scripts/social-analytics/pollers/reddit.js +306 -0
  310. package/scripts/social-analytics/pollers/threads.js +233 -0
  311. package/scripts/social-analytics/pollers/tiktok.js +203 -0
  312. package/scripts/social-analytics/pollers/x.js +227 -0
  313. package/scripts/social-analytics/pollers/youtube.js +304 -0
  314. package/scripts/social-analytics/pollers/zernio.js +183 -0
  315. package/scripts/social-analytics/publish-instagram-thumbgate.js +98 -0
  316. package/scripts/social-analytics/publish-thumbgate-launch.js +316 -0
  317. package/scripts/social-analytics/publishers/devto.js +122 -0
  318. package/scripts/social-analytics/publishers/instagram.js +317 -0
  319. package/scripts/social-analytics/publishers/linkedin.js +294 -0
  320. package/scripts/social-analytics/publishers/reddit.js +390 -0
  321. package/scripts/social-analytics/publishers/threads.js +275 -0
  322. package/scripts/social-analytics/publishers/tiktok.js +217 -0
  323. package/scripts/social-analytics/publishers/x.js +259 -0
  324. package/scripts/social-analytics/publishers/youtube.js +223 -0
  325. package/scripts/social-analytics/publishers/zernio.js +378 -0
  326. package/scripts/social-analytics/run-digest.js +34 -0
  327. package/scripts/social-analytics/store.js +257 -0
  328. package/scripts/social-analytics/utm.js +143 -0
  329. package/scripts/social-pipeline.js +2628 -0
  330. package/scripts/social-quality-gate.js +18 -0
  331. package/scripts/social-reply-monitor.js +445 -0
  332. package/scripts/status-dashboard.js +155 -0
  333. package/scripts/statusline-lesson.js +16 -0
  334. package/scripts/statusline-tower.js +8 -0
  335. package/scripts/statusline.sh +116 -0
  336. package/scripts/stripe-live-status.js +115 -0
  337. package/scripts/subagent-profiles.js +79 -0
  338. package/scripts/sync-gh-secrets-from-env.sh +70 -0
  339. package/scripts/sync-github-about.js +52 -0
  340. package/scripts/sync-version.js +447 -0
  341. package/scripts/synthetic-dpo.js +234 -0
  342. package/scripts/telemetry-analytics.js +821 -0
  343. package/scripts/tessl-export.js +371 -0
  344. package/scripts/test-coverage.js +120 -0
  345. package/scripts/thompson-sampling.js +417 -0
  346. package/scripts/thumbgate-search.js +189 -0
  347. package/scripts/tool-kpi-tracker.js +12 -0
  348. package/scripts/tool-registry.js +811 -0
  349. package/scripts/train_from_feedback.py +933 -0
  350. package/scripts/user-profile.js +78 -0
  351. package/scripts/validate-feedback.js +581 -0
  352. package/scripts/validate-workflow-contract.js +287 -0
  353. package/scripts/vector-store.js +197 -0
  354. package/scripts/verification-loop.js +291 -0
  355. package/scripts/verify-obsidian-setup.sh +269 -0
  356. package/scripts/verify-run.js +269 -0
  357. package/scripts/webhook-delivery.js +62 -0
  358. package/scripts/weekly-auto-post.js +124 -0
  359. package/scripts/workflow-runs.js +154 -0
  360. package/scripts/workflow-sprint-intake.js +475 -0
  361. package/scripts/workspace-evolver.js +374 -0
  362. package/scripts/x-autonomous-marketing.js +139 -0
  363. package/scripts/xmemory-lite.js +405 -0
  364. package/skills/agent-memory/SKILL.md +97 -0
  365. package/skills/rlhf-feedback/SKILL.md +49 -0
  366. package/skills/solve-architecture-autonomy/SKILL.md +17 -0
  367. package/skills/solve-architecture-autonomy/tool.js +33 -0
  368. package/skills/thumbgate/SKILL.md +114 -0
  369. package/src/api/server.js +4206 -0
@@ -0,0 +1,675 @@
1
+ 'use strict';
2
+ /**
3
+ * Hybrid Feedback Context — Pre-Tool Guard Engine (ATTR-02)
4
+ *
5
+ * Builds attributed feedback state from multiple JSONL sources and compiles
6
+ * it into a fast guard artifact for pre-tool execution decisions:
7
+ * block — attributed negative patterns exceed threshold
8
+ * warn — soft negative signal; proceed with caution
9
+ * allow — no matching negative patterns (default)
10
+ *
11
+ * Exports:
12
+ * buildHybridState, evaluatePretool, compileGuardArtifact,
13
+ * writeGuardArtifact, readGuardArtifact, evaluateCompiledGuards,
14
+ * evaluatePretoolFromState, deriveConstraints, buildAdditionalContext
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { resolveFeedbackDir } = require('./feedback-paths');
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Paths
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const FEEDBACK_DIR = resolveFeedbackDir();
26
+ const PATHS = {
27
+ feedbackLog: path.join(FEEDBACK_DIR, 'feedback-log.jsonl'),
28
+ inbox: path.join(FEEDBACK_DIR, 'inbox.jsonl'),
29
+ pendingSync: path.join(FEEDBACK_DIR, 'pending_cortex_sync.jsonl'),
30
+ attributedFeedback: path.join(FEEDBACK_DIR, 'attributed-feedback.jsonl'),
31
+ guardArtifact: path.join(FEEDBACK_DIR, 'pretool-guards.json'),
32
+ };
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Constants
36
+ // ---------------------------------------------------------------------------
37
+
38
+ const STOPWORDS = new Set([
39
+ 'the', 'and', 'for', 'was', 'with', 'from', 'that', 'this', 'are', 'have',
40
+ 'has', 'had', 'not', 'but', 'they', 'you', 'can', 'will', 'all', 'any',
41
+ 'one', 'its', 'our', 'also', 'more', 'very', 'just', 'into', 'been',
42
+ 'bash', 'edit', 'write', 'tool', 'hook', 'clear',
43
+ ]);
44
+
45
+ const NEG = new Set([
46
+ 'negative', 'thumbsdown', 'thumbs_down', 'thumbs-down', 'down', 'bad',
47
+ 'wrong', 'error', 'fail', 'failed', 'failure', 'mistake', 'bug', 'broken',
48
+ ]);
49
+
50
+ const POS = new Set([
51
+ 'positive', 'thumbsup', 'thumbs_up', 'thumbs-up', 'up', 'good', 'correct',
52
+ 'success', 'pass', 'passed', 'great', 'excellent', 'perfect', 'works',
53
+ ]);
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Low-level helpers
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Read last maxLines of a JSONL file in reverse, then re-reverse so oldest-first.
61
+ */
62
+ function readJsonl(filePath, maxLines) {
63
+ const limit = maxLines !== undefined ? maxLines : 400;
64
+ if (!fs.existsSync(filePath)) return [];
65
+ let raw;
66
+ try {
67
+ raw = fs.readFileSync(filePath, 'utf8').trimEnd();
68
+ } catch (_) {
69
+ return [];
70
+ }
71
+ if (!raw) return [];
72
+ const lines = raw.split('\n');
73
+ const slice = lines.slice(-limit);
74
+ const parsed = [];
75
+ for (let i = slice.length - 1; i >= 0; i--) {
76
+ const line = slice[i].trim();
77
+ if (!line) continue;
78
+ try {
79
+ parsed.push(JSON.parse(line));
80
+ } catch (_) {
81
+ // skip malformed
82
+ }
83
+ }
84
+ parsed.reverse(); // back to chronological order
85
+ return parsed;
86
+ }
87
+
88
+ /**
89
+ * Normalize text: strip /Users/ paths, port numbers, lowercase.
90
+ */
91
+ function normalize(text) {
92
+ if (!text || typeof text !== 'string') return '';
93
+ return text
94
+ .replace(/\/Users\/[^\s/]+/g, '/Users/redacted')
95
+ .replace(/:\d{4,5}\b/g, ':PORT')
96
+ .toLowerCase()
97
+ .trim();
98
+ }
99
+
100
+ /**
101
+ * Strip common feedback prefix tokens from a string.
102
+ */
103
+ function stripFeedbackPrefix(text) {
104
+ if (!text) return '';
105
+ return text
106
+ .replace(/^(thumbs?\s*(up|down)\s*:?\s*)/i, '')
107
+ .replace(/^(positive|negative)\s*(feedback)?\s*:?\s*/i, '')
108
+ .replace(/^(good|bad|wrong|error|fail(ed|ure)?)\s*:?\s*/i, '')
109
+ .trim();
110
+ }
111
+
112
+ /**
113
+ * Compose normalize + stripFeedbackPrefix.
114
+ */
115
+ function normalizePatternText(text) {
116
+ return normalize(stripFeedbackPrefix(text));
117
+ }
118
+
119
+ /**
120
+ * Infer tool name from raw name or context keywords.
121
+ */
122
+ function inferToolName(rawToolName, context) {
123
+ if (rawToolName && rawToolName !== 'unknown') return rawToolName;
124
+ const ctx = (context || '').toLowerCase();
125
+ if (ctx.includes('bash') || ctx.includes('command') || ctx.includes('shell')) return 'Bash';
126
+ if (ctx.includes('edit') || ctx.includes('patch') || ctx.includes('replace')) return 'Edit';
127
+ if (ctx.includes('write') || ctx.includes('create file') || ctx.includes('overwrite')) return 'Write';
128
+ if (ctx.includes('read') || ctx.includes('cat ') || ctx.includes('view file')) return 'Read';
129
+ if (ctx.includes('search') || ctx.includes('grep') || ctx.includes('find')) return 'Grep';
130
+ if (ctx.includes('glob') || ctx.includes('list files')) return 'Glob';
131
+ return rawToolName || 'unknown';
132
+ }
133
+
134
+ /**
135
+ * Classify an entry as 'positive', 'negative', or 'neutral'.
136
+ */
137
+ function classify(entry) {
138
+ const raw = String(entry.signal || entry.feedback || '').toLowerCase().trim();
139
+ if (NEG.has(raw)) return 'negative';
140
+ if (POS.has(raw)) return 'positive';
141
+ return 'neutral';
142
+ }
143
+
144
+ /**
145
+ * Extract ms from a timestamp value. Returns 0 on failure.
146
+ */
147
+ function getTimestampMs(value) {
148
+ if (!value) return 0;
149
+ const ms = Date.parse(value);
150
+ return isNaN(ms) ? 0 : ms;
151
+ }
152
+
153
+ /**
154
+ * Extract meaningful keywords from text.
155
+ * min 4 chars, no stopwords, max 8 tokens.
156
+ */
157
+ function keywords(text) {
158
+ if (!text) return [];
159
+ const tokens = normalize(text)
160
+ .replace(/[^a-z0-9\s_-]/g, ' ')
161
+ .split(/\s+/)
162
+ .filter((t) => t.length >= 4 && !STOPWORDS.has(t));
163
+ return [...new Set(tokens)].slice(0, 8);
164
+ }
165
+
166
+ /**
167
+ * FNV-1a 32-bit hash.
168
+ */
169
+ function hashText(text) {
170
+ let hash = 2166136261;
171
+ const str = String(text || '');
172
+ for (let i = 0; i < str.length; i++) {
173
+ hash ^= str.charCodeAt(i);
174
+ hash = (hash * 16777619) >>> 0;
175
+ }
176
+ return hash.toString(16).padStart(8, '0');
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // buildHybridState
181
+ // ---------------------------------------------------------------------------
182
+
183
+ /**
184
+ * Build hybrid state by reading from all JSONL sources.
185
+ *
186
+ * @param {Object} opts
187
+ * @param {string} [opts.feedbackLogPath]
188
+ * @param {string} [opts.inboxPath]
189
+ * @param {string} [opts.pendingSyncPath]
190
+ * @param {string} [opts.attributedFeedbackPath]
191
+ * @returns {Object} state
192
+ */
193
+ function buildHybridState(opts) {
194
+ const o = opts || {};
195
+ const feedbackLogPath = o.feedbackLogPath || process.env.THUMBGATE_FEEDBACK_LOG || PATHS.feedbackLog;
196
+ const inboxPath = o.inboxPath || process.env.THUMBGATE_FEEDBACK_INBOX || PATHS.inbox;
197
+ const pendingSyncPath = o.pendingSyncPath || process.env.THUMBGATE_PENDING_SYNC || PATHS.pendingSync;
198
+ const attributedFeedbackPath = o.attributedFeedbackPath || process.env.THUMBGATE_ATTRIBUTED_FEEDBACK || PATHS.attributedFeedback;
199
+
200
+ const feedbackEntries = readJsonl(feedbackLogPath);
201
+ const inboxEntries = readJsonl(inboxPath);
202
+ const pendingSyncEntries = readJsonl(pendingSyncPath);
203
+ const attributedEntries = readJsonl(attributedFeedbackPath);
204
+
205
+ // Deduplicate by id across all sources
206
+ const seen = new Set();
207
+ const allEntries = [];
208
+ for (const entry of [...feedbackEntries, ...inboxEntries, ...pendingSyncEntries]) {
209
+ const key = entry.id || hashText(JSON.stringify(entry));
210
+ if (!seen.has(key)) {
211
+ seen.add(key);
212
+ allEntries.push(entry);
213
+ }
214
+ }
215
+
216
+ // Build counts
217
+ let total = 0;
218
+ let positive = 0;
219
+ let negative = 0;
220
+ const patternMap = {}; // normalized text -> { count, lastSeen, sources, text }
221
+ const toolNegatives = {}; // toolName -> count
222
+ const toolNegativesAttributed = {}; // toolName -> count (from attributed only)
223
+
224
+ for (const entry of allEntries) {
225
+ total++;
226
+ const cls = classify(entry);
227
+ if (cls === 'positive') positive++;
228
+ if (cls === 'negative') {
229
+ negative++;
230
+ // Track tool-level negative counts
231
+ const toolName = inferToolName(entry.toolName || entry.tool_name || 'unknown', entry.context || '');
232
+ toolNegatives[toolName] = (toolNegatives[toolName] || 0) + 1;
233
+
234
+ // Build pattern from context / whatWentWrong / what_went_wrong
235
+ const rawText = [
236
+ entry.context || '',
237
+ entry.whatWentWrong || entry.what_went_wrong || '',
238
+ entry.whatToChange || entry.what_to_change || '',
239
+ ].join(' ');
240
+ const norm = normalizePatternText(rawText);
241
+ if (!norm) continue;
242
+ const words = keywords(norm);
243
+ if (words.length < 2) continue; // need at least 2 meaningful words
244
+ const patKey = words.slice(0, 4).join('_');
245
+ if (!patternMap[patKey]) {
246
+ patternMap[patKey] = { count: 0, lastSeen: 0, sources: [], text: norm, words };
247
+ }
248
+ patternMap[patKey].count++;
249
+ const ts = getTimestampMs(entry.timestamp);
250
+ if (ts > patternMap[patKey].lastSeen) patternMap[patKey].lastSeen = ts;
251
+ patternMap[patKey].sources.push('feedbackLog');
252
+ }
253
+ }
254
+
255
+ // Process attributed feedback separately to track attributed tool counts
256
+ for (const entry of attributedEntries) {
257
+ const toolName = inferToolName(entry.toolName || entry.tool_name || entry.attributed_tool || 'unknown', entry.context || '');
258
+ toolNegativesAttributed[toolName] = (toolNegativesAttributed[toolName] || 0) + 1;
259
+
260
+ const rawText = [
261
+ entry.context || '',
262
+ entry.whatWentWrong || entry.what_went_wrong || '',
263
+ ].join(' ');
264
+ const norm = normalizePatternText(rawText);
265
+ if (!norm) continue;
266
+ const words = keywords(norm);
267
+ if (words.length < 2) continue;
268
+ const patKey = words.slice(0, 4).join('_');
269
+ if (!patternMap[patKey]) {
270
+ patternMap[patKey] = { count: 0, lastSeen: 0, sources: [], text: norm, words };
271
+ }
272
+ // Mark as attributed source (prefer over raw feedbackLog)
273
+ if (!patternMap[patKey].sources.includes('attributedFeedback')) {
274
+ patternMap[patKey].sources.push('attributedFeedback');
275
+ }
276
+ patternMap[patKey].count++;
277
+ const ts = getTimestampMs(entry.timestamp);
278
+ if (ts > patternMap[patKey].lastSeen) patternMap[patKey].lastSeen = ts;
279
+ }
280
+
281
+ // Recurring = count >= 2
282
+ const recurringNegativePatterns = Object.values(patternMap)
283
+ .filter((p) => p.count >= 2)
284
+ .sort((a, b) => b.count - a.count);
285
+
286
+ // Prevention rules from feedbackLog (whatToChange fields)
287
+ const preventionRules = allEntries
288
+ .filter((e) => classify(e) === 'negative' && (e.whatToChange || e.what_to_change))
289
+ .map((e) => normalize(e.whatToChange || e.what_to_change))
290
+ .filter(Boolean);
291
+
292
+ return {
293
+ counts: { total, positive, negative },
294
+ recurringNegativePatterns,
295
+ preventionRules,
296
+ negativeToolCounts: toolNegatives,
297
+ negativeToolCountsAttributed: toolNegativesAttributed,
298
+ };
299
+ }
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // deriveConstraints
303
+ // ---------------------------------------------------------------------------
304
+
305
+ /**
306
+ * Produce up to `max` actionable constraint strings from recurring patterns.
307
+ *
308
+ * @param {Object} state - from buildHybridState()
309
+ * @param {number} [max=5]
310
+ * @returns {string[]}
311
+ */
312
+ function deriveConstraints(state, max) {
313
+ const limit = max !== undefined ? max : 5;
314
+ const constraints = [];
315
+
316
+ // Top recurring patterns become constraints
317
+ for (const pattern of (state.recurringNegativePatterns || []).slice(0, limit)) {
318
+ const truncated = pattern.text.length > 100 ? pattern.text.slice(0, 100) + '...' : pattern.text;
319
+ constraints.push(`Avoid: "${truncated}" (seen ${pattern.count}x)`);
320
+ }
321
+
322
+ // Prevention rules fill remaining slots
323
+ const remaining = limit - constraints.length;
324
+ for (const rule of (state.preventionRules || []).slice(0, remaining)) {
325
+ const truncated = rule.length > 100 ? rule.slice(0, 100) + '...' : rule;
326
+ constraints.push(`Rule: ${truncated}`);
327
+ }
328
+
329
+ return constraints.slice(0, limit);
330
+ }
331
+
332
+ // ---------------------------------------------------------------------------
333
+ // buildAdditionalContext
334
+ // ---------------------------------------------------------------------------
335
+
336
+ /**
337
+ * Format a single summary string for pre-tool context injection.
338
+ *
339
+ * @param {Object} state
340
+ * @param {string[]} constraints
341
+ * @param {number} [maxChars=800]
342
+ * @returns {string}
343
+ */
344
+ function buildAdditionalContext(state, constraints, maxChars) {
345
+ const limit = maxChars !== undefined ? maxChars : 800;
346
+ const { counts } = state;
347
+ const lines = [
348
+ `Feedback history: ${counts.total} total (${counts.positive} positive, ${counts.negative} negative)`,
349
+ `Recurring patterns: ${(state.recurringNegativePatterns || []).length}`,
350
+ ];
351
+ if (constraints && constraints.length > 0) {
352
+ lines.push('Active constraints:');
353
+ constraints.forEach((c) => lines.push(` - ${c}`));
354
+ }
355
+ let result = lines.join('\n');
356
+ if (result.length > limit) {
357
+ result = result.slice(0, limit - 3) + '...';
358
+ }
359
+ return result;
360
+ }
361
+
362
+ // ---------------------------------------------------------------------------
363
+ // hasTwoKeywordHits
364
+ // ---------------------------------------------------------------------------
365
+
366
+ /**
367
+ * Require 2+ keyword matches to reduce false positives (ATTR-03 no-false-positive invariant).
368
+ *
369
+ * @param {string} normalizedInput
370
+ * @param {string[]} words - keyword list from a pattern
371
+ * @returns {boolean}
372
+ */
373
+ function hasTwoKeywordHits(normalizedInput, words) {
374
+ if (!normalizedInput || !words || words.length === 0) return false;
375
+ let hits = 0;
376
+ for (const word of words) {
377
+ if (normalizedInput.includes(word)) {
378
+ hits++;
379
+ if (hits >= 2) return true;
380
+ }
381
+ }
382
+ return false;
383
+ }
384
+
385
+ // ---------------------------------------------------------------------------
386
+ // compileGuardArtifact
387
+ // ---------------------------------------------------------------------------
388
+
389
+ /**
390
+ * Build deduped guards array from state.
391
+ * Prefers patterns sourced from attributedFeedback. Assigns block/warn mode.
392
+ *
393
+ * @param {Object} state - from buildHybridState()
394
+ * @param {Object} [opts]
395
+ * @param {number} [opts.blockThreshold=3] - count >= this → block
396
+ * @returns {Object} artifact
397
+ */
398
+ function compileGuardArtifact(state, opts) {
399
+ const o = opts || {};
400
+ const blockThreshold = o.blockThreshold !== undefined ? o.blockThreshold : 3;
401
+
402
+ const guards = [];
403
+ const seenHashes = new Set();
404
+
405
+ for (const pattern of state.recurringNegativePatterns || []) {
406
+ const h = hashText(pattern.text);
407
+ if (seenHashes.has(h)) continue;
408
+ seenHashes.add(h);
409
+
410
+ const isAttributed = pattern.sources && pattern.sources.includes('attributedFeedback');
411
+ const mode = pattern.count >= blockThreshold ? 'block' : 'warn';
412
+
413
+ guards.push({
414
+ hash: h,
415
+ text: pattern.text,
416
+ words: pattern.words,
417
+ count: pattern.count,
418
+ lastSeen: pattern.lastSeen,
419
+ attributed: isAttributed,
420
+ mode,
421
+ });
422
+ }
423
+
424
+ // Sort: attributed first, then by count desc
425
+ guards.sort((a, b) => {
426
+ if (a.attributed && !b.attributed) return -1;
427
+ if (!a.attributed && b.attributed) return 1;
428
+ return b.count - a.count;
429
+ });
430
+
431
+ return {
432
+ compiledAt: new Date().toISOString(),
433
+ guardCount: guards.length,
434
+ blockThreshold,
435
+ guards,
436
+ };
437
+ }
438
+
439
+ // ---------------------------------------------------------------------------
440
+ // writeGuardArtifact / readGuardArtifact
441
+ // ---------------------------------------------------------------------------
442
+
443
+ /**
444
+ * Atomic write via tmp → rename.
445
+ *
446
+ * @param {string} filePath
447
+ * @param {Object} artifact
448
+ */
449
+ function writeGuardArtifact(filePath, artifact) {
450
+ const outPath = filePath || PATHS.guardArtifact;
451
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
452
+ const tmp = `${outPath}.tmp.${process.pid}.${Date.now()}`;
453
+ fs.writeFileSync(tmp, JSON.stringify(artifact, null, 2) + '\n');
454
+ fs.renameSync(tmp, outPath);
455
+ }
456
+
457
+ /**
458
+ * Read + validate a guard artifact.
459
+ *
460
+ * @param {string} [filePath]
461
+ * @returns {Object|null} artifact or null if invalid/missing
462
+ */
463
+ function readGuardArtifact(filePath) {
464
+ const inPath = filePath || process.env.THUMBGATE_GUARDS_PATH || PATHS.guardArtifact;
465
+ if (!fs.existsSync(inPath)) return null;
466
+ try {
467
+ const raw = fs.readFileSync(inPath, 'utf8');
468
+ const obj = JSON.parse(raw);
469
+ if (!Array.isArray(obj.guards)) return null;
470
+ return obj;
471
+ } catch (_) {
472
+ return null;
473
+ }
474
+ }
475
+
476
+ // ---------------------------------------------------------------------------
477
+ // evaluateCompiledGuards (fast path)
478
+ // ---------------------------------------------------------------------------
479
+
480
+ /**
481
+ * Check compiled artifact against toolName + toolInput.
482
+ *
483
+ * @param {Object} artifact
484
+ * @param {string} toolName
485
+ * @param {string} toolInput
486
+ * @returns {{ mode: string, reason: string, source: string }}
487
+ */
488
+ function evaluateCompiledGuards(artifact, toolName, toolInput) {
489
+ if (!artifact || !Array.isArray(artifact.guards)) {
490
+ return { mode: 'allow', reason: '', source: 'compiled' };
491
+ }
492
+
493
+ const normInput = normalize(toolInput || '');
494
+ const normTool = (toolName || '').toLowerCase();
495
+
496
+ for (const guard of artifact.guards) {
497
+ // Check if tool context is relevant
498
+ const guardText = normalize(guard.text || '');
499
+ const toolMentioned = guardText.includes(normTool) || normTool === 'unknown';
500
+
501
+ if (hasTwoKeywordHits(normInput, guard.words || [])) {
502
+ return {
503
+ mode: guard.mode || 'warn',
504
+ reason: `Matched guard pattern (count: ${guard.count}): "${(guard.text || '').slice(0, 80)}"`,
505
+ source: 'compiled',
506
+ guardHash: guard.hash,
507
+ attributed: guard.attributed,
508
+ };
509
+ }
510
+
511
+ // Also check tool-level match when input is empty or short
512
+ if (normInput.length < 10 && toolMentioned && guard.count >= (artifact.blockThreshold || 3)) {
513
+ return {
514
+ mode: guard.mode || 'warn',
515
+ reason: `Tool "${toolName}" has recurring negative patterns (count: ${guard.count})`,
516
+ source: 'compiled',
517
+ guardHash: guard.hash,
518
+ attributed: guard.attributed,
519
+ };
520
+ }
521
+ }
522
+
523
+ return { mode: 'allow', reason: '', source: 'compiled' };
524
+ }
525
+
526
+ // ---------------------------------------------------------------------------
527
+ // evaluatePretoolFromState (live path)
528
+ // ---------------------------------------------------------------------------
529
+
530
+ /**
531
+ * Live path: check recurringNegativePatterns + negativeToolCounts.
532
+ *
533
+ * @param {Object} state - from buildHybridState()
534
+ * @param {string} toolName
535
+ * @param {string} toolInput
536
+ * @returns {{ mode: string, reason: string, source: string }}
537
+ */
538
+ function evaluatePretoolFromState(state, toolName, toolInput) {
539
+ const normInput = normalize(toolInput || '');
540
+ const normTool = (toolName || '').toLowerCase();
541
+
542
+ for (const pattern of state.recurringNegativePatterns || []) {
543
+ if (hasTwoKeywordHits(normInput, pattern.words || [])) {
544
+ const mode = pattern.count >= 3 ? 'block' : 'warn';
545
+ return {
546
+ mode,
547
+ reason: `Recurring negative pattern (count: ${pattern.count}): "${(pattern.text || '').slice(0, 80)}"`,
548
+ source: 'state',
549
+ };
550
+ }
551
+ }
552
+
553
+ // Tool-level check: if this tool has many attributed negatives
554
+ const attrCount = (state.negativeToolCountsAttributed || {})[toolName] || 0;
555
+ const rawCount = (state.negativeToolCounts || {})[toolName] || 0;
556
+ if (attrCount >= 3 || rawCount >= 5) {
557
+ return {
558
+ mode: attrCount >= 3 ? 'block' : 'warn',
559
+ reason: `Tool "${toolName}" has ${attrCount} attributed negative(s), ${rawCount} total negative(s)`,
560
+ source: 'state',
561
+ };
562
+ }
563
+
564
+ return { mode: 'allow', reason: '', source: 'state' };
565
+ }
566
+
567
+ // ---------------------------------------------------------------------------
568
+ // evaluatePretool (orchestrator)
569
+ // ---------------------------------------------------------------------------
570
+
571
+ /**
572
+ * Main pre-tool evaluation. Tries compiled artifact first, falls back to live state.
573
+ *
574
+ * Important invariant: a tool+input with NEVER a negative returns {mode:'allow'}.
575
+ * hasTwoKeywordHits and count >= 2 filters enforce this (ATTR-03 no-false-positives).
576
+ *
577
+ * @param {string} toolName
578
+ * @param {string} toolInput
579
+ * @param {Object} [opts]
580
+ * @param {string} [opts.guardArtifactPath]
581
+ * @param {string} [opts.feedbackLogPath]
582
+ * @param {string} [opts.attributedFeedbackPath]
583
+ * @returns {{ mode: 'block'|'warn'|'allow', reason: string, source: string }}
584
+ */
585
+ function evaluatePretool(toolName, toolInput, opts) {
586
+ const o = opts || {};
587
+
588
+ // Fast path: compiled artifact
589
+ const artifactPath = o.guardArtifactPath || process.env.THUMBGATE_GUARDS_PATH || PATHS.guardArtifact;
590
+ const artifact = readGuardArtifact(artifactPath);
591
+ if (artifact) {
592
+ const result = evaluateCompiledGuards(artifact, toolName, toolInput);
593
+ if (result.mode !== 'allow') return result;
594
+ // Even if compiled says allow, we're done (trust compiled)
595
+ return result;
596
+ }
597
+
598
+ // Slow path: build live state
599
+ const state = buildHybridState({
600
+ feedbackLogPath: o.feedbackLogPath,
601
+ attributedFeedbackPath: o.attributedFeedbackPath,
602
+ });
603
+ return evaluatePretoolFromState(state, toolName, toolInput);
604
+ }
605
+
606
+ // ---------------------------------------------------------------------------
607
+ // CLI main()
608
+ // ---------------------------------------------------------------------------
609
+
610
+ function main() {
611
+ const args = process.argv.slice(2);
612
+
613
+ if (args[0] === '--pretool') {
614
+ const toolName = args[1] || 'unknown';
615
+ const rawInput = args[2] || '';
616
+ let toolInput = rawInput;
617
+ try {
618
+ const parsed = JSON.parse(rawInput);
619
+ toolInput = typeof parsed === 'object' ? JSON.stringify(parsed) : String(parsed);
620
+ } catch (_) {
621
+ toolInput = rawInput;
622
+ }
623
+ const result = evaluatePretool(toolName, toolInput);
624
+ console.log(JSON.stringify(result, null, 2));
625
+ process.exit(result.mode === 'block' ? 2 : 0);
626
+ return;
627
+ }
628
+
629
+ if (args[0] === '--compile-guards') {
630
+ const outPath = args[1] || PATHS.guardArtifact;
631
+ const state = buildHybridState({});
632
+ const artifact = compileGuardArtifact(state);
633
+ writeGuardArtifact(outPath, artifact);
634
+ console.log(JSON.stringify({ guardCount: artifact.guardCount, outPath, compiledAt: artifact.compiledAt }, null, 2));
635
+ process.exit(0);
636
+ return;
637
+ }
638
+
639
+ // Default: print full state + constraints + additional context
640
+ const state = buildHybridState({});
641
+ const constraints = deriveConstraints(state);
642
+ const additionalContext = buildAdditionalContext(state, constraints);
643
+ console.log('=== Hybrid Feedback State ===');
644
+ console.log(JSON.stringify({ state, constraints, additionalContext }, null, 2));
645
+ }
646
+
647
+ // ---------------------------------------------------------------------------
648
+ // Exports
649
+ // ---------------------------------------------------------------------------
650
+
651
+ module.exports = {
652
+ buildHybridState,
653
+ evaluatePretool,
654
+ compileGuardArtifact,
655
+ writeGuardArtifact,
656
+ readGuardArtifact,
657
+ evaluateCompiledGuards,
658
+ evaluatePretoolFromState,
659
+ deriveConstraints,
660
+ buildAdditionalContext,
661
+ // Internal helpers (exposed for testing)
662
+ normalize,
663
+ normalizePatternText,
664
+ inferToolName,
665
+ classify,
666
+ keywords,
667
+ hashText,
668
+ hasTwoKeywordHits,
669
+ readJsonl,
670
+ PATHS,
671
+ };
672
+
673
+ if (require.main === module) {
674
+ main();
675
+ }