ruvnet-brain 4.5.16 → 4.6.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/README.md +15 -19
- package/bin/developer-update.mjs +3 -0
- package/bin/install.mjs +15 -3
- package/bin/nightly-refresh.mjs +17 -1
- package/config/practical-rule-catalog.json +111 -0
- package/console/CONTRACT.md +47 -3
- package/console/app.js +112 -20
- package/package.json +5 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +13 -1
- package/plugin/commands/configure.md +2 -2
- package/plugin/commands/rnb.md +13 -1
- package/plugin/commands/rnbc.md +13 -1
- package/plugin/commands/rvbc.md +13 -1
- package/plugin/commands/rvcb.md +13 -1
- package/plugin/hooks/codex-hooks.json +13 -1
- package/plugin/hooks/hook-contracts.json +20 -4
- package/plugin/scripts/adr-currency-gate.mjs +75 -31
- package/plugin/scripts/advocacy-outcomes.mjs +18 -4
- package/plugin/scripts/agentdb-recall.mjs +161 -26
- package/plugin/scripts/automatic-update.mjs +11 -2
- package/plugin/scripts/capability-inventory-receipt.mjs +16 -7
- package/plugin/scripts/capability-registry.mjs +55 -114
- package/plugin/scripts/capacity-aware-parallel-work.mjs +1 -1
- package/plugin/scripts/codex-hook-adapter.mjs +33 -6
- package/plugin/scripts/codex-hook-wrapper.mjs +23 -3
- package/plugin/scripts/continuation-gate.mjs +2 -1
- package/plugin/scripts/continuity-brief.mjs +55 -22
- package/plugin/scripts/continuity-events.mjs +93 -17
- package/plugin/scripts/continuity-hook-policy.mjs +1 -1
- package/plugin/scripts/continuity-journal.mjs +77 -29
- package/plugin/scripts/decision-gate.mjs +62 -37
- package/plugin/scripts/decision-outcomes.mjs +192 -199
- package/plugin/scripts/detach.mjs +53 -13
- package/plugin/scripts/developer-update-cleanup.mjs +38 -0
- package/plugin/scripts/developer-update-lock.mjs +30 -0
- package/plugin/scripts/developer-update-maintenance.mjs +178 -0
- package/plugin/scripts/developer-update-owner.mjs +24 -0
- package/plugin/scripts/developer-update-policy.mjs +48 -0
- package/plugin/scripts/developer-update.mjs +435 -0
- package/plugin/scripts/doc-currency.mjs +993 -0
- package/plugin/scripts/ground-ruvnet.sh +16 -3
- package/plugin/scripts/grounding-answer.mjs +18 -16
- package/plugin/scripts/grounding-stamp.sh +3 -0
- package/plugin/scripts/grounding-turn-evidence.mjs +60 -65
- package/plugin/scripts/grounding-turn-gate.mjs +34 -29
- package/plugin/scripts/grounding-turn-mark.mjs +101 -17
- package/plugin/scripts/hook-context-budget.mjs +90 -0
- package/plugin/scripts/hook-registry.mjs +129 -1
- package/plugin/scripts/hook-shim.mjs +20 -2
- package/plugin/scripts/host-update.mjs +6 -3
- package/plugin/scripts/lesson-gate.mjs +1 -1
- package/plugin/scripts/nightly-scheduler.mjs +55 -16
- package/plugin/scripts/plugin-artifact-proof.mjs +135 -0
- package/plugin/scripts/project-capture-queue.mjs +22 -15
- package/plugin/scripts/project-progression-producer.mjs +15 -8
- package/plugin/scripts/project-progression-reader.mjs +14 -4
- package/plugin/scripts/project-progression-session-start.mjs +29 -17
- package/plugin/scripts/project-progression-sources.mjs +24 -10
- package/plugin/scripts/project-progression-store.mjs +7 -2
- package/plugin/scripts/project-store-resolver.mjs +10 -7
- package/plugin/scripts/project-transition-hook.mjs +20 -12
- package/plugin/scripts/session-snapshot-hook.mjs +20 -9
- package/plugin/scripts/session-start-budget.mjs +18 -0
- package/plugin/scripts/session-start-core.mjs +32 -6
- package/plugin/scripts/session-start-trace.mjs +2 -1
- package/plugin/scripts/turn-outcome-capture.mjs +7 -5
- package/plugin/scripts/unprompted-runtime.mjs +36 -10
- package/plugin/scripts/user-settings.mjs +7 -1
- package/plugin/skills/ruvnet-brain/SKILL.md +2 -2
- package/scripts/codex-managed-terminal.mjs +1 -1
- package/scripts/console-runtime-identity.mjs +2 -0
- package/scripts/console-suite-update.mjs +117 -0
- package/scripts/doc-currency.mjs +6 -990
- package/scripts/hook-qualify-core.mjs +30 -1
- package/scripts/model-managed-prompt.mjs +1 -1
- package/scripts/model-managed-workflow-service.mjs +113 -14
- package/scripts/model-routing-controller.mjs +112 -21
- package/scripts/onboarding-console.mjs +41 -12
- package/scripts/practical-rule-selector.mjs +51 -0
- package/scripts/product-integrity-contract.mjs +11 -4
- package/scripts/release-qualification-contract.mjs +14 -0
- package/scripts/remedy-registry.mjs +1 -0
- package/scripts/source-scope-receipt.mjs +9 -4
- package/scripts/stack-sync.mjs +19 -132
|
@@ -25,11 +25,12 @@
|
|
|
25
25
|
* import graph in four fixtures, two ship-command definitions shipped disagreeing on day one).
|
|
26
26
|
*
|
|
27
27
|
* FAIL OPEN, ALWAYS. Not a git repo, unreadable doc, git unavailable, anything unexpected: ALLOW,
|
|
28
|
-
*
|
|
28
|
+
* with a typed skipped-policy diagnostic when an owning-checkout inspection cannot finish. An adversarial review earlier today found a sibling hook turning a missing `sqlite3`
|
|
29
29
|
* into a confident claim that the memory store was corrupt. A gate that fabricates a reason is worse
|
|
30
30
|
* than no gate, because it spends the credibility every other gate is drawing on.
|
|
31
31
|
*/
|
|
32
32
|
import fs from 'node:fs';
|
|
33
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
33
34
|
import path from 'node:path';
|
|
34
35
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
36
|
|
|
@@ -47,24 +48,18 @@ const STALE = new Set(['presumed-stale']);
|
|
|
47
48
|
* the Write path. Same logic, narrower question.
|
|
48
49
|
*/
|
|
49
50
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* `import('../../scripts/doc-currency.mjs')` would have shipped a specifier that resolves only in a
|
|
54
|
-
* checkout and throws ERR_MODULE_NOT_FOUND on every real install — the exact defect ADR-065 exists
|
|
55
|
-
* to stop, which `payload-self-contained.test.mjs` caught in the very commit citing ADR-065.
|
|
56
|
-
*
|
|
57
|
-
* So the dependency is resolved at runtime and its ABSENCE IS A CLEAN SKIP, not an error: outside a
|
|
58
|
-
* checkout this gate simply has no opinion.
|
|
51
|
+
* The canonical evaluator ships adjacent to this owned hook. The root CLI is a thin compatibility
|
|
52
|
+
* entrypoint; an edited checkout supplies only files and Git history, never executable evaluator
|
|
53
|
+
* code. Missing owned bytes are unavailable evidence, not permission to import project JavaScript.
|
|
59
54
|
*/
|
|
60
|
-
async function loadDocCurrency(
|
|
61
|
-
const p = path.join(
|
|
55
|
+
async function loadDocCurrency() {
|
|
56
|
+
const p = path.join(HERE, 'doc-currency.mjs');
|
|
62
57
|
if (!fs.existsSync(p)) return null;
|
|
63
58
|
try { return await import(pathToFileURL(p).href); } catch { return null; }
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
export async function staleGovernorsOf(relPath, { root = REPO, docCurrency = null, readFile = null } = {}) {
|
|
67
|
-
const mod = docCurrency ?? await loadDocCurrency(
|
|
62
|
+
const mod = docCurrency ?? await loadDocCurrency();
|
|
68
63
|
if (!mod) return [];
|
|
69
64
|
const { listDocs, evaluateDoc, parseFrontmatter, resolveGoverned, blockingFindings, DEFAULT_DIRS, isGitRepo } = mod;
|
|
70
65
|
// `readFile` is injectable for one reason, and it is not tidiness: with `fs.readFileSync` hardcoded
|
|
@@ -137,24 +132,73 @@ const isMain = (() => {
|
|
|
137
132
|
catch { return false; }
|
|
138
133
|
})();
|
|
139
134
|
|
|
135
|
+
/** Installed location is transport, never the authority for the edited repository. */
|
|
136
|
+
export function owningEditedCheckout(payload, deadline = Date.now() + 1000) {
|
|
137
|
+
const input = payload?.tool_input ?? {};
|
|
138
|
+
const file = input.file_path || input.path;
|
|
139
|
+
if (typeof file !== 'string' || !file) return null;
|
|
140
|
+
let absolute = path.resolve(payload.cwd || process.cwd(), file);
|
|
141
|
+
if (fs.existsSync(absolute)) absolute = fs.realpathSync.native(absolute);
|
|
142
|
+
let directory = path.dirname(absolute);
|
|
143
|
+
while (!fs.existsSync(directory)) {
|
|
144
|
+
const parent = path.dirname(directory);
|
|
145
|
+
if (parent === directory) return null;
|
|
146
|
+
directory = parent;
|
|
147
|
+
}
|
|
148
|
+
const suffix = path.relative(directory, absolute);
|
|
149
|
+
directory = fs.realpathSync.native(directory);
|
|
150
|
+
absolute = path.join(directory, suffix);
|
|
151
|
+
const remaining = deadline - Date.now();
|
|
152
|
+
if (remaining <= 0) throw new Error('ADR scope deadline exceeded');
|
|
153
|
+
let root;
|
|
154
|
+
try { root = execFileSync('git', ['-C', directory, 'rev-parse', '--show-toplevel'], {
|
|
155
|
+
encoding: 'utf8', timeout: remaining, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'ignore'],
|
|
156
|
+
}).trim(); } catch (error) { if (error.code === 'ETIMEDOUT') throw error; return null; }
|
|
157
|
+
const rel = path.relative(root, absolute).split(path.sep).join('/');
|
|
158
|
+
if (rel.startsWith('../') || rel === '..' || rel.startsWith('docs/')) return null;
|
|
159
|
+
if (!fs.existsSync(path.join(root, 'docs', 'adr'))) return null;
|
|
160
|
+
return { root, rel };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Keep imports and the reused evaluator's Git descendants inside the parent decision budget. */
|
|
164
|
+
export function boundedStaleGovernorsOf(scope, deadline) {
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
const remaining = deadline - Date.now() - 250;
|
|
167
|
+
if (remaining <= 0) return resolve({ skipped: 'ADR evaluation deadline exceeded' });
|
|
168
|
+
let settled = false; let timer; let output = '';
|
|
169
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), '--evaluate-owning-checkout', scope.root, scope.rel], {
|
|
170
|
+
detached: process.platform !== 'win32' && process.env.RUVNET_DECISION_GATE !== '1', stdio: ['ignore', 'pipe', 'ignore'],
|
|
171
|
+
});
|
|
172
|
+
const finish = (value) => { if (!settled) { settled = true; clearTimeout(timer); resolve(value); } };
|
|
173
|
+
const kill = () => { try { if (process.platform !== 'win32' && process.env.RUVNET_DECISION_GATE !== '1') process.kill(-child.pid, 'SIGKILL'); else child.kill('SIGKILL'); } catch {} };
|
|
174
|
+
timer = setTimeout(() => { kill(); finish({ skipped: 'ADR evaluation deadline exceeded' }); }, remaining);
|
|
175
|
+
child.stdout.on('data', (chunk) => { output += chunk; if (output.length > 65536) { kill(); finish({ skipped: 'ADR evaluation output exceeded bound' }); } });
|
|
176
|
+
child.once('error', () => finish({ skipped: 'ADR evaluator unavailable' }));
|
|
177
|
+
child.once('close', (code) => { kill(); if (code !== 0) return finish({ skipped: 'ADR evaluator failed' });
|
|
178
|
+
try { const stale = JSON.parse(output); if (!Array.isArray(stale)) throw Error(); finish({ stale }); }
|
|
179
|
+
catch { finish({ skipped: 'ADR evaluator returned invalid result' }); }
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
140
184
|
if (isMain) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
process.
|
|
185
|
+
if (process.argv[2] === '--evaluate-owning-checkout') {
|
|
186
|
+
try {
|
|
187
|
+
const ownedEvaluator = await loadDocCurrency();
|
|
188
|
+
if (!ownedEvaluator) throw new Error('Brain-owned evaluator unavailable');
|
|
189
|
+
process.stdout.write(JSON.stringify(await staleGovernorsOf(process.argv[4], { root: process.argv[3], docCurrency: ownedEvaluator })));
|
|
190
|
+
}
|
|
191
|
+
catch { process.exitCode = 1; }
|
|
192
|
+
} else {
|
|
193
|
+
try {
|
|
194
|
+
const deadline = Math.min(Number(process.env.RUVNET_DECISION_DEADLINE) || Infinity, Date.now() + 3000);
|
|
195
|
+
const payload = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
196
|
+
const scope = owningEditedCheckout(payload, deadline);
|
|
197
|
+
if (scope) {
|
|
198
|
+
const result = await boundedStaleGovernorsOf(scope, deadline);
|
|
199
|
+
if (result.skipped) { process.stderr.write(result.skipped + '\n'); process.exitCode = 3; }
|
|
200
|
+
else if (result.stale.length) { process.stderr.write(refusalText(scope.rel, result.stale) + '\n'); process.exitCode = 2; }
|
|
201
|
+
}
|
|
202
|
+
} catch { process.stderr.write('ADR owning-checkout inspection unavailable; no currency verdict was obtained.\n'); process.exitCode = 3; }
|
|
159
203
|
}
|
|
160
204
|
}
|
|
@@ -366,15 +366,25 @@ export function record(spec, { file = OUTCOMES_PATH } = {}) {
|
|
|
366
366
|
* the brain — and it would be silent in exactly the way it is silent when everything is healthy, so
|
|
367
367
|
* nobody would ever find out.
|
|
368
368
|
*/
|
|
369
|
-
export function loadOutcomes(file = OUTCOMES_PATH) {
|
|
369
|
+
export function loadOutcomes(file = OUTCOMES_PATH, { strict = false } = {}) {
|
|
370
370
|
let raw;
|
|
371
|
-
try { raw = fs.readFileSync(file, 'utf8'); } catch {
|
|
371
|
+
try { raw = fs.readFileSync(file, 'utf8'); } catch (error) {
|
|
372
|
+
if (strict && error.code !== 'ENOENT') throw error;
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
372
375
|
const out = [];
|
|
373
376
|
for (const line of raw.split('\n')) {
|
|
374
377
|
const s = line.trim();
|
|
375
378
|
if (!s) continue;
|
|
376
379
|
let r;
|
|
377
|
-
try { r = JSON.parse(s); } catch { continue; } // torn or hand-mangled line: drop it, keep the rest
|
|
380
|
+
try { r = JSON.parse(s); } catch (error) { if (strict) throw error; continue; } // torn or hand-mangled line: drop it, keep the rest
|
|
381
|
+
if (strict && (!r || typeof r !== 'object' || Array.isArray(r)
|
|
382
|
+
|| (r.v !== undefined && r.v !== 1)
|
|
383
|
+
|| typeof r.id !== 'string' || !r.id || !ACTION_VALUES.has(r.action)
|
|
384
|
+
|| (r.scope !== undefined && r.scope !== null && r.scope !== 'forever')
|
|
385
|
+
|| (r.scope === 'forever' && r.action !== ACTIONS.DISMISSED))) {
|
|
386
|
+
throw new Error('Suppression history contains an unsupported record');
|
|
387
|
+
}
|
|
378
388
|
if (!r || typeof r !== 'object') continue;
|
|
379
389
|
if (typeof r.id !== 'string' || !r.id) continue;
|
|
380
390
|
if (!ACTION_VALUES.has(r.action)) continue; // an action we do not understand is not counted as one we do
|
|
@@ -476,7 +486,11 @@ export function outcomesFor(id, { file = OUTCOMES_PATH, all = null, project = nu
|
|
|
476
486
|
export function shouldStillOffer(id, {
|
|
477
487
|
severity = null, stateHash = null, file = OUTCOMES_PATH, all = null,
|
|
478
488
|
} = {}) {
|
|
479
|
-
|
|
489
|
+
let history = all;
|
|
490
|
+
if (history === null) {
|
|
491
|
+
try { history = loadOutcomes(file, { strict: true }); } catch { return false; }
|
|
492
|
+
}
|
|
493
|
+
const o = outcomesFor(id, { file, all: history });
|
|
480
494
|
|
|
481
495
|
if (o.silencedForever) return false;
|
|
482
496
|
if (!o.offered) return true;
|
|
@@ -19,11 +19,40 @@ import { learningTarget, LEARNING_NAMESPACE } from './learning-store.mjs';
|
|
|
19
19
|
import { safeAction } from './learning-queue.mjs';
|
|
20
20
|
|
|
21
21
|
import { redactText } from './continuity-events.mjs';
|
|
22
|
+
import { withProgressionReader } from './project-progression-reader.mjs';
|
|
23
|
+
import { makeLesson } from './lesson-store.mjs';
|
|
22
24
|
export const STORE_FILES = Object.freeze(['memory.db']);
|
|
23
25
|
export const DEFAULT_DEADLINE_MS = 1900;
|
|
24
26
|
export const BLOCK_MAX_BYTES = 600;
|
|
27
|
+
export const CONSEQUENTIAL_BLOCK_MAX_BYTES = 1800;
|
|
25
28
|
export const MIN_RELEVANCE = 0.45;
|
|
26
29
|
|
|
30
|
+
export function recallBinding({ binding = {}, projectRoot, storePath, prompt } = {}) {
|
|
31
|
+
const allowed = ['projectRoot', 'storePath', 'sessionId', 'workflowId', 'phase', 'workerId', 'requestDigest'];
|
|
32
|
+
if (!binding || typeof binding !== 'object' || Array.isArray(binding) || Object.keys(binding).some(key => !allowed.includes(key))
|
|
33
|
+
|| binding.projectRoot !== undefined && binding.projectRoot !== projectRoot || binding.storePath !== undefined && binding.storePath !== storePath
|
|
34
|
+
|| ['sessionId', 'workflowId', 'workerId'].some(key => binding[key] != null && (typeof binding[key] !== 'string' || !binding[key] || binding[key].length > 128))
|
|
35
|
+
|| binding.phase !== undefined && (typeof binding.phase !== 'string' || !binding.phase || binding.phase.length > 64)
|
|
36
|
+
|| binding.requestDigest !== undefined && !/^[a-f0-9]{64}$/.test(binding.requestDigest)) throw new Error('Invalid canonical recall phase binding');
|
|
37
|
+
return { projectRoot, storePath, sessionId: binding.sessionId ?? null, workflowId: binding.workflowId ?? null,
|
|
38
|
+
phase: binding.phase ?? 'prompt', workerId: binding.workerId ?? null,
|
|
39
|
+
requestDigest: binding.requestDigest ?? crypto.createHash('sha256').update(String(prompt)).digest('hex') };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Bounded enumeration is structural evidence; exact values still come from global Ruflo. */
|
|
43
|
+
function enumerateCanonicalKeys({ storePath, namespaces, deadline, signal }) {
|
|
44
|
+
return withProgressionReader(storePath, reader => Object.fromEntries(namespaces.map(namespace => [namespace, reader.listKeys(namespace, { maxEntries: 2000 })])), { deadlineAt: deadline, signal });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function applicableRecallLesson(value, projectRoot) {
|
|
48
|
+
try {
|
|
49
|
+
const spec = JSON.parse(value);
|
|
50
|
+
if (!spec || !Array.isArray(spec.projects) || !spec.projects.includes(projectRoot) || !['ratified', 'active'].includes(spec.status)
|
|
51
|
+
|| typeof spec.ratifiedBy !== 'string' || !spec.ratifiedBy || spec.demoted === true) return null;
|
|
52
|
+
return makeLesson(spec);
|
|
53
|
+
} catch { return null; }
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
export function agentdbFirstEnabled(env = process.env) {
|
|
28
57
|
return !/^(?:off|0|false|no|disabled?)$/i.test(String(env.RUVNET_AGENTDB_FIRST || '').trim());
|
|
29
58
|
}
|
|
@@ -66,13 +95,18 @@ export function recallQuery(prompt) {
|
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
export function parseSearchJson(stdout) {
|
|
98
|
+
return searchJsonResult(stdout).rows;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function searchJsonResult(stdout) {
|
|
69
102
|
const s = String(stdout || '');
|
|
70
103
|
// Live Ruflo prints warnings AFTER the JSON object as well as logs before it.
|
|
71
104
|
const start = s.indexOf('{'); const end = s.lastIndexOf('}');
|
|
72
|
-
if (start < 0 || end < start) return [];
|
|
105
|
+
if (start < 0 || end < start) return { valid: false, rows: [] };
|
|
73
106
|
try { const rows = JSON.parse(s.slice(start, end + 1)).results;
|
|
74
|
-
return Array.isArray(rows)
|
|
75
|
-
|
|
107
|
+
return Array.isArray(rows) && rows.every(r => r && typeof r.key === 'string' && r.key)
|
|
108
|
+
? { valid: true, rows } : { valid: false, rows: [] };
|
|
109
|
+
} catch { return { valid: false, rows: [] }; }
|
|
76
110
|
}
|
|
77
111
|
|
|
78
112
|
function clean(value, limit) {
|
|
@@ -199,10 +233,10 @@ function killGroup(child) {
|
|
|
199
233
|
}
|
|
200
234
|
|
|
201
235
|
/** One ruflo search, bounded by an absolute deadline. Resolves { rows, state } — never rejects. */
|
|
202
|
-
function searchOnce({ ruflo, store, args, deadline, env, scratch, operation = 'search' }) {
|
|
236
|
+
function searchOnce({ ruflo, store, args, deadline, env, scratch, operation = 'search', signal }) {
|
|
203
237
|
return new Promise((resolve) => {
|
|
204
238
|
const remaining = deadline - Date.now();
|
|
205
|
-
if (remaining <= 0) { resolve({ rows: [], state: 'timed out' }); return; }
|
|
239
|
+
if (signal?.aborted || remaining <= 0) { resolve({ rows: [], state: signal?.aborted ? 'unavailable' : 'timed out' }); return; }
|
|
206
240
|
let cwd;
|
|
207
241
|
try { cwd = fs.mkdtempSync(path.join(scratch(store.path), 'run-')); } catch { resolve({ rows: [], state: 'unavailable' }); return; }
|
|
208
242
|
const cleanup = () => { try { fs.rmSync(cwd, { recursive: true, force: true }); } catch { /* swept later as a stale run- dir */ } };
|
|
@@ -214,33 +248,125 @@ function searchOnce({ ruflo, store, args, deadline, env, scratch, operation = 's
|
|
|
214
248
|
child = spawn(inv.executable, inv.args, { cwd, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0' },
|
|
215
249
|
stdio: ['ignore', 'pipe', 'ignore'], detached: process.platform !== 'win32', windowsHide: true });
|
|
216
250
|
} catch { cleanup(); resolve({ rows: [], state: 'unavailable' }); return; }
|
|
217
|
-
let out = '';
|
|
218
|
-
|
|
219
|
-
|
|
251
|
+
let out = '', outputBytes = 0;
|
|
252
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
253
|
+
let done = false, retiring = null, retirementTimer, timer;
|
|
254
|
+
const finish = (state, retirement = process.platform === 'win32' ? 'direct-child-only' : 'confirmed') => {
|
|
220
255
|
if (done) return;
|
|
221
256
|
done = true;
|
|
222
257
|
clearTimeout(timer);
|
|
223
|
-
|
|
258
|
+
clearTimeout(retirementTimer);
|
|
259
|
+
signal?.removeEventListener('abort', cancel);
|
|
224
260
|
cleanup();
|
|
225
|
-
|
|
261
|
+
const parsed = operation === 'search' ? searchJsonResult(out) : null;
|
|
262
|
+
if (state === 'ok' && parsed && !parsed.valid) state = 'unavailable';
|
|
263
|
+
resolve({ rows: state === 'ok' && parsed ? parsed.rows : [], value: state === 'ok' ? out.trim() : '', state, retirement });
|
|
226
264
|
};
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
265
|
+
const retire = state => {
|
|
266
|
+
if (done || retiring) return;
|
|
267
|
+
retiring = state; killGroup(child);
|
|
268
|
+
retirementTimer = setTimeout(() => finish('unavailable', 'unconfirmed'), Math.max(1, deadline - Date.now()));
|
|
269
|
+
};
|
|
270
|
+
const cancel = () => retire('unavailable');
|
|
271
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
272
|
+
// Reserve cleanup inside the same caller deadline; pipe inheritance cannot report success.
|
|
273
|
+
timer = setTimeout(() => retire('timed out'), Math.max(1, deadline - Date.now() - 100));
|
|
274
|
+
child.stdout.on('data', c => { if (done || retiring) return; outputBytes += c.length;
|
|
275
|
+
if (outputBytes > 1 << 20) return retire('unavailable');
|
|
276
|
+
try { out += decoder.decode(c, { stream: true }); } catch { retire('unavailable'); } });
|
|
277
|
+
child.on('error', () => retire('unavailable'));
|
|
278
|
+
child.on('close', code => {
|
|
279
|
+
killGroup(child); // Successful parent exit must not leave an owned detached grandchild.
|
|
280
|
+
let valid = true; try { out += decoder.decode(); } catch { valid = false; }
|
|
281
|
+
const settle = () => {
|
|
282
|
+
if (done) return;
|
|
283
|
+
let alive = false;
|
|
284
|
+
if (process.platform !== 'win32') {
|
|
285
|
+
try { process.kill(-child.pid, 0); alive = true; } catch (error) { alive = error.code !== 'ESRCH'; }
|
|
286
|
+
}
|
|
287
|
+
if (alive && Date.now() < deadline) { retirementTimer = setTimeout(settle, 5); return; }
|
|
288
|
+
finish(alive || !valid ? 'unavailable' : retiring ?? (code === 0 ? 'ok' : 'failed'), alive ? 'unconfirmed' : undefined);
|
|
289
|
+
};
|
|
290
|
+
clearTimeout(retirementTimer); settle();
|
|
291
|
+
});
|
|
292
|
+
if (signal?.aborted) cancel();
|
|
232
293
|
});
|
|
233
294
|
}
|
|
234
295
|
|
|
235
|
-
|
|
296
|
+
async function consequentialRecall({ prompt, root, store, bin, scratch, env, deadline, binding, signal, enumerateKeys }) {
|
|
297
|
+
const phaseBinding = recallBinding({ binding, projectRoot: root, storePath: store.path, prompt });
|
|
298
|
+
const namespaces = [...new Set([path.basename(root), 'default', 'lessons', 'continuity-events'])];
|
|
299
|
+
const enumeration = await enumerateKeys({ storePath: store.path, namespaces, deadline, signal });
|
|
300
|
+
if (!enumeration?.ok || signal?.aborted || Date.now() >= deadline) return { block: formatBlock({ picks: [], status: 'unavailable structural enumeration' }), picks: [], stores: [store],
|
|
301
|
+
status: { 'memory.db': 'unavailable' }, outcome: signal?.aborted ? 'unavailable' : Date.now() >= deadline ? 'timed-out' : 'unavailable', categories: { state: 'unavailable', decisions: 'unavailable', lessons: 'unavailable' } };
|
|
302
|
+
const keys = enumeration.value;
|
|
303
|
+
const stateKeys = keys[path.basename(root)].filter(key => /^project-state-current-\d+$/.test(key) && Number.isSafeInteger(Number(key.split('-').at(-1))))
|
|
304
|
+
.sort((a, b) => Number(b.split('-').at(-1)) - Number(a.split('-').at(-1)));
|
|
305
|
+
const decisionNamespaces = namespaces.filter(namespace => namespace !== 'lessons');
|
|
306
|
+
const decisionKeys = decisionNamespaces.flatMap(namespace => keys[namespace].filter(key => /^decision[-_]|^cevt-.*-decision-/i.test(key)).map(key => ({ category: 'decisions', namespace, key })));
|
|
307
|
+
let relevantDecisions = decisionKeys;
|
|
308
|
+
let decisionAvailability = 'ok-empty';
|
|
309
|
+
if (decisionKeys.length > 8) {
|
|
310
|
+
const searched = await Promise.all(decisionNamespaces.map(async namespace => ({ namespace,
|
|
311
|
+
...await searchOnce({ ruflo: bin, store, deadline, env, scratch, signal, args: ['--format', 'json', '-q', recallQuery(prompt), '-n', namespace, '--limit', '12'] }) })));
|
|
312
|
+
if (searched.some(row => row.state !== 'ok')) decisionAvailability = searched.some(row => row.state === 'timed out') ? 'timed-out' : 'unavailable';
|
|
313
|
+
relevantDecisions = searched.flatMap(result => result.rows.filter(row => row.namespace === result.namespace && Number.isFinite(row.score) && row.score >= MIN_RELEVANCE
|
|
314
|
+
&& decisionKeys.some(key => key.namespace === row.namespace && key.key === row.key)).map(row => ({ category: 'decisions', namespace: row.namespace, key: row.key, score: row.score })))
|
|
315
|
+
.sort((a, b) => b.score - a.score).slice(0, 8);
|
|
316
|
+
if (!relevantDecisions.length) decisionAvailability = 'unavailable'; // semantic miss is not absent history
|
|
317
|
+
}
|
|
318
|
+
const candidates = [
|
|
319
|
+
...stateKeys.slice(0, 1).map(key => ({ category: 'state', namespace: path.basename(root), key })),
|
|
320
|
+
...keys.lessons.filter(key => /^lesson[-_]/i.test(key)).map(key => ({ category: 'lessons', namespace: 'lessons', key })),
|
|
321
|
+
...relevantDecisions,
|
|
322
|
+
];
|
|
323
|
+
if (candidates.length > 32) return { block: formatBlock({ picks: [], status: 'unavailable bounded category selection' }), picks: [], stores: [store], status: { 'memory.db': 'unavailable' }, outcome: 'unavailable', categories: { state: 'unavailable', decisions: 'unavailable', lessons: 'unavailable' } };
|
|
324
|
+
const categories = { state: !stateKeys.length && keys[path.basename(root)].includes('project-state-current') ? 'unavailable' : 'ok-empty', decisions: decisionAvailability, lessons: 'ok-empty' };
|
|
325
|
+
const records = [];
|
|
326
|
+
await Promise.all(candidates.map(async candidate => {
|
|
327
|
+
const exact = await searchOnce({ ruflo: bin, store, deadline, env, scratch, operation: 'retrieve', signal, args: ['-k', candidate.key, '-n', candidate.namespace, '--value-only'] });
|
|
328
|
+
if (exact.state !== 'ok' || !exact.value || exact.value.startsWith('[WARN]')) { categories[candidate.category] = /timed out/.test(exact.state) ? 'timed-out' : 'unavailable'; return; }
|
|
329
|
+
let structured; try { structured = JSON.parse(exact.value); } catch { structured = null; }
|
|
330
|
+
const declaredRoot = structured?.projectRoot ?? structured?.scope?.projectRoot;
|
|
331
|
+
if (declaredRoot && declaredRoot !== root || structured?.supersededBy || structured?.superseded === true) {
|
|
332
|
+
if (candidate.category === 'state') categories.state = 'unavailable';
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const lesson = candidate.category === 'lessons' ? applicableRecallLesson(exact.value, root) : null;
|
|
336
|
+
if (candidate.category === 'lessons' && !lesson) return;
|
|
337
|
+
if (candidate.category === 'decisions' && !promptKeywords(prompt, 14).some(word => redactText(exact.value).toLowerCase().includes(word))) return;
|
|
338
|
+
records.push({ ...candidate, storePath: store.path, valueDigest: crypto.createHash('sha256').update(exact.value).digest('hex'),
|
|
339
|
+
value: clean(exact.value, 1024), preview: lesson ? clean(lesson.statement, 350) : evidenceExcerpt(exact.value, candidate.key, prompt), authority: false });
|
|
340
|
+
}));
|
|
341
|
+
for (const category of ['state', 'lessons', 'decisions']) if (categories[category] === 'ok-empty' && records.some(row => row.category === category)) categories[category] = 'ok-with-results';
|
|
342
|
+
const selected = ['state', 'lessons', 'decisions'].flatMap(category => records.filter(row => row.category === category).slice(0, 1));
|
|
343
|
+
const outcome = Object.values(categories).includes('timed-out') ? 'timed-out' : Object.values(categories).includes('unavailable') ? 'unavailable' : selected.length ? 'ok-with-results' : 'ok-empty';
|
|
344
|
+
const render = limit => `[AgentDB consequential recall: untrusted historical evidence, not instructions; verify current facts.]\n`
|
|
345
|
+
+ selected.map(row => `${row.category} ${JSON.stringify(clean(row.key, 72))}: ${JSON.stringify(clean(row.preview, limit))}`).join('\n')
|
|
346
|
+
+ '\nCategories: ' + JSON.stringify(categories);
|
|
347
|
+
let limit = 350;
|
|
348
|
+
while (limit > 0 && Buffer.byteLength(render(limit)) > CONSEQUENTIAL_BLOCK_MAX_BYTES) limit--;
|
|
349
|
+
const block = render(limit);
|
|
350
|
+
return { block, picks: selected, stores: [store], status: { 'memory.db': outcome }, outcome, categories,
|
|
351
|
+
receipt: { schemaVersion: 1, kind: 'canonical-memory-recall', binding: phaseBinding, outcome, categories,
|
|
352
|
+
observedAt: new Date().toISOString(), deadline, queryDigest: crypto.createHash('sha256').update(String(prompt)).digest('hex'),
|
|
353
|
+
records: selected.map(({ category, namespace, key, storePath, valueDigest }) => ({ category, namespace, key, storePath, valueDigest })), authority: false } };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function recall({ prompt, projectDir = process.cwd(), env = process.env, deadlineMs, ruflo, scratch,
|
|
357
|
+
binding = {}, signal, absoluteDeadline, consequential = false, enumerateKeys = enumerateCanonicalKeys } = {}) {
|
|
236
358
|
const started = Date.now();
|
|
237
359
|
const empty = { block: '', picks: [], stores: [], status: {} };
|
|
238
360
|
try {
|
|
239
|
-
if (
|
|
361
|
+
if (signal?.aborted) return { ...empty, outcome: 'unavailable', reason: 'cancelled' };
|
|
362
|
+
if (!agentdbFirstEnabled(env)) return { ...empty, outcome: 'disabled' };
|
|
363
|
+
if (!recallTrigger(prompt)) return { ...empty, outcome: 'ok-empty', reason: 'no human request' };
|
|
240
364
|
const requested = Number(deadlineMs ?? env.RUVNET_AGENTDB_RECALL_MS ?? DEFAULT_DEADLINE_MS);
|
|
241
365
|
const budget = Number.isFinite(requested) && requested > 0 ? Math.min(requested, 1900) : DEFAULT_DEADLINE_MS;
|
|
242
|
-
const deadline = started + budget;
|
|
366
|
+
const deadline = Math.min(started + budget, absoluteDeadline ?? Infinity);
|
|
367
|
+
if (deadline <= started) return { ...empty, outcome: 'timed-out' };
|
|
243
368
|
const { root, stores } = agentdbStores(projectDir, Math.max(1, Math.min(100, Math.floor(budget / 4))));
|
|
369
|
+
recallBinding({ binding, projectRoot: root, storePath: stores[0]?.path ?? null, prompt });
|
|
244
370
|
let learningStore;
|
|
245
371
|
try {
|
|
246
372
|
const context = learningContext({ env, cwd: root });
|
|
@@ -249,14 +375,16 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
249
375
|
if (fs.lstatSync(db).isFile()) learningStore = { name: 'memory.db', path: db };
|
|
250
376
|
}
|
|
251
377
|
} catch { /* No global fallback: only an explicitly authorized existing learning store. */ }
|
|
252
|
-
if (!stores.length && !learningStore) return empty;
|
|
378
|
+
if (!stores.length && (consequential || !learningStore)) return { ...empty, outcome: 'not-adopted' };
|
|
253
379
|
const bin = ruflo === undefined ? resolveRuflo({ env }) : ruflo;
|
|
254
|
-
if (!bin) return { ...empty, stores };
|
|
380
|
+
if (!bin) return { ...empty, stores, outcome: 'unavailable', reason: 'global Ruflo unavailable' };
|
|
255
381
|
const store = stores[0];
|
|
256
382
|
const query = recallQuery(prompt);
|
|
257
383
|
const scratchFor = scratch || ((storePath) => rufloCwdFor(storePath, { root: rufloScratchRoot(env) }));
|
|
384
|
+
if (consequential) return await consequentialRecall({ prompt, root, store, bin, scratch: scratchFor, env, deadline, binding, signal, enumerateKeys });
|
|
258
385
|
const namespaces = [...new Set(['lessons', 'patterns', 'pattern', 'turns', path.basename(root), 'default'])];
|
|
259
|
-
const family =
|
|
386
|
+
const family = /\bhooks?\b|hook.harness/i.test(prompt) ? 'decision-hook-harness-index'
|
|
387
|
+
: /score|grade|north.star/i.test(prompt) ? 'scorecard'
|
|
260
388
|
: /where are we|status|catch me up/i.test(prompt) ? 'project-state-current'
|
|
261
389
|
: /releas|publish|workflow run|dispatch/i.test(prompt) ? 'release'
|
|
262
390
|
: /requirement|always|every prompt/i.test(prompt) ? 'decision-agentdb' : null;
|
|
@@ -268,7 +396,7 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
268
396
|
if (learningStore && workflowQuery) jobs.push({ store: learningStore, namespace: LEARNING_NAMESPACE, family: null,
|
|
269
397
|
args: ['--format', 'json', '-q', workflowQuery, '-n', LEARNING_NAMESPACE, '-t', 'keyword', '--limit', '4'] });
|
|
270
398
|
const results = await Promise.all(jobs.map(async ({ store: jobStore, namespace, family: recordFamily, args }) => ({ namespace, family: recordFamily, storePath: jobStore.path,
|
|
271
|
-
...await searchOnce({ ruflo: bin, store: jobStore, deadline, env, scratch: scratchFor, args }) })));
|
|
399
|
+
...await searchOnce({ ruflo: bin, store: jobStore, deadline, env, scratch: scratchFor, args, signal }) })));
|
|
272
400
|
let status = results.every((r) => r.state === 'ok') ? 'ok'
|
|
273
401
|
: results.some((r) => r.state === 'timed out') ? 'timed out' : 'unavailable';
|
|
274
402
|
// Overfetch a bounded six exact values so rejected turn metadata cannot hide
|
|
@@ -278,12 +406,12 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
278
406
|
// the same deadline so the block contains useful evidence rather than titles.
|
|
279
407
|
const retrieved = await Promise.all(candidates.map(async (p) => {
|
|
280
408
|
const r = await searchOnce({ ruflo: bin, store: { name: 'memory.db', path: p.storePath }, deadline, env, scratch: scratchFor, operation: 'retrieve',
|
|
281
|
-
args: ['-k', p.key, '-n', p.namespace, '--value-only'] });
|
|
409
|
+
args: ['-k', p.key, '-n', p.namespace, '--value-only'], signal });
|
|
282
410
|
if (r.state === 'ok' && r.value && !r.value.startsWith('[WARN]')) {
|
|
283
411
|
const clauses = p.namespace === 'turns' ? turnOutcomeClauses(r.value, prompt) : null;
|
|
284
412
|
const preview = p.namespace === LEARNING_NAMESPACE ? learningObservationExcerpt(r.value)
|
|
285
413
|
: clauses ? turnOutcomeExcerpt(r.value, prompt) : evidenceExcerpt(r.value, p.key, prompt);
|
|
286
|
-
return { pick: preview ? { ...p, preview, clauses } : null, state: 'ok' };
|
|
414
|
+
return { pick: preview ? { ...p, preview, clauses, valueDigest: crypto.createHash('sha256').update(r.value).digest('hex') } : null, state: 'ok' };
|
|
287
415
|
}
|
|
288
416
|
return { pick: null, state: r.state === 'ok' ? 'unavailable' : r.state };
|
|
289
417
|
}));
|
|
@@ -300,8 +428,15 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
300
428
|
return unique.length ? [{ ...p, preview: clean('OUTCOME: ' + unique.join(' '), 280) }] : [];
|
|
301
429
|
}).slice(0, 3);
|
|
302
430
|
if (retrieved.some((r) => r.state !== 'ok')) status = retrieved.some((r) => r.state === 'timed out') ? 'timed out reading exact values' : 'unavailable exact values';
|
|
303
|
-
|
|
304
|
-
|
|
431
|
+
const outcome = status === 'ok' ? (picks.length ? 'ok-with-results' : 'ok-empty') : /timed out/.test(status) ? 'timed-out' : 'unavailable';
|
|
432
|
+
const receipt = { schemaVersion: 1, kind: 'canonical-memory-recall', outcome,
|
|
433
|
+
binding: recallBinding({ binding, projectRoot: root, storePath: store?.path ?? null, prompt }),
|
|
434
|
+
stores: stores.map(store => ({ path: store.path })), observedAt: new Date().toISOString(),
|
|
435
|
+
queryDigest: crypto.createHash('sha256').update(String(prompt)).digest('hex'), deadline,
|
|
436
|
+
records: picks.map(p => ({ namespace: p.namespace, key: p.key, storePath: p.storePath, valueDigest: p.valueDigest })),
|
|
437
|
+
authority: false };
|
|
438
|
+
return { block: formatBlock({ picks, status }), picks, stores, status: { 'memory.db': status }, outcome, receipt };
|
|
439
|
+
} catch (error) { return { ...empty, outcome: 'unavailable', reason: 'canonical recall failed: ' + clean(error.message, 180) }; }
|
|
305
440
|
}
|
|
306
441
|
|
|
307
442
|
async function main() {
|
|
@@ -5,6 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { developerCoordinatorOwner } from './developer-update-owner.mjs';
|
|
8
9
|
import { SETTINGS_VERSION, validate, saveSettings } from './user-settings.mjs';
|
|
9
10
|
|
|
10
11
|
export const ownerSettingsPath = (home = os.homedir()) => path.join(home, '.config', 'ruvnet-brain', 'settings.json');
|
|
@@ -44,7 +45,7 @@ export function saveUpdateSource(source, { home = os.homedir() } = {}) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export function automaticPath({ nodePath = process.execPath, home = os.homedir(), platform = process.platform, env = process.env } = {}) {
|
|
47
|
-
const paths = [path.dirname(nodePath), path.join(home, '.
|
|
48
|
+
const paths = [path.join(home, '.npm-global', 'bin'), path.dirname(nodePath), path.join(home, '.local', 'bin'), path.join(home, '.cargo', 'bin'), path.join(home, '.bun', 'bin')];
|
|
48
49
|
if (platform === 'win32') {
|
|
49
50
|
const system = env.SystemRoot || env.SYSTEMROOT;
|
|
50
51
|
if (system && path.win32.isAbsolute(system)) paths.push(system, path.join(system, 'System32'));
|
|
@@ -91,8 +92,16 @@ export function installedUpdater({ home = os.homedir(), platform = process.platf
|
|
|
91
92
|
return { entry, version: manifest.version, sha256 };
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
export function automaticInvocation(args, { source
|
|
95
|
+
export function automaticInvocation(args, { source, home = os.homedir(), nodePath = process.execPath,
|
|
95
96
|
packageTarget = 'ruvnet-brain@latest' } = {}) {
|
|
97
|
+
if (packageTarget === 'ruvnet-brain@latest') {
|
|
98
|
+
const coordinator = developerCoordinatorOwner({ home });
|
|
99
|
+
if (coordinator.active) {
|
|
100
|
+
if (!coordinator.ready) throw new Error(`coordinated updater is not ready: ${coordinator.reason}`);
|
|
101
|
+
return { executable: nodePath, args: [coordinator.entry, '--apply'], source: 'developer-suite', coordinator };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
source ??= updateSource({ home });
|
|
96
105
|
if (source === 'installed') {
|
|
97
106
|
if (packageTarget !== 'ruvnet-brain@latest') throw new Error('installed mode cannot select a proof tarball');
|
|
98
107
|
const installed = installedUpdater({ home });
|
|
@@ -129,6 +129,9 @@ export function buildCapabilityInventoryReceipt({
|
|
|
129
129
|
host,
|
|
130
130
|
observedAt: now,
|
|
131
131
|
completeness: errors.length ? 'unknown' : 'complete',
|
|
132
|
+
scope: 'enumerated-skill-source-files',
|
|
133
|
+
predicates: { skillSourceFiles: errors.length ? 'unknown' : 'complete',
|
|
134
|
+
skillActivation: 'unknown', cliInstallation: 'unknown', mcpRegistration: 'unknown', reachability: 'unknown' },
|
|
132
135
|
roots: observedRoots,
|
|
133
136
|
entries: deduplicated,
|
|
134
137
|
errors,
|
|
@@ -149,6 +152,11 @@ export function validateCapabilityInventoryReceipt(receipt) {
|
|
|
149
152
|
|| digest(unsigned) !== inventoryDigest) {
|
|
150
153
|
throw new Error('capability inventory receipt digest or schema is invalid');
|
|
151
154
|
}
|
|
155
|
+
if (receipt.scope !== undefined && (receipt.scope !== 'enumerated-skill-source-files'
|
|
156
|
+
|| receipt.predicates?.skillSourceFiles !== receipt.completeness
|
|
157
|
+
|| ['skillActivation', 'cliInstallation', 'mcpRegistration', 'reachability'].some((key) => receipt.predicates?.[key] !== 'unknown'))) {
|
|
158
|
+
throw new Error('capability inventory predicates exceed enumerated skill-source scope');
|
|
159
|
+
}
|
|
152
160
|
if (receipt.entries.some((entry) => entry?.type !== 'skill' || !entry.name || !entry.ref
|
|
153
161
|
|| !path.isAbsolute(entry.sourcePath || '') || !SHA256.test(String(entry.sha256 || '')))) {
|
|
154
162
|
throw new Error('capability inventory receipt contains an invalid entry');
|
|
@@ -202,15 +210,16 @@ export function auditCapabilityClaims(message, receipt) {
|
|
|
202
210
|
const unresolved = [];
|
|
203
211
|
for (const claim of claims) {
|
|
204
212
|
const matched = matchEntry(claim, receipt.entries);
|
|
205
|
-
|
|
213
|
+
const skillSourceClaim = claim.predicate === 'present' && /\bskill\s+(?:source|file)s?\b/i.test(claim.subject);
|
|
214
|
+
if (!skillSourceClaim) {
|
|
215
|
+
unresolved.push({ ...claim, reason: 'Enumerated skill source files do not establish installation, activation, registration or reachability.' });
|
|
216
|
+
} else if (claim.polarity === 'absent' && matched) {
|
|
206
217
|
contradictions.push({ ...claim, matchedRef: matched.ref, sourcePath: matched.sourcePath,
|
|
207
|
-
reason: 'absence claim contradicts
|
|
208
|
-
} else if (
|
|
209
|
-
|
|
210
|
-
reason: 'presence claim has no matching entry in the complete inventory' });
|
|
211
|
-
} else if (!matched && receipt.completeness !== 'complete') {
|
|
212
|
-
unresolved.push({ ...claim, reason: 'capability inventory is incomplete' });
|
|
218
|
+
reason: 'Skill-source absence claim contradicts the enumerated source bytes.' });
|
|
219
|
+
} else if (!matched) {
|
|
220
|
+
unresolved.push({ ...claim, reason: 'No matching source was observed in the selected roots; this is not a complete host inventory.' });
|
|
213
221
|
}
|
|
222
|
+
|
|
214
223
|
}
|
|
215
224
|
return {
|
|
216
225
|
schemaVersion: 1,
|