thumbgate 1.2.0 → 1.4.0

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 (160) hide show
  1. package/.claude-plugin/README.md +4 -4
  2. package/.claude-plugin/marketplace.json +32 -13
  3. package/.claude-plugin/plugin.json +15 -2
  4. package/.well-known/llms.txt +60 -0
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +133 -23
  7. package/adapters/README.md +1 -1
  8. package/adapters/chatgpt/openapi.yaml +168 -0
  9. package/adapters/claude/.mcp.json +2 -2
  10. package/adapters/codex/config.toml +2 -2
  11. package/adapters/mcp/server-stdio.js +85 -2
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/bin/cli.js +215 -19
  14. package/bin/postinstall.js +8 -2
  15. package/config/budget.json +18 -0
  16. package/config/gates/code-edit.json +61 -0
  17. package/config/gates/db-write.json +61 -0
  18. package/config/gates/default.json +154 -3
  19. package/config/gates/deploy.json +61 -0
  20. package/config/github-about.json +2 -1
  21. package/config/merge-quality-checks.json +23 -0
  22. package/config/model-tiers.json +11 -0
  23. package/openapi/openapi.yaml +168 -0
  24. package/package.json +47 -13
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  27. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  28. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  29. package/plugins/codex-profile/.mcp.json +1 -1
  30. package/plugins/codex-profile/INSTALL.md +27 -4
  31. package/plugins/codex-profile/README.md +33 -9
  32. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  33. package/plugins/cursor-marketplace/README.md +2 -2
  34. package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
  35. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
  36. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
  37. package/plugins/opencode-profile/INSTALL.md +1 -1
  38. package/public/blog.html +73 -0
  39. package/public/compare/mem0.html +189 -0
  40. package/public/compare/speclock.html +180 -0
  41. package/public/compare.html +12 -4
  42. package/public/guide.html +5 -5
  43. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  44. package/public/guides/codex-cli-guardrails.html +158 -0
  45. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  46. package/public/guides/pre-action-gates.html +162 -0
  47. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  48. package/public/index.html +169 -70
  49. package/public/learn/ai-agent-persistent-memory.html +1 -0
  50. package/public/lessons.html +334 -17
  51. package/public/llm-context.md +140 -0
  52. package/public/pro.html +24 -22
  53. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  54. package/scripts/access-anomaly-detector.js +1 -1
  55. package/scripts/adk-consolidator.js +1 -5
  56. package/scripts/agent-security-hardening.js +4 -6
  57. package/scripts/agentic-data-pipeline.js +1 -3
  58. package/scripts/async-job-runner.js +1 -5
  59. package/scripts/audit-trail.js +7 -5
  60. package/scripts/background-agent-governance.js +2 -10
  61. package/scripts/billing.js +2 -16
  62. package/scripts/budget-enforcer.js +173 -0
  63. package/scripts/build-codex-plugin.js +152 -0
  64. package/scripts/capture-railway-diagnostics.sh +97 -0
  65. package/scripts/check-congruence.js +133 -15
  66. package/scripts/claude-feedback-sync.js +320 -0
  67. package/scripts/cli-telemetry.js +4 -1
  68. package/scripts/commercial-offer.js +5 -7
  69. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  70. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  71. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  72. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  73. package/scripts/context-engine.js +21 -6
  74. package/scripts/contextfs.js +33 -44
  75. package/scripts/dashboard.js +104 -0
  76. package/scripts/decision-journal.js +341 -0
  77. package/scripts/delegation-runtime.js +1 -5
  78. package/scripts/distribution-surfaces.js +26 -0
  79. package/scripts/document-intake.js +927 -0
  80. package/scripts/ephemeral-agent-store.js +1 -8
  81. package/scripts/evolution-state.js +1 -5
  82. package/scripts/experiment-tracker.js +1 -5
  83. package/scripts/export-databricks-bundle.js +1 -5
  84. package/scripts/export-hf-dataset.js +1 -5
  85. package/scripts/export-training.js +1 -5
  86. package/scripts/feedback-attribution.js +1 -16
  87. package/scripts/feedback-history-distiller.js +1 -16
  88. package/scripts/feedback-loop.js +17 -5
  89. package/scripts/feedback-root-consolidator.js +2 -21
  90. package/scripts/feedback-session.js +49 -0
  91. package/scripts/feedback-to-rules.js +188 -28
  92. package/scripts/filesystem-search.js +1 -9
  93. package/scripts/fs-utils.js +104 -0
  94. package/scripts/gates-engine.js +149 -4
  95. package/scripts/github-about.js +32 -8
  96. package/scripts/gtm-revenue-loop.js +1 -5
  97. package/scripts/harness-selector.js +148 -0
  98. package/scripts/hosted-job-launcher.js +1 -5
  99. package/scripts/hybrid-feedback-context.js +7 -33
  100. package/scripts/intervention-policy.js +753 -0
  101. package/scripts/lesson-db.js +3 -18
  102. package/scripts/lesson-inference.js +194 -16
  103. package/scripts/lesson-retrieval.js +60 -24
  104. package/scripts/llm-client.js +59 -0
  105. package/scripts/local-model-profile.js +18 -2
  106. package/scripts/managed-lesson-agent.js +183 -0
  107. package/scripts/marketing-experiment.js +8 -22
  108. package/scripts/meta-agent-loop.js +624 -0
  109. package/scripts/metered-billing.js +1 -1
  110. package/scripts/model-tier-router.js +10 -1
  111. package/scripts/money-watcher.js +1 -4
  112. package/scripts/obsidian-export.js +1 -5
  113. package/scripts/operational-integrity.js +369 -34
  114. package/scripts/org-dashboard.js +6 -1
  115. package/scripts/per-step-scoring.js +2 -4
  116. package/scripts/pr-manager.js +201 -19
  117. package/scripts/pro-features.js +3 -2
  118. package/scripts/prompt-dlp.js +3 -3
  119. package/scripts/prove-adapters.js +2 -5
  120. package/scripts/prove-attribution.js +1 -5
  121. package/scripts/prove-automation.js +3 -5
  122. package/scripts/prove-cloudflare-sandbox.js +1 -3
  123. package/scripts/prove-data-pipeline.js +1 -3
  124. package/scripts/prove-intelligence.js +1 -3
  125. package/scripts/prove-lancedb.js +1 -5
  126. package/scripts/prove-local-intelligence.js +1 -3
  127. package/scripts/prove-packaged-runtime.js +326 -0
  128. package/scripts/prove-predictive-insights.js +1 -3
  129. package/scripts/prove-runtime.js +13 -0
  130. package/scripts/prove-training-export.js +1 -3
  131. package/scripts/prove-workflow-contract.js +1 -5
  132. package/scripts/rate-limiter.js +6 -4
  133. package/scripts/reddit-dm-outreach.js +14 -4
  134. package/scripts/schedule-manager.js +3 -5
  135. package/scripts/security-scanner.js +448 -0
  136. package/scripts/self-distill-agent.js +579 -0
  137. package/scripts/semantic-dedup.js +115 -0
  138. package/scripts/skill-exporter.js +1 -3
  139. package/scripts/skill-generator.js +1 -5
  140. package/scripts/social-analytics/engagement-audit.js +1 -18
  141. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  142. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  143. package/scripts/social-analytics/publishers/zernio.js +51 -0
  144. package/scripts/social-pipeline.js +1 -3
  145. package/scripts/social-post-hourly.js +47 -4
  146. package/scripts/statusline-links.js +6 -5
  147. package/scripts/statusline-local-stats.js +2 -0
  148. package/scripts/statusline.sh +38 -7
  149. package/scripts/sync-branch-protection.js +340 -0
  150. package/scripts/tessl-export.js +1 -3
  151. package/scripts/thumbgate-search.js +32 -1
  152. package/scripts/tool-kpi-tracker.js +1 -1
  153. package/scripts/tool-registry.js +108 -4
  154. package/scripts/vector-store.js +1 -5
  155. package/scripts/weekly-auto-post.js +1 -1
  156. package/scripts/workflow-sentinel.js +205 -4
  157. package/skills/thumbgate/SKILL.md +2 -2
  158. package/src/api/server.js +273 -4
  159. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  160. /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
