thumbgate 1.2.0 → 1.3.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +35 -14
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +2 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +20 -11
- package/config/github-about.json +1 -1
- package/config/model-tiers.json +11 -0
- package/package.json +8 -6
- 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/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/compare.html +4 -4
- package/public/guide.html +4 -4
- package/public/index.html +51 -38
- package/public/learn/ai-agent-persistent-memory.html +1 -0
- package/public/lessons.html +325 -17
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/audit-trail.js +6 -0
- package/scripts/capture-railway-diagnostics.sh +97 -0
- package/scripts/check-congruence.js +1 -1
- package/scripts/claude-feedback-sync.js +320 -0
- package/scripts/cli-telemetry.js +4 -1
- package/scripts/contextfs.js +32 -23
- package/scripts/dashboard.js +84 -0
- package/scripts/feedback-loop.js +16 -0
- package/scripts/intervention-policy.js +696 -0
- package/scripts/local-model-profile.js +18 -2
- package/scripts/model-tier-router.js +10 -1
- package/scripts/operational-integrity.js +354 -31
- package/scripts/prove-adapters.js +1 -0
- package/scripts/prove-automation.js +2 -2
- package/scripts/prove-packaged-runtime.js +260 -0
- package/scripts/prove-runtime.js +13 -0
- package/scripts/rate-limiter.js +3 -3
- package/scripts/statusline-local-stats.js +2 -0
- package/scripts/statusline.sh +166 -11
- package/scripts/tool-registry.js +2 -2
- package/scripts/workflow-sentinel.js +114 -4
- package/skills/thumbgate/SKILL.md +1 -1
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const {
|
|
7
|
+
captureFeedback,
|
|
8
|
+
getFeedbackPaths,
|
|
9
|
+
readJSONL,
|
|
10
|
+
analyzeFeedback,
|
|
11
|
+
} = require('./feedback-loop');
|
|
12
|
+
const { normalizeFeedbackText } = require('./feedback-quality');
|
|
13
|
+
const {
|
|
14
|
+
resolveFeedbackDir,
|
|
15
|
+
resolveProjectDir,
|
|
16
|
+
} = require('./feedback-paths');
|
|
17
|
+
const { refreshStatuslineCache } = require('./hook-thumbgate-cache-updater');
|
|
18
|
+
|
|
19
|
+
const SYNC_STATE_FILE = 'claude-feedback-sync-state.json';
|
|
20
|
+
const DEFAULT_RECENT_FEEDBACK_LIMIT = 250;
|
|
21
|
+
const DEFAULT_PROCESSED_ID_LIMIT = 512;
|
|
22
|
+
const DUPLICATE_WINDOW_MS = 30 * 1000;
|
|
23
|
+
|
|
24
|
+
function getClaudeHistoryPath(options = {}) {
|
|
25
|
+
if (options.historyPath) return options.historyPath;
|
|
26
|
+
if (process.env.THUMBGATE_CLAUDE_HISTORY_PATH) return process.env.THUMBGATE_CLAUDE_HISTORY_PATH;
|
|
27
|
+
const homeDir = options.homeDir || process.env.HOME || process.env.USERPROFILE || '';
|
|
28
|
+
return path.join(homeDir, '.claude', 'history.jsonl');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getSyncStatePath(options = {}) {
|
|
32
|
+
const feedbackDir = resolveFeedbackDir({ feedbackDir: options.feedbackDir });
|
|
33
|
+
return path.join(feedbackDir, SYNC_STATE_FILE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readSyncState(options = {}) {
|
|
37
|
+
const statePath = getSyncStatePath(options);
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
40
|
+
return {
|
|
41
|
+
historyOffset: Number(parsed.historyOffset || 0),
|
|
42
|
+
historySize: Number(parsed.historySize || 0),
|
|
43
|
+
processedIds: Array.isArray(parsed.processedIds) ? parsed.processedIds : [],
|
|
44
|
+
statePath,
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return {
|
|
48
|
+
historyOffset: 0,
|
|
49
|
+
historySize: 0,
|
|
50
|
+
processedIds: [],
|
|
51
|
+
statePath,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeSyncState(state, options = {}) {
|
|
57
|
+
const statePath = getSyncStatePath(options);
|
|
58
|
+
const payload = {
|
|
59
|
+
historyOffset: Number(state.historyOffset || 0),
|
|
60
|
+
historySize: Number(state.historySize || 0),
|
|
61
|
+
processedIds: Array.isArray(state.processedIds) ? state.processedIds.slice(-DEFAULT_PROCESSED_ID_LIMIT) : [],
|
|
62
|
+
updatedAt: new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
65
|
+
fs.writeFileSync(statePath, JSON.stringify(payload, null, 2));
|
|
66
|
+
return payload;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readHistoryEntriesSince(filePath, state) {
|
|
70
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
71
|
+
return {
|
|
72
|
+
entries: [],
|
|
73
|
+
nextOffset: 0,
|
|
74
|
+
size: 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stat = fs.statSync(filePath);
|
|
79
|
+
const safeOffset = state && state.historyOffset > 0 && state.historyOffset <= stat.size
|
|
80
|
+
? state.historyOffset
|
|
81
|
+
: 0;
|
|
82
|
+
|
|
83
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
84
|
+
const contents = fileBuffer.slice(safeOffset).toString('utf8');
|
|
85
|
+
|
|
86
|
+
const entries = contents
|
|
87
|
+
.split('\n')
|
|
88
|
+
.map((line) => line.trim())
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.map((line) => {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(line);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.filter(Boolean);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
entries,
|
|
101
|
+
nextOffset: stat.size,
|
|
102
|
+
size: stat.size,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeProjectPath(value) {
|
|
107
|
+
try {
|
|
108
|
+
return value ? path.resolve(String(value)) : null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseHistoryTimestamp(value) {
|
|
115
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
116
|
+
return value > 1e12 ? value : value * 1000;
|
|
117
|
+
}
|
|
118
|
+
const text = String(value || '').trim();
|
|
119
|
+
if (!text) return null;
|
|
120
|
+
if (/^\d+$/.test(text)) {
|
|
121
|
+
const numeric = Number(text);
|
|
122
|
+
return numeric > 1e12 ? numeric : numeric * 1000;
|
|
123
|
+
}
|
|
124
|
+
const parsed = Date.parse(text);
|
|
125
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function detectSignal(text) {
|
|
129
|
+
const normalized = String(text || '').toLowerCase();
|
|
130
|
+
if (/(thumbs?\s*down|that failed|that was wrong|fix this)/i.test(normalized)) return 'down';
|
|
131
|
+
if (/(thumbs?\s*up|that worked|looks good|nice work|perfect|good job)/i.test(normalized)) return 'up';
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function extractPromptText(entry) {
|
|
136
|
+
const candidates = [
|
|
137
|
+
entry && entry.display,
|
|
138
|
+
entry && entry.message && entry.message.content,
|
|
139
|
+
entry && entry.attachment && entry.attachment.prompt,
|
|
140
|
+
entry && entry.content,
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
for (const candidate of candidates) {
|
|
144
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
145
|
+
return candidate.trim();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildExternalId(entry, promptText) {
|
|
152
|
+
const hash = crypto.createHash('sha256');
|
|
153
|
+
hash.update(String(entry.sessionId || ''));
|
|
154
|
+
hash.update('|');
|
|
155
|
+
hash.update(String(entry.timestamp || ''));
|
|
156
|
+
hash.update('|');
|
|
157
|
+
hash.update(String(entry.project || entry.cwd || ''));
|
|
158
|
+
hash.update('|');
|
|
159
|
+
hash.update(String(promptText || ''));
|
|
160
|
+
return `claude-history:${hash.digest('hex')}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function toHistoryCandidate(entry, options = {}) {
|
|
164
|
+
const promptText = extractPromptText(entry);
|
|
165
|
+
const signal = detectSignal(promptText);
|
|
166
|
+
if (!signal) return null;
|
|
167
|
+
|
|
168
|
+
const projectDir = normalizeProjectPath(options.projectDir);
|
|
169
|
+
const entryProject = normalizeProjectPath(entry.project || entry.cwd || '');
|
|
170
|
+
if (projectDir && entryProject && entryProject !== projectDir && !entryProject.startsWith(`${projectDir}${path.sep}`)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
if (projectDir && !entryProject) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
externalId: buildExternalId(entry, promptText),
|
|
179
|
+
promptText,
|
|
180
|
+
signal,
|
|
181
|
+
timestampMs: parseHistoryTimestamp(entry.timestamp),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeCandidateText(value) {
|
|
186
|
+
return normalizeFeedbackText(String(value || '').replace(/\s+/g, ' '));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function hasMatchingFeedbackEntry(candidate, feedbackEntries) {
|
|
190
|
+
const candidateText = normalizeCandidateText(candidate.promptText);
|
|
191
|
+
if (!candidateText) return false;
|
|
192
|
+
|
|
193
|
+
return feedbackEntries.some((entry) => {
|
|
194
|
+
const signal = entry && entry.signal === 'negative' ? 'down' : 'up';
|
|
195
|
+
if (signal !== candidate.signal) return false;
|
|
196
|
+
|
|
197
|
+
const feedbackText = normalizeCandidateText(
|
|
198
|
+
entry.submittedContext
|
|
199
|
+
|| entry.context
|
|
200
|
+
|| entry.whatWentWrong
|
|
201
|
+
|| entry.whatWorked
|
|
202
|
+
|| ''
|
|
203
|
+
);
|
|
204
|
+
if (feedbackText !== candidateText) return false;
|
|
205
|
+
|
|
206
|
+
const feedbackTimestamp = Date.parse(entry.timestamp || '');
|
|
207
|
+
if (!Number.isFinite(feedbackTimestamp) || !Number.isFinite(candidate.timestampMs)) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return Math.abs(feedbackTimestamp - candidate.timestampMs) <= DUPLICATE_WINDOW_MS;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function syncClaudeHistoryFeedback(options = {}) {
|
|
215
|
+
if (options.disabled || process.env.THUMBGATE_DISABLE_CLAUDE_HISTORY_SYNC === '1') {
|
|
216
|
+
return {
|
|
217
|
+
importedCount: 0,
|
|
218
|
+
skippedCount: 0,
|
|
219
|
+
reason: 'disabled',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const originalEnv = {
|
|
224
|
+
THUMBGATE_FEEDBACK_DIR: process.env.THUMBGATE_FEEDBACK_DIR,
|
|
225
|
+
THUMBGATE_PROJECT_DIR: process.env.THUMBGATE_PROJECT_DIR,
|
|
226
|
+
THUMBGATE_CLAUDE_HISTORY_PATH: process.env.THUMBGATE_CLAUDE_HISTORY_PATH,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if (options.feedbackDir) process.env.THUMBGATE_FEEDBACK_DIR = options.feedbackDir;
|
|
230
|
+
if (options.projectDir && !options.feedbackDir) process.env.THUMBGATE_PROJECT_DIR = options.projectDir;
|
|
231
|
+
if (options.historyPath) process.env.THUMBGATE_CLAUDE_HISTORY_PATH = options.historyPath;
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const feedbackDir = resolveFeedbackDir({ feedbackDir: options.feedbackDir });
|
|
235
|
+
const projectDir = normalizeProjectPath(options.projectDir) || resolveProjectDir({
|
|
236
|
+
cwd: process.cwd(),
|
|
237
|
+
env: process.env,
|
|
238
|
+
});
|
|
239
|
+
const historyPath = getClaudeHistoryPath(options);
|
|
240
|
+
const state = readSyncState({ feedbackDir });
|
|
241
|
+
const history = readHistoryEntriesSince(historyPath, state);
|
|
242
|
+
const existingEntries = readJSONL(path.join(feedbackDir, 'feedback-log.jsonl'), {
|
|
243
|
+
maxLines: DEFAULT_RECENT_FEEDBACK_LIMIT,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
let importedCount = 0;
|
|
247
|
+
let skippedCount = 0;
|
|
248
|
+
const processedIds = new Set(state.processedIds || []);
|
|
249
|
+
|
|
250
|
+
for (const entry of history.entries) {
|
|
251
|
+
const candidate = toHistoryCandidate(entry, { projectDir });
|
|
252
|
+
if (!candidate) continue;
|
|
253
|
+
if (processedIds.has(candidate.externalId)) {
|
|
254
|
+
skippedCount += 1;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (hasMatchingFeedbackEntry(candidate, existingEntries)) {
|
|
259
|
+
processedIds.add(candidate.externalId);
|
|
260
|
+
skippedCount += 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const captureResult = captureFeedback({
|
|
265
|
+
signal: candidate.signal,
|
|
266
|
+
context: candidate.promptText,
|
|
267
|
+
whatWentWrong: candidate.signal === 'down' ? candidate.promptText : undefined,
|
|
268
|
+
whatWorked: candidate.signal === 'up' ? candidate.promptText : undefined,
|
|
269
|
+
tags: ['claude-history-sync', 'auto-capture-fallback'],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (captureResult && captureResult.feedbackEvent) {
|
|
273
|
+
existingEntries.push(captureResult.feedbackEvent);
|
|
274
|
+
}
|
|
275
|
+
processedIds.add(candidate.externalId);
|
|
276
|
+
importedCount += 1;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
writeSyncState({
|
|
280
|
+
historyOffset: history.nextOffset,
|
|
281
|
+
historySize: history.size,
|
|
282
|
+
processedIds: Array.from(processedIds),
|
|
283
|
+
}, { feedbackDir });
|
|
284
|
+
|
|
285
|
+
if (importedCount > 0) {
|
|
286
|
+
refreshStatuslineCache(analyzeFeedback(path.join(feedbackDir, 'feedback-log.jsonl')), path.join(feedbackDir, 'statusline_cache.json'));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
importedCount,
|
|
291
|
+
skippedCount,
|
|
292
|
+
historyPath,
|
|
293
|
+
feedbackDir,
|
|
294
|
+
projectDir,
|
|
295
|
+
};
|
|
296
|
+
} finally {
|
|
297
|
+
if (originalEnv.THUMBGATE_FEEDBACK_DIR == null) delete process.env.THUMBGATE_FEEDBACK_DIR;
|
|
298
|
+
else process.env.THUMBGATE_FEEDBACK_DIR = originalEnv.THUMBGATE_FEEDBACK_DIR;
|
|
299
|
+
|
|
300
|
+
if (originalEnv.THUMBGATE_PROJECT_DIR == null) delete process.env.THUMBGATE_PROJECT_DIR;
|
|
301
|
+
else process.env.THUMBGATE_PROJECT_DIR = originalEnv.THUMBGATE_PROJECT_DIR;
|
|
302
|
+
|
|
303
|
+
if (originalEnv.THUMBGATE_CLAUDE_HISTORY_PATH == null) delete process.env.THUMBGATE_CLAUDE_HISTORY_PATH;
|
|
304
|
+
else process.env.THUMBGATE_CLAUDE_HISTORY_PATH = originalEnv.THUMBGATE_CLAUDE_HISTORY_PATH;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
SYNC_STATE_FILE,
|
|
310
|
+
detectSignal,
|
|
311
|
+
extractPromptText,
|
|
312
|
+
getClaudeHistoryPath,
|
|
313
|
+
hasMatchingFeedbackEntry,
|
|
314
|
+
parseHistoryTimestamp,
|
|
315
|
+
readHistoryEntriesSince,
|
|
316
|
+
readSyncState,
|
|
317
|
+
syncClaudeHistoryFeedback,
|
|
318
|
+
toHistoryCandidate,
|
|
319
|
+
writeSyncState,
|
|
320
|
+
};
|
package/scripts/cli-telemetry.js
CHANGED
|
@@ -5,7 +5,9 @@ const crypto = require('crypto');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const _DEFAULT_TELEMETRY_HOST = 'https://thumbgate-production.up.railway.app';
|
|
9
|
+
// Respect THUMBGATE_API_URL so test environments can point to a local stub
|
|
10
|
+
const TELEMETRY_ENDPOINT = `${process.env.THUMBGATE_API_URL || _DEFAULT_TELEMETRY_HOST}/v1/telemetry/ping`;
|
|
9
11
|
const INSTALL_ID_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.thumbgate', 'install-id');
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -80,6 +82,7 @@ function trackEvent(eventType, metadata = {}) {
|
|
|
80
82
|
});
|
|
81
83
|
req.on('error', () => {}); // silently ignore
|
|
82
84
|
req.on('timeout', () => req.destroy());
|
|
85
|
+
req.on('socket', (s) => s.unref()); // fire-and-forget: never block process exit
|
|
83
86
|
req.end(payload);
|
|
84
87
|
} catch (_) {} // never crash the CLI
|
|
85
88
|
}
|
package/scripts/contextfs.js
CHANGED
|
@@ -23,9 +23,15 @@ function getFeedbackBaseDir() {
|
|
|
23
23
|
return resolveFeedbackDir();
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
26
|
+
function getContextFsRoot() {
|
|
27
|
+
const feedbackDir = getFeedbackBaseDir();
|
|
28
|
+
if (process.env.THUMBGATE_CONTEXTFS_DIR) return process.env.THUMBGATE_CONTEXTFS_DIR;
|
|
29
|
+
return feedbackDir.endsWith('contextfs') ? feedbackDir : path.join(feedbackDir, 'contextfs');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function contextFsPath(...segments) {
|
|
33
|
+
return path.join(getContextFsRoot(), ...segments);
|
|
34
|
+
}
|
|
29
35
|
|
|
30
36
|
const NAMESPACES = {
|
|
31
37
|
rawHistory: 'raw_history',
|
|
@@ -101,7 +107,7 @@ function ensureDir(dirPath) {
|
|
|
101
107
|
|
|
102
108
|
function ensureContextFs() {
|
|
103
109
|
Object.values(NAMESPACES).forEach((subPath) => {
|
|
104
|
-
ensureDir(
|
|
110
|
+
ensureDir(contextFsPath(subPath));
|
|
105
111
|
});
|
|
106
112
|
}
|
|
107
113
|
|
|
@@ -111,7 +117,7 @@ function nowIso() {
|
|
|
111
117
|
|
|
112
118
|
function inferNamespaceFromPath(filePath) {
|
|
113
119
|
if (!filePath) return '';
|
|
114
|
-
const relativeDir = path.relative(
|
|
120
|
+
const relativeDir = path.relative(getContextFsRoot(), path.dirname(filePath));
|
|
115
121
|
if (!relativeDir || relativeDir.startsWith('..')) return '';
|
|
116
122
|
return relativeDir;
|
|
117
123
|
}
|
|
@@ -211,7 +217,7 @@ function getSemanticCacheConfig() {
|
|
|
211
217
|
}
|
|
212
218
|
|
|
213
219
|
function getSemanticCachePath() {
|
|
214
|
-
return
|
|
220
|
+
return contextFsPath(NAMESPACES.provenance, 'semantic-cache.jsonl');
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
function loadSemanticCacheEntries() {
|
|
@@ -227,7 +233,7 @@ function getSourceHash(namespaces) {
|
|
|
227
233
|
const normalizedNamespaces = normalizeNamespaces(namespaces);
|
|
228
234
|
|
|
229
235
|
for (const ns of normalizedNamespaces) {
|
|
230
|
-
const dirPath =
|
|
236
|
+
const dirPath = contextFsPath(ns);
|
|
231
237
|
if (!fs.existsSync(dirPath)) continue;
|
|
232
238
|
|
|
233
239
|
const files = fs.readdirSync(dirPath).sort();
|
|
@@ -291,7 +297,7 @@ function recordProvenance(event) {
|
|
|
291
297
|
timestamp: nowIso(),
|
|
292
298
|
...event,
|
|
293
299
|
};
|
|
294
|
-
appendJsonl(
|
|
300
|
+
appendJsonl(contextFsPath(NAMESPACES.provenance, 'events.jsonl'), payload);
|
|
295
301
|
return payload;
|
|
296
302
|
}
|
|
297
303
|
|
|
@@ -299,7 +305,7 @@ function writeContextObject({ namespace, title, content, tags = [], source, ttl
|
|
|
299
305
|
ensureContextFs();
|
|
300
306
|
|
|
301
307
|
const id = `${Date.now()}_${toSlug(title)}`;
|
|
302
|
-
const filePath =
|
|
308
|
+
const filePath = contextFsPath(namespace, `${id}.json`);
|
|
303
309
|
|
|
304
310
|
const doc = {
|
|
305
311
|
id,
|
|
@@ -361,7 +367,7 @@ function findExistingContextObject({ namespace, title, content, tags = [], sourc
|
|
|
361
367
|
ensureContextFs();
|
|
362
368
|
|
|
363
369
|
const expectedTags = normalizeTagList(tags);
|
|
364
|
-
const dirPath =
|
|
370
|
+
const dirPath = contextFsPath(namespace);
|
|
365
371
|
const files = listJsonFiles(dirPath).sort();
|
|
366
372
|
|
|
367
373
|
for (const filePath of files) {
|
|
@@ -507,7 +513,7 @@ function loadCandidates(namespaces) {
|
|
|
507
513
|
const docs = [];
|
|
508
514
|
|
|
509
515
|
selected.forEach((namespace) => {
|
|
510
|
-
const dir =
|
|
516
|
+
const dir = contextFsPath(namespace);
|
|
511
517
|
const files = listJsonFiles(dir);
|
|
512
518
|
files.forEach((filePath) => {
|
|
513
519
|
try {
|
|
@@ -624,7 +630,7 @@ function selectFlatContextItems(candidates, maxItems, maxChars) {
|
|
|
624
630
|
const MEMEX_INDEX_FILE = 'memex-index.jsonl';
|
|
625
631
|
|
|
626
632
|
function getMemexIndexPath() {
|
|
627
|
-
return
|
|
633
|
+
return contextFsPath(NAMESPACES.provenance, MEMEX_INDEX_FILE);
|
|
628
634
|
}
|
|
629
635
|
|
|
630
636
|
function buildIndexEntry(doc, filePath) {
|
|
@@ -751,7 +757,7 @@ function constructMemexPack({ query = '', maxItems = 8, maxChars = 6000, namespa
|
|
|
751
757
|
cache: { hit: false },
|
|
752
758
|
};
|
|
753
759
|
|
|
754
|
-
appendJsonl(
|
|
760
|
+
appendJsonl(contextFsPath(NAMESPACES.provenance, 'packs.jsonl'), pack);
|
|
755
761
|
recordProvenance({
|
|
756
762
|
type: 'memex_pack_constructed',
|
|
757
763
|
packId,
|
|
@@ -792,7 +798,7 @@ function constructContextPack({ query = '', maxItems = 8, maxChars = 6000, names
|
|
|
792
798
|
},
|
|
793
799
|
};
|
|
794
800
|
|
|
795
|
-
appendJsonl(
|
|
801
|
+
appendJsonl(contextFsPath(NAMESPACES.provenance, 'packs.jsonl'), pack);
|
|
796
802
|
recordProvenance({
|
|
797
803
|
type: 'context_pack_cache_hit',
|
|
798
804
|
packId,
|
|
@@ -861,7 +867,7 @@ function constructContextPack({ query = '', maxItems = 8, maxChars = 6000, names
|
|
|
861
867
|
retrieval: selection.retrieval,
|
|
862
868
|
};
|
|
863
869
|
|
|
864
|
-
appendJsonl(
|
|
870
|
+
appendJsonl(contextFsPath(NAMESPACES.provenance, 'packs.jsonl'), pack);
|
|
865
871
|
appendSemanticCacheEntry({
|
|
866
872
|
id: `cache_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
867
873
|
timestamp: nowIso(),
|
|
@@ -899,7 +905,7 @@ function evaluateContextPack({ packId, outcome, signal = null, notes = '', rubri
|
|
|
899
905
|
timestamp: nowIso(),
|
|
900
906
|
};
|
|
901
907
|
|
|
902
|
-
appendJsonl(
|
|
908
|
+
appendJsonl(contextFsPath(NAMESPACES.provenance, 'evaluations.jsonl'), evaluation);
|
|
903
909
|
recordProvenance({
|
|
904
910
|
type: 'context_pack_evaluated',
|
|
905
911
|
packId,
|
|
@@ -912,7 +918,7 @@ function evaluateContextPack({ packId, outcome, signal = null, notes = '', rubri
|
|
|
912
918
|
}
|
|
913
919
|
|
|
914
920
|
function getProvenance(limit = 50) {
|
|
915
|
-
const eventsPath =
|
|
921
|
+
const eventsPath = contextFsPath(NAMESPACES.provenance, 'events.jsonl');
|
|
916
922
|
const events = readJsonl(eventsPath);
|
|
917
923
|
return events.slice(-limit);
|
|
918
924
|
}
|
|
@@ -923,7 +929,7 @@ function getProvenance(limit = 50) {
|
|
|
923
929
|
* session starts with full context — no manual primer.md needed.
|
|
924
930
|
*/
|
|
925
931
|
function writeSessionHandoff({ project, branch, lastTask, nextStep, blockers, openFiles, customContext } = {}) {
|
|
926
|
-
ensureDir(
|
|
932
|
+
ensureDir(contextFsPath(NAMESPACES.session));
|
|
927
933
|
|
|
928
934
|
let gitContext = {};
|
|
929
935
|
try {
|
|
@@ -951,7 +957,7 @@ function writeSessionHandoff({ project, branch, lastTask, nextStep, blockers, op
|
|
|
951
957
|
customContext: customContext || null,
|
|
952
958
|
};
|
|
953
959
|
|
|
954
|
-
const primerPath =
|
|
960
|
+
const primerPath = contextFsPath(NAMESPACES.session, 'primer.json');
|
|
955
961
|
fs.writeFileSync(primerPath, JSON.stringify(primer, null, 2));
|
|
956
962
|
|
|
957
963
|
// Sync to primer.md if it exists
|
|
@@ -991,7 +997,7 @@ function writeSessionHandoff({ project, branch, lastTask, nextStep, blockers, op
|
|
|
991
997
|
* Read the most recent session handoff primer.
|
|
992
998
|
*/
|
|
993
999
|
function readSessionHandoff() {
|
|
994
|
-
const primerPath =
|
|
1000
|
+
const primerPath = contextFsPath(NAMESPACES.session, 'primer.json');
|
|
995
1001
|
if (!fs.existsSync(primerPath)) return null;
|
|
996
1002
|
try {
|
|
997
1003
|
return JSON.parse(fs.readFileSync(primerPath, 'utf8'));
|
|
@@ -1192,7 +1198,7 @@ function constructMultiHopPack({ query = '', maxItems = 8, maxChars = 6000, name
|
|
|
1192
1198
|
},
|
|
1193
1199
|
};
|
|
1194
1200
|
|
|
1195
|
-
appendJsonl(
|
|
1201
|
+
appendJsonl(contextFsPath(NAMESPACES.provenance, 'packs.jsonl'), pack);
|
|
1196
1202
|
appendSemanticCacheEntry({
|
|
1197
1203
|
id: `cache_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
1198
1204
|
timestamp: nowIso(),
|
|
@@ -1244,7 +1250,10 @@ function listPackTemplates() {
|
|
|
1244
1250
|
}
|
|
1245
1251
|
|
|
1246
1252
|
module.exports = {
|
|
1247
|
-
CONTEXTFS_ROOT
|
|
1253
|
+
get CONTEXTFS_ROOT() {
|
|
1254
|
+
return getContextFsRoot();
|
|
1255
|
+
},
|
|
1256
|
+
getContextFsRoot,
|
|
1248
1257
|
NAMESPACES,
|
|
1249
1258
|
ensureContextFs,
|
|
1250
1259
|
recordProvenance,
|
|
@@ -1283,5 +1292,5 @@ module.exports = {
|
|
|
1283
1292
|
|
|
1284
1293
|
if (require.main === module) {
|
|
1285
1294
|
ensureContextFs();
|
|
1286
|
-
console.log(`ContextFS ready at ${
|
|
1295
|
+
console.log(`ContextFS ready at ${getContextFsRoot()}`);
|
|
1287
1296
|
}
|
package/scripts/dashboard.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { aggregateFailureDiagnostics } = require('./failure-diagnostics');
|
|
7
|
+
const { AUDIT_LOG_FILENAME } = require('./audit-trail');
|
|
7
8
|
const { getBillingSummary, loadFunnelLedger, loadResolvedRevenueEvents } = require('./billing');
|
|
8
9
|
const { getTelemetryAnalytics, loadTelemetryEvents } = require('./telemetry-analytics');
|
|
9
10
|
const { getAutoGatesPath } = require('./auto-promote-gates');
|
|
@@ -19,6 +20,7 @@ const { routeProfile } = require('./profile-router');
|
|
|
19
20
|
const { getSettingsStatus } = require('./settings-hierarchy');
|
|
20
21
|
const { summarizeWorkflowRuns } = require('./workflow-runs');
|
|
21
22
|
const { searchLessons } = require('./lesson-search');
|
|
23
|
+
const { getInterventionPolicySummary } = require('./intervention-policy');
|
|
22
24
|
|
|
23
25
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
24
26
|
const DEFAULT_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
|
|
@@ -60,6 +62,15 @@ function pickFirstText(...values) {
|
|
|
60
62
|
return null;
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
function toLocalDayKey(value) {
|
|
66
|
+
const ts = value instanceof Date ? value : new Date(value);
|
|
67
|
+
if (Number.isNaN(ts.getTime())) return null;
|
|
68
|
+
const year = ts.getFullYear();
|
|
69
|
+
const month = String(ts.getMonth() + 1).padStart(2, '0');
|
|
70
|
+
const day = String(ts.getDate()).padStart(2, '0');
|
|
71
|
+
return `${year}-${month}-${day}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
// ---------------------------------------------------------------------------
|
|
64
75
|
// Approval rate + trend
|
|
65
76
|
// ---------------------------------------------------------------------------
|
|
@@ -143,6 +154,58 @@ function computeGateStats() {
|
|
|
143
154
|
};
|
|
144
155
|
}
|
|
145
156
|
|
|
157
|
+
function computeGateAuditSeries(feedbackDir, options = {}) {
|
|
158
|
+
const auditLogPath = path.join(feedbackDir, AUDIT_LOG_FILENAME);
|
|
159
|
+
const entries = readJSONL(auditLogPath).filter((entry) => entry && entry.timestamp);
|
|
160
|
+
const dayCount = Number.isInteger(options.dayCount) ? options.dayCount : 14;
|
|
161
|
+
const today = new Date();
|
|
162
|
+
today.setHours(0, 0, 0, 0);
|
|
163
|
+
const countsByDay = new Map();
|
|
164
|
+
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (!['allow', 'deny', 'warn'].includes(entry.decision)) continue;
|
|
167
|
+
const dayKey = toLocalDayKey(entry.timestamp);
|
|
168
|
+
if (!dayKey) continue;
|
|
169
|
+
if (!countsByDay.has(dayKey)) {
|
|
170
|
+
countsByDay.set(dayKey, { allow: 0, deny: 0, warn: 0 });
|
|
171
|
+
}
|
|
172
|
+
countsByDay.get(dayKey)[entry.decision] += 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const days = [];
|
|
176
|
+
const totals = { allow: 0, deny: 0, warn: 0, intercepted: 0, total: 0 };
|
|
177
|
+
|
|
178
|
+
for (let offset = dayCount - 1; offset >= 0; offset -= 1) {
|
|
179
|
+
const day = new Date(today);
|
|
180
|
+
day.setDate(today.getDate() - offset);
|
|
181
|
+
const dayKey = toLocalDayKey(day);
|
|
182
|
+
const record = countsByDay.get(dayKey) || { allow: 0, deny: 0, warn: 0 };
|
|
183
|
+
const intercepted = record.deny + record.warn;
|
|
184
|
+
const total = intercepted + record.allow;
|
|
185
|
+
const summary = {
|
|
186
|
+
dayKey,
|
|
187
|
+
allow: record.allow,
|
|
188
|
+
deny: record.deny,
|
|
189
|
+
warn: record.warn,
|
|
190
|
+
intercepted,
|
|
191
|
+
total,
|
|
192
|
+
};
|
|
193
|
+
totals.allow += record.allow;
|
|
194
|
+
totals.deny += record.deny;
|
|
195
|
+
totals.warn += record.warn;
|
|
196
|
+
totals.intercepted += intercepted;
|
|
197
|
+
totals.total += total;
|
|
198
|
+
days.push(summary);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
dayCount,
|
|
203
|
+
days,
|
|
204
|
+
totals,
|
|
205
|
+
activeDays: days.filter((day) => day.total > 0).length,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
146
209
|
function listActiveGates() {
|
|
147
210
|
try {
|
|
148
211
|
const config = loadGatesConfig();
|
|
@@ -710,6 +773,7 @@ function generateDashboard(feedbackDir, options = {}) {
|
|
|
710
773
|
const prevention = computePreventionImpact(feedbackDir, gateStats);
|
|
711
774
|
const trend = computeSessionTrend(entries, 10);
|
|
712
775
|
const health = computeSystemHealth(feedbackDir, gateStats);
|
|
776
|
+
const gateAudit = computeGateAuditSeries(feedbackDir);
|
|
713
777
|
const diagnostics = aggregateFailureDiagnostics([...entries, ...diagnosticEntries]);
|
|
714
778
|
const secretGuard = computeSecretGuardStats(diagnosticEntries);
|
|
715
779
|
const gates = listActiveGates();
|
|
@@ -722,6 +786,7 @@ function generateDashboard(feedbackDir, options = {}) {
|
|
|
722
786
|
const delegation = summarizeDelegation(feedbackDir);
|
|
723
787
|
const readiness = generateAgentReadinessReport({ projectRoot: PROJECT_ROOT });
|
|
724
788
|
const harness = computeHarnessOverview(feedbackDir, entries);
|
|
789
|
+
const interventionPolicy = getInterventionPolicySummary(feedbackDir);
|
|
725
790
|
const settingsStatus = getSettingsStatus({ projectRoot: PROJECT_ROOT });
|
|
726
791
|
settingsStatus.routingPreview = {
|
|
727
792
|
dashboardTool: routeProfile({
|
|
@@ -782,6 +847,7 @@ function generateDashboard(feedbackDir, options = {}) {
|
|
|
782
847
|
prevention,
|
|
783
848
|
trend,
|
|
784
849
|
health,
|
|
850
|
+
gateAudit,
|
|
785
851
|
diagnostics,
|
|
786
852
|
delegation,
|
|
787
853
|
secretGuard,
|
|
@@ -790,6 +856,7 @@ function generateDashboard(feedbackDir, options = {}) {
|
|
|
790
856
|
observability,
|
|
791
857
|
instrumentation,
|
|
792
858
|
readiness,
|
|
859
|
+
interventionPolicy,
|
|
793
860
|
settingsStatus,
|
|
794
861
|
team,
|
|
795
862
|
templateLibrary,
|
|
@@ -809,6 +876,7 @@ function printDashboard(data) {
|
|
|
809
876
|
prevention,
|
|
810
877
|
trend,
|
|
811
878
|
health,
|
|
879
|
+
gateAudit,
|
|
812
880
|
diagnostics,
|
|
813
881
|
delegation,
|
|
814
882
|
secretGuard,
|
|
@@ -817,6 +885,7 @@ function printDashboard(data) {
|
|
|
817
885
|
observability,
|
|
818
886
|
instrumentation,
|
|
819
887
|
readiness,
|
|
888
|
+
interventionPolicy,
|
|
820
889
|
settingsStatus,
|
|
821
890
|
team,
|
|
822
891
|
templateLibrary,
|
|
@@ -862,6 +931,20 @@ function printDashboard(data) {
|
|
|
862
931
|
console.log(` Top Next Fix : ${harness.topRecommendations[0].type} (${harness.topRecommendations[0].count} lessons)`);
|
|
863
932
|
}
|
|
864
933
|
|
|
934
|
+
console.log('');
|
|
935
|
+
console.log('🧠 Learned Policy');
|
|
936
|
+
console.log(` Enabled : ${interventionPolicy.enabled ? 'yes' : 'no'}`);
|
|
937
|
+
console.log(` Examples : ${interventionPolicy.exampleCount}`);
|
|
938
|
+
console.log(` Train Accuracy : ${Math.round((interventionPolicy.metrics.trainingAccuracy || 0) * 100)}%`);
|
|
939
|
+
console.log(` Holdout Accuracy : ${Math.round((interventionPolicy.metrics.holdoutAccuracy || 0) * 100)}%`);
|
|
940
|
+
console.log(` Recent Pressure : ${Math.round((interventionPolicy.nonAllowRate || 0) * 100)}% non-allow`);
|
|
941
|
+
if (interventionPolicy.updatedAt) {
|
|
942
|
+
console.log(` Updated : ${interventionPolicy.updatedAt}`);
|
|
943
|
+
}
|
|
944
|
+
if (interventionPolicy.topTokens && interventionPolicy.topTokens.deny && interventionPolicy.topTokens.deny[0]) {
|
|
945
|
+
console.log(` Top Deny Signal : ${interventionPolicy.topTokens.deny[0].token}`);
|
|
946
|
+
}
|
|
947
|
+
|
|
865
948
|
console.log('');
|
|
866
949
|
console.log('🎯 North Star');
|
|
867
950
|
console.log(` Weekly Proof Runs: ${analytics.northStar.weeklyActiveProofBackedWorkflowRuns}`);
|
|
@@ -1043,6 +1126,7 @@ module.exports = {
|
|
|
1043
1126
|
computeSystemHealth,
|
|
1044
1127
|
computeEfficiencyMetrics,
|
|
1045
1128
|
computeHarnessOverview,
|
|
1129
|
+
getInterventionPolicySummary,
|
|
1046
1130
|
computeAnalyticsSummary,
|
|
1047
1131
|
computeSecretGuardStats,
|
|
1048
1132
|
computeObservabilityStats,
|