ruvnet-brain 4.3.21 → 4.3.26
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 +5 -5
- package/bin/install.mjs +275 -60
- package/console/app.js +189 -9
- package/console/index.html +70 -24
- package/console/scope.css +137 -0
- package/console/scope.html +144 -0
- package/console/scope.js +209 -0
- package/console/style.css +26 -0
- package/console/tips.html +1 -0
- package/kb/corpus-release-identity.mjs +239 -0
- package/kb/update-storage-transaction.mjs +20 -3
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/checkpoint.md +61 -0
- package/plugin/hooks/codex-hooks.json +64 -1
- package/plugin/hooks/hook-contracts.json +299 -6
- package/plugin/hooks/hooks.json +81 -1
- package/plugin/mcp/server.mjs +23 -0
- package/plugin/scripts/advocacy-catalog.mjs +245 -0
- package/plugin/scripts/advocacy-route.mjs +460 -0
- package/plugin/scripts/continuation-gate.mjs +25 -2
- package/plugin/scripts/continuation-objective.mjs +7 -1
- package/plugin/scripts/continuity-hook-policy.mjs +190 -15
- package/plugin/scripts/coverage-integrity.mjs +7 -0
- package/plugin/scripts/gates.mjs +113 -10
- package/plugin/scripts/grounding-turn-gate.mjs +167 -0
- package/plugin/scripts/grounding-turn-mark.mjs +91 -0
- package/plugin/scripts/hook-shim.mjs +14 -0
- package/plugin/scripts/nightly-scheduler.mjs +37 -4
- package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
- package/plugin/scripts/project-progression-contract.mjs +16 -0
- package/plugin/scripts/project-progression-hook.mjs +3 -0
- package/plugin/scripts/project-progression-producer.mjs +252 -0
- package/plugin/scripts/project-progression-reader.mjs +271 -0
- package/plugin/scripts/project-progression-session-start.mjs +93 -16
- package/plugin/scripts/project-progression-sources.mjs +220 -0
- package/plugin/scripts/project-progression-store.mjs +106 -13
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
- package/plugin/scripts/session-snapshot-hook.mjs +115 -7
- package/plugin/scripts/session-start-budget.mjs +59 -0
- package/plugin/scripts/session-start-core.mjs +234 -457
- package/plugin/scripts/session-start-fsutil.mjs +61 -0
- package/plugin/scripts/session-start-health.mjs +64 -0
- package/plugin/scripts/session-start-hook-description.mjs +45 -0
- package/plugin/scripts/session-start-issue-alert.mjs +77 -0
- package/plugin/scripts/session-start-repo-identity.mjs +54 -0
- package/plugin/scripts/session-start-signals.mjs +73 -0
- package/plugin/scripts/session-start-trace.mjs +86 -0
- package/plugin/scripts/session-start-update-plane.mjs +104 -0
- package/plugin/scripts/unprompted-runtime.mjs +32 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
- package/scripts/adr-072-completion.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +5 -1
- package/scripts/approved-runtime.mjs +197 -0
- package/scripts/brain-novice-50.mjs +16 -1
- package/scripts/brain-score.mjs +23 -5
- package/scripts/build-bundle.mjs +971 -530
- package/scripts/build-concepts.mjs +36 -116
- package/scripts/console-engine.test.mjs +8 -7
- package/scripts/console-runtime-identity.mjs +4 -0
- package/scripts/corpus-aggregates.mjs +94 -77
- package/scripts/corpus-candidate.mjs +475 -222
- package/scripts/corpus-next-seed.mjs +225 -0
- package/scripts/corpus-promotion.mjs +58 -0
- package/scripts/corpus-reconcile.mjs +411 -105
- package/scripts/doc-currency.mjs +16 -1
- package/scripts/dual-host-deliberation.mjs +25 -2
- package/scripts/dual-host-suggest.mjs +17 -1
- package/scripts/falsify.mjs +13 -3
- package/scripts/gist-receipts.mjs +482 -87
- package/scripts/github-health-watch.mjs +12 -2
- package/scripts/handoff-asset.mjs +34 -0
- package/scripts/hook-retirement-check.mjs +8 -1
- package/scripts/host-registry.mjs +1 -1
- package/scripts/ingest-gists.mjs +74 -101
- package/scripts/job-heartbeat.sh +77 -14
- package/scripts/learning-replay-execution.mjs +10 -4
- package/scripts/nightly-gists.sh +27 -13
- package/scripts/nightly-two-run-proof.mjs +1 -1
- package/scripts/nightly-watchdog.mjs +61 -4
- package/scripts/onboarding-console.mjs +364 -28
- package/scripts/oracle/produce-questions.mjs +293 -0
- package/scripts/oracle/producer-hosts.mjs +235 -0
- package/scripts/oracle/repo-recall.mjs +448 -0
- package/scripts/oracle/retrieval-accuracy.mjs +818 -0
- package/scripts/oracle/source-tree.mjs +165 -0
- package/scripts/oracle/source-units.mjs +391 -0
- package/scripts/oracle/spike-run.mjs +98 -0
- package/scripts/oracle/unit-inventory.mjs +141 -0
- package/scripts/oracle/unit-sampling.mjs +128 -0
- package/scripts/oracle/validate-labels.mjs +250 -0
- package/scripts/private-overlay.mjs +248 -0
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/proxy/claude-proxied.sh +6 -0
- package/scripts/proxy/proxy-revert.sh +5 -0
- package/scripts/proxy/proxy-up.sh +6 -0
- package/scripts/proxy/proxy-verify.mjs +4 -0
- package/scripts/public-inputs.mjs +409 -0
- package/scripts/public-verification-inputs.mjs +112 -26
- package/scripts/public-verification-lane.mjs +1 -1
- package/scripts/published-surface-probe.mjs +34 -4
- package/scripts/qe/card-lane-gate.mjs +16 -1
- package/scripts/qe/session-start-gate.mjs +16 -1
- package/scripts/rebuild-gists-from-receipts.mjs +58 -78
- package/scripts/record-lesson.mjs +4 -1
- package/scripts/rehearse-corpus-pipeline.mjs +994 -0
- package/scripts/release-abort-stale.mjs +5 -1
- package/scripts/release-authority.mjs +104 -12
- package/scripts/release-channel-kind.mjs +86 -0
- package/scripts/release-convergence-watchdog.mjs +7 -2
- package/scripts/release-projection.mjs +177 -72
- package/scripts/release-transaction-provider.mjs +47 -10
- package/scripts/release-transaction.mjs +40 -11
- package/scripts/release.mjs +252 -17
- package/scripts/retrieval-canary.mjs +87 -0
- package/scripts/rvf-index-audit.mjs +573 -13
- package/scripts/rvf-wire.mjs +269 -0
- package/scripts/seal-gist-receipt.mjs +65 -0
- package/scripts/selfcheck.mjs +42 -21
- package/scripts/source-coverage.mjs +253 -24
- package/scripts/status-honesty.mjs +25 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-version.mjs +2 -0
- package/scripts/trismart.mjs +42 -0
- package/scripts/updater-manifest.mjs +162 -0
- package/scripts/verify-channels.mjs +17 -5
- package/scripts/wired-check.mjs +48 -10
- package/tri-smart-skill/QUICKSTART.md +37 -0
- package/tri-smart-skill/README.md +92 -0
- package/tri-smart-skill/install.cmd +14 -0
- package/tri-smart-skill/install.command +13 -0
- package/tri-smart-skill/install.mjs +51 -0
- package/tri-smart-skill/install.sh +9 -0
- package/tri-smart-skill/tri-smart/SKILL.md +90 -0
- package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
- package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
- package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
- package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
- package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
- package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
- package/scripts/corpus-seed-publish.mjs +0 -110
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Updated: 2026-09-
|
|
1
|
+
Updated: 2026-09-11 08:00:00 EDT | Version 4.3.21
|
|
2
2
|
Created: 2026-06-29 22:36:38 EDT
|
|
3
3
|
|
|
4
4
|
<div align="center">
|
|
@@ -7,7 +7,7 @@ Created: 2026-06-29 22:36:38 EDT
|
|
|
7
7
|
|
|
8
8
|
# 🧠 RuvNet Brain
|
|
9
9
|
|
|
10
|
-
### 🧠 RuvNet Brain — [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
11
11
|
|
|
12
12
|
**A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
|
|
13
13
|
|
|
@@ -47,7 +47,7 @@ safe inverse must not be advertised as a one-click undo. See the
|
|
|
47
47
|
[](https://isovision.ai/ruvnet-brain/)
|
|
48
48
|
[](LICENSE)
|
|
49
49
|
[](#testing--proof)
|
|
50
|
-
[](#testing--proof)
|
|
51
51
|
|
|
52
52
|
> **One Brain generation everywhere.** npm, the GitHub tag/release, bundle manifests, source metadata, and checksum-bound RVF generations must share the same product version. Headline claims are regenerated and checked by the claims ledger (`scripts/claims-verify.mjs`); other numbers below are hand-stamped and dated:
|
|
53
53
|
> - **`plugin`** (badge above) — the Claude Code plugin itself: SKILL.md, the grounding hooks, the MCP server. Read live from [`plugin/.claude-plugin/plugin.json`](plugin/.claude-plugin/plugin.json). Updates often — this is where behavior fixes land.
|
|
@@ -525,7 +525,7 @@ node plugin/test/run-tests.mjs # full plugin QA over real JSO
|
|
|
525
525
|
| **L4 "orchestrate"** | **downgraded — measures speech, not obedience** | L4 asserts the hook's own injected prose contains required words (`must: ['take the wheel','SPARC','swarm',…]`). That proves **the brain spoke**. It cannot fail when the advice is read and ignored — which is the failure this product exists to prevent. Counterfactual replay against a brain-off control (ADR-058 §D4) is what will earn this row back |
|
|
526
526
|
| **Plugin QA** | **60 / 60** | manifests, hook firing, MCP `initialize`/`tools/list`, capability battery |
|
|
527
527
|
| **Clean-room install** | **3 / 3** | download the published bundle fresh → unzip → query → grounded, cited answers |
|
|
528
|
-
| **Unit tests** | **3,035 passing, 161 todo** ·
|
|
528
|
+
| **Unit tests** | **3,035 passing, 161 todo** · 42% of ALL source covered | `npm run test:cov` regenerates both — the coverage floor fails CI if it slips (`claims:verify` re-derives the %, it is not a hand-typed badge). 42% is the honest number over every shipped file; the previous "75%" measured a hand-picked 8-file subset |
|
|
529
529
|
| **Grounding proof** | `npx ruvnet-brain --doctor` | asks a real question, then checks the cited path really exists in the on-disk store; a citation that doesn't resolve is reported as **NOT grounded** |
|
|
530
530
|
| **Held-out eval** | **grounded 100/100** · routed 63/80 | `npm run eval` — 120 frozen, hash-pinned questions across 5 strata, never used for tuning, graded on ground truth, never by a model |
|
|
531
531
|
|
|
@@ -562,7 +562,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
|
|
|
562
562
|
|
|
563
563
|
This project versions in the open (see the live badge up top for the exact plugin version; the downloadable knowledge bundle is a separate track) — we don't claim “done,” “complete,” or “zero hallucinations.” Where it stands:
|
|
564
564
|
|
|
565
|
-
- ✅ **The grounding brain is real and proven** —
|
|
565
|
+
- ✅ **The grounding brain is real and proven** — 182 public stores · 143,719 public source chunks, dual embeddings, cross-encoder rerank, plugin (MCP tool + explicit skills; automatic hooks retired), all re-runnable.
|
|
566
566
|
- ✅ **Code-level depth** — the code-rich repos are indexed to full function bodies; “how is it implemented?” returns the implementation. Verified in the shipped bundle (clean-room 3/3).
|
|
567
567
|
- ✅ **Routing holds** — named 47/48, described 26/28, scenario 7/8; behavioral L1–L3 all pass (**L4 downgraded — it measures that the brain spoke, not that anything listened**); private stores fenced out of the public bundle (zero-leak verified).
|
|
568
568
|
- ⚠️ **Two routing residuals** (above) — surfaced, not hidden.
|
package/bin/install.mjs
CHANGED
|
@@ -32,8 +32,9 @@ import { applyManagedCatalogUpdate } from '../scripts/model-router-catalog.mjs';
|
|
|
32
32
|
import { cmpVersion } from '../scripts/stack-sync.mjs';
|
|
33
33
|
import { validateCoverageDirectory } from '../plugin/scripts/coverage-integrity.mjs';
|
|
34
34
|
import {
|
|
35
|
-
CONTINUITY_EVENTS,
|
|
36
35
|
continuityContractIds,
|
|
36
|
+
continuityHookId,
|
|
37
|
+
continuityRegistrations,
|
|
37
38
|
isAllowedContinuityRegistration,
|
|
38
39
|
} from '../plugin/scripts/continuity-hook-policy.mjs';
|
|
39
40
|
import {
|
|
@@ -68,6 +69,9 @@ import {
|
|
|
68
69
|
CONSOLE_RUNTIME_SURFACE, CONSOLE_RUNTIME_IDENTITY_FILE, consoleRuntimeDigest,
|
|
69
70
|
} from '../scripts/console-runtime-identity.mjs';
|
|
70
71
|
import { shellDiff as pluginShellDiff } from '../plugin/scripts/host-shell-boundary.mjs';
|
|
72
|
+
import {
|
|
73
|
+
writeInstalledRuntimeIdentity, recordCorpusTransportIdentity, isCorpusReleaseTag, rejectedReleasePath,
|
|
74
|
+
} from '../kb/corpus-release-identity.mjs';
|
|
71
75
|
|
|
72
76
|
// SEC-0010 #6 — the Ed25519 PUBLIC key is EMBEDDED here (not a separate file) so the installer's
|
|
73
77
|
// trust root travels with the installer code itself: an attacker who swaps the downloaded bundle
|
|
@@ -318,6 +322,32 @@ function fetchJson(url, redirects = 0) {
|
|
|
318
322
|
// --version <tag> forces a tag; --pin skips the network check and uses the bundled known-good tag.
|
|
319
323
|
// Any failure (offline / rate-limited / no releases) FALLS BACK to the pinned known-good Release,
|
|
320
324
|
// narrated clearly so the user knows exactly what happened.
|
|
325
|
+
/**
|
|
326
|
+
* Which asset of a Release actually holds the brain bundle.
|
|
327
|
+
*
|
|
328
|
+
* THE UNAMBIGUOUS SINGLE ZIP — the lesson kb/forge-update.mjs's resolveBundleUrl() already learned
|
|
329
|
+
* (issue #35) and this installer had not. Matching ONLY the conventional `ruvnet-brain.zip` means a
|
|
330
|
+
* release whose bundle asset is named anything else falls through to `fallbackUrl(tag)`, a URL
|
|
331
|
+
* assembled from that same conventional name — so the "fallback" is a guaranteed 404, not a
|
|
332
|
+
* download. The path that reaches it is a FRESH install: the one case with no brain already on disk
|
|
333
|
+
* to keep working. ADR-086 step 16 makes this reachable in practice, because `releases/latest` can
|
|
334
|
+
* now be a corpus release.
|
|
335
|
+
*
|
|
336
|
+
* A release carrying exactly one .zip is not ambiguous about which zip is the bundle. Two or more
|
|
337
|
+
* and guessing would be worse than the honest warning, so it falls through as before.
|
|
338
|
+
*
|
|
339
|
+
* Pure and exported so the choice is testable without a network round-trip.
|
|
340
|
+
* @returns {{url: string, origin: 'exact-name'|'single-zip'|'conventional-url', assetName: string|null}}
|
|
341
|
+
*/
|
|
342
|
+
export function resolveReleaseAsset({ tag, assets, assetName = ASSET_NAME }) {
|
|
343
|
+
const list = Array.isArray(assets) ? assets : [];
|
|
344
|
+
const exact = list.find((a) => a && a.name === assetName && a.browser_download_url);
|
|
345
|
+
if (exact) return { url: exact.browser_download_url, origin: 'exact-name', assetName: exact.name };
|
|
346
|
+
const zips = list.filter((a) => a && typeof a.name === 'string' && a.name.endsWith('.zip') && a.browser_download_url);
|
|
347
|
+
if (zips.length === 1) return { url: zips[0].browser_download_url, origin: 'single-zip', assetName: zips[0].name };
|
|
348
|
+
return { url: fallbackUrl(tag), origin: 'conventional-url', assetName: null };
|
|
349
|
+
}
|
|
350
|
+
|
|
321
351
|
async function resolveRelease() {
|
|
322
352
|
step(
|
|
323
353
|
'Finding the latest brain to install',
|
|
@@ -347,9 +377,10 @@ async function resolveRelease() {
|
|
|
347
377
|
const rel = await fetchJson(RELEASE_API);
|
|
348
378
|
const tag = rel && rel.tag_name;
|
|
349
379
|
if (!tag) throw new Error('latest Release has no tag_name');
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
380
|
+
const { url, origin, assetName } = resolveReleaseAsset({ tag, assets: rel.assets });
|
|
381
|
+
if (origin === 'single-zip') {
|
|
382
|
+
warn(`latest Release ${tag} has no ${ASSET_NAME} — using its only .zip asset, ${c.bold(assetName)}`);
|
|
383
|
+
} else if (origin === 'conventional-url') {
|
|
353
384
|
warn(`latest Release ${tag} has no ${ASSET_NAME} asset listed — using the conventional download URL`);
|
|
354
385
|
}
|
|
355
386
|
ok(`latest Release is ${c.bold(tag)}`);
|
|
@@ -485,7 +516,51 @@ export function copyLocalBundleInto(sourceDir, cacheDir) {
|
|
|
485
516
|
return copied;
|
|
486
517
|
}
|
|
487
518
|
|
|
488
|
-
|
|
519
|
+
// ── the trusted coverage validator lives beside the updater, and only the INSTALLER puts it there ──
|
|
520
|
+
// kb/forge-update.mjs (`loadTrustedCoverageValidator`) judges every downloaded bundle with
|
|
521
|
+
// `KB_DIR/coverage-integrity.mjs` and dies — "installed coverage validator is missing; re-run the
|
|
522
|
+
// current installer before self-update" — when it is absent. The bundle cannot be the source: the
|
|
523
|
+
// validator vets the bundle, so it must come from the signed npm package. Measured 2026-09-12: no
|
|
524
|
+
// production path had ever placed it (build-bundle's import walk cannot see the updater's dynamic
|
|
525
|
+
// load; this installer imported the module for its own checks and never copied it), so every
|
|
526
|
+
// 4.3.21 `--update` — the nightly included — died in the updater and fell back to a fresh install,
|
|
527
|
+
// which a private-overlay brain refuses. Idempotent, byte-compared, atomic; a symlink is replaced by
|
|
528
|
+
// a real file because a link is not a trusted regular file.
|
|
529
|
+
const TRUSTED_VALIDATOR_SOURCE = path.join(REPO_ROOT, 'plugin', 'scripts', 'coverage-integrity.mjs');
|
|
530
|
+
export function placeTrustedCoverageValidator(kbDir, { source = TRUSTED_VALIDATOR_SOURCE,
|
|
531
|
+
brainVersion = PACKAGE_VERSION } = {}) {
|
|
532
|
+
let bytes;
|
|
533
|
+
try { bytes = fs.readFileSync(source); }
|
|
534
|
+
catch (error) { throw new Error(`trusted coverage validator is missing from this package (${source}): ${error.message}`); }
|
|
535
|
+
const target = path.join(kbDir, 'coverage-integrity.mjs');
|
|
536
|
+
let existing = null;
|
|
537
|
+
try { existing = fs.lstatSync(target); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
538
|
+
const unchanged = existing && existing.isFile() && !existing.isSymbolicLink() && fs.readFileSync(target).equals(bytes);
|
|
539
|
+
if (!unchanged) {
|
|
540
|
+
const staged = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
541
|
+
fs.writeFileSync(staged, bytes, { mode: 0o644 });
|
|
542
|
+
if (existing && existing.isSymbolicLink()) fs.unlinkSync(target); // never write through a link
|
|
543
|
+
fs.renameSync(staged, target);
|
|
544
|
+
}
|
|
545
|
+
// STAMP THE APPROVED RUNTIME IN THE SAME BREATH AS PLACING ITS EXECUTABLES (ADR-086 step 16).
|
|
546
|
+
//
|
|
547
|
+
// This is the one moment where "which runtime is this brain running" is a measured fact rather
|
|
548
|
+
// than an assertion: the bytes were just written from THIS package, so the version and the hashes
|
|
549
|
+
// are recorded together and cannot drift. kb/forge-update.mjs re-hashes them before it will accept
|
|
550
|
+
// a corpus release — Dual: "Pinning survives only through enforced equality to the approved
|
|
551
|
+
// shipped runtime and its executable hashes. Copying current-main package.json or preserving a
|
|
552
|
+
// version string alone is insufficient." Re-stamped on every placement (install AND `--update`
|
|
553
|
+
// preflight), so an upgraded runtime immediately supersedes the previous pin.
|
|
554
|
+
const runtime = writeInstalledRuntimeIdentity(kbDir, { brainVersion });
|
|
555
|
+
return { action: unchanged ? 'unchanged' : (existing ? 'replaced' : 'placed'), path: target, runtimeIdentity: runtime };
|
|
556
|
+
}
|
|
557
|
+
/** `--update` preflight: place the validator only where an updater exists to consume it. */
|
|
558
|
+
export function ensureUpdaterPrerequisites(kbDir) {
|
|
559
|
+
if (!fs.existsSync(path.join(kbDir, 'forge-update.mjs'))) return { updater: false, validator: null };
|
|
560
|
+
return { updater: true, validator: placeTrustedCoverageValidator(kbDir) };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export async function unzipInto(zipPath, cacheDir, sourceDir = null, { releaseTag = null } = {}) {
|
|
489
564
|
step(
|
|
490
565
|
'Unpacking the brain into place',
|
|
491
566
|
'so the plugin finds forge-mcp-all.mjs and the vector stores right where it looks',
|
|
@@ -566,6 +641,25 @@ export async function unzipInto(zipPath, cacheDir, sourceDir = null) {
|
|
|
566
641
|
`The archive layout may have changed. Re-run, or report this at https://github.com/stuinfla/ruvnet-brain/issues`,
|
|
567
642
|
);
|
|
568
643
|
}
|
|
644
|
+
// A freshly installed brain must be able to self-update on its first night: the bundle never
|
|
645
|
+
// carries the trusted validator its own updater demands, so the installer lays it into the stage.
|
|
646
|
+
placeTrustedCoverageValidator(stageDir);
|
|
647
|
+
// THE TRANSPORT IDENTITY LANDS WITH THE BYTES. When `releases/latest` is a corpus release, the
|
|
648
|
+
// tag that authenticated these bytes (`corpus-sha256-<64 hex>`) is recorded in the STAGE, so the
|
|
649
|
+
// single rename below promotes the tree and its provenance together or promotes neither. Writing
|
|
650
|
+
// it after activation would leave a window where a crash yields an installed corpus this brain
|
|
651
|
+
// cannot name — and an unnamed corpus is a corpus the updater re-downloads every night.
|
|
652
|
+
// `brainVersion` / `releaseTag` are untouched: they are the runtime the bundle was built by, and
|
|
653
|
+
// a content address is not a version of that runtime (ADR-086 step 16, "keep runtime version
|
|
654
|
+
// distinct"). A non-corpus tag CLEARS any stale corpus tag — see recordCorpusTransportIdentity.
|
|
655
|
+
if (releaseTag) {
|
|
656
|
+
try { recordCorpusTransportIdentity(stageDir, { releaseTag }); }
|
|
657
|
+
catch (error) {
|
|
658
|
+
fs.rmSync(stageDir, { recursive: true, force: true });
|
|
659
|
+
die(`could not record the release identity into the staged brain (${error.message})`,
|
|
660
|
+
'The live brain was not touched. Re-run the installer.');
|
|
661
|
+
}
|
|
662
|
+
}
|
|
569
663
|
const stagedCoverage = validateCoverageDirectory(stageDir, { expectedVersion: PACKAGE_VERSION });
|
|
570
664
|
if (!stagedCoverage.valid) {
|
|
571
665
|
fs.rmSync(stageDir, { recursive: true, force: true });
|
|
@@ -647,7 +741,8 @@ export async function unzipInto(zipPath, cacheDir, sourceDir = null) {
|
|
|
647
741
|
`Candidate retained for inspection at ${stageDir}.`);
|
|
648
742
|
}
|
|
649
743
|
if (hadPrior) warn(`PRESERVED_UNCLASSIFIED: prior generation retained at ${preservedDir}. ` +
|
|
650
|
-
'
|
|
744
|
+
'The updater (kb/forge-update.mjs) releases it only once every byte is proven to survive in the live brain; ' +
|
|
745
|
+
'until then it stays, and repeated installs can grow disk usage.');
|
|
651
746
|
ok(`brain unpacked to ${cacheDir}`);
|
|
652
747
|
return { status: 'ACTIVATED', priorGeneration: hadPrior
|
|
653
748
|
? { status: 'PRESERVED_UNCLASSIFIED', path: preservedDir, automaticCleanupEligible: false }
|
|
@@ -1901,14 +1996,25 @@ export function classifyCodexLifecycle(plugin, listed = null) {
|
|
|
1901
1996
|
if (!plugin.enabled) return { state: 'disabled', plugin, hooks: [] };
|
|
1902
1997
|
if (!listed.ok) return { state: 'probe-failed', plugin, hooks: [], error: listed.error };
|
|
1903
1998
|
const groups = Array.isArray(listed.value?.data) ? listed.value.data : [];
|
|
1904
|
-
const hooks = groups.flatMap((group) => Array.isArray(group?.hooks) ? group.hooks : [])
|
|
1999
|
+
const hooks = groups.flatMap((group) => (Array.isArray(group?.hooks) ? group.hooks : [])
|
|
2000
|
+
.map((hook) => ({ ...hook, event: hook?.event ?? group?.event ?? null })))
|
|
1905
2001
|
.filter((hook) => hook?.pluginId === CODEX_PLUGIN_ID);
|
|
1906
2002
|
const errors = groups.flatMap((group) => Array.isArray(group?.errors) ? group.errors : []);
|
|
1907
2003
|
if (errors.length) {
|
|
1908
2004
|
return { state: 'missing-runtime-hooks', plugin, hooks, errors };
|
|
1909
2005
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
2006
|
+
// A REGISTERED CONTINUITY HOOK IS NOT A RETIRED ONE.
|
|
2007
|
+
//
|
|
2008
|
+
// This used to treat EVERY Brain-owned runtime registration as stale, because at the time the
|
|
2009
|
+
// policy permitted none on Codex. The doctor therefore reported the SessionStart restore and the
|
|
2010
|
+
// Stop continuation gate — the two handlers the policy itself requires — as "retired Brain
|
|
2011
|
+
// lifecycle hooks", told the user to upgrade, and returned a failing exit code for a correctly
|
|
2012
|
+
// wired machine. The authority on what belongs is the policy, so ask it instead of assuming zero.
|
|
2013
|
+
const conforming = hooks.filter((hook) => continuityHookId(hook?.command, hook?.event));
|
|
2014
|
+
const unexpected = hooks.filter((hook) => !continuityHookId(hook?.command, hook?.event));
|
|
2015
|
+
if (unexpected.length) return { state: 'unexpected-runtime-hooks', plugin, hooks: unexpected, errors };
|
|
2016
|
+
if (conforming.length === 0) return { state: 'inactive-by-design', plugin, hooks, errors };
|
|
2017
|
+
return { state: 'continuity-registered', plugin, hooks: conforming, errors };
|
|
1912
2018
|
}
|
|
1913
2019
|
|
|
1914
2020
|
export async function codexLifecycleStatus(options = {}) {
|
|
@@ -1930,6 +2036,16 @@ export function codexLifecycleGuidance(status) {
|
|
|
1930
2036
|
detail: 'Any Brain-owned runtime registration is stale and must not be trusted or executed.',
|
|
1931
2037
|
action: `Upgrade ${CODEX_PLUGIN_ID}, then start a fresh Codex session and re-run --doctor.`,
|
|
1932
2038
|
};
|
|
2039
|
+
case 'continuity-registered':
|
|
2040
|
+
return {
|
|
2041
|
+
healthy: true,
|
|
2042
|
+
intentional: true,
|
|
2043
|
+
summary: `Codex carries ${hookCount} declared Brain continuity hook${hookCount === 1 ? '' : 's'}.`,
|
|
2044
|
+
detail: 'Each one is named in plugin/hooks/hook-contracts.json. Codex capture is SessionEnd only:'
|
|
2045
|
+
+ ' a 2026-09-11 probe observed SessionStart, UserPromptSubmit and SessionEnd firing, and did not'
|
|
2046
|
+
+ ' observe Stop or PreCompact, so no capture handler was registered on those.',
|
|
2047
|
+
action: null,
|
|
2048
|
+
};
|
|
1933
2049
|
case 'inactive-by-design':
|
|
1934
2050
|
return {
|
|
1935
2051
|
healthy: true,
|
|
@@ -2054,22 +2170,31 @@ export function automaticHookRetirementStatus(root = REPO_ROOT, { scope = 'sourc
|
|
|
2054
2170
|
// Project-local host settings must remain empty. The package registries may carry only
|
|
2055
2171
|
// the two continuity handlers; every former gate is still retired.
|
|
2056
2172
|
const packageRegistry = relative === 'plugin/hooks/hooks.json' || relative === 'plugin/hooks/codex-hooks.json';
|
|
2057
|
-
|
|
2173
|
+
const host = relative === 'plugin/hooks/codex-hooks.json' ? 'codex' : 'claude';
|
|
2174
|
+
if (!packageRegistry || !isAllowedContinuityRegistration({ ...row, host })) registrations.push(row);
|
|
2058
2175
|
}
|
|
2059
2176
|
}
|
|
2060
2177
|
}
|
|
2061
2178
|
if (relative === 'plugin/hooks/hooks.json' || relative === 'plugin/hooks/codex-hooks.json') {
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2179
|
+
// PAIRS, NOT IDS. `session-snapshot` is legitimately registered at Stop, PreCompact and
|
|
2180
|
+
// SessionEnd, so "exactly one registration per id" is the wrong invariant; "exactly one per
|
|
2181
|
+
// (event, id), on the hosts that are proven to deliver that event" is the right one.
|
|
2182
|
+
const host = relative === 'plugin/hooks/codex-hooks.json' ? 'codex' : 'claude';
|
|
2183
|
+
const expected = continuityRegistrations(host);
|
|
2184
|
+
for (const spec of expected) {
|
|
2185
|
+
const groups = doc.hooks[spec.event] ?? [];
|
|
2186
|
+
const count = groups.flatMap((group) => (group?.hooks ?? []).map((hook) => ({ group, hook })))
|
|
2187
|
+
.filter(({ group, hook }) => String(group?.matcher ?? '') === spec.matcher
|
|
2188
|
+
&& isAllowedContinuityRegistration({ event: spec.event, matcher: group?.matcher, command: hook?.command, host })
|
|
2189
|
+
&& continuityHookId(hook?.command, spec.event)?.id === spec.id).length;
|
|
2190
|
+
if (count !== 1) {
|
|
2191
|
+
errors.push(`${relative}: continuity ${spec.event}:${spec.id} must have exactly one registration (found ${count})`);
|
|
2192
|
+
}
|
|
2070
2193
|
}
|
|
2071
2194
|
for (const event of Object.keys(doc.hooks)) {
|
|
2072
|
-
if (!
|
|
2195
|
+
if (!expected.some((spec) => spec.event === event)) {
|
|
2196
|
+
errors.push(`${relative}: legacy automatic event ${event} remains registered`);
|
|
2197
|
+
}
|
|
2073
2198
|
}
|
|
2074
2199
|
}
|
|
2075
2200
|
} catch (error) {
|
|
@@ -2080,12 +2205,19 @@ export function automaticHookRetirementStatus(root = REPO_ROOT, { scope = 'sourc
|
|
|
2080
2205
|
const contractsFile = 'plugin/hooks/hook-contracts.json';
|
|
2081
2206
|
const contracts = JSON.parse(fs.readFileSync(path.join(root, contractsFile), 'utf8'));
|
|
2082
2207
|
checkedFiles.push(contractsFile);
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
|
|
2208
|
+
const pairs = Array.isArray(contracts.contracts) ? contracts.contracts.map((c) => `${c?.event}:${c?.id}`) : [];
|
|
2209
|
+
const expectedPairs = continuityContractIds();
|
|
2210
|
+
if (pairs.length !== expectedPairs.length || expectedPairs.some((pair) => !pairs.includes(pair))) {
|
|
2211
|
+
errors.push(`${contractsFile}: contracts must list exactly the continuity handlers (${expectedPairs.join(', ')})`);
|
|
2086
2212
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2213
|
+
// One allowlist entry per DISTINCT (event, matcher): the matcher is a property of the event, so
|
|
2214
|
+
// three snapshot registrations on three events need three entries, not three copies of one.
|
|
2215
|
+
const expectedMatchers = [...new Set(continuityRegistrations().map((spec) => `${spec.event}:${spec.matcher}`))];
|
|
2216
|
+
const declaredMatchers = Array.isArray(contracts.matcherAllowlist)
|
|
2217
|
+
? contracts.matcherAllowlist.map((row) => `${row?.event}:${row?.matcher}`) : [];
|
|
2218
|
+
if (declaredMatchers.length !== expectedMatchers.length
|
|
2219
|
+
|| expectedMatchers.some((entry) => !declaredMatchers.includes(entry))) {
|
|
2220
|
+
errors.push(`${contractsFile}: matcherAllowlist must list exactly the continuity matchers (${expectedMatchers.join(', ')})`);
|
|
2089
2221
|
}
|
|
2090
2222
|
} catch (error) {
|
|
2091
2223
|
errors.push(`plugin/hooks/hook-contracts.json: ${error.message}`);
|
|
@@ -2141,17 +2273,21 @@ export function claudeInstalledHookRetirementStatus({ home = os.homedir(), plugi
|
|
|
2141
2273
|
}
|
|
2142
2274
|
}
|
|
2143
2275
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
command: hook?.command,
|
|
2150
|
-
|
|
2151
|
-
if (count !== 1)
|
|
2276
|
+
const expected = continuityRegistrations('claude');
|
|
2277
|
+
for (const spec of expected) {
|
|
2278
|
+
const count = (doc.hooks[spec.event] ?? [])
|
|
2279
|
+
.flatMap((group) => (group?.hooks ?? []).map((hook) => ({ group, hook })))
|
|
2280
|
+
.filter(({ group, hook }) => String(group?.matcher ?? '') === spec.matcher
|
|
2281
|
+
&& isAllowedContinuityRegistration({ event: spec.event, matcher: group?.matcher, command: hook?.command, host: 'claude' })
|
|
2282
|
+
&& continuityHookId(hook?.command, spec.event)?.id === spec.id).length;
|
|
2283
|
+
if (count !== 1) {
|
|
2284
|
+
errors.push(`installed Claude continuity ${spec.event}:${spec.id} must have exactly one registration (found ${count})`);
|
|
2285
|
+
}
|
|
2152
2286
|
}
|
|
2153
2287
|
for (const event of Object.keys(doc.hooks)) {
|
|
2154
|
-
if (!
|
|
2288
|
+
if (!expected.some((spec) => spec.event === event)) {
|
|
2289
|
+
errors.push(`installed Claude legacy automatic event ${event} remains registered`);
|
|
2290
|
+
}
|
|
2155
2291
|
}
|
|
2156
2292
|
}
|
|
2157
2293
|
} catch (error) {
|
|
@@ -2295,7 +2431,19 @@ async function smokeQuery(cacheDir) {
|
|
|
2295
2431
|
const secs = ((Date.now() - started) / 1000).toFixed(1);
|
|
2296
2432
|
const out = `${r.stdout || ''}`;
|
|
2297
2433
|
if (r.status !== 0 || !out.trim()) {
|
|
2298
|
-
|
|
2434
|
+
// NAME THE ACTUAL CAUSE, DO NOT GUESS A REASSURING ONE.
|
|
2435
|
+
//
|
|
2436
|
+
// This said "first-run model download or offline" unconditionally. It is one plausible cause
|
|
2437
|
+
// among several, asserted as though it had been checked — and it is the reassuring one, so a
|
|
2438
|
+
// crash, a timeout, and a missing module all read as "nothing is wrong, it will warm up".
|
|
2439
|
+
// Observed on this machine: a smoke query that produced no answer in 240s was reported as a
|
|
2440
|
+
// first-run download. spawnSync already tells us which it was; say that instead.
|
|
2441
|
+
const cause = r.error ? `could not launch the reader: ${r.error.message}`
|
|
2442
|
+
: r.signal === 'SIGTERM' ? `timed out after ${secs}s (240s limit) with no answer`
|
|
2443
|
+
: r.signal ? `the reader was killed by ${r.signal} after ${secs}s`
|
|
2444
|
+
: r.status !== 0 ? `the reader exited ${r.status} after ${secs}s`
|
|
2445
|
+
: `the reader exited 0 after ${secs}s but printed nothing`;
|
|
2446
|
+
warn(`no answer came back — ${cause}`);
|
|
2299
2447
|
// SHOW THE ACTUAL ERROR (issue #37 bug 2, Agentist-Elder, 2026-07-21).
|
|
2300
2448
|
//
|
|
2301
2449
|
// This captured stderr and then threw it away, so every hard failure — a crash, a missing
|
|
@@ -2315,7 +2463,9 @@ async function smokeQuery(cacheDir) {
|
|
|
2315
2463
|
for (const line of lines.slice(0, 12)) info(c.dim(` ${line.slice(0, 200)}`));
|
|
2316
2464
|
if (lines.length > 12) info(c.dim(` … ${lines.length - 12} more line(s)`));
|
|
2317
2465
|
}
|
|
2318
|
-
|
|
2466
|
+
// The reason travels with the verdict so the doctor's "Grounding NOT proven (<reason>)" line
|
|
2467
|
+
// names the real cause too, instead of the generic token.
|
|
2468
|
+
return { ran: true, grounded: false, reason: `no-answer: ${cause}`, secs, stderr: err.slice(0, 4000) };
|
|
2319
2469
|
}
|
|
2320
2470
|
|
|
2321
2471
|
const verifier = await loadCitationVerifier(cacheDir);
|
|
@@ -2613,11 +2763,16 @@ async function doctor() {
|
|
|
2613
2763
|
const v = verifyInstall(cacheDir);
|
|
2614
2764
|
const smoke = await smokeQuery(cacheDir);
|
|
2615
2765
|
const allGreen = v.repos > 0 && v.reader && v.mcp;
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2766
|
+
// ONE VERDICT, AND IT COMES AFTER ITS EVIDENCE.
|
|
2767
|
+
//
|
|
2768
|
+
// This line used to print "✓ Healthy." here, from `allGreen` — which reads only repos/reader/mcp —
|
|
2769
|
+
// and then a SECOND verdict, "✗ FAILING", printed ~120 lines later from the much wider `failed`.
|
|
2770
|
+
// A --doctor run genuinely emitted both, and exited 0. Two verdicts is not a cosmetic problem: a
|
|
2771
|
+
// reader stops at the first one, so the tool told people they were healthy while its own exit-code
|
|
2772
|
+
// logic had already decided otherwise. The single verdict is now emitted at the end, where every
|
|
2773
|
+
// input to `failed` exists; this position keeps only the installed-and-reachable READING.
|
|
2774
|
+
console.log(`\n ${allGreen ? c.green('Install: present and reachable.')
|
|
2775
|
+
: c.yellow('Install: incomplete — see the warnings above.')}`);
|
|
2621
2776
|
// Installed-and-reachable and actually-grounded are different claims. Keep them separate, so a
|
|
2622
2777
|
// healthy install can never be mistaken for proven grounding.
|
|
2623
2778
|
if (smoke.grounded === true) {
|
|
@@ -2736,8 +2891,11 @@ async function doctor() {
|
|
|
2736
2891
|
|| codexReadinessFailed
|
|
2737
2892
|
|| !hostConvergence.healthy
|
|
2738
2893
|
|| Boolean(rufloOperational && !rufloOperational.healthy);
|
|
2739
|
-
|
|
2740
|
-
|
|
2894
|
+
// THE ONE VERDICT. Always printed, always consistent with the exit code, never alongside another.
|
|
2895
|
+
if (failed) {
|
|
2896
|
+
console.log(`\n ${c.red('✗ FAILING')} — the warnings above are real. Re-run ${c.bold('npx ruvnet-brain')} to repair.`);
|
|
2897
|
+
} else {
|
|
2898
|
+
console.log(`\n ${c.green('✓ Healthy.')} The brain is installed, reachable, and its checks pass.`);
|
|
2741
2899
|
}
|
|
2742
2900
|
return failed ? 1 : 0;
|
|
2743
2901
|
}
|
|
@@ -2805,6 +2963,20 @@ function cmpTag(a, b) {
|
|
|
2805
2963
|
return A.pre > B.pre ? 1 : -1;
|
|
2806
2964
|
}
|
|
2807
2965
|
|
|
2966
|
+
/**
|
|
2967
|
+
* The CORPUS transport tag this brain last received, or null.
|
|
2968
|
+
*
|
|
2969
|
+
* Deliberately separate from installedBrainVersion(): that one answers "which runtime built the KB
|
|
2970
|
+
* on disk" (a semver), this one answers "which published corpus archive is on disk" (a content
|
|
2971
|
+
* address). Conflating them is the defect ADR-086 step 16 exists to fix.
|
|
2972
|
+
*/
|
|
2973
|
+
function installedCorpusTag(cacheDir) {
|
|
2974
|
+
try {
|
|
2975
|
+
const tag = JSON.parse(fs.readFileSync(path.join(cacheDir, 'SOURCE.json'), 'utf8')).corpusReleaseTag;
|
|
2976
|
+
return isCorpusReleaseTag(tag) ? tag : null;
|
|
2977
|
+
} catch { return null; }
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2808
2980
|
function installedBrainVersion(cacheDir) {
|
|
2809
2981
|
// Same read the telemetry ping uses: the bundle stamps its Release tag into SOURCE.json.
|
|
2810
2982
|
// "unknown" is honest for a locally-built or pre-stamping bundle — never guess a tag.
|
|
@@ -2909,7 +3081,11 @@ function feedbackHealthLines(cacheDir) {
|
|
|
2909
3081
|
return [
|
|
2910
3082
|
`${s.repos} repo stores on disk · reader ${s.reader ? 'ok' : 'MISSING'} · search_ruvnet ${s.mcp ? 'ok' : 'MISSING'} · plugin ${s.plugin ? 'ok' : 'NOT INSTALLED (no /rvbc)'}`,
|
|
2911
3083
|
`toolkit: Ruflo ${env.ruflo ? 'present' : 'not found'} · RuVector ${env.ruvector ? 'present' : 'not found'} · claude CLI ${env.claude ? 'present' : 'not found'}`,
|
|
2912
|
-
|
|
3084
|
+
// NOT called a "verdict": this reads only repos/reader/mcp, while --doctor's verdict also weighs
|
|
3085
|
+
// grounding, Codex wiring, nightly health, host convergence and the hook policy. Two lines both
|
|
3086
|
+
// labelled "verdict" that answer different questions can disagree in public, which is the exact
|
|
3087
|
+
// Healthy-and-FAILING confusion the doctor's single verdict was collapsed to remove.
|
|
3088
|
+
allGreen ? 'install reading: present and reachable' : 'install reading: incomplete — re-run npx ruvnet-brain',
|
|
2913
3089
|
];
|
|
2914
3090
|
}
|
|
2915
3091
|
|
|
@@ -3321,6 +3497,17 @@ function runUpdate() {
|
|
|
3321
3497
|
settleRefresh(1, { phase: 'source-enumeration' });
|
|
3322
3498
|
return;
|
|
3323
3499
|
}
|
|
3500
|
+
// The updater exists; make sure the trusted validator it will load exists too (package copy,
|
|
3501
|
+
// byte-compared). A failure here is reported, not fatal — the updater states the same absence
|
|
3502
|
+
// in its own words and the run then takes the documented fallback.
|
|
3503
|
+
try {
|
|
3504
|
+
const prerequisites = ensureUpdaterPrerequisites(kbDir);
|
|
3505
|
+
if (prerequisites.validator?.action !== 'unchanged') {
|
|
3506
|
+
info(c.dim(`trusted coverage validator ${prerequisites.validator.action} beside the updater`));
|
|
3507
|
+
}
|
|
3508
|
+
} catch (error) {
|
|
3509
|
+
warn(`could not place the trusted coverage validator (${error.message}); the updater will report what it finds`);
|
|
3510
|
+
}
|
|
3324
3511
|
info(c.dim("running the bundle's own self-updater (backs up first, re-verifies, never half-applies)…\n"));
|
|
3325
3512
|
// Relative filename + matching cwd — same launch convention as smokeQuery(); stdio:'inherit'
|
|
3326
3513
|
// streams the updater's narration live and unedited.
|
|
@@ -3667,6 +3854,12 @@ export function machineFootprint() {
|
|
|
3667
3854
|
const artifact = nightlyArtifact({ platform: process.platform, env: process.env });
|
|
3668
3855
|
if (artifact.kind === 'launchd') add('Nightly updater (LaunchAgent)', artifact.path, 'npx ruvnet-brain --disable-nightly');
|
|
3669
3856
|
add('Nightly scheduler registration', path.join(process.env.RUVNET_BRAIN_HOME || path.dirname(resolvedKbDir()), 'scheduler', 'registration.json'), 'npx ruvnet-brain --disable-nightly');
|
|
3857
|
+
// Only ever present after the updater refused an incompatible corpus release (ADR-086 step 16).
|
|
3858
|
+
// It lives BESIDE the KB rather than inside it so an exact-tree update cannot erase the memory of
|
|
3859
|
+
// the refusal — which is what keeps a refusal from being rediscovered, and re-downloaded, nightly.
|
|
3860
|
+
// Declared here because a file we write is a file we own and must be able to take back.
|
|
3861
|
+
add('Rejected-release memo (only after an incompatible corpus release)',
|
|
3862
|
+
rejectedReleasePath(resolvedKbDir()), 'npx ruvnet-brain --uninstall');
|
|
3670
3863
|
}
|
|
3671
3864
|
if (process.platform === 'darwin') {
|
|
3672
3865
|
add('Spend watchdog (LaunchAgent)', spendGuardPlistPath(), 'npx ruvnet-brain --disable-spend-guard');
|
|
@@ -3847,6 +4040,7 @@ function uninstallAll() {
|
|
|
3847
4040
|
// owns (settings.json entries, the MCP registration) and the Claude Code plugin itself are not
|
|
3848
4041
|
// ours to delete, so they are handed over as commands.
|
|
3849
4042
|
const AUTO = new Set(['Brain bundle (knowledge base)', 'Nightly updater (LaunchAgent)', 'Nightly scheduler registration',
|
|
4043
|
+
'Rejected-release memo (only after an incompatible corpus release)',
|
|
3850
4044
|
'Spend watchdog (LaunchAgent)', 'Spend watchdog script', 'CLAUDE.md block (6 lines, between markers)',
|
|
3851
4045
|
'Model-router files', 'Status-bar version script', 'Status-bar preference', 'Usage-counts preference',
|
|
3852
4046
|
// Two gaps closed here: the statusLine KEY is now removable in place (we know exactly what we
|
|
@@ -4471,7 +4665,11 @@ export function classifyRufloOperationalHealth({ status = '', memory = '', metri
|
|
|
4471
4665
|
|
|
4472
4666
|
function probeRufloOperationalHealth() {
|
|
4473
4667
|
const run = (args) => {
|
|
4474
|
-
|
|
4668
|
+
// Every `ruflo` invocation auto-starts a project background daemon unless this is set
|
|
4669
|
+
// (verified live: ~/.npm-global/lib/node_modules/ruflo/node_modules/@claude-flow/cli/dist/src/
|
|
4670
|
+
// services/daemon-autostart.js:85) — a read-only health probe must not leave one running.
|
|
4671
|
+
const result = spawnSync('ruflo', args, { cwd: process.cwd(), encoding: 'utf8', timeout: 10_000,
|
|
4672
|
+
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' } });
|
|
4475
4673
|
return `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
4476
4674
|
};
|
|
4477
4675
|
return classifyRufloOperationalHealth({
|
|
@@ -5169,21 +5367,33 @@ the installer reports that boot-level declarations changed.
|
|
|
5169
5367
|
? (resolvedRelease.tag_name || resolvedRelease.tag || null)
|
|
5170
5368
|
: null;
|
|
5171
5369
|
} catch { latestTag = null; }
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
//
|
|
5175
|
-
//
|
|
5176
|
-
//
|
|
5177
|
-
//
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5370
|
+
// A CORPUS RELEASE IS NOT ORDERABLE AGAINST A SEMVER (ADR-086 step 16). `corpus-sha256-<64 hex>`
|
|
5371
|
+
// is a content address; cmpTag() parses its leading segment with parseInt, gets NaN, floors it
|
|
5372
|
+
// to 0, and concludes any installed version is NEWER — so a corpus release would be reported as
|
|
5373
|
+
// "installed is NEWER than the latest release" and silently skipped, forever. Compare the
|
|
5374
|
+
// installed corpus tag against the offered corpus tag instead: equal = current, different =
|
|
5375
|
+
// behind, never "ahead", because content addresses have no order.
|
|
5376
|
+
if (isCorpusReleaseTag(latestTag)) {
|
|
5377
|
+
const installedCorpus = installedCorpusTag(cacheDir);
|
|
5378
|
+
ahead = false;
|
|
5379
|
+
staleSkip = installedCorpus !== latestTag;
|
|
5380
|
+
} else {
|
|
5381
|
+
const norm = (v) => (v == null || v === 'unknown' ? null : String(v).replace(/^v/, ''));
|
|
5382
|
+
const a = norm(installedTag), b = norm(latestTag);
|
|
5383
|
+
// BEHIND => download. SAME => skip. AHEAD => skip, and say so honestly.
|
|
5384
|
+
//
|
|
5385
|
+
// This was a bare `a !== b`, which treats "newer than the latest release" as staleness. Anyone
|
|
5386
|
+
// running a pre-release or dev build — or who simply updated in the window before a release was
|
|
5387
|
+
// cut — was told "Brain is out of date" and pushed through a 2 GB download that would DOWNGRADE
|
|
5388
|
+
// them. Found 2026-07-22 the moment this repo's own version moved to 3.5.0-dev ahead of the
|
|
5389
|
+
// 3.4.22-dev release: the installer immediately declared its own newest brain stale.
|
|
5390
|
+
//
|
|
5391
|
+
// stack-sync.mjs has modelled AHEAD as legal from the start ("AHEAD is legal and produces NO
|
|
5392
|
+
// recommendation — that modelling choice is what makes the alpha-vs-latest downgrade war
|
|
5393
|
+
// structurally impossible"). The installer never learned the same lesson. It has now.
|
|
5394
|
+
ahead = Boolean(a && b && cmpTag(a, b) > 0);
|
|
5395
|
+
staleSkip = Boolean(b && (a === null || (a !== b && !ahead)));
|
|
5396
|
+
}
|
|
5187
5397
|
}
|
|
5188
5398
|
|
|
5189
5399
|
if (alreadyInstalled && !FLAG_FORCE && !staleSkip) {
|
|
@@ -5242,7 +5452,12 @@ the installer reports that boot-level declarations changed.
|
|
|
5242
5452
|
ok(reason);
|
|
5243
5453
|
}
|
|
5244
5454
|
}
|
|
5245
|
-
|
|
5455
|
+
// The tag is only carried when it came from a genuine `latest` resolution — a pinned/offline
|
|
5456
|
+
// fallback is not evidence of which release these bytes are, and recording a guess would be
|
|
5457
|
+
// worse than recording nothing.
|
|
5458
|
+
await unzipInto(zipPath, cacheDir, sourceDir, {
|
|
5459
|
+
releaseTag: release && release.source === 'latest' ? (release.tag_name || release.tag || null) : null,
|
|
5460
|
+
});
|
|
5246
5461
|
const brainProfile = readBrainProfile();
|
|
5247
5462
|
if (brainProfile !== 'complete') {
|
|
5248
5463
|
const scoped = applyBrainProfile(cacheDir, brainProfile);
|