@@ -0,0 +1,175 @@
1
+ # LinkedIn Content: ThumbGate Gates (2026-04-09)
2
+
3
+ Generated from: `config/gates/default.json`
4
+ Gate count in config: 25
5
+ Posts generated: 7
6
+
7
+ ---
8
+
9
+ ## Post 1: Local Only Git Writes
10
+
11
+ 🚨 Your AI agents are running without guardrails.
12
+
13
+ Blocks git writes when local-only mode is active, preventing accidental remote pushes during development.
14
+
15
+ The problem? AI agents run autonomously. A single unchecked operation—a force-push, an unapproved deploy, a dependency injection—can unwind days of work in seconds. Traditional CI won't catch it. Your human reviewer might miss it.
16
+
17
+ The solution? **Gate `local-only-git-writes`** in ThumbGate stops high-risk operations *before* they execute. No second chances. Just prevention.
18
+
19
+ This isn't about slowing down. It's about building trust in autonomous systems. Every gate is a rule learned from real failures.
20
+
21
+ 🔒 Install ThumbGate today:
22
+ ```bash
23
+ npx thumbgate@latest init
24
+ ```
25
+
26
+ Then add this gate to your config and sleep better.
27
+
28
+ #AIGovernance #DevTools #AgentSafety #EngineeringTeams
29
+
30
+ ---
31
+
32
+
33
+ ## Post 2: Gh Pr Create Restricted
34
+
35
+ ⚠️ One missing gate. One catastrophic mistake.
36
+
37
+ Restricts PR creation to explicitly approved workflows, preventing unvetted code changes.
38
+
39
+ The problem? AI agents run autonomously. A single unchecked operation—a force-push, an unapproved deploy, a dependency injection—can unwind days of work in seconds. Traditional CI won't catch it. Your human reviewer might miss it.
40
+
41
+ The solution? **Gate `gh-pr-create-restricted`** in ThumbGate stops high-risk operations *before* they execute. No second chances. Just prevention.
42
+
43
+ This isn't about slowing down. It's about building trust in autonomous systems. Every gate is a rule learned from real failures.
44
+
45
+ 🔒 Install ThumbGate today:
46
+ ```bash
47
+ npx thumbgate@latest init
48
+ ```
49
+
50
+ Then add this gate to your config and sleep better.
51
+
52
+ #AIGovernance #DevTools #AgentSafety #EngineeringTeams
53
+
54
+ ---
55
+
56
+
57
+ ## Post 3: Env File Edit
58
+
59
+ 🛡️ Even the best engineers miss edge cases.
60
+
61
+ Warns when editing .env files—catches accidental token deletion.
62
+
63
+ The problem? AI agents run autonomously. A single unchecked operation—a force-push, an unapproved deploy, a dependency injection—can unwind days of work in seconds. Traditional CI won't catch it. Your human reviewer might miss it.
64
+
65
+ The solution? **Gate `env-file-edit`** in ThumbGate stops high-risk operations *before* they execute. No second chances. Just prevention.
66
+
67
+ This isn't about slowing down. It's about building trust in autonomous systems. Every gate is a rule learned from real failures.
68
+
69
+ 🔒 Install ThumbGate today:
70
+ ```bash
71
+ npx thumbgate@latest init
72
+ ```
73
+
74
+ Then add this gate to your config and sleep better.
75
+
76
+ #AIGovernance #DevTools #AgentSafety #EngineeringTeams
77
+
78
+ ---
79
+
80
+
81
+ ## Post 4: Style Violation Log
82
+
83
+ 💥 Your deployment pipeline has a blind spot.
84
+
85
+ Protects your workflow by style audit mode active. action recorded for review but allowed to proceed.
86
+
87
+ The problem? AI agents run autonomously. A single unchecked operation—a force-push, an unapproved deploy, a dependency injection—can unwind days of work in seconds. Traditional CI won't catch it. Your human reviewer might miss it.
88
+
89
+ The solution? **Gate `style-violation-log`** in ThumbGate stops high-risk operations *before* they execute. No second chances. Just prevention.
90
+
91
+ This isn't about slowing down. It's about building trust in autonomous systems. Every gate is a rule learned from real failures.
92
+
93
+ 🔒 Install ThumbGate today:
94
+ ```bash
95
+ npx thumbgate@latest init
96
+ ```
97
+
98
+ Then add this gate to your config and sleep better.
99
+
100
+ #AIGovernance #DevTools #AgentSafety #EngineeringTeams
101
+
102
+ ---
103
+
104
+
105
+ ## Post 5: Loop Abuse Prevention
106
+
107
+ 🔓 Git operations—unguarded by default.
108
+
109
+ Protects your workflow by high-risk command detected inside a loop. scheduled tasks must not perform egress or destructive writes without explicit approval.
110
+
111
+ The problem? AI agents run autonomously. A single unchecked operation—a force-push, an unapproved deploy, a dependency injection—can unwind days of work in seconds. Traditional CI won't catch it. Your human reviewer might miss it.
112
+
113
+ The solution? **Gate `loop-abuse-prevention`** in ThumbGate stops high-risk operations *before* they execute. No second chances. Just prevention.
114
+
115
+ This isn't about slowing down. It's about building trust in autonomous systems. Every gate is a rule learned from real failures.
116
+
117
+ 🔒 Install ThumbGate today:
118
+ ```bash
119
+ npx thumbgate@latest init
120
+ ```
121
+
122
+ Then add this gate to your config and sleep better.
123
+
124
+ #AIGovernance #DevTools #AgentSafety #EngineeringTeams
125
+
126
+ ---
127
+
128
+
129
+ ## Post 6: Release Readiness Required
130
+
131
+ 🎯 Prevention beats firefighting.
132
+
133
+ Ensures releases only happen from releasable mainline commits with version alignment.
134
+
135
+ The problem? AI agents run autonomously. A single unchecked operation—a force-push, an unapproved deploy, a dependency injection—can unwind days of work in seconds. Traditional CI won't catch it. Your human reviewer might miss it.
136
+
137
+ The solution? **Gate `release-readiness-required`** in ThumbGate stops high-risk operations *before* they execute. No second chances. Just prevention.
138
+
139
+ This isn't about slowing down. It's about building trust in autonomous systems. Every gate is a rule learned from real failures.
140
+
141
+ 🔒 Install ThumbGate today:
142
+ ```bash
143
+ npx thumbgate@latest init
144
+ ```
145
+
146
+ Then add this gate to your config and sleep better.
147
+
148
+ #AIGovernance #DevTools #AgentSafety #EngineeringTeams
149
+
150
+ ---
151
+
152
+
153
+ ## Post 7: Protected Branch Push
154
+
155
+ ⏱️ How fast can your agent destroy a month of work?
156
+
157
+ Prevents direct pushes to main/develop. All changes flow through PR review.
158
+
159
+ The problem? AI agents run autonomously. A single unchecked operation—a force-push, an unapproved deploy, a dependency injection—can unwind days of work in seconds. Traditional CI won't catch it. Your human reviewer might miss it.
160
+
161
+ The solution? **Gate `protected-branch-push`** in ThumbGate stops high-risk operations *before* they execute. No second chances. Just prevention.
162
+
163
+ This isn't about slowing down. It's about building trust in autonomous systems. Every gate is a rule learned from real failures.
164
+
165
+ 🔒 Install ThumbGate today:
166
+ ```bash
167
+ npx thumbgate@latest init
168
+ ```
169
+
170
+ Then add this gate to your config and sleep better.
171
+
172
+ #AIGovernance #DevTools #AgentSafety #EngineeringTeams
173
+
174
+ ---
175
+
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reddit Thread Finder for ThumbGate Engagement
4
+ *
5
+ * Add to package.json:
6
+ * "content:reddit": "node scripts/content-engine/reddit-thread-finder.js"
7
+ * "content:reddit:dry": "node scripts/content-engine/reddit-thread-finder.js --dry-run"
8
+ * "content:reddit:limit": "node scripts/content-engine/reddit-thread-finder.js --limit 20"
9
+ */
10
+
11
+ const https = require('https');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const SUBREDDITS = [
16
+ 'ChatGPTCoding', 'ClaudeAI', 'cursor', 'devops',
17
+ 'SoftwareEngineering', 'ExperiencedDevs', 'MachineLearning', 'LocalLLaMA'
18
+ ];
19
+
20
+ const KEYWORDS = [
21
+ 'agent broke', 'agent deleted', 'force push', 'prevent AI from',
22
+ 'guardrails', 'agent governance', 'coding agent mistakes'
23
+ ];
24
+
25
+ const USER_AGENT = 'script:thumbgate-content:v1.0';
26
+ const DELAY = 2000; // ms between requests
27
+ const DEFAULT_LIMIT = 10;
28
+
29
+ let dryRun = false;
30
+ let outputLimit = DEFAULT_LIMIT;
31
+
32
+ process.argv.slice(2).forEach(arg => {
33
+ if (arg === '--dry-run') dryRun = true;
34
+ if (arg.startsWith('--limit')) outputLimit = parseInt(arg.split('=')[1] || arg.split(' ')[1], 10);
35
+ });
36
+
37
+ function fetchReddit(url) {
38
+ return new Promise((resolve, reject) => {
39
+ const req = https.get(url, { headers: { 'User-Agent': USER_AGENT } }, res => {
40
+ let data = '';
41
+ res.on('data', chunk => data += chunk);
42
+ res.on('end', () => {
43
+ try {
44
+ resolve(JSON.parse(data));
45
+ } catch (e) {
46
+ reject(e);
47
+ }
48
+ });
49
+ });
50
+ req.on('error', reject);
51
+ });
52
+ }
53
+
54
+ async function searchSubreddit(sub, keyword) {
55
+ const url = `https://www.reddit.com/r/${sub}/search.json?q=${encodeURIComponent(keyword)}&sort=new&t=week&limit=5`;
56
+ try {
57
+ const data = await fetchReddit(url);
58
+ return (data.data?.children || []).map(post => ({
59
+ id: post.data.id,
60
+ title: post.data.title,
61
+ url: `https://reddit.com${post.data.permalink}`,
62
+ subreddit: post.data.subreddit,
63
+ score: post.data.score,
64
+ numComments: post.data.num_comments,
65
+ created: post.data.created_utc,
66
+ selftext: post.data.selftext
67
+ }));
68
+ } catch (err) {
69
+ console.error(`Error searching ${sub} for "${keyword}": ${err.message}`);
70
+ return [];
71
+ }
72
+ }
73
+
74
+ function scoreThread(thread) {
75
+ const now = Math.floor(Date.now() / 1000);
76
+ const ageHours = (now - thread.created) / 3600;
77
+ const recencyScore = Math.max(0, 1 - ageHours / 168); // 0-1 over a week
78
+ const upvoteScore = Math.log(Math.max(1, thread.score)) / Math.log(100);
79
+ const commentScore = Math.log(Math.max(1, thread.numComments)) / Math.log(100);
80
+
81
+ return (recencyScore * 0.5) + (upvoteScore * 0.3) + (commentScore * 0.2);
82
+ }
83
+
84
+ function generateReply(thread) {
85
+ const context = thread.selftext.substring(0, 200);
86
+ return `
87
+ **ThumbGate can help prevent this.** Our pre-action gates catch agent mistakes before they happen:
88
+ - Stop force pushes on protected branches
89
+ - Prevent deletions of critical files
90
+ - Verify AI actions before execution
91
+ - Capture lessons from failures to block similar mistakes
92
+
93
+ Learn more: https://thumbgate-production.up.railway.app/dashboard
94
+ `;
95
+ }
96
+
97
+ async function main() {
98
+ console.log(`[${new Date().toISOString()}] Starting Reddit thread finder...`);
99
+
100
+ const threads = {};
101
+ let requestCount = 0;
102
+
103
+ for (const sub of SUBREDDITS) {
104
+ for (const keyword of KEYWORDS) {
105
+ if (requestCount > 0) await new Promise(r => setTimeout(r, DELAY));
106
+
107
+ console.log(` Searching r/${sub} for "${keyword}"...`);
108
+ const results = await searchSubreddit(sub, keyword);
109
+
110
+ results.forEach(thread => {
111
+ if (!threads[thread.id]) {
112
+ threads[thread.id] = thread;
113
+ }
114
+ });
115
+ requestCount++;
116
+ }
117
+ }
118
+
119
+ const sorted = Object.values(threads)
120
+ .sort((a, b) => scoreThread(b) - scoreThread(a))
121
+ .slice(0, outputLimit);
122
+
123
+ const date = new Date().toISOString().split('T')[0];
124
+ const outputDir = path.join(__dirname, 'output');
125
+
126
+ if (!fs.existsSync(outputDir)) {
127
+ fs.mkdirSync(outputDir, { recursive: true });
128
+ }
129
+
130
+ let markdown = `# Reddit Threads - ${date}\n\nFound ${sorted.length} high-relevance threads.\n\n`;
131
+
132
+ sorted.forEach((thread, idx) => {
133
+ const score = scoreThread(thread);
134
+ markdown += `## ${idx + 1}. ${thread.title}\n`;
135
+ markdown += `**r/${thread.subreddit}** | [Link](${thread.url}) | Score: ${thread.score} | Comments: ${thread.numComments}\n`;
136
+ markdown += `**Relevance Score:** ${score.toFixed(2)}\n\n`;
137
+
138
+ if (!dryRun) {
139
+ markdown += `**Suggested Reply:**\n${generateReply(thread)}\n\n`;
140
+ }
141
+ markdown += '---\n\n';
142
+ });
143
+
144
+ const outputFile = path.join(outputDir, `reddit-threads-${date}.md`);
145
+ fs.writeFileSync(outputFile, markdown);
146
+
147
+ console.log(`\n✅ Generated ${sorted.length} threads to ${outputFile}`);
148
+ if (dryRun) console.log(' (--dry-run: no reply suggestions included)');
149
+ }
150
+
151
+ main().catch(err => {
152
+ console.error('❌ Fatal error:', err.message);
153
+ process.exit(1);
154
+ });
@@ -20,6 +20,7 @@ const fs = require('fs');
20
20
  const path = require('path');
