thumbgate 0.9.10 → 0.9.11

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 (113) 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 +89 -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 -0
  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 +68 -6
  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 +7 -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 +34 -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 +3 -18
  92. package/scripts/social-analytics/pollers/zernio.js +3 -0
  93. package/scripts/social-analytics/publish-instagram-thumbgate.js +22 -3
  94. package/scripts/social-analytics/publish-thumbgate-launch.js +316 -0
  95. package/scripts/social-analytics/publishers/reddit.js +7 -12
  96. package/scripts/social-analytics/publishers/zernio.js +210 -22
  97. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
  98. package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
  99. package/scripts/social-analytics/sync-launch-assets.js +185 -0
  100. package/scripts/social-post-hourly.js +185 -0
  101. package/scripts/social-quality-gate.js +119 -3
  102. package/scripts/social-reply-monitor.js +148 -32
  103. package/scripts/statusline-cache-path.js +27 -0
  104. package/scripts/statusline-meta.js +22 -0
  105. package/scripts/statusline.sh +24 -32
  106. package/scripts/sync-version.js +11 -3
  107. package/scripts/test-coverage.js +20 -13
  108. package/scripts/tool-registry.js +97 -0
  109. package/scripts/train_from_feedback.py +32 -9
  110. package/scripts/validate-feedback.js +3 -2
  111. package/scripts/vector-store.js +2 -3
  112. package/scripts/verify-obsidian-setup.sh +3 -3
  113. package/src/api/server.js +281 -33
@@ -1,7 +1,6 @@
1
1
  #!/bin/bash
2
2
  # ThumbGate Status Line for Claude Code
3
- # Shows ThumbGate feedback stats + most recent lesson at a glance.
4
- # Thumbs icons trigger CLI feedback capture inline (no browser).
3
+ # Shows ThumbGate feedback stats + package version/tier at a glance.
5
4
  # Installed by: npx thumbgate init --agent claude-code
6
5
 
7
6
  # Resolve script directory safely (CodeQL: no uncontrolled paths)
@@ -17,16 +16,21 @@ CTX_PCT="${CTX_PCT:-0}"
17
16
 
18
17
  # ── ThumbGate stats from cache ────────────────────────────────────────
19
18
  THUMBGATE_CACHE=""
20
- for base in "${THUMBGATE_FEEDBACK_DIR:-.}" "." "${HOME}"; do
21
- for rel in ".thumbgate/statusline_cache.json"; do
22
- if [ -f "${base}/${rel}" ]; then
23
- THUMBGATE_CACHE="${base}/${rel}"
24
- break 2
19
+ _CACHE_CANDIDATES_JSON=$(node "${SCRIPT_DIR}/statusline-cache-path.js" 2>/dev/null)
20
+ if [ -n "$_CACHE_CANDIDATES_JSON" ]; then
21
+ while IFS= read -r candidate; do
22
+ [ -z "$candidate" ] && continue
23
+ if [ -f "$candidate" ]; then
24
+ THUMBGATE_CACHE="$candidate"
25
+ break
25
26
  fi
26
- done
27
- done
27
+ done < <(echo "$_CACHE_CANDIDATES_JSON" | jq -r '.candidates[]?' 2>/dev/null)
28
+ fi
29
+ if [ -z "$THUMBGATE_CACHE" ]; then
30
+ THUMBGATE_CACHE="$(echo "$_CACHE_CANDIDATES_JSON" | jq -r '.candidates[0] // empty' 2>/dev/null)"
31
+ fi
28
32
  if [ -z "$THUMBGATE_CACHE" ]; then
29
- THUMBGATE_CACHE="${THUMBGATE_FEEDBACK_DIR:-.}/.thumbgate/statusline_cache.json"
33
+ THUMBGATE_CACHE="${THUMBGATE_FEEDBACK_DIR:-.}/statusline_cache.json"
30
34
  fi
31
35
 
32
36
  UP="0"; DOWN="0"; LESSONS="0"; TREND="?"; CACHE_TS="0"
