thumbgate 0.9.10 → 0.9.12

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 (115) hide show
  1. package/.claude-plugin/README.md +2 -2
  2. package/.claude-plugin/marketplace.json +4 -2
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +115 -312
  6. package/adapters/README.md +1 -1
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/codex/config.toml +4 -4
  9. package/adapters/mcp/server-stdio.js +61 -1
  10. package/adapters/opencode/opencode.json +4 -2
  11. package/bin/cli.js +156 -8
  12. package/bin/memory.sh +3 -3
  13. package/config/e2e-critical-flows.json +4 -0
  14. package/config/gates/default.json +74 -2
  15. package/config/github-about.json +1 -1
  16. package/config/mcp-allowlists.json +27 -0
  17. package/package.json +22 -5
  18. package/plugins/amp-skill/INSTALL.md +1 -0
  19. package/plugins/amp-skill/SKILL.md +1 -0
  20. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  21. package/plugins/claude-codex-bridge/.mcp.json +4 -2
  22. package/plugins/claude-skill/INSTALL.md +1 -0
  23. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  24. package/plugins/codex-profile/.mcp.json +4 -2
  25. package/plugins/codex-profile/INSTALL.md +1 -1
  26. package/plugins/codex-profile/README.md +1 -1
  27. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  28. package/plugins/cursor-marketplace/README.md +3 -3
  29. package/plugins/cursor-marketplace/mcp.json +3 -1
  30. package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
  31. package/plugins/gemini-extension/INSTALL.md +3 -3
  32. package/plugins/opencode-profile/INSTALL.md +1 -1
  33. package/public/dashboard.html +15 -8
  34. package/public/index.html +125 -185
  35. package/public/js/buyer-intent.js +252 -0
  36. package/public/pro.html +1085 -0
  37. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  38. package/scripts/adk-consolidator.js +14 -2
  39. package/scripts/agent-readiness.js +3 -1
  40. package/scripts/agent-security-hardening.js +4 -4
  41. package/scripts/auto-promote-gates.js +2 -0
  42. package/scripts/auto-wire-hooks.js +105 -17
  43. package/scripts/behavioral-extraction.js +2 -6
  44. package/scripts/billing.js +107 -3
  45. package/scripts/budget-guard.js +2 -2
  46. package/scripts/build-metadata.js +14 -0
  47. package/scripts/context-engine.js +1 -0
  48. package/scripts/deploy-policy.js +3 -17
  49. package/scripts/dpo-optimizer.js +3 -6
  50. package/scripts/ensure-repo-bootstrap.js +129 -0
  51. package/scripts/export-dpo-pairs.js +2 -3
  52. package/scripts/export-kto-pairs.js +3 -4
  53. package/scripts/export-training.js +8 -6
  54. package/scripts/feedback-attribution.js +23 -11
  55. package/scripts/feedback-loop.js +40 -2
  56. package/scripts/feedback-to-rules.js +2 -1
  57. package/scripts/filesystem-search.js +3 -2
  58. package/scripts/gates-engine.js +760 -29
  59. package/scripts/generate-pretool-hook.sh +0 -0
  60. package/scripts/gtm-revenue-loop.js +20 -1
  61. package/scripts/hook-auto-capture.sh +8 -3
  62. package/scripts/hook-runtime.js +81 -0
  63. package/scripts/hook-stop-self-score.sh +3 -3
  64. package/scripts/hook-thumbgate-cache-updater.js +99 -38
  65. package/scripts/hosted-config.js +4 -16
  66. package/scripts/hybrid-feedback-context.js +54 -14
  67. package/scripts/install-mcp.js +13 -3
  68. package/scripts/intent-router.js +2 -2
  69. package/scripts/license.js +52 -14
  70. package/scripts/local-model-profile.js +3 -2
  71. package/scripts/mcp-config.js +62 -7
  72. package/scripts/meta-policy.js +4 -8
  73. package/scripts/money-watcher.js +166 -16
  74. package/scripts/obsidian-export.js +1 -0
  75. package/scripts/operational-integrity.js +480 -0
  76. package/scripts/post-everywhere.js +35 -12
  77. package/scripts/pr-manager.js +14 -11
  78. package/scripts/profile-router.js +2 -0
  79. package/scripts/prompt-dlp.js +1 -0
  80. package/scripts/publish-decision.js +10 -0
  81. package/scripts/published-cli.js +61 -0
  82. package/scripts/risk-scorer.js +3 -2
  83. package/scripts/rlhf_session_start.sh +32 -0
  84. package/scripts/skill-quality-tracker.js +3 -5
  85. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  86. package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
  87. package/scripts/social-analytics/engagement-audit.js +202 -0
  88. package/scripts/social-analytics/instagram-thumbgate-post.js +45 -7
  89. package/scripts/social-analytics/install-growth-automation.js +114 -0
  90. package/scripts/social-analytics/load-env.js +46 -0
  91. package/scripts/social-analytics/poll-all.js +23 -23
  92. package/scripts/social-analytics/pollers/plausible.js +2 -4
  93. package/scripts/social-analytics/pollers/zernio.js +3 -0
  94. package/scripts/social-analytics/publish-instagram-thumbgate.js +22 -3
  95. package/scripts/social-analytics/publish-thumbgate-launch.js +322 -0
  96. package/scripts/social-analytics/publishers/reddit.js +7 -12
  97. package/scripts/social-analytics/publishers/zernio.js +301 -22
  98. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
  99. package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
  100. package/scripts/social-analytics/sync-launch-assets.js +185 -0
  101. package/scripts/social-post-hourly.js +185 -0
  102. package/scripts/social-quality-gate.js +119 -3
  103. package/scripts/social-reply-monitor.js +184 -37
  104. package/scripts/statusline-cache-path.js +27 -0
  105. package/scripts/statusline-local-stats.js +16 -0
  106. package/scripts/statusline-meta.js +22 -0
  107. package/scripts/statusline.sh +40 -33
  108. package/scripts/sync-version.js +24 -3
  109. package/scripts/test-coverage.js +21 -13
  110. package/scripts/tool-registry.js +97 -0
  111. package/scripts/train_from_feedback.py +32 -9
  112. package/scripts/validate-feedback.js +3 -2
  113. package/scripts/vector-store.js +2 -3
  114. package/scripts/verify-obsidian-setup.sh +3 -3
  115. package/src/api/server.js +281 -33
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const REPO_ROOT = path.resolve(process.argv[2] || process.cwd());
8
+ const RLHF_ENTRY = {
9
+ command: 'npx',
10
+ args: ['-y', 'thumbgate@latest', 'serve'],
11
+ };
12
+ const LEGACY_SERVER_NAMES = ['thumbgate', 'rlhf_feedback_loop'];
13
+ const INFO_EXCLUDE_ENTRIES = ['.rlhf/', '.thumbgate/', '.mcp.json'];
14
+
15
+ function readJson(filePath) {
16
+ try {
17
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ function writeJsonIfChanged(filePath, value) {
24
+ const next = JSON.stringify(value, null, 2) + '\n';
25
+ let current = null;
26
+ try {
27
+ current = fs.readFileSync(filePath, 'utf8');
28
+ } catch {
29
+ current = null;
30
+ }
31
+ if (current === next) {
32
+ return false;
33
+ }
34
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
35
+ fs.writeFileSync(filePath, next);
36
+ return true;
37
+ }
38
+
39
+ function mergeRlhfEntry(entry = {}) {
40
+ return {
41
+ ...entry,
42
+ command: RLHF_ENTRY.command,
43
+ args: RLHF_ENTRY.args.slice(),
44
+ };
45
+ }
46
+
47
+ function ensureMcpJson(repoRoot) {
48
+ const filePath = path.join(repoRoot, '.mcp.json');
49
+ const existing = readJson(filePath);
50
+ const config = existing && typeof existing === 'object' ? existing : {};
51
+ config.mcpServers = config.mcpServers && typeof config.mcpServers === 'object' ? config.mcpServers : {};
52
+ config.mcpServers.rlhf = mergeRlhfEntry(config.mcpServers.rlhf);
53
+ for (const legacyName of LEGACY_SERVER_NAMES) {
54
+ delete config.mcpServers[legacyName];
55
+ }
56
+ return writeJsonIfChanged(filePath, config);
57
+ }
58
+
59
+ function ensureClaudeSettings(repoRoot) {
60
+ const filePath = path.join(repoRoot, '.claude', 'settings.json');
61
+ const existing = readJson(filePath);
62
+ if (!existing || typeof existing !== 'object') {
63
+ return false;
64
+ }
65
+ const hasRelevantServer =
66
+ Boolean(existing.mcpServers && existing.mcpServers.rlhf) ||
67
+ LEGACY_SERVER_NAMES.some((name) => Boolean(existing.mcpServers && existing.mcpServers[name]));
68
+ if (!hasRelevantServer) {
69
+ return false;
70
+ }
71
+ existing.mcpServers = existing.mcpServers && typeof existing.mcpServers === 'object' ? existing.mcpServers : {};
72
+ existing.mcpServers.rlhf = mergeRlhfEntry(existing.mcpServers.rlhf);
73
+ for (const legacyName of LEGACY_SERVER_NAMES) {
74
+ delete existing.mcpServers[legacyName];
75
+ }
76
+ return writeJsonIfChanged(filePath, existing);
77
+ }
78
+
79
+ function ensureInfoExclude(repoRoot) {
80
+ const excludePath = path.join(repoRoot, '.git', 'info', 'exclude');
81
+ let current = '';
82
+ try {
83
+ current = fs.readFileSync(excludePath, 'utf8');
84
+ } catch {
85
+ current = '';
86
+ }
87
+ const lines = new Set(
88
+ current
89
+ .split(/\r?\n/)
90
+ .map((line) => line.trim())
91
+ .filter(Boolean)
92
+ );
93
+ let changed = false;
94
+ for (const entry of INFO_EXCLUDE_ENTRIES) {
95
+ if (!lines.has(entry)) {
96
+ lines.add(entry);
97
+ changed = true;
98
+ }
99
+ }
100
+ if (!changed) {
101
+ return false;
102
+ }
103
+ const next = `${Array.from(lines).sort().join('\n')}\n`;
104
+ fs.mkdirSync(path.dirname(excludePath), { recursive: true });
105
+ fs.writeFileSync(excludePath, next);
106
+ return true;
107
+ }
108
+
109
+ function ensureRlhfDir(repoRoot) {
110
+ const rlhfDir = path.join(repoRoot, '.rlhf');
111
+ if (fs.existsSync(rlhfDir)) {
112
+ return false;
113
+ }
114
+ fs.mkdirSync(rlhfDir, { recursive: true });
115
+ return true;
116
+ }
117
+
118
+ function main() {
119
+ const results = {
120
+ repoRoot: REPO_ROOT,
121
+ createdRlhfDir: ensureRlhfDir(REPO_ROOT),
122
+ updatedMcpJson: ensureMcpJson(REPO_ROOT),
123
+ updatedClaudeSettings: ensureClaudeSettings(REPO_ROOT),
124
+ updatedInfoExclude: ensureInfoExclude(REPO_ROOT),
125
+ };
126
+ process.stdout.write(`${JSON.stringify(results)}\n`);
127
+ }
128
+
129
+ main();
@@ -8,10 +8,9 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const { traceForDpoPair, aggregateTraces } = require('./code-reasoning');
11
+ const { resolveFeedbackDir } = require('./feedback-paths');
11
12
 
12
- const PROJECT_ROOT = path.join(__dirname, '..');
13
- const FEEDBACK_DIR = process.env.THUMBGATE_FEEDBACK_DIR || path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
14
- const DEFAULT_LOCAL_MEMORY_LOG = path.join(FEEDBACK_DIR, 'memory-log.jsonl');
13
+ const DEFAULT_LOCAL_MEMORY_LOG = path.join(resolveFeedbackDir(), 'memory-log.jsonl');
15
14
 
16
15
  function readJSONL(filePath) {
17
16
  if (!fs.existsSync(filePath)) return [];
@@ -12,11 +12,10 @@
12
12
 
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
+ const { resolveFeedbackDir } = require('./feedback-paths');
15
16
 
16
- const PROJECT_ROOT = path.join(__dirname, '..');
17
- const FEEDBACK_DIR = process.env.THUMBGATE_FEEDBACK_DIR || path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
18
- const DEFAULT_FEEDBACK_LOG = path.join(FEEDBACK_DIR, 'feedback-log.jsonl');
19
- const DEFAULT_MEMORY_LOG = path.join(FEEDBACK_DIR, 'memory-log.jsonl');
17
+ const DEFAULT_FEEDBACK_LOG = path.join(resolveFeedbackDir(), 'feedback-log.jsonl');
18
+ const DEFAULT_MEMORY_LOG = path.join(resolveFeedbackDir(), 'memory-log.jsonl');
20
19
 
21
20
  function readJSONL(filePath) {
22
21
  if (!fs.existsSync(filePath)) return [];
@@ -16,12 +16,14 @@
16
16
 
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
+ const { resolveFeedbackDir } = require('./feedback-paths');
19
20
 
20
- const PROJECT_ROOT = path.join(__dirname, '..');
21
- const FEEDBACK_DIR = process.env.THUMBGATE_FEEDBACK_DIR
22
- || path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
23
21
  const SEQUENCE_WINDOW = 10;
24
22
 
23
+ function getFeedbackDir() {
24
+ return resolveFeedbackDir();
25
+ }
26
+
25
27
  // ---------------------------------------------------------------------------
26
28
  // Utility helpers
27
29
  // ---------------------------------------------------------------------------
@@ -173,7 +175,7 @@ function buildPreferencePairs(feedbackEntries) {
173
175
  * @returns {{ outputPath: string, pairCount: number, sequenceCount: number }}
174
176
  */
175
177
  function exportPyTorchJSON(feedbackDir, outputPath) {
176
- const fbDir = feedbackDir || FEEDBACK_DIR;
178
+ const fbDir = feedbackDir || getFeedbackDir();
177
179
  const feedbackPath = path.join(fbDir, 'feedback-log.jsonl');
178
180
  const sequencePath = path.join(fbDir, 'feedback-sequences.jsonl');
179
181
  const exportDir = path.join(fbDir, 'training-data');
@@ -246,7 +248,7 @@ function escapeCsvField(value) {
246
248
  * @returns {{ outputPath: string, rowCount: number }}
247
249
  */
248
250
  function exportCSV(feedbackDir, outputPath) {
249
- const fbDir = feedbackDir || FEEDBACK_DIR;
251
+ const fbDir = feedbackDir || getFeedbackDir();
250
252
  const feedbackPath = path.join(fbDir, 'feedback-log.jsonl');
251
253
  const exportDir = path.join(fbDir, 'training-data');
252
254
 
@@ -288,7 +290,7 @@ function exportCSV(feedbackDir, outputPath) {
288
290
  * @returns {{ outputPath: string, report: object }}
289
291
  */
290
292
  function exportActionAnalysis(feedbackDir, outputPath) {
291
- const fbDir = feedbackDir || FEEDBACK_DIR;
293
+ const fbDir = feedbackDir || getFeedbackDir();
292
294
  const feedbackPath = path.join(fbDir, 'feedback-log.jsonl');
293
295
  const sequencePath = path.join(fbDir, 'feedback-sequences.jsonl');
294
296
  const exportDir = path.join(fbDir, 'training-data');
@@ -3,14 +3,24 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const { resolveFeedbackDir } = require('./feedback-paths');
7
+
8
+ function getAttributionPaths(options = {}) {
9
+ const feedbackDir = resolveFeedbackDir({
10
+ cwd: options.cwd,
11
+ env: options.env,
12
+ feedbackDir: options.feedbackDir,
13
+ home: options.home,
14
+ });
15
+ return {
16
+ feedbackDir,
17
+ actionLog: path.join(feedbackDir, 'action-log.jsonl'),
18
+ attributions: path.join(feedbackDir, 'feedback-attributions.jsonl'),
19
+ attributedFeedback: path.join(feedbackDir, 'attributed-feedback.jsonl'),
20
+ };
21
+ }
6
22
 
7
- // ThumbGate repo: scripts/ is 1 level below repo root (not 2 like Subway .claude/scripts/feedback/)
8
- const ROOT = path.join(__dirname, '..');
9
- const PATHS = {
10
- actionLog: path.join(ROOT, '.claude', 'memory', 'feedback', 'action-log.jsonl'),
11
- attributions: path.join(ROOT, '.claude', 'memory', 'feedback', 'feedback-attributions.jsonl'),
12
- attributedFeedback: path.join(ROOT, '.claude', 'memory', 'feedback', 'attributed-feedback.jsonl'),
13
- };
23
+ const PATHS = getAttributionPaths();
14
24
 
15
25
  const STOPWORDS = new Set([
16
26
  'about', 'after', 'again', 'allow', 'already', 'always', 'because', 'before', 'being', 'between',
@@ -155,7 +165,7 @@ function scoreCandidate(action, feedbackNormalized, feedbackTokens, nowMs) {
155
165
  }
156
166
 
157
167
  function recordAction(toolName, toolInput, opts = {}) {
158
- const actionLogPath = opts.actionLogPath || process.env.THUMBGATE_ACTION_LOG || PATHS.actionLog;
168
+ const actionLogPath = opts.actionLogPath || process.env.THUMBGATE_ACTION_LOG || getAttributionPaths(opts).actionLog;
159
169
  const tool = String(toolName || 'unknown');
160
170
  const inputSummary = summarizeToolInput(tool, toolInput);
161
171
  const normalized = normalize(inputSummary);
@@ -175,9 +185,10 @@ function recordAction(toolName, toolInput, opts = {}) {
175
185
 
176
186
  function attributeFeedback(signal, feedbackContext, opts = {}) {
177
187
  const sig = String(signal || '').toLowerCase().trim();
178
- const actionLogPath = opts.actionLogPath || process.env.THUMBGATE_ACTION_LOG || PATHS.actionLog;
179
- const attributionsPath = opts.attributionsPath || process.env.THUMBGATE_FEEDBACK_ATTRIBUTIONS || PATHS.attributions;
180
- const attributedFeedbackPath = opts.attributedFeedbackPath || process.env.THUMBGATE_ATTRIBUTED_FEEDBACK || PATHS.attributedFeedback;
188
+ const paths = getAttributionPaths(opts);
189
+ const actionLogPath = opts.actionLogPath || process.env.THUMBGATE_ACTION_LOG || paths.actionLog;
190
+ const attributionsPath = opts.attributionsPath || process.env.THUMBGATE_FEEDBACK_ATTRIBUTIONS || paths.attributions;
191
+ const attributedFeedbackPath = opts.attributedFeedbackPath || process.env.THUMBGATE_ATTRIBUTED_FEEDBACK || paths.attributedFeedback;
181
192
 
182
193
  if (sig !== 'negative' && sig !== 'positive') {
183
194
  return { ok: true, skipped: true, reason: 'signal_not_supported' };
@@ -298,6 +309,7 @@ if (require.main === module) {
298
309
 
299
310
  module.exports = {
300
311
  PATHS,
312
+ getAttributionPaths,
301
313
  readJsonl,
302
314
  appendJsonl,
303
315
  normalize,
@@ -105,8 +105,7 @@ const pendingBackgroundSideEffects = new Set();
105
105
  */
106
106
  function updateStatuslineWithLesson({ accepted, signal, memoryId, feedbackId, lesson, turnCount }) {
107
107
  try {
108
- const cacheDir = process.env.THUMBGATE_FEEDBACK_DIR || HOME || '.';
109
- const cachePath = path.join(cacheDir, '.thumbgate', 'statusline_cache.json');
108
+ const cachePath = path.join(getFeedbackPaths().FEEDBACK_DIR, 'statusline_cache.json');
110
109
  let cache = {};
111
110
  try {
112
111
  cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
@@ -138,6 +137,12 @@ function updateStatuslineWithLesson({ accepted, signal, memoryId, feedbackId, le
138
137
  cache.updated_at = String(Math.floor(Date.now() / 1000));
139
138
  fs.mkdirSync(path.dirname(cachePath), { recursive: true });
140
139
  fs.writeFileSync(cachePath, JSON.stringify(cache));
140
+ try {
141
+ const { refreshStatuslineCache } = require('./hook-thumbgate-cache-updater');
142
+ refreshStatuslineCache(analyzeFeedback(), cachePath);
143
+ } catch {
144
+ /* keep lesson refresh best-effort */
145
+ }
141
146
  } catch { /* statusline update is best-effort */ }
142
147
  }
143
148
 
@@ -615,6 +620,35 @@ function enrichFeedbackContext(feedbackEvent, params) {
615
620
  ? params.filePaths.split(',').map((f) => f.trim()).filter(Boolean)
616
621
  : [];
617
622
  const errorType = params.errorType || null;
623
+ const protectedFiles = filePaths.filter((filePath) => /(^|\/)(agents\.md|claude(\.local)?\.md|gemini\.md|readme\.md|\.gitignore|skill\.md)$|^\.husky\/|^config\/gates\//i.test(filePath));
624
+ const combinedText = [
625
+ feedbackEvent.context || '',
626
+ feedbackEvent.whatWentWrong || '',
627
+ feedbackEvent.whatToChange || '',
628
+ ...(Array.isArray(feedbackEvent.tags) ? feedbackEvent.tags : []),
629
+ ].join(' ').toLowerCase();
630
+ const includesPhrase = (phrase) => combinedText.includes(phrase);
631
+ const includesOrderedTerms = (firstTerm, secondTerm) => {
632
+ const firstIndex = combinedText.indexOf(firstTerm);
633
+ if (firstIndex === -1) return false;
634
+ return combinedText.indexOf(secondTerm, firstIndex + firstTerm.length) !== -1;
635
+ };
636
+ const enforcement = {
637
+ scopeViolation: includesPhrase('scope creep')
638
+ || includesPhrase('out of scope')
639
+ || includesOrderedTerms('outside', 'scope')
640
+ || includesPhrase('wrong files')
641
+ || includesPhrase('unrelated files'),
642
+ approvalFailure: includesPhrase('without approval')
643
+ || includesPhrase('missing approval')
644
+ || includesPhrase('approval required')
645
+ || includesPhrase('permission required'),
646
+ protectedFileViolation: protectedFiles.length > 0
647
+ || includesPhrase('protected file')
648
+ || includesPhrase('policy file')
649
+ || includesPhrase('hook file'),
650
+ protectedFiles,
651
+ };
618
652
 
619
653
  return {
620
654
  ...feedbackEvent,
@@ -623,6 +657,7 @@ function enrichFeedbackContext(feedbackEvent, params) {
623
657
  filePaths,
624
658
  errorType,
625
659
  outcomeCategory,
660
+ enforcement,
626
661
  },
627
662
  };
628
663
  } catch (_err) {
@@ -812,6 +847,9 @@ function inferLessonFromConversation(conversationWindow, signal) {
812
847
  const tags = [];
813
848
  if (filePaths.length > 0) tags.push('has-file-context');
814
849
  if (errorPatterns.length > 0) tags.push('has-error-context');
850
+ if (filePaths.some((filePath) => /(^|\/)(agents\.md|claude(\.local)?\.md|gemini\.md|readme\.md|\.gitignore|skill\.md)$|^\.husky\/|^config\/gates\//i.test(filePath))) {
851
+ tags.push('protected-file-context');
852
+ }
815
853
 
816
854
  return {
817
855
  lesson,
@@ -3,8 +3,9 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { getAutoGatesPath } = require('./auto-promote-gates');
6
+ const { resolveFeedbackDir } = require('./feedback-paths');
6
7
 
7
- const DEFAULT_LOG = path.join(__dirname, '..', '.claude', 'memory', 'feedback', 'feedback-log.jsonl');
8
+ const DEFAULT_LOG = path.join(resolveFeedbackDir(), 'feedback-log.jsonl');
8
9
  const NEG = new Set(['negative', 'negative_strong', 'down', 'thumbs_down']);
9
10
  const POS = new Set(['positive', 'positive_strong', 'up', 'thumbs_up']);
10
11
 
@@ -19,9 +19,10 @@
19
19
  const fs = require('node:fs');
20
20
  const path = require('node:path');
21
21
  const crypto = require('node:crypto');
22
+ const { resolveFeedbackDir } = require('./feedback-paths');
22
23
 
23
24
  const PROJECT_ROOT = path.join(__dirname, '..');
24
- const DEFAULT_FEEDBACK_DIR = path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
25
+ const DEFAULT_FEEDBACK_DIR = resolveFeedbackDir();
25
26
  const DEFAULT_CONTEXTFS_DIR = path.join(DEFAULT_FEEDBACK_DIR, 'contextfs');
26
27
 
27
28
  // ---------------------------------------------------------------------------
@@ -29,7 +30,7 @@ const DEFAULT_CONTEXTFS_DIR = path.join(DEFAULT_FEEDBACK_DIR, 'contextfs');
29
30
  // ---------------------------------------------------------------------------
30
31
 
31
32
  function getFeedbackDir() {
32
- return process.env.THUMBGATE_FEEDBACK_DIR || DEFAULT_FEEDBACK_DIR;
33
+ return resolveFeedbackDir();
33
34
  }
34
35
 
35
36
  function getContextFsDir() {