thumbgate 1.4.1 → 1.4.3
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 +45 -34
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +3 -3
- package/.well-known/llms.txt +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +26 -2
- package/adapters/README.md +4 -1
- package/adapters/chatgpt/INSTALL.md +39 -19
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +10 -4
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/perplexity/.mcp.json +36 -0
- package/adapters/perplexity/config.toml +16 -0
- package/adapters/perplexity/opencode.json +29 -0
- package/bin/cli.js +246 -90
- package/config/mcp-allowlists.json +11 -3
- package/package.json +28 -13
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +1 -1
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/index.html +121 -24
- package/public/llm-context.md +17 -1
- package/scripts/ai-search-visibility.js +10 -36
- package/scripts/audit-trail.js +25 -15
- package/scripts/auto-wire-hooks.js +127 -0
- package/scripts/cli-demo.js +102 -0
- package/scripts/cli-schema.js +285 -0
- package/scripts/cli-status.js +166 -0
- package/scripts/cross-encoder-reranker.js +235 -0
- package/scripts/explore-subcommands.js +277 -0
- package/scripts/explore.js +569 -0
- package/scripts/feedback-loop.js +20 -6
- package/scripts/lesson-inference.js +27 -2
- package/scripts/lesson-reranker.js +263 -0
- package/scripts/lesson-retrieval.js +34 -17
- package/scripts/lesson-search.js +69 -0
- package/scripts/perplexity-client.js +210 -0
- package/scripts/perplexity-command-center.js +644 -0
- package/scripts/perplexity-marketing.js +17 -29
- package/scripts/prove-packaged-runtime.js +5 -4
- package/scripts/ralph-mode-ci.js +122 -19
- package/scripts/reflector-agent.js +2 -2
- package/scripts/session-analyzer.js +533 -0
- package/scripts/social-analytics/db/marketing-db.js +179 -0
- package/scripts/social-analytics/db/schema.sql +23 -0
- package/scripts/social-analytics/generate-instagram-card.js +31 -5
- package/scripts/social-analytics/generate-slides.js +268 -0
- package/scripts/social-analytics/post-video.js +316 -0
- package/scripts/social-analytics/publishers/zernio.js +52 -23
- package/scripts/statusline-local-stats.js +3 -1
- package/scripts/statusline.sh +15 -10
- package/scripts/thumbgate-bench.js +494 -0
- package/src/api/server.js +65 -1
- package/scripts/social-analytics/db/analytics.sqlite +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* cli-status.js — agent-friendly health check for ThumbGate.
|
|
6
|
+
*
|
|
7
|
+
* Combines feedback stats, gate stats, lesson count, and agent detection
|
|
8
|
+
* into a single JSON-friendly payload.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
function detectAgent(projectDir) {
|
|
15
|
+
if (fs.existsSync(path.join(projectDir, '.claude'))) return 'claude-code';
|
|
16
|
+
if (fs.existsSync(path.join(projectDir, '.cursorrules'))) return 'cursor';
|
|
17
|
+
if (fs.existsSync(path.join(projectDir, '.cursor'))) return 'cursor';
|
|
18
|
+
if (fs.existsSync(path.join(projectDir, '.codex'))) return 'codex';
|
|
19
|
+
if (fs.existsSync(path.join(projectDir, '.gemini'))) return 'gemini';
|
|
20
|
+
if (fs.existsSync(path.join(projectDir, '.amp'))) return 'amp';
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function generateAgentStatus(options = {}) {
|
|
25
|
+
const PKG_ROOT = options.pkgRoot || path.join(__dirname, '..');
|
|
26
|
+
const projectDir = options.projectDir || process.cwd();
|
|
27
|
+
|
|
28
|
+
// Feedback paths
|
|
29
|
+
const { getFeedbackPaths, readJSONL, analyzeFeedback } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
|
|
30
|
+
const paths = getFeedbackPaths();
|
|
31
|
+
|
|
32
|
+
// Feedback entries
|
|
33
|
+
const feedbackEntries = readJSONL(paths.FEEDBACK_LOG_PATH);
|
|
34
|
+
const memoryEntries = readJSONL(paths.MEMORY_LOG_PATH);
|
|
35
|
+
|
|
36
|
+
// Gate stats
|
|
37
|
+
let gateData = { totalGates: 0, autoPromotedGates: 0, manualGates: 0, totalBlocked: 0 };
|
|
38
|
+
try {
|
|
39
|
+
const { calculateStats } = require(path.join(PKG_ROOT, 'scripts', 'gate-stats'));
|
|
40
|
+
gateData = calculateStats();
|
|
41
|
+
} catch { /* gate-stats not available */ }
|
|
42
|
+
|
|
43
|
+
// Prevention rules count
|
|
44
|
+
let preventionRuleCount = 0;
|
|
45
|
+
if (fs.existsSync(paths.PREVENTION_RULES_PATH)) {
|
|
46
|
+
const rulesContent = fs.readFileSync(paths.PREVENTION_RULES_PATH, 'utf-8');
|
|
47
|
+
const ruleHeaders = rulesContent.match(/^## /gm);
|
|
48
|
+
preventionRuleCount = ruleHeaders ? ruleHeaders.length : 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Last feedback timestamp
|
|
52
|
+
const lastFeedback = feedbackEntries.length > 0
|
|
53
|
+
? feedbackEntries[feedbackEntries.length - 1]
|
|
54
|
+
: null;
|
|
55
|
+
const lastFeedbackTimestamp = lastFeedback
|
|
56
|
+
? (lastFeedback.timestamp || lastFeedback.createdAt || null)
|
|
57
|
+
: null;
|
|
58
|
+
|
|
59
|
+
// Agent detection
|
|
60
|
+
const agent = detectAgent(projectDir);
|
|
61
|
+
|
|
62
|
+
// Enforcement check: is there at least one blocking gate?
|
|
63
|
+
const hasBlockingGates = gateData.totalGates > 0;
|
|
64
|
+
const hasPreToolHook = (() => {
|
|
65
|
+
try {
|
|
66
|
+
const settingsPath = path.join(projectDir, '.claude', 'settings.json');
|
|
67
|
+
if (!fs.existsSync(settingsPath)) return false;
|
|
68
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
69
|
+
return Boolean(
|
|
70
|
+
settings.hooks &&
|
|
71
|
+
settings.hooks.PreToolUse &&
|
|
72
|
+
settings.hooks.PreToolUse.length > 0
|
|
73
|
+
);
|
|
74
|
+
} catch { return false; }
|
|
75
|
+
})();
|
|
76
|
+
const enforcementActive = hasBlockingGates || hasPreToolHook;
|
|
77
|
+
|
|
78
|
+
// Config
|
|
79
|
+
const configPath = path.join(projectDir, '.thumbgate', 'config.json');
|
|
80
|
+
let config = null;
|
|
81
|
+
try {
|
|
82
|
+
if (fs.existsSync(configPath)) {
|
|
83
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
84
|
+
}
|
|
85
|
+
} catch { /* ignore */ }
|
|
86
|
+
|
|
87
|
+
// Version
|
|
88
|
+
let version = 'unknown';
|
|
89
|
+
try {
|
|
90
|
+
version = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version;
|
|
91
|
+
} catch { /* ignore */ }
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
version,
|
|
95
|
+
agent,
|
|
96
|
+
enforcementActive,
|
|
97
|
+
gates: {
|
|
98
|
+
total: gateData.totalGates,
|
|
99
|
+
manual: gateData.manualGates,
|
|
100
|
+
autoPromoted: gateData.autoPromotedGates,
|
|
101
|
+
blocking: gateData.blockGates || 0,
|
|
102
|
+
warning: gateData.warnGates || 0,
|
|
103
|
+
},
|
|
104
|
+
lessons: memoryEntries.length,
|
|
105
|
+
feedback: {
|
|
106
|
+
total: feedbackEntries.length,
|
|
107
|
+
positive: feedbackEntries.filter(e => e.signal === 'positive').length,
|
|
108
|
+
negative: feedbackEntries.filter(e => e.signal === 'negative').length,
|
|
109
|
+
},
|
|
110
|
+
preventionRules: preventionRuleCount,
|
|
111
|
+
lastFeedbackTimestamp,
|
|
112
|
+
feedbackDir: paths.FEEDBACK_DIR,
|
|
113
|
+
initialized: Boolean(config),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatStatus(data) {
|
|
118
|
+
const BD = '\x1b[1m';
|
|
119
|
+
const RST = '\x1b[0m';
|
|
120
|
+
const G = '\x1b[32m';
|
|
121
|
+
const R = '\x1b[31m';
|
|
122
|
+
const C = '\x1b[36m';
|
|
123
|
+
const Y = '\x1b[33m';
|
|
124
|
+
const D = '\x1b[90m';
|
|
125
|
+
|
|
126
|
+
const lines = [];
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push(`${BD}thumbgate status${RST} v${data.version}`);
|
|
129
|
+
lines.push('─'.repeat(50));
|
|
130
|
+
|
|
131
|
+
// Scope badge
|
|
132
|
+
const scope = '[LOCAL]';
|
|
133
|
+
lines.push(` ${C}${scope}${RST} ${data.feedbackDir}`);
|
|
134
|
+
lines.push('');
|
|
135
|
+
|
|
136
|
+
// Core metrics
|
|
137
|
+
const enfBadge = data.enforcementActive
|
|
138
|
+
? `${G}[ACTIVE]${RST}`
|
|
139
|
+
: `${Y}[LEARNING]${RST}`;
|
|
140
|
+
lines.push(` Enforcement : ${enfBadge}`);
|
|
141
|
+
lines.push(` Agent : ${data.agent || 'none detected'}`);
|
|
142
|
+
lines.push(` Gates : ${data.gates.total} (${data.gates.blocking} block, ${data.gates.warning} warn)`);
|
|
143
|
+
lines.push(` Lessons : ${data.lessons}`);
|
|
144
|
+
lines.push(` Prevention Rules: ${data.preventionRules}`);
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push(` Feedback : ${data.feedback.total} total (${G}${data.feedback.positive} up${RST}, ${R}${data.feedback.negative} down${RST})`);
|
|
147
|
+
if (data.lastFeedbackTimestamp) {
|
|
148
|
+
const ago = relativeTime(data.lastFeedbackTimestamp);
|
|
149
|
+
lines.push(` Last Feedback : ${ago} ${D}(${data.lastFeedbackTimestamp})${RST}`);
|
|
150
|
+
} else {
|
|
151
|
+
lines.push(` Last Feedback : ${D}none${RST}`);
|
|
152
|
+
}
|
|
153
|
+
lines.push('');
|
|
154
|
+
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function relativeTime(ts) {
|
|
159
|
+
if (!ts) return '';
|
|
160
|
+
const d = Math.floor((Date.now() - new Date(ts).getTime()) / 86400000);
|
|
161
|
+
if (d === 0) return 'today';
|
|
162
|
+
if (d === 1) return '1 day ago';
|
|
163
|
+
return `${d} days ago`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { generateAgentStatus, formatStatus };
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cross-Encoder Reranker for ThumbGate lesson retrieval.
|
|
6
|
+
*
|
|
7
|
+
* Two-stage retrieval:
|
|
8
|
+
* Stage 1: Fast candidate retrieval (existing bigram Jaccard + keyword matching)
|
|
9
|
+
* Stage 2: Cross-encoder reranking scores query-document pairs jointly
|
|
10
|
+
*
|
|
11
|
+
* The cross-encoder evaluates the query AND each lesson together (not independently),
|
|
12
|
+
* catching false positives that keyword/vector search misses.
|
|
13
|
+
*
|
|
14
|
+
* Architecture reference: "Advanced RAG Retrieval: Cross-Encoders & Reranking"
|
|
15
|
+
* (Towards Data Science, April 2026)
|
|
16
|
+
*
|
|
17
|
+
* When LLM is available (ANTHROPIC_API_KEY), uses Claude as the cross-encoder.
|
|
18
|
+
* Falls back to enhanced heuristic scoring when LLM is unavailable.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { retrieveRelevantLessons, scoreRelevance, buildActionSignature } = require('./lesson-retrieval');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Heuristic cross-encoder: scores a (query, document) pair jointly.
|
|
25
|
+
* Unlike bi-encoder (independent embeddings), this examines the pair together
|
|
26
|
+
* to find semantic relationships that keyword matching misses.
|
|
27
|
+
*/
|
|
28
|
+
function heuristicCrossEncode(query, document) {
|
|
29
|
+
const queryLower = (query || '').toLowerCase();
|
|
30
|
+
const docLower = (document || '').toLowerCase();
|
|
31
|
+
|
|
32
|
+
let score = 0;
|
|
33
|
+
|
|
34
|
+
// 1. Exact substring containment (strongest signal)
|
|
35
|
+
if (queryLower.length > 3 && docLower.length > 3 &&
|
|
36
|
+
(docLower.includes(queryLower) || queryLower.includes(docLower))) {
|
|
37
|
+
score += 0.9;
|
|
38
|
+
return Math.min(score, 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Shared noun phrases (not just tokens — consecutive word pairs)
|
|
42
|
+
const queryPhrases = extractPhrases(queryLower);
|
|
43
|
+
const docPhrases = extractPhrases(docLower);
|
|
44
|
+
const phraseOverlap = queryPhrases.filter((p) => docPhrases.includes(p));
|
|
45
|
+
score += Math.min(phraseOverlap.length * 0.15, 0.5);
|
|
46
|
+
|
|
47
|
+
// 3. Semantic category matching
|
|
48
|
+
const categories = {
|
|
49
|
+
destructive: ['delete', 'remove', 'drop', 'destroy', 'wipe', 'truncate', 'rm -rf', 'force-push', 'reset --hard'],
|
|
50
|
+
git: ['git', 'push', 'pull', 'merge', 'rebase', 'branch', 'commit', 'checkout', 'stash'],
|
|
51
|
+
database: ['sql', 'query', 'table', 'migration', 'schema', 'database', 'insert', 'update', 'select'],
|
|
52
|
+
deploy: ['deploy', 'release', 'publish', 'railway', 'vercel', 'heroku', 'npm publish'],
|
|
53
|
+
security: ['secret', 'token', 'api key', 'password', 'credential', 'env', '.env', 'pem'],
|
|
54
|
+
file: ['edit', 'write', 'create', 'modify', 'config', 'package.json', 'readme'],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
for (const [, terms] of Object.entries(categories)) {
|
|
58
|
+
const queryHit = terms.some((t) => queryLower.includes(t));
|
|
59
|
+
const docHit = terms.some((t) => docLower.includes(t));
|
|
60
|
+
if (queryHit && docHit) {
|
|
61
|
+
score += 0.25;
|
|
62
|
+
break; // Only count strongest category match
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Action-target alignment (e.g., "git push" in query matches "push to main" in doc)
|
|
67
|
+
const queryVerbs = extractVerbs(queryLower);
|
|
68
|
+
const docVerbs = extractVerbs(docLower);
|
|
69
|
+
const verbOverlap = queryVerbs.filter((v) => docVerbs.includes(v));
|
|
70
|
+
score += Math.min(verbOverlap.length * 0.1, 0.3);
|
|
71
|
+
|
|
72
|
+
// 5. Negation alignment (both about what NOT to do)
|
|
73
|
+
const queryNegated = /\b(don'?t|never|avoid|block|prevent|stop)\b/.test(queryLower);
|
|
74
|
+
const docNegated = /\b(don'?t|never|avoid|block|prevent|stop)\b/.test(docLower);
|
|
75
|
+
if (queryNegated && docNegated) score += 0.1;
|
|
76
|
+
|
|
77
|
+
return Math.min(score, 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* LLM cross-encoder: uses Claude to score relevance of query-document pairs.
|
|
82
|
+
* More accurate but requires API key and costs tokens.
|
|
83
|
+
*/
|
|
84
|
+
async function llmCrossEncode(query, documents) {
|
|
85
|
+
const { isAvailable, callClaude, MODELS } = require('./llm-client');
|
|
86
|
+
if (!isAvailable()) return null;
|
|
87
|
+
|
|
88
|
+
const docList = documents
|
|
89
|
+
.map((d, i) => `[${i}] ${(d.title || '').slice(0, 100)} | ${(d.content || '').slice(0, 200)}`)
|
|
90
|
+
.join('\n');
|
|
91
|
+
|
|
92
|
+
const prompt = `You are a relevance scoring engine. Given a query and a list of documents, score each document's relevance to the query from 0.0 (irrelevant) to 1.0 (highly relevant).
|
|
93
|
+
|
|
94
|
+
Query: "${query.slice(0, 300)}"
|
|
95
|
+
|
|
96
|
+
Documents:
|
|
97
|
+
${docList}
|
|
98
|
+
|
|
99
|
+
Return ONLY a JSON array of scores, one per document. Example: [0.9, 0.2, 0.7, 0.1, 0.5]
|
|
100
|
+
No other text.`;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const raw = await callClaude({
|
|
104
|
+
systemPrompt: 'You are a relevance scoring engine. Return only JSON arrays of numbers.',
|
|
105
|
+
userPrompt: prompt,
|
|
106
|
+
model: MODELS.FAST,
|
|
107
|
+
maxTokens: 256,
|
|
108
|
+
});
|
|
109
|
+
const scores = JSON.parse(raw);
|
|
110
|
+
if (Array.isArray(scores) && scores.length === documents.length) {
|
|
111
|
+
return scores.map((s) => Math.max(0, Math.min(1, Number(s) || 0)));
|
|
112
|
+
}
|
|
113
|
+
} catch { /* fall back to heuristic */ }
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Two-stage retrieval with cross-encoder reranking.
|
|
119
|
+
*
|
|
120
|
+
* Stage 1: Retrieve top N candidates using existing keyword + bigram matching
|
|
121
|
+
* Stage 2: Rerank candidates using cross-encoder (LLM or heuristic)
|
|
122
|
+
* Return top K results by cross-encoder score
|
|
123
|
+
*/
|
|
124
|
+
async function retrieveWithReranking(toolName, actionContext, options = {}) {
|
|
125
|
+
const {
|
|
126
|
+
candidateCount = 20,
|
|
127
|
+
maxResults = 5,
|
|
128
|
+
useLLM = false,
|
|
129
|
+
feedbackDir,
|
|
130
|
+
} = options;
|
|
131
|
+
|
|
132
|
+
// Stage 1: Fast candidate retrieval (existing system)
|
|
133
|
+
const candidates = retrieveRelevantLessons(toolName, actionContext, {
|
|
134
|
+
maxResults: candidateCount,
|
|
135
|
+
feedbackDir,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (candidates.length === 0) return [];
|
|
139
|
+
if (candidates.length <= maxResults) return candidates;
|
|
140
|
+
|
|
141
|
+
const query = `${toolName || ''} ${actionContext || ''}`.trim();
|
|
142
|
+
|
|
143
|
+
// Stage 2: Cross-encoder reranking
|
|
144
|
+
let rerankedScores;
|
|
145
|
+
|
|
146
|
+
if (useLLM) {
|
|
147
|
+
rerankedScores = await llmCrossEncode(query, candidates);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fall back to heuristic cross-encoder if LLM unavailable or failed
|
|
151
|
+
if (!rerankedScores) {
|
|
152
|
+
rerankedScores = candidates.map((c) => {
|
|
153
|
+
const docText = `${c.title || ''} ${c.content || ''}`;
|
|
154
|
+
return heuristicCrossEncode(query, docText);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Combine original relevance score with cross-encoder score
|
|
159
|
+
// Weight: 40% original, 60% cross-encoder (cross-encoder is more precise)
|
|
160
|
+
const reranked = candidates.map((c, i) => ({
|
|
161
|
+
...c,
|
|
162
|
+
crossEncoderScore: rerankedScores[i],
|
|
163
|
+
combinedScore: c.relevanceScore * 0.4 + rerankedScores[i] * 0.6,
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
return reranked
|
|
167
|
+
.sort((a, b) => b.combinedScore - a.combinedScore)
|
|
168
|
+
.slice(0, maxResults);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Synchronous version for use in PreToolUse hooks (cannot be async).
|
|
173
|
+
*/
|
|
174
|
+
function retrieveWithRerankingSync(toolName, actionContext, options = {}) {
|
|
175
|
+
const {
|
|
176
|
+
candidateCount = 20,
|
|
177
|
+
maxResults = 5,
|
|
178
|
+
feedbackDir,
|
|
179
|
+
} = options;
|
|
180
|
+
|
|
181
|
+
const candidates = retrieveRelevantLessons(toolName, actionContext, {
|
|
182
|
+
maxResults: candidateCount,
|
|
183
|
+
feedbackDir,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (candidates.length === 0) return [];
|
|
187
|
+
if (candidates.length <= maxResults) return candidates;
|
|
188
|
+
|
|
189
|
+
const query = `${toolName || ''} ${actionContext || ''}`.trim();
|
|
190
|
+
|
|
191
|
+
const rerankedScores = candidates.map((c) => {
|
|
192
|
+
const docText = `${c.title || ''} ${c.content || ''}`;
|
|
193
|
+
return heuristicCrossEncode(query, docText);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const reranked = candidates.map((c, i) => ({
|
|
197
|
+
...c,
|
|
198
|
+
crossEncoderScore: rerankedScores[i],
|
|
199
|
+
combinedScore: c.relevanceScore * 0.4 + rerankedScores[i] * 0.6,
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
return reranked
|
|
203
|
+
.sort((a, b) => b.combinedScore - a.combinedScore)
|
|
204
|
+
.slice(0, maxResults);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- Utility functions ---
|
|
208
|
+
|
|
209
|
+
function extractPhrases(text) {
|
|
210
|
+
const words = text.split(/\s+/).filter((w) => w.length > 2);
|
|
211
|
+
const phrases = [];
|
|
212
|
+
for (let i = 0; i < words.length - 1; i++) {
|
|
213
|
+
phrases.push(`${words[i]} ${words[i + 1]}`);
|
|
214
|
+
}
|
|
215
|
+
return phrases;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function extractVerbs(text) {
|
|
219
|
+
const verbPatterns = [
|
|
220
|
+
'push', 'pull', 'merge', 'delete', 'create', 'edit', 'write', 'read',
|
|
221
|
+
'deploy', 'install', 'remove', 'run', 'execute', 'build', 'test',
|
|
222
|
+
'commit', 'rebase', 'reset', 'drop', 'truncate', 'migrate', 'publish',
|
|
223
|
+
'block', 'allow', 'approve', 'deny', 'warn', 'log',
|
|
224
|
+
];
|
|
225
|
+
return verbPatterns.filter((v) => text.includes(v));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
heuristicCrossEncode,
|
|
230
|
+
llmCrossEncode,
|
|
231
|
+
retrieveWithReranking,
|
|
232
|
+
retrieveWithRerankingSync,
|
|
233
|
+
extractPhrases,
|
|
234
|
+
extractVerbs,
|
|
235
|
+
};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* explore-subcommands.js — non-interactive explore for agent consumption.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* thumbgate explore lessons [--json] [--limit=N]
|
|
9
|
+
* thumbgate explore rules [--json]
|
|
10
|
+
* thumbgate explore gates [--json]
|
|
11
|
+
*
|
|
12
|
+
* When --json is passed, outputs structured data. Otherwise renders
|
|
13
|
+
* human-readable tables with context signal badges.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const BD = '\x1b[1m';
|
|
20
|
+
const RST = '\x1b[0m';
|
|
21
|
+
const G = '\x1b[32m';
|
|
22
|
+
const R = '\x1b[31m';
|
|
23
|
+
const C = '\x1b[36m';
|
|
24
|
+
const Y = '\x1b[33m';
|
|
25
|
+
const D = '\x1b[90m';
|
|
26
|
+
|
|
27
|
+
function relDate(ts) {
|
|
28
|
+
if (!ts) return '';
|
|
29
|
+
const d = Math.floor((Date.now() - new Date(ts).getTime()) / 86400000);
|
|
30
|
+
if (d === 0) return 'today';
|
|
31
|
+
if (d === 1) return '1d ago';
|
|
32
|
+
return `${d}d ago`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function confidenceBadge(lesson) {
|
|
36
|
+
// Derive confidence from signal + whether it became a gate
|
|
37
|
+
const signal = (lesson.signal || lesson.feedback || '').toLowerCase();
|
|
38
|
+
if (lesson.gatePromoted || lesson.autoGateId) return `${G}[ACTIVE]${RST}`;
|
|
39
|
+
if (signal.includes('negative') || signal === 'down') return `${Y}[LEARNING]${RST}`;
|
|
40
|
+
return `${D}[LEARNING]${RST}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function scopeBadge() {
|
|
44
|
+
return `${C}[LOCAL]${RST}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function actionBadge(action) {
|
|
48
|
+
if (action === 'block') return `${R}[BLOCKED]${RST}`;
|
|
49
|
+
if (action === 'warn') return `${Y}[WARN]${RST}`;
|
|
50
|
+
return `${G}[ALLOWED]${RST}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Data loaders (reuse from explore.js patterns)
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function loadLessons(feedbackDir) {
|
|
58
|
+
const p = path.join(feedbackDir, 'memory-log.jsonl');
|
|
59
|
+
if (!fs.existsSync(p)) return [];
|
|
60
|
+
return fs.readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map(l => {
|
|
61
|
+
try { return JSON.parse(l); } catch { return null; }
|
|
62
|
+
}).filter(Boolean).reverse();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadGates(pkgRoot) {
|
|
66
|
+
const gatesDir = path.join(pkgRoot, 'config', 'gates');
|
|
67
|
+
const gates = [];
|
|
68
|
+
if (!fs.existsSync(gatesDir)) return gates;
|
|
69
|
+
for (const f of fs.readdirSync(gatesDir).sort((a, b) => a.localeCompare(b))) {
|
|
70
|
+
if (!f.endsWith('.json') || f === 'custom.json') continue;
|
|
71
|
+
try {
|
|
72
|
+
const raw = JSON.parse(fs.readFileSync(path.join(gatesDir, f), 'utf8'));
|
|
73
|
+
const items = Array.isArray(raw) ? raw : (raw.gates || raw.rules || [raw]);
|
|
74
|
+
items.forEach(g => gates.push({ ...g, _file: f }));
|
|
75
|
+
} catch { /* skip */ }
|
|
76
|
+
}
|
|
77
|
+
// Auto-promoted gates
|
|
78
|
+
try {
|
|
79
|
+
const { loadAutoGates } = require(path.join(pkgRoot, 'scripts', 'auto-promote-gates'));
|
|
80
|
+
const auto = loadAutoGates();
|
|
81
|
+
(auto.gates || []).forEach(g => gates.push({ ...g, _file: 'auto-promoted', _auto: true }));
|
|
82
|
+
} catch { /* ok */ }
|
|
83
|
+
return gates;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function loadRules(feedbackDir) {
|
|
87
|
+
const p = path.join(feedbackDir, 'prevention-rules.md');
|
|
88
|
+
if (!fs.existsSync(p)) return [];
|
|
89
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
90
|
+
// Split on ## headers; the first segment is the preamble (before any ##), skip it
|
|
91
|
+
const parts = content.split(/^## /m);
|
|
92
|
+
const ruleSections = parts.slice(1).filter(Boolean);
|
|
93
|
+
return ruleSections.map((section, i) => {
|
|
94
|
+
const lines = section.trim().split('\n');
|
|
95
|
+
const title = lines[0] || `Rule ${i + 1}`;
|
|
96
|
+
const body = lines.slice(1).join('\n').trim();
|
|
97
|
+
return { id: i + 1, title: title.trim(), body };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function loadGateFirings(feedbackDir) {
|
|
102
|
+
const p = path.join(feedbackDir, 'rejection-ledger.jsonl');
|
|
103
|
+
if (!fs.existsSync(p)) return [];
|
|
104
|
+
return fs.readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map(l => {
|
|
105
|
+
try { return JSON.parse(l); } catch { return null; }
|
|
106
|
+
}).filter(Boolean).reverse();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Subcommand handlers
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
function exploreLessons(options = {}) {
|
|
114
|
+
const { feedbackDir, limit = 20, json = false } = options;
|
|
115
|
+
const lessons = loadLessons(feedbackDir).slice(0, limit);
|
|
116
|
+
|
|
117
|
+
if (json) {
|
|
118
|
+
const payload = lessons.map(l => ({
|
|
119
|
+
id: l.id,
|
|
120
|
+
signal: l.signal || l.feedback,
|
|
121
|
+
context: l.content || l.context || '',
|
|
122
|
+
tags: l.tags || [],
|
|
123
|
+
domain: l.domain || null,
|
|
124
|
+
importance: l.importance || 'medium',
|
|
125
|
+
timestamp: l.timestamp || l.createdAt,
|
|
126
|
+
scope: 'local',
|
|
127
|
+
confidence: (l.gatePromoted || l.autoGateId) ? 'active' : 'learning',
|
|
128
|
+
}));
|
|
129
|
+
return { lessons: payload, total: payload.length, scope: 'local' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const lines = [];
|
|
133
|
+
lines.push('');
|
|
134
|
+
lines.push(`${BD}thumbgate explore lessons${RST} ${scopeBadge()}`);
|
|
135
|
+
lines.push('─'.repeat(60));
|
|
136
|
+
|
|
137
|
+
if (lessons.length === 0) {
|
|
138
|
+
lines.push(' No lessons stored yet. Capture feedback to create lessons.');
|
|
139
|
+
lines.push(' Run: npx thumbgate capture --feedback=down --context="what failed"');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const l of lessons) {
|
|
143
|
+
const sig = (l.signal || l.feedback || '').toLowerCase();
|
|
144
|
+
const icon = sig.includes('positive') || sig === 'up' ? `${G}+${RST}` : `${R}-${RST}`;
|
|
145
|
+
const badge = confidenceBadge(l);
|
|
146
|
+
const context = (l.content || l.context || '').slice(0, 70);
|
|
147
|
+
const ts = relDate(l.timestamp || l.createdAt);
|
|
148
|
+
const tags = (l.tags || []).join(', ');
|
|
149
|
+
lines.push(` ${icon} ${badge} ${context}`);
|
|
150
|
+
if (tags || ts) {
|
|
151
|
+
lines.push(` ${D}${ts}${tags ? ' tags: ' + tags : ''}${RST}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
lines.push('');
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function exploreRules(options = {}) {
|
|
159
|
+
const { feedbackDir, json = false } = options;
|
|
160
|
+
const rules = loadRules(feedbackDir);
|
|
161
|
+
|
|
162
|
+
if (json) {
|
|
163
|
+
const payload = rules.map(r => ({
|
|
164
|
+
id: r.id,
|
|
165
|
+
title: r.title,
|
|
166
|
+
body: r.body,
|
|
167
|
+
scope: 'local',
|
|
168
|
+
}));
|
|
169
|
+
return { rules: payload, total: payload.length, scope: 'local' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const lines = [];
|
|
173
|
+
lines.push('');
|
|
174
|
+
lines.push(`${BD}thumbgate explore rules${RST} ${scopeBadge()}`);
|
|
175
|
+
lines.push('─'.repeat(60));
|
|
176
|
+
|
|
177
|
+
if (rules.length === 0) {
|
|
178
|
+
lines.push(' No prevention rules generated yet.');
|
|
179
|
+
lines.push(' Run: npx thumbgate rules');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const r of rules) {
|
|
183
|
+
lines.push(` ${Y}Rule ${r.id}${RST}: ${r.title}`);
|
|
184
|
+
if (r.body) {
|
|
185
|
+
const preview = r.body.split('\n')[0].slice(0, 80);
|
|
186
|
+
lines.push(` ${D}${preview}${RST}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
lines.push('');
|
|
190
|
+
return lines.join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function exploreGates(options = {}) {
|
|
194
|
+
const { pkgRoot, json = false } = options;
|
|
195
|
+
const gates = loadGates(pkgRoot || path.join(__dirname, '..'));
|
|
196
|
+
|
|
197
|
+
if (json) {
|
|
198
|
+
const payload = gates.map(g => ({
|
|
199
|
+
id: g.id || g.name || 'unnamed',
|
|
200
|
+
pattern: g.pattern || g.toolName || '',
|
|
201
|
+
action: g.action || 'warn',
|
|
202
|
+
occurrences: g.occurrences || 0,
|
|
203
|
+
source: g._auto ? 'auto-promoted' : (g._file || 'manual'),
|
|
204
|
+
scope: 'local',
|
|
205
|
+
status: g.action === 'block' ? 'blocked' : 'allowed',
|
|
206
|
+
}));
|
|
207
|
+
return { gates: payload, total: payload.length, scope: 'local' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const lines = [];
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push(`${BD}thumbgate explore gates${RST} ${scopeBadge()}`);
|
|
213
|
+
lines.push('─'.repeat(60));
|
|
214
|
+
|
|
215
|
+
if (gates.length === 0) {
|
|
216
|
+
lines.push(' No gates configured. Run: npx thumbgate init');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const g of gates) {
|
|
220
|
+
const name = g.id || g.name || 'unnamed';
|
|
221
|
+
const badge = actionBadge(g.action);
|
|
222
|
+
const source = g._auto ? 'auto' : g._file || 'manual';
|
|
223
|
+
const occ = g.occurrences ? ` (${g.occurrences} fires)` : '';
|
|
224
|
+
lines.push(` ${badge} ${name} ${D}${source}${occ}${RST}`);
|
|
225
|
+
if (g.pattern) {
|
|
226
|
+
lines.push(` ${D}pattern: ${g.pattern}${RST}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
lines.push('');
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function exploreGateFirings(options = {}) {
|
|
234
|
+
const { feedbackDir, limit = 20, json = false } = options;
|
|
235
|
+
const firings = loadGateFirings(feedbackDir).slice(0, limit);
|
|
236
|
+
|
|
237
|
+
if (json) {
|
|
238
|
+
const payload = firings.map(f => ({
|
|
239
|
+
id: f.id,
|
|
240
|
+
signal: f.signal,
|
|
241
|
+
context: f.context || '',
|
|
242
|
+
reason: f.reason || '',
|
|
243
|
+
timestamp: f.timestamp,
|
|
244
|
+
scope: 'local',
|
|
245
|
+
result: 'blocked',
|
|
246
|
+
}));
|
|
247
|
+
return { firings: payload, total: payload.length, scope: 'local' };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const lines = [];
|
|
251
|
+
lines.push('');
|
|
252
|
+
lines.push(`${BD}thumbgate explore firings${RST} ${scopeBadge()}`);
|
|
253
|
+
lines.push('─'.repeat(60));
|
|
254
|
+
|
|
255
|
+
if (firings.length === 0) {
|
|
256
|
+
lines.push(' No gate firings recorded yet.');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const f of firings) {
|
|
260
|
+
const ts = relDate(f.timestamp);
|
|
261
|
+
lines.push(` ${R}[BLOCKED]${RST} ${(f.context || f.reason || '').slice(0, 60)}`);
|
|
262
|
+
lines.push(` ${D}${ts} reason: ${f.reason || 'unknown'}${RST}`);
|
|
263
|
+
}
|
|
264
|
+
lines.push('');
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
exploreLessons,
|
|
270
|
+
exploreRules,
|
|
271
|
+
exploreGates,
|
|
272
|
+
exploreGateFirings,
|
|
273
|
+
loadLessons,
|
|
274
|
+
loadGates,
|
|
275
|
+
loadRules,
|
|
276
|
+
loadGateFirings,
|
|
277
|
+
};
|