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,713 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function ensureDir(dirPath) {
11
+ if (!fs.existsSync(dirPath)) {
12
+ fs.mkdirSync(dirPath, { recursive: true });
13
+ }
14
+ }
15
+
16
+ function readJSONL(filePath) {
17
+ if (!fs.existsSync(filePath)) return [];
18
+ const raw = fs.readFileSync(filePath, 'utf-8').trim();
19
+ if (!raw) return [];
20
+ return raw
21
+ .split('\n')
22
+ .map((line) => {
23
+ try {
24
+ return JSON.parse(line);
25
+ } catch {
26
+ return null;
27
+ }
28
+ })
29
+ .filter(Boolean);
30
+ }
31
+
32
+ function slugify(text, maxLen) {
33
+ maxLen = maxLen || 80;
34
+ return String(text || '')
35
+ .toLowerCase()
36
+ .replace(/[^a-z0-9]+/g, '-')
37
+ .replace(/^-+|-+$/g, '')
38
+ .slice(0, maxLen);
39
+ }
40
+
41
+ function extractDate(entry) {
42
+ const ts = entry.timestamp || entry.date || entry.createdAt || entry.promotedAt || '';
43
+ if (!ts) return 'unknown';
44
+ const d = new Date(ts);
45
+ if (!Number.isFinite(d.getTime())) return 'unknown';
46
+ return d.toISOString().slice(0, 10);
47
+ }
48
+
49
+ /**
50
+ * Escape a YAML value — wrap in quotes if it contains colons, special chars,
51
+ * or starts with a YAML-special character.
52
+ */
53
+ function yamlEscape(value) {
54
+ if (value === null || value === undefined) return '""';
55
+ const s = String(value);
56
+ if (s === '') return '""';
57
+ // Wrap in double quotes if it contains colons, #, [], {}, `, or starts with special chars
58
+ if (/[:\#\[\]\{\}`|>]/.test(s) || /^[&*!%@]/.test(s) || /^['"]/.test(s) || s.includes('\n')) {
59
+ return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
60
+ }
61
+ return s;
62
+ }
63
+
64
+ function yamlArray(arr) {
65
+ if (!arr || arr.length === 0) return '[]';
66
+ return '\n' + arr.map((item) => ' - ' + yamlEscape(item)).join('\n');
67
+ }
68
+
69
+ function buildFrontmatter(fields) {
70
+ const lines = ['---'];
71
+ for (const [key, value] of Object.entries(fields)) {
72
+ if (Array.isArray(value)) {
73
+ lines.push(key + ': ' + yamlArray(value));
74
+ } else {
75
+ lines.push(key + ': ' + yamlEscape(value));
76
+ }
77
+ }
78
+ lines.push('---');
79
+ return lines.join('\n');
80
+ }
81
+
82
+ function wikiLink(name) {
83
+ return '[[' + String(name || '') + ']]';
84
+ }
85
+
86
+ function tagWikiLinks(tags) {
87
+ if (!tags || !Array.isArray(tags) || tags.length === 0) return '';
88
+ return tags.map((t) => wikiLink(t)).join(', ');
89
+ }
90
+
91
+ function writeNote(filePath, frontmatter, body) {
92
+ ensureDir(path.dirname(filePath));
93
+ fs.writeFileSync(filePath, frontmatter + '\n\n' + body.trim() + '\n', 'utf-8');
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Export: Feedback Log
98
+ // ---------------------------------------------------------------------------
99
+
100
+ function exportFeedbackLog(feedbackDir, outputDir) {
101
+ const errors = [];
102
+ let exported = 0;
103
+ const logPath = path.join(feedbackDir, 'feedback-log.jsonl');
104
+ const entries = readJSONL(logPath);
105
+ const outDir = path.join(outputDir, 'Feedback');
106
+ ensureDir(outDir);
107
+
108
+ for (const entry of entries) {
109
+ try {
110
+ if (!entry || !entry.id) {
111
+ errors.push('Skipped feedback entry with missing id');
112
+ continue;
113
+ }
114
+ const date = extractDate(entry);
115
+ const slug = slugify(entry.context || entry.whatWentWrong || entry.whatWorked || entry.id);
116
+ const fileName = date + '-' + slug + '.md';
117
+
118
+ const signal = entry.signal === 'positive' || entry.signal === 'up' ? 'up' : 'down';
119
+ const tags = Array.isArray(entry.tags) ? entry.tags : [];
120
+ const category = entry.category || (tags[0] || 'uncategorized');
121
+ const actionType = entry.actionType || entry.action || 'feedback';
122
+
123
+ const fm = buildFrontmatter({
124
+ title: entry.context || entry.whatWentWrong || entry.whatWorked || entry.id,
125
+ date: date,
126
+ signal: signal,
127
+ category: category,
128
+ tags: tags,
129
+ actionType: actionType,
130
+ sourceFeedbackId: entry.id,
131
+ });
132
+
133
+ const bodyParts = ['# ' + (entry.context || entry.id)];
134
+ if (entry.whatWentWrong) {
135
+ bodyParts.push('\n## Context\n\n' + entry.whatWentWrong);
136
+ }
137
+ if (entry.whatWorked) {
138
+ bodyParts.push('\n## What Worked\n\n' + entry.whatWorked);
139
+ }
140
+ if (entry.correctiveAction || entry.whatToChange) {
141
+ bodyParts.push('\n## Corrective Action\n\n' + (entry.correctiveAction || entry.whatToChange));
142
+ }
143
+ if (tags.length > 0) {
144
+ bodyParts.push('\n## Tags\n\n' + tagWikiLinks(tags));
145
+ }
146
+ if (entry.toolName) {
147
+ bodyParts.push('\n## Tool\n\n`' + entry.toolName + '`');
148
+ }
149
+
150
+ writeNote(path.join(outDir, fileName), fm, bodyParts.join('\n'));
151
+ exported++;
152
+ } catch (err) {
153
+ errors.push('Feedback entry error: ' + (err.message || String(err)));
154
+ }
155
+ }
156
+
157
+ return { exported, errors };
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Export: Memory Log
162
+ // ---------------------------------------------------------------------------
163
+
164
+ function exportMemoryLog(feedbackDir, outputDir) {
165
+ const errors = [];
166
+ let exported = 0;
167
+ const logPath = path.join(feedbackDir, 'memory-log.jsonl');
168
+ const entries = readJSONL(logPath);
169
+ const outDir = path.join(outputDir, 'Memories');
170
+ ensureDir(outDir);
171
+
172
+ for (const entry of entries) {
173
+ try {
174
+ if (!entry || !entry.id) {
175
+ errors.push('Skipped memory entry with missing id');
176
+ continue;
177
+ }
178
+ const date = extractDate(entry);
179
+ const slug = slugify(entry.title || entry.content || entry.id);
180
+ const fileName = date + '-' + slug + '.md';
181
+
182
+ const tags = Array.isArray(entry.tags) ? entry.tags : [];
183
+ const category = entry.category || 'uncategorized';
184
+ const signal = entry.signal || (category === 'error' ? 'down' : 'up');
185
+
186
+ const fm = buildFrontmatter({
187
+ title: entry.title || entry.id,
188
+ date: date,
189
+ category: category,
190
+ tags: tags,
191
+ signal: signal,
192
+ });
193
+
194
+ const bodyParts = ['# ' + (entry.title || entry.id)];
195
+ if (entry.content) {
196
+ bodyParts.push('\n' + entry.content);
197
+ }
198
+ if (tags.length > 0) {
199
+ bodyParts.push('\n## Tags\n\n' + tagWikiLinks(tags));
200
+ }
201
+ if (entry.sourceFeedbackId) {
202
+ bodyParts.push('\n## Source\n\nBacklink: ' + wikiLink('Feedback/' + entry.sourceFeedbackId));
203
+ }
204
+
205
+ writeNote(path.join(outDir, fileName), fm, bodyParts.join('\n'));
206
+ exported++;
207
+ } catch (err) {
208
+ errors.push('Memory entry error: ' + (err.message || String(err)));
209
+ }
210
+ }
211
+
212
+ return { exported, errors };
213
+ }
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Export: Prevention Rules
217
+ // ---------------------------------------------------------------------------
218
+
219
+ function deriveSeverity(text) {
220
+ const s = String(text || '').toLowerCase();
221
+ if (/\bcritical\b|\bblock\b|\bblocked\b|\bnever\b|\bforce push\b/.test(s)) return 'critical';
222
+ if (/\bhigh\b|\bwarn\b|\bdanger\b/.test(s)) return 'high';
223
+ if (/\blow\b|\bminor\b/.test(s)) return 'low';
224
+ return 'medium';
225
+ }
226
+
227
+ function parsePreventionRulesMarkdown(content) {
228
+ const rules = [];
229
+ if (!content) return rules;
230
+
231
+ const lines = content.split('\n');
232
+ let current = null;
233
+
234
+ for (const line of lines) {
235
+ // Match rule headers like "## Rule: ...", "### ...", "## 1. ..." (skip top-level # headers)
236
+ const headerMatch = line.match(/^#{2,3}\s+(?:Rule:\s*)?(?:\d+\.\s*)?(.+)/);
237
+ if (headerMatch) {
238
+ if (current && current.title) {
239
+ // Derive severity from title + body before pushing
240
+ current.severity = deriveSeverity(current.title + ' ' + current.body);
241
+ rules.push(current);
242
+ }
243
+ current = {
244
+ title: headerMatch[1].trim(),
245
+ severity: 'medium',
246
+ source: 'prevention-rules.md',
247
+ body: '',
248
+ };
249
+ continue;
250
+ }
251
+ if (current) {
252
+ current.body += line + '\n';
253
+ }
254
+ }
255
+ if (current && current.title) {
256
+ current.severity = deriveSeverity(current.title + ' ' + current.body);
257
+ rules.push(current);
258
+ }
259
+
260
+ return rules;
261
+ }
262
+
263
+ function exportPreventionRules(feedbackDir, outputDir) {
264
+ const errors = [];
265
+ let exported = 0;
266
+ const rulesPath = path.join(feedbackDir, 'prevention-rules.md');
267
+ const outDir = path.join(outputDir, 'Rules');
268
+ ensureDir(outDir);
269
+
270
+ if (!fs.existsSync(rulesPath)) {
271
+ return { exported: 0, errors: [] };
272
+ }
273
+
274
+ let content;
275
+ try {
276
+ content = fs.readFileSync(rulesPath, 'utf-8');
277
+ } catch (err) {
278
+ return { exported: 0, errors: ['Failed to read prevention-rules.md: ' + err.message] };
279
+ }
280
+
281
+ const rules = parsePreventionRulesMarkdown(content);
282
+ const indexLinks = [];
283
+
284
+ for (const rule of rules) {
285
+ try {
286
+ const slug = slugify(rule.title);
287
+ if (!slug) {
288
+ errors.push('Skipped rule with empty title');
289
+ continue;
290
+ }
291
+ const fileName = slug + '.md';
292
+
293
+ const fm = buildFrontmatter({
294
+ title: rule.title,
295
+ type: 'prevention-rule',
296
+ severity: rule.severity,
297
+ source: rule.source,
298
+ });
299
+
300
+ const body = '# ' + rule.title + '\n\n' + (rule.body || '').trim();
301
+ writeNote(path.join(outDir, fileName), fm, body);
302
+ indexLinks.push('- ' + wikiLink('Rules/' + slug));
303
+ exported++;
304
+ } catch (err) {
305
+ errors.push('Rule export error: ' + (err.message || String(err)));
306
+ }
307
+ }
308
+
309
+ // Write index
310
+ if (indexLinks.length > 0) {
311
+ const indexFm = buildFrontmatter({
312
+ title: 'Prevention Rules Index',
313
+ type: 'index',
314
+ });
315
+ const indexBody = '# Prevention Rules Index\n\n' + indexLinks.join('\n');
316
+ writeNote(path.join(outDir, 'Prevention Rules Index.md'), indexFm, indexBody);
317
+ }
318
+
319
+ return { exported, errors };
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Export: Gates
324
+ // ---------------------------------------------------------------------------
325
+
326
+ function exportGates(configPath, outputDir) {
327
+ const errors = [];
328
+ let exported = 0;
329
+ const outDir = path.join(outputDir, 'Gates');
330
+ ensureDir(outDir);
331
+
332
+ let gates = [];
333
+
334
+ // Read default gates config
335
+ if (fs.existsSync(configPath)) {
336
+ try {
337
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
338
+ if (Array.isArray(raw.gates)) {
339
+ gates = gates.concat(raw.gates);
340
+ }
341
+ } catch (err) {
342
+ errors.push('Failed to read gates config: ' + err.message);
343
+ }
344
+ }
345
+
346
+ // Read auto-promoted gates if present (check common locations)
347
+ const autoGatePaths = [
348
+ path.join(path.dirname(configPath), '..', '.thumbgate', 'auto-promoted-gates.json'),
349
+ path.join(path.dirname(configPath), '..', '.rlhf', 'auto-promoted-gates.json'),
350
+ path.join(path.dirname(configPath), '..', '.claude', 'memory', 'feedback', 'auto-promoted-gates.json'),
351
+ ];
352
+ for (const agPath of autoGatePaths) {
353
+ if (fs.existsSync(agPath)) {
354
+ try {
355
+ const raw = JSON.parse(fs.readFileSync(agPath, 'utf-8'));
356
+ if (Array.isArray(raw.gates)) {
357
+ gates = gates.concat(raw.gates);
358
+ }
359
+ } catch (err) {
360
+ errors.push('Failed to read auto-promoted gates: ' + err.message);
361
+ }
362
+ }
363
+ }
364
+
365
+ const indexLinks = [];
366
+
367
+ for (const gate of gates) {
368
+ try {
369
+ if (!gate || !gate.id) {
370
+ errors.push('Skipped gate with missing id');
371
+ continue;
372
+ }
373
+ const fileName = gate.id + '.md';
374
+ const action = gate.action || 'warn';
375
+ const tool = gate.trigger || gate.tool || 'any';
376
+ const pattern = gate.pattern || '';
377
+
378
+ const fm = buildFrontmatter({
379
+ title: gate.id,
380
+ type: 'gate',
381
+ action: action,
382
+ tool: tool,
383
+ pattern: pattern,
384
+ severity: gate.severity || 'medium',
385
+ layer: gate.layer || 'unknown',
386
+ });
387
+
388
+ const bodyParts = ['# Gate: ' + gate.id];
389
+ if (gate.message) {
390
+ bodyParts.push('\n## Description\n\n' + gate.message);
391
+ }
392
+ bodyParts.push('\n## Match Conditions\n');
393
+ bodyParts.push('- **Pattern**: `' + pattern + '`');
394
+ bodyParts.push('- **Layer**: ' + (gate.layer || 'unknown'));
395
+ if (gate.unless) {
396
+ bodyParts.push('- **Unless**: `' + gate.unless + '`');
397
+ }
398
+ bodyParts.push('\n## Enforcement\n');
399
+ bodyParts.push('- **Action**: ' + action);
400
+ bodyParts.push('- **Severity**: ' + (gate.severity || 'medium'));
401
+
402
+ writeNote(path.join(outDir, fileName), fm, bodyParts.join('\n'));
403
+ indexLinks.push('- ' + wikiLink('Gates/' + gate.id));
404
+ exported++;
405
+ } catch (err) {
406
+ errors.push('Gate export error: ' + (err.message || String(err)));
407
+ }
408
+ }
409
+
410
+ // Write index
411
+ if (indexLinks.length > 0) {
412
+ const indexFm = buildFrontmatter({
413
+ title: 'Gates Index',
414
+ type: 'index',
415
+ });
416
+ const indexBody = '# Gates Index\n\n' + indexLinks.join('\n');
417
+ writeNote(path.join(outDir, 'Gates Index.md'), indexFm, indexBody);
418
+ }
419
+
420
+ return { exported, errors };
421
+ }
422
+
423
+ // ---------------------------------------------------------------------------
424
+ // Export: ContextFS Packs
425
+ // ---------------------------------------------------------------------------
426
+
427
+ function exportContextFsPacks(feedbackDir, outputDir) {
428
+ const errors = [];
429
+ let exported = 0;
430
+ const provDir = path.join(feedbackDir, 'contextfs', 'provenance');
431
+ const outDir = path.join(outputDir, 'Context Packs');
432
+ ensureDir(outDir);
433
+
434
+ if (!fs.existsSync(provDir)) {
435
+ return { exported: 0, errors: [] };
436
+ }
437
+
438
+ // Read packs.jsonl
439
+ const packsPath = path.join(provDir, 'packs.jsonl');
440
+ const packs = readJSONL(packsPath);
441
+
442
+ // Also try reading individual JSON files in provenance
443
+ let provFiles = [];
444
+ try {
445
+ provFiles = fs.readdirSync(provDir).filter((f) => f.endsWith('.json'));
446
+ } catch (_) { /* ignore */ }
447
+
448
+ for (const file of provFiles) {
449
+ try {
450
+ const raw = JSON.parse(fs.readFileSync(path.join(provDir, file), 'utf-8'));
451
+ if (raw && raw.packId && !packs.find((p) => p.packId === raw.packId)) {
452
+ packs.push(raw);
453
+ }
454
+ } catch (_) { /* skip malformed */ }
455
+ }
456
+
457
+ for (const pack of packs) {
458
+ try {
459
+ if (!pack || !pack.packId) {
460
+ errors.push('Skipped pack with missing packId');
461
+ continue;
462
+ }
463
+ const date = extractDate(pack);
464
+ const fileName = pack.packId + '.md';
465
+
466
+ const fm = buildFrontmatter({
467
+ packId: pack.packId,
468
+ date: date,
469
+ template: pack.template || 'unknown',
470
+ itemCount: pack.itemCount || (Array.isArray(pack.items) ? pack.items.length : 0),
471
+ usedChars: pack.usedChars || pack.charCount || 0,
472
+ });
473
+
474
+ const bodyParts = ['# Context Pack: ' + pack.packId];
475
+ if (pack.template) {
476
+ bodyParts.push('\n**Template**: ' + pack.template);
477
+ }
478
+ if (Array.isArray(pack.items) && pack.items.length > 0) {
479
+ bodyParts.push('\n## Items\n');
480
+ for (const item of pack.items) {
481
+ if (typeof item === 'string') {
482
+ bodyParts.push('- ' + item);
483
+ } else if (item && item.id) {
484
+ bodyParts.push('- ' + wikiLink(item.namespace ? item.namespace + '/' + item.id : item.id));
485
+ } else if (item && item.content) {
486
+ bodyParts.push('- ' + String(item.content).slice(0, 100));
487
+ }
488
+ }
489
+ }
490
+ if (pack.outcome) {
491
+ bodyParts.push('\n## Outcome\n\n' + String(pack.outcome));
492
+ }
493
+
494
+ writeNote(path.join(outDir, fileName), fm, bodyParts.join('\n'));
495
+ exported++;
496
+ } catch (err) {
497
+ errors.push('ContextFS pack error: ' + (err.message || String(err)));
498
+ }
499
+ }
500
+
501
+ return { exported, errors };
502
+ }
503
+
504
+ // ---------------------------------------------------------------------------
505
+ // Export: Promoted Lessons
506
+ // ---------------------------------------------------------------------------
507
+
508
+ function exportLessons(feedbackDir, outputDir) {
509
+ const errors = [];
510
+ let exported = 0;
511
+ const outDir = path.join(outputDir, 'Lessons');
512
+ ensureDir(outDir);
513
+
514
+ // Lessons come from feedback entries that were promoted (generated rules or have promoted flag)
515
+ const feedbackPath = path.join(feedbackDir, 'feedback-log.jsonl');
516
+ const memoryPath = path.join(feedbackDir, 'memory-log.jsonl');
517
+
518
+ const feedbackEntries = readJSONL(feedbackPath);
519
+ const memoryEntries = readJSONL(memoryPath);
520
+
521
+ // Promoted lessons: memories with category 'learning' or 'error' that have content,
522
+ // or feedback entries with promoted flag
523
+ const lessons = [];
524
+
525
+ for (const mem of memoryEntries) {
526
+ if (!mem || !mem.id) continue;
527
+ const isPromoted = mem.promoted === true ||
528
+ mem.category === 'learning' ||
529
+ (mem.category === 'error' && mem.content);
530
+ if (isPromoted) {
531
+ lessons.push(mem);
532
+ }
533
+ }
534
+
535
+ // Also check feedback for explicitly promoted entries
536
+ for (const fb of feedbackEntries) {
537
+ if (!fb || !fb.id) continue;
538
+ if (fb.promoted === true && !lessons.find((l) => l.sourceFeedbackId === fb.id || l.id === fb.id)) {
539
+ lessons.push({
540
+ id: fb.id,
541
+ title: fb.context || fb.whatWentWrong || fb.whatWorked || fb.id,
542
+ date: fb.timestamp,
543
+ timestamp: fb.timestamp,
544
+ category: fb.category || 'learning',
545
+ tags: fb.tags,
546
+ content: fb.correctiveAction || fb.whatToChange || fb.whatWorked || '',
547
+ promoted: true,
548
+ sourceFeedbackId: fb.id,
549
+ });
550
+ }
551
+ }
552
+
553
+ const indexLinks = [];
554
+
555
+ for (const lesson of lessons) {
556
+ try {
557
+ const date = extractDate(lesson);
558
+ const slug = slugify(lesson.title || lesson.content || lesson.id);
559
+ if (!slug) {
560
+ errors.push('Skipped lesson with empty slug');
561
+ continue;
562
+ }
563
+ const fileName = date + '-' + slug + '.md';
564
+
565
+ const tags = Array.isArray(lesson.tags) ? lesson.tags : [];
566
+ const linkedRules = Array.isArray(lesson.linkedRules) ? lesson.linkedRules : [];
567
+ const linkedGates = Array.isArray(lesson.linkedGates) ? lesson.linkedGates : [];
568
+
569
+ const fm = buildFrontmatter({
570
+ title: lesson.title || lesson.id,
571
+ date: date,
572
+ category: lesson.category || 'learning',
573
+ promoted: true,
574
+ linkedRules: linkedRules,
575
+ linkedGates: linkedGates,
576
+ });
577
+
578
+ const bodyParts = ['# ' + (lesson.title || lesson.id)];
579
+ if (lesson.content) {
580
+ bodyParts.push('\n## Corrective Action\n\n' + lesson.content);
581
+ }
582
+ if (lesson.lifecycleState || lesson.state) {
583
+ bodyParts.push('\n## Lifecycle State\n\n' + (lesson.lifecycleState || lesson.state));
584
+ }
585
+ if (linkedRules.length > 0) {
586
+ bodyParts.push('\n## Linked Rules\n\n' + linkedRules.map((r) => wikiLink('Rules/' + slugify(r))).join(', '));
587
+ }
588
+ if (linkedGates.length > 0) {
589
+ bodyParts.push('\n## Linked Gates\n\n' + linkedGates.map((g) => wikiLink('Gates/' + g)).join(', '));
590
+ }
591
+ if (tags.length > 0) {
592
+ bodyParts.push('\n## Tags\n\n' + tagWikiLinks(tags));
593
+ }
594
+ if (lesson.sourceFeedbackId) {
595
+ bodyParts.push('\n## Source\n\nBacklink: ' + wikiLink('Feedback/' + lesson.sourceFeedbackId));
596
+ }
597
+
598
+ writeNote(path.join(outDir, fileName), fm, bodyParts.join('\n'));
599
+ indexLinks.push('- ' + wikiLink('Lessons/' + date + '-' + slug));
600
+ exported++;
601
+ } catch (err) {
602
+ errors.push('Lesson export error: ' + (err.message || String(err)));
603
+ }
604
+ }
605
+
606
+ // Write index
607
+ if (indexLinks.length > 0) {
608
+ const indexFm = buildFrontmatter({
609
+ title: 'Lessons Index',
610
+ type: 'index',
611
+ });
612
+ const indexBody = '# Lessons Index\n\n' + indexLinks.join('\n');
613
+ writeNote(path.join(outDir, 'Lessons Index.md'), indexFm, indexBody);
614
+ }
615
+
616
+ return { exported, errors };
617
+ }
618
+
619
+ // ---------------------------------------------------------------------------
620
+ // Export All
621
+ // ---------------------------------------------------------------------------
622
+
623
+ function exportAll(options) {
624
+ const feedbackDir = options.feedbackDir;
625
+ const outputDir = options.outputDir;
626
+ const gatesConfigPath = options.gatesConfigPath || path.join(__dirname, '..', 'config', 'gates', 'default.json');
627
+ const includeIndex = options.includeIndex !== false;
628
+
629
+ ensureDir(outputDir);
630
+
631
+ const feedback = exportFeedbackLog(feedbackDir, outputDir);
632
+ const memories = exportMemoryLog(feedbackDir, outputDir);
633
+ const rules = exportPreventionRules(feedbackDir, outputDir);
634
+ const gates = exportGates(gatesConfigPath, outputDir);
635
+ const packs = exportContextFsPacks(feedbackDir, outputDir);
636
+ const lessons = exportLessons(feedbackDir, outputDir);
637
+
638
+ const allErrors = [].concat(
639
+ feedback.errors,
640
+ memories.errors,
641
+ rules.errors,
642
+ gates.errors,
643
+ packs.errors,
644
+ lessons.errors
645
+ );
646
+
647
+ const stats = {
648
+ feedback: feedback.exported,
649
+ memories: memories.exported,
650
+ rules: rules.exported,
651
+ gates: gates.exported,
652
+ packs: packs.exported,
653
+ lessons: lessons.exported,
654
+ errors: allErrors,
655
+ };
656
+
657
+ if (includeIndex) {
658
+ const indexFm = buildFrontmatter({
659
+ title: 'ThumbGate',
660
+ type: 'master-index',
661
+ exported: new Date().toISOString(),
662
+ });
663
+
664
+ const indexBody = [
665
+ '# ThumbGate',
666
+ '',
667
+ '## Export Summary',
668
+ '',
669
+ '| Type | Count |',
670
+ '|------|-------|',
671
+ '| Feedback | ' + stats.feedback + ' |',
672
+ '| Memories | ' + stats.memories + ' |',
673
+ '| Prevention Rules | ' + stats.rules + ' |',
674
+ '| Gates | ' + stats.gates + ' |',
675
+ '| Context Packs | ' + stats.packs + ' |',
676
+ '| Lessons | ' + stats.lessons + ' |',
677
+ '',
678
+ '## Indexes',
679
+ '',
680
+ '- ' + wikiLink('Rules/Prevention Rules Index'),
681
+ '- ' + wikiLink('Gates/Gates Index'),
682
+ '- ' + wikiLink('Lessons/Lessons Index'),
683
+ '',
684
+ '## Last Export',
685
+ '',
686
+ new Date().toISOString(),
687
+ ].join('\n');
688
+
689
+ writeNote(path.join(outputDir, 'ThumbGate.md'), indexFm, indexBody);
690
+ }
691
+
692
+ return stats;
693
+ }
694
+
695
+ // ---------------------------------------------------------------------------
696
+ // Module exports
697
+ // ---------------------------------------------------------------------------
698
+
699
+ module.exports = {
700
+ exportFeedbackLog,
701
+ exportMemoryLog,
702
+ exportPreventionRules,
703
+ exportGates,
704
+ exportContextFsPacks,
705
+ exportLessons,
706
+ exportAll,
707
+ // Internals exposed for testing
708
+ slugify,
709
+ yamlEscape,
710
+ buildFrontmatter,
711
+ wikiLink,
712
+ parsePreventionRulesMarkdown,
713
+ };