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.
Files changed (83) hide show
  1. package/README.md +26 -9
  2. package/bin/install.mjs +646 -366
  3. package/bin/nightly-refresh.mjs +115 -0
  4. package/kb/brain-profile.mjs +118 -32
  5. package/kb/lifecycle-evidence-retention.mjs +237 -0
  6. package/kb/refresh-run.mjs +367 -0
  7. package/kb/retrieval-result.mjs +39 -0
  8. package/kb/update-storage-transaction.mjs +439 -0
  9. package/package.json +11 -2
  10. package/plugin/.claude-plugin/plugin.json +1 -1
  11. package/plugin/.codex-plugin/plugin.json +1 -1
  12. package/plugin/hooks/codex-hooks.json +2 -165
  13. package/plugin/hooks/hook-contracts.json +4 -65
  14. package/plugin/hooks/hooks.json +2 -208
  15. package/plugin/scripts/adr-currency-gate.mjs +29 -19
  16. package/plugin/scripts/capability-registry.mjs +10 -83
  17. package/plugin/scripts/codex-hook-adapter.mjs +62 -9
  18. package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
  19. package/plugin/scripts/continuation-gate.mjs +30 -28
  20. package/plugin/scripts/continuation-objective.mjs +34 -0
  21. package/plugin/scripts/development-maintenance.mjs +49 -0
  22. package/plugin/scripts/hook-shim.mjs +3 -0
  23. package/plugin/scripts/learn-flush.mjs +6 -1
  24. package/plugin/scripts/lesson-gate.mjs +5 -2
  25. package/plugin/scripts/lesson-presentation.mjs +5 -0
  26. package/plugin/scripts/md-stamp.mjs +109 -6
  27. package/plugin/scripts/memory-doctor.mjs +3 -7
  28. package/plugin/scripts/nightly-controller.mjs +9 -27
  29. package/plugin/scripts/nightly-scheduler.mjs +472 -0
  30. package/plugin/scripts/project-progression-contract.mjs +4 -4
  31. package/plugin/scripts/project-progression-store.mjs +35 -6
  32. package/plugin/scripts/ruflo-bin.mjs +20 -0
  33. package/plugin/scripts/session-snapshot-contract.mjs +6 -1
  34. package/plugin/scripts/version-bump-gate.sh +14 -2
  35. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
  36. package/scripts/build-bundle.mjs +8 -0
  37. package/scripts/build-primer.mjs +2 -3
  38. package/scripts/candidate-host-evidence.mjs +59 -41
  39. package/scripts/ci/build-fixture-kb.mjs +59 -2
  40. package/scripts/ci/mutate-hook-timeout.mjs +24 -47
  41. package/scripts/ci/stranger-scenario.mjs +13 -28
  42. package/scripts/claims-verify.mjs +219 -21
  43. package/scripts/console-engine.mjs +4 -18
  44. package/scripts/console-runtime-identity.mjs +6 -0
  45. package/scripts/development-maintenance.mjs +47 -0
  46. package/scripts/development-push-check.mjs +35 -0
  47. package/scripts/distill-project.mjs +24 -47
  48. package/scripts/doc-currency.mjs +91 -19
  49. package/scripts/git-hooks/pre-push +4 -162
  50. package/scripts/health-repair.mjs +22 -4
  51. package/scripts/hook-retirement-check.mjs +22 -0
  52. package/scripts/host-install-matrix.mjs +125 -61
  53. package/scripts/integration-evidence.mjs +11 -6
  54. package/scripts/learning-replay-fixture.mjs +23 -2
  55. package/scripts/nightly-two-run-proof.mjs +415 -0
  56. package/scripts/nightly-watchdog.mjs +17 -2
  57. package/scripts/npm-invocation.mjs +20 -0
  58. package/scripts/prepublication-evidence.mjs +42 -6
  59. package/scripts/primer-grounding.mjs +20 -0
  60. package/scripts/product-integrity-contract.mjs +22 -2
  61. package/scripts/public-verification-abandon.mjs +101 -0
  62. package/scripts/public-verification-aggregate.mjs +53 -19
  63. package/scripts/public-verification-finalizer.mjs +8 -2
  64. package/scripts/public-verification-lane.mjs +63 -8
  65. package/scripts/publication-receipt.mjs +109 -45
  66. package/scripts/published-surface-probe.mjs +10 -2
  67. package/scripts/qa-contract.mjs +57 -0
  68. package/scripts/qa-lanes.mjs +40 -0
  69. package/scripts/qa-runner.mjs +54 -64
  70. package/scripts/qe/ux-suite.mjs +10 -34
  71. package/scripts/qualified-candidate-check.mjs +124 -0
  72. package/scripts/release-abort-stale.mjs +2 -2
  73. package/scripts/release-projection.mjs +20 -1
  74. package/scripts/release-qualification-contract.mjs +87 -0
  75. package/scripts/release-qualification.mjs +135 -0
  76. package/scripts/release-transaction.mjs +81 -1
  77. package/scripts/remedy-registry.mjs +6 -13
  78. package/scripts/retrieval-canary.mjs +91 -18
  79. package/scripts/selfcheck.mjs +12 -4
  80. package/scripts/snapshot-freshness.mjs +58 -0
  81. package/scripts/stack-sync.mjs +41 -43
  82. package/scripts/staged-host-verifier.mjs +63 -14
  83. package/scripts/wired-check.mjs +31 -2
