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,391 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { resolveFeedbackDir: resolveSharedFeedbackDir } = require('./feedback-paths');
6
+
7
+ const DEFAULT_HISTORY_LIMIT = 10;
8
+
9
+ const CORRECTION_PATTERNS = [
10
+ /\bdon['โ€™]?t\b/i,
11
+ /\bdo not\b/i,
12
+ /\bnever\b/i,
13
+ /\bavoid\b/i,
14
+ /\bstop\b/i,
15
+ /\bmust\b/i,
16
+ /\bshould\b/i,
17
+ /\bneed to\b/i,
18
+ /\buse\b/i,
19
+ /\brun tests?\b/i,
20
+ /\binclude\b/i,
21
+ /\bprove\b/i,
22
+ ];
23
+
24
+ const FAILURE_PATTERNS = [
25
+ /\bfailed\b/i,
26
+ /\bbroke\b/i,
27
+ /\berror\b/i,
28
+ /\bwrong\b/i,
29
+ /\bignored\b/i,
30
+ /\bskipped\b/i,
31
+ /\bhallucinat/i,
32
+ /\bclaimed done\b/i,
33
+ /\bwithout proof\b/i,
34
+ /\bwithout evidence\b/i,
35
+ ];
36
+
37
+ const SUCCESS_PATTERNS = [
38
+ /\bworked\b/i,
39
+ /\bpassed\b/i,
40
+ /\bverified\b/i,
41
+ /\bproof\b/i,
42
+ /\bevidence\b/i,
43
+ /\btests?\b/i,
44
+ /\bfixed\b/i,
45
+ /\bsuccess/i,
46
+ ];
47
+
48
+ function normalizeText(value) {
49
+ return String(value || '')
50
+ .replace(/\s+/g, ' ')
51
+ .trim();
52
+ }
53
+
54
+ function truncate(value, max = 180) {
55
+ const text = normalizeText(value);
56
+ if (text.length <= max) return text;
57
+ return `${text.slice(0, max - 1).trim()}โ€ฆ`;
58
+ }
59
+
60
+ function appendJsonl(filePath, record) {
61
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
62
+ fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`);
63
+ }
64
+
65
+ function readJsonlTail(filePath, limit = DEFAULT_HISTORY_LIMIT) {
66
+ if (!filePath || !fs.existsSync(filePath)) return [];
67
+ const lines = fs.readFileSync(filePath, 'utf8').split('\n');
68
+ const records = [];
69
+ for (let index = lines.length - 1; index >= 0 && records.length < limit; index -= 1) {
70
+ const line = lines[index].trim();
71
+ if (!line) continue;
72
+ try {
73
+ records.push(JSON.parse(line));
74
+ } catch {
75
+ // ignore malformed lines
76
+ }
77
+ }
78
+ return records.reverse();
79
+ }
80
+
81
+ function resolveFeedbackDir(feedbackDir) {
82
+ return resolveSharedFeedbackDir({ feedbackDir });
83
+ }
84
+
85
+ function getConversationPaths(feedbackDir) {
86
+ const resolved = resolveFeedbackDir(feedbackDir);
87
+ return {
88
+ feedbackDir: resolved,
89
+ conversationLogPath: path.join(resolved, 'conversation-window.jsonl'),
90
+ feedbackLogPath: path.join(resolved, 'feedback-log.jsonl'),
91
+ };
92
+ }
93
+
94
+ function normalizeChatHistory(entries = []) {
95
+ if (!Array.isArray(entries)) return [];
96
+ return entries
97
+ .map((entry) => {
98
+ if (typeof entry === 'string') {
99
+ const text = normalizeText(entry);
100
+ return text ? { author: null, text, timestamp: null, source: 'chat_history' } : null;
101
+ }
102
+ if (!entry || typeof entry !== 'object') return null;
103
+ const text = normalizeText(entry.text || entry.body || entry.message || entry.content);
104
+ if (!text) return null;
105
+ return {
106
+ author: normalizeText(entry.author || entry.role || entry.user || entry.name) || null,
107
+ text,
108
+ timestamp: normalizeText(entry.timestamp || entry.createdAt || entry.updatedAt) || null,
109
+ source: normalizeText(entry.source) || 'chat_history',
110
+ };
111
+ })
112
+ .filter(Boolean);
113
+ }
114
+
115
+ function recordConversationEntry(entry, options = {}) {
116
+ const { conversationLogPath } = getConversationPaths(options.feedbackDir);
117
+ const normalized = normalizeChatHistory([entry])[0];
118
+ if (!normalized) {
119
+ return { recorded: false, reason: 'empty_text', conversationLogPath };
120
+ }
121
+ const record = {
122
+ ...normalized,
123
+ timestamp: normalized.timestamp || new Date().toISOString(),
124
+ };
125
+ appendJsonl(conversationLogPath, record);
126
+ return { recorded: true, record, conversationLogPath };
127
+ }
128
+
129
+ function readRecentConversationWindow(options = {}) {
130
+ const limit = Number(options.limit || DEFAULT_HISTORY_LIMIT);
131
+ const { conversationLogPath } = getConversationPaths(options.feedbackDir);
132
+ return readJsonlTail(conversationLogPath, limit)
133
+ .map((entry) => normalizeChatHistory([entry])[0])
134
+ .filter(Boolean);
135
+ }
136
+
137
+ function findFeedbackEventById(feedbackId, options = {}) {
138
+ if (!feedbackId) return null;
139
+ const { feedbackLogPath } = getConversationPaths(options.feedbackDir);
140
+ const matches = readJsonlTail(feedbackLogPath, Number(options.searchLimit || 200));
141
+ for (let index = matches.length - 1; index >= 0; index -= 1) {
142
+ if (matches[index] && matches[index].id === feedbackId) {
143
+ return matches[index];
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+
149
+ function buildLastActionMessage(lastAction) {
150
+ if (!lastAction || typeof lastAction !== 'object') return null;
151
+ const tool = normalizeText(lastAction.tool || lastAction.tool_name || 'unknown tool');
152
+ const file = normalizeText(lastAction.file || lastAction.path || '');
153
+ const detail = file ? `${tool} on ${file}` : tool;
154
+ return {
155
+ author: 'tool',
156
+ text: `Last action: ${detail}`,
157
+ timestamp: normalizeText(lastAction.timestamp) || null,
158
+ source: 'last_action',
159
+ };
160
+ }
161
+
162
+ function buildRelatedFeedbackMessages(feedbackEvent) {
163
+ if (!feedbackEvent || typeof feedbackEvent !== 'object') return [];
164
+ const messages = [];
165
+
166
+ if (feedbackEvent.conversationWindow && Array.isArray(feedbackEvent.conversationWindow)) {
167
+ for (const entry of normalizeChatHistory(feedbackEvent.conversationWindow)) {
168
+ messages.push({ ...entry, source: 'related_feedback_window' });
169
+ }
170
+ }
171
+
172
+ const snippets = [
173
+ feedbackEvent.submittedContext || null,
174
+ feedbackEvent.context || null,
175
+ feedbackEvent.whatWentWrong || null,
176
+ feedbackEvent.whatWorked || null,
177
+ feedbackEvent.whatToChange || null,
178
+ ].filter(Boolean);
179
+
180
+ for (const text of snippets) {
181
+ messages.push({
182
+ author: 'related-feedback',
183
+ text: normalizeText(text),
184
+ timestamp: normalizeText(feedbackEvent.timestamp) || null,
185
+ source: 'related_feedback',
186
+ });
187
+ }
188
+
189
+ const lastActionMessage = buildLastActionMessage(feedbackEvent.lastAction);
190
+ if (lastActionMessage) messages.push(lastActionMessage);
191
+
192
+ return messages;
193
+ }
194
+
195
+ function matchesAny(text, patterns) {
196
+ return patterns.some((pattern) => pattern.test(text));
197
+ }
198
+
199
+ function chooseMessage(messages, predicate) {
200
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
201
+ if (predicate(messages[index])) return messages[index];
202
+ }
203
+ return null;
204
+ }
205
+
206
+ function buildRuleSuggestion(correctionMessage, signal) {
207
+ if (!correctionMessage) return null;
208
+ const text = truncate(correctionMessage.text, 120);
209
+ if (signal === 'negative' && /\bnever\b/i.test(text)) return text;
210
+ if (signal === 'negative' && /\bdo not\b/i.test(text)) {
211
+ return text.replace(/\bdo not\b/i, 'Never');
212
+ }
213
+ if (signal === 'negative' && /\bdon['โ€™]?t\b/i.test(text)) {
214
+ return text
215
+ .replace(/\bdon['โ€™]?t\b/i, 'Never')
216
+ .replace(/\bdo not\b/i, 'Never');
217
+ }
218
+ if (signal === 'negative' && /\bavoid\b/i.test(text)) {
219
+ return text.replace(/\bavoid\b/i, 'Avoid');
220
+ }
221
+ if (signal === 'negative') return `Follow the earlier instruction: ${text}`;
222
+ return `Repeat the successful pattern: ${text}`;
223
+ }
224
+
225
+ function inferNegativeDistillation(messages, params) {
226
+ const correctionMessage = chooseMessage(messages, (entry) => {
227
+ const text = normalizeText(entry.text);
228
+ return Boolean(text) && matchesAny(text, CORRECTION_PATTERNS);
229
+ });
230
+
231
+ const failureMessage = chooseMessage(messages, (entry) => {
232
+ const text = normalizeText(entry.text);
233
+ return Boolean(text) && matchesAny(text, FAILURE_PATTERNS);
234
+ }) || buildLastActionMessage(params.lastAction);
235
+
236
+ if (!correctionMessage && !failureMessage) {
237
+ return {
238
+ usedHistory: false,
239
+ inferredFields: {},
240
+ lessonProposal: null,
241
+ evidence: [],
242
+ source: 'none',
243
+ };
244
+ }
245
+
246
+ const evidence = [correctionMessage, failureMessage].filter(Boolean).map((entry) => truncate(entry.text));
247
+ const whatWentWrong = correctionMessage
248
+ ? `It ignored a prior instruction: ${truncate(correctionMessage.text, 140)}`
249
+ : `The failure centered on: ${truncate(failureMessage.text, 140)}`;
250
+ const whatToChange = correctionMessage
251
+ ? `Follow the earlier instruction instead of repeating the same pattern: ${truncate(correctionMessage.text, 140)}`
252
+ : failureMessage
253
+ ? `Inspect and correct the failing step before repeating it: ${truncate(failureMessage.text, 140)}`
254
+ : null;
255
+ const context = failureMessage
256
+ ? `History-aware distillation linked this failure to ${truncate(failureMessage.text, 140)}`
257
+ : `History-aware distillation linked this failure to ${truncate(correctionMessage.text, 140)}`;
258
+
259
+ return {
260
+ usedHistory: true,
261
+ source: correctionMessage ? correctionMessage.source : failureMessage.source,
262
+ inferredFields: {
263
+ context,
264
+ whatWentWrong,
265
+ whatToChange,
266
+ },
267
+ lessonProposal: {
268
+ summary: whatWentWrong,
269
+ proposedRule: buildRuleSuggestion(correctionMessage, 'negative'),
270
+ confidence: correctionMessage && failureMessage ? 0.92 : 0.78,
271
+ },
272
+ evidence,
273
+ };
274
+ }
275
+
276
+ function inferPositiveDistillation(messages) {
277
+ const successMessage = chooseMessage(messages, (entry) => {
278
+ const text = normalizeText(entry.text);
279
+ return Boolean(text) && matchesAny(text, SUCCESS_PATTERNS);
280
+ });
281
+
282
+ if (!successMessage) {
283
+ return {
284
+ usedHistory: false,
285
+ inferredFields: {},
286
+ lessonProposal: null,
287
+ evidence: [],
288
+ source: 'none',
289
+ };
290
+ }
291
+
292
+ const whatWorked = `The successful pattern was: ${truncate(successMessage.text, 140)}`;
293
+ return {
294
+ usedHistory: true,
295
+ source: successMessage.source,
296
+ inferredFields: {
297
+ context: `History-aware distillation found a successful pattern in recent conversation: ${truncate(successMessage.text, 140)}`,
298
+ whatWorked,
299
+ },
300
+ lessonProposal: {
301
+ summary: whatWorked,
302
+ proposedRule: buildRuleSuggestion(successMessage, 'positive'),
303
+ confidence: 0.81,
304
+ },
305
+ evidence: [truncate(successMessage.text)],
306
+ };
307
+ }
308
+
309
+ function distillFeedbackHistory(params = {}) {
310
+ const signal = String(params.signal || '').toLowerCase().trim();
311
+ const historyLimit = Number(params.historyLimit || DEFAULT_HISTORY_LIMIT);
312
+ const explicitHistory = normalizeChatHistory(params.chatHistory || params.messages || []);
313
+ const fallbackHistory = params.allowLocalConversationFallback
314
+ ? readRecentConversationWindow({ feedbackDir: params.feedbackDir, limit: historyLimit })
315
+ : [];
316
+ const relatedEvent = findFeedbackEventById(params.relatedFeedbackId, {
317
+ feedbackDir: params.feedbackDir,
318
+ searchLimit: params.searchLimit,
319
+ });
320
+
321
+ const conversationWindow = [
322
+ ...explicitHistory,
323
+ ...(explicitHistory.length === 0 ? fallbackHistory : []),
324
+ ...buildRelatedFeedbackMessages(relatedEvent),
325
+ ]
326
+ .filter((entry) => entry && normalizeText(entry.text))
327
+ .slice(-historyLimit);
328
+
329
+ const distillation = signal === 'negative'
330
+ ? inferNegativeDistillation(conversationWindow, params)
331
+ : inferPositiveDistillation(conversationWindow, params);
332
+
333
+ return {
334
+ usedHistory: distillation.usedHistory,
335
+ source: explicitHistory.length > 0
336
+ ? 'chat_history'
337
+ : fallbackHistory.length > 0
338
+ ? 'local_conversation_window'
339
+ : relatedEvent ? 'related_feedback' : 'none',
340
+ conversationWindow,
341
+ relatedFeedbackId: relatedEvent ? relatedEvent.id : null,
342
+ inferredFields: distillation.inferredFields,
343
+ lessonProposal: distillation.lessonProposal,
344
+ evidence: distillation.evidence,
345
+ };
346
+ }
347
+
348
+ function main(argv = process.argv.slice(2)) {
349
+ const [command, ...rest] = argv;
350
+ if (command !== 'record') {
351
+ console.error('Usage: node scripts/feedback-history-distiller.js record --author=user --text="..."');
352
+ process.exit(1);
353
+ }
354
+
355
+ const args = {};
356
+ for (const token of rest) {
357
+ if (!token.startsWith('--')) continue;
358
+ const [key, ...valueParts] = token.slice(2).split('=');
359
+ args[key] = valueParts.join('=');
360
+ }
361
+
362
+ const result = recordConversationEntry({
363
+ author: args.author || null,
364
+ text: args.text || '',
365
+ timestamp: args.timestamp || new Date().toISOString(),
366
+ source: args.source || 'cli_record',
367
+ }, {
368
+ feedbackDir: args.feedbackDir,
369
+ });
370
+
371
+ if (!result.recorded) {
372
+ console.error(result.reason || 'failed_to_record');
373
+ process.exit(1);
374
+ }
375
+
376
+ process.stdout.write(JSON.stringify(result, null, 2));
377
+ }
378
+
379
+ if (require.main === module) {
380
+ main();
381
+ }
382
+
383
+ module.exports = {
384
+ DEFAULT_HISTORY_LIMIT,
385
+ distillFeedbackHistory,
386
+ findFeedbackEventById,
387
+ getConversationPaths,
388
+ normalizeChatHistory,
389
+ readRecentConversationWindow,
390
+ recordConversationEntry,
391
+ };
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Feedback Inbox Reader
4
+ *
5
+ * Reads new feedback entries from the inbox JSONL file, using a cursor
6
+ * to avoid reprocessing. External systems (Phoenix bridge, other agents)
7
+ * append feedback signals to the inbox; Amp's reflexion-preflight skill
8
+ * calls this script each turn to ingest new signals.
9
+ *
10
+ * Usage:
11
+ * node scripts/feedback-inbox-read.js # output new entries as JSON array
12
+ * node scripts/feedback-inbox-read.js --peek # show count without advancing cursor
13
+ * node scripts/feedback-inbox-read.js --reset # reset cursor to re-read all
14
+ * node scripts/feedback-inbox-read.js --test # run built-in tests
15
+ *
16
+ * Inbox: .claude/feedback-loop/inbox.jsonl
17
+ * Cursor: .claude/feedback-loop/inbox.cursor.json
18
+ */
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const PROJECT_ROOT = path.join(__dirname, '..');
25
+ const INBOX_PATH = path.join(PROJECT_ROOT, '.claude', 'feedback-loop', 'inbox.jsonl');
26
+ const CURSOR_PATH = path.join(PROJECT_ROOT, '.claude', 'feedback-loop', 'inbox.cursor.json');
27
+
28
+ function readInbox() {
29
+ if (!fs.existsSync(INBOX_PATH)) return [];
30
+ const raw = fs.readFileSync(INBOX_PATH, 'utf-8').trim();
31
+ if (!raw) return [];
32
+ return raw.split('\n').map((line, idx) => {
33
+ try {
34
+ return { _lineIndex: idx, ...JSON.parse(line) };
35
+ } catch {
36
+ return null;
37
+ }
38
+ }).filter(Boolean);
39
+ }
40
+
41
+ function loadCursor() {
42
+ if (!fs.existsSync(CURSOR_PATH)) return { lastLineIndex: -1 };
43
+ try {
44
+ return JSON.parse(fs.readFileSync(CURSOR_PATH, 'utf-8'));
45
+ } catch {
46
+ return { lastLineIndex: -1 };
47
+ }
48
+ }
49
+
50
+ function saveCursor(cursor) {
51
+ const dir = path.dirname(CURSOR_PATH);
52
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
53
+ fs.writeFileSync(CURSOR_PATH, JSON.stringify(cursor, null, 2) + '\n');
54
+ }
55
+
56
+ function getNewEntries(advance) {
57
+ const entries = readInbox();
58
+ if (entries.length === 0) return [];
59
+
60
+ const cursor = loadCursor();
61
+ const newEntries = entries.filter((e) => e._lineIndex > cursor.lastLineIndex);
62
+
63
+ if (advance && newEntries.length > 0) {
64
+ const maxIdx = Math.max(...newEntries.map((e) => e._lineIndex));
65
+ saveCursor({ lastLineIndex: maxIdx, updatedAt: new Date().toISOString() });
66
+ }
67
+
68
+ // Strip internal _lineIndex before returning
69
+ return newEntries.map(({ _lineIndex, ...rest }) => rest);
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // CLI
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function runTests() {
77
+ const os = require('os');
78
+ let passed = 0;
79
+ let failed = 0;
80
+
81
+ function assert(condition, name) {
82
+ if (condition) { passed++; console.log(` โœ… ${name}`); }
83
+ else { failed++; console.log(` โŒ ${name}`); }
84
+ }
85
+
86
+ console.log('\n๐Ÿงช feedback-inbox-read.js โ€” Tests\n');
87
+
88
+ // Setup temp inbox
89
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'inbox-test-'));
90
+ const tmpInbox = path.join(tmpDir, 'inbox.jsonl');
91
+ const tmpCursor = path.join(tmpDir, 'inbox.cursor.json');
92
+
93
+ // Write test entries
94
+ const entries = [
95
+ { signal: 'negative', context: 'Bad thing happened', tags: ['testing'] },
96
+ { signal: 'positive', context: 'Good thing happened', tags: ['testing'] },
97
+ { signal: 'negative', context: 'Another bad thing', tags: ['thumbgate'] },
98
+ ];
99
+ fs.writeFileSync(tmpInbox, entries.map((e) => JSON.stringify(e)).join('\n') + '\n');
100
+
101
+ // Test reading all (no cursor)
102
+ const allEntries = (() => {
103
+ const raw = fs.readFileSync(tmpInbox, 'utf-8').trim();
104
+ return raw.split('\n').map((line, idx) => {
105
+ try { return { _lineIndex: idx, ...JSON.parse(line) }; }
106
+ catch { return null; }
107
+ }).filter(Boolean);
108
+ })();
109
+ assert(allEntries.length === 3, 'reads all 3 entries from inbox');
110
+
111
+ // Test cursor-based filtering
112
+ fs.writeFileSync(tmpCursor, JSON.stringify({ lastLineIndex: 0 }));
113
+ const afterFirst = allEntries.filter((e) => e._lineIndex > 0);
114
+ assert(afterFirst.length === 2, 'cursor at 0 โ†’ 2 new entries');
115
+
116
+ fs.writeFileSync(tmpCursor, JSON.stringify({ lastLineIndex: 2 }));
117
+ const afterAll = allEntries.filter((e) => e._lineIndex > 2);
118
+ assert(afterAll.length === 0, 'cursor at 2 โ†’ 0 new entries');
119
+
120
+ // Test loadCursor with missing file
121
+ const missingCursor = path.join(tmpDir, 'missing.cursor.json');
122
+ const fakeMod = { loadCursor: () => {
123
+ if (!fs.existsSync(missingCursor)) return { lastLineIndex: -1 };
124
+ try { return JSON.parse(fs.readFileSync(missingCursor, 'utf-8')); } catch { return { lastLineIndex: -1 }; }
125
+ }};
126
+ assert(fakeMod.loadCursor().lastLineIndex === -1, 'loadCursor returns -1 for missing file');
127
+
128
+ // Test saveCursor creates directory
129
+ const deepDir = path.join(tmpDir, 'deep', 'nested');
130
+ const deepCursor = path.join(deepDir, 'cursor.json');
131
+ if (!fs.existsSync(deepDir)) fs.mkdirSync(deepDir, { recursive: true });
132
+ fs.writeFileSync(deepCursor, JSON.stringify({ lastLineIndex: 5, updatedAt: new Date().toISOString() }) + '\n');
133
+ const loaded = JSON.parse(fs.readFileSync(deepCursor, 'utf-8'));
134
+ assert(loaded.lastLineIndex === 5, 'saveCursor persists cursor value');
135
+
136
+ // Cleanup
137
+ fs.rmSync(tmpDir, { recursive: true, force: true });
138
+
139
+ console.log(`\n${'โ•'.repeat(50)}`);
140
+ console.log(`Results: ${passed} passed, ${failed} failed, ${passed + failed} total`);
141
+ console.log(`${'โ•'.repeat(50)}\n`);
142
+ process.exit(failed > 0 ? 1 : 0);
143
+ }
144
+
145
+ if (require.main === module) {
146
+ if (process.argv.includes('--test')) {
147
+ runTests();
148
+ } else if (process.argv.includes('--reset')) {
149
+ if (fs.existsSync(CURSOR_PATH)) fs.unlinkSync(CURSOR_PATH);
150
+ console.log('Cursor reset.');
151
+ } else {
152
+ const peek = process.argv.includes('--peek');
153
+ const entries = getNewEntries(!peek);
154
+ if (entries.length === 0) {
155
+ // Silent โ€” no new entries
156
+ process.exit(0);
157
+ }
158
+ process.stdout.write(JSON.stringify(entries, null, 2) + '\n');
159
+ }
160
+ }
161
+
162
+ module.exports = { getNewEntries, readInbox, loadCursor, saveCursor, INBOX_PATH, CURSOR_PATH };