@@ -59,13 +63,13 @@ except:pass
59
63
  disown 2>/dev/null
60
64
  fi
61
65
 
62
- # ── Most recent lesson from lesson-inference ──────────────────────
63
- LESSON_TEXT=""; LESSON_ID=""
64
- _LESSON_JSON=$(node "${SCRIPT_DIR}/statusline-lesson.js" 2>/dev/null)
65
- if [ -n "$_LESSON_JSON" ]; then
66
- eval "$(echo "$_LESSON_JSON" | jq -r '
67
- @sh "LESSON_TEXT=\(.text // "")",
68
- @sh "LESSON_ID=\(.lessonId // "")"
66
+ # ── ThumbGate package metadata ────────────────────────────────────────
67
+ TG_VERSION="unknown"; TG_TIER="Free"
68
+ _META_JSON=$(node "${SCRIPT_DIR}/statusline-meta.js" 2>/dev/null)
69
+ if [ -n "$_META_JSON" ]; then
70
+ eval "$(echo "$_META_JSON" | jq -r '
71
+ @sh "TG_VERSION=\(.version // "unknown")",
72
+ @sh "TG_TIER=\(.tier // "Free")"
69
73
  ' 2>/dev/null)"
70
74
  fi
71
75
 
@@ -88,29 +92,17 @@ case "${TREND}" in
88
92
  improving) ARROW="↗" ;; degrading) ARROW="↘" ;; stable) ARROW="→" ;; *) ARROW="?" ;;
89
93
  esac
90
94
 
91
- # ── OSC 8 clickable links ────────────────────────────────────────
92
- # Links use CLI commands instead of browser URLs.
93
- # Clicking 👍 runs: node bin/cli.js feedback --signal=up
94
- # Clicking 👎 runs: node bin/cli.js feedback --signal=down
95
- osc_link() { printf '\033]8;;%s\a%s\033]8;;\a' "$1" "$2"; }
96
- CLI="node ${SCRIPT_DIR}/../bin/cli.js"
97
-
98
95
  # ── Output (single line) ─────────────────────────────────────────
96
+ LINE="ThumbGate v${TG_VERSION} · ${TG_TIER}"
99
97
  if [ "$UP" = "0" ] && [ "$DOWN" = "0" ]; then
100
- echo -e "${D}ThumbGate: no feedback yet — type 'thumbs up' or 'thumbs down'${RST}"
98
+ echo -e "${D}${LINE} · no feedback yet${RST}"
101
99
  else
102
- # Feedback counts
103
- LINE="ThumbGate: ${G}${BD}${UP}${RST}👍 ${R}${BD}${DOWN}${RST}👎 · ${M}${BD}${LESSONS}${RST} lessons ${ARROW}"
100
+ LINE="${LINE} · ${G}${BD}${UP}${RST}👍 ${R}${BD}${DOWN}${RST}👎 ${ARROW}"
104
101
 
105
102
  # Control Tower alerts (if any)
106
103
  [ "${SLO_V:-0}" -gt 0 ] && LINE="${LINE} ${R}${SLO_V} SLO${RST}"
107
104
  [ "${AT_RISK:-0}" -gt 0 ] && LINE="${LINE} ${R}${AT_RISK}⚠${RST}"
108
105
  [ "${ANOMALIES:-0}" -gt 0 ] && LINE="${LINE} ${R}${ANOMALIES}☠${RST}"
109
106
 
110
- # Most recent lesson
111
- if [ -n "$LESSON_TEXT" ]; then
112
- LINE="${LINE} · ${C}${LESSON_TEXT}${RST}"
113
- fi
114
-
115
107
  echo -e "$LINE"
116
108
  fi
@@ -19,6 +19,14 @@ const path = require('path');
19
19
 
20
20
  const PROJECT_ROOT = path.join(__dirname, '..');
21
21
 
22
+ function explicitPinnedServeArgs(version) {
23
+ return ['--yes', '--package', `thumbgate@${version}`, 'thumbgate', 'serve'];
24
+ }
25
+
26
+ function explicitLatestServeArgs() {
27
+ return ['--yes', '--package', 'thumbgate@latest', 'thumbgate', 'serve'];
28
+ }
29
+
22
30
  function readJson(relPath) {
23
31
  return JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, relPath), 'utf-8'));
