thumbgate 1.3.0 → 1.4.1
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 +25 -0
- 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 +242 -126
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/INSTALL.md +59 -4
- package/adapters/chatgpt/openapi.yaml +168 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +84 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +204 -13
- package/bin/postinstall.js +8 -2
- package/config/budget.json +18 -0
- package/config/gates/code-edit.json +61 -0
- package/config/gates/db-write.json +61 -0
- package/config/gates/default.json +154 -3
- package/config/gates/deploy.json +61 -0
- package/config/github-about.json +2 -1
- package/config/merge-quality-checks.json +23 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +47 -11
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +27 -4
- package/plugins/codex-profile/README.md +33 -9
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +73 -0
- package/public/compare/mem0.html +189 -0
- package/public/compare/speclock.html +180 -0
- package/public/compare.html +10 -2
- package/public/guide.html +2 -2
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/codex-cli-guardrails.html +158 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/pre-action-gates.html +162 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
- package/public/index.html +172 -65
- package/public/lessons.html +33 -24
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/access-anomaly-detector.js +1 -1
- package/scripts/adk-consolidator.js +1 -5
- package/scripts/agent-security-hardening.js +4 -6
- package/scripts/agentic-data-pipeline.js +1 -3
- package/scripts/async-job-runner.js +1 -5
- package/scripts/audit-trail.js +1 -5
- package/scripts/auto-promote-gates.js +5 -3
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing-setup.js +109 -0
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-claude-mcpb.js +71 -5
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/check-congruence.js +132 -14
- package/scripts/commercial-offer.js +5 -7
- package/scripts/content-engine/linkedin-content-generator.js +154 -0
- package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
- package/scripts/content-engine/reddit-thread-finder.js +154 -0
- package/scripts/context-engine.js +21 -6
- package/scripts/contextfs.js +1 -21
- package/scripts/dashboard.js +20 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +54 -0
- package/scripts/document-intake.js +927 -0
- package/scripts/ephemeral-agent-store.js +1 -8
- package/scripts/evolution-state.js +1 -5
- package/scripts/experiment-tracker.js +1 -5
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-hf-dataset.js +1 -5
- package/scripts/export-training.js +1 -5
- package/scripts/feedback-attribution.js +1 -16
- package/scripts/feedback-history-distiller.js +1 -16
- package/scripts/feedback-loop.js +1 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +215 -36
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +200 -11
- 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-config.js +2 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +33 -49
- package/scripts/intervention-policy.js +58 -1
- package/scripts/lesson-db.js +3 -18
- package/scripts/lesson-inference.js +194 -16
- package/scripts/lesson-retrieval.js +60 -24
- package/scripts/llm-client.js +59 -0
- package/scripts/managed-lesson-agent.js +183 -0
- package/scripts/marketing-experiment.js +8 -22
- package/scripts/meta-agent-loop.js +624 -0
- package/scripts/metered-billing.js +1 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +15 -3
- package/scripts/operational-summary.js +41 -5
- package/scripts/org-dashboard.js +6 -1
- package/scripts/per-step-scoring.js +2 -4
- package/scripts/pr-manager.js +201 -19
- package/scripts/pro-features.js +3 -2
- package/scripts/prompt-dlp.js +3 -3
- package/scripts/prove-adapters.js +1 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +1 -3
- package/scripts/prove-cloudflare-sandbox.js +1 -3
- package/scripts/prove-data-pipeline.js +1 -3
- package/scripts/prove-intelligence.js +1 -3
- package/scripts/prove-lancedb.js +1 -5
- package/scripts/prove-local-intelligence.js +1 -3
- package/scripts/prove-packaged-runtime.js +75 -9
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/ralph-loop.js +376 -0
- package/scripts/ralph-mode-ci.js +331 -0
- package/scripts/rate-limiter.js +3 -1
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/rotate-stripe-webhook-secret.js +314 -0
- package/scripts/schedule-manager.js +3 -5
- package/scripts/security-scanner.js +448 -0
- package/scripts/self-distill-agent.js +579 -0
- package/scripts/semantic-dedup.js +115 -0
- package/scripts/skill-exporter.js +1 -3
- package/scripts/skill-generator.js +1 -5
- package/scripts/social-analytics/engagement-audit.js +1 -18
- package/scripts/social-analytics/pollers/linkedin.js +26 -16
- package/scripts/social-analytics/publishers/linkedin.js +1 -1
- package/scripts/social-analytics/publishers/zernio.js +51 -0
- package/scripts/social-pipeline.js +1 -3
- package/scripts/social-post-hourly.js +47 -4
- package/scripts/statusline-links.js +6 -5
- package/scripts/statusline.sh +29 -153
- package/scripts/sync-branch-protection.js +340 -0
- package/scripts/tessl-export.js +1 -3
- package/scripts/thumbgate-search.js +32 -1
- package/scripts/tool-kpi-tracker.js +1 -1
- package/scripts/tool-registry.js +106 -2
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +91 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +296 -7
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- 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,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Harness Selector — Context-Aware Gate Harness Loading
|
|
5
|
+
*
|
|
6
|
+
* Auto Agent concept: instead of one monolithic gate config, select a
|
|
7
|
+
* specialized harness based on the workflow type detected from the tool call.
|
|
8
|
+
*
|
|
9
|
+
* Detection priority (first match wins):
|
|
10
|
+
* 1. THUMBGATE_HARNESS env var — explicit override
|
|
11
|
+
* 2. Tool-name heuristic (Edit/Write/MultiEdit → code-edit)
|
|
12
|
+
* 3. Command-text heuristic (deploy keywords → deploy, SQL keywords → db-write)
|
|
13
|
+
* 4. null → load only default.json + auto-promoted gates
|
|
14
|
+
*
|
|
15
|
+
* Each harness is ADDITIVE — default.json gates always load first.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const HARNESS_DIR = path.join(__dirname, '..', 'config', 'gates');
|
|
21
|
+
|
|
22
|
+
const HARNESSES = Object.freeze({
|
|
23
|
+
deploy: path.join(HARNESS_DIR, 'deploy.json'),
|
|
24
|
+
'code-edit': path.join(HARNESS_DIR, 'code-edit.json'),
|
|
25
|
+
'db-write': path.join(HARNESS_DIR, 'db-write.json'),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Detection patterns
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const DEPLOY_PATTERNS = [
|
|
33
|
+
/\brailway\s+(deploy|up|run)\b/i,
|
|
34
|
+
/\bdocker\s+(push|build)\b/i,
|
|
35
|
+
/\bnpm\s+publish\b/i,
|
|
36
|
+
/\byarn\s+publish\b/i,
|
|
37
|
+
/\bpnpm\s+publish\b/i,
|
|
38
|
+
/\bgit\s+push\b/i,
|
|
39
|
+
/\bgh\s+pr\s+(create|merge)\b/i,
|
|
40
|
+
/\bchangeset\s+(publish|version)\b/i,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const DB_WRITE_PATTERNS = [
|
|
44
|
+
/\b(DROP|TRUNCATE|DELETE|ALTER|INSERT|UPDATE)\s+(TABLE|FROM|INTO|COLUMN)\b/i,
|
|
45
|
+
/\b(sqlite3|better-sqlite3|knex|sequelize)\b.*\.(run|exec|query)\b/i,
|
|
46
|
+
/\brm\s+.*\.sqlite\b/i,
|
|
47
|
+
/\blancedb\b.*(?:create|delete|drop|truncate)/i,
|
|
48
|
+
/\.db\.exec\(|\.db\.prepare\(/i,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const CODE_EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Public API
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Given a tool name and input, return the path to the best matching
|
|
59
|
+
* specialized harness config, or null if none applies.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} toolName - e.g. "Bash", "Edit", "Write"
|
|
62
|
+
* @param {object|string} toolInput - raw tool input object or string
|
|
63
|
+
* @returns {string|null} absolute path to harness JSON, or null
|
|
64
|
+
*/
|
|
65
|
+
function selectHarness(toolName, toolInput) {
|
|
66
|
+
// 1. Explicit override
|
|
67
|
+
if (process.env.THUMBGATE_HARNESS) {
|
|
68
|
+
const override = process.env.THUMBGATE_HARNESS;
|
|
69
|
+
if (HARNESSES[override]) return HARNESSES[override];
|
|
70
|
+
// Allow absolute path override
|
|
71
|
+
if (path.isAbsolute(override)) return override;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. Edit/Write tools always get code-edit harness
|
|
75
|
+
if (CODE_EDIT_TOOL_NAMES.has(toolName)) {
|
|
76
|
+
return HARNESSES['code-edit'];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Inspect command text for Bash tool
|
|
80
|
+
const commandText = extractCommandText(toolInput);
|
|
81
|
+
if (commandText) {
|
|
82
|
+
if (DB_WRITE_PATTERNS.some((p) => p.test(commandText))) {
|
|
83
|
+
return HARNESSES['db-write'];
|
|
84
|
+
}
|
|
85
|
+
if (DEPLOY_PATTERNS.some((p) => p.test(commandText))) {
|
|
86
|
+
return HARNESSES['deploy'];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Return the harness name (e.g. "deploy") for a given tool call, or null.
|
|
95
|
+
*/
|
|
96
|
+
function selectHarnessName(toolName, toolInput) {
|
|
97
|
+
const harnessPath = selectHarness(toolName, toolInput);
|
|
98
|
+
if (!harnessPath) return null;
|
|
99
|
+
return Object.entries(HARNESSES).find(([, p]) => p === harnessPath)?.[0] ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Return the full list of available harness names.
|
|
104
|
+
*/
|
|
105
|
+
function listHarnesses() {
|
|
106
|
+
return Object.keys(HARNESSES);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Return the path for a harness by name.
|
|
111
|
+
*/
|
|
112
|
+
function getHarnessPath(name) {
|
|
113
|
+
return HARNESSES[name] ?? null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Internal helpers
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
function extractCommandText(toolInput) {
|
|
121
|
+
if (!toolInput) return '';
|
|
122
|
+
if (typeof toolInput === 'string') return toolInput;
|
|
123
|
+
if (typeof toolInput === 'object') {
|
|
124
|
+
// Claude Code Bash tool: { command: "..." }
|
|
125
|
+
if (typeof toolInput.command === 'string') return toolInput.command;
|
|
126
|
+
// file_path for Edit/Write tools
|
|
127
|
+
if (typeof toolInput.file_path === 'string') return toolInput.file_path;
|
|
128
|
+
// Generic text fields
|
|
129
|
+
for (const key of ['input', 'text', 'content', 'query']) {
|
|
130
|
+
if (typeof toolInput[key] === 'string') return toolInput[key];
|
|
131
|
+
}
|
|
132
|
+
// Fall back to serialised form
|
|
133
|
+
try { return JSON.stringify(toolInput); } catch { return ''; }
|
|
134
|
+
}
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
selectHarness,
|
|
140
|
+
selectHarnessName,
|
|
141
|
+
listHarnesses,
|
|
142
|
+
getHarnessPath,
|
|
143
|
+
extractCommandText,
|
|
144
|
+
HARNESSES,
|
|
145
|
+
DEPLOY_PATTERNS,
|
|
146
|
+
DB_WRITE_PATTERNS,
|
|
147
|
+
CODE_EDIT_TOOL_NAMES,
|
|
148
|
+
};
|
package/scripts/hosted-config.js
CHANGED
|
@@ -123,6 +123,7 @@ function resolveHostedBillingConfig({ requestOrigin } = {}, env = process.env) {
|
|
|
123
123
|
const proPriceDollars = normalizePriceDollars(env.THUMBGATE_PRO_PRICE_DOLLARS) || DEFAULT_PRO_PRICE_DOLLARS;
|
|
124
124
|
const proPriceLabel = env.THUMBGATE_PRO_PRICE_LABEL || DEFAULT_PRO_PRICE_LABEL;
|
|
125
125
|
const gaMeasurementId = normalizeTrackingId(env.THUMBGATE_GA_MEASUREMENT_ID, GA_MEASUREMENT_ID_PATTERN);
|
|
126
|
+
const posthogApiKey = env.POSTHOG_API_KEY || '';
|
|
126
127
|
const googleSiteVerification = normalizeTrackingId(env.THUMBGATE_GOOGLE_SITE_VERIFICATION);
|
|
127
128
|
|
|
128
129
|
return {
|
|
@@ -137,6 +138,7 @@ function resolveHostedBillingConfig({ requestOrigin } = {}, env = process.env) {
|
|
|
137
138
|
proPriceLabel,
|
|
138
139
|
gaMeasurementId,
|
|
139
140
|
googleSiteVerification,
|
|
141
|
+
posthogApiKey,
|
|
140
142
|
};
|
|
141
143
|
}
|
|
142
144
|
|
|
@@ -6,6 +6,7 @@ const { spawn } = require('child_process');
|
|
|
6
6
|
|
|
7
7
|
const runner = require('./async-job-runner');
|
|
8
8
|
const { buildHarnessJob } = require('./natural-language-harness');
|
|
9
|
+
const { ensureDir } = require('./fs-utils');
|
|
9
10
|
|
|
10
11
|
const RUNNER_SCRIPT_PATH = path.join(__dirname, 'async-job-runner.js');
|
|
11
12
|
const MANAGED_DPO_EXPORT_SCRIPT_PATH = path.join(__dirname, 'managed-dpo-export.js');
|
|
@@ -17,11 +18,6 @@ function nowIso() {
|
|
|
17
18
|
return new Date().toISOString();
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function ensureDir(dirPath) {
|
|
21
|
-
if (!fs.existsSync(dirPath)) {
|
|
22
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
21
|
|
|
26
22
|
function shellQuote(value) {
|
|
27
23
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
20
|
+
const { readJsonl } = require('./fs-utils');
|
|
20
21
|
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Paths
|
|
@@ -62,39 +63,12 @@ const POS = new Set([
|
|
|
62
63
|
'success', 'pass', 'passed', 'great', 'excellent', 'perfect', 'works',
|
|
63
64
|
]);
|
|
64
65
|
|
|
66
|
+
const HYBRID_JSONL_READ_LIMIT = 400;
|
|
67
|
+
|
|
65
68
|
// ---------------------------------------------------------------------------
|
|
66
69
|
// Low-level helpers
|
|
67
70
|
// ---------------------------------------------------------------------------
|
|
68
71
|
|
|
69
|
-
/**
|
|
70
|
-
* Read last maxLines of a JSONL file in reverse, then re-reverse so oldest-first.
|
|
71
|
-
*/
|
|
72
|
-
function readJsonl(filePath, maxLines) {
|
|
73
|
-
const limit = maxLines !== undefined ? maxLines : 400;
|
|
74
|
-
if (!fs.existsSync(filePath)) return [];
|
|
75
|
-
let raw;
|
|
76
|
-
try {
|
|
77
|
-
raw = fs.readFileSync(filePath, 'utf8').trimEnd();
|
|
78
|
-
} catch (_) {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
if (!raw) return [];
|
|
82
|
-
const lines = raw.split('\n');
|
|
83
|
-
const slice = lines.slice(-limit);
|
|
84
|
-
const parsed = [];
|
|
85
|
-
for (let i = slice.length - 1; i >= 0; i--) {
|
|
86
|
-
const line = slice[i].trim();
|
|
87
|
-
if (!line) continue;
|
|
88
|
-
try {
|
|
89
|
-
parsed.push(JSON.parse(line));
|
|
90
|
-
} catch (_) {
|
|
91
|
-
// skip malformed
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
parsed.reverse(); // back to chronological order
|
|
95
|
-
return parsed;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
72
|
/**
|
|
99
73
|
* Normalize text: strip /Users/ paths, port numbers, lowercase.
|
|
100
74
|
*/
|
|
@@ -208,10 +182,10 @@ function buildHybridState(opts) {
|
|
|
208
182
|
const pendingSyncPath = o.pendingSyncPath || process.env.THUMBGATE_PENDING_SYNC || paths.pendingSync;
|
|
209
183
|
const attributedFeedbackPath = o.attributedFeedbackPath || process.env.THUMBGATE_ATTRIBUTED_FEEDBACK || paths.attributedFeedback;
|
|
210
184
|
|
|
211
|
-
const feedbackEntries = readJsonl(feedbackLogPath);
|
|
212
|
-
const inboxEntries = readJsonl(inboxPath);
|
|
213
|
-
const pendingSyncEntries = readJsonl(pendingSyncPath);
|
|
214
|
-
const attributedEntries = readJsonl(attributedFeedbackPath);
|
|
185
|
+
const feedbackEntries = readJsonl(feedbackLogPath, HYBRID_JSONL_READ_LIMIT);
|
|
186
|
+
const inboxEntries = readJsonl(inboxPath, HYBRID_JSONL_READ_LIMIT);
|
|
187
|
+
const pendingSyncEntries = readJsonl(pendingSyncPath, HYBRID_JSONL_READ_LIMIT);
|
|
188
|
+
const attributedEntries = readJsonl(attributedFeedbackPath, HYBRID_JSONL_READ_LIMIT);
|
|
215
189
|
|
|
216
190
|
// Deduplicate by id across all sources
|
|
217
191
|
const seen = new Set();
|
|
@@ -534,25 +508,21 @@ function evaluateCompiledGuards(artifact, toolName, toolInput) {
|
|
|
534
508
|
const normTool = (toolName || '').toLowerCase();
|
|
535
509
|
|
|
536
510
|
for (const guard of artifact.guards) {
|
|
537
|
-
// Check if tool context is relevant
|
|
538
511
|
const guardText = normalize(guard.text || '');
|
|
539
512
|
const toolMentioned = guardText.includes(normTool) || normTool === 'unknown';
|
|
540
513
|
|
|
541
|
-
|
|
542
|
-
return {
|
|
543
|
-
mode: guard.mode || 'warn',
|
|
544
|
-
reason: `Matched guard pattern (count: ${guard.count}): "${(guard.text || '').slice(0, 80)}"`,
|
|
545
|
-
source: 'compiled',
|
|
546
|
-
guardHash: guard.hash,
|
|
547
|
-
attributed: guard.attributed,
|
|
548
|
-
};
|
|
549
|
-
}
|
|
514
|
+
const keywordMatch = hasTwoKeywordHits(normInput, guard.words || []);
|
|
550
515
|
|
|
551
|
-
//
|
|
552
|
-
|
|
516
|
+
// Match if: keyword hits in input, OR tool mentioned + high count.
|
|
517
|
+
// Previously tool-name matching only worked for short inputs — this was
|
|
518
|
+
// a false-negative gap that let tool-specific patterns slip through.
|
|
519
|
+
if (keywordMatch || (toolMentioned && guard.count >= (artifact.blockThreshold || 3))) {
|
|
520
|
+
const reason = keywordMatch
|
|
521
|
+
? `Matched guard pattern (count: ${guard.count}): "${(guard.text || '').slice(0, 80)}"`
|
|
522
|
+
: `Tool "${toolName}" has recurring negative patterns (count: ${guard.count})`;
|
|
553
523
|
return {
|
|
554
524
|
mode: guard.mode || 'warn',
|
|
555
|
-
reason
|
|
525
|
+
reason,
|
|
556
526
|
source: 'compiled',
|
|
557
527
|
guardHash: guard.hash,
|
|
558
528
|
attributed: guard.attributed,
|
|
@@ -622,6 +592,12 @@ function evaluatePretoolFromState(state, toolName, toolInput) {
|
|
|
622
592
|
* @param {string} [opts.attributedFeedbackPath]
|
|
623
593
|
* @returns {{ mode: 'block'|'warn'|'allow', reason: string, source: string }}
|
|
624
594
|
*/
|
|
595
|
+
/**
|
|
596
|
+
* Max age (ms) before compiled guards are considered stale and live state
|
|
597
|
+
* is also consulted. Default: 1 hour.
|
|
598
|
+
*/
|
|
599
|
+
const GUARD_STALENESS_MS = 60 * 60 * 1000;
|
|
600
|
+
|
|
625
601
|
function evaluatePretool(toolName, toolInput, opts) {
|
|
626
602
|
const o = opts || {};
|
|
627
603
|
|
|
@@ -631,11 +607,18 @@ function evaluatePretool(toolName, toolInput, opts) {
|
|
|
631
607
|
if (artifact) {
|
|
632
608
|
const result = evaluateCompiledGuards(artifact, toolName, toolInput);
|
|
633
609
|
if (result.mode !== 'allow') return result;
|
|
634
|
-
|
|
635
|
-
|
|
610
|
+
|
|
611
|
+
// Check staleness: if compiled artifact is fresh enough, trust it
|
|
612
|
+
const compiledAt = artifact.compiledAt ? Date.parse(artifact.compiledAt) : 0;
|
|
613
|
+
const age = Date.now() - compiledAt;
|
|
614
|
+
if (age < GUARD_STALENESS_MS) {
|
|
615
|
+
return result; // Fresh compiled artifact says allow — trust it
|
|
616
|
+
}
|
|
617
|
+
// Stale artifact said allow — fall through to live evaluation
|
|
618
|
+
// in case new feedback was captured since compilation
|
|
636
619
|
}
|
|
637
620
|
|
|
638
|
-
// Slow path: build live state
|
|
621
|
+
// Slow path: build live state (also used when compiled guards are stale)
|
|
639
622
|
const state = buildHybridState({
|
|
640
623
|
feedbackLogPath: o.feedbackLogPath,
|
|
641
624
|
attributedFeedbackPath: o.attributedFeedbackPath,
|
|
@@ -709,6 +692,7 @@ module.exports = {
|
|
|
709
692
|
readJsonl,
|
|
710
693
|
getHybridPaths,
|
|
711
694
|
PATHS,
|
|
695
|
+
GUARD_STALENESS_MS,
|
|
712
696
|
};
|
|
713
697
|
|
|
714
698
|
if (require.main === module) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
7
|
+
const { getDecisionLogPath, readDecisionLog, collapseDecisionTimeline } = require('./decision-journal');
|
|
7
8
|
|
|
8
9
|
const LABELS = ['allow', 'recall', 'verify', 'warn', 'deny'];
|
|
9
10
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -323,14 +324,64 @@ function buildDiagnosticExample(entry) {
|
|
|
323
324
|
};
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
function deriveLabelFromDecisionOutcome(outcome) {
|
|
328
|
+
const status = normalizeText(outcome && outcome.outcome);
|
|
329
|
+
const actualDecision = normalizeText(outcome && outcome.actualDecision);
|
|
330
|
+
if (status === 'blocked' || status === 'rolled_back' || actualDecision === 'deny') return 'deny';
|
|
331
|
+
if (status === 'warned' || status === 'overridden' || actualDecision === 'warn') return 'warn';
|
|
332
|
+
if (status === 'accepted' || status === 'completed') return 'allow';
|
|
333
|
+
if (status === 'aborted') return 'warn';
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function buildDecisionExample(action) {
|
|
338
|
+
const evaluation = action && action.evaluation ? action.evaluation : null;
|
|
339
|
+
const latestOutcome = action && Array.isArray(action.outcomes) && action.outcomes.length > 0
|
|
340
|
+
? action.outcomes[action.outcomes.length - 1]
|
|
341
|
+
: null;
|
|
342
|
+
const label = deriveLabelFromDecisionOutcome(latestOutcome);
|
|
343
|
+
if (!evaluation || !latestOutcome || !label) return null;
|
|
344
|
+
|
|
345
|
+
const recommendation = evaluation.recommendation || {};
|
|
346
|
+
const blastRadius = evaluation.blastRadius || {};
|
|
347
|
+
const toolInput = evaluation.toolInput && typeof evaluation.toolInput === 'object' ? evaluation.toolInput : {};
|
|
348
|
+
const changedFiles = Array.isArray(evaluation.changedFiles) ? evaluation.changedFiles : [];
|
|
349
|
+
const tokens = buildFeatureTokens([
|
|
350
|
+
'kind:decision',
|
|
351
|
+
`tool:${evaluation.toolName || latestOutcome.toolName || 'unknown'}`,
|
|
352
|
+
`decision:${recommendation.decision || 'allow'}`,
|
|
353
|
+
`execution:${recommendation.executionMode || 'auto_execute'}`,
|
|
354
|
+
`owner:${recommendation.decisionOwner || 'agent'}`,
|
|
355
|
+
`reversibility:${recommendation.reversibility || 'reviewable'}`,
|
|
356
|
+
recommendation.riskBand ? `risk:${recommendation.riskBand}` : null,
|
|
357
|
+
blastRadius.severity ? `blast:${blastRadius.severity}` : null,
|
|
358
|
+
latestOutcome.outcome ? `outcome:${latestOutcome.outcome}` : null,
|
|
359
|
+
latestOutcome.actor ? `actor:${latestOutcome.actor}` : null,
|
|
360
|
+
...extractCommandTokens(toolInput.command || ''),
|
|
361
|
+
...changedFiles.flatMap((filePath) => extractFileTokens(filePath)),
|
|
362
|
+
...tokenizeText([recommendation.summary, latestOutcome.notes].filter(Boolean).join(' '), 10).map((token) => `decisiontok:${token}`),
|
|
363
|
+
]);
|
|
364
|
+
|
|
365
|
+
if (!tokens.length) return null;
|
|
366
|
+
return {
|
|
367
|
+
id: latestOutcome.actionId || evaluation.actionId || null,
|
|
368
|
+
source: 'decision',
|
|
369
|
+
label,
|
|
370
|
+
timestamp: latestOutcome.timestamp || evaluation.timestamp || new Date().toISOString(),
|
|
371
|
+
tokens,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
326
375
|
function buildExamplesFromFeedbackDir(feedbackDir) {
|
|
327
376
|
const resolvedDir = resolveFeedbackDir({ feedbackDir });
|
|
328
377
|
const feedbackEntries = readJSONL(path.join(resolvedDir, 'feedback-log.jsonl'));
|
|
329
378
|
const auditEntries = readJSONL(path.join(resolvedDir, 'audit-trail.jsonl'));
|
|
330
379
|
const diagnosticEntries = readJSONL(path.join(resolvedDir, 'diagnostic-log.jsonl'));
|
|
380
|
+
const decisionEntries = readDecisionLog(getDecisionLogPath(resolvedDir));
|
|
381
|
+
const decisions = collapseDecisionTimeline(decisionEntries);
|
|
331
382
|
|
|
332
383
|
const examples = [];
|
|
333
|
-
const sourceCounts = { feedback: 0, audit: 0, diagnostic: 0 };
|
|
384
|
+
const sourceCounts = { feedback: 0, audit: 0, diagnostic: 0, decision: 0 };
|
|
334
385
|
|
|
335
386
|
for (const entry of feedbackEntries) {
|
|
336
387
|
const example = buildFeedbackExample(entry);
|
|
@@ -350,6 +401,12 @@ function buildExamplesFromFeedbackDir(feedbackDir) {
|
|
|
350
401
|
sourceCounts.diagnostic += 1;
|
|
351
402
|
examples.push(example);
|
|
352
403
|
}
|
|
404
|
+
for (const action of decisions) {
|
|
405
|
+
const example = buildDecisionExample(action);
|
|
406
|
+
if (!example) continue;
|
|
407
|
+
sourceCounts.decision += 1;
|
|
408
|
+
examples.push(example);
|
|
409
|
+
}
|
|
353
410
|
|
|
354
411
|
examples.sort((left, right) => {
|
|
355
412
|
return Date.parse(left.timestamp || 0) - Date.parse(right.timestamp || 0);
|
package/scripts/lesson-db.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
const path = require('node:path');
|
|
16
16
|
const fs = require('node:fs');
|
|
17
|
+
const { readJsonl } = require('./fs-utils');
|
|
17
18
|
|
|
18
19
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
19
20
|
const DEFAULT_DB_PATH = path.join(PROJECT_ROOT, '.claude', 'memory', 'lessons.sqlite');
|
|
@@ -495,8 +496,8 @@ function backfillFromJsonl(db, feedbackDir) {
|
|
|
495
496
|
const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
|
|
496
497
|
const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
|
|
497
498
|
|
|
498
|
-
const feedbackEntries =
|
|
499
|
-
const memoryEntries =
|
|
499
|
+
const feedbackEntries = readJsonl(feedbackLogPath);
|
|
500
|
+
const memoryEntries = readJsonl(memoryLogPath);
|
|
500
501
|
|
|
501
502
|
// Index memories by sourceFeedbackId for joining
|
|
502
503
|
const memoryByFeedbackId = new Map();
|
|
@@ -581,22 +582,6 @@ function safeParseTags(tagsStr) {
|
|
|
581
582
|
}
|
|
582
583
|
}
|
|
583
584
|
|
|
584
|
-
function readJsonlSafe(filePath) {
|
|
585
|
-
if (!fs.existsSync(filePath)) return [];
|
|
586
|
-
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
587
|
-
if (!raw) return [];
|
|
588
|
-
return raw
|
|
589
|
-
.split('\n')
|
|
590
|
-
.map((line) => {
|
|
591
|
-
try {
|
|
592
|
-
return JSON.parse(line);
|
|
593
|
-
} catch {
|
|
594
|
-
return null;
|
|
595
|
-
}
|
|
596
|
-
})
|
|
597
|
-
.filter(Boolean);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
585
|
module.exports = {
|
|
601
586
|
initDB,
|
|
602
587
|
upsertLesson,
|