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,446 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { resolveFeedbackDir } = require('./feedback-paths');
7
+
8
+ const NEG = new Set(['negative', 'negative_strong', 'down', 'thumbs_down']);
9
+ const POS = new Set(['positive', 'positive_strong', 'up', 'thumbs_up']);
10
+
11
+ /** Minimum cluster size to generate a skill */
12
+ const MIN_CLUSTER_SIZE = 3;
13
+
14
+ /** Minimum tag overlap to consider two entries related */
15
+ const MIN_TAG_OVERLAP = 2;
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Directory discovery (mirrors feedback-loop.js)
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Discover the feedback directory using the standard ThumbGate resolution order:
23
+ * 1. THUMBGATE_FEEDBACK_DIR env var
24
+ * 2. .thumbgate/ in cwd
25
+ * 3. .claude/memory/feedback/ in cwd
26
+ * 4. ~/.thumbgate/projects/<cwd-basename>/
27
+ * @returns {string} Resolved feedback directory path
28
+ */
29
+ function discoverFeedbackDir() {
30
+ return resolveFeedbackDir();
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Ensure a directory exists, creating it recursively if needed.
39
+ * @param {string} dirPath
40
+ */
41
+ function ensureDir(dirPath) {
42
+ if (!fs.existsSync(dirPath)) {
43
+ fs.mkdirSync(dirPath, { recursive: true });
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Append a JSON record as a single line to a JSONL file.
49
+ * @param {string} filePath
50
+ * @param {object} record
51
+ */
52
+ function appendJSONL(filePath, record) {
53
+ ensureDir(path.dirname(filePath));
54
+ fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`);
55
+ }
56
+
57
+ /**
58
+ * Parse a JSONL feedback file into an array of entries.
59
+ * @param {string} filePath
60
+ * @returns {object[]}
61
+ */
62
+ function parseFeedbackFile(filePath) {
63
+ if (!fs.existsSync(filePath)) return [];
64
+ const entries = [];
65
+ for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
66
+ const trimmed = line.trim();
67
+ if (!trimmed) continue;
68
+ try { entries.push(JSON.parse(trimmed)); } catch { /* skip malformed */ }
69
+ }
70
+ return entries;
71
+ }
72
+
73
+ /**
74
+ * Classify a feedback entry signal as positive, negative, or null.
75
+ * @param {object} entry
76
+ * @returns {'positive'|'negative'|null}
77
+ */
78
+ function classifySignal(entry) {
79
+ const sig = (entry.signal || entry.feedback || '').toLowerCase();
80
+ if (NEG.has(sig)) return 'negative';
81
+ if (POS.has(sig)) return 'positive';
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * Extract tags from a feedback entry, including richContext.domain.
87
+ * @param {object} entry
88
+ * @returns {string[]}
89
+ */
90
+ function extractTags(entry) {
91
+ const tags = new Set();
92
+ if (Array.isArray(entry.tags)) {
93
+ for (const t of entry.tags) {
94
+ if (t && typeof t === 'string') tags.add(t.toLowerCase());
95
+ }
96
+ }
97
+ if (entry.richContext && entry.richContext.domain) {
98
+ tags.add(entry.richContext.domain.toLowerCase());
99
+ }
100
+ if (entry.task_category) {
101
+ tags.add(entry.task_category.toLowerCase());
102
+ }
103
+ if (entry.category) {
104
+ tags.add(entry.category.toLowerCase());
105
+ }
106
+ return [...tags];
107
+ }
108
+
109
+ /**
110
+ * Count overlapping elements between two string arrays.
111
+ * @param {string[]} a
112
+ * @param {string[]} b
113
+ * @returns {number}
114
+ */
115
+ function tagOverlap(a, b) {
116
+ const setB = new Set(b);
117
+ let count = 0;
118
+ for (const t of a) {
119
+ if (setB.has(t)) count++;
120
+ }
121
+ return count;
122
+ }
123
+
124
+ /**
125
+ * Slugify a string for use as a filename.
126
+ * @param {string} str
127
+ * @returns {string}
128
+ */
129
+ function slugify(str) {
130
+ return str
131
+ .toLowerCase()
132
+ .replace(/[^a-z0-9]+/g, '-')
133
+ .replace(/^-|-$/g, '')
134
+ .slice(0, 60);
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Clustering
139
+ // ---------------------------------------------------------------------------
140
+
141
+ /**
142
+ * Cluster negative feedback entries by tag overlap (>= minOverlap shared tags).
143
+ * Uses a single-pass union approach: each entry joins the first cluster
144
+ * it overlaps with, or starts a new one.
145
+ *
146
+ * @param {object[]} negEntries - Negative feedback entries (already classified)
147
+ * @param {number} [minOverlap=2] - Minimum shared tags to merge into a cluster
148
+ * @returns {Map<string, {tags: string[], entries: object[]}>}
149
+ */
150
+ function clusterByTags(negEntries, minOverlap) {
151
+ if (minOverlap === undefined) minOverlap = MIN_TAG_OVERLAP;
152
+
153
+ /** @type {Array<{tags: Set<string>, entries: object[]}>} */
154
+ const clusters = [];
155
+
156
+ for (const entry of negEntries) {
157
+ const entryTags = extractTags(entry);
158
+ if (entryTags.length === 0) continue;
159
+
160
+ let merged = false;
161
+ for (const cluster of clusters) {
162
+ const clusterTagsArr = [...cluster.tags];
163
+ if (tagOverlap(entryTags, clusterTagsArr) >= minOverlap) {
164
+ cluster.entries.push(entry);
165
+ for (const t of entryTags) cluster.tags.add(t);
166
+ merged = true;
167
+ break;
168
+ }
169
+ }
170
+
171
+ if (!merged) {
172
+ clusters.push({ tags: new Set(entryTags), entries: [entry] });
173
+ }
174
+ }
175
+
176
+ // Convert to a map keyed by sorted tag string
177
+ const result = new Map();
178
+ for (const cluster of clusters) {
179
+ const key = [...cluster.tags].sort().join(', ');
180
+ result.set(key, {
181
+ tags: [...cluster.tags].sort(),
182
+ entries: cluster.entries,
183
+ });
184
+ }
185
+ return result;
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Skill generation
190
+ // ---------------------------------------------------------------------------
191
+
192
+ /**
193
+ * Build DO rules from positive feedback entries whose tags overlap with the cluster.
194
+ * @param {object[]} posEntries - All positive feedback entries
195
+ * @param {string[]} clusterTags - Tags from the negative cluster
196
+ * @returns {string[]}
197
+ */
198
+ function buildDoRules(posEntries, clusterTags) {
199
+ const rules = [];
200
+ const seen = new Set();
201
+
202
+ for (const entry of posEntries) {
203
+ const entryTags = extractTags(entry);
204
+ if (tagOverlap(entryTags, clusterTags) < 1) continue;
205
+
206
+ const text = entry.whatWorked || entry.context || '';
207
+ if (!text || text.length < 10) continue;
208
+
209
+ const normalized = text.slice(0, 120).toLowerCase().trim();
210
+ if (seen.has(normalized)) continue;
211
+ seen.add(normalized);
212
+
213
+ rules.push(text.slice(0, 200));
214
+ }
215
+ return rules;
216
+ }
217
+
218
+ /**
219
+ * Build INSTEAD rules from the negative entries in a cluster.
220
+ * @param {object[]} negEntries - Negative entries from the cluster
221
+ * @returns {string[]}
222
+ */
223
+ function buildInsteadRules(negEntries) {
224
+ const rules = [];
225
+ const seen = new Set();
226
+
227
+ for (const entry of negEntries) {
228
+ const text = entry.whatWentWrong || entry.whatToChange || entry.context || '';
229
+ if (!text || text.length < 10) continue;
230
+
231
+ const normalized = text.slice(0, 120).toLowerCase().trim();
232
+ if (seen.has(normalized)) continue;
233
+ seen.add(normalized);
234
+
235
+ rules.push(text.slice(0, 200));
236
+ }
237
+ return rules;
238
+ }
239
+
240
+ /**
241
+ * Generate a SKILL.md string from a single cluster.
242
+ *
243
+ * @param {{tags: string[], entries: object[], doRules: string[], insteadRules: string[], approvalRate: string}} cluster
244
+ * @returns {string}
245
+ */
246
+ function generateSkillFromCluster(cluster) {
247
+ const { tags, entries, approvalRate } = cluster;
248
+ const doRules = cluster.doRules || (cluster.positiveEntries ? buildDoRules(cluster.positiveEntries, tags) : []);
249
+ const insteadRules = cluster.insteadRules || buildInsteadRules(entries);
250
+ const name = tags.slice(0, 3).join('-') || 'unnamed';
251
+ const description = `Prevention rules for ${tags.join(', ')} domain — auto-generated from ${entries.length} negative feedback signals.`;
252
+ const triggers = tags.map(t => `- Task involves \`${t}\``).join('\n');
253
+
254
+ const lines = [];
255
+
256
+ // Frontmatter
257
+ lines.push('---');
258
+ lines.push(`name: ${name}`);
259
+ lines.push(`description: ${description}`);
260
+ lines.push(`generated: ${new Date().toISOString()}`);
261
+ lines.push(`evidence_count: ${entries.length}`);
262
+ lines.push(`approval_rate: ${approvalRate}`);
263
+ lines.push('---');
264
+ lines.push('');
265
+
266
+ // Trigger conditions
267
+ lines.push('# Trigger Conditions');
268
+ lines.push('');
269
+ lines.push(triggers);
270
+ lines.push('');
271
+
272
+ // DO rules
273
+ lines.push('# DO Rules');
274
+ lines.push('');
275
+ if (doRules.length > 0) {
276
+ for (const rule of doRules) {
277
+ lines.push(`- ${rule}`);
278
+ }
279
+ } else {
280
+ lines.push('- No positive patterns recorded yet for this domain.');
281
+ }
282
+ lines.push('');
283
+
284
+ // INSTEAD rules
285
+ lines.push('# INSTEAD Rules');
286
+ lines.push('');
287
+ if (insteadRules.length > 0) {
288
+ for (const rule of insteadRules) {
289
+ lines.push(`- NEVER: ${rule}`);
290
+ }
291
+ } else {
292
+ lines.push('- No recurring anti-patterns extracted.');
293
+ }
294
+ lines.push('');
295
+
296
+ // Evidence
297
+ lines.push('# Evidence');
298
+ lines.push('');
299
+ lines.push(`- **Negative signals**: ${entries.length}`);
300
+ lines.push(`- **Domain approval rate**: ${approvalRate}`);
301
+ lines.push(`- **Tags**: ${tags.join(', ')}`);
302
+ lines.push(`- **DO rules**: ${doRules.length}`);
303
+ lines.push(`- **INSTEAD rules**: ${insteadRules.length}`);
304
+ lines.push('');
305
+
306
+ return lines.join('\n');
307
+ }
308
+
309
+ /**
310
+ * Generate skill files from ThumbGate feedback logs.
311
+ *
312
+ * Reads the feedback log, clusters negative feedback by tag overlap,
313
+ * and produces SKILL.md files for clusters with 3+ entries.
314
+ *
315
+ * @param {object} [options]
316
+ * @param {string} [options.feedbackDir] - Override feedback directory
317
+ * @param {number} [options.minClusterSize] - Minimum entries per cluster (default 3)
318
+ * @param {number} [options.minTagOverlap] - Minimum tag overlap for clustering (default 2)
319
+ * @param {boolean} [options.dryRun] - If true, return results without writing files
320
+ * @returns {{skillName: string, filePath: string, ruleCount: number, evidenceCount: number}[]}
321
+ */
322
+ function generateSkills(options) {
323
+ if (!options) options = {};
324
+ const feedbackDir = options.feedbackDir || discoverFeedbackDir();
325
+ const minClusterSize = options.minClusterSize || MIN_CLUSTER_SIZE;
326
+ const minTagOverlap = options.minTagOverlap || MIN_TAG_OVERLAP;
327
+ const dryRun = options.dryRun || false;
328
+
329
+ const logPath = path.join(feedbackDir, 'feedback-log.jsonl');
330
+ const outputDir = path.join(feedbackDir, 'generated-skills');
331
+ const auditLogPath = path.join(feedbackDir, 'skill-generation-audit.jsonl');
332
+
333
+ const entries = parseFeedbackFile(logPath);
334
+ if (entries.length === 0) return [];
335
+
336
+ // Separate positive and negative entries
337
+ const posEntries = [];
338
+ const negEntries = [];
339
+ for (const entry of entries) {
340
+ const cls = classifySignal(entry);
341
+ if (cls === 'positive') posEntries.push(entry);
342
+ else if (cls === 'negative') negEntries.push(entry);
343
+ }
344
+
345
+ if (negEntries.length === 0) return [];
346
+
347
+ // Cluster negative feedback by tag overlap
348
+ const clusters = clusterByTags(negEntries, minTagOverlap);
349
+
350
+ const results = [];
351
+
352
+ for (const [key, cluster] of clusters) {
353
+ if (cluster.entries.length < minClusterSize) continue;
354
+
355
+ // Compute domain-scoped approval rate
356
+ const clusterTags = cluster.tags;
357
+ let domainPos = 0;
358
+ const domainNeg = cluster.entries.length;
359
+ for (const pe of posEntries) {
360
+ if (tagOverlap(extractTags(pe), clusterTags) >= 1) domainPos++;
361
+ }
362
+ const domainTotal = domainPos + domainNeg;
363
+ const approvalRate = domainTotal > 0
364
+ ? `${((domainPos / domainTotal) * 100).toFixed(1)}%`
365
+ : '0.0%';
366
+
367
+ const doRules = buildDoRules(posEntries, clusterTags);
368
+ const insteadRules = buildInsteadRules(cluster.entries);
369
+ const ruleCount = doRules.length + insteadRules.length;
370
+
371
+ const skillContent = generateSkillFromCluster({
372
+ tags: clusterTags,
373
+ entries: cluster.entries,
374
+ doRules,
375
+ insteadRules,
376
+ approvalRate,
377
+ });
378
+
379
+ const skillName = slugify(clusterTags.slice(0, 3).join('-')) || 'unnamed';
380
+ const fileName = `${skillName}.SKILL.md`;
381
+ const filePath = path.join(outputDir, fileName);
382
+
383
+ if (!dryRun) {
384
+ ensureDir(outputDir);
385
+ fs.writeFileSync(filePath, skillContent, 'utf8');
386
+
387
+ // Audit log
388
+ appendJSONL(auditLogPath, {
389
+ event: 'skill_generated',
390
+ skillName,
391
+ filePath,
392
+ ruleCount,
393
+ evidenceCount: cluster.entries.length,
394
+ tags: clusterTags,
395
+ approvalRate,
396
+ timestamp: new Date().toISOString(),
397
+ });
398
+ }
399
+
400
+ results.push({
401
+ skillName,
402
+ filePath,
403
+ ruleCount,
404
+ evidenceCount: cluster.entries.length,
405
+ });
406
+ }
407
+
408
+ return results;
409
+ }
410
+
411
+ // ---------------------------------------------------------------------------
412
+ // CLI entry point
413
+ // ---------------------------------------------------------------------------
414
+
415
+ if (require.main === module) {
416
+ try {
417
+ const dryRun = process.argv.includes('--dry-run');
418
+ const feedbackDir = process.argv.find(function(a) {
419
+ return !a.startsWith('-') && a !== process.argv[0] && a !== process.argv[1];
420
+ });
421
+ const results = generateSkills({ feedbackDir: feedbackDir || undefined, dryRun: dryRun });
422
+
423
+ if (results.length === 0) {
424
+ console.log('No clusters met the threshold for skill generation.');
425
+ } else {
426
+ console.log('Generated ' + results.length + ' skill(s):');
427
+ for (const r of results) {
428
+ console.log(' ' + r.skillName + ' — ' + r.ruleCount + ' rules, ' + r.evidenceCount + ' signals → ' + r.filePath);
429
+ }
430
+ }
431
+ if (dryRun) console.log('(dry run — no files written)');
432
+ } catch (err) {
433
+ console.error('Error:', err.message);
434
+ process.exit(1);
435
+ }
436
+ }
437
+
438
+ module.exports = {
439
+ generateSkills,
440
+ generateSkillFromCluster,
441
+ discoverFeedbackDir,
442
+ clusterByTags,
443
+ extractTags,
444
+ parseFeedbackFile,
445
+ classifySignal,
446
+ };
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Skill Materializer (EvoSkill Phase 2)
4
+ *
5
+ * Takes a JSON proposal from Skill Proposer and 'materializes'
6
+ * a functional MCP tool definition + SKILL.md documentation.
7
+ *
8
+ * Ensures every skill has a standardized entry point for the agent.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { discoverFeedbackDir } = require('./skill-generator');
14
+
15
+ function materializeSkills(options = {}) {
16
+ const feedbackDir = options.feedbackDir || discoverFeedbackDir();
17
+ const proposalsDir = path.join(feedbackDir, 'skill-proposals');
18
+ const skillsOutDir = options.skillsOutDir || process.env.THUMBGATE_SKILLS_DIR || path.join(process.cwd(), 'skills');
19
+
20
+ if (!fs.existsSync(proposalsDir)) {
21
+ console.log('No proposals directory found.');
22
+ return;
23
+ }
24
+
25
+ const proposals = fs.readdirSync(proposalsDir)
26
+ .filter(f => f.endsWith('.json'))
27
+ .map(f => JSON.parse(fs.readFileSync(path.join(proposalsDir, f), 'utf-8')));
28
+
29
+ if (proposals.length === 0) {
30
+ console.log('No pending skill proposals.');
31
+ return;
32
+ }
33
+
34
+ if (!fs.existsSync(skillsOutDir)) fs.mkdirSync(skillsOutDir, { recursive: true });
35
+
36
+ const results = [];
37
+
38
+ for (const proposal of proposals) {
39
+ if (proposal.status !== 'pending') continue;
40
+
41
+ const skillName = proposal.suggestedSkill.name;
42
+ const skillDir = path.join(skillsOutDir, skillName);
43
+ if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
44
+
45
+ // Generate SKILL.md
46
+ const skillMd = generateSkillMarkdown(proposal);
47
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillMd);
48
+
49
+ // Generate functional tool code (template)
50
+ const toolCode = generateToolCode(proposal);
51
+ fs.writeFileSync(path.join(skillDir, 'tool.js'), toolCode);
52
+
53
+ // Update proposal status
54
+ proposal.status = 'materialized';
55
+ proposal.materializedAt = new Date().toISOString();
56
+ fs.writeFileSync(
57
+ path.join(proposalsDir, `${skillName}.json`),
58
+ JSON.stringify(proposal, null, 2)
59
+ );
60
+
61
+ results.push(skillName);
62
+ console.log(`Materialized skill: ${skillName} -> ${skillDir}`);
63
+ }
64
+
65
+ return results;
66
+ }
67
+
68
+ function generateSkillMarkdown(proposal) {
69
+ const { suggestedSkill, problem, diagnosis } = proposal;
70
+ return `---
71
+ name: ${suggestedSkill.name}
72
+ description: ${suggestedSkill.description}
73
+ diagnosis: ${diagnosis}
74
+ status: materialized
75
+ ---
76
+
77
+ # ${suggestedSkill.name.toUpperCase()} Capability
78
+
79
+ ## Problem
80
+ ${problem}
81
+
82
+ ## Automated Diagnosis
83
+ ${diagnosis}
84
+
85
+ ## Usage
86
+ The agent should call the \`${suggestedSkill.toolSpec.name}\` tool when tasks involve \`${suggestedSkill.tags.join(', ')}\`.
87
+ `;
88
+ }
89
+
90
+ function generateToolCode(proposal) {
91
+ const { suggestedSkill } = proposal;
92
+ const toolName = suggestedSkill.toolSpec.name;
93
+
94
+ return `/**
95
+ * Automated Skill: ${suggestedSkill.name}
96
+ * Generated: ${new Date().toISOString()}
97
+ *
98
+ * This tool was materialized by the EvoSkill loop to address:
99
+ * "${proposal.problem}"
100
+ */
101
+
102
+ const { execSync } = require('child_process');
103
+
104
+ /**
105
+ * ${suggestedSkill.toolSpec.description}
106
+ */
107
+ async function ${toolName}(args) {
108
+ const { context } = args;
109
+
110
+ // LOGIC: Materialized code should implement the fix derived from the diagnosis.
111
+ // For now, we provide a structured wrapper that logs intent and applies
112
+ // the suggested corrective action.
113
+
114
+ console.log(\`[EVOSKILL] Executing ${toolName} to resolve: ${proposal.problem}\`);
115
+
116
+ // Corrective action placeholder - in a full loop, this would be LLM-generated code
117
+ // derived from the 'how-to-avoid' fields in memory-log.jsonl.
118
+
119
+ return {
120
+ status: 'success',
121
+ appliedFix: \`Automated handling of ${proposal.problem} pattern.\`,
122
+ context: context
123
+ };
124
+ }
125
+
126
+ module.exports = { ${toolName} };
127
+ `;
128
+ }
129
+
130
+ if (require.main === module) {
131
+ materializeSkills();
132
+ }
133
+
134
+ module.exports = { materializeSkills };