ruvnet-brain 4.0.2 → 4.0.4
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 +1 -1
- package/bin/install.mjs +215 -31
- package/docs/RELEASE-NOTES-4.0.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/whats-new.md +6 -6
- package/plugin/docs/RELEASE-NOTES-4.0.md +88 -0
- package/plugin/mcp/server.mjs +70 -33
- package/plugin/scripts/hook-shim.mjs +27 -22
- package/plugin/scripts/session-start-core.mjs +28 -2
- package/plugin/scripts/whats-new.mjs +42 -0
- package/plugin/skills/release-proof/SKILL.md +21 -4
- package/plugin/skills/release-proof/references/receipt-contract.md +8 -2
- package/plugin/skills/release-proof/scripts/release-proof.mjs +77 -1
- package/plugin/skills/ruvnet-brain/SKILL.md +22 -7
- package/plugin/skills/whats-new/SKILL.md +4 -4
- package/scripts/build-bundle.mjs +34 -25
- package/scripts/fix-workstream.mjs +291 -0
- package/scripts/health-repair.mjs +14 -27
- package/scripts/issue-fix.mjs +135 -216
- package/scripts/memory-doctor.mjs +64 -9
- package/scripts/nightly-wrapper.sh +20 -28
- package/scripts/onboarding-console.mjs +226 -53
- package/scripts/protected-release-invocation.mjs +76 -0
- package/scripts/publication-receipt.mjs +307 -0
- package/scripts/release-authority.mjs +93 -0
- package/scripts/release.mjs +55 -11
- package/scripts/self-update.mjs +15 -227
- package/scripts/stabilization-receipt.mjs +108 -0
- package/scripts/wired-check.mjs +3 -0
package/scripts/self-update.mjs
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
// node scripts/self-update.mjs --apply --tier T0 # limit scope
|
|
9
9
|
// node scripts/self-update.mjs --apply --repo ruflo
|
|
10
10
|
//
|
|
11
|
+
// Publication is intentionally NOT supported here. A scheduled or local rebuild may prepare
|
|
12
|
+
// candidate bytes, but only the protected release workflow may publish them after release-proof
|
|
13
|
+
// seals the exact clean SHA and packed-artifact digest.
|
|
14
|
+
//
|
|
11
15
|
// Designed to be invoked by deploy/com.ruvnet.brain-nightly.plist (LaunchAgent — NOT auto-installed).
|
|
12
16
|
import fs from 'node:fs';
|
|
13
17
|
import os from 'node:os';
|
|
@@ -48,6 +52,11 @@ const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && proc
|
|
|
48
52
|
const APPLY = has('--apply');
|
|
49
53
|
const TIER = arg('--tier', null);
|
|
50
54
|
const ONLY = arg('--repo', null);
|
|
55
|
+
if (has('--publish')) {
|
|
56
|
+
console.error('[release-authority] DENIED: self-update is rebuild-only; --publish cannot create a GitHub Release, publish npm, move a dist-tag, push a release commit, or label anything shipped.');
|
|
57
|
+
console.error('[release-authority] Use the protected release workflow after release-proof seals the exact clean SHA and packed-artifact digest.');
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
51
60
|
// --publish's whole job is to end this run with `git commit` + `git push origin main` (see the
|
|
52
61
|
// PUBLISH block far below) — and those two commands operate on whatever branch is CURRENTLY
|
|
53
62
|
// CHECKED OUT in this working tree, not necessarily main. Real failure (2026-07-19): the nightly
|
|
@@ -259,14 +268,12 @@ for (const p of todo) {
|
|
|
259
268
|
} catch (e) { console.error(`[FAIL] ${p.name}: ${e.message}`); failures.push({ name: p.name, error: e.message }); }
|
|
260
269
|
}
|
|
261
270
|
|
|
262
|
-
// A per-repo build failure used to be logged and swallowed
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
// and the publish block (gh release create / git push) with a non-zero exit, so the nightly log surfaces it
|
|
266
|
-
// and nothing partial is released.
|
|
271
|
+
// A per-repo build failure used to be logged and swallowed before the run re-stamped and re-bundled
|
|
272
|
+
// partial data. Fail loud: if ANY repo failed, abort before stamp/bundle. This rebuild process has no
|
|
273
|
+
// publication authority; a later protected release may consume only a fully prepared candidate.
|
|
267
274
|
if (failures.length) {
|
|
268
|
-
NOTIFY('🔴 Nightly brain
|
|
269
|
-
console.error(`\n[FATAL] ${failures.length} repo build(s) failed this run — aborting before stamp/bundle
|
|
275
|
+
NOTIFY('🔴 Nightly brain rebuild ABORTED', `${failures.length} repo build(s) failed — no candidate prepared. See logs/nightly.log`, 'urgent');
|
|
276
|
+
console.error(`\n[FATAL] ${failures.length} repo build(s) failed this run — aborting before stamp/bundle. No candidate prepared:`);
|
|
270
277
|
for (const f of failures) console.error(` - ${f.name}: ${f.error}`);
|
|
271
278
|
console.error('Fix the failing repo(s) and re-run.');
|
|
272
279
|
process.exit(1);
|
|
@@ -279,226 +286,7 @@ console.log('[bundle] re-assembling dist/ruvnet-brain');
|
|
|
279
286
|
execFileSync(NODE, ['scripts/build-bundle.mjs'], { cwd: ROOT, env: { ...process.env, KB_MODEL_CACHE: MODEL_CACHE }, stdio: 'inherit' });
|
|
280
287
|
console.log('self-update done. (Deep-source refreshed + bundle re-assembled. Primer/L2/concepts + grading are supervised steps — re-run them when a repo materially changes.)');
|
|
281
288
|
|
|
282
|
-
//
|
|
283
|
-
// Without this, the nightly makes the LOCAL brain smarter while releases/latest never advances —
|
|
284
|
-
// users' auto-updaters correctly report "up to date" against a stale Release forever. Gated on
|
|
285
|
-
// --publish (the LaunchAgent passes it; ad-hoc manual runs don't publish by accident) and on
|
|
286
|
-
// something having actually been rebuilt this run. ONE product version: plugin.json's version is
|
|
287
|
-
// bumped (patch) and shipped in the same Release tag, so brain content and plugin always move
|
|
288
|
-
// under a single user-visible number. Fail-loud: any error here exits non-zero into the nightly
|
|
289
|
-
// log; nothing is half-published silently (release create is atomic per-tag; the version-bump
|
|
290
|
-
// commit only pushes after the Release exists).
|
|
291
|
-
if (has('--publish')) {
|
|
292
|
-
// Packaging gap (issue #29, found by Jan Lafko): the bundle carries EXECUTABLE reader scripts
|
|
293
|
-
// (kb/*.mjs) — so a reader fix could exist in git while releases/latest kept shipping the stale
|
|
294
|
-
// copy FOREVER whenever no corpus rebuild happened to come along (he caught the #27 fix present
|
|
295
|
-
// in source but absent from his installed plugin). A Release is now ALSO cut when any shipped
|
|
296
|
-
// script drifted vs the last release tag, even with zero corpus changes.
|
|
297
|
-
let scriptDrift = [];
|
|
298
|
-
try {
|
|
299
|
-
const lastTag = execFileSync('git', ['describe', '--tags', '--abbrev=0'], { cwd: ROOT, encoding: 'utf8' }).trim();
|
|
300
|
-
const out = execFileSync('git', ['diff', '--name-only', `${lastTag}..HEAD`, '--', 'kb/*.mjs', 'kb/package.json', 'bin/install.mjs'], { cwd: ROOT, encoding: 'utf8' }).trim();
|
|
301
|
-
scriptDrift = out ? out.split('\n') : [];
|
|
302
|
-
} catch { scriptDrift = ['(no release tag baseline — treating shipped scripts as drifted)']; }
|
|
303
|
-
if (todo.length === 0 && scriptDrift.length === 0) {
|
|
304
|
-
console.log('[publish] nothing was rebuilt and no shipped script changed since the last Release — no new Release needed. Done.');
|
|
305
|
-
} else {
|
|
306
|
-
if (todo.length === 0) console.log(`[publish] no corpus rebuild, but ${scriptDrift.length} shipped script(s) drifted since the last Release (issue #29 class) — cutting a Release so reader fixes actually reach users:\n${scriptDrift.map((f) => ' · ' + f).join('\n')}`);
|
|
307
|
-
const PLUGIN_JSON = path.join(ROOT, 'plugin', '.claude-plugin', 'plugin.json');
|
|
308
|
-
const pj = JSON.parse(fs.readFileSync(PLUGIN_JSON, 'utf8'));
|
|
309
|
-
const m = pj.version.match(/^(\d+)\.(\d+)\.(\d+)(-dev)?$/);
|
|
310
|
-
if (!m) { console.error(`[publish] FAIL: cannot parse plugin version "${pj.version}"`); process.exit(1); }
|
|
311
|
-
const next = `${m[1]}.${m[2]}.${Number(m[3]) + 1}${m[4] || ''}`;
|
|
312
|
-
const tag = `v${next}`;
|
|
313
|
-
pj.version = next;
|
|
314
|
-
pj.updated = new Date().toISOString().slice(0, 10);
|
|
315
|
-
fs.writeFileSync(PLUGIN_JSON, JSON.stringify(pj, null, 2) + '\n');
|
|
316
|
-
console.log(`[publish] product version → ${next} (${tag})`);
|
|
317
|
-
|
|
318
|
-
// Keep the README's bright-blue version badge in lockstep (Stuart's rule: version + updated
|
|
319
|
-
// timestamp with timezone, bright blue, first thing anyone sees). GitHub strips color from
|
|
320
|
-
// markdown text, so the line is a shields.io for-the-badge image — colors guaranteed.
|
|
321
|
-
const fmt = new Intl.DateTimeFormat('en-CA', {
|
|
322
|
-
timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
323
|
-
hour: '2-digit', minute: '2-digit', hour12: false, timeZoneName: 'short',
|
|
324
|
-
}).formatToParts(new Date());
|
|
325
|
-
const g = (t) => fmt.find((p) => p.type === t)?.value || '';
|
|
326
|
-
const human = `${g('year')}-${g('month')}-${g('day')} ${g('hour')}:${g('minute')} ${g('timeZoneName')}`;
|
|
327
|
-
const badgeStamp = human.replace(/-/g, '--').replace(/ /g, '_');
|
|
328
|
-
const badgeVer = next.replace(/-/g, '--');
|
|
329
|
-
const badge = `https://img.shields.io/badge/version_${badgeVer}-updated_${badgeStamp}-1E90FF?style=for-the-badge&labelColor=0757BA`;
|
|
330
|
-
const README = path.join(ROOT, 'README.md');
|
|
331
|
-
const readme = fs.readFileSync(README, 'utf8').replace(
|
|
332
|
-
/^### 🧠 RuvNet Brain — \[!\[.*$/m,
|
|
333
|
-
`### 🧠 RuvNet Brain — [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)`,
|
|
334
|
-
);
|
|
335
|
-
fs.writeFileSync(README, readme);
|
|
336
|
-
|
|
337
|
-
// FAN THE BUMP OUT ACROSS ROOT'S OTHER SURFACES — the step whose absence deadlocked publishing.
|
|
338
|
-
//
|
|
339
|
-
// The two writes above bump ROOT's plugin.json and README because the BUNDLE has to be stamped
|
|
340
|
-
// with its own release tag (issue #35). The other five surfaces — package.json, kb/package.json,
|
|
341
|
-
// data/manifest.json, primer/ruvnet-primer.md, explainer/index.html — were left behind, because
|
|
342
|
-
// sync-version.mjs was only ever run inside the publish worktree.
|
|
343
|
-
//
|
|
344
|
-
// That is fine for the COMMIT (the worktree is self-consistent and its version:check passes) and
|
|
345
|
-
// fatal for the PUSH. Git hooks are shared across worktrees and the pre-push gate resolves the
|
|
346
|
-
// repo it checks to ROOT — so it read ROOT, found plugin.json at the new version and five
|
|
347
|
-
// surfaces one behind, and refused: "5 surface(s) drifted from the source of truth".
|
|
348
|
-
//
|
|
349
|
-
// MEASURED 2026-07-24, after a ~3h rebuild: the worktree commit 899ed8d was correct and complete,
|
|
350
|
-
// version:check inside it passed cleanly, and the push was still rejected by a gate reading a
|
|
351
|
-
// directory the commit had nothing to do with. The whole night's work was thrown away at the last
|
|
352
|
-
// step by an inconsistency in a tree that was never going to be pushed.
|
|
353
|
-
//
|
|
354
|
-
// The gate is RIGHT — those five surfaces really were inconsistent, and a user reading ROOT would
|
|
355
|
-
// have seen a version that does not exist. The bug is that we made ROOT inconsistent and never
|
|
356
|
-
// repaired it. One line, in the same place the bump happens, so the two can never drift again.
|
|
357
|
-
execFileSync(NODE, ['scripts/sync-version.mjs'], { cwd: ROOT, stdio: 'inherit' });
|
|
358
|
-
|
|
359
|
-
// ── RE-STAMP THE BUNDLE WITH ITS OWN RELEASE TAG (issue #35, Dr. Mark Allen) ────────────────
|
|
360
|
-
//
|
|
361
|
-
// EVERY nightly release shipped a bundle stamped ONE VERSION BEHIND its own tag. build-bundle
|
|
362
|
-
// runs far above (it assembles dist/ before the publish block), and it stamps
|
|
363
|
-
// SOURCE.json.releaseTag = getVersionTag() — which at that moment is still the PREVIOUS version,
|
|
364
|
-
// because `next` is not computed until line ~284. Proven on the live release: the v3.4.21-dev
|
|
365
|
-
// asset (uploaded 07:21:52Z) carries releaseTag "v3.4.20-dev", one second before the version
|
|
366
|
-
// bump commit at 07:21:53Z.
|
|
367
|
-
//
|
|
368
|
-
// The consequence is not cosmetic. It is exactly the "my version says X but my knowledge base
|
|
369
|
-
// is older" confusion users reported: every installed brain misidentifies itself, --doctor's
|
|
370
|
-
// drift check compares against a wrong number, and forge-update's own behind/current logic
|
|
371
|
-
// reads a tag that never matches the release it came from.
|
|
372
|
-
//
|
|
373
|
-
// build-bundle.mjs already anticipated this fix in its own comment ("a release workflow that
|
|
374
|
-
// knows the exact git tag may overwrite this with the bundle's own Release tag") — it was just
|
|
375
|
-
// never written. Here it is, at the only point in the pipeline where the final tag is known and
|
|
376
|
-
// the bytes have not yet been zipped.
|
|
377
|
-
const bundleSource = path.join(ROOT, 'dist', 'ruvnet-brain', 'SOURCE.json');
|
|
378
|
-
try {
|
|
379
|
-
const doc = JSON.parse(fs.readFileSync(bundleSource, 'utf8'));
|
|
380
|
-
const before = doc.releaseTag;
|
|
381
|
-
doc.releaseTag = tag;
|
|
382
|
-
fs.writeFileSync(bundleSource, JSON.stringify(doc, null, 2) + '\n');
|
|
383
|
-
console.log(`[publish] bundle releaseTag ${before || '(none)'} → ${tag}`);
|
|
384
|
-
} catch (e) {
|
|
385
|
-
// Fail LOUD. Shipping a mis-stamped bundle is precisely the defect this block exists to end,
|
|
386
|
-
// and a silent skip here would restore it invisibly.
|
|
387
|
-
console.error(`[publish] FATAL: could not stamp the bundle's releaseTag (${e.message}). Refusing to publish a bundle that would misidentify its own version.`);
|
|
388
|
-
process.exit(1);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const zipPath = path.join(ROOT, 'dist', 'ruvnet-brain.zip');
|
|
392
|
-
console.log('[publish] zipping bundle (private stores already fenced out at assembly)');
|
|
393
|
-
try { fs.unlinkSync(zipPath); } catch {}
|
|
394
|
-
execFileSync('ditto', ['-c', '-k', '--sequesterRsrc', path.join(ROOT, 'dist', 'ruvnet-brain'), zipPath], { stdio: 'inherit' });
|
|
395
|
-
|
|
396
|
-
// Sign the bundle (SEC-0010 #6) so the installer can verify-before-extract. Transitional: if no
|
|
397
|
-
// signing key is available in this env yet (RUVNET_SIGNING_KEY / .secrets/…key.pem), warn and ship
|
|
398
|
-
// unsigned rather than break the nightly — once the key is wired in, every release ships signed and
|
|
399
|
-
// install.mjs's SIGNING_REQUIRED can flip to true. sign-bundle emits <zip>.sig + <zip>.sha256.
|
|
400
|
-
const sigAssets = [];
|
|
401
|
-
try {
|
|
402
|
-
execFileSync(NODE, ['scripts/sign-bundle.mjs', '--bundle', zipPath], { cwd: ROOT, stdio: 'inherit' });
|
|
403
|
-
for (const a of [`${zipPath}.sig`, `${zipPath}.sha256`]) if (fs.existsSync(a)) sigAssets.push(a);
|
|
404
|
-
console.log(`[publish] signed bundle (${sigAssets.length} signature asset(s) will be uploaded)`);
|
|
405
|
-
} catch (e) {
|
|
406
|
-
console.warn(`[publish] WARNING: bundle NOT signed (${e.message.split('\n')[0]}). Shipping unsigned (transitional). Set RUVNET_SIGNING_KEY in the nightly env to enable signing.`);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
console.log(`[publish] creating GitHub Release ${tag} + uploading bundle (~this can take minutes)`);
|
|
410
|
-
execFileSync(GH, ['release', 'create', tag, zipPath, ...sigAssets,
|
|
411
|
-
'--title', `${tag} — nightly brain refresh`,
|
|
412
|
-
'--notes', `Automated nightly: ${todo.length ? `re-ingested upstream changes in: ${todo.map((p) => p.name).join(', ')}` : `shipped reader-script fixes (no corpus change — issue #29 class)`}. One product version — plugin ${next} + this knowledge bundle ship together; user installs pick both up automatically.`,
|
|
413
|
-
], { cwd: ROOT, stdio: 'inherit' });
|
|
414
|
-
|
|
415
|
-
console.log('[publish] syncing version to EVERY surface, committing, pushing');
|
|
416
|
-
// Propagate `next` to package.json / kb/package.json / manifest / primer / explainer — NOT just the
|
|
417
|
-
// four files this block used to add. Otherwise those surfaces drift behind plugin.json (the exact
|
|
418
|
-
// 2026-07-19 3.4.19-vs-3.4.20 split that reddened repo-count + version gates).
|
|
419
|
-
//
|
|
420
|
-
// ── THE VERSION COMMIT RUNS IN A MAIN-PINNED WORKTREE, NEVER IN ROOT ────────────────────────────
|
|
421
|
-
//
|
|
422
|
-
// WHY (measured, twice). These four git commands operate on whatever branch ROOT happens to have
|
|
423
|
-
// checked out. On 2026-07-19 that produced commit 4a10833 "Nightly brain refresh v3.4.21-dev"
|
|
424
|
-
// stranded on feat/meta-proxy-passthrough. The guard at the top of this file was added in
|
|
425
|
-
// response and is CORRECT — it aborts rather than checking out main under a developer's
|
|
426
|
-
// uncommitted work, which would be unrecoverable.
|
|
427
|
-
//
|
|
428
|
-
// But an abort has a cost nobody was watching. This file is the ONLY thing that cuts GitHub
|
|
429
|
-
// Releases; `release.mjs --publish` does npm and contains zero `gh release` calls. So on
|
|
430
|
-
// 2026-07-23 the nightly aborted (repo was on branch 4.0), releases froze at v3.9.18-dev, npm kept
|
|
431
|
-
// advancing, and the channel verifier could not see it because it only checked the bundle was
|
|
432
|
-
// REACHABLE, never that it was CURRENT. A correct guard, a real abort, and 32 versions of drift.
|
|
433
|
-
//
|
|
434
|
-
// A worktree dissolves the dilemma instead of trading one failure for the other: a separate
|
|
435
|
-
// directory pinned to origin/main, so the developer's tree is never touched AND the release is
|
|
436
|
-
// never blocked by what they have checked out. Git refuses the same BRANCH in two worktrees, so it
|
|
437
|
-
// is DETACHED at origin/main and pushed as HEAD:main.
|
|
438
|
-
//
|
|
439
|
-
// The build artifacts stay in ROOT — only the version-bump commit moves. Convention follows
|
|
440
|
-
// scripts/issue-fix.mjs's WORKTREE_ROOT rather than inventing a second one.
|
|
441
|
-
const PUBLISH_WT = process.env.RUVNET_PUBLISH_WORKTREE
|
|
442
|
-
|| path.join(os.homedir(), '.cache', 'ruvnet-brain', 'publish-worktree');
|
|
443
|
-
const git = (args, cwd) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
444
|
-
|
|
445
|
-
let wt = null;
|
|
446
|
-
try {
|
|
447
|
-
git(['fetch', 'origin', 'main'], ROOT);
|
|
448
|
-
// Reuse a healthy worktree; rebuild it if a prior run left it wedged. `prune` first so a
|
|
449
|
-
// manually-deleted directory does not leave a stale registration that blocks `add`.
|
|
450
|
-
git(['worktree', 'prune'], ROOT);
|
|
451
|
-
if (!fs.existsSync(path.join(PUBLISH_WT, '.git'))) {
|
|
452
|
-
fs.mkdirSync(path.dirname(PUBLISH_WT), { recursive: true });
|
|
453
|
-
try { git(['worktree', 'remove', '--force', PUBLISH_WT], ROOT); } catch { /* not registered */ }
|
|
454
|
-
fs.rmSync(PUBLISH_WT, { recursive: true, force: true });
|
|
455
|
-
git(['worktree', 'add', '--detach', PUBLISH_WT, 'origin/main'], ROOT);
|
|
456
|
-
} else {
|
|
457
|
-
// Existing worktree: hard-reset to the real origin/main so a half-finished prior run cannot
|
|
458
|
-
// contribute stale files to this release.
|
|
459
|
-
git(['reset', '--hard', 'origin/main'], PUBLISH_WT);
|
|
460
|
-
git(['clean', '-fd'], PUBLISH_WT);
|
|
461
|
-
}
|
|
462
|
-
wt = PUBLISH_WT;
|
|
463
|
-
console.log(`[publish] version commit will run in ${PUBLISH_WT} (detached at origin/main) — ROOT is untouched`);
|
|
464
|
-
} catch (e) {
|
|
465
|
-
console.error(`[publish] FATAL: could not prepare the main-pinned publish worktree (${String(e.message || e).split('\n')[0]}).`);
|
|
466
|
-
console.error('[publish] Refusing to fall back to committing in ROOT — that is the exact stranded-commit bug this replaces.');
|
|
467
|
-
process.exit(1);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Seed the worktree with the new version, then let sync-version.mjs fan it out from its single
|
|
471
|
-
// source of truth exactly as it does in ROOT.
|
|
472
|
-
const wtPlugin = path.join(wt, 'plugin', '.claude-plugin', 'plugin.json');
|
|
473
|
-
const wtDoc = JSON.parse(fs.readFileSync(wtPlugin, 'utf8'));
|
|
474
|
-
wtDoc.version = next;
|
|
475
|
-
fs.writeFileSync(wtPlugin, JSON.stringify(wtDoc, null, 2) + '\n');
|
|
476
|
-
execFileSync(NODE, ['scripts/sync-version.mjs'], { cwd: wt, stdio: 'inherit' });
|
|
477
|
-
execFileSync('git', ['add', 'README.md', 'plugin/.claude-plugin/plugin.json', 'plugin/.codex-plugin/plugin.json',
|
|
478
|
-
'package.json', 'kb/package.json',
|
|
479
|
-
'data/manifest.json', 'primer/ruvnet-primer.md', 'explainer/index.html'], { cwd: wt, stdio: 'inherit' });
|
|
480
|
-
execFileSync('git', ['commit', '-m', `Nightly brain refresh ${tag}: ${todo.map((p) => p.name).join(', ')}\n\nAutomated by scripts/self-update.mjs --publish (launchd com.ruvnet.brain-nightly).`], { cwd: wt, stdio: 'inherit' });
|
|
481
|
-
execFileSync('git', ['push', 'origin', 'HEAD:main'], { cwd: wt, stdio: 'inherit' });
|
|
482
|
-
// npm publish — ATOMIC with the GitHub Release so npm `latest` can NEVER lag behind it. This block
|
|
483
|
-
// previously shipped GitHub + plugin but never touched npm, so every nightly left npm further behind
|
|
484
|
-
// (the 2026-07-19 npm 3.4.18 / GitHub 3.4.20 drift). Fail LOUD if npm cannot be published — GitHub
|
|
485
|
-
// being ahead of npm must abort the run visibly, never pass silently. One product version, every channel.
|
|
486
|
-
try {
|
|
487
|
-
let onNpm = '';
|
|
488
|
-
try { onNpm = execFileSync('npm', ['view', `ruvnet-brain@${next}`, 'version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { /* not yet on npm — fine */ }
|
|
489
|
-
if (onNpm !== next) execFileSync('npm', ['publish', '--tag', 'latest'], { cwd: ROOT, stdio: 'inherit' });
|
|
490
|
-
execFileSync('npm', ['dist-tag', 'add', `ruvnet-brain@${next}`, 'latest'], { cwd: ROOT, stdio: 'inherit' });
|
|
491
|
-
console.log(`[publish] npm ${next} + \`latest\` — channels IN SYNC (npm == GitHub == plugin)`);
|
|
492
|
-
} catch (e) {
|
|
493
|
-
console.error(`[publish] FATAL: npm publish/dist-tag FAILED (${e.message.split('\n')[0]}). GitHub is now AHEAD of npm — the exact drift this guards against. The nightly aborts here; republish npm manually (\`npm publish --tag latest && npm dist-tag add ruvnet-brain@${next} latest\`) and check npm auth in the launchd env.`);
|
|
494
|
-
process.exit(1);
|
|
495
|
-
}
|
|
496
|
-
NOTIFY('🟢 Nightly brain published', `${tag} is live on releases/latest + npm — ${todo.length ? `rebuilt: ${todo.map((p) => p.name).join(', ')}` : `reader-script fixes only`}`);
|
|
497
|
-
console.log(`[publish] DONE — ${tag} live. Users' heartbeats will pick up plugin + brain automatically.`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Sol amendment to F4 (end of run): the verified rebuilds/publish above are real work and were NOT
|
|
289
|
+
// Sol amendment to F4 (end of run): the verified rebuilds above are real work and were NOT
|
|
502
290
|
// skipped — but if any freshness probe failed, this run still may not report clean. Exit 1 AFTER the
|
|
503
291
|
// work so the wrapper's retry re-probes (rebuilds are change-gated, so the retry is cheap) and a
|
|
504
292
|
// persistent blind spot escalates instead of hiding inside a green "no-op".
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Produce the explicit maintenance-release seal used when the owner chooses to publish a
|
|
3
|
+
// stabilization build before the separate >=95 promotion program is complete. This receipt never
|
|
4
|
+
// claims 95 and cannot satisfy the strict candidate schema by accident.
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { evaluateStabilizationCandidateReceipt } from './release-proof.mjs';
|
|
11
|
+
|
|
12
|
+
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
13
|
+
const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
14
|
+
|
|
15
|
+
function argument(args, name) {
|
|
16
|
+
const index = args.indexOf(name);
|
|
17
|
+
return index >= 0 ? args[index + 1] : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function packedVersion(artifact) {
|
|
21
|
+
const result = spawnSync('tar', ['-xOf', artifact, 'package/package.json'], { encoding: 'utf8' });
|
|
22
|
+
if (result.status !== 0) throw new Error(`cannot read packed package.json: ${String(result.stderr || '').trim()}`);
|
|
23
|
+
return JSON.parse(result.stdout).version;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createStabilizationReceipt({ root, artifactPath, qe, audit }) {
|
|
27
|
+
const git = (...args) => {
|
|
28
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
|
29
|
+
if (result.status !== 0) throw new Error(`git ${args.join(' ')} failed`);
|
|
30
|
+
return result.stdout.trim();
|
|
31
|
+
};
|
|
32
|
+
const manifest = readJson(path.join(root, 'package.json'));
|
|
33
|
+
const claude = readJson(path.join(root, 'plugin/.claude-plugin/plugin.json'));
|
|
34
|
+
const codex = readJson(path.join(root, 'plugin/.codex-plugin/plugin.json'));
|
|
35
|
+
const sha = git('rev-parse', 'HEAD');
|
|
36
|
+
const digest = sha256(artifactPath);
|
|
37
|
+
const total = Number(qe?.numTotalTests ?? qe?.testResults?.reduce((sum, file) => sum + (file.assertionResults?.length || 0), 0) ?? 0);
|
|
38
|
+
const failed = Number(qe?.numFailedTests ?? 0);
|
|
39
|
+
const skipped = Number(qe?.numPendingTests ?? 0);
|
|
40
|
+
const passed = Number(qe?.numPassedTests ?? Math.max(0, total - failed - skipped));
|
|
41
|
+
const vulnerabilities = audit?.metadata?.vulnerabilities;
|
|
42
|
+
if (!vulnerabilities || !Number.isFinite(Number(vulnerabilities.critical))
|
|
43
|
+
|| !Number.isFinite(Number(vulnerabilities.high))) {
|
|
44
|
+
throw new Error('dependency audit evidence is missing critical/high counts');
|
|
45
|
+
}
|
|
46
|
+
const receipt = {
|
|
47
|
+
schemaVersion: 1,
|
|
48
|
+
phase: 'stabilization-candidate',
|
|
49
|
+
mode: 'stabilization',
|
|
50
|
+
reason: 'Owner-authorized 4.0.4 stabilization release; >=95 promotion remains open work',
|
|
51
|
+
targetScore: 95,
|
|
52
|
+
scoreClaimed: false,
|
|
53
|
+
sha,
|
|
54
|
+
tree: git('rev-parse', 'HEAD^{tree}'),
|
|
55
|
+
dirty: Boolean(git('status', '--porcelain')),
|
|
56
|
+
version: manifest.version,
|
|
57
|
+
tag: `v${manifest.version}`,
|
|
58
|
+
sourceVersions: { package: manifest.version, claudePlugin: claude.version, codexPlugin: codex.version },
|
|
59
|
+
artifact: {
|
|
60
|
+
path: `release-evidence/${path.basename(artifactPath)}`,
|
|
61
|
+
sha256: digest,
|
|
62
|
+
sourceSha: sha,
|
|
63
|
+
version: packedVersion(artifactPath),
|
|
64
|
+
},
|
|
65
|
+
security: {
|
|
66
|
+
status: Number(vulnerabilities.critical || 0) === 0 && Number(vulnerabilities.high || 0) === 0 ? 'PASS' : 'FAIL',
|
|
67
|
+
critical: Number(vulnerabilities.critical || 0),
|
|
68
|
+
high: Number(vulnerabilities.high || 0),
|
|
69
|
+
},
|
|
70
|
+
qe: { status: total > 0 && failed === 0 && skipped === 0 ? 'PASS' : 'FAIL', total, passed, failed, skipped },
|
|
71
|
+
limitations: [
|
|
72
|
+
'No >=95 score claimed',
|
|
73
|
+
'Strict promotion receipt remains unsatisfied',
|
|
74
|
+
'Clean-host and installed-Brain acceptance are required after publication',
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
const result = evaluateStabilizationCandidateReceipt(receipt);
|
|
78
|
+
if (result.verdict !== 'PASS') throw new Error(`stabilization seal failed: ${result.failures.map(({ code }) => code).join(',')}`);
|
|
79
|
+
return receipt;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function main(args = process.argv.slice(2)) {
|
|
83
|
+
try {
|
|
84
|
+
const rootArg = argument(args, '--root');
|
|
85
|
+
const artifactArg = argument(args, '--artifact');
|
|
86
|
+
const outArg = argument(args, '--out');
|
|
87
|
+
if (!artifactArg || !outArg) throw new Error('--artifact and --out are required');
|
|
88
|
+
const root = path.resolve(rootArg || process.cwd());
|
|
89
|
+
const artifactPath = path.resolve(artifactArg);
|
|
90
|
+
const outPath = path.resolve(outArg);
|
|
91
|
+
if (fs.existsSync(outPath)) throw new Error(`refusing to overwrite existing receipt: ${outPath}`);
|
|
92
|
+
const receipt = createStabilizationReceipt({
|
|
93
|
+
root,
|
|
94
|
+
artifactPath,
|
|
95
|
+
qe: readJson(argument(args, '--qe')),
|
|
96
|
+
audit: readJson(argument(args, '--audit')),
|
|
97
|
+
});
|
|
98
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
99
|
+
fs.writeFileSync(outPath, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
100
|
+
console.log(JSON.stringify({ verdict: 'PASS', sha: receipt.sha, artifactSha256: receipt.artifact.sha256, version: receipt.version }));
|
|
101
|
+
return 0;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(`stabilization-receipt: ${error.message}`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) process.exitCode = main();
|
package/scripts/wired-check.mjs
CHANGED
|
@@ -95,6 +95,9 @@ const STANDALONE = [
|
|
|
95
95
|
+ 'RVFs to RVF-GENERATIONS.json and optionally prunes legacy sidecars; build-bundle.mjs consumes '
|
|
96
96
|
+ 'and validates the resulting manifest, so scheduling this destructive migration would be wrong'],
|
|
97
97
|
['release', 'the ship path, run by a human'],
|
|
98
|
+
['fix-workstream', 'session-supervised coordination CLI run explicitly by the integration owner or an '
|
|
99
|
+
+ 'isolated writing agent to start and hand off a fix lane. Scheduling it would violate its safety '
|
|
100
|
+
+ 'boundary: it prepares evidence but never merges, pushes, publishes, deletes, or cleans worktrees'],
|
|
98
101
|
// Run by the launchd nightly, which lives OUTSIDE this repo — so no in-repo caller can exist.
|
|
99
102
|
// This is the one category the scanner genuinely cannot reach, and saying so is the honest form.
|
|
100
103
|
['self-update', 'launchd nightly (out-of-repo scheduler)'],
|