24
32
  }
@@ -211,7 +219,7 @@ function syncVersion(opts) {
211
219
  if (fs.existsSync(path.join(PROJECT_ROOT, codexPluginConfigPath))) {
212
220
  const codexPluginConfig = readJson(codexPluginConfigPath);
213
221
  const server = codexPluginConfig.mcpServers && codexPluginConfig.mcpServers.thumbgate;
214
- const expectedArgs = ['-y', `thumbgate@${version}`, 'serve'];
222
+ const expectedArgs = explicitPinnedServeArgs(version);
215
223
  const currentArgs = server && Array.isArray(server.args) ? server.args : [];
216
224
  if (server && server.command === 'npx' && JSON.stringify(currentArgs) !== JSON.stringify(expectedArgs)) {
217
225
  drifted.push({ file: codexPluginConfigPath, field: 'mcpServers.thumbgate.args', current: JSON.stringify(currentArgs) });
@@ -243,7 +251,7 @@ function syncVersion(opts) {
243
251
  if (fs.existsSync(path.join(PROJECT_ROOT, claudeCodexBridgeConfigPath))) {
244
252
  const bridgeConfig = readJson(claudeCodexBridgeConfigPath);
245
253
  const server = bridgeConfig.mcpServers && bridgeConfig.mcpServers.thumbgate;
246
- const expectedArgs = ['-y', `thumbgate@${version}`, 'serve'];
254
+ const expectedArgs = explicitPinnedServeArgs(version);
247
255
  const currentArgs = server && Array.isArray(server.args) ? server.args : [];
248
256
  if (server && server.command === 'npx' && JSON.stringify(currentArgs) !== JSON.stringify(expectedArgs)) {
249
257
  drifted.push({ file: claudeCodexBridgeConfigPath, field: 'mcpServers.thumbgate.args', current: JSON.stringify(currentArgs) });
@@ -260,7 +268,7 @@ function syncVersion(opts) {
260
268
  if (fs.existsSync(path.join(PROJECT_ROOT, cursorPluginConfigPath))) {
261
269
  const cursorPluginConfig = readJson(cursorPluginConfigPath);
262
270
  const server = cursorPluginConfig.mcpServers && cursorPluginConfig.mcpServers.thumbgate;
263
- const expectedArgs = ['-y', 'thumbgate@latest', 'serve'];
271
+ const expectedArgs = explicitLatestServeArgs();
264
272
  const currentArgs = server && Array.isArray(server.args) ? server.args : [];
265
273
  if (server && server.command === 'npx' && JSON.stringify(currentArgs) !== JSON.stringify(expectedArgs)) {
266
274
  drifted.push({ file: cursorPluginConfigPath, field: 'mcpServers.thumbgate.args', current: JSON.stringify(currentArgs) });
@@ -16,6 +16,7 @@ const COVERAGE_INCLUDE_GLOBS = [
16
16
  const COVERAGE_EXCLUDE_GLOBS = [
17
17
  'tests/**/*.js',
18
18
  ];
19
+ let cachedCoverageFilterSupport;
19
20
 
20
21
  function findCoverageTestFiles({
21
22
  dir = TESTS_DIR,
@@ -39,29 +40,35 @@ function findCoverageTestFiles({
39
40
  return files.sort();
40
41
  }
41
42
 
42
- function supportsCoveragePatternFlags({
43
- spawn = spawnSync,
44
- } = {}) {
43
+ function detectCoverageFilterSupport({ spawn = spawnSync } = {}) {
44
+ if (spawn === spawnSync && cachedCoverageFilterSupport !== undefined) {
45
+ return cachedCoverageFilterSupport;
46
+ }
47
+
45
48
  const result = spawn(process.execPath, ['--help'], {
46
49
  encoding: 'utf8',
47
50
  });
51
+ const helpText = `${result.stdout || ''}\n${result.stderr || ''}`;
52
+ const supported = helpText.includes('--test-coverage-include') && helpText.includes('--test-coverage-exclude');
48
53
 
49
- if (result.error) {
50
- return false;
54
+ if (spawn === spawnSync) {
55
+ cachedCoverageFilterSupport = supported;
51
56
  }
52
57
 
53
- const help = `${result.stdout || ''}\n${result.stderr || ''}`;
54
- return help.includes('--test-coverage-include') && help.includes('--test-coverage-exclude');
58
+ return supported;
55
59
  }
56
60
 
57
- function buildCoverageArgs(files, { supportsPatternFlags = true } = {}) {
61
+ function buildCoverageArgs(files, { spawn = spawnSync, supportsFilters } = {}) {
58
62
  const args = [
59
63
  '--test',
60
64
  '--test-concurrency=1',
61
65
  '--experimental-test-coverage',
62
66
  ];
63
67
 
64
- if (supportsPatternFlags) {
68
+ const useFilterFlags = supportsFilters === undefined
69
+ ? detectCoverageFilterSupport({ spawn })
70
+ : supportsFilters;
71
+ if (useFilterFlags) {
65
72
  args.push(
66
73
  ...COVERAGE_INCLUDE_GLOBS.flatMap((pattern) => ['--test-coverage-include', pattern]),
67
74
  ...COVERAGE_EXCLUDE_GLOBS.flatMap((pattern) => ['--test-coverage-exclude', pattern]),
@@ -76,17 +83,17 @@ function runCoverage({
76
83
  files = findCoverageTestFiles(),
77
84
  cwd = PROJECT_ROOT,
78
85
  spawn = spawnSync,
79
- supportsPatternFlags = supportsCoveragePatternFlags({ spawn }),
86
+ supportsFilters,
80
87
  } = {}) {
81
88
  if (files.length === 0) {
82
89
  return {
83
90
  exitCode: 1,
84
91
  error: 'No test files found for coverage run.',
85
- args: buildCoverageArgs(files, { supportsPatternFlags }),
92
+ args: buildCoverageArgs(files, { spawn, supportsFilters }),
86
93
  };
87
94
  }
88
95
 
89
- const args = buildCoverageArgs(files, { supportsPatternFlags });
96
+ const args = buildCoverageArgs(files, { spawn, supportsFilters });
90
97
  const result = spawn(process.execPath, args, {
91
98
  cwd,
92
99
  env: process.env,
@@ -113,8 +120,8 @@ module.exports = {
113
120
  COVERAGE_INCLUDE_GLOBS,
114
121
  PROJECT_ROOT,
115
122
  TESTS_DIR,
123
+ detectCoverageFilterSupport,
116
124
  findCoverageTestFiles,
117
125
  buildCoverageArgs,
118
126
  runCoverage,
119
- supportsCoveragePatternFlags,
120
127
  };
@@ -512,6 +512,89 @@ const TOOLS = [
512
512
  },
513
513
  },
514
514
  }),
515
+ destructiveTool({
516
+ name: 'set_task_scope',
517
+ description: 'Declare or clear the current task scope so ThumbGate can compare affected files and diffs against the approved path set.',
518
+ inputSchema: {
519
+ type: 'object',
520
+ properties: {
521
+ taskId: { type: 'string', description: 'Optional stable task identifier (ticket, issue, or work item id)' },
522
+ summary: { type: 'string', description: 'Short summary of the task being worked' },
523
+ allowedPaths: {
524
+ type: 'array',
525
+ items: { type: 'string' },
526
+ description: 'Glob patterns that define the allowed file scope for this task',
527
+ },
528
+ protectedPaths: {
529
+ type: 'array',
530
+ items: { type: 'string' },
531
+ description: 'Optional protected-file globs that require explicit approval before editing or publishing',
532
+ },
533
+ repoPath: { type: 'string', description: 'Optional repo root used when evaluating git diff scope' },
534
+ localOnly: { type: 'boolean', description: 'When true, also marks the task as local-only' },
535
+ clear: { type: 'boolean', description: 'Clear the current task scope instead of setting one' },
536
+ },
537
+ },
538
+ }),
539
+ readOnlyTool({
540
+ name: 'get_scope_state',
541
+ description: 'Return the active task scope and any unexpired protected-file approvals.',
542
+ inputSchema: {
543
+ type: 'object',
544
+ properties: {},
545
+ },
546
+ }),
547
+ destructiveTool({
548
+ name: 'set_branch_governance',
549
+ description: 'Declare or clear branch and release governance so PR, merge, release, and publish actions can be evaluated against explicit workflow state.',
550
+ inputSchema: {
551
+ type: 'object',
552
+ properties: {
553
+ branchName: { type: 'string', description: 'Optional branch name the governance applies to' },
554
+ baseBranch: { type: 'string', description: 'Protected base branch for merge and release operations (defaults to main)' },
555
+ prRequired: { type: 'boolean', description: 'Whether this lane must go through a pull request (defaults to true)' },
556
+ prNumber: { type: 'string', description: 'Optional pull request number once a PR exists' },
557
+ prUrl: { type: 'string', description: 'Optional pull request URL once a PR exists' },
558
+ queueRequired: { type: 'boolean', description: 'Whether the target branch requires a merge queue' },
559
+ localOnly: { type: 'boolean', description: 'When true, PR, merge, release, and publish actions are blocked for this lane' },
560
+ releaseVersion: { type: 'string', description: 'Expected package version for release or publish actions' },
561
+ releaseEvidence: { type: 'string', description: 'Optional evidence or release plan note for the governed version' },
562
+ releaseSensitiveGlobs: {
563
+ type: 'array',
564
+ items: { type: 'string' },
565
+ description: 'Optional custom globs that define release-sensitive files for this branch lane',
566
+ },
567
+ clear: { type: 'boolean', description: 'Clear the current branch governance state instead of setting it' },
568
+ },
569
+ },
570
+ }),
571
+ readOnlyTool({
572
+ name: 'get_branch_governance',
573
+ description: 'Return the active branch and release governance state.',
574
+ inputSchema: {
575
+ type: 'object',
576
+ properties: {},
577
+ },
578
+ }),
579
+ destructiveTool({
580
+ name: 'approve_protected_action',
581
+ description: 'Grant a time-limited approval for edits or publish actions that touch protected files.',
582
+ inputSchema: {
583
+ type: 'object',
584
+ required: ['pathGlobs', 'reason'],
585
+ properties: {
586
+ pathGlobs: {
587
+ type: 'array',
588
+ items: { type: 'string' },
589
+ description: 'Protected-file globs covered by this approval',
590
+ },
591
+ reason: { type: 'string', description: 'Why this protected-file action is approved' },
592
+ evidence: { type: 'string', description: 'Optional supporting evidence or approval note' },
593
+ taskId: { type: 'string', description: 'Optional task id this approval is tied to' },
594
+ ttlMs: { type: 'number', description: 'Optional approval lifetime in milliseconds (defaults to 1 hour, max 24 hours)' },
595
+ },
596
+ },
597
+ }),
515
598
  destructiveTool({
516
599
  name: 'track_action',
517
600
  description: 'Record a verification action in the current session (for example figma_verified or tests_passed). Session actions expire after one hour.',
@@ -535,6 +618,20 @@ const TOOLS = [
535
618
  },
536
619
  },
537
620
  }),
621
+ readOnlyTool({
622
+ name: 'check_operational_integrity',
623
+ description: 'Evaluate whether the current repo state is safe for PR, merge, release, and publish operations.',
624
+ inputSchema: {
625
+ type: 'object',
626
+ properties: {
627
+ repoPath: { type: 'string', description: 'Optional repository path to inspect' },
628
+ baseBranch: { type: 'string', description: 'Protected base branch to compare against (defaults to main)' },
629
+ command: { type: 'string', description: 'Optional git, PR, or publish command to evaluate against the current governance state' },
630
+ requirePrForReleaseSensitive: { type: 'boolean', description: 'When true, release-sensitive changes on non-base branches require an open PR' },
631
+ requireVersionNotBehindBase: { type: 'boolean', description: 'When true, release-sensitive changes cannot lag behind the base branch package version' },
632
+ },
633
+ },
634
+ }),
538
635
  destructiveTool({
539
636
  name: 'register_claim_gate',
540
637
  description: 'Register a custom claim verification rule in local runtime state without editing tracked repo config.',
@@ -15,7 +15,7 @@ Usage:
15
15
  python train_from_feedback.py --dpo-train # DPO batch optimization (Feb 2026)
16
16
  python train_from_feedback.py --config config.json # Use custom categories
17
17
 
18
- This script only reads and writes local feedback artifacts under .claude/memory/feedback.
18
+ This script only reads and writes local feedback artifacts under the active ThumbGate feedback directory.
19
19
  Those runtime outputs are git-ignored even though this utility is intentionally versioned.
20
20
  """
21
21
 
@@ -23,15 +23,37 @@ import json
23
23
  import math
24
24
  import random
25
25
  import argparse
26
+ import os
26
27
  from datetime import datetime
27
28
  from pathlib import Path
28
29
  from typing import Dict, List, Any, Optional, Tuple
29
30
 
30
31
  # Configuration
31
32
  PROJECT_ROOT = Path(__file__).parent.parent
32
- FEEDBACK_LOG = PROJECT_ROOT / ".claude" / "memory" / "feedback" / "feedback-log.jsonl"
33
- MODEL_FILE = PROJECT_ROOT / ".claude" / "memory" / "feedback" / "feedback_model.json"
34
- SNAPSHOTS_DIR = PROJECT_ROOT / ".claude" / "memory" / "feedback" / "model_snapshots"
33
+
34
+ def resolve_feedback_dir() -> Path:
35
+ env_dir = os.environ.get("THUMBGATE_FEEDBACK_DIR")
36
+ if env_dir:
37
+ return Path(env_dir)
38
+
39
+ local_thumbgate = PROJECT_ROOT / ".thumbgate"
40
+ if local_thumbgate.exists():
41
+ return local_thumbgate
42
+
43
+ local_rlhf = PROJECT_ROOT / ".rlhf"
44
+ if local_rlhf.exists():
45
+ return local_rlhf
46
+
47
+ local_legacy = PROJECT_ROOT / ".claude" / "memory" / "feedback"
48
+ if local_legacy.exists():
49
+ return local_legacy
50
+
51
+ return Path.home() / ".thumbgate" / "projects" / PROJECT_ROOT.name
52
+
53
+ FEEDBACK_DIR = resolve_feedback_dir()
54
+ FEEDBACK_LOG = FEEDBACK_DIR / "feedback-log.jsonl"
55
+ MODEL_FILE = FEEDBACK_DIR / "feedback_model.json"
56
+ SNAPSHOTS_DIR = FEEDBACK_DIR / "model_snapshots"
35
57
 
36
58
  # Default categories (overridden by --config)
37
59
  DEFAULT_CATEGORIES = {
@@ -132,10 +154,11 @@ def create_initial_model(categories: Dict) -> Dict:
132
154
 
133
155
  def save_model(model: Dict):
134
156
  """Save model to disk."""
135
- # Resolve and verify path stays within project root (CodeQL S2083)
157
+ # Resolve and verify path stays within trusted local ThumbGate roots (CodeQL S2083)
136
158
  resolved = MODEL_FILE.resolve()
137
- if not str(resolved).startswith(str(PROJECT_ROOT.resolve())):
138
- raise ValueError(f"Model path escapes project root: {resolved}")
159
+ allowed_roots = [PROJECT_ROOT.resolve(), FEEDBACK_DIR.resolve()]
160
+ if not any(str(resolved).startswith(str(root)) for root in allowed_roots):
161
+ raise ValueError(f"Model path escapes allowed ThumbGate roots: {resolved}")
139
162
  resolved.parent.mkdir(parents=True, exist_ok=True)
140
163
  model["updated"] = datetime.now().isoformat()
141
164
  resolved.write_text(json.dumps(model, indent=2))
@@ -344,7 +367,7 @@ def save_snapshot(model: Dict) -> Path:
344
367
  # Based on: Meta-Policy Reflexion (arXiv:2509.03990)
345
368
  # ============================================
346
369
 
347
- META_POLICY_FILE = PROJECT_ROOT / ".claude" / "memory" / "feedback" / "meta_policy_rules.json"
370
+ META_POLICY_FILE = FEEDBACK_DIR / "meta_policy_rules.json"
348
371
 
349
372
 
350
373
  def extract_meta_policy_rules(min_occurrences: int = 3) -> List[Dict[str, Any]]:
@@ -508,7 +531,7 @@ def load_meta_policy_rules() -> List[Dict[str, Any]]:
508
531
  # Reference: Rafailov et al. 2023 (arXiv:2305.18290)
509
532
  # ============================================
510
533
 
511
- DPO_MODEL_FILE = PROJECT_ROOT / ".claude" / "memory" / "feedback" / "dpo_model.json"
534
+ DPO_MODEL_FILE = FEEDBACK_DIR / "dpo_model.json"
512
535
  DPO_BETA = 0.1 # Temperature parameter (lower = more aggressive preference following)
513
536
 
514
537
 
@@ -26,15 +26,16 @@
26
26
 
27
27
  const fs = require('fs');
28
28
  const path = require('path');
29
+ const { resolveFeedbackDir } = require('./feedback-paths');
29
30
 
30
31
  // =============================================================================
31
32
  // PATH RESOLUTION
32
33
  // =============================================================================
33
34
 
34
- const DEFAULT_FEEDBACK_DIR = path.join(__dirname, '..', '.claude', 'memory', 'feedback');
35
+ const DEFAULT_FEEDBACK_DIR = resolveFeedbackDir();
35
36
 
36
37
  function getFeedbackDir() {
37
- return process.env.THUMBGATE_FEEDBACK_DIR || DEFAULT_FEEDBACK_DIR;
38
+ return resolveFeedbackDir();
38
39
  }
39
40
 
40
41
  function getFeedbackPaths() {
@@ -8,8 +8,7 @@ const {
8
8
  resolveFeedbackDir,
9
9
  } = require('./local-model-profile');
10
10
 
11
- const PROJECT_ROOT = path.join(__dirname, '..');
12
- const DEFAULT_FEEDBACK_DIR = path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
11
+ const DEFAULT_FEEDBACK_DIR = resolveFeedbackDir();
13
12
  const DEFAULT_LANCE_DIR = path.join(DEFAULT_FEEDBACK_DIR, 'lancedb');
14
13
 
15
14
  // Module-level cache — prevents re-importing on every upsertFeedback() call
@@ -29,7 +28,7 @@ async function getLanceDB() {
29
28
  }
30
29
 
31
30
  function getFeedbackDir() {
32
- return resolveFeedbackDir(process.env.THUMBGATE_FEEDBACK_DIR || DEFAULT_FEEDBACK_DIR);
31
+ return resolveFeedbackDir();
33
32
  }
34
33
 
35
34
  function getLanceDir() {
@@ -134,9 +134,9 @@ check_path_documented() {
134
134
  fi
135
135
  }
136
136
 
137
- check_path_documented ".claude/memory/feedback/memory-log.jsonl" "memory-log.jsonl"
138
- check_path_documented ".claude/memory/feedback/prevention-rules.md" "prevention-rules.md"
139
- check_path_documented ".claude/memory/feedback/feedback-log.jsonl" "feedback-log.jsonl"
137
+ check_path_documented ".thumbgate/memory-log.jsonl" "memory-log.jsonl"
138
+ check_path_documented ".thumbgate/prevention-rules.md" "prevention-rules.md"
139
+ check_path_documented ".thumbgate/feedback-log.jsonl" "feedback-log.jsonl"
140
140
 
141
141
  # primer.md must exist (it is committed)
142
142
  if [ -f "$REPO_ROOT/primer.md" ]; then