21
21
  const crypto = require('crypto');
22
22
  const { constructContextPack } = require('./contextfs');
23
+ const { ensureDir } = require('./fs-utils');
23
24
 
24
25
  // ---------------------------------------------------------------------------
25
26
  // Default paths
@@ -75,11 +76,6 @@ const TOOL_CONSOLIDATION = {
75
76
  // Utility: ensure directory exists
76
77
  // ---------------------------------------------------------------------------
77
78
 
78
- function ensureDir(dirPath) {
79
- if (!fs.existsSync(dirPath)) {
80
- fs.mkdirSync(dirPath, { recursive: true });
81
- }
82
- }
83
79
 
84
80
  // ---------------------------------------------------------------------------
85
81
  // Knowledge Bundle Builder
@@ -649,10 +645,29 @@ function compactContext(entries, anchors, opts) {
649
645
  return true;
650
646
  });
651
647
 
648
+ // Stage 6: Global token budget — drop entries (oldest first) until total chars fit
649
+ let finalStage = 5;
650
+ const totalMaxChars = typeof options.totalMaxChars === 'number' ? options.totalMaxChars : null;
651
+ if (totalMaxChars !== null) {
652
+ let budget = totalMaxChars;
653
+ const budgeted = [];
654
+ // Iterate newest-first so most recent entries are preserved
655
+ for (let i = working.length - 1; i >= 0; i--) {
656
+ const entrySize = JSON.stringify(working[i]).length;
657
+ if (budget - entrySize < 0) break;
658
+ budget -= entrySize;
659
+ budgeted.unshift(working[i]);
660
+ }
661
+ if (budgeted.length < working.length) {
662
+ working = budgeted;
663
+ finalStage = 6;
664
+ }
665
+ }
666
+
652
667
  const removedCount = initial - working.length;
