thumbgate 1.3.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 (146) hide show
  1. package/.claude-plugin/marketplace.json +32 -13
  2. package/.claude-plugin/plugin.json +15 -2
  3. package/.well-known/llms.txt +60 -0
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +109 -20
  6. package/adapters/README.md +1 -1
  7. package/adapters/chatgpt/openapi.yaml +168 -0
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/mcp/server-stdio.js +84 -1
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +200 -13
  13. package/bin/postinstall.js +8 -2
  14. package/config/budget.json +18 -0
  15. package/config/gates/code-edit.json +61 -0
  16. package/config/gates/db-write.json +61 -0
  17. package/config/gates/default.json +154 -3
  18. package/config/gates/deploy.json +61 -0
  19. package/config/github-about.json +2 -1
  20. package/config/merge-quality-checks.json +23 -0
  21. package/openapi/openapi.yaml +168 -0
  22. package/package.json +42 -10
  23. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  24. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  25. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  26. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  27. package/plugins/codex-profile/.mcp.json +1 -1
  28. package/plugins/codex-profile/INSTALL.md +27 -4
  29. package/plugins/codex-profile/README.md +33 -9
  30. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  31. package/plugins/opencode-profile/INSTALL.md +1 -1
  32. package/public/blog.html +73 -0
  33. package/public/compare/mem0.html +189 -0
  34. package/public/compare/speclock.html +180 -0
  35. package/public/compare.html +10 -2
  36. package/public/guide.html +2 -2
  37. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  38. package/public/guides/codex-cli-guardrails.html +158 -0
  39. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  40. package/public/guides/pre-action-gates.html +162 -0
  41. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  42. package/public/index.html +136 -50
  43. package/public/lessons.html +33 -24
  44. package/public/llm-context.md +140 -0
  45. package/public/pro.html +24 -22
  46. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  47. package/scripts/access-anomaly-detector.js +1 -1
  48. package/scripts/adk-consolidator.js +1 -5
  49. package/scripts/agent-security-hardening.js +4 -6
  50. package/scripts/agentic-data-pipeline.js +1 -3
  51. package/scripts/async-job-runner.js +1 -5
  52. package/scripts/audit-trail.js +1 -5
  53. package/scripts/background-agent-governance.js +2 -10
  54. package/scripts/billing.js +2 -16
  55. package/scripts/budget-enforcer.js +173 -0
  56. package/scripts/build-codex-plugin.js +152 -0
  57. package/scripts/check-congruence.js +132 -14
  58. package/scripts/commercial-offer.js +5 -7
  59. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  60. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  61. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  62. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  63. package/scripts/context-engine.js +21 -6
  64. package/scripts/contextfs.js +1 -21
  65. package/scripts/dashboard.js +20 -0
  66. package/scripts/decision-journal.js +341 -0
  67. package/scripts/delegation-runtime.js +1 -5
  68. package/scripts/distribution-surfaces.js +26 -0
  69. package/scripts/document-intake.js +927 -0
  70. package/scripts/ephemeral-agent-store.js +1 -8
  71. package/scripts/evolution-state.js +1 -5
  72. package/scripts/experiment-tracker.js +1 -5
  73. package/scripts/export-databricks-bundle.js +1 -5
  74. package/scripts/export-hf-dataset.js +1 -5
  75. package/scripts/export-training.js +1 -5
  76. package/scripts/feedback-attribution.js +1 -16
  77. package/scripts/feedback-history-distiller.js +1 -16
  78. package/scripts/feedback-loop.js +1 -5
  79. package/scripts/feedback-root-consolidator.js +2 -21
  80. package/scripts/feedback-session.js +49 -0
  81. package/scripts/feedback-to-rules.js +188 -28
  82. package/scripts/filesystem-search.js +1 -9
  83. package/scripts/fs-utils.js +104 -0
  84. package/scripts/gates-engine.js +149 -4
  85. package/scripts/github-about.js +32 -8
  86. package/scripts/gtm-revenue-loop.js +1 -5
  87. package/scripts/harness-selector.js +148 -0
  88. package/scripts/hosted-job-launcher.js +1 -5
  89. package/scripts/hybrid-feedback-context.js +7 -33
  90. package/scripts/intervention-policy.js +58 -1
  91. package/scripts/lesson-db.js +3 -18
  92. package/scripts/lesson-inference.js +194 -16
  93. package/scripts/lesson-retrieval.js +60 -24
  94. package/scripts/llm-client.js +59 -0
  95. package/scripts/managed-lesson-agent.js +183 -0
  96. package/scripts/marketing-experiment.js +8 -22
  97. package/scripts/meta-agent-loop.js +624 -0
  98. package/scripts/metered-billing.js +1 -1
  99. package/scripts/money-watcher.js +1 -4
  100. package/scripts/obsidian-export.js +1 -5
  101. package/scripts/operational-integrity.js +15 -3
  102. package/scripts/org-dashboard.js +6 -1
  103. package/scripts/per-step-scoring.js +2 -4
  104. package/scripts/pr-manager.js +201 -19
  105. package/scripts/pro-features.js +3 -2
  106. package/scripts/prompt-dlp.js +3 -3
  107. package/scripts/prove-adapters.js +1 -5
  108. package/scripts/prove-attribution.js +1 -5
  109. package/scripts/prove-automation.js +1 -3
  110. package/scripts/prove-cloudflare-sandbox.js +1 -3
  111. package/scripts/prove-data-pipeline.js +1 -3
  112. package/scripts/prove-intelligence.js +1 -3
  113. package/scripts/prove-lancedb.js +1 -5
  114. package/scripts/prove-local-intelligence.js +1 -3
  115. package/scripts/prove-packaged-runtime.js +75 -9
  116. package/scripts/prove-predictive-insights.js +1 -3
  117. package/scripts/prove-training-export.js +1 -3
  118. package/scripts/prove-workflow-contract.js +1 -5
  119. package/scripts/rate-limiter.js +3 -1
  120. package/scripts/reddit-dm-outreach.js +14 -4
  121. package/scripts/schedule-manager.js +3 -5
  122. package/scripts/security-scanner.js +448 -0
  123. package/scripts/self-distill-agent.js +579 -0
  124. package/scripts/semantic-dedup.js +115 -0
  125. package/scripts/skill-exporter.js +1 -3
  126. package/scripts/skill-generator.js +1 -5
  127. package/scripts/social-analytics/engagement-audit.js +1 -18
  128. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  129. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  130. package/scripts/social-analytics/publishers/zernio.js +51 -0
  131. package/scripts/social-pipeline.js +1 -3
  132. package/scripts/social-post-hourly.js +47 -4
  133. package/scripts/statusline-links.js +6 -5
  134. package/scripts/statusline.sh +29 -153
  135. package/scripts/sync-branch-protection.js +340 -0
  136. package/scripts/tessl-export.js +1 -3
  137. package/scripts/thumbgate-search.js +32 -1
  138. package/scripts/tool-kpi-tracker.js +1 -1
  139. package/scripts/tool-registry.js +106 -2
  140. package/scripts/vector-store.js +1 -5
  141. package/scripts/weekly-auto-post.js +1 -1
  142. package/scripts/workflow-sentinel.js +91 -0
  143. package/skills/thumbgate/SKILL.md +1 -1
  144. package/src/api/server.js +273 -4
  145. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  146. /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
