thumbgate 1.27.6 → 1.27.7
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/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +6 -2
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +49 -5
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +16 -1
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bin/cli.js +180 -2
- package/bin/postinstall.js +1 -1
- package/config/gate-templates.json +84 -0
- package/config/gates/claim-verification.json +6 -0
- package/config/gates/default.json +20 -0
- package/config/github-about.json +1 -1
- package/config/model-candidates.json +50 -0
- package/package.json +65 -25
- package/public/agent-manager.html +41 -1
- package/public/agents-cost-savings.html +1 -1
- package/public/ai-malpractice-prevention.html +2 -1
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-mark-inline.svg +15 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +15 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +1 -1
- package/public/brand/thumbgate-mark.svg +15 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/codex-enterprise.html +1 -1
- package/public/codex-plugin.html +1 -1
- package/public/compare.html +23 -3
- package/public/dashboard.html +312 -30
- package/public/federal.html +1 -1
- package/public/guide.html +5 -4
- package/public/index.html +167 -49
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +74 -7
- package/public/lessons.html +2 -1
- package/public/numbers.html +3 -3
- package/public/pricing.html +63 -15
- package/public/pro.html +7 -7
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/auto-promote-gates.js +75 -4
- package/scripts/build-metadata.js +24 -3
- package/scripts/cli-schema.js +22 -0
- package/scripts/dashboard-chat.js +2 -1
- package/scripts/dashboard.js +8 -0
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-loop.js +34 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gates-engine.js +198 -6
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-stop-anti-claim.js +227 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/operational-integrity.js +39 -5
- package/scripts/plausible-domain-config.js +4 -2
- package/scripts/rate-limiter.js +12 -6
- package/scripts/secret-redaction.js +166 -0
- package/scripts/security-scanner.js +100 -0
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/seo-gsd.js +635 -0
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +5 -2
- package/scripts/statusline.sh +13 -1
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +9 -0
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/vector-store.js +44 -0
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +715 -86
package/scripts/gates-engine.js
CHANGED
|
@@ -119,8 +119,16 @@ const MAX_COMMAND_SCAN_CHARS = 20000;
|
|
|
119
119
|
const BOOSTED_RISK_BLOCK_SCORE = 0.8;
|
|
120
120
|
const BOOSTED_RISK_MIN_EXAMPLES = 3;
|
|
121
121
|
const PR_THREAD_RESOLUTION_ACTION = 'pr_thread_resolution_verified_after_commit';
|
|
122
|
+
const HELPER_BYPASS_ACTION = 'helper_script_modified';
|
|
122
123
|
const KNOWLEDGE_ENTROPY_THRESHOLD = 0.7;
|
|
123
|
-
const KNOWLEDGE_CONFLICT_STRICT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+merge\b|gh\s+release\s+(?:create|delete|edit|upload)\b|(?:npm|yarn|pnpm)\s+publish\b|rm\s+-rf\b|git\s+reset\s+--hard\b|git\s+clean\s+-f
|
|
124
|
+
const KNOWLEDGE_CONFLICT_STRICT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+merge\b|gh\s+release\s+(?:create|delete|edit|upload)\b|(?:npm|yarn|pnpm)\s+publish\b|rm\s+-rf\b|git\s+reset\s+--hard\b|git\s+clean\s+-f[a-z]*|railway\s+(?:deploy|up)\b|gcloud\s+(?:run\s+deploy|app\s+deploy)\b|firebase\s+deploy\b|vercel\s+--prod\b|kubectl\s+(?:apply|delete)\b|terraform\s+(?:apply|destroy)\b)\b/i;
|
|
125
|
+
const HELPER_SCRIPT_FILE_PATTERN = /(?:^|\/)(?:scripts|bin|tools|tasks|\.githooks|\.github\/workflows)\/|(?:^|\/)(?:package\.json|Makefile|justfile|Taskfile\.ya?ml)$|\.(?:sh|bash|zsh|fish|js|mjs|cjs|ts|tsx|py|rb|pl|ps1|yml|yaml)$/i;
|
|
126
|
+
const PACKAGE_RUN_PATTERN = /\b(?:npm|yarn|pnpm)\s+run\s+([:@./\w-]+)\b/i;
|
|
127
|
+
const HELPER_EXEC_PATTERN = /(?:^|[;&|]\s*|\b(?:bash|sh|zsh|node|python3?|ruby|perl|tsx|ts-node)\s+)(?:(?:\.?\.?\/)?(?:scripts|bin|tools|tasks|tmp|build|dist|\.tmp|\.cache)\/[^\s;&|]+|\.\/[^\s;&|]+\.(?:sh|bash|zsh|js|mjs|cjs|ts|py|rb|pl|ps1))\b/i;
|
|
128
|
+
const HELPER_WRITE_PATTERN = /\b(?:cat|printf|echo|tee|npm\s+pkg\s+set|jq|node\s+-e|python3?\s+-c)\b[\s\S]{0,300}(?:>|--field|scripts\.|package\.json|(?:scripts|bin|tools|tasks|tmp|build|dist|\.tmp|\.cache)\/[^\s;&|]+|\.(?:sh|js|mjs|cjs|ts|py|rb|pl|ps1))\b/i;
|
|
129
|
+
const NETWORK_OR_PROCESS_BOUNDARY_PATTERN = /\b(?:curl|wget|nc|ncat|socat|ssh|scp|rsync|ftp|python3?\s+-m\s+http\.server|node\s+-e|python3?\s+-c|perl\s+-e|ruby\s+-e|bash\s+-c|sh\s+-c|osascript|open)\b/i;
|
|
130
|
+
const DOWNLOAD_EXEC_CHAIN_PATTERN = /\b(?:curl|wget)\b[\s\S]{0,400}(?:\|\s*(?:bash|sh|zsh)|&&[\s\S]{0,200}\bchmod\s+\+x\b[\s\S]{0,200}&&[\s\S]{0,120}(?:\.\/|bash|sh|node|python3?))/i;
|
|
131
|
+
const DESTRUCTIVE_OR_PRIVILEGE_BOUNDARY_PATTERN = /\b(?:rm\s+-rf|chmod\s+(?:\+x|777)|chown\b|sudo\b|dd\s+if=|mkfs|git\s+reset\s+--hard|git\s+clean\s+-f[a-z]*|kubectl\s+(?:apply|delete)|terraform\s+(?:apply|destroy)|railway\s+(?:deploy|up)|gcloud\s+(?:run\s+deploy|app\s+deploy)|vercel\s+--prod|firebase\s+deploy)\b/i;
|
|
124
132
|
|
|
125
133
|
// ---------------------------------------------------------------------------
|
|
126
134
|
// Enforcement posture (CEO decision 2026-06-04): warn-by-default.
|
|
@@ -834,9 +842,26 @@ function toRepoRelativePath(filePath, repoRoot) {
|
|
|
834
842
|
const value = String(filePath || '').trim();
|
|
835
843
|
if (!value) return '';
|
|
836
844
|
if (repoRoot && path.isAbsolute(value)) {
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
845
|
+
const candidates = [[path.resolve(repoRoot), path.resolve(value)]];
|
|
846
|
+
try {
|
|
847
|
+
const rootReal = fs.realpathSync.native(repoRoot);
|
|
848
|
+
let valueReal = null;
|
|
849
|
+
try {
|
|
850
|
+
valueReal = fs.realpathSync.native(value);
|
|
851
|
+
} catch {
|
|
852
|
+
const parentReal = fs.realpathSync.native(path.dirname(value));
|
|
853
|
+
valueReal = path.join(parentReal, path.basename(value));
|
|
854
|
+
}
|
|
855
|
+
candidates.push([rootReal, valueReal]);
|
|
856
|
+
} catch {
|
|
857
|
+
// Fall back to lexical path comparison below.
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
for (const [rootCandidate, valueCandidate] of candidates) {
|
|
861
|
+
const relative = path.relative(rootCandidate, valueCandidate);
|
|
862
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
863
|
+
return normalizePosix(relative);
|
|
864
|
+
}
|
|
840
865
|
}
|
|
841
866
|
}
|
|
842
867
|
return normalizePosix(value);
|
|
@@ -1195,6 +1220,125 @@ function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governa
|
|
|
1195
1220
|
};
|
|
1196
1221
|
}
|
|
1197
1222
|
|
|
1223
|
+
function helperRiskReasons(text) {
|
|
1224
|
+
const value = String(text || '');
|
|
1225
|
+
const reasons = [];
|
|
1226
|
+
if (DOWNLOAD_EXEC_CHAIN_PATTERN.test(value)) reasons.push('download-then-execute chain');
|
|
1227
|
+
if (NETWORK_OR_PROCESS_BOUNDARY_PATTERN.test(value)) reasons.push('network/process boundary');
|
|
1228
|
+
if (DESTRUCTIVE_OR_PRIVILEGE_BOUNDARY_PATTERN.test(value)) reasons.push('destructive/privileged side effect');
|
|
1229
|
+
if (/\b(?:child_process|spawn\(|exec\(|execFile\(|subprocess|ProcessBuilder)\b/i.test(value)) {
|
|
1230
|
+
reasons.push('process spawn');
|
|
1231
|
+
}
|
|
1232
|
+
return [...new Set(reasons)];
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function readPackageScript(repoRoot, scriptName) {
|
|
1236
|
+
if (!repoRoot || !scriptName) return '';
|
|
1237
|
+
try {
|
|
1238
|
+
const packagePath = path.join(repoRoot, 'package.json');
|
|
1239
|
+
if (!fs.existsSync(packagePath)) return '';
|
|
1240
|
+
const { scripts = {} } = JSON.parse(fs.readFileSync(packagePath, 'utf8')) || {};
|
|
1241
|
+
return typeof scripts[scriptName] === 'string' ? scripts[scriptName] : '';
|
|
1242
|
+
} catch {
|
|
1243
|
+
return '';
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function recordHelperScriptWrite(toolName, toolInput = {}) {
|
|
1248
|
+
if (process.env.THUMBGATE_HELPER_BYPASS_GUARD === '0') return null;
|
|
1249
|
+
|
|
1250
|
+
const affected = extractAffectedFiles(toolName, toolInput);
|
|
1251
|
+
const affectedFiles = affected.files || [];
|
|
1252
|
+
const command = String(toolInput.command || '');
|
|
1253
|
+
const actionContext = extractActionContext(toolName, toolInput);
|
|
1254
|
+
const helperFiles = affectedFiles.filter((filePath) => HELPER_SCRIPT_FILE_PATTERN.test(normalizePosix(filePath)));
|
|
1255
|
+
const packageScriptTouched = affectedFiles.some((filePath) => normalizePosix(filePath) === 'package.json') || /\bpackage\.json\b|\bscripts\./i.test(actionContext);
|
|
1256
|
+
const commandWrite = toolName === 'Bash' && HELPER_WRITE_PATTERN.test(command);
|
|
1257
|
+
|
|
1258
|
+
if (helperFiles.length === 0 && !packageScriptTouched && !commandWrite) return null;
|
|
1259
|
+
|
|
1260
|
+
const riskText = [
|
|
1261
|
+
actionContext,
|
|
1262
|
+
command,
|
|
1263
|
+
helperFiles.join(' '),
|
|
1264
|
+
packageScriptTouched ? 'package.json scripts' : '',
|
|
1265
|
+
].filter(Boolean).join('\n');
|
|
1266
|
+
const reasons = helperRiskReasons(riskText);
|
|
1267
|
+
|
|
1268
|
+
const metadata = {
|
|
1269
|
+
repoRoot: affected.repoRoot || resolveRepoRoot(toolInput) || null,
|
|
1270
|
+
helperFiles,
|
|
1271
|
+
packageScriptTouched,
|
|
1272
|
+
reasons,
|
|
1273
|
+
};
|
|
1274
|
+
trackAction(HELPER_BYPASS_ACTION, metadata);
|
|
1275
|
+
return metadata;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function evaluateStatefulHelperBypassGate(toolName, toolInput = {}) {
|
|
1279
|
+
if (process.env.THUMBGATE_HELPER_BYPASS_GUARD === '0') return null;
|
|
1280
|
+
if (toolName !== 'Bash') {
|
|
1281
|
+
recordHelperScriptWrite(toolName, toolInput);
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const command = String(toolInput.command || '').trim();
|
|
1286
|
+
if (!command) return null;
|
|
1287
|
+
|
|
1288
|
+
if (DOWNLOAD_EXEC_CHAIN_PATTERN.test(command)) {
|
|
1289
|
+
return {
|
|
1290
|
+
decision: 'deny',
|
|
1291
|
+
gate: 'stateful-helper-script-bypass',
|
|
1292
|
+
message: 'Download-then-execute chains are blocked before the real action moves below the visible tool call.',
|
|
1293
|
+
severity: 'critical',
|
|
1294
|
+
reasoning: [
|
|
1295
|
+
`Command matched download/execute chain: ${command.slice(0, 180)}`,
|
|
1296
|
+
'PreToolUse must review the whole action chain, not only the first low-risk command',
|
|
1297
|
+
],
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const writeMetadata = recordHelperScriptWrite(toolName, toolInput);
|
|
1302
|
+
const actions = listSessionActions();
|
|
1303
|
+
const recentWrite = actions[HELPER_BYPASS_ACTION];
|
|
1304
|
+
const recentMetadata = recentWrite && recentWrite.metadata && typeof recentWrite.metadata === 'object'
|
|
1305
|
+
? recentWrite.metadata
|
|
1306
|
+
: null;
|
|
1307
|
+
|
|
1308
|
+
const scriptMatch = command.match(PACKAGE_RUN_PATTERN);
|
|
1309
|
+
const scriptName = scriptMatch ? scriptMatch[1] : '';
|
|
1310
|
+
const repoRoot = resolveRepoRoot(toolInput);
|
|
1311
|
+
const packageScript = scriptName ? readPackageScript(repoRoot, scriptName) : '';
|
|
1312
|
+
const commandBoundaryReasons = helperRiskReasons(`${command}\n${packageScript}`);
|
|
1313
|
+
const executesRecentHelper = HELPER_EXEC_PATTERN.test(command);
|
|
1314
|
+
const runsRecentPackageScript = Boolean(scriptName && recentMetadata && recentMetadata.packageScriptTouched);
|
|
1315
|
+
const recentReasons = recentMetadata && Array.isArray(recentMetadata.reasons) ? recentMetadata.reasons : [];
|
|
1316
|
+
const writeRiskReasons = writeMetadata && Array.isArray(writeMetadata.reasons) ? writeMetadata.reasons : [];
|
|
1317
|
+
const correlatedReasons = [...new Set([...recentReasons, ...writeRiskReasons, ...commandBoundaryReasons])];
|
|
1318
|
+
|
|
1319
|
+
if (
|
|
1320
|
+
(executesRecentHelper || runsRecentPackageScript) &&
|
|
1321
|
+
(recentMetadata || writeMetadata) &&
|
|
1322
|
+
correlatedReasons.length > 0
|
|
1323
|
+
) {
|
|
1324
|
+
const target = scriptName ? `package script "${scriptName}"` : 'recent helper script';
|
|
1325
|
+
return {
|
|
1326
|
+
decision: 'deny',
|
|
1327
|
+
gate: 'stateful-helper-script-bypass',
|
|
1328
|
+
message: `A recently modified ${target} now crosses a risky boundary. Review it or constrain cwd, network, process, and writable paths before execution.`,
|
|
1329
|
+
severity: 'critical',
|
|
1330
|
+
reasoning: [
|
|
1331
|
+
`Recent helper/package modification: ${(recentMetadata && recentMetadata.helperFiles || []).join(', ') || (recentMetadata && recentMetadata.packageScriptTouched ? 'package.json scripts' : 'current command write')}`,
|
|
1332
|
+
`Execution command: ${command.slice(0, 180)}`,
|
|
1333
|
+
`Risk reasons: ${correlatedReasons.join(', ')}`,
|
|
1334
|
+
'This blocks the helper-script/package-script bypass class raised in external review',
|
|
1335
|
+
],
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1198
1342
|
function recordStructuralGateBlock(toolName, toolInput, result) {
|
|
1199
1343
|
recordStat(result.gate, 'block', null, { toolName, toolInput });
|
|
1200
1344
|
const auditRecord = recordAuditEvent({
|
|
@@ -1454,8 +1598,19 @@ function checkWhenClause(when, constraints) {
|
|
|
1454
1598
|
if (!when || !when.constraints) return true;
|
|
1455
1599
|
|
|
1456
1600
|
for (const [key, expectedValue] of Object.entries(when.constraints)) {
|
|
1457
|
-
|
|
1458
|
-
if (
|
|
1601
|
+
let value;
|
|
1602
|
+
if (key === 'careful_mode') {
|
|
1603
|
+
const isEnvTrue = process.env.THUMBGATE_CAREFUL_MODE === '1' ||
|
|
1604
|
+
String(process.env.THUMBGATE_CAREFUL_MODE).toLowerCase() === 'true';
|
|
1605
|
+
value = isEnvTrue || (constraints[key] && constraints[key].value === expectedValue);
|
|
1606
|
+
} else if (key === 'freeze_mode') {
|
|
1607
|
+
value = Boolean(process.env.THUMBGATE_FREEZE_PATHS) ||
|
|
1608
|
+
(constraints[key] && constraints[key].value !== false && constraints[key].value !== null && constraints[key].value !== undefined);
|
|
1609
|
+
} else {
|
|
1610
|
+
const constraint = constraints[key];
|
|
1611
|
+
value = constraint && constraint.value === expectedValue;
|
|
1612
|
+
}
|
|
1613
|
+
if (!value) {
|
|
1459
1614
|
return false;
|
|
1460
1615
|
}
|
|
1461
1616
|
}
|
|
@@ -1503,6 +1658,29 @@ function matchGate(gate, toolName, toolInput = {}) {
|
|
|
1503
1658
|
const affectedFiles = affected.files;
|
|
1504
1659
|
const repoRoot = affected.repoRoot;
|
|
1505
1660
|
const governanceState = loadGovernanceState();
|
|
1661
|
+
const constraints = loadConstraints();
|
|
1662
|
+
|
|
1663
|
+
if (gate.id === 'on-demand-freeze-mode' || (gate.when && gate.when.constraints && gate.when.constraints.freeze_mode)) {
|
|
1664
|
+
let freezePaths = [];
|
|
1665
|
+
if (process.env.THUMBGATE_FREEZE_PATHS) {
|
|
1666
|
+
freezePaths = process.env.THUMBGATE_FREEZE_PATHS.split(',').map(p => p.trim()).filter(Boolean);
|
|
1667
|
+
} else if (constraints.freeze_mode && typeof constraints.freeze_mode.value === 'string') {
|
|
1668
|
+
freezePaths = constraints.freeze_mode.value.split(',').map(p => p.trim()).filter(Boolean);
|
|
1669
|
+
} else if (constraints.freeze_mode && Array.isArray(constraints.freeze_mode.value)) {
|
|
1670
|
+
freezePaths = constraints.freeze_mode.value;
|
|
1671
|
+
} else if (governanceState.taskScope && Array.isArray(governanceState.taskScope.allowedPaths)) {
|
|
1672
|
+
freezePaths = governanceState.taskScope.allowedPaths;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (freezePaths.length > 0) {
|
|
1676
|
+
const outsideFiles = affectedFiles.filter((filePath) => !matchesAnyGlob(filePath, freezePaths));
|
|
1677
|
+
if (outsideFiles.length > 0) {
|
|
1678
|
+
return { matched: true, matchText, affectedFiles };
|
|
1679
|
+
} else {
|
|
1680
|
+
return { matched: false, matchText, affectedFiles };
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1506
1684
|
|
|
1507
1685
|
if (Array.isArray(gate.toolNames) && gate.toolNames.length > 0 && !gate.toolNames.includes(toolName)) {
|
|
1508
1686
|
return { matched: false, matchText, affectedFiles };
|
|
@@ -1838,6 +2016,10 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1838
2016
|
if (localOnlyRemoteSideEffectGate) {
|
|
1839
2017
|
return recordStructuralGateBlock(toolName, toolInput, localOnlyRemoteSideEffectGate);
|
|
1840
2018
|
}
|
|
2019
|
+
const statefulHelperBypassGate = evaluateStatefulHelperBypassGate(toolName, toolInput);
|
|
2020
|
+
if (statefulHelperBypassGate) {
|
|
2021
|
+
return recordStructuralGateBlock(toolName, toolInput, statefulHelperBypassGate);
|
|
2022
|
+
}
|
|
1841
2023
|
if (isBreakGlassSettingsRecoveryAction(toolName, toolInput)) {
|
|
1842
2024
|
recordAuditEvent({
|
|
1843
2025
|
toolName,
|
|
@@ -2065,6 +2247,10 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
2065
2247
|
if (localOnlyRemoteSideEffectGate) {
|
|
2066
2248
|
return recordStructuralGateBlock(toolName, toolInput, localOnlyRemoteSideEffectGate);
|
|
2067
2249
|
}
|
|
2250
|
+
const statefulHelperBypassGate = evaluateStatefulHelperBypassGate(toolName, toolInput);
|
|
2251
|
+
if (statefulHelperBypassGate) {
|
|
2252
|
+
return recordStructuralGateBlock(toolName, toolInput, statefulHelperBypassGate);
|
|
2253
|
+
}
|
|
2068
2254
|
if (isBreakGlassSettingsRecoveryAction(toolName, toolInput)) {
|
|
2069
2255
|
recordAuditEvent({
|
|
2070
2256
|
toolName,
|
|
@@ -2707,6 +2893,9 @@ function extractActionContext(toolName, toolInput) {
|
|
|
2707
2893
|
const parts = [toolName];
|
|
2708
2894
|
if (toolInput.command) parts.push(String(toolInput.command).slice(0, 400));
|
|
2709
2895
|
if (toolInput.file_path) parts.push(String(toolInput.file_path));
|
|
2896
|
+
if (toolInput.content) parts.push(String(toolInput.content).slice(0, 600));
|
|
2897
|
+
if (toolInput.new_string) parts.push(String(toolInput.new_string).slice(0, 600));
|
|
2898
|
+
if (toolInput.old_string) parts.push(String(toolInput.old_string).slice(0, 240));
|
|
2710
2899
|
if (toolInput.description) parts.push(String(toolInput.description).slice(0, 200));
|
|
2711
2900
|
if (toolInput.prompt) parts.push(String(toolInput.prompt).slice(0, 400));
|
|
2712
2901
|
if (toolInput.pattern) parts.push(String(toolInput.pattern).slice(0, 200));
|
|
@@ -3149,9 +3338,12 @@ module.exports = {
|
|
|
3149
3338
|
getLocalOnlyScopeSources,
|
|
3150
3339
|
isRemoteSideEffectCommand,
|
|
3151
3340
|
evaluateLocalOnlyRemoteSideEffectGate,
|
|
3341
|
+
recordHelperScriptWrite,
|
|
3342
|
+
evaluateStatefulHelperBypassGate,
|
|
3152
3343
|
isAgentHookSettingsFile,
|
|
3153
3344
|
isBreakGlassSettingsRecoveryAction,
|
|
3154
3345
|
PR_THREAD_RESOLUTION_ACTION,
|
|
3346
|
+
HELPER_BYPASS_ACTION,
|
|
3155
3347
|
buildBlockActionProCta,
|
|
3156
3348
|
applyDailyBlockCap,
|
|
3157
3349
|
getTodayBlockCount,
|
|
@@ -122,7 +122,7 @@ function resolveGeminiEmbeddingConfig(env = process.env) {
|
|
|
122
122
|
|
|
123
123
|
return {
|
|
124
124
|
enabled,
|
|
125
|
-
provider: enabled ? 'gemini' : 'local',
|
|
125
|
+
provider: provider === 'coreai' ? 'coreai' : (enabled ? 'gemini' : 'local'),
|
|
126
126
|
model: String(env.THUMBGATE_GEMINI_EMBED_MODEL || GEMINI_EMBEDDING_2_MODEL).trim() || GEMINI_EMBEDDING_2_MODEL,
|
|
127
127
|
apiKey,
|
|
128
128
|
apiBaseUrl: trimTrailingSlashes(env.THUMBGATE_GEMINI_API_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta'),
|
|
@@ -171,6 +171,7 @@ function buildGeminiEmbeddingRolloutPlan(args = {}) {
|
|
|
171
171
|
},
|
|
172
172
|
rolloutSteps: [
|
|
173
173
|
'Keep local embeddings as the default offline path.',
|
|
174
|
+
'For Apple Silicon developers, route local queries through Core AI (AOT compiled models) to bypass CPU overhead.',
|
|
174
175
|
'Enable Gemini Embedding 2 only when a Gemini API key is present.',
|
|
175
176
|
'Use task-specific query/document prefixes at index and retrieval time.',
|
|
176
177
|
'Start at 768 dimensions, then benchmark 1536 only if recall misses show up.',
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stop hook: anti-claim enforcement.
|
|
6
|
+
*
|
|
7
|
+
* Scans the assistant's most recent turn (assistant text + same-turn tool_use
|
|
8
|
+
* blocks) and blocks the "deployed / live / done / fixed / ready" claim
|
|
9
|
+
* unless that same turn included a proof tool call (curl / grep / test).
|
|
10
|
+
*
|
|
11
|
+
* Why: CLAUDE.md anti-lying directive ("Never claim fix done until
|
|
12
|
+
* committed+pushed. Never claim 'ready' without running e2e.") was
|
|
13
|
+
* aspirational, not enforced. Per CEO 2026-05-13 feedback after a session in
|
|
14
|
+
* which 5+ unverified claims slipped through, this is the harness-level
|
|
15
|
+
* enforcement that ends the recurring trust-burn pattern. ThumbGate-on-
|
|
16
|
+
* ThumbGate dogfood — we are the prevention-rule generator and a perfect
|
|
17
|
+
* customer for our own gate.
|
|
18
|
+
*
|
|
19
|
+
* Wires through .claude/settings.json Stop hooks list. Always exits 0
|
|
20
|
+
* (informational): the goal is to surface a system reminder in the next
|
|
21
|
+
* turn so the agent corrects mid-conversation rather than to hard-block
|
|
22
|
+
* the turn that already happened.
|
|
23
|
+
*
|
|
24
|
+
* Stdin: Claude Code passes the hook payload as JSON on stdin. We read
|
|
25
|
+
* `transcript_path` to locate the JSONL session log and scan the last
|
|
26
|
+
* assistant message.
|
|
27
|
+
*
|
|
28
|
+
* Stdout: any text printed is shown to the agent on the next turn.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
|
|
33
|
+
// Lie-phrase patterns. These match common "claim of completion" wording
|
|
34
|
+
// the agent emits without verification. Word-boundary anchored to avoid
|
|
35
|
+
// false positives ("ready-made", "live-streaming", etc).
|
|
36
|
+
const CLAIM_PATTERNS = [
|
|
37
|
+
/\bis\s+live\b/i,
|
|
38
|
+
/\bnow\s+live\b/i,
|
|
39
|
+
/\bgoing\s+live\b/i,
|
|
40
|
+
/\bdeployed\b(?!\s*(yet|to\s+staging|on\s+a\s+branch))/i,
|
|
41
|
+
/\b(?:is|are|it'?s)\s+(?:now\s+)?(?:fully\s+)?(?:fixed|resolved|merged|shipped)\b/i,
|
|
42
|
+
/\bproduction[-\s]ready\b/i,
|
|
43
|
+
/\beverything\s+(?:is\s+)?(?:done|working|ready)\b/i,
|
|
44
|
+
/\b(?:github|repo|repository)\s+(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b/i,
|
|
45
|
+
/\b(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b.*\b(?:github|repo|repository)\b/i,
|
|
46
|
+
/\b(?:money|payment|charge|checkout|revenue|price|pricing|invoice|billing|tax|sales tax|inventory|stock|permission|access|customer[-\s]facing)\b.*\b(?:correct|accurate|verified|valid|matches|working|fixed|resolved|calculated|configured)\b/i,
|
|
47
|
+
/\b(?:correct|accurate|verified|valid|matches|working|fixed|resolved|calculated|configured)\b.*\b(?:money|payment|charge|checkout|revenue|price|pricing|invoice|billing|tax|sales tax|inventory|stock|permission|access|customer[-\s]facing)\b/i,
|
|
48
|
+
// Added 2026-06-11 after a cross-project failure analysis: these completion
|
|
49
|
+
// claims ("all green / stable / verified / race over / tests pass") were
|
|
50
|
+
// asserted without proof and slipped past the original set. The proof-gate
|
|
51
|
+
// below suppresses them whenever the SAME turn ran a verification tool, so a
|
|
52
|
+
// "verified" claim backed by a test/curl/Read stays silent.
|
|
53
|
+
/\ball\s+(?:the\s+)?(?:tests?\s+|checks?\s+)?(?:are\s+)?green\b/i,
|
|
54
|
+
/\b(?:all\s+)?(?:tests?|checks?|ci)\s+(?:are\s+)?(?:now\s+)?passing\b/i,
|
|
55
|
+
/\ball\s+(?:tests?|checks?)\s+pass(?:ed)?\b/i,
|
|
56
|
+
/\bverified\b/i,
|
|
57
|
+
/\bconfirmed\b/i,
|
|
58
|
+
/\b(?:is|are|it'?s|now)\s+stable\b/i,
|
|
59
|
+
/\ball\s+clear\b/i,
|
|
60
|
+
/\bgood\s+to\s+go\b/i,
|
|
61
|
+
/\brace\s+(?:is\s+)?over\b/i,
|
|
62
|
+
/\bno\s+longer\s+racing\b/i,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Proof-of-verification patterns. If the SAME turn included one of these
|
|
66
|
+
// tool calls or shell command tokens, the claim is considered backed and
|
|
67
|
+
// the hook stays silent.
|
|
68
|
+
const PROOF_PATTERNS = [
|
|
69
|
+
/\bcurl\b/,
|
|
70
|
+
/\bgh\s+pr\s+(?:view|checks|status)\b/,
|
|
71
|
+
/\bgh\s+run\s+view\b/,
|
|
72
|
+
/\bgh\s+api\b/,
|
|
73
|
+
/\bnode\s+--test\b/,
|
|
74
|
+
/\bnpm\s+(?:run\s+)?test\b/,
|
|
75
|
+
/\bnpm\s+pack\b/,
|
|
76
|
+
/\bjest\b/,
|
|
77
|
+
/\bmocha\b/,
|
|
78
|
+
/\bpytest\b/,
|
|
79
|
+
/\bplaywright\b/,
|
|
80
|
+
/\bgrep\b/,
|
|
81
|
+
/\bstripe\b/,
|
|
82
|
+
/\bplaid\b/,
|
|
83
|
+
/\bshopify\b/,
|
|
84
|
+
/\bsquare\b/,
|
|
85
|
+
/\bquickbooks\b/,
|
|
86
|
+
/Read\s*\(/, // Claude Code Read tool call
|
|
87
|
+
/Bash\s*\(/, // Claude Code Bash tool call
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
function readLastAssistantTurn(transcriptPath) {
|
|
91
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) return null;
|
|
92
|
+
let content;
|
|
93
|
+
try {
|
|
94
|
+
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const lines = content.trim().split('\n');
|
|
99
|
+
// Walk backwards to find the last assistant message
|
|
100
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
101
|
+
const raw = lines[i].trim();
|
|
102
|
+
if (!raw) continue;
|
|
103
|
+
let entry;
|
|
104
|
+
try {
|
|
105
|
+
entry = JSON.parse(raw);
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (entry.type === 'assistant' && entry.message) {
|
|
110
|
+
return entry.message;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function extractText(message) {
|
|
117
|
+
if (!message || !Array.isArray(message.content)) return '';
|
|
118
|
+
return message.content
|
|
119
|
+
.filter((b) => b && typeof b.text === 'string')
|
|
120
|
+
.map((b) => b.text)
|
|
121
|
+
.join('\n');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function extractToolUseSummary(message) {
|
|
125
|
+
if (!message || !Array.isArray(message.content)) return '';
|
|
126
|
+
return message.content
|
|
127
|
+
.filter((b) => b?.type === 'tool_use')
|
|
128
|
+
.map((b) => {
|
|
129
|
+
const name = b.name || 'tool';
|
|
130
|
+
let summary = '';
|
|
131
|
+
if (b.input && typeof b.input === 'object') {
|
|
132
|
+
if (typeof b.input.command === 'string') summary = b.input.command;
|
|
133
|
+
else if (typeof b.input.file_path === 'string') summary = b.input.file_path;
|
|
134
|
+
else if (typeof b.input.query === 'string') summary = b.input.query;
|
|
135
|
+
else summary = JSON.stringify(b.input).slice(0, 200);
|
|
136
|
+
}
|
|
137
|
+
return `${name}: ${summary}`;
|
|
138
|
+
})
|
|
139
|
+
.join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function findClaim(text) {
|
|
143
|
+
for (const p of CLAIM_PATTERNS) {
|
|
144
|
+
const m = text.match(p);
|
|
145
|
+
if (m) return m[0];
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hasProof(combined) {
|
|
151
|
+
return PROOF_PATTERNS.some((p) => p.test(combined));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readStdinSync() {
|
|
155
|
+
try {
|
|
156
|
+
return fs.readFileSync(0, 'utf8');
|
|
157
|
+
} catch {
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function main() {
|
|
163
|
+
const raw = readStdinSync();
|
|
164
|
+
let payload = {};
|
|
165
|
+
try {
|
|
166
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
167
|
+
} catch {
|
|
168
|
+
payload = {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const transcriptPath = payload.transcript_path || process.env.CLAUDE_TRANSCRIPT_PATH;
|
|
172
|
+
const message = readLastAssistantTurn(transcriptPath);
|
|
173
|
+
if (!message) return; // no transcript visible; nothing to check
|
|
174
|
+
|
|
175
|
+
const text = extractText(message);
|
|
176
|
+
const toolUseSummary = extractToolUseSummary(message);
|
|
177
|
+
const claim = findClaim(text);
|
|
178
|
+
if (!claim) return; // no completion claim made; silent
|
|
179
|
+
|
|
180
|
+
const proofText = `${text}\n${toolUseSummary}`;
|
|
181
|
+
if (hasProof(proofText)) return; // claim backed by proof in same turn
|
|
182
|
+
|
|
183
|
+
// Strict mode (THUMBGATE_STRICT_ENFORCEMENT=1): hard-block the stop. Emit a
|
|
184
|
+
// Stop-hook block decision so Claude Code does NOT end the turn — the agent
|
|
185
|
+
// must run the verification (or retract) before it can stop. Default mode
|
|
186
|
+
// stays soft (a reminder for the next turn) so we don't break existing wiring.
|
|
187
|
+
if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') {
|
|
188
|
+
const reason = `ThumbGate anti-claim gate (strict): you claimed completion ("${claim}") without a proof tool call in the same message. Run the verification (curl / grep / test / Read) and restate with the proof, or retract — do not end the turn on an unverified claim.`;
|
|
189
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason }) + '\n');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Default (soft): surface a system reminder for the NEXT turn. Do not hard-block.
|
|
194
|
+
const reminder = [
|
|
195
|
+
'⚠️ ThumbGate anti-claim gate: previous turn claimed completion',
|
|
196
|
+
` ("${claim}") without a proof tool call in the same message.`,
|
|
197
|
+
' Per CLAUDE.md anti-lying: never claim "done / live / deployed / fixed /',
|
|
198
|
+
' verified / all green / stable" or commercial truth (money / tax / inventory /',
|
|
199
|
+
' permissions / customer-facing state) without curl / grep / test / source-of-truth output in the SAME turn.',
|
|
200
|
+
' If the work really is verified, re-state the claim with the proof.',
|
|
201
|
+
' If not, retract and run the verification before re-asserting.',
|
|
202
|
+
].join('\n');
|
|
203
|
+
process.stdout.write(reminder + '\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Path-resolve check instead of `require.main === module`. SonarCloud's
|
|
207
|
+
// strict type inference (rule S3403) flags the === form as always-false
|
|
208
|
+
// in CommonJS, and CLAUDE.md "Hard-Won Lessons" pins the path-based form
|
|
209
|
+
// as the portable fix (incident 2026-04-21 / PR #1115). Resolve BOTH sides
|
|
210
|
+
// so the comparison is between two normalized absolute paths.
|
|
211
|
+
const path = require('node:path');
|
|
212
|
+
if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
|
|
213
|
+
try {
|
|
214
|
+
main();
|
|
215
|
+
} catch {
|
|
216
|
+
// never crash the agent
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
CLAIM_PATTERNS,
|
|
222
|
+
PROOF_PATTERNS,
|
|
223
|
+
findClaim,
|
|
224
|
+
hasProof,
|
|
225
|
+
extractText,
|
|
226
|
+
extractToolUseSummary,
|
|
227
|
+
};
|
|
@@ -6,11 +6,27 @@
|
|
|
6
6
|
* Also used directly by the CLI to refresh statusline counters after feedback capture.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const os = require('node:os');
|
|
11
|
+
const path = require('node:path');
|
|
11
12
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
13
|
+
const {
|
|
14
|
+
getAggregateStatuslineCachePath,
|
|
15
|
+
shouldAggregateFeedback,
|
|
16
|
+
} = require('./feedback-aggregate');
|
|
12
17
|
|
|
13
18
|
function getCachePath() {
|
|
19
|
+
const explicitFeedbackDir = process.env.THUMBGATE_FEEDBACK_DIR;
|
|
20
|
+
if (shouldAggregateFeedback() && explicitFeedbackDir) {
|
|
21
|
+
try {
|
|
22
|
+
if (path.resolve(explicitFeedbackDir).startsWith(path.resolve(os.tmpdir()) + path.sep)) {
|
|
23
|
+
return path.join(resolveFeedbackDir(), 'statusline_cache.json');
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
return path.join(resolveFeedbackDir(), 'statusline_cache.json');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (shouldAggregateFeedback()) return getAggregateStatuslineCachePath();
|
|
14
30
|
return path.join(resolveFeedbackDir(), 'statusline_cache.json');
|
|
15
31
|
}
|
|
16
32
|
|
|
@@ -18,6 +18,7 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
20
20
|
const { ensureParentDir, readJsonl } = require('./fs-utils');
|
|
21
|
+
const { redactSecretsDeep } = require('./secret-redaction');
|
|
21
22
|
const {
|
|
22
23
|
buildStableId,
|
|
23
24
|
extractFilePaths,
|
|
@@ -137,15 +138,19 @@ function createLesson({ feedbackId, signal, inferredLesson, triggerMessage, prio
|
|
|
137
138
|
// Stable link: dashboard deep-link to this lesson
|
|
138
139
|
lesson.link = `${getLessonBaseUrl()}/lessons#${lesson.id}`;
|
|
139
140
|
|
|
141
|
+
// Redact secrets before persisting — lesson text is derived from conversation content that may
|
|
142
|
+
// have captured a pasted credential. See scripts/secret-redaction.js.
|
|
143
|
+
const redactedLesson = redactSecretsDeep(lesson);
|
|
144
|
+
|
|
140
145
|
const lessonsPath = getLessonsPath();
|
|
141
146
|
ensureParentDir(lessonsPath);
|
|
142
|
-
fs.appendFileSync(lessonsPath, JSON.stringify(
|
|
147
|
+
fs.appendFileSync(lessonsPath, JSON.stringify(redactedLesson) + '\n');
|
|
143
148
|
|
|
144
149
|
// Update recent lesson for statusbar
|
|
145
150
|
const recentPath = getRecentLessonPath();
|
|
146
|
-
fs.writeFileSync(recentPath, JSON.stringify(
|
|
151
|
+
fs.writeFileSync(recentPath, JSON.stringify(redactedLesson, null, 2) + '\n');
|
|
147
152
|
|
|
148
|
-
return
|
|
153
|
+
return redactedLesson;
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
/**
|
package/scripts/lesson-search.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { readJSONL, getFeedbackPaths } = require('./feedback-loop');
|
|
5
|
+
const { buildMemoryLifecycleView, scoreHybridMemoryMatch } = require('./agent-memory-lifecycle');
|
|
5
6
|
const { loadOptionalModule } = require('./private-core-boundary');
|
|
6
7
|
|
|
7
8
|
const HIGH_RISK_TAGS = new Set([
|
|
@@ -411,7 +412,8 @@ function scoreLesson(queryText, memory, parsed, sourceFeedback) {
|
|
|
411
412
|
const score = jaccardSimilarity(queryTokens, lessonTokens)
|
|
412
413
|
+ substringBoost(queryText, lessonText)
|
|
413
414
|
+ recencyScore(memory.timestamp)
|
|
414
|
-
+ (memory.category === 'error' ? 0.05 : 0)
|
|
415
|
+
+ (memory.category === 'error' ? 0.05 : 0)
|
|
416
|
+
+ Math.min(0.2, scoreHybridMemoryMatch(queryText, memory).score * 0.1);
|
|
415
417
|
|
|
416
418
|
return {
|
|
417
419
|
score,
|
|
@@ -427,6 +429,7 @@ function buildLessonResult(memory, sourceFeedback, options = {}) {
|
|
|
427
429
|
const { score, matchedTokens } = scoreLesson(options.query || '', memory, parsed, sourceFeedback);
|
|
428
430
|
const harnessRecommendations = buildHarnessRecommendations(memory, parsed, sourceFeedback, ruleMatches, gateMatches);
|
|
429
431
|
const lifecycle = buildLifecycle(memory, parsed, sourceFeedback, ruleMatches, gateMatches, harnessRecommendations);
|
|
432
|
+
const memoryLifecycle = buildMemoryLifecycleView(memory, { query: options.query || '' });
|
|
430
433
|
|
|
431
434
|
return {
|
|
432
435
|
id: memory.id,
|
|
@@ -452,6 +455,7 @@ function buildLessonResult(memory, sourceFeedback, options = {}) {
|
|
|
452
455
|
systemResponse: {
|
|
453
456
|
promotedToMemory: true,
|
|
454
457
|
lifecycle,
|
|
458
|
+
memoryLifecycle,
|
|
455
459
|
diagnosis: memory.diagnosis || null,
|
|
456
460
|
sourceFeedback: sourceFeedback
|
|
457
461
|
? {
|
|
@@ -589,6 +593,14 @@ function tryFts5Search(query, options) {
|
|
|
589
593
|
tags: row.tags,
|
|
590
594
|
importance: row.importance,
|
|
591
595
|
timestamp: row.timestamp,
|
|
596
|
+
memoryLifecycle: buildMemoryLifecycleView({
|
|
597
|
+
title: row.context,
|
|
598
|
+
content: [row.whatWentWrong, row.whatToChange, row.whatWorked].filter(Boolean).join('\n'),
|
|
599
|
+
domain: row.domain,
|
|
600
|
+
tags: row.tags,
|
|
601
|
+
importance: row.importance,
|
|
602
|
+
timestamp: row.timestamp,
|
|
603
|
+
}, { query: query || '' }),
|
|
592
604
|
})),
|
|
593
605
|
backend: 'sqlite-fts5',
|
|
594
606
|
};
|
|
@@ -618,6 +630,10 @@ function formatLessonSearchResults(payload) {
|
|
|
618
630
|
payload.results.forEach((result, index) => {
|
|
619
631
|
lines.push(`${index + 1}. ${result.title}`);
|
|
620
632
|
lines.push(` Category: ${result.category} | Tags: ${result.tags.join(', ') || 'none'} | Score: ${result.score}`);
|
|
633
|
+
if (result.systemResponse.memoryLifecycle) {
|
|
634
|
+
const memoryLifecycle = result.systemResponse.memoryLifecycle;
|
|
635
|
+
lines.push(` Memory: scope=${memoryLifecycle.scope} | decay=${memoryLifecycle.decay.state} | hybrid=${memoryLifecycle.retrievalHints.hybridScore}`);
|
|
636
|
+
}
|
|
621
637
|
if (result.lesson.summary) {
|
|
622
638
|
lines.push(` Lesson: ${result.lesson.summary}`);
|
|
623
639
|
}
|