ruvnet-brain 4.3.9 → 4.3.10
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 +26 -9
- package/bin/install.mjs +646 -366
- package/bin/nightly-refresh.mjs +115 -0
- package/kb/brain-profile.mjs +118 -32
- package/kb/lifecycle-evidence-retention.mjs +237 -0
- package/kb/refresh-run.mjs +367 -0
- package/kb/retrieval-result.mjs +39 -0
- package/kb/update-storage-transaction.mjs +439 -0
- package/package.json +11 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +2 -165
- package/plugin/hooks/hook-contracts.json +4 -65
- package/plugin/hooks/hooks.json +2 -208
- package/plugin/scripts/adr-currency-gate.mjs +29 -19
- package/plugin/scripts/capability-registry.mjs +10 -83
- package/plugin/scripts/codex-hook-adapter.mjs +62 -9
- package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
- package/plugin/scripts/continuation-gate.mjs +30 -28
- package/plugin/scripts/continuation-objective.mjs +34 -0
- package/plugin/scripts/development-maintenance.mjs +49 -0
- package/plugin/scripts/hook-shim.mjs +3 -0
- package/plugin/scripts/learn-flush.mjs +6 -1
- package/plugin/scripts/lesson-gate.mjs +5 -2
- package/plugin/scripts/lesson-presentation.mjs +5 -0
- package/plugin/scripts/md-stamp.mjs +109 -6
- package/plugin/scripts/memory-doctor.mjs +3 -7
- package/plugin/scripts/nightly-controller.mjs +9 -27
- package/plugin/scripts/nightly-scheduler.mjs +472 -0
- package/plugin/scripts/project-progression-contract.mjs +4 -4
- package/plugin/scripts/project-progression-store.mjs +35 -6
- package/plugin/scripts/ruflo-bin.mjs +20 -0
- package/plugin/scripts/session-snapshot-contract.mjs +6 -1
- package/plugin/scripts/version-bump-gate.sh +14 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
- package/scripts/build-bundle.mjs +8 -0
- package/scripts/build-primer.mjs +2 -3
- package/scripts/candidate-host-evidence.mjs +59 -41
- package/scripts/ci/build-fixture-kb.mjs +59 -2
- package/scripts/ci/mutate-hook-timeout.mjs +24 -47
- package/scripts/ci/stranger-scenario.mjs +13 -28
- package/scripts/claims-verify.mjs +219 -21
- package/scripts/console-engine.mjs +4 -18
- package/scripts/console-runtime-identity.mjs +6 -0
- package/scripts/development-maintenance.mjs +47 -0
- package/scripts/development-push-check.mjs +35 -0
- package/scripts/distill-project.mjs +24 -47
- package/scripts/doc-currency.mjs +91 -19
- package/scripts/git-hooks/pre-push +4 -162
- package/scripts/health-repair.mjs +22 -4
- package/scripts/hook-retirement-check.mjs +22 -0
- package/scripts/host-install-matrix.mjs +125 -61
- package/scripts/integration-evidence.mjs +11 -6
- package/scripts/learning-replay-fixture.mjs +23 -2
- package/scripts/nightly-two-run-proof.mjs +415 -0
- package/scripts/nightly-watchdog.mjs +17 -2
- package/scripts/npm-invocation.mjs +20 -0
- package/scripts/prepublication-evidence.mjs +42 -6
- package/scripts/primer-grounding.mjs +20 -0
- package/scripts/product-integrity-contract.mjs +22 -2
- package/scripts/public-verification-abandon.mjs +101 -0
- package/scripts/public-verification-aggregate.mjs +53 -19
- package/scripts/public-verification-finalizer.mjs +8 -2
- package/scripts/public-verification-lane.mjs +63 -8
- package/scripts/publication-receipt.mjs +109 -45
- package/scripts/published-surface-probe.mjs +10 -2
- package/scripts/qa-contract.mjs +57 -0
- package/scripts/qa-lanes.mjs +40 -0
- package/scripts/qa-runner.mjs +54 -64
- package/scripts/qe/ux-suite.mjs +10 -34
- package/scripts/qualified-candidate-check.mjs +124 -0
- package/scripts/release-abort-stale.mjs +2 -2
- package/scripts/release-projection.mjs +20 -1
- package/scripts/release-qualification-contract.mjs +87 -0
- package/scripts/release-qualification.mjs +135 -0
- package/scripts/release-transaction.mjs +81 -1
- package/scripts/remedy-registry.mjs +6 -13
- package/scripts/retrieval-canary.mjs +91 -18
- package/scripts/selfcheck.mjs +12 -4
- package/scripts/snapshot-freshness.mjs +58 -0
- package/scripts/stack-sync.mjs +41 -43
- package/scripts/staged-host-verifier.mjs +63 -14
- package/scripts/wired-check.mjs +31 -2
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const SAFE_ID = /^[A-Za-z0-9._-]+$/;
|
|
8
|
+
export const REFRESH_PHASE_CONTRACT = Object.freeze({
|
|
9
|
+
schemaVersion: 1,
|
|
10
|
+
update: Object.freeze([
|
|
11
|
+
'source-enumeration',
|
|
12
|
+
'ingestion',
|
|
13
|
+
'local-overlay-restoration',
|
|
14
|
+
'generation-ledger-reconciliation',
|
|
15
|
+
'coverage-generation',
|
|
16
|
+
'bundle-assembly',
|
|
17
|
+
'update',
|
|
18
|
+
]),
|
|
19
|
+
hostConvergence: 'host-convergence',
|
|
20
|
+
cleanup: 'cleanup',
|
|
21
|
+
});
|
|
22
|
+
export const UPDATE_REFRESH_PHASES = REFRESH_PHASE_CONTRACT.update;
|
|
23
|
+
export const REQUIRED_REFRESH_PHASES = Object.freeze([
|
|
24
|
+
...UPDATE_REFRESH_PHASES,
|
|
25
|
+
REFRESH_PHASE_CONTRACT.hostConvergence,
|
|
26
|
+
REFRESH_PHASE_CONTRACT.cleanup,
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const alive = (pid) => {
|
|
30
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function observedProcessIdentity(pid, platform = process.platform) {
|
|
34
|
+
if (!Number.isSafeInteger(pid) || pid < 1) return { state: 'unknown' };
|
|
35
|
+
if (!alive(pid)) return { state: 'dead' };
|
|
36
|
+
try {
|
|
37
|
+
if (platform === 'win32') {
|
|
38
|
+
const result = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command',
|
|
39
|
+
`$p=Get-Process -Id ${pid} -ErrorAction Stop; `
|
|
40
|
+
+ `Write-Output ($p.StartTime.ToUniversalTime().ToString('o') + '|' + $p.Path)`], { encoding: 'utf8' });
|
|
41
|
+
if (result.status !== 0) return { state: 'unknown' };
|
|
42
|
+
const [processStart, ...rest] = String(result.stdout || '').trim().split('|');
|
|
43
|
+
return processStart && rest.length ? { state: 'live', processStart, executable: rest.join('|') } : { state: 'unknown' };
|
|
44
|
+
}
|
|
45
|
+
const started = spawnSync('ps', ['-p', String(pid), '-o', 'lstart='], {
|
|
46
|
+
encoding: 'utf8', env: { ...process.env, TZ: 'UTC', LC_ALL: 'C', LANG: 'C' },
|
|
47
|
+
});
|
|
48
|
+
const command = spawnSync('ps', ['-p', String(pid), '-o', 'comm='], { encoding: 'utf8' });
|
|
49
|
+
const processStart = String(started.stdout || '').trim();
|
|
50
|
+
const executable = String(command.stdout || '').trim();
|
|
51
|
+
return started.status === 0 && command.status === 0 && processStart && executable
|
|
52
|
+
? { state: 'live', processStart, executable }
|
|
53
|
+
: { state: 'unknown' };
|
|
54
|
+
} catch { return { state: 'unknown' }; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sameExecutable(left, right) {
|
|
58
|
+
try { return fs.realpathSync(left) === fs.realpathSync(right); } catch { return path.resolve(left || '') === path.resolve(right || ''); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ownerState(owner, { hostname, inspectProcess, isAlive }) {
|
|
62
|
+
if (!owner || owner.host !== hostname || !owner.processStart || !owner.executable) return 'unknown';
|
|
63
|
+
const observed = inspectProcess
|
|
64
|
+
? inspectProcess(owner.pid)
|
|
65
|
+
: (isAlive && isAlive !== alive)
|
|
66
|
+
? (isAlive(owner.pid)
|
|
67
|
+
? { state: 'live', processStart: owner.processStart, executable: owner.executable }
|
|
68
|
+
: { state: 'dead' })
|
|
69
|
+
: observedProcessIdentity(owner.pid);
|
|
70
|
+
if (observed?.state !== 'live') return observed?.state === 'dead' ? 'dead' : 'unknown';
|
|
71
|
+
return observed.processStart === owner.processStart && sameExecutable(observed.executable, owner.executable)
|
|
72
|
+
? 'live'
|
|
73
|
+
: 'dead';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function inspectRefreshOwner(owner, { hostname = os.hostname(), inspectProcess = null,
|
|
77
|
+
isAlive = alive } = {}) {
|
|
78
|
+
return ownerState(owner, { hostname, inspectProcess, isAlive });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const refreshLockPath = (kbDir) =>
|
|
82
|
+
path.join(path.dirname(path.resolve(kbDir)), `.${path.basename(path.resolve(kbDir))}.refresh-run.lock`);
|
|
83
|
+
|
|
84
|
+
export function refreshReceiptPath(brainHome, runId) {
|
|
85
|
+
if (!SAFE_ID.test(String(runId || ''))) throw new Error('refresh run has no safe runId');
|
|
86
|
+
const root = path.resolve(brainHome || '');
|
|
87
|
+
const file = path.resolve(root, 'refresh-runs', `${runId}.json`);
|
|
88
|
+
const relative = path.relative(root, file);
|
|
89
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('refresh receipt path escapes brain home');
|
|
90
|
+
return file;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function abandonDeadOwner({ lockPath, owner, hostname, inspectProcess, isAlive, now }) {
|
|
94
|
+
const claimFile = path.join(lockPath, 'takeover.json');
|
|
95
|
+
const claim = { schemaVersion: 1, token: crypto.randomBytes(24).toString('hex'), host: hostname,
|
|
96
|
+
pid: process.pid, claimedAt: now() };
|
|
97
|
+
const fd = fs.openSync(claimFile, 'wx', 0o600);
|
|
98
|
+
try { fs.writeFileSync(fd, `${JSON.stringify(claim, null, 2)}\n`); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
99
|
+
const observed = JSON.parse(fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8'));
|
|
100
|
+
if (observed.schemaVersion !== 3 || observed.runId !== owner.runId || observed.token !== owner.token
|
|
101
|
+
|| ownerState(observed, { hostname, inspectProcess, isAlive }) !== 'dead') {
|
|
102
|
+
throw new Error('refresh owner changed or is no longer exactly dead during takeover');
|
|
103
|
+
}
|
|
104
|
+
const expectedReceipt = refreshReceiptPath(observed.brainHome, observed.runId);
|
|
105
|
+
if (path.resolve(observed.receiptPath || '') !== expectedReceipt) throw new Error('dead refresh owner receipt path is invalid');
|
|
106
|
+
fs.mkdirSync(path.dirname(expectedReceipt), { recursive: true });
|
|
107
|
+
let receipt;
|
|
108
|
+
if (fs.existsSync(expectedReceipt)) {
|
|
109
|
+
const stat = fs.lstatSync(expectedReceipt);
|
|
110
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('dead refresh owner receipt is not a regular file');
|
|
111
|
+
receipt = JSON.parse(fs.readFileSync(expectedReceipt, 'utf8'));
|
|
112
|
+
if (receipt.runId !== observed.runId || receipt.ownerToken?.token !== observed.token) {
|
|
113
|
+
throw new Error('dead refresh owner receipt identity differs');
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
receipt = { schemaVersion: 3, kind: 'ruvnet-brain-refresh-run', runId: observed.runId,
|
|
117
|
+
...observed.receiptSeed, ownerToken: { token: observed.token, host: observed.host, pid: observed.pid,
|
|
118
|
+
executable: observed.executable, processStart: observed.processStart, startedAt: observed.startedAt },
|
|
119
|
+
startedAt: observed.startedAt, status: 'RUNNING', terminalVerdict: null,
|
|
120
|
+
requiredPhaseOrder: [...REQUIRED_REFRESH_PHASES], phases: [], advisories: [] };
|
|
121
|
+
}
|
|
122
|
+
if (['RUNNING', 'SETTLING'].includes(receipt.status)) {
|
|
123
|
+
receipt.status = 'ABANDONED';
|
|
124
|
+
receipt.terminalVerdict = 'abandoned';
|
|
125
|
+
receipt.finishedAt = now();
|
|
126
|
+
receipt.detail = { reason: 'exact-dead-owner', abandonedOwner: { host: observed.host, pid: observed.pid,
|
|
127
|
+
processStart: observed.processStart, executable: observed.executable }, takeoverToken: claim.token };
|
|
128
|
+
delete receipt.intendedStatus;
|
|
129
|
+
delete receipt.intendedTerminalVerdict;
|
|
130
|
+
atomicJson(expectedReceipt, receipt);
|
|
131
|
+
} else if (!['SUCCEEDED', 'FAILED', 'ABANDONED'].includes(receipt.status)) {
|
|
132
|
+
throw new Error('dead refresh owner receipt has an unsupported state');
|
|
133
|
+
}
|
|
134
|
+
return claim;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function acquireRefreshLock({
|
|
138
|
+
kbDir,
|
|
139
|
+
brainHome = path.dirname(path.resolve(kbDir || '')),
|
|
140
|
+
action = 'update',
|
|
141
|
+
desiredVersion = null,
|
|
142
|
+
schedulerIdentity = null,
|
|
143
|
+
executableIdentity = null,
|
|
144
|
+
env = process.env,
|
|
145
|
+
pid = process.pid,
|
|
146
|
+
isAlive = alive,
|
|
147
|
+
now = () => new Date().toISOString(),
|
|
148
|
+
hostname = os.hostname(),
|
|
149
|
+
executable = process.execPath,
|
|
150
|
+
processStart = null,
|
|
151
|
+
inspectProcess = null,
|
|
152
|
+
afterStage = null,
|
|
153
|
+
} = {}) {
|
|
154
|
+
const root = path.resolve(kbDir || '');
|
|
155
|
+
const lockPath = refreshLockPath(root);
|
|
156
|
+
const inherited = String(env.RUVNET_REFRESH_RUN_TOKEN || '');
|
|
157
|
+
if (inherited) {
|
|
158
|
+
const owner = JSON.parse(fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8'));
|
|
159
|
+
if (owner.schemaVersion !== 3 || owner.token !== inherited || path.resolve(owner.kbDir) !== root
|
|
160
|
+
|| path.resolve(owner.receiptPath || '') !== refreshReceiptPath(owner.brainHome, owner.runId)) {
|
|
161
|
+
throw new Error(`refresh lock inheritance does not match ${lockPath}`);
|
|
162
|
+
}
|
|
163
|
+
return { path: lockPath, token: inherited, runId: owner.runId, owned: false, owner };
|
|
164
|
+
}
|
|
165
|
+
const current = processStart
|
|
166
|
+
? { state: 'live', processStart, executable }
|
|
167
|
+
: (isAlive !== alive ? { state: 'live', processStart: `injected:${pid}`, executable } : observedProcessIdentity(pid));
|
|
168
|
+
if (current.state !== 'live' || !current.processStart || !current.executable) {
|
|
169
|
+
throw new Error(`cannot establish exact process identity for refresh owner pid ${pid}`);
|
|
170
|
+
}
|
|
171
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
172
|
+
const staged = `${lockPath}.acquire-${pid}-${crypto.randomBytes(8).toString('hex')}`;
|
|
173
|
+
try {
|
|
174
|
+
const token = crypto.randomBytes(24).toString('hex');
|
|
175
|
+
const runId = `${Date.now()}-${crypto.randomBytes(8).toString('hex')}`;
|
|
176
|
+
const startedAt = now();
|
|
177
|
+
const owner = { schemaVersion: 3, runId, pid, token, kbDir: root, brainHome: path.resolve(brainHome),
|
|
178
|
+
receiptPath: refreshReceiptPath(brainHome, runId), host: hostname,
|
|
179
|
+
executable: current.executable, processStart: current.processStart, startedAt,
|
|
180
|
+
receiptSeed: { action, desiredVersion, schedulerIdentity, executableIdentity } };
|
|
181
|
+
fs.mkdirSync(staged, { mode: 0o700 });
|
|
182
|
+
fs.writeFileSync(path.join(staged, 'owner.json'), `${JSON.stringify(owner, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
183
|
+
afterStage?.({ staged, lockPath, runId, token });
|
|
184
|
+
fs.renameSync(staged, lockPath);
|
|
185
|
+
return { path: lockPath, token, runId, owned: true, owner };
|
|
186
|
+
} catch (error) {
|
|
187
|
+
fs.rmSync(staged, { recursive: true, force: true });
|
|
188
|
+
// Windows refuses rename-over-existing with EPERM/EACCES rather than EEXIST. Treat those
|
|
189
|
+
// codes as contention, then inspect the owner through the same liveness fence.
|
|
190
|
+
if (!['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES'].includes(error?.code)) throw error;
|
|
191
|
+
let owner = null;
|
|
192
|
+
try { owner = JSON.parse(fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8')); } catch {
|
|
193
|
+
throw new Error(`refresh lock owner is incomplete or unreadable; refusing stale takeover (${lockPath})`);
|
|
194
|
+
}
|
|
195
|
+
const state = ownerState(owner, { hostname, inspectProcess, isAlive });
|
|
196
|
+
if (state !== 'dead') {
|
|
197
|
+
throw new Error(`${state === 'live' ? 'another refresh run is active' : 'refresh lock owner is ambiguous'} `
|
|
198
|
+
+ `(pid ${owner.pid || '?'}, lock ${lockPath})`);
|
|
199
|
+
}
|
|
200
|
+
abandonDeadOwner({ lockPath, owner, hostname, inspectProcess, isAlive, now });
|
|
201
|
+
const stale = `${lockPath}.stale-${pid}-${Date.now()}`;
|
|
202
|
+
try {
|
|
203
|
+
fs.renameSync(lockPath, stale);
|
|
204
|
+
fs.rmSync(stale, { recursive: true, force: true });
|
|
205
|
+
} catch (renameError) {
|
|
206
|
+
if (renameError?.code !== 'ENOENT') throw renameError;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
throw new Error(`could not acquire refresh lock ${lockPath}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function releaseRefreshLock(lock) {
|
|
214
|
+
if (!lock?.owned) return false;
|
|
215
|
+
const quarantined = `${lock.path}.release-${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
|
|
216
|
+
try {
|
|
217
|
+
fs.renameSync(lock.path, quarantined);
|
|
218
|
+
const owner = JSON.parse(fs.readFileSync(path.join(quarantined, 'owner.json'), 'utf8'));
|
|
219
|
+
if (owner.token !== lock.token || owner.runId !== lock.runId) {
|
|
220
|
+
if (!fs.existsSync(lock.path)) fs.renameSync(quarantined, lock.path);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
lock.afterQuarantine?.({ quarantined, lockPath: lock.path });
|
|
224
|
+
fs.rmSync(quarantined, { recursive: true, force: true });
|
|
225
|
+
return true;
|
|
226
|
+
} catch {
|
|
227
|
+
if (fs.existsSync(quarantined) && !fs.existsSync(lock.path)) {
|
|
228
|
+
try { fs.renameSync(quarantined, lock.path); } catch { /* retain quarantined evidence */ }
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function assertRefreshLockOwner(lock) {
|
|
235
|
+
if (!lock?.path || !lock?.token || !lock?.runId) throw new Error('refresh lock identity is incomplete');
|
|
236
|
+
const owner = JSON.parse(fs.readFileSync(path.join(lock.path, 'owner.json'), 'utf8'));
|
|
237
|
+
if (owner.token !== lock.token || owner.runId !== lock.runId) {
|
|
238
|
+
throw new Error('refresh lock owner changed during settlement');
|
|
239
|
+
}
|
|
240
|
+
return owner;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function atomicJson(file, value) {
|
|
244
|
+
const temporary = `${file}.tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
245
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
246
|
+
const fd = fs.openSync(temporary, 'wx', 0o600);
|
|
247
|
+
try { fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
248
|
+
fs.renameSync(temporary, file);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function openRefreshReceipt({ brainHome, lock, action, desiredVersion = null,
|
|
252
|
+
schedulerIdentity = null, executableIdentity = null, now = () => new Date().toISOString() }) {
|
|
253
|
+
if (!lock?.runId || !SAFE_ID.test(lock.runId)) throw new Error('refresh run has no safe runId');
|
|
254
|
+
const owner = assertRefreshLockOwner(lock);
|
|
255
|
+
const file = refreshReceiptPath(brainHome, lock.runId);
|
|
256
|
+
if (owner.schemaVersion !== 3 || path.resolve(owner.receiptPath || '') !== file
|
|
257
|
+
|| path.resolve(owner.brainHome || '') !== path.resolve(brainHome)) throw new Error('refresh owner receipt identity differs');
|
|
258
|
+
const receipt = { schemaVersion: 3, kind: 'ruvnet-brain-refresh-run', runId: lock.runId,
|
|
259
|
+
action, desiredVersion, schedulerIdentity, executableIdentity,
|
|
260
|
+
ownerToken: { token: lock.token, ...(lock.owner || {}) },
|
|
261
|
+
startedAt: now(), status: 'RUNNING', terminalVerdict: null,
|
|
262
|
+
requiredPhaseOrder: [...REQUIRED_REFRESH_PHASES], phases: [], advisories: [] };
|
|
263
|
+
atomicJson(file, receipt);
|
|
264
|
+
return { file, runId: lock.runId };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Consumer convergence permits validated upstream artifacts. The stronger native
|
|
268
|
+
// nine-phase proof must establish that every operation belongs to this run.
|
|
269
|
+
export function validateCurrentRunPhaseExecution(receipt) {
|
|
270
|
+
const failures = [];
|
|
271
|
+
const phases = Array.isArray(receipt?.phases) ? receipt.phases : [];
|
|
272
|
+
const start = Date.parse(receipt?.startedAt || '');
|
|
273
|
+
const end = Date.parse(receipt?.finishedAt || '');
|
|
274
|
+
if (!receipt?.runId || !Number.isFinite(start) || !Number.isFinite(end) || end < start) {
|
|
275
|
+
failures.push('current-run execution identity or time window is missing');
|
|
276
|
+
}
|
|
277
|
+
if (JSON.stringify(phases.map((phase) => phase?.phase)) !== JSON.stringify(REQUIRED_REFRESH_PHASES)) {
|
|
278
|
+
failures.push('current-run execution phase ledger is incomplete or out of order');
|
|
279
|
+
}
|
|
280
|
+
for (const phase of phases) {
|
|
281
|
+
const execution = phase?.evidence?.execution;
|
|
282
|
+
const at = Date.parse(phase?.at || '');
|
|
283
|
+
if (phase?.status !== 'PASS' || execution?.kind !== 'executed'
|
|
284
|
+
|| execution.runId !== receipt.runId || !Number.isFinite(at) || at < start || at > end) {
|
|
285
|
+
failures.push(`${phase?.phase || 'unknown phase'} does not establish current-run execution`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return { ok: failures.length === 0, failures };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function recordRefreshPhase(handle, phase, status, detail = null, now = () => new Date().toISOString()) {
|
|
292
|
+
if (!handle?.file || !SAFE_ID.test(String(phase)) || !['PASS', 'FAIL', 'SKIP'].includes(status)) {
|
|
293
|
+
throw new Error('invalid refresh phase receipt update');
|
|
294
|
+
}
|
|
295
|
+
const receipt = JSON.parse(fs.readFileSync(handle.file, 'utf8'));
|
|
296
|
+
if (receipt.runId !== handle.runId || receipt.status !== 'RUNNING') throw new Error('refresh receipt is not active');
|
|
297
|
+
const expected = REQUIRED_REFRESH_PHASES[receipt.phases.length];
|
|
298
|
+
if (phase !== expected) throw new Error(`refresh phase ${phase} is out of order; expected ${expected || '(none)'}`);
|
|
299
|
+
const required = detail?.required !== false;
|
|
300
|
+
const evidence = detail && Object.hasOwn(detail, 'required') ? { ...detail } : detail;
|
|
301
|
+
if (evidence && Object.hasOwn(evidence, 'required')) delete evidence.required;
|
|
302
|
+
receipt.phases.push({ phase, required, status, at: now(), evidence });
|
|
303
|
+
atomicJson(handle.file, receipt);
|
|
304
|
+
return receipt;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function recordRefreshAdvisory(handle, advisory, status, detail = null, now = () => new Date().toISOString()) {
|
|
308
|
+
if (!handle?.file || !SAFE_ID.test(String(advisory)) || !['PASS', 'FAIL', 'SKIP'].includes(status)) {
|
|
309
|
+
throw new Error('invalid refresh advisory receipt update');
|
|
310
|
+
}
|
|
311
|
+
const receipt = JSON.parse(fs.readFileSync(handle.file, 'utf8'));
|
|
312
|
+
if (receipt.runId !== handle.runId || receipt.status !== 'RUNNING') throw new Error('refresh receipt is not active');
|
|
313
|
+
if (receipt.advisories.some((entry) => entry.advisory === advisory)) throw new Error(`refresh advisory is duplicated: ${advisory}`);
|
|
314
|
+
receipt.advisories.push({ advisory, status, at: now(), evidence: detail });
|
|
315
|
+
atomicJson(handle.file, receipt);
|
|
316
|
+
return receipt;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function beginRefreshSettlement(handle, lock, status, detail = null, now = () => new Date().toISOString()) {
|
|
320
|
+
if (!['SUCCEEDED', 'FAILED', 'ABANDONED'].includes(status)) throw new Error('invalid terminal refresh status');
|
|
321
|
+
assertRefreshLockOwner(lock);
|
|
322
|
+
const receipt = JSON.parse(fs.readFileSync(handle.file, 'utf8'));
|
|
323
|
+
if (receipt.runId !== handle.runId || receipt.status !== 'RUNNING') throw new Error('refresh receipt is not active');
|
|
324
|
+
const requiredFailure = receipt.phases.find((phase) => phase.required !== false && phase.status !== 'PASS');
|
|
325
|
+
const exactOrder = receipt.phases.map(({ phase }) => phase);
|
|
326
|
+
if (status === 'SUCCEEDED' && requiredFailure) {
|
|
327
|
+
throw new Error(`required refresh phase ${requiredFailure.phase} is ${requiredFailure.status}; refusing success`);
|
|
328
|
+
}
|
|
329
|
+
if (status === 'SUCCEEDED' && JSON.stringify(exactOrder) !== JSON.stringify(REQUIRED_REFRESH_PHASES)) {
|
|
330
|
+
throw new Error('required refresh phase ledger is incomplete or out of order; refusing success');
|
|
331
|
+
}
|
|
332
|
+
receipt.status = 'SETTLING';
|
|
333
|
+
receipt.settlingAt = now();
|
|
334
|
+
receipt.intendedStatus = status;
|
|
335
|
+
receipt.intendedTerminalVerdict = status === 'SUCCEEDED'
|
|
336
|
+
? (detail?.terminalVerdict === 'noop' ? 'noop' : 'applied')
|
|
337
|
+
: status === 'ABANDONED' ? 'abandoned'
|
|
338
|
+
: (['cleanup-pending', 'recovery-required'].includes(detail?.terminalVerdict)
|
|
339
|
+
? detail.terminalVerdict : 'failed');
|
|
340
|
+
receipt.detail = detail;
|
|
341
|
+
atomicJson(handle.file, receipt);
|
|
342
|
+
return receipt;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function finishRefreshReceipt(handle, lock, now = () => new Date().toISOString()) {
|
|
346
|
+
assertRefreshLockOwner(lock);
|
|
347
|
+
const receipt = JSON.parse(fs.readFileSync(handle.file, 'utf8'));
|
|
348
|
+
if (receipt.runId !== handle.runId || receipt.status !== 'SETTLING'
|
|
349
|
+
|| !['SUCCEEDED', 'FAILED', 'ABANDONED'].includes(receipt.intendedStatus)) {
|
|
350
|
+
throw new Error('refresh receipt is not settling');
|
|
351
|
+
}
|
|
352
|
+
receipt.status = receipt.intendedStatus;
|
|
353
|
+
receipt.terminalVerdict = receipt.intendedTerminalVerdict;
|
|
354
|
+
receipt.finishedAt = now();
|
|
355
|
+
delete receipt.intendedStatus;
|
|
356
|
+
delete receipt.intendedTerminalVerdict;
|
|
357
|
+
atomicJson(handle.file, receipt);
|
|
358
|
+
return receipt;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function settleRefreshRun({ handle, lock, status, detail = null, now = () => new Date().toISOString() }) {
|
|
362
|
+
beginRefreshSettlement(handle, lock, status, detail, now);
|
|
363
|
+
const receipt = finishRefreshReceipt(handle, lock, now);
|
|
364
|
+
const lockReleased = releaseRefreshLock(lock);
|
|
365
|
+
if (!lockReleased) throw new Error('refresh terminal receipt committed but lock release could not be verified');
|
|
366
|
+
return { receipt, lockReleased };
|
|
367
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const sha256 = (text) => crypto.createHash('sha256').update(text).digest('hex');
|
|
5
|
+
const safePath = (value) => typeof value === 'string' && value.length > 0
|
|
6
|
+
&& !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value)
|
|
7
|
+
&& !value.split(/[\\/]/).includes('..');
|
|
8
|
+
|
|
9
|
+
// Independent of grounding receipts (which filter and cap sources). This is the actual
|
|
10
|
+
// ordered retrieval result, captured before any human-readable document rendering.
|
|
11
|
+
export function buildRetrievalResult({ query, k, results }) {
|
|
12
|
+
const retrieval = { schemaVersion: 1, kind: 'ruvnet-brain.retrieval-result', query, k,
|
|
13
|
+
results: results.slice(0, k).map((row, index) => {
|
|
14
|
+
const text = String(row.fullText || row.text || '');
|
|
15
|
+
return { rank: index + 1, repo: row.repo, path: row.path, text, contentSha256: sha256(text) };
|
|
16
|
+
}) };
|
|
17
|
+
parseRetrievalResult({ structuredContent: { retrieval } }, { query, k });
|
|
18
|
+
return retrieval;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseRetrievalResult(result, { query, k }) {
|
|
22
|
+
const envelope = result?.structuredContent?.retrieval;
|
|
23
|
+
if (!envelope) throw new Error('UNKNOWN: incompatible MCP response lacks structured retrieval results; prose and grounding receipts cannot prove ranking');
|
|
24
|
+
if (result.isError || result.disabled || result._meta?.disabled
|
|
25
|
+
|| envelope.schemaVersion !== 1 || envelope.kind !== 'ruvnet-brain.retrieval-result'
|
|
26
|
+
|| typeof query !== 'string' || !query || envelope.query !== query
|
|
27
|
+
|| !Number.isSafeInteger(k) || k < 1 || envelope.k !== k
|
|
28
|
+
|| !Array.isArray(envelope.results) || envelope.results.length > k) {
|
|
29
|
+
throw new Error('structured retrieval query, depth, or result envelope mismatch');
|
|
30
|
+
}
|
|
31
|
+
return envelope.results.map((row, index) => {
|
|
32
|
+
if (row?.rank !== index + 1 || typeof row.repo !== 'string'
|
|
33
|
+
|| !/^[a-z0-9][a-z0-9._-]*$/i.test(row.repo) || !safePath(row.path)
|
|
34
|
+
|| typeof row.text !== 'string' || !row.text || row.contentSha256 !== sha256(row.text)) {
|
|
35
|
+
throw new Error('structured retrieval ordered hit or content binding mismatch');
|
|
36
|
+
}
|
|
37
|
+
return { ...row, repo: row.repo.toLowerCase() };
|
|
38
|
+
});
|
|
39
|
+
}
|