@@ -0,0 +1,624 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Meta-Agent Loop — Automated Harness Self-Improvement
6
+ *
7
+ * Inspired by the "Auto Agent" architecture:
8
+ * - Task Agent does the work; Meta Agent observes outcomes and rewrites the harness.
9
+ *
10
+ * This runner closes the self-improvement loop without human feedback:
11
+ *
12
+ * 1. Read gate-program.md for the domain's success definition
13
+ * 2. Pull recent failures from feedback-log.jsonl
14
+ * 3. Generate N candidate rule mutations via LLM (or heuristic fallback)
15
+ * 4. Evaluate each candidate by replaying it against the lesson DB:
16
+ * hit-rate = failures it would have caught / total failures
17
+ * fp-rate = successes it would have blocked / total successes
18
+ * score = hit-rate - (fp_weight * fp-rate)
19
+ * 5. Promote candidates whose score beats the current baseline
20
+ * 6. Revert (discard) candidates that regress
21
+ * 7. Write promoted rules to auto-promoted-gates.json + prevention-rules.md
22
+ * 8. Record results in evolution-state.json with a rollback snapshot
23
+ * 9. [optional] Run workspace evolution to auto-tune Thompson Sampling hyperparameters
24
+ * when >= EVOLVE_MIN_FAILURES failures are present (--evolve flag or THUMBGATE_META_EVOLVE=1)
25
+ *
26
+ * Runs autonomously at session end (Stop hook) or on demand:
27
+ * node scripts/meta-agent-loop.js
28
+ * node scripts/meta-agent-loop.js --dry-run
29
+ * node scripts/meta-agent-loop.js --status
30
+ * node scripts/meta-agent-loop.js --evolve (also runs workspace evolution)
31
+ */
32
+
33
+ const fs = require('fs');
34
+ const path = require('path');
35
+ const { resolveFeedbackDir } = require('./feedback-paths');
36
+ const { parseFeedbackFile, classifySignal, promoteToGates } = require('./feedback-to-rules');
37
+ const { loadAutoGates, saveAutoGates, getAutoGatesPath, patternToGateId } = require('./auto-promote-gates');
38
+ const { readEvolutionState, writeEvolutionState, captureEvolutionSnapshot, applyAcceptedMutation } = require('./evolution-state');
39
+ const { isAvailable, callClaude, MODELS } = require('./llm-client');
40
+ const { ensureParentDir } = require('./fs-utils');
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Constants
44
+ // ---------------------------------------------------------------------------
45
+
46
+ const GATE_PROGRAM_PATHS = [
47
+ path.join(process.cwd(), 'gate-program.md'),
48
+ path.join(process.cwd(), '..', 'gate-program.md'),
49
+ ];
50
+
51
+ const CANDIDATES_PER_RUN = 5;
52
+ const FP_WEIGHT = 2.0; // false positives penalised 2× vs true positives
53
+ const MIN_SCORE_THRESHOLD = 0.1; // candidate must score at least 0.1 to be promoted
54
+ const MAX_PROMOTED_PER_RUN = 3; // at most 3 new rules per overnight run
55
+ const RECENT_WINDOW_DAYS = 14; // look back 14 days for failures
56
+ const EVOLVE_MIN_FAILURES = 5; // minimum failures before workspace evolution runs
57
+
58
+ const META_RUNS_PATH = path.join(
59
+ require('os').homedir(),
60
+ '.thumbgate',
61
+ 'meta-agent-runs.jsonl'
62
+ );
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // 1. Read gate-program.md
66
+ // ---------------------------------------------------------------------------
67
+
68
+ function readGateProgram() {
69
+ for (const p of GATE_PROGRAM_PATHS) {
70
+ if (fs.existsSync(p)) {
71
+ return fs.readFileSync(p, 'utf-8');
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function extractSuccessDefinition(gateProgramText) {
78
+ if (!gateProgramText) return '';
79
+ const match = gateProgramText.match(/## Success Looks Like([\s\S]*?)(?=##|$)/);
80
+ return match ? match[1].trim() : '';
81
+ }
82
+
83
+ function extractBlockPatterns(gateProgramText) {
84
+ if (!gateProgramText) return [];
85
+ const match = gateProgramText.match(/## Patterns to Block[\s\S]*?\n([\s\S]*?)(?=##|$)/);
86
+ if (!match) return [];
87
+ return match[1]
88
+ .split('\n')
89
+ .filter((l) => /^\d+\./.test(l.trim()))
90
+ .map((l) => l.replace(/^\d+\.\s*\*\*[^*]+\*\*\s*—?\s*/, '').trim())
91
+ .filter(Boolean);
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // 2. Pull recent failures
96
+ // ---------------------------------------------------------------------------
97
+
98
+ function getRecentFailures(feedbackLogPath, windowDays = RECENT_WINDOW_DAYS) {
99
+ const entries = parseFeedbackFile(feedbackLogPath);
100
+ const cutoff = Date.now() - windowDays * 24 * 60 * 60 * 1000;
101
+
102
+ return entries.filter((e) => {
103
+ if (classifySignal(e) !== 'negative') return false;
104
+ const ts = e.timestamp ? new Date(e.timestamp).getTime() : 0;
105
+ return ts >= cutoff;
106
+ });
107
+ }
108
+
109
+ function getRecentSuccesses(feedbackLogPath, windowDays = RECENT_WINDOW_DAYS) {
110
+ const entries = parseFeedbackFile(feedbackLogPath);
111
+ const cutoff = Date.now() - windowDays * 24 * 60 * 60 * 1000;
112
+
113
+ return entries.filter((e) => {
114
+ if (classifySignal(e) !== 'positive') return false;
115
+ const ts = e.timestamp ? new Date(e.timestamp).getTime() : 0;
116
+ return ts >= cutoff;
117
+ });
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // 3. Candidate rule generation
122
+ // ---------------------------------------------------------------------------
123
+
124
+ const CANDIDATE_SYSTEM_PROMPT = `You are a meta-agent for ThumbGate, an AI coding agent safety system.
125
+
126
+ Your job: Given recent failure events and a domain success definition, generate
127
+ candidate prevention rules that would have caught these failures WITHOUT blocking
128
+ legitimate successful actions.
129
+
130
+ Return ONLY a JSON array of candidate rule objects (no markdown fences):
131
+ [
132
+ {
133
+ "pattern": "<JavaScript regex to match against tool call context/command>",
134
+ "action": "block" | "warn",
135
+ "message": "<why this is blocked/warned, shown to the agent>",
136
+ "severity": "critical" | "high" | "medium",
137
+ "rationale": "<why this rule would catch the failure pattern>"
138
+ }
139
+ ]
140
+
141
+ Rules:
142
+ - Pattern must be a valid JavaScript regex string (used with new RegExp(pattern, 'i'))
143
+ - Prefer specific patterns. "force.*push.*main" beats "push"
144
+ - Use "block" for destructive/irreversible actions, "warn" for review-needed
145
+ - Each rule should catch at least one of the listed failures
146
+ - Do NOT generate rules so broad they would block common, successful operations
147
+ - Max ${CANDIDATES_PER_RUN} candidates`;
148
+
149
+ async function generateCandidatesViaLLM(failures, successDef, blockPatterns) {
150
+ if (!isAvailable()) return null;
151
+
152
+ const failureBatch = failures
153
+ .slice(0, 20)
154
+ .map((e, i) => {
155
+ const ctx = (e.context || e.whatWentWrong || '').slice(0, 200);
156
+ const tags = (e.tags || []).join(', ');
157
+ return `${i + 1}. ${ctx}${tags ? ` [tags: ${tags}]` : ''}`;
158
+ })
159
+ .join('\n');
160
+
161
+ const userPrompt = [
162
+ `## Success Definition\n${successDef || '(not specified)'}`,
163
+ `## Known Block Patterns from gate-program.md\n${blockPatterns.map((p, i) => `${i + 1}. ${p}`).join('\n') || '(none)'}`,
164
+ `## Recent Failures (${failures.length} total, showing up to 20)\n${failureBatch || '(none)'}`,
165
+ `Generate ${CANDIDATES_PER_RUN} candidate prevention rules that would catch these failures.`,
166
+ ].join('\n\n');
167
+
168
+ const raw = await callClaude({
169
+ systemPrompt: CANDIDATE_SYSTEM_PROMPT,
170
+ userPrompt,
171
+ model: MODELS.FAST,
172
+ maxTokens: 1200,
173
+ });
174
+
175
+ if (!raw) return null;
176
+
177
+ try {
178
+ const parsed = JSON.parse(raw);
179
+ if (!Array.isArray(parsed)) return null;
180
+ return parsed
181
+ .filter((r) => r.pattern && r.action && r.message && r.severity)
182
+ .slice(0, CANDIDATES_PER_RUN);
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ function generateCandidatesHeuristic(failures, blockPatterns) {
189
+ // Fallback when no LLM is available: derive candidates from:
190
+ // (a) gate-program.md block patterns
191
+ // (b) top repeated failure contexts
192
+ const candidates = [];
193
+
194
+ // From gate-program.md block patterns
195
+ for (const pattern of blockPatterns.slice(0, 3)) {
196
+ const keywords = pattern
197
+ .toLowerCase()
198
+ .replace(/[^a-z0-9\s]/g, ' ')
199
+ .split(/\s+/)
200
+ .filter((w) => w.length > 4)
201
+ .slice(0, 3);
202
+ if (keywords.length >= 2) {
203
+ candidates.push({
204
+ pattern: keywords.join('.*'),
205
+ action: 'block',
206
+ message: `Blocked by gate-program.md rule: ${pattern.slice(0, 80)}`,
207
+ severity: 'high',
208
+ rationale: 'Derived from gate-program.md block pattern',
209
+ source: 'heuristic',
210
+ });
211
+ }
212
+ }
213
+
214
+ // From top repeated failure contexts
215
+ const ctxCounts = {};
216
+ for (const f of failures) {
217
+ const ctx = (f.context || f.whatWentWrong || '').trim();
218
+ if (ctx.length < 10) continue;
219
+ const key = ctx.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').slice(0, 80);
220
+ ctxCounts[key] = (ctxCounts[key] || 0) + 1;
221
+ }
222
+
223
+ const topContexts = Object.entries(ctxCounts)
224
+ .filter(([, c]) => c >= 2)
225
+ .sort((a, b) => b[1] - a[1])
226
+ .slice(0, 3);
227
+
228
+ for (const [ctx] of topContexts) {
229
+ const keywords = ctx.split(/\s+/).filter((w) => w.length > 4).slice(0, 3);
230
+ if (keywords.length >= 2) {
231
+ candidates.push({
232
+ pattern: keywords.join('.*'),
233
+ action: 'warn',
234
+ message: `Repeated failure pattern: ${ctx.slice(0, 80)}`,
235
+ severity: 'medium',
236
+ rationale: `Appeared ${ctxCounts[ctx]}× in recent failures`,
237
+ source: 'heuristic',
238
+ });
239
+ }
240
+ }
241
+
242
+ return candidates.slice(0, CANDIDATES_PER_RUN);
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // 4. Evaluate candidates
247
+ // ---------------------------------------------------------------------------
248
+
249
+ function matchesEntry(pattern, entry) {
250
+ try {
251
+ const re = new RegExp(pattern, 'i');
252
+ const text = [
253
+ entry.context,
254
+ entry.whatWentWrong,
255
+ entry.whatToChange,
256
+ (entry.tags || []).join(' '),
257
+ ].filter(Boolean).join(' ');
258
+ return re.test(text);
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+
264
+ function scoreCandidate(candidate, failures, successes) {
265
+ if (!failures.length && !successes.length) return { score: 0, hitRate: 0, fpRate: 0 };
266
+
267
+ const hits = failures.filter((f) => matchesEntry(candidate.pattern, f)).length;
268
+ const fps = successes.filter((s) => matchesEntry(candidate.pattern, s)).length;
269
+
270
+ const hitRate = failures.length > 0 ? hits / failures.length : 0;
271
+ const fpRate = successes.length > 0 ? fps / successes.length : 0;
272
+ const score = hitRate - FP_WEIGHT * fpRate;
273
+
274
+ return { score, hitRate, fpRate, hits, fps };
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // 5. Promote / revert
279
+ // ---------------------------------------------------------------------------
280
+
281
+ function buildPromotedGate(candidate, metrics, runId) {
282
+ return {
283
+ id: patternToGateId(`meta-${candidate.pattern}`),
284
+ pattern: candidate.pattern,
285
+ action: candidate.action,
286
+ message: candidate.message,
287
+ severity: candidate.severity,
288
+ occurrences: metrics.hits,
289
+ promotedAt: new Date().toISOString(),
290
+ source: 'meta-agent',
291
+ runId,
292
+ score: parseFloat(metrics.score.toFixed(3)),
293
+ hitRate: parseFloat(metrics.hitRate.toFixed(3)),
294
+ fpRate: parseFloat(metrics.fpRate.toFixed(3)),
295
+ rationale: candidate.rationale || '',
296
+ };
297
+ }
298
+
299
+ function writePreventionRulesFromGates(autoGatesData, rulesPath) {
300
+ const lines = [
301
+ '# Prevention Rules (Meta-Agent Generated)',
302
+ `# Updated: ${new Date().toISOString()}`,
303
+ '',
304
+ ];
305
+
306
+ for (const gate of autoGatesData.gates) {
307
+ const prefix = gate.action === 'block' ? '[BLOCK]' : '[WARN]';
308
+ lines.push(`- ${prefix} ${gate.message}`);
309
+ }
310
+
311
+ if (!autoGatesData.gates.length) {
312
+ lines.push('- No prevention rules active.');
313
+ }
314
+
315
+ const dir = path.dirname(rulesPath);
316
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
317
+ fs.writeFileSync(rulesPath, lines.join('\n') + '\n');
318
+ }
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // 6. Persistence
322
+ // ---------------------------------------------------------------------------
323
+
324
+
325
+ function appendRunManifest(manifest) {
326
+ ensureParentDir(META_RUNS_PATH);
327
+ fs.appendFileSync(META_RUNS_PATH, JSON.stringify(manifest) + '\n');
328
+ }
329
+
330
+ function readRunManifests() {
331
+ if (!fs.existsSync(META_RUNS_PATH)) return [];
332
+ const raw = fs.readFileSync(META_RUNS_PATH, 'utf-8').trim();
333
+ if (!raw) return [];
334
+ return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
335
+ }
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // 7. Main entry point
339
+ // ---------------------------------------------------------------------------
340
+
341
+ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
342
+ const feedbackDir = resolveFeedbackDir();
343
+ const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
344
+ const autoGatesPath = getAutoGatesPath();
345
+ const rulesPath = path.join(process.cwd(), '.thumbgate', 'prevention-rules.md');
346
+
347
+ const runId = `meta_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
348
+ const startedAt = new Date().toISOString();
349
+
350
+ // Step 1: Read gate-program.md
351
+ const gateProgramText = readGateProgram();
352
+ const successDef = extractSuccessDefinition(gateProgramText);
353
+ const blockPatterns = extractBlockPatterns(gateProgramText);
354
+
355
+ if (verbose) {
356
+ process.stdout.write(`[meta-agent] run=${runId}\n`);
357
+ process.stdout.write(`[meta-agent] gate-program.md ${gateProgramText ? 'found' : 'NOT FOUND — using heuristics only'}\n`);
358
+ process.stdout.write(`[meta-agent] block patterns from gate-program.md: ${blockPatterns.length}\n`);
359
+ }
360
+
361
+ // Step 2: Pull recent failures + successes
362
+ const failures = getRecentFailures(feedbackLogPath);
363
+ const successes = getRecentSuccesses(feedbackLogPath);
364
+
365
+ if (verbose) {
366
+ process.stdout.write(`[meta-agent] failures (${RECENT_WINDOW_DAYS}d): ${failures.length}, successes: ${successes.length}\n`);
367
+ }
368
+
369
+ // Step 3: Generate candidate rules
370
+ let candidates = null;
371
+ const analysisMode = isAvailable() ? 'llm' : 'heuristic';
372
+
373
+ if (isAvailable()) {
374
+ candidates = await generateCandidatesViaLLM(failures, successDef, blockPatterns);
375
+ }
376
+ if (!candidates || candidates.length === 0) {
377
+ candidates = generateCandidatesHeuristic(failures, blockPatterns);
378
+ }
379
+
380
+ if (verbose) {
381
+ process.stdout.write(`[meta-agent] candidates generated: ${candidates.length} (mode=${analysisMode})\n`);
382
+ }
383
+
384
+ // Step 4: Score each candidate
385
+ const evaluated = candidates.map((c) => ({
386
+ candidate: c,
387
+ metrics: scoreCandidate(c, failures, successes),
388
+ })).sort((a, b) => b.metrics.score - a.metrics.score);
389
+
390
+ // Step 5: Select promotions
391
+ const toPromote = evaluated
392
+ .filter((e) => e.metrics.score >= MIN_SCORE_THRESHOLD)
393
+ .slice(0, MAX_PROMOTED_PER_RUN);
394
+
395
+ const toRevert = evaluated.filter((e) => e.metrics.score < MIN_SCORE_THRESHOLD);
396
+
397
+ if (verbose) {
398
+ process.stdout.write(`[meta-agent] candidates above threshold: ${toPromote.length}, below: ${toRevert.length}\n`);
399
+ for (const { candidate, metrics } of evaluated) {
400
+ const mark = metrics.score >= MIN_SCORE_THRESHOLD ? 'KEEP' : 'REVERT';
401
+ process.stdout.write(
402
+ `[meta-agent] [${mark}] score=${metrics.score.toFixed(3)} hit=${metrics.hitRate.toFixed(2)} fp=${metrics.fpRate.toFixed(2)} — ${candidate.pattern}\n`
403
+ );
404
+ }
405
+ }
406
+
407
+ // Step 6: Persist promoted rules (unless dry-run)
408
+ const promotedGates = [];
409
+
410
+ if (!dryRun && toPromote.length > 0) {
411
+ // Snapshot before mutating
412
+ captureEvolutionSnapshot({
413
+ label: `meta-agent-pre-${runId}`,
414
+ reason: 'meta-agent-loop',
415
+ source: 'meta-agent-loop',
416
+ metadata: { runId, candidateCount: candidates.length, failureCount: failures.length },
417
+ });
418
+
419
+ const autoGatesData = loadAutoGates();
420
+
421
+ for (const { candidate, metrics } of toPromote) {
422
+ const gate = buildPromotedGate(candidate, metrics, runId);
423
+ // Avoid duplicates by id
424
+ const existingIdx = autoGatesData.gates.findIndex((g) => g.id === gate.id);
425
+ if (existingIdx !== -1) {
426
+ autoGatesData.gates[existingIdx] = { ...autoGatesData.gates[existingIdx], ...gate };
427
+ } else {
428
+ autoGatesData.gates.push(gate);
429
+ }
430
+ promotedGates.push(gate);
431
+ }
432
+
433
+ // Enforce max gates (10 free, rotate oldest)
434
+ const MAX_GATES = 10;
435
+ if (autoGatesData.gates.length > MAX_GATES) {
436
+ autoGatesData.gates = autoGatesData.gates.slice(-MAX_GATES);
437
+ }
438
+
439
+ saveAutoGates(autoGatesData);
440
+ writePreventionRulesFromGates(autoGatesData, rulesPath);
441
+
442
+ // Record in evolution-state
443
+ const state = readEvolutionState();
444
+ writeEvolutionState({
445
+ ...state,
446
+ settings: {
447
+ ...state.settings,
448
+ last_meta_agent_run: runId,
449
+ last_meta_agent_at: startedAt,
450
+ meta_agent_total_promoted: (state.settings.meta_agent_total_promoted || 0) + toPromote.length,
451
+ },
452
+ });
453
+ }
454
+
455
+ // Step 9: Workspace evolution — auto-tune Thompson Sampling hyperparameters.
456
+ // Runs when: not dry-run, --evolve flag or THUMBGATE_META_EVOLVE=1, and
457
+ // enough failure signal exists (>= EVOLVE_MIN_FAILURES).
458
+ let evolutionResult = null;
459
+ const shouldEvolve = !dryRun && (process.env.THUMBGATE_META_EVOLVE === '1');
460
+ if (shouldEvolve && failures.length >= EVOLVE_MIN_FAILURES) {
461
+ try {
462
+ const { runWorkspaceEvolution, recommendEvolutionTarget } = require('./workspace-evolver');
463
+ const failureTags = failures.flatMap((f) => f.tags || []);
464
+ const dominantFailureType = toRevert.length > toPromote.length ? 'decision' : 'execution';
465
+ const targetName = recommendEvolutionTarget({ failureType: dominantFailureType, tags: failureTags });
466
+
467
+ if (verbose) {
468
+ process.stdout.write(`[meta-agent] running workspace evolution: target=${targetName}\n`);
469
+ }
470
+
471
+ evolutionResult = runWorkspaceEvolution({
472
+ targetName,
473
+ primaryCommands: ['node --test tests/meta-agent-loop.test.js'],
474
+ timeoutMs: 30000,
475
+ });
476
+
477
+ if (verbose) {
478
+ const status = evolutionResult.skipped ? 'skipped' : (evolutionResult.kept ? 'kept' : 'reverted');
479
+ process.stdout.write(`[meta-agent] evolution: target=${targetName} status=${status}\n`);
480
+ if (!evolutionResult.skipped) {
481
+ process.stdout.write(`[meta-agent] evolution: ${evolutionResult.currentValue} → ${evolutionResult.nextValue} (kept=${evolutionResult.kept})\n`);
482
+ }
483
+ }
484
+ } catch (err) {
485
+ if (verbose) process.stdout.write(`[meta-agent] workspace evolution failed (non-fatal): ${err.message}\n`);
486
+ }
487
+ }
488
+
489
+ const completedAt = new Date().toISOString();
490
+ const manifest = {
491
+ runId,
492
+ startedAt,
493
+ completedAt,
494
+ dryRun,
495
+ analysisMode,
496
+ gateProgramFound: Boolean(gateProgramText),
497
+ failureCount: failures.length,
498
+ successCount: successes.length,
499
+ candidateCount: candidates.length,
500
+ promotedCount: toPromote.length,
501
+ revertedCount: toRevert.length,
502
+ promoted: promotedGates.map((g) => ({ id: g.id, action: g.action, score: g.score, pattern: g.pattern })),
503
+ reverted: toRevert.map(({ candidate, metrics }) => ({
504
+ pattern: candidate.pattern,
505
+ score: parseFloat(metrics.score.toFixed(3)),
506
+ })),
507
+ evolution: evolutionResult
508
+ ? {
509
+ target: evolutionResult.target?.name,
510
+ from: evolutionResult.currentValue,
511
+ to: evolutionResult.nextValue,
512
+ kept: evolutionResult.kept,
513
+ skipped: evolutionResult.skipped || false,
514
+ }
515
+ : null,
516
+ };
517
+
518
+ if (!dryRun) {
519
+ appendRunManifest(manifest);
520
+ }
521
+
522
+ return manifest;
523
+ }
524
+
525
+ // ---------------------------------------------------------------------------
526
+ // 8. Status
527
+ // ---------------------------------------------------------------------------
528
+
529
+ function getMetaAgentStatus() {
530
+ const runs = readRunManifests();
531
+ if (runs.length === 0) return null;
532
+ const last = runs[runs.length - 1];
533
+ return {
534
+ totalRuns: runs.length,
535
+ lastRunId: last.runId,
536
+ lastRunAt: last.completedAt,
537
+ lastAnalysisMode: last.analysisMode,
538
+ lastFailureCount: last.failureCount,
539
+ lastCandidateCount: last.candidateCount,
540
+ lastPromotedCount: last.promotedCount,
541
+ lastRevertedCount: last.revertedCount,
542
+ totalPromoted: runs.reduce((s, r) => s + (r.promotedCount || 0), 0),
543
+ };
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // 9. CLI
548
+ // ---------------------------------------------------------------------------
549
+
550
+ async function main() {
551
+ const args = process.argv.slice(2);
552
+ const dryRun = args.includes('--dry-run');
553
+ const verbose = args.includes('--verbose') || args.includes('-v');
554
+
555
+ if (args.includes('--status')) {
556
+ const status = getMetaAgentStatus();
557
+ if (!status) {
558
+ console.log('No meta-agent runs recorded yet.');
559
+ } else {
560
+ console.log(JSON.stringify(status, null, 2));
561
+ }
562
+ return;
563
+ }
564
+
565
+ const mode = dryRun ? 'DRY RUN' : 'LIVE';
566
+ console.log(`Meta-agent loop starting [${mode}]...`);
567
+
568
+ const manifest = await runMetaAgentLoop({ dryRun, verbose: verbose || true });
569
+
570
+ console.log(`Run ID : ${manifest.runId}`);
571
+ console.log(`Analysis mode : ${manifest.analysisMode}`);
572
+ console.log(`Gate program : ${manifest.gateProgramFound ? 'found' : 'not found'}`);
573
+ console.log(`Failures (${RECENT_WINDOW_DAYS}d): ${manifest.failureCount}`);
574
+ console.log(`Candidates : ${manifest.candidateCount}`);
575
+ console.log(`Promoted : ${manifest.promotedCount}`);
576
+ console.log(`Reverted : ${manifest.revertedCount}`);
577
+
578
+ if (manifest.promoted.length > 0) {
579
+ console.log('\nPromoted rules:');
580
+ for (const g of manifest.promoted) {
581
+ console.log(` [${g.action.toUpperCase()}] score=${g.score} — ${g.pattern}`);
582
+ }
583
+ }
584
+
585
+ if (manifest.reverted.length > 0 && verbose) {
586
+ console.log('\nReverted (below threshold):');
587
+ for (const r of manifest.reverted) {
588
+ console.log(` score=${r.score} — ${r.pattern}`);
589
+ }
590
+ }
591
+
592
+ if (dryRun) {
593
+ console.log('\n[DRY RUN] No rules written.');
594
+ }
595
+ }
596
+
597
+ if (require.main === module) {
598
+ main().catch((err) => {
599
+ console.error('Meta-agent loop failed:', err.message);
600
+ process.exitCode = 1;
601
+ });
602
+ }
603
+
604
+ module.exports = {
605
+ runMetaAgentLoop,
606
+ getMetaAgentStatus,
607
+ readGateProgram,
608
+ extractSuccessDefinition,
609
+ extractBlockPatterns,
610
+ getRecentFailures,
611
+ getRecentSuccesses,
612
+ generateCandidatesHeuristic,
613
+ scoreCandidate,
614
+ buildPromotedGate,
615
+ writePreventionRulesFromGates,
616
+ appendRunManifest,
617
+ readRunManifests,
618
+ matchesEntry,
619
+ META_RUNS_PATH,
620
+ CANDIDATES_PER_RUN,
621
+ MIN_SCORE_THRESHOLD,
622
+ FP_WEIGHT,
623
+ EVOLVE_MIN_FAILURES,
624
+ };
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { resolveFeedbackDir } = require('./feedback-paths');
6
+ const { readJsonl } = require('./fs-utils');
6
7
  const METERED_RATE_PRO = 0.10;
7
8
  const METERED_RATE_TEAM = 0.08;
8
9
  const MINUTES_SAVED_PER_BLOCK = 16;
@@ -10,7 +11,6 @@ const PRO_FLOOR = 19;
10
11
  const TEAM_FLOOR_PER_SEAT = 12;
11
12
  const TEAM_MIN_SEATS = 3;
12
13
  function getMeteredLedgerPath() { return path.join(resolveFeedbackDir(), 'metered-usage.jsonl'); }
13
- function readJsonl(fp) { if (!fs.existsSync(fp)) return []; const r = fs.readFileSync(fp, 'utf-8').trim(); if (!r) return []; return r.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
14
14
  function recordMeteredUsage({ agentId, gateId, decision, toolName } = {}) { const lp = getMeteredLedgerPath(); const dir = path.dirname(lp); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const e = { id: `meter_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, timestamp: new Date().toISOString(), agentId: agentId || 'unknown', gateId: gateId || 'unknown', decision: decision || 'deny', toolName: toolName || 'unknown' }; fs.appendFileSync(lp, JSON.stringify(e) + '\n'); return e; }
15
15
  function getMeteredUsageSummary({ periodDays = 30, seats = 1, plan = 'pro' } = {}) { const entries = readJsonl(getMeteredLedgerPath()); const cutoff = Date.now() - periodDays * 24 * 60 * 60 * 1000; const pe = entries.filter((e) => new Date(e.timestamp).getTime() > cutoff); const bc = pe.filter((e) => e.decision === 'deny').length; const wc = pe.filter((e) => e.decision === 'warn').length; const rate = plan === 'team' ? METERED_RATE_TEAM : METERED_RATE_PRO; const es = Math.max(TEAM_MIN_SEATS, seats); const floor = plan === 'team' ? TEAM_FLOOR_PER_SEAT * es : PRO_FLOOR; const raw = bc * rate * (plan === 'team' ? seats : 1); const billed = Math.max(floor, raw); const ms = bc * MINUTES_SAVED_PER_BLOCK; return { periodDays, plan, seats, blockedCount: bc, warnedCount: wc, totalEvents: pe.length, rate, floor, rawCost: Math.round(raw * 100) / 100, billedAmount: Math.round(billed * 100) / 100, minutesSaved: ms, hoursSaved: Math.round(ms / 60 * 10) / 10, periodStart: new Date(cutoff).toISOString(), periodEnd: new Date().toISOString() }; }
16
16
  module.exports = { METERED_RATE_PRO, METERED_RATE_TEAM, MINUTES_SAVED_PER_BLOCK, PRO_FLOOR, TEAM_FLOOR_PER_SEAT, TEAM_MIN_SEATS, recordMeteredUsage, getMeteredUsageSummary, getMeteredLedgerPath };
@@ -9,14 +9,11 @@
9
9
  const fs = require('node:fs');
10
10
  const path = require('node:path');
11
11
  const { getOperationalBillingSummary } = require('./operational-summary');
12
+ const { ensureParentDir } = require('./fs-utils');
12
13
 
13
14
  const DEFAULT_STATE_PATH = path.resolve(__dirname, '..', '.thumbgate', 'commercial-watch-state.json');
14
15
  const DEFAULT_ALERT_LOG_PATH = path.resolve(__dirname, '..', '.thumbgate', 'commercial-alerts.jsonl');
15
16
 
16
- function ensureParentDir(filePath) {
17
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
18
- }
19
-
20
17
  function getCommercialRevenueSnapshot(summary = {}) {
21
18
  const revenue = summary && typeof summary === 'object' ? summary.revenue || {} : {};
22
19
  return {
@@ -2,16 +2,12 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { ensureDir } = require('./fs-utils');
5
6
 
6
7
  // ---------------------------------------------------------------------------
7
8
  // Helpers
8
9
  // ---------------------------------------------------------------------------
9
10
 
10
- function ensureDir(dirPath) {
11
- if (!fs.existsSync(dirPath)) {
12
- fs.mkdirSync(dirPath, { recursive: true });
13
- }
14
- }
15
11
 
16
12
  function readJSONL(filePath) {
17
13
  if (!fs.existsSync(filePath)) return [];