653
668
  return {
654
669
  entries: [...anchorEntries, ...working],
655
- stage: 5,
670
+ stage: finalStage,
656
671
  removedCount,
657
672
  compacted: removedCount > 0,
658
673
  };
@@ -12,6 +12,7 @@ const fs = require('fs');
12
12
  const path = require('path');
13
13
  const crypto = require('crypto');
14
14
  const { resolveFeedbackDir } = require('./feedback-paths');
15
+ const { ensureDir, readJsonl } = require('./fs-utils');
15
16
  const {
16
17
  retrieveHierarchicalDocuments,
17
18
  shouldUseHierarchicalRetrieval,
@@ -23,9 +24,15 @@ function getFeedbackBaseDir() {
23
24
  return resolveFeedbackDir();
24
25
  }
25
26
 
26
- const FEEDBACK_DIR = getFeedbackBaseDir();
27
- const CONTEXTFS_ROOT = process.env.THUMBGATE_CONTEXTFS_DIR
28
- || (FEEDBACK_DIR.endsWith('contextfs') ? FEEDBACK_DIR : path.join(FEEDBACK_DIR, 'contextfs'));
27
+ function getContextFsRoot() {
28
+ const feedbackDir = getFeedbackBaseDir();
29
+ if (process.env.THUMBGATE_CONTEXTFS_DIR) return process.env.THUMBGATE_CONTEXTFS_DIR;
30
+ return feedbackDir.endsWith('contextfs') ? feedbackDir : path.join(feedbackDir, 'contextfs');
31
+ }
32
+
33
+ function contextFsPath(...segments) {
34
+ return path.join(getContextFsRoot(), ...segments);
35
+ }
29
36
 
30
37
  const NAMESPACES = {
31
38
  rawHistory: 'raw_history',
@@ -93,15 +100,10 @@ const PACK_TEMPLATES = {
93
100
  },
94
101
  };
95
102
 
96
- function ensureDir(dirPath) {
97
- if (!fs.existsSync(dirPath)) {
98
- fs.mkdirSync(dirPath, { recursive: true });
99
- }
100
- }
101
103
 
102
104
  function ensureContextFs() {
103
105
  Object.values(NAMESPACES).forEach((subPath) => {
104
- ensureDir(path.join(CONTEXTFS_ROOT, subPath));
106
+ ensureDir(contextFsPath(subPath));
105
107
  });
106
108
  }
107
109
 
@@ -111,7 +113,7 @@ function nowIso() {
111
113
 
112
114
  function inferNamespaceFromPath(filePath) {
113
115
  if (!filePath) return '';
114
- const relativeDir = path.relative(CONTEXTFS_ROOT, path.dirname(filePath));
116
+ const relativeDir = path.relative(getContextFsRoot(), path.dirname(filePath));
115
117
  if (!relativeDir || relativeDir.startsWith('..')) return '';
116
118
  return relativeDir;
117
119
  }
@@ -134,22 +136,6 @@ function appendJsonl(filePath, payload) {
134
136
  fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`);
135
137
  }
136
138
 
137
- function readJsonl(filePath) {
138
- if (!fs.existsSync(filePath)) return [];
139
- const raw = fs.readFileSync(filePath, 'utf-8').trim();
140
- if (!raw) return [];
141
- return raw
142
- .split('\n')
143
- .map((line) => {
144
- try {
145
- return JSON.parse(line);
146
- } catch {
147
- return null;
148
- }
149
- })
150
- .filter(Boolean);
151
- }
152
-
153
139
  function listJsonFiles(dirPath) {
154
140
  if (!fs.existsSync(dirPath)) return [];
155
141
  const files = fs.readdirSync(dirPath, { withFileTypes: true });
@@ -211,7 +197,7 @@ function getSemanticCacheConfig() {
211
197
  }
212
198
 
213
199
  function getSemanticCachePath() {
214
- return path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'semantic-cache.jsonl');
200
+ return contextFsPath(NAMESPACES.provenance, 'semantic-cache.jsonl');
215
201
  }
216
202
 
217
203
  function loadSemanticCacheEntries() {
@@ -227,7 +213,7 @@ function getSourceHash(namespaces) {
227
213
  const normalizedNamespaces = normalizeNamespaces(namespaces);
228
214
 
229
215
  for (const ns of normalizedNamespaces) {
230
- const dirPath = path.join(CONTEXTFS_ROOT, ns);
216
+ const dirPath = contextFsPath(ns);
231
217
  if (!fs.existsSync(dirPath)) continue;
232
218
 
233
219
  const files = fs.readdirSync(dirPath).sort();
@@ -291,7 +277,7 @@ function recordProvenance(event) {
291
277
  timestamp: nowIso(),
292
278
  ...event,
293
279
  };
294
- appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'events.jsonl'), payload);
280
+ appendJsonl(contextFsPath(NAMESPACES.provenance, 'events.jsonl'), payload);
295
281
  return payload;
296
282
  }
297
283
 
@@ -299,7 +285,7 @@ function writeContextObject({ namespace, title, content, tags = [], source, ttl
299
285
  ensureContextFs();
300
286
 
301
287
  const id = `${Date.now()}_${toSlug(title)}`;
302
- const filePath = path.join(CONTEXTFS_ROOT, namespace, `${id}.json`);
288
+ const filePath = contextFsPath(namespace, `${id}.json`);
303
289
 
304
290
  const doc = {
305
291
  id,
@@ -361,7 +347,7 @@ function findExistingContextObject({ namespace, title, content, tags = [], sourc
361
347
  ensureContextFs();
362
348
 
363
349
  const expectedTags = normalizeTagList(tags);
364
- const dirPath = path.join(CONTEXTFS_ROOT, namespace);
350
+ const dirPath = contextFsPath(namespace);
365
351
  const files = listJsonFiles(dirPath).sort();
366
352
 
367
353
  for (const filePath of files) {
@@ -507,7 +493,7 @@ function loadCandidates(namespaces) {
507
493
  const docs = [];
508
494
 
509
495
  selected.forEach((namespace) => {
510
- const dir = path.join(CONTEXTFS_ROOT, namespace);
496
+ const dir = contextFsPath(namespace);
511
497
  const files = listJsonFiles(dir);
512
498
  files.forEach((filePath) => {
513
499
  try {
@@ -624,7 +610,7 @@ function selectFlatContextItems(candidates, maxItems, maxChars) {
624
610
  const MEMEX_INDEX_FILE = 'memex-index.jsonl';
625
611
 
626
612
  function getMemexIndexPath() {
627
- return path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, MEMEX_INDEX_FILE);
613
+ return contextFsPath(NAMESPACES.provenance, MEMEX_INDEX_FILE);
628
614
  }
629
615
 
630
616
  function buildIndexEntry(doc, filePath) {
@@ -751,7 +737,7 @@ function constructMemexPack({ query = '', maxItems = 8, maxChars = 6000, namespa
751
737
  cache: { hit: false },
752
738
  };
753
739
 
754
- appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
740
+ appendJsonl(contextFsPath(NAMESPACES.provenance, 'packs.jsonl'), pack);
755
741
  recordProvenance({
756
742
  type: 'memex_pack_constructed',
757
743
  packId,
@@ -792,7 +778,7 @@ function constructContextPack({ query = '', maxItems = 8, maxChars = 6000, names
792
778
  },
793
779
  };
794
780
 
795
- appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
781
+ appendJsonl(contextFsPath(NAMESPACES.provenance, 'packs.jsonl'), pack);
796
782
  recordProvenance({
797
783
  type: 'context_pack_cache_hit',
798
784
  packId,
@@ -861,7 +847,7 @@ function constructContextPack({ query = '', maxItems = 8, maxChars = 6000, names
861
847
  retrieval: selection.retrieval,
862
848
  };
863
849
 
864
- appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
850
+ appendJsonl(contextFsPath(NAMESPACES.provenance, 'packs.jsonl'), pack);
865
851
  appendSemanticCacheEntry({
866
852
  id: `cache_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
867
853
  timestamp: nowIso(),
@@ -899,7 +885,7 @@ function evaluateContextPack({ packId, outcome, signal = null, notes = '', rubri
899
885
  timestamp: nowIso(),
900
886
  };
901
887
 
902
- appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'evaluations.jsonl'), evaluation);
888
+ appendJsonl(contextFsPath(NAMESPACES.provenance, 'evaluations.jsonl'), evaluation);
903
889
  recordProvenance({
904
890
  type: 'context_pack_evaluated',
905
891
  packId,
@@ -912,7 +898,7 @@ function evaluateContextPack({ packId, outcome, signal = null, notes = '', rubri
912
898
  }
913
899
 
914
900
  function getProvenance(limit = 50) {
915
- const eventsPath = path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'events.jsonl');
901
+ const eventsPath = contextFsPath(NAMESPACES.provenance, 'events.jsonl');
916
902
  const events = readJsonl(eventsPath);
917
903
  return events.slice(-limit);
918
904
  }
@@ -923,7 +909,7 @@ function getProvenance(limit = 50) {
923
909
  * session starts with full context — no manual primer.md needed.
924
910
  */
925
911
  function writeSessionHandoff({ project, branch, lastTask, nextStep, blockers, openFiles, customContext } = {}) {
926
- ensureDir(path.join(CONTEXTFS_ROOT, NAMESPACES.session));
912
+ ensureDir(contextFsPath(NAMESPACES.session));
927
913
 
928
914
  let gitContext = {};
929
915
  try {
@@ -951,7 +937,7 @@ function writeSessionHandoff({ project, branch, lastTask, nextStep, blockers, op
951
937
  customContext: customContext || null,
952
938
  };
953
939
 
954
- const primerPath = path.join(CONTEXTFS_ROOT, NAMESPACES.session, 'primer.json');
940
+ const primerPath = contextFsPath(NAMESPACES.session, 'primer.json');
955
941
  fs.writeFileSync(primerPath, JSON.stringify(primer, null, 2));
956
942
 
957
943
  // Sync to primer.md if it exists
@@ -991,7 +977,7 @@ function writeSessionHandoff({ project, branch, lastTask, nextStep, blockers, op
991
977
  * Read the most recent session handoff primer.
992
978
  */
993
979
  function readSessionHandoff() {
994
- const primerPath = path.join(CONTEXTFS_ROOT, NAMESPACES.session, 'primer.json');
980
+ const primerPath = contextFsPath(NAMESPACES.session, 'primer.json');
995
981
  if (!fs.existsSync(primerPath)) return null;
996
982
  try {
997
983
  return JSON.parse(fs.readFileSync(primerPath, 'utf8'));
@@ -1192,7 +1178,7 @@ function constructMultiHopPack({ query = '', maxItems = 8, maxChars = 6000, name
1192
1178
  },
1193
1179
  };
1194
1180
 
1195
- appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
1181
+ appendJsonl(contextFsPath(NAMESPACES.provenance, 'packs.jsonl'), pack);
1196
1182
  appendSemanticCacheEntry({
1197
1183
  id: `cache_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1198
1184
  timestamp: nowIso(),
@@ -1244,7 +1230,10 @@ function listPackTemplates() {
1244
1230
  }
1245
1231
 
1246
1232
  module.exports = {
1247
- CONTEXTFS_ROOT,
1233
+ get CONTEXTFS_ROOT() {
1234
+ return getContextFsRoot();
1235
+ },
1236
+ getContextFsRoot,
1248
1237
  NAMESPACES,
1249
1238
  ensureContextFs,
1250
1239
  recordProvenance,
@@ -1283,5 +1272,5 @@ module.exports = {
1283
1272
 
1284
1273
  if (require.main === module) {
1285
1274
  ensureContextFs();
1286
- console.log(`ContextFS ready at ${CONTEXTFS_ROOT}`);
1275
+ console.log(`ContextFS ready at ${getContextFsRoot()}`);
1287
1276
  }