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.
- package/.claude-plugin/marketplace.json +32 -13
- package/.claude-plugin/plugin.json +15 -2
- package/.well-known/llms.txt +60 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +109 -20
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/openapi.yaml +168 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +84 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +200 -13
- package/bin/postinstall.js +8 -2
- package/config/budget.json +18 -0
- package/config/gates/code-edit.json +61 -0
- package/config/gates/db-write.json +61 -0
- package/config/gates/default.json +154 -3
- package/config/gates/deploy.json +61 -0
- package/config/github-about.json +2 -1
- package/config/merge-quality-checks.json +23 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +42 -10
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +27 -4
- package/plugins/codex-profile/README.md +33 -9
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +73 -0
- package/public/compare/mem0.html +189 -0
- package/public/compare/speclock.html +180 -0
- package/public/compare.html +10 -2
- package/public/guide.html +2 -2
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/codex-cli-guardrails.html +158 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/pre-action-gates.html +162 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
- package/public/index.html +136 -50
- package/public/lessons.html +33 -24
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/access-anomaly-detector.js +1 -1
- package/scripts/adk-consolidator.js +1 -5
- package/scripts/agent-security-hardening.js +4 -6
- package/scripts/agentic-data-pipeline.js +1 -3
- package/scripts/async-job-runner.js +1 -5
- package/scripts/audit-trail.js +1 -5
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/check-congruence.js +132 -14
- package/scripts/commercial-offer.js +5 -7
- package/scripts/content-engine/linkedin-content-generator.js +154 -0
- package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
- package/scripts/content-engine/reddit-thread-finder.js +154 -0
- package/scripts/context-engine.js +21 -6
- package/scripts/contextfs.js +1 -21
- package/scripts/dashboard.js +20 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +26 -0
- package/scripts/document-intake.js +927 -0
- package/scripts/ephemeral-agent-store.js +1 -8
- package/scripts/evolution-state.js +1 -5
- package/scripts/experiment-tracker.js +1 -5
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-hf-dataset.js +1 -5
- package/scripts/export-training.js +1 -5
- package/scripts/feedback-attribution.js +1 -16
- package/scripts/feedback-history-distiller.js +1 -16
- package/scripts/feedback-loop.js +1 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +188 -28
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +149 -4
- package/scripts/github-about.js +32 -8
- package/scripts/gtm-revenue-loop.js +1 -5
- package/scripts/harness-selector.js +148 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +7 -33
- package/scripts/intervention-policy.js +58 -1
- package/scripts/lesson-db.js +3 -18
- package/scripts/lesson-inference.js +194 -16
- package/scripts/lesson-retrieval.js +60 -24
- package/scripts/llm-client.js +59 -0
- package/scripts/managed-lesson-agent.js +183 -0
- package/scripts/marketing-experiment.js +8 -22
- package/scripts/meta-agent-loop.js +624 -0
- package/scripts/metered-billing.js +1 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +15 -3
- package/scripts/org-dashboard.js +6 -1
- package/scripts/per-step-scoring.js +2 -4
- package/scripts/pr-manager.js +201 -19
- package/scripts/pro-features.js +3 -2
- package/scripts/prompt-dlp.js +3 -3
- package/scripts/prove-adapters.js +1 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +1 -3
- package/scripts/prove-cloudflare-sandbox.js +1 -3
- package/scripts/prove-data-pipeline.js +1 -3
- package/scripts/prove-intelligence.js +1 -3
- package/scripts/prove-lancedb.js +1 -5
- package/scripts/prove-local-intelligence.js +1 -3
- package/scripts/prove-packaged-runtime.js +75 -9
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/rate-limiter.js +3 -1
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/schedule-manager.js +3 -5
- package/scripts/security-scanner.js +448 -0
- package/scripts/self-distill-agent.js +579 -0
- package/scripts/semantic-dedup.js +115 -0
- package/scripts/skill-exporter.js +1 -3
- package/scripts/skill-generator.js +1 -5
- package/scripts/social-analytics/engagement-audit.js +1 -18
- package/scripts/social-analytics/pollers/linkedin.js +26 -16
- package/scripts/social-analytics/publishers/linkedin.js +1 -1
- package/scripts/social-analytics/publishers/zernio.js +51 -0
- package/scripts/social-pipeline.js +1 -3
- package/scripts/social-post-hourly.js +47 -4
- package/scripts/statusline-links.js +6 -5
- package/scripts/statusline.sh +29 -153
- package/scripts/sync-branch-protection.js +340 -0
- package/scripts/tessl-export.js +1 -3
- package/scripts/thumbgate-search.js +32 -1
- package/scripts/tool-kpi-tracker.js +1 -1
- package/scripts/tool-registry.js +106 -2
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +91 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +273 -4
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- /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 };
|
package/scripts/money-watcher.js
CHANGED
|
@@ -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 [];
|