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.
- package/.claude-plugin/README.md +4 -4
- 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 +133 -23
- 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 +85 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +215 -19
- 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/config/model-tiers.json +11 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +47 -13
- 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/cursor-marketplace/README.md +2 -2
- package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
- 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 +12 -4
- package/public/guide.html +5 -5
- 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 +169 -70
- package/public/learn/ai-agent-persistent-memory.html +1 -0
- package/public/lessons.html +334 -17
- 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 +7 -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/capture-railway-diagnostics.sh +97 -0
- package/scripts/check-congruence.js +133 -15
- package/scripts/claude-feedback-sync.js +320 -0
- package/scripts/cli-telemetry.js +4 -1
- 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 +33 -44
- package/scripts/dashboard.js +104 -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 +17 -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 +753 -0
- 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/local-model-profile.js +18 -2
- 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/model-tier-router.js +10 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +369 -34
- 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 +2 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +3 -5
- 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 +326 -0
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-runtime.js +13 -0
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/rate-limiter.js +6 -4
- 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-local-stats.js +2 -0
- package/scripts/statusline.sh +38 -7
- 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 +108 -4
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +205 -4
- package/skills/thumbgate/SKILL.md +2 -2
- 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,340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
const MERGE_QUALITY_CHECKS = require('../config/merge-quality-checks.json');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_REPO = process.env.GITHUB_REPOSITORY || 'IgorGanapolsky/ThumbGate';
|
|
10
|
+
const DEFAULT_BRANCH = process.env.DEFAULT_BRANCH || 'main';
|
|
11
|
+
const FIXED_GH_BINARIES = [
|
|
12
|
+
'/usr/bin/gh',
|
|
13
|
+
'/usr/local/bin/gh',
|
|
14
|
+
'/opt/homebrew/bin/gh',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function assertSafeGhArgs(args) {
|
|
18
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
19
|
+
throw new Error('GH CLI args must be a non-empty array.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return args.map((arg) => {
|
|
23
|
+
const normalized = String(arg ?? '');
|
|
24
|
+
if (!normalized || /\0/.test(normalized)) {
|
|
25
|
+
throw new Error(`Unsafe GH CLI arg: ${arg}`);
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveGhBinary(options = {}) {
|
|
32
|
+
const accessSync = options.accessSync || fs.accessSync;
|
|
33
|
+
const candidates = [];
|
|
34
|
+
const configuredBinary = String(process.env.THUMBGATE_GH_BIN || '').trim();
|
|
35
|
+
|
|
36
|
+
if (configuredBinary) {
|
|
37
|
+
if (!path.isAbsolute(configuredBinary)) {
|
|
38
|
+
throw new Error(`Unsafe GH binary path: ${configuredBinary}`);
|
|
39
|
+
}
|
|
40
|
+
candidates.push(configuredBinary);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
candidates.push(...FIXED_GH_BINARIES);
|
|
44
|
+
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
try {
|
|
47
|
+
accessSync(candidate, fs.constants.X_OK);
|
|
48
|
+
return candidate;
|
|
49
|
+
} catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new Error(`Unable to locate GH CLI in fixed paths: ${candidates.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function runGh(args, options = {}) {
|
|
58
|
+
const env = { ...process.env };
|
|
59
|
+
if (!env.GITHUB_ACTIONS && env.GITHUB_TOKEN && !env.GH_TOKEN) {
|
|
60
|
+
delete env.GITHUB_TOKEN;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return spawnSync(resolveGhBinary(options), assertSafeGhArgs(args), {
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
env,
|
|
66
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatGhError(result) {
|
|
71
|
+
return (result.stderr || result.stdout || 'Unknown GH CLI failure').trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
75
|
+
const options = {
|
|
76
|
+
check: false,
|
|
77
|
+
json: false,
|
|
78
|
+
repo: DEFAULT_REPO,
|
|
79
|
+
branch: DEFAULT_BRANCH,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
83
|
+
const arg = argv[index];
|
|
84
|
+
if (arg === '--check') {
|
|
85
|
+
options.check = true;
|
|
86
|
+
} else if (arg === '--json') {
|
|
87
|
+
options.json = true;
|
|
88
|
+
} else if (arg === '--repo' && argv[index + 1]) {
|
|
89
|
+
options.repo = argv[index + 1];
|
|
90
|
+
index += 1;
|
|
91
|
+
} else if (arg === '--branch' && argv[index + 1]) {
|
|
92
|
+
options.branch = argv[index + 1];
|
|
93
|
+
index += 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return options;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function assertSafeRepoSegment(value, label) {
|
|
101
|
+
const normalized = String(value || '').trim();
|
|
102
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(normalized)) {
|
|
103
|
+
throw new Error(`Unsafe repository ${label}: ${value}`);
|
|
104
|
+
}
|
|
105
|
+
return normalized;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function splitRepo(repo) {
|
|
109
|
+
const [owner, name] = String(repo || '').trim().split('/');
|
|
110
|
+
if (!owner || !name) {
|
|
111
|
+
throw new Error(`Invalid repository "${repo}". Expected owner/name.`);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
owner: assertSafeRepoSegment(owner, 'owner'),
|
|
115
|
+
name: assertSafeRepoSegment(name, 'name'),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function assertSafeBranchPattern(branch) {
|
|
120
|
+
const normalized = String(branch || '').trim();
|
|
121
|
+
if (!normalized) {
|
|
122
|
+
throw new Error('Branch pattern is required.');
|
|
123
|
+
}
|
|
124
|
+
if (normalized.startsWith('-') || normalized.includes('..') || normalized.includes('//') || normalized.includes('@{')) {
|
|
125
|
+
throw new Error(`Unsafe branch pattern: ${branch}`);
|
|
126
|
+
}
|
|
127
|
+
if (normalized.endsWith('.') || normalized.endsWith('/')) {
|
|
128
|
+
throw new Error(`Unsafe branch pattern: ${branch}`);
|
|
129
|
+
}
|
|
130
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(normalized)) {
|
|
131
|
+
throw new Error(`Unsafe branch pattern: ${branch}`);
|
|
132
|
+
}
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertSafeRuleId(ruleId) {
|
|
137
|
+
const normalized = String(ruleId || '').trim();
|
|
138
|
+
if (!/^[A-Za-z0-9_=-]+$/.test(normalized)) {
|
|
139
|
+
throw new Error(`Unsafe branch protection rule id: ${ruleId}`);
|
|
140
|
+
}
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function assertSafeStatusContext(context) {
|
|
145
|
+
const normalized = String(context || '').trim();
|
|
146
|
+
if (!normalized || /[\0\r\n]/.test(normalized)) {
|
|
147
|
+
throw new Error(`Unsafe status check context: ${context}`);
|
|
148
|
+
}
|
|
149
|
+
return normalized;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeContexts(contexts = []) {
|
|
153
|
+
return [...new Set((Array.isArray(contexts) ? contexts : []).map((value) => {
|
|
154
|
+
const normalized = String(value || '').trim();
|
|
155
|
+
return normalized ? assertSafeStatusContext(normalized) : '';
|
|
156
|
+
}).filter(Boolean))].sort((left, right) => left.localeCompare(right));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function loadBranchProtectionRule(repo, runner = runGh) {
|
|
160
|
+
const { owner, name } = splitRepo(repo);
|
|
161
|
+
const query = `
|
|
162
|
+
query BranchProtectionRules($owner: String!, $name: String!) {
|
|
163
|
+
repository(owner: $owner, name: $name) {
|
|
164
|
+
branchProtectionRules(first: 50) {
|
|
165
|
+
nodes {
|
|
166
|
+
id
|
|
167
|
+
pattern
|
|
168
|
+
requiresStatusChecks
|
|
169
|
+
requiredStatusCheckContexts
|
|
170
|
+
requiresApprovingReviews
|
|
171
|
+
requiresConversationResolution
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
const result = runner([
|
|
179
|
+
'api',
|
|
180
|
+
'graphql',
|
|
181
|
+
'-f',
|
|
182
|
+
`query=${query}`,
|
|
183
|
+
'-f',
|
|
184
|
+
`owner=${owner}`,
|
|
185
|
+
'-f',
|
|
186
|
+
`name=${name}`,
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
if (result.status !== 0) {
|
|
190
|
+
throw new Error(`Failed to load branch protection: ${formatGhError(result)}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const payload = JSON.parse(result.stdout || '{}');
|
|
194
|
+
return payload.data?.repository?.branchProtectionRules?.nodes || [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function findBranchProtectionRule(rules, branch) {
|
|
198
|
+
return (Array.isArray(rules) ? rules : []).find((rule) => rule.pattern === branch) || null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function diffContexts(actual, expected) {
|
|
202
|
+
const actualSet = new Set(normalizeContexts(actual));
|
|
203
|
+
const expectedSet = new Set(normalizeContexts(expected));
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
missing: [...expectedSet].filter((value) => !actualSet.has(value)),
|
|
207
|
+
unexpected: [...actualSet].filter((value) => !expectedSet.has(value)),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function updateBranchProtectionRule(ruleId, requiredStatusCheckContexts, runner = runGh) {
|
|
212
|
+
const safeRuleId = assertSafeRuleId(ruleId);
|
|
213
|
+
const contexts = normalizeContexts(requiredStatusCheckContexts);
|
|
214
|
+
const mutation = `
|
|
215
|
+
mutation UpdateBranchProtectionRule($ruleId: ID!, $contexts: [String!]) {
|
|
216
|
+
updateBranchProtectionRule(input: {
|
|
217
|
+
branchProtectionRuleId: $ruleId
|
|
218
|
+
requiresStatusChecks: true
|
|
219
|
+
requiredStatusCheckContexts: $contexts
|
|
220
|
+
}) {
|
|
221
|
+
branchProtectionRule {
|
|
222
|
+
id
|
|
223
|
+
pattern
|
|
224
|
+
requiresStatusChecks
|
|
225
|
+
requiredStatusCheckContexts
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
`;
|
|
230
|
+
|
|
231
|
+
const args = [
|
|
232
|
+
'api',
|
|
233
|
+
'graphql',
|
|
234
|
+
'-f',
|
|
235
|
+
`query=${mutation}`,
|
|
236
|
+
'-f',
|
|
237
|
+
`ruleId=${safeRuleId}`,
|
|
238
|
+
];
|
|
239
|
+
for (const context of contexts) {
|
|
240
|
+
args.push('-F', `contexts[]=${context}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const result = runner(args);
|
|
244
|
+
|
|
245
|
+
if (result.status !== 0) {
|
|
246
|
+
throw new Error(`Failed to update branch protection: ${formatGhError(result)}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return JSON.parse(result.stdout || '{}').data?.updateBranchProtectionRule?.branchProtectionRule || null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function syncBranchProtection(options = {}, runner = runGh) {
|
|
253
|
+
const repo = options.repo || DEFAULT_REPO;
|
|
254
|
+
const branch = assertSafeBranchPattern(options.branch || DEFAULT_BRANCH);
|
|
255
|
+
const expectedContexts = normalizeContexts(MERGE_QUALITY_CHECKS.requiredStatusCheckContexts);
|
|
256
|
+
const rules = loadBranchProtectionRule(repo, runner);
|
|
257
|
+
const rule = findBranchProtectionRule(rules, branch);
|
|
258
|
+
|
|
259
|
+
if (!rule) {
|
|
260
|
+
throw new Error(`No branch protection rule found for ${repo}#${branch}.`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const actualContexts = normalizeContexts(rule.requiredStatusCheckContexts);
|
|
264
|
+
const diff = diffContexts(actualContexts, expectedContexts);
|
|
265
|
+
const inSync = diff.missing.length === 0 && diff.unexpected.length === 0 && rule.requiresStatusChecks === true;
|
|
266
|
+
|
|
267
|
+
if (options.check) {
|
|
268
|
+
return {
|
|
269
|
+
ok: inSync,
|
|
270
|
+
repo,
|
|
271
|
+
branch,
|
|
272
|
+
ruleId: rule.id,
|
|
273
|
+
actualContexts,
|
|
274
|
+
expectedContexts,
|
|
275
|
+
diff,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const updatedRule = inSync
|
|
280
|
+
? rule
|
|
281
|
+
: updateBranchProtectionRule(rule.id, expectedContexts, runner);
|
|
282
|
+
const finalContexts = normalizeContexts(updatedRule.requiredStatusCheckContexts);
|
|
283
|
+
const finalDiff = diffContexts(finalContexts, expectedContexts);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
ok: true,
|
|
287
|
+
repo,
|
|
288
|
+
branch,
|
|
289
|
+
ruleId: rule.id,
|
|
290
|
+
actualContexts: finalContexts,
|
|
291
|
+
expectedContexts,
|
|
292
|
+
diff: finalDiff,
|
|
293
|
+
updated: !inSync,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function runCli(argv = process.argv.slice(2), runner = runGh) {
|
|
298
|
+
const options = parseArgs(argv);
|
|
299
|
+
const result = syncBranchProtection(options, runner);
|
|
300
|
+
|
|
301
|
+
if (options.json) {
|
|
302
|
+
console.log(JSON.stringify(result, null, 2));
|
|
303
|
+
} else if (options.check) {
|
|
304
|
+
const status = result.ok ? 'ok' : 'drift';
|
|
305
|
+
console.log(`Branch protection ${status}: ${result.repo} ${result.branch}`);
|
|
306
|
+
if (!result.ok) {
|
|
307
|
+
if (result.diff.missing.length > 0) {
|
|
308
|
+
console.log(`Missing contexts: ${result.diff.missing.join(', ')}`);
|
|
309
|
+
}
|
|
310
|
+
if (result.diff.unexpected.length > 0) {
|
|
311
|
+
console.log(`Unexpected contexts: ${result.diff.unexpected.join(', ')}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
console.log(`Branch protection synced: ${result.repo} ${result.branch}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return options.check ? (result.ok ? 0 : 1) : 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
|
|
322
|
+
process.exitCode = runCli();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
assertSafeBranchPattern,
|
|
327
|
+
assertSafeGhArgs,
|
|
328
|
+
assertSafeRuleId,
|
|
329
|
+
assertSafeStatusContext,
|
|
330
|
+
diffContexts,
|
|
331
|
+
findBranchProtectionRule,
|
|
332
|
+
loadBranchProtectionRule,
|
|
333
|
+
normalizeContexts,
|
|
334
|
+
parseArgs,
|
|
335
|
+
resolveGhBinary,
|
|
336
|
+
runCli,
|
|
337
|
+
splitRepo,
|
|
338
|
+
syncBranchProtection,
|
|
339
|
+
updateBranchProtectionRule,
|
|
340
|
+
};
|
package/scripts/tessl-export.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
+
const { ensureDir } = require('./fs-utils');
|
|
7
8
|
|
|
8
9
|
const ROOT = path.join(__dirname, '..');
|
|
9
10
|
const DEFAULT_CONFIG_PATH = path.join(ROOT, 'config', 'tessl-tiles.json');
|
|
@@ -14,9 +15,6 @@ function readJson(filePath) {
|
|
|
14
15
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
function ensureDir(dirPath) {
|
|
18
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
19
|
-
}
|
|
20
18
|
|
|
21
19
|
function cleanDir(dirPath) {
|
|
22
20
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
@@ -6,8 +6,11 @@ const {
|
|
|
6
6
|
searchContextFs,
|
|
7
7
|
searchPreventionRulesSync,
|
|
8
8
|
} = require('./filesystem-search');
|
|
9
|
+
const {
|
|
10
|
+
searchImportedDocuments,
|
|
11
|
+
} = require('./document-intake');
|
|
9
12
|
|
|
10
|
-
const VALID_SOURCES = ['all', 'feedback', 'context', 'rules'];
|
|
13
|
+
const VALID_SOURCES = ['all', 'feedback', 'context', 'rules', 'documents'];
|
|
11
14
|
const SIGNAL_ALIASES = {
|
|
12
15
|
up: 'up',
|
|
13
16
|
positive: 'up',
|
|
@@ -118,6 +121,27 @@ function mapRuleResult(record) {
|
|
|
118
121
|
};
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
function mapDocumentResult(record) {
|
|
125
|
+
return {
|
|
126
|
+
id: record.documentId || null,
|
|
127
|
+
source: 'document',
|
|
128
|
+
score: clampScore(record._score),
|
|
129
|
+
signal: null,
|
|
130
|
+
tags: safeArray(record.tags),
|
|
131
|
+
timestamp: record.importedAt || null,
|
|
132
|
+
title: record.title || null,
|
|
133
|
+
context: excerpt(record.excerpt || record.content || ''),
|
|
134
|
+
correctiveAction: safeArray(record.proposals)[0]
|
|
135
|
+
? safeArray(record.proposals)[0].title || safeArray(record.proposals)[0].evidence || null
|
|
136
|
+
: null,
|
|
137
|
+
matchedTokens: safeArray(record._matchedTokens),
|
|
138
|
+
documentId: record.documentId || null,
|
|
139
|
+
proposalCount: safeArray(record.proposals).length,
|
|
140
|
+
matchedTemplateIds: safeArray(record.matchedTemplateIds),
|
|
141
|
+
sourceFormat: record.sourceFormat || null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
121
145
|
function sortResults(results) {
|
|
122
146
|
return [...results].sort((left, right) => {
|
|
123
147
|
if ((right.score || 0) !== (left.score || 0)) {
|
|
@@ -144,6 +168,10 @@ function getRuleResults(query, limit) {
|
|
|
144
168
|
return searchPreventionRulesSync(query, limit).map(mapRuleResult);
|
|
145
169
|
}
|
|
146
170
|
|
|
171
|
+
function getDocumentResults(query, limit) {
|
|
172
|
+
return searchImportedDocuments({ query, limit }).map(mapDocumentResult);
|
|
173
|
+
}
|
|
174
|
+
|
|
147
175
|
function searchThumbgate({ query, source = 'all', limit = 10, signal = null } = {}) {
|
|
148
176
|
const trimmedQuery = String(query || '').trim();
|
|
149
177
|
if (!trimmedQuery) {
|
|
@@ -161,11 +189,14 @@ function searchThumbgate({ query, source = 'all', limit = 10, signal = null } =
|
|
|
161
189
|
results = getContextResults(trimmedQuery, normalizedLimit);
|
|
162
190
|
} else if (normalizedSource === 'rules') {
|
|
163
191
|
results = getRuleResults(trimmedQuery, normalizedLimit);
|
|
192
|
+
} else if (normalizedSource === 'documents') {
|
|
193
|
+
results = getDocumentResults(trimmedQuery, normalizedLimit);
|
|
164
194
|
} else {
|
|
165
195
|
results = sortResults([
|
|
166
196
|
...getFeedbackResults(trimmedQuery, normalizedLimit, normalizedSignal),
|
|
167
197
|
...getContextResults(trimmedQuery, normalizedLimit),
|
|
168
198
|
...getRuleResults(trimmedQuery, normalizedLimit),
|
|
199
|
+
...getDocumentResults(trimmedQuery, normalizedLimit),
|
|
169
200
|
]).slice(0, normalizedLimit);
|
|
170
201
|
}
|
|
171
202
|
|
|
@@ -3,8 +3,8 @@
|
|
|
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
|
function getKpiLogPath() { return path.join(resolveFeedbackDir(), 'tool-kpi.jsonl'); }
|
|
7
|
-
function readJsonl(fp) { if (!fs.existsSync(fp)) return []; const raw = fs.readFileSync(fp, 'utf-8').trim(); if (!raw) return []; return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
|
|
8
8
|
function recordToolCall({ toolName, serverName, latencyMs, success, agentId, metadata } = {}) { const lp = getKpiLogPath(); const dir = path.dirname(lp); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const e = { id: `kpi_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, timestamp: new Date().toISOString(), toolName: toolName || 'unknown', serverName: serverName || 'default', latencyMs: typeof latencyMs === 'number' ? latencyMs : 0, success: success !== false, agentId: agentId || 'unknown', metadata: metadata || {} }; fs.appendFileSync(lp, JSON.stringify(e) + '\n'); return e; }
|
|
9
9
|
function percentile(sorted, p) { if (sorted.length === 0) return 0; const idx = Math.ceil((p / 100) * sorted.length) - 1; return sorted[Math.max(0, idx)]; }
|
|
10
10
|
function computeToolKpis({ periodHours = 24 } = {}) { const entries = readJsonl(getKpiLogPath()); const cutoff = Date.now() - periodHours * 60 * 60 * 1000; const recent = entries.filter((e) => new Date(e.timestamp).getTime() > cutoff); const byTool = {}; for (const e of recent) { const k = e.toolName; if (!byTool[k]) byTool[k] = { toolName: k, calls: [], successes: 0, failures: 0 }; byTool[k].calls.push(e.latencyMs); if (e.success) byTool[k].successes++; else byTool[k].failures++; } const tools = Object.values(byTool).map((t) => { const sorted = t.calls.slice().sort((a, b) => a - b); const total = t.successes + t.failures; return { toolName: t.toolName, requestCount: total, successRate: total > 0 ? Math.round((t.successes / total) * 1000) / 10 : 100, p50: Math.round(percentile(sorted, 50)), p90: Math.round(percentile(sorted, 90)), p95: Math.round(percentile(sorted, 95)), successes: t.successes, failures: t.failures }; }).sort((a, b) => b.requestCount - a.requestCount); const byServer = {}; for (const e of recent) { const k = e.serverName; if (!byServer[k]) byServer[k] = { serverName: k, total: 0, successes: 0 }; byServer[k].total++; if (e.success) byServer[k].successes++; } const servers = Object.values(byServer).map((s) => ({ serverName: s.serverName, totalCalls: s.total, successRate: s.total > 0 ? Math.round((s.successes / s.total) * 1000) / 10 : 100 })); return { periodHours, totalCalls: recent.length, tools, servers }; }
|
package/scripts/tool-registry.js
CHANGED
|
@@ -36,7 +36,7 @@ const TOOLS = [
|
|
|
36
36
|
whatWorked: { type: 'string' },
|
|
37
37
|
chatHistory: {
|
|
38
38
|
type: 'array',
|
|
39
|
-
description: 'Optional recent conversation window used for history-aware lesson distillation.',
|
|
39
|
+
description: 'Optional caller-supplied recent conversation window used for history-aware lesson distillation. The current Claude auto-capture path sends up to 8 prior recorded entries for vague negative inline signals.',
|
|
40
40
|
items: {
|
|
41
41
|
type: 'object',
|
|
42
42
|
properties: {
|
|
@@ -59,7 +59,7 @@ const TOOLS = [
|
|
|
59
59
|
timestamp: { type: 'string' },
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
|
-
description: '
|
|
62
|
+
description: 'Recent conversation turns before the feedback signal. Raw messages, not summaries.',
|
|
63
63
|
},
|
|
64
64
|
rubricScores: {
|
|
65
65
|
type: 'array',
|
|
@@ -122,18 +122,57 @@ const TOOLS = [
|
|
|
122
122
|
}),
|
|
123
123
|
readOnlyTool({
|
|
124
124
|
name: 'search_thumbgate',
|
|
125
|
-
description: 'Search raw ThumbGate state across feedback logs, ContextFS memory, and
|
|
125
|
+
description: 'Search raw ThumbGate state across feedback logs, ContextFS memory, prevention rules, and imported policy documents.',
|
|
126
126
|
inputSchema: {
|
|
127
127
|
type: 'object',
|
|
128
128
|
required: ['query'],
|
|
129
129
|
properties: {
|
|
130
130
|
query: { type: 'string', description: 'Search query for ThumbGate state.' },
|
|
131
131
|
limit: { type: 'number', description: 'Maximum results to return (default 10)' },
|
|
132
|
-
source: { type: 'string', enum: ['all', 'feedback', 'context', 'rules'], description: 'Restrict search to a single ThumbGate source.' },
|
|
132
|
+
source: { type: 'string', enum: ['all', 'feedback', 'context', 'rules', 'documents'], description: 'Restrict search to a single ThumbGate source.' },
|
|
133
133
|
signal: { type: 'string', enum: ['up', 'down', 'positive', 'negative'], description: 'Optional feedback-signal filter when searching feedback data.' },
|
|
134
134
|
},
|
|
135
135
|
},
|
|
136
136
|
}),
|
|
137
|
+
destructiveTool({
|
|
138
|
+
name: 'import_document',
|
|
139
|
+
description: 'Import a local policy or runbook document into ThumbGate, normalize it for search, and propose provenance-backed gate candidates.',
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
filePath: { type: 'string', description: 'Local file path inside the active workspace or ThumbGate runtime.' },
|
|
144
|
+
content: { type: 'string', description: 'Inline document content for hosted or generated imports.' },
|
|
145
|
+
title: { type: 'string', description: 'Optional display title override.' },
|
|
146
|
+
sourceFormat: { type: 'string', enum: ['markdown', 'text', 'yaml', 'json', 'html'], description: 'Optional source format override when importing inline content.' },
|
|
147
|
+
sourceUrl: { type: 'string', description: 'Optional external URL or provenance label for the imported document.' },
|
|
148
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags such as policy, runbook, or team.' },
|
|
149
|
+
proposeGates: { type: 'boolean', description: 'When true (default), derive reviewable gate proposals from the document.' },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
readOnlyTool({
|
|
154
|
+
name: 'list_imported_documents',
|
|
155
|
+
description: 'List imported policy and runbook documents stored in local ThumbGate state.',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
query: { type: 'string', description: 'Optional title or excerpt filter.' },
|
|
160
|
+
tag: { type: 'string', description: 'Optional tag or matched template id filter.' },
|
|
161
|
+
limit: { type: 'number', description: 'Maximum documents to return (default 20).' },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
readOnlyTool({
|
|
166
|
+
name: 'get_imported_document',
|
|
167
|
+
description: 'Read a previously imported document with its proposed gate candidates and provenance.',
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
required: ['documentId'],
|
|
171
|
+
properties: {
|
|
172
|
+
documentId: { type: 'string', description: 'Imported document id.' },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
137
176
|
readOnlyTool({
|
|
138
177
|
name: 'feedback_stats',
|
|
139
178
|
description: 'Get feedback stats and recommendations',
|
|
@@ -296,6 +335,19 @@ const TOOLS = [
|
|
|
296
335
|
properties: {},
|
|
297
336
|
},
|
|
298
337
|
}),
|
|
338
|
+
readOnlyTool({
|
|
339
|
+
name: 'security_scan',
|
|
340
|
+
description: 'Scan code for OWASP vulnerabilities (injection, XSS, path traversal, SSRF, prototype pollution) and supply chain risks (typosquatting, install script abuse, wildcard versions). Returns findings with severity, category, and line numbers.',
|
|
341
|
+
inputSchema: {
|
|
342
|
+
type: 'object',
|
|
343
|
+
required: ['content'],
|
|
344
|
+
properties: {
|
|
345
|
+
content: { type: 'string', description: 'Code content to scan' },
|
|
346
|
+
filePath: { type: 'string', description: 'File path for language-aware scanning' },
|
|
347
|
+
diffMode: { type: 'boolean', description: 'When true, treats content as git diff output' },
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
}),
|
|
299
351
|
readOnlyTool({
|
|
300
352
|
name: 'capture_memory_feedback',
|
|
301
353
|
description: 'Capture success/failure feedback to harden future workflows. Aliased to capture_feedback.',
|
|
@@ -949,6 +1001,58 @@ const TOOLS = [
|
|
|
949
1001
|
},
|
|
950
1002
|
},
|
|
951
1003
|
}),
|
|
1004
|
+
destructiveTool({
|
|
1005
|
+
name: 'run_managed_lesson_agent',
|
|
1006
|
+
description: 'Run the LLM-powered lesson inference and rule generation agent over accumulated feedback. Requires ANTHROPIC_API_KEY for LLM mode; falls back to heuristics if unavailable.',
|
|
1007
|
+
inputSchema: {
|
|
1008
|
+
type: 'object',
|
|
1009
|
+
properties: {
|
|
1010
|
+
dryRun: { type: 'boolean', description: 'Preview what would be written without persisting' },
|
|
1011
|
+
limit: { type: 'number', description: 'Max feedback entries to process (default: 20)' },
|
|
1012
|
+
model: { type: 'string', description: 'Override the Claude model (default: claude-haiku-4-5)' },
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
}),
|
|
1016
|
+
readOnlyTool({
|
|
1017
|
+
name: 'managed_agent_status',
|
|
1018
|
+
description: 'Show status of the last managed lesson agent run: entries processed, lessons created, gates promoted, and total runs.',
|
|
1019
|
+
inputSchema: {
|
|
1020
|
+
type: 'object',
|
|
1021
|
+
properties: {},
|
|
1022
|
+
},
|
|
1023
|
+
}),
|
|
1024
|
+
destructiveTool({
|
|
1025
|
+
name: 'run_self_distill',
|
|
1026
|
+
description: 'Run the self-distillation agent to auto-evaluate recent agent sessions and generate improvement lessons without human feedback. Reads conversation logs, detects success/failure signals, and persists lessons.',
|
|
1027
|
+
inputSchema: {
|
|
1028
|
+
type: 'object',
|
|
1029
|
+
properties: {
|
|
1030
|
+
dryRun: { type: 'boolean', description: 'If true, analyzes but does not persist lessons' },
|
|
1031
|
+
limit: { type: 'number', description: 'Max conversation logs to process (default 20)' },
|
|
1032
|
+
model: { type: 'string', description: 'LLM model to use for analysis (requires ANTHROPIC_API_KEY)' },
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
}),
|
|
1036
|
+
readOnlyTool({
|
|
1037
|
+
name: 'self_distill_status',
|
|
1038
|
+
description: 'Show status of the last self-distillation run: sessions analyzed, lessons generated, signals detected.',
|
|
1039
|
+
inputSchema: {
|
|
1040
|
+
type: 'object',
|
|
1041
|
+
properties: {},
|
|
1042
|
+
},
|
|
1043
|
+
}),
|
|
1044
|
+
readOnlyTool({
|
|
1045
|
+
name: 'context_stuff_lessons',
|
|
1046
|
+
description: 'Dump ALL prevention lessons into a single text block for context-window injection. Bypasses RAG/search — returns every lesson sorted by confidence. For most projects (20-200 lessons), fits in 1K-10K tokens.',
|
|
1047
|
+
inputSchema: {
|
|
1048
|
+
type: 'object',
|
|
1049
|
+
properties: {
|
|
1050
|
+
maxTokenBudget: { type: 'number', description: 'Approximate token budget (default: 10000)' },
|
|
1051
|
+
signal: { type: 'string', enum: ['positive', 'negative'], description: 'Filter by signal type' },
|
|
1052
|
+
format: { type: 'string', enum: ['compact', 'full'], description: 'Output format (default: compact)' },
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1055
|
+
}),
|
|
952
1056
|
];
|
|
953
1057
|
|
|
954
1058
|
module.exports = {
|
package/scripts/vector-store.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { ensureDir } = require('./fs-utils');
|
|
5
6
|
const {
|
|
6
7
|
resolveEmbeddingProfile,
|
|
7
8
|
writeModelFitReport,
|
|
@@ -35,11 +36,6 @@ function getLanceDir() {
|
|
|
35
36
|
return path.join(getFeedbackDir(), 'lancedb');
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
function ensureDir(dirPath) {
|
|
39
|
-
if (!fs.existsSync(dirPath)) {
|
|
40
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
39
|
|
|
44
40
|
function truncateForEmbedding(text, maxChars) {
|
|
45
41
|
const raw = String(text || '');
|
|
@@ -14,10 +14,10 @@ const path = require('path');
|
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const { generateWeeklyStatsPost } = require('./daily-digest');
|
|
16
16
|
const { createSchedule } = require('./schedule-manager');
|
|
17
|
+
const { ensureDir } = require('./fs-utils');
|
|
17
18
|
|
|
18
19
|
const POSTS_DIR = path.join(os.homedir(), '.thumbgate', 'weekly-posts');
|
|
19
20
|
|
|
20
|
-
function ensureDir(p) { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Generate a weekly stats post file in post-everywhere format.
|