@@ -0,0 +1,439 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const STATES = Object.freeze([
6
+ 'LOCKED',
7
+ 'CANDIDATE_BUILDING',
8
+ 'CANDIDATE_VERIFIED',
9
+ 'NOOP',
10
+ 'OLD_RENAME_STARTED',
11
+ 'OLD_RENAMED',
12
+ 'ACTIVATED',
13
+ 'LIVE_VERIFIED',
14
+ 'CLEANUP_PENDING',
15
+ 'COMMITTED',
16
+ 'ROLLED_BACK',
17
+ 'RECOVERY_REQUIRED',
18
+ ]);
19
+
20
+ function assertDirectory(dir, label) {
21
+ const stat = fs.lstatSync(dir);
22
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`${label} is not a trusted directory: ${dir}`);
23
+ }
24
+
25
+ function canonicalEntryInventory(root, prefix = '') {
26
+ const entries = [];
27
+ for (const entry of fs.readdirSync(path.join(root, prefix), { withFileTypes: true })
28
+ .sort((left, right) => left.name.localeCompare(right.name))) {
29
+ const relative = prefix ? path.join(prefix, entry.name) : entry.name;
30
+ const file = path.join(root, relative);
31
+ if (entry.isDirectory()) entries.push(...canonicalEntryInventory(root, relative));
32
+ else if (entry.isFile()) {
33
+ const bytes = fs.readFileSync(file);
34
+ entries.push({ path: relative.split(path.sep).join('/'), type: 'file', bytes: bytes.length,
35
+ sha256: crypto.createHash('sha256').update(bytes).digest('hex') });
36
+ } else if (entry.isSymbolicLink()) {
37
+ entries.push({ path: relative.split(path.sep).join('/'), type: 'symlink', target: fs.readlinkSync(file) });
38
+ } else throw new Error(`unsupported filesystem entry in transaction tree: ${relative}`);
39
+ }
40
+ return entries;
41
+ }
42
+
43
+ export function treeIdentity(dir) {
44
+ assertDirectory(dir, 'transaction tree');
45
+ const entries = canonicalEntryInventory(dir);
46
+ const bytes = entries.reduce((total, entry) => total + (entry.bytes || 0), 0);
47
+ return { sha256: crypto.createHash('sha256').update(JSON.stringify(entries)).digest('hex'), bytes,
48
+ fileCount: entries.filter((entry) => entry.type === 'file').length, entries };
49
+ }
50
+
51
+ function trustedTreeSummary(dir, kind) {
52
+ const identity = treeIdentity(dir);
53
+ if (identity.entries.some((entry) => entry.type === 'symlink')) {
54
+ throw new Error(`managed ${kind} tree contains a symbolic link: ${dir}`);
55
+ }
56
+ return { kind, path: dir, sha256: identity.sha256, bytes: identity.bytes, fileCount: identity.fileCount };
57
+ }
58
+
59
+ // Installer-retained paths are observations, not transaction-owned cleanup candidates. Count link
60
+ // payloads themselves without following targets, including a symlink at the retained root.
61
+ function observedInstallerSummary(dir, kind) {
62
+ let bytes = 0; let fileCount = 0; let symlinkCount = 0;
63
+ const visit = (file) => {
64
+ const stat = fs.lstatSync(file);
65
+ if (stat.isSymbolicLink()) { symlinkCount++; bytes += Buffer.byteLength(fs.readlinkSync(file)); }
66
+ else if (stat.isDirectory()) for (const name of fs.readdirSync(file)) visit(path.join(file, name));
67
+ else if (stat.isFile()) { fileCount++; bytes += stat.size; }
68
+ else throw new Error(`unsupported installer-retained entry: ${file}`);
69
+ };
70
+ visit(dir);
71
+ return { kind, path: dir, bytes, fileCount, symlinkCount,
72
+ status: kind === 'installer-preserved' ? 'PRESERVED_UNCLASSIFIED' : 'UNRESOLVED_INSTALLER_STATE',
73
+ automaticCleanupEligible: false };
74
+ }
75
+
76
+ export function managedStorageInventory(liveDir, { measuredAt = new Date().toISOString() } = {}) {
77
+ const live = path.resolve(liveDir);
78
+ const parent = path.dirname(live);
79
+ const basename = path.basename(live);
80
+ const fullCorpusCopies = [];
81
+ const evidence = [];
82
+ for (const name of fs.readdirSync(parent).sort()) {
83
+ const file = path.join(parent, name);
84
+ const stat = fs.lstatSync(file);
85
+ const installerKind = name.startsWith(`${basename}.install-preserved-`) ? 'installer-preserved'
86
+ : name.startsWith(`${basename}.install-prior-`) ? 'installer-prior'
87
+ : name.startsWith(`.${basename}.install-stage-`) ? 'installer-stage' : null;
88
+ if (installerKind) {
89
+ fullCorpusCopies.push(observedInstallerSummary(file, installerKind));
90
+ continue;
91
+ }
92
+ const kind = name === basename ? 'active'
93
+ : name.startsWith(`${basename}.next-`) ? 'candidate'
94
+ : name.startsWith(`${basename}.rollback-`) ? 'rollback'
95
+ : name.startsWith(`${basename}.failed-`) ? 'failed'
96
+ : name.startsWith(`${basename}.bak-`) ? 'backup' : null;
97
+ if (kind) {
98
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`managed ${kind} entry is not a trusted directory: ${file}`);
99
+ fullCorpusCopies.push(trustedTreeSummary(file, kind));
100
+ } else if ([`.${basename}.update-transactions`, 'refresh-runs'].includes(name)) {
101
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`managed evidence entry is not a trusted directory: ${file}`);
102
+ evidence.push(trustedTreeSummary(file, name === 'refresh-runs' ? 'refresh-evidence' : 'transaction-evidence'));
103
+ }
104
+ }
105
+ const active = fullCorpusCopies.find(({ kind }) => kind === 'active') || null;
106
+ const redundant = fullCorpusCopies.filter(({ kind }) => kind !== 'active');
107
+ return { schemaVersion: 1, liveDir: live, active, fullCorpusCopies,
108
+ additionalFullCorpusCopyCount: redundant.length,
109
+ additionalFullCorpusBytes: redundant.reduce((sum, row) => sum + row.bytes, 0),
110
+ evidence, evidenceBytes: evidence.reduce((sum, row) => sum + row.bytes, 0),
111
+ totalManagedBytes: [...fullCorpusCopies, ...evidence].reduce((sum, row) => sum + row.bytes, 0), measuredAt };
112
+ }
113
+
114
+ function optionalTreeIdentity(dir) {
115
+ if (!fs.existsSync(dir)) return { exists: false, sha256: null, bytes: 0, fileCount: 0 };
116
+ const { sha256, bytes, fileCount } = treeIdentity(dir);
117
+ return { exists: true, sha256, bytes, fileCount };
118
+ }
119
+
120
+ const identitySummary = (identity) => identity
121
+ ? { sha256: identity.sha256, bytes: identity.bytes, fileCount: identity.fileCount }
122
+ : { sha256: null, bytes: 0, fileCount: 0 };
123
+
124
+ export function storageDelta(paths, { prior = null, candidate = null, cleanupPending = null,
125
+ inventoryBefore = null, measuredAt = new Date().toISOString() } = {}) {
126
+ const fullCorpusCopies = Object.fromEntries(['live', 'candidate', 'rollback', 'failed']
127
+ .map((name) => [name, optionalTreeIdentity(paths[name])]));
128
+ const redundant = ['candidate', 'rollback', 'failed'].map((name) => fullCorpusCopies[name])
129
+ .filter(({ exists }) => exists);
130
+ const activeBefore = identitySummary(prior);
131
+ const activeAfter = fullCorpusCopies.live;
132
+ const managedAfter = managedStorageInventory(paths.live, { measuredAt });
133
+ const managedBefore = inventoryBefore;
134
+ return {
135
+ activeBefore,
136
+ activeAfter,
137
+ activeBytesDelta: activeAfter.bytes - activeBefore.bytes,
138
+ expectedCandidate: candidate ? identitySummary(candidate) : null,
139
+ fullCorpusCopies,
140
+ redundantCopyCount: managedAfter.additionalFullCorpusCopyCount,
141
+ redundantBytes: managedAfter.additionalFullCorpusBytes,
142
+ managedBefore,
143
+ managedAfter,
144
+ additionalFullCorpusCopyDelta: managedAfter.additionalFullCorpusCopyCount
145
+ - (managedBefore?.additionalFullCorpusCopyCount || 0),
146
+ managedBytesDelta: managedAfter.totalManagedBytes - (managedBefore?.totalManagedBytes || activeBefore.bytes),
147
+ evidenceBytesDelta: managedAfter.evidenceBytes - (managedBefore?.evidenceBytes || 0),
148
+ cleanupPending: cleanupPending ?? fullCorpusCopies.rollback.exists,
149
+ measuredAt,
150
+ };
151
+ }
152
+
153
+ function resultFailures(result) {
154
+ if (result === true || result?.valid === true || result?.ok === true) return [];
155
+ if (Array.isArray(result?.failures)) return result.failures.map(String);
156
+ return [String(result?.reason || 'validation returned no passing verdict')];
157
+ }
158
+
159
+ function transactionPaths(liveDir, transactionId) {
160
+ if (!/^[a-zA-Z0-9._-]+$/.test(transactionId)) throw new Error('transactionId contains unsafe characters');
161
+ const live = path.resolve(liveDir);
162
+ const parent = path.dirname(live);
163
+ const basename = path.basename(live);
164
+ return {
165
+ live,
166
+ candidate: path.join(parent, `${basename}.next-${transactionId}`),
167
+ rollback: path.join(parent, `${basename}.rollback-${transactionId}`),
168
+ failed: path.join(parent, `${basename}.failed-${transactionId}`),
169
+ receipts: path.join(parent, `.${basename}.update-transactions`, transactionId),
170
+ };
171
+ }
172
+
173
+ function recorder(paths, transactionId) {
174
+ fs.mkdirSync(paths.receipts, { recursive: false });
175
+ let sequence = 0;
176
+ return (state, details = {}) => {
177
+ if (!STATES.includes(state)) throw new Error(`unsupported storage transaction state: ${state}`);
178
+ sequence += 1;
179
+ const receipt = { schemaVersion: 1, kind: 'ruvnet-brain-storage-transaction-phase', transactionId,
180
+ sequence, state, recordedAt: new Date().toISOString(), paths: {
181
+ live: paths.live, candidate: paths.candidate, rollback: paths.rollback, failed: paths.failed,
182
+ receipts: paths.receipts,
183
+ }, ...details };
184
+ const name = `${String(sequence).padStart(3, '0')}-${state}.json`;
185
+ writeReceiptAtomic(path.join(paths.receipts, name), receipt);
186
+ return receipt;
187
+ };
188
+ }
189
+
190
+ function writeReceiptAtomic(file, receipt) {
191
+ const temporary = `${file}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
192
+ const descriptor = fs.openSync(temporary, 'wx', 0o600);
193
+ try {
194
+ fs.writeFileSync(descriptor, `${JSON.stringify(receipt, null, 2)}\n`);
195
+ fs.fsyncSync(descriptor);
196
+ } finally { fs.closeSync(descriptor); }
197
+ try { fs.renameSync(temporary, file); }
198
+ catch (error) { fs.rmSync(temporary, { force: true }); throw error; }
199
+ }
200
+
201
+ function appendRecoveryReceipt(receipts, state, details) {
202
+ const existing = fs.readdirSync(receipts).filter((name) => /^\d+-[A-Z_]+\.json$/.test(name))
203
+ .map((name) => ({ name, sequence: Number.parseInt(name, 10) }))
204
+ .sort((left, right) => left.sequence - right.sequence);
205
+ const sequence = existing.at(-1).sequence + 1;
206
+ const prior = JSON.parse(fs.readFileSync(path.join(receipts, existing.at(-1).name), 'utf8'));
207
+ const receipt = { ...prior, sequence, state, recordedAt: new Date().toISOString(), ...details };
208
+ writeReceiptAtomic(path.join(receipts, `${String(sequence).padStart(3, '0')}-${state}.json`), receipt);
209
+ return receipt;
210
+ }
211
+
212
+ function removeCandidate(candidate) {
213
+ if (fs.existsSync(candidate)) fs.rmSync(candidate, { recursive: true, force: true });
214
+ }
215
+
216
+ function requireDigest(dir, expected, label) {
217
+ if (!expected?.sha256) throw new Error(`${label} tree has no sealed receipt identity`);
218
+ const actual = treeIdentity(dir);
219
+ if (actual.sha256 !== expected.sha256 || actual.bytes !== expected.bytes || actual.fileCount !== expected.fileCount) {
220
+ throw new Error(`${label} tree identity differs from its sealed receipt`);
221
+ }
222
+ return actual;
223
+ }
224
+
225
+ export function recoverIncompleteStorageTransactions(liveDir, { removeTree = removeCandidate } = {}) {
226
+ const live = path.resolve(liveDir);
227
+ const receiptsRoot = path.join(path.dirname(live), `.${path.basename(live)}.update-transactions`);
228
+ if (!fs.existsSync(receiptsRoot)) return [];
229
+ const recovered = [];
230
+ const removeIfPresent = (target) => { if (fs.existsSync(target)) removeTree(target); };
231
+ for (const transactionId of fs.readdirSync(receiptsRoot).sort()) {
232
+ if (/^\.prune-[A-Za-z0-9._-]+-[a-f0-9]{16}$/.test(transactionId)) continue;
233
+ const receipts = path.join(receiptsRoot, transactionId);
234
+ if (!fs.lstatSync(receipts).isDirectory()) throw new Error(`storage transaction receipt entry is not a directory: ${receipts}`);
235
+ const phaseFiles = fs.readdirSync(receipts).filter((name) => /^\d{3}-[A-Z_]+\.json$/.test(name)).sort();
236
+ if (!phaseFiles.length) throw new Error(`storage transaction receipt is empty: ${receipts}`);
237
+ const latest = JSON.parse(fs.readFileSync(path.join(receipts, phaseFiles.at(-1)), 'utf8'));
238
+ if (['NOOP', 'COMMITTED', 'ROLLED_BACK'].includes(latest.state)) continue;
239
+ if (latest.state === 'RECOVERY_REQUIRED') throw new Error(`storage transaction requires manual recovery: ${transactionId}`);
240
+ const paths = latest.paths;
241
+ const expectedPaths = transactionPaths(live, transactionId);
242
+ if (!paths || ['live', 'candidate', 'rollback', 'failed'].some((name) =>
243
+ path.resolve(paths[name] || '') !== expectedPaths[name])
244
+ || (paths.receipts && path.resolve(paths.receipts) !== expectedPaths.receipts)) {
245
+ throw new Error(`storage transaction paths are unsafe or belong to another live tree: ${transactionId}`);
246
+ }
247
+ paths.receipts = expectedPaths.receipts;
248
+ const prior = latest.prior;
249
+ if (!prior?.sha256) throw new Error(`storage transaction has no prior tree identity: ${transactionId}`);
250
+ try {
251
+ // Validate every retained tree before any rename or deletion. A receipt owns
252
+ // paths, but cannot authorize discarding bytes added after the process died.
253
+ if (fs.existsSync(paths.candidate)) requireDigest(paths.candidate, latest.candidate, 'interrupted candidate');
254
+ if (fs.existsSync(paths.rollback)) requireDigest(paths.rollback, prior, 'interrupted rollback');
255
+ if (fs.existsSync(paths.failed)) throw new Error('interrupted failed tree has no safe recovery disposition');
256
+ if (['LOCKED', 'CANDIDATE_BUILDING', 'CANDIDATE_VERIFIED'].includes(latest.state)) {
257
+ requireDigest(live, prior, 'interrupted live');
258
+ removeIfPresent(paths.candidate);
259
+ } else if (latest.state === 'OLD_RENAME_STARTED') {
260
+ const hasLive = fs.existsSync(live);
261
+ const hasRollback = fs.existsSync(paths.rollback);
262
+ if (hasLive === hasRollback) throw new Error('old-tree rename state is ambiguous');
263
+ if (hasRollback) fs.renameSync(paths.rollback, live);
264
+ requireDigest(live, prior, 'recovered live');
265
+ removeIfPresent(paths.candidate);
266
+ } else if (['OLD_RENAMED', 'ACTIVATED'].includes(latest.state)) {
267
+ if (!fs.existsSync(paths.rollback)) throw new Error('rollback tree is missing');
268
+ if (fs.existsSync(live)) requireDigest(live, latest.candidate, 'interrupted activated live');
269
+ if (fs.existsSync(live)) fs.renameSync(live, paths.failed);
270
+ fs.renameSync(paths.rollback, live);
271
+ requireDigest(live, prior, 'recovered live');
272
+ removeIfPresent(paths.failed);
273
+ removeIfPresent(paths.candidate);
274
+ const delta = storageDelta(paths, { prior, candidate: latest.candidate || null });
275
+ appendRecoveryReceipt(receipts, 'ROLLED_BACK', { terminalVerdict: 'interrupted-run-restored', prior,
276
+ storageDelta: delta, reason: `recovered interrupted ${latest.state} transaction before new work` });
277
+ recovered.push({ transactionId, from: latest.state, terminalVerdict: 'interrupted-run-restored',
278
+ storageDelta: delta });
279
+ continue;
280
+ } else if (['LIVE_VERIFIED', 'CLEANUP_PENDING'].includes(latest.state)) {
281
+ const candidate = latest.candidate;
282
+ if (!candidate?.sha256) throw new Error('verified candidate identity is missing');
283
+ requireDigest(live, candidate, 'verified live');
284
+ if (fs.existsSync(paths.candidate) || fs.existsSync(paths.failed)) {
285
+ throw new Error('verified-live cleanup state contains an unexpected extra tree');
286
+ }
287
+ if (fs.existsSync(paths.rollback)) assertDirectory(paths.rollback, 'redundant rollback');
288
+ try { removeIfPresent(paths.rollback); }
289
+ catch (cleanupError) {
290
+ const delta = storageDelta(paths, { prior, candidate, cleanupPending: true });
291
+ appendRecoveryReceipt(receipts, 'CLEANUP_PENDING', { terminalVerdict: 'cleanup-pending', prior,
292
+ candidate, storageDelta: delta, retryable: true, reason: cleanupError.message });
293
+ recovered.push({ transactionId, from: latest.state, terminalVerdict: 'cleanup-pending',
294
+ retryable: true, paths, storageDelta: delta });
295
+ continue;
296
+ }
297
+ const delta = storageDelta(paths, { prior, candidate, cleanupPending: false });
298
+ appendRecoveryReceipt(receipts, 'COMMITTED', { terminalVerdict: 'applied', prior, candidate,
299
+ storageDelta: delta, reason: `completed cleanup for interrupted ${latest.state} transaction` });
300
+ recovered.push({ transactionId, from: latest.state, terminalVerdict: 'applied', storageDelta: delta });
301
+ continue;
302
+ } else throw new Error(`unsupported interrupted state ${latest.state}`);
303
+ const delta = storageDelta(paths, { prior, candidate: latest.candidate || null });
304
+ appendRecoveryReceipt(receipts, 'ROLLED_BACK', { terminalVerdict: 'interrupted-run-restored', prior,
305
+ storageDelta: delta, reason: `recovered interrupted ${latest.state} transaction before new work` });
306
+ recovered.push({ transactionId, from: latest.state, terminalVerdict: 'interrupted-run-restored',
307
+ storageDelta: delta });
308
+ } catch (error) {
309
+ appendRecoveryReceipt(receipts, 'RECOVERY_REQUIRED', { terminalVerdict: 'recovery-required', prior,
310
+ reason: error.message });
311
+ throw new Error(`storage transaction ${transactionId} recovery failed: ${error.message}`);
312
+ }
313
+ }
314
+ return recovered;
315
+ }
316
+
317
+ /**
318
+ * Build and validate a complete sibling candidate, then activate it with two directory renames.
319
+ * The live directory is never modified before candidate validation succeeds.
320
+ */
321
+ export function runStorageTransaction({
322
+ liveDir,
323
+ sourceDir,
324
+ transactionId,
325
+ prepareCandidate = () => {},
326
+ validateCandidate = () => ({ valid: true }),
327
+ validateLive = validateCandidate,
328
+ checkpoint = () => {},
329
+ removeRollback = (dir) => fs.rmSync(dir, { recursive: true }),
330
+ }) {
331
+ const recovered = recoverIncompleteStorageTransactions(liveDir, { removeTree: removeRollback });
332
+ const pendingCleanup = recovered.find(({ retryable }) => retryable);
333
+ if (pendingCleanup) return { terminalVerdict: 'cleanup-pending', cleanupPending: true,
334
+ recovered: true, paths: pendingCleanup.paths, storageDelta: pendingCleanup.storageDelta };
335
+ const paths = transactionPaths(liveDir, transactionId);
336
+ assertDirectory(paths.live, 'live tree');
337
+ assertDirectory(path.resolve(sourceDir), 'source tree');
338
+ for (const target of [paths.candidate, paths.rollback, paths.failed, paths.receipts]) {
339
+ if (fs.existsSync(target)) throw new Error(`transaction path already exists: ${target}`);
340
+ }
341
+ const inventoryBefore = managedStorageInventory(paths.live);
342
+ fs.mkdirSync(path.dirname(paths.receipts), { recursive: true });
343
+ const record = recorder(paths, transactionId);
344
+ const prior = treeIdentity(paths.live);
345
+ record('LOCKED', { prior });
346
+ checkpoint('LOCKED', paths);
347
+ record('CANDIDATE_BUILDING', { prior });
348
+ checkpoint('CANDIDATE_BUILDING', paths);
349
+
350
+ try {
351
+ fs.cpSync(path.resolve(sourceDir), paths.candidate, { recursive: true, errorOnExist: true, preserveTimestamps: true });
352
+ prepareCandidate({ candidateDir: paths.candidate, liveDir: paths.live, paths });
353
+ const failures = resultFailures(validateCandidate({ dir: paths.candidate, phase: 'candidate' }));
354
+ if (failures.length) throw new Error(`candidate validation failed: ${failures.join('; ')}`);
355
+ } catch (error) {
356
+ removeCandidate(paths.candidate);
357
+ record('ROLLED_BACK', { terminalVerdict: 'failed-before-activation', prior,
358
+ storageDelta: storageDelta(paths, { prior, inventoryBefore }), reason: error.message });
359
+ requireDigest(paths.live, prior, 'unchanged live');
360
+ throw error;
361
+ }
362
+
363
+ const candidate = treeIdentity(paths.candidate);
364
+ record('CANDIDATE_VERIFIED', { prior, candidate });
365
+ checkpoint('CANDIDATE_VERIFIED', paths);
366
+ if (candidate.sha256 === prior.sha256 && candidate.bytes === prior.bytes && candidate.fileCount === prior.fileCount) {
367
+ removeCandidate(paths.candidate);
368
+ const delta = storageDelta(paths, { prior, candidate, inventoryBefore });
369
+ record('NOOP', { terminalVerdict: 'noop', prior, candidate, storageDelta: delta });
370
+ return { terminalVerdict: 'noop', prior, candidate, paths, storageDelta: delta };
371
+ }
372
+
373
+ try {
374
+ record('OLD_RENAME_STARTED', { prior, candidate });
375
+ checkpoint('OLD_RENAME_STARTED', paths);
376
+ fs.renameSync(paths.live, paths.rollback);
377
+ checkpoint('OLD_RENAMED_UNRECEIPTED', paths);
378
+ record('OLD_RENAMED', { prior, candidate });
379
+ checkpoint('OLD_RENAMED', paths);
380
+ try {
381
+ fs.renameSync(paths.candidate, paths.live);
382
+ checkpoint('ACTIVATED_UNRECEIPTED', paths);
383
+ } catch (error) {
384
+ fs.renameSync(paths.rollback, paths.live);
385
+ requireDigest(paths.live, prior, 'restored live');
386
+ record('ROLLED_BACK', { terminalVerdict: 'rolled-back', prior, candidate,
387
+ storageDelta: storageDelta(paths, { prior, candidate, inventoryBefore }), reason: error.message });
388
+ throw new Error(`candidate activation failed; prior tree restored: ${error.message}`);
389
+ }
390
+ record('ACTIVATED', { prior, candidate });
391
+ checkpoint('ACTIVATED', paths);
392
+ requireDigest(paths.live, candidate, 'activated live');
393
+ const failures = resultFailures(validateLive({ dir: paths.live, phase: 'live' }));
394
+ if (failures.length) throw new Error(`live validation failed: ${failures.join('; ')}`);
395
+ record('LIVE_VERIFIED', { prior, candidate });
396
+ checkpoint('LIVE_VERIFIED', paths);
397
+ } catch (error) {
398
+ if (fs.existsSync(paths.rollback)) {
399
+ try {
400
+ if (fs.existsSync(paths.live)) fs.renameSync(paths.live, paths.failed);
401
+ fs.renameSync(paths.rollback, paths.live);
402
+ requireDigest(paths.live, prior, 'rolled-back live');
403
+ removeCandidate(paths.failed);
404
+ removeCandidate(paths.candidate);
405
+ record('ROLLED_BACK', { terminalVerdict: 'rolled-back', prior, candidate,
406
+ storageDelta: storageDelta(paths, { prior, candidate, inventoryBefore }), reason: error.message });
407
+ throw new Error(`${error.message}; prior tree restored and verified`);
408
+ } catch (rollbackError) {
409
+ if (String(rollbackError.message).includes('prior tree restored and verified')) throw rollbackError;
410
+ record('RECOVERY_REQUIRED', { terminalVerdict: 'recovery-required', prior, candidate,
411
+ storageDelta: storageDelta(paths, { prior, candidate, inventoryBefore }),
412
+ reason: `${error.message}; rollback failed: ${rollbackError.message}` });
413
+ throw new Error(`${error.message}; rollback failed and manual recovery is required: ${rollbackError.message}`);
414
+ }
415
+ }
416
+ throw error;
417
+ }
418
+
419
+ try {
420
+ removeRollback(paths.rollback);
421
+ checkpoint('ROLLBACK_REMOVED', paths);
422
+ }
423
+ catch (error) {
424
+ if (!fs.existsSync(paths.rollback)) {
425
+ const delta = storageDelta(paths, { prior, candidate, cleanupPending: false, inventoryBefore });
426
+ record('COMMITTED', { terminalVerdict: 'applied', prior, candidate, storageDelta: delta,
427
+ cleanupWarning: error.message });
428
+ return { terminalVerdict: 'applied', prior, candidate, paths, storageDelta: delta };
429
+ }
430
+ const delta = storageDelta(paths, { prior, candidate, cleanupPending: true, inventoryBefore });
431
+ record('CLEANUP_PENDING', { terminalVerdict: 'cleanup-pending', prior, candidate,
432
+ storageDelta: delta, reason: error.message });
433
+ return { terminalVerdict: 'cleanup-pending', cleanupPending: true, prior, candidate, paths,
434
+ storageDelta: delta };
435
+ }
436
+ const delta = storageDelta(paths, { prior, candidate, inventoryBefore });
437
+ record('COMMITTED', { terminalVerdict: 'applied', prior, candidate, storageDelta: delta });
438
+ return { terminalVerdict: 'applied', prior, candidate, paths, storageDelta: delta };
439
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.9",
3
+ "version": "4.3.10",
4
4
  "description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "version:check": "node scripts/sync-version.mjs --check",
16
16
  "version:set": "node scripts/set-version.mjs",
17
17
  "qa:pr": "node scripts/qa-runner.mjs",
18
+ "maintenance": "node scripts/development-maintenance.mjs",
18
19
  "qa:release": "node scripts/qa-runner.mjs --release",
19
20
  "release:authority": "node scripts/release-authority.mjs",
20
21
  "card-from-source": "node scripts/card-from-source.mjs",
@@ -60,11 +61,14 @@
60
61
  "status:check": "node scripts/status-honesty.mjs",
61
62
  "cap:collect": "node scripts/rerank-cap-eval.mjs --collect",
62
63
  "cap:report": "node scripts/rerank-cap-eval.mjs --report",
63
- "cap:ab": "node scripts/rerank-cap-warm-ab.mjs"
64
+ "cap:ab": "node scripts/rerank-cap-warm-ab.mjs",
65
+ "hooks:check": "node scripts/hook-retirement-check.mjs",
66
+ "release:qualify": "node scripts/release-qualification.mjs"
64
67
  },
65
68
  "files": [
66
69
  ".claude-plugin/marketplace.json",
67
70
  "bin/install.mjs",
71
+ "bin/nightly-refresh.mjs",
68
72
  "plugin/",
69
73
  "plugin/mcp/managed-cli-interface.mjs",
70
74
  "plugin/mcp/server.mjs",
@@ -82,8 +86,12 @@
82
86
  "data/model-catalog.json",
83
87
  "scripts/",
84
88
  "kb/verify-citation.mjs",
89
+ "kb/retrieval-result.mjs",
85
90
  "kb/zip-extract.mjs",
86
91
  "kb/brain-profile.mjs",
92
+ "kb/lifecycle-evidence-retention.mjs",
93
+ "kb/refresh-run.mjs",
94
+ "kb/update-storage-transaction.mjs",
87
95
  "kb/model-requirements.mjs",
88
96
  "keys/ruvnet-brain-signing.pub.pem"
89
97
  ],
@@ -112,6 +120,7 @@
112
120
  "bugs": "https://github.com/stuinfla/ruvnet-brain/issues",
113
121
  "author": "Stuart Kerr",
114
122
  "devDependencies": {
123
+ "@babel/parser": "^7.29.7",
115
124
  "@metaharness/darwin": "~0.9.2",
116
125
  "@ruvector/rvf": "^0.3.4",
117
126
  "@vitest/coverage-v8": "^4.1.10",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
3
  "description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
4
- "version": "4.3.9",
4
+ "version": "4.3.10",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.9",
3
+ "version": "4.3.10",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"