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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// plugin/scripts/md-stamp.mjs — PostToolUse (Write|Edit|MultiEdit). Refreshes an EXISTING doc
|
|
3
3
|
// stamp's date to today, in-place, whenever a touched .md file's stamp has gone stale.
|
|
4
|
+
// RUVNET_MD_STAMP=ensure explicitly enables managed creation/update timestamps and content-digest
|
|
5
|
+
// idempotence. That mode uses observed file edit times in UTC; unknown creation remains unknown.
|
|
4
6
|
//
|
|
5
7
|
// WHY. The owner: "I told you to update all .md docs with time/date stamps when touched. I don't
|
|
6
8
|
// want to have to remind you again in any repo." Today that convention lives only in the model's
|
|
@@ -31,7 +33,9 @@
|
|
|
31
33
|
import fs from 'node:fs';
|
|
32
34
|
import path from 'node:path';
|
|
33
35
|
import { fileURLToPath } from 'node:url';
|
|
36
|
+
import { createHash } from 'node:crypto';
|
|
34
37
|
import { parseHookEvent, toolName, field, readStdinBounded } from './hook-input.mjs';
|
|
38
|
+
import { contains, projectDirectory } from './project-identity.mjs';
|
|
35
39
|
|
|
36
40
|
// ── date, from the system clock, formatted in the repo's standard timezone ─────────────────────────
|
|
37
41
|
// Same idiom as scripts/self-update.mjs's README badge stamp: Intl.DateTimeFormat is a Node builtin
|
|
@@ -173,9 +177,90 @@ export function ensureStamp(content, { updated, created } = {}) {
|
|
|
173
177
|
|
|
174
178
|
/** Pure: given a .md file's current bytes, return the bytes it should have. Identical in ⇒ identical out. */
|
|
175
179
|
export function computeStampedContent(content, today = todayNY()) {
|
|
180
|
+
if (isPinned(content)) return content;
|
|
176
181
|
return refreshPlainStamp(refreshFrontmatterStamp(content, today), today);
|
|
177
182
|
}
|
|
178
183
|
|
|
184
|
+
function isPinned(content) {
|
|
185
|
+
return /^updated_pinned:[ \t]*(?:true|'true'|"true")[ \t]*(?:#.*)?$/m.test(content.match(FRONTMATTER_BLOCK_RE)?.[0] || '');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Explicit RUVNET_MD_STAMP=ensure opts this project into managed metadata. Defaults remain
|
|
189
|
+
// refresh-only: installing the plugin is not consent to impose stamps on a stranger's documents.
|
|
190
|
+
// The digest covers authored bytes, excluding only metadata we maintain. Unlike restoring mtime
|
|
191
|
+
// after writing, it remains idempotent when filesystem timestamp operations fail or hooks repeat.
|
|
192
|
+
const MANAGED_EXCLUDED = new Set(['node_modules', '.git', 'kb', 'dist', 'clones', 'archive',
|
|
193
|
+
'.agentic-qe', 'coverage', '.next', 'build', 'tmp', '.swarm', 'vendor']);
|
|
194
|
+
const DIGEST_LINE = /^<!-- ruvnet-md-stamp: ([a-f0-9]{64}) -->\r?\n?/m;
|
|
195
|
+
const STAMP_VALUE = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}\.\d{3}Z)?$/;
|
|
196
|
+
const MANAGED_HEAD_LINES = PLAIN_STAMP_MAX_LINES + 6;
|
|
197
|
+
|
|
198
|
+
function stampBody(content) {
|
|
199
|
+
const fm = content.match(FRONTMATTER_BLOCK_RE)?.[0];
|
|
200
|
+
if (fm) return fm.replace(/^(?:updated|updated_at|created_at|date|stamp_content_sha256):[^\n]*\n/gm, '') + content.slice(fm.length);
|
|
201
|
+
const head = headSlice(content, MANAGED_HEAD_LINES);
|
|
202
|
+
return head.replace(/^([ \t]*`?(?:Updated|Created):)[^\n]*\n?/gm, '')
|
|
203
|
+
.replace(DIGEST_LINE, '') + content.slice(head.length);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Managed mode: edit time is observed by the caller; creation requires positive provenance. */
|
|
207
|
+
export function computeManagedStamp(content, { updated, created = null } = {}) {
|
|
208
|
+
if (!STAMP_VALUE.test(updated || '') || isPinned(content)) return content;
|
|
209
|
+
const at = stampInsertionPoint(content);
|
|
210
|
+
if (!at) return content;
|
|
211
|
+
const fm = content.match(FRONTMATTER_BLOCK_RE)?.[0];
|
|
212
|
+
const oldDigest = fm?.match(/^stamp_content_sha256: ([a-f0-9]{64})$/m)?.[1]
|
|
213
|
+
|| headSlice(content, MANAGED_HEAD_LINES).match(DIGEST_LINE)?.[1];
|
|
214
|
+
const digest = (text) => createHash('sha256').update(stampBody(text)).digest('hex');
|
|
215
|
+
const complete = fm
|
|
216
|
+
? ['date', 'updated', 'created_at', 'updated_at'].every((key) => new RegExp(`^${key}: .+`, 'm').test(fm))
|
|
217
|
+
: ['Created', 'Updated'].every((key) => new RegExp(`^[ \\t]*\u0060?${key}: .+`, 'm').test(headSlice(content, MANAGED_HEAD_LINES)));
|
|
218
|
+
if (complete && oldDigest && oldDigest === digest(content)) return content;
|
|
219
|
+
const creation = STAMP_VALUE.test(created || '') ? created : null;
|
|
220
|
+
let out;
|
|
221
|
+
if (fm) {
|
|
222
|
+
let block = fm;
|
|
223
|
+
const put = (key, value, preserve = false) => {
|
|
224
|
+
const re = new RegExp(`^${key}:[^\\n]*`, 'm');
|
|
225
|
+
if (re.test(block)) { if (!preserve) block = block.replace(re, `${key}: ${value}`); }
|
|
226
|
+
else { const closing = block.lastIndexOf('---'); block = block.slice(0, closing) + `${key}: ${value}\n` + block.slice(closing); }
|
|
227
|
+
};
|
|
228
|
+
put('date', creation?.slice(0, 10) || 'unknown', true);
|
|
229
|
+
put('created_at', creation || 'unknown', true);
|
|
230
|
+
put('updated', updated.slice(0, 10));
|
|
231
|
+
put('updated_at', updated);
|
|
232
|
+
put('stamp_content_sha256', 'pending');
|
|
233
|
+
out = block + content.slice(fm.length);
|
|
234
|
+
// Hash after insertion so placement whitespace cannot make the next delivery appear edited.
|
|
235
|
+
out = out.replace(/^stamp_content_sha256: pending$/m, `stamp_content_sha256: ${digest(out)}`);
|
|
236
|
+
} else {
|
|
237
|
+
let head = headSlice(content, MANAGED_HEAD_LINES);
|
|
238
|
+
const rest = content.slice(head.length);
|
|
239
|
+
if (!head.endsWith('\n')) head += '\n';
|
|
240
|
+
const updatedRe = /^([ \t]*`?Updated:[ \t]*)[^\n|`]*(.*)$/m;
|
|
241
|
+
if (updatedRe.test(head)) head = head.replace(updatedRe, (_match, prefix, suffix) => `${prefix}${updated}${suffix ? ` ${suffix.trimStart()}` : ''}`);
|
|
242
|
+
else head = head.slice(0, at.index) + `\nUpdated: ${updated}\n` + head.slice(at.index);
|
|
243
|
+
if (!/^[ \t]*`?Created:/m.test(head)) head = head.replace(/^(.*Updated:[^\n]*\n)/m, `$1Created: ${creation || 'unknown (not recorded)'}\n`);
|
|
244
|
+
if (DIGEST_LINE.test(head)) head = head.replace(DIGEST_LINE, '<!-- ruvnet-md-stamp: pending -->\n');
|
|
245
|
+
else head = head.replace(/^(.*Created:[^\n]*\n)/m, '$1<!-- ruvnet-md-stamp: pending -->\n');
|
|
246
|
+
out = head + rest;
|
|
247
|
+
const withoutPending = out.replace(/^<!-- ruvnet-md-stamp: pending -->\n/m, '');
|
|
248
|
+
out = out.replace('<!-- ruvnet-md-stamp: pending -->', `<!-- ruvnet-md-stamp: ${digest(withoutPending)} -->`);
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Avoid overwriting a file changed since the hook's read. Session leases still govern writers. */
|
|
254
|
+
export function writeStampIfUnchanged(file, original, stamped, observed) {
|
|
255
|
+
if (stamped === original) return false;
|
|
256
|
+
const current = fs.statSync(file);
|
|
257
|
+
if (current.dev !== observed.dev || current.ino !== observed.ino
|
|
258
|
+
|| current.mtimeMs !== observed.mtimeMs || current.ctimeMs !== observed.ctimeMs
|
|
259
|
+
|| fs.readFileSync(file, 'utf8') !== original) return false;
|
|
260
|
+
fs.writeFileSync(file, stamped);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
179
264
|
// ── the hook body ────────────────────────────────────────────────────────────────────────────────
|
|
180
265
|
async function readHookInput() {
|
|
181
266
|
// Never block waiting on stdin: a TTY (someone running this file by hand) has none to give.
|
|
@@ -193,19 +278,37 @@ async function main() {
|
|
|
193
278
|
|
|
194
279
|
const ev = await readHookInput();
|
|
195
280
|
if (!['Write', 'Edit', 'MultiEdit'].includes(toolName(ev))) return; // wrong tool: do nothing
|
|
281
|
+
if (ev?.hook_event_name && ev.hook_event_name !== 'PostToolUse') return;
|
|
282
|
+
if (ev?.tool_response?.isError || ev?.tool_response?.error || ev?.tool_response?.success === false) return;
|
|
196
283
|
|
|
197
|
-
const
|
|
198
|
-
if (!
|
|
284
|
+
const supplied = field(ev, 'tool_input.file_path');
|
|
285
|
+
if (!supplied || path.extname(supplied).toLowerCase() !== '.md') return;
|
|
286
|
+
const root = projectDirectory();
|
|
287
|
+
const filePath = path.resolve(root, supplied);
|
|
288
|
+
if (!contains(root, filePath)) return;
|
|
289
|
+
if (path.relative(root, fs.realpathSync(filePath)).split(path.sep).some((part) => MANAGED_EXCLUDED.has(part))) return;
|
|
290
|
+
// The shared helper is present in current generations. Older standalone payloads can omit it.
|
|
291
|
+
try {
|
|
292
|
+
const { developmentHooksSuspended } = await import('./development-maintenance.mjs');
|
|
293
|
+
if (developmentHooksSuspended(root)) return;
|
|
294
|
+
} catch (error) { if (error.code !== 'ERR_MODULE_NOT_FOUND') return; }
|
|
199
295
|
|
|
200
|
-
let original;
|
|
201
|
-
try { original = fs.readFileSync(filePath, 'utf8'); } catch { return; }
|
|
296
|
+
let original, observed;
|
|
297
|
+
try { observed = fs.statSync(filePath); original = fs.readFileSync(filePath, 'utf8'); } catch { return; }
|
|
202
298
|
|
|
203
299
|
let stamped;
|
|
204
|
-
try {
|
|
300
|
+
try {
|
|
301
|
+
stamped = sw === 'ensure'
|
|
302
|
+
? computeManagedStamp(original, {
|
|
303
|
+
updated: observed.mtime.toISOString(),
|
|
304
|
+
created: ev?.tool_response?.type === 'create' ? observed.mtime.toISOString() : null,
|
|
305
|
+
})
|
|
306
|
+
: computeStampedContent(original);
|
|
307
|
+
} catch { return; }
|
|
205
308
|
|
|
206
309
|
if (stamped === original) return; // already current (or no stamp at all) — NEVER write; loop guard
|
|
207
310
|
|
|
208
|
-
try {
|
|
311
|
+
try { writeStampIfUnchanged(filePath, original, stamped, observed); } catch { /* advisory */ }
|
|
209
312
|
}
|
|
210
313
|
|
|
211
314
|
function isMain() {
|
|
@@ -111,14 +111,10 @@ const q = (db, sql) => {
|
|
|
111
111
|
|
|
112
112
|
try {
|
|
113
113
|
const value = run(uri);
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// litter is removed rather than left in the caller's directory.
|
|
114
|
+
// A concurrent writer invalidates the immutable-read assumption. Reject this result, but
|
|
115
|
+
// never remove sidecars: they may contain that writer's committed data and we do not own them.
|
|
116
|
+
// A later diagnosis will select ordinary read-only mode while the sidecars remain present.
|
|
118
117
|
if (restingWal && walSidecarsPresent(db)) {
|
|
119
|
-
for (const suffix of ['-wal', '-shm']) {
|
|
120
|
-
try { fs.unlinkSync(`${db}${suffix}`); } catch { /* best-effort cleanup only */ }
|
|
121
|
-
}
|
|
122
118
|
return { ok: false, err: 'a writer opened the store mid-read' };
|
|
123
119
|
}
|
|
124
120
|
return { ok: true, value, viaImmutable: restingWal || undefined };
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
// console reach the same `bin/install.mjs --enable-nightly/--disable-nightly` door; this adapter only
|
|
5
5
|
// supplies structured status and captures its exit result for the console.
|
|
6
6
|
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import os from 'node:os';
|
|
9
7
|
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
10
9
|
import { spawnSync } from 'node:child_process';
|
|
11
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { NIGHTLY_LABEL, nightlyArtifact, schedulerStatus } from './nightly-scheduler.mjs';
|
|
12
|
+
export { NIGHTLY_LABEL, nightlyArtifact } from './nightly-scheduler.mjs';
|
|
12
13
|
|
|
13
14
|
// ROOT is the tree that holds `bin/install.mjs`, and it is resolved by an EXACT layout test rather
|
|
14
15
|
// than by `..` — because `..` means two different things since this file moved into the payload
|
|
@@ -44,8 +45,6 @@ const INSTALLER = path.join(ROOT, 'bin', 'install.mjs');
|
|
|
44
45
|
* tests/unit/nightly-job-identity.test.mjs asserts the two are the same string, so a drift is a red
|
|
45
46
|
* test rather than a capability that quietly disappears from the console.
|
|
46
47
|
*/
|
|
47
|
-
export const NIGHTLY_LABEL = 'com.ruvnet.brain-update';
|
|
48
|
-
|
|
49
48
|
function schedulerEnvironment(env) {
|
|
50
49
|
const fixtureRoot = env.RUVNET_CONSOLE_ROOT;
|
|
51
50
|
if (!fixtureRoot) return env;
|
|
@@ -61,29 +60,12 @@ function schedulerEnvironment(env) {
|
|
|
61
60
|
}
|
|
62
61
|
return { ...env, HOME: isolatedHome, USERPROFILE: isolatedHome };
|
|
63
62
|
}
|
|
64
|
-
|
|
65
|
-
export function nightlyArtifact({ env = process.env, platform = process.platform } = {}) {
|
|
66
|
-
const schedulerEnv = schedulerEnvironment(env);
|
|
67
|
-
const home = schedulerEnv.HOME || os.homedir();
|
|
68
|
-
return {
|
|
69
|
-
supported: platform === 'darwin',
|
|
70
|
-
platform,
|
|
71
|
-
path: path.join(home, 'Library', 'LaunchAgents', `${NIGHTLY_LABEL}.plist`),
|
|
72
|
-
label: NIGHTLY_LABEL,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
63
|
export function nightlyStatus(options = {}) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
state: present ? 'on' : 'off',
|
|
84
|
-
evidence: present ? `LaunchAgent plist exists at ${artifact.path}` : `No LaunchAgent plist at ${artifact.path}`,
|
|
85
|
-
artifact,
|
|
86
|
-
};
|
|
64
|
+
const env = schedulerEnvironment(options.env || process.env);
|
|
65
|
+
const brainHome = options.brainHome || env.RUVNET_BRAIN_HOME
|
|
66
|
+
|| path.join(env.HOME || env.USERPROFILE || '', '.cache', 'ruvnet-brain');
|
|
67
|
+
return schedulerStatus({ ...options, env, brainHome, kbDir: options.kbDir || env.RUVNET_BRAIN_KB,
|
|
68
|
+
testMode: options.testMode ?? env.RUVNET_BRAIN_SCHEDULER_TEST === '1' });
|
|
87
69
|
}
|
|
88
70
|
|
|
89
71
|
export function applyNightlyChoice(enabled, options = {}) {
|
|
@@ -109,7 +91,7 @@ export function applyNightlyChoice(enabled, options = {}) {
|
|
|
109
91
|
before,
|
|
110
92
|
after,
|
|
111
93
|
log: ok
|
|
112
|
-
? `Nightly refresh is ${desired};
|
|
94
|
+
? `Nightly refresh is ${desired}; ${after.evidence}.`
|
|
113
95
|
: `Nightly refresh did not reach ${desired}: ${run.error?.message || run.stderr?.trim() || run.stdout?.trim() || `exit ${run.status}`}`,
|
|
114
96
|
};
|
|
115
97
|
}
|