ruvnet-brain 4.3.1 → 4.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +12 -13
  2. package/bin/install.mjs +18 -8
  3. package/console/app.js +4 -4
  4. package/data/model-catalog.json +3 -3
  5. package/kb/verify-citation.mjs +32 -5
  6. package/package.json +13 -9
  7. package/plugin/.claude-plugin/plugin.json +1 -1
  8. package/plugin/.codex-plugin/plugin.json +1 -1
  9. package/plugin/mcp/managed-cli-interface.mjs +4 -2
  10. package/plugin/scripts/codex-hook-adapter.mjs +12 -7
  11. package/plugin/scripts/codex-hook-events.mjs +39 -0
  12. package/plugin/scripts/coverage-integrity.mjs +4 -4
  13. package/plugin/scripts/hook-shim.mjs +9 -7
  14. package/plugin/scripts/learn-capture.sh +20 -1
  15. package/plugin/scripts/learn-flush.mjs +7 -1
  16. package/plugin/scripts/lesson-gate.mjs +17 -2
  17. package/plugin/scripts/lesson-hooks.sh +17 -2
  18. package/plugin/scripts/session-start-core.mjs +47 -11
  19. package/plugin/skills/release-proof/SKILL.md +19 -10
  20. package/plugin/skills/release-proof/references/receipt-contract.md +18 -8
  21. package/scripts/brain-stamp-resolve.mjs +22 -0
  22. package/scripts/brain-stamp.mjs +5 -3
  23. package/scripts/build-bundle.mjs +41 -4
  24. package/scripts/candidate-host-evidence.mjs +9 -2
  25. package/scripts/ci/step-watchdog.mjs +86 -0
  26. package/scripts/convergence-manifest.mjs +66 -0
  27. package/scripts/distill-project.mjs +22 -4
  28. package/scripts/eval-brain.mjs +13 -3
  29. package/scripts/execution-policy.mjs +111 -0
  30. package/scripts/execution-preflight.mjs +18 -0
  31. package/scripts/git-hooks/pre-push +16 -0
  32. package/scripts/health-repair.mjs +5 -1
  33. package/scripts/host-install-matrix.mjs +247 -40
  34. package/scripts/integration-evidence.mjs +73 -0
  35. package/scripts/nightly-wrapper.sh +4 -3
  36. package/scripts/onboarding-console.mjs +3 -3
  37. package/scripts/prepublication-evidence.mjs +174 -0
  38. package/scripts/protected-release-invocation.mjs +32 -4
  39. package/scripts/public-verification-aggregate.mjs +27 -54
  40. package/scripts/public-verification-inputs.mjs +5 -3
  41. package/scripts/qa-runner.mjs +72 -0
  42. package/scripts/qe/agentic-qe-4.3.mjs +195 -0
  43. package/scripts/qe/aggregate-4.3.mjs +42 -0
  44. package/scripts/record-lesson.mjs +28 -6
  45. package/scripts/refresh-model-catalog.mjs +32 -3
  46. package/scripts/release-authority.mjs +19 -7
  47. package/scripts/release-convergence-watchdog.mjs +10 -22
  48. package/scripts/release-projection.mjs +76 -32
  49. package/scripts/release-transaction.mjs +1 -1
  50. package/scripts/release.mjs +9 -15
  51. package/scripts/retrieval-canary.mjs +14 -6
  52. package/scripts/set-version.mjs +31 -0
  53. package/scripts/source-coverage.mjs +11 -2
  54. package/scripts/staged-host-verifier.mjs +2 -2
  55. package/scripts/verify-channels.mjs +1 -1
  56. package/scripts/wired-check.mjs +1 -0
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # 🧠 RuvNet Brain
6
6
 
7
- ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.3.1 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.1-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
7
+ ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.3.8 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.8-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
8
8
 
9
9
  **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.**
10
10
 
@@ -35,7 +35,7 @@
35
35
  > **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:
36
36
  > - **`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.
37
37
  > - **`installer (npm)`** (badge above) — the `npx ruvnet-brain` setup script. Read live from the [npm registry](https://www.npmjs.com/package/ruvnet-brain). Only moves when the installer script itself changes — rare.
38
- > - **Brain Release** (the downloadable knowledge bundle, linked from the "download" badge above) — always resolves to [`releases/latest`](https://github.com/stuinfla/ruvnet-brain/releases/latest). It moves only when the protected exact-SHA release workflow accepts rebuilt knowledge bytes — separate again from the two above.
38
+ > - **Brain Release** (the downloadable knowledge bundle, linked from the "download" badge above) — always resolves to [`releases/latest`](https://github.com/stuinfla/ruvnet-brain/releases/latest) (the nightly publishes fresh bundles as the corpus grows). Only moves when the underlying knowledge base is rebuilt — separate again from the two above.
39
39
  > - **On an old version? One line makes you current — and, with `--auto`, keeps you current forever:**
40
40
  > ```
41
41
  > npx ruvnet-brain@latest --update --auto
@@ -56,17 +56,16 @@
56
56
 
57
57
  ---
58
58
 
59
- ## What's new in 4.3 — it loads what rUv ships, without being asked
59
+ ## Earlier — 4.2 loaded what rUv ships, without being asked
60
60
 
61
- **The corpus gained an explicit new-repository ingestion path.** Until 4.2 nothing ingested a new
62
- repo. `brain-stamp.mjs` measured the gap, but nothing consumed it until
63
- `scripts/ingest-new-repos.mjs` shipped. Its mutating mode is now deliberately human-run from a clean
64
- linked worktree; the former primary-checkout scheduler was retired rather than allowed to trade
65
- corpus freshness for uncontrolled source changes.
61
+ **The corpus stopped drifting behind the org.** Until 4.2 nothing ever ingested a new repo: the
62
+ nightly refreshed lessons, health and proofs and contained *zero* ingestion, so a repo entered the
63
+ brain only when a human typed the command. `brain-stamp.mjs` had been measuring that gap every
64
+ night, but nothing consumed it until the ingestion loop shipped.
66
65
 
67
- - **187 stores, up from 69.** Content-bearing rUv repositories can be pulled newest-first through
68
- `scripts/ingest-new-repos.mjs`. Empty repos (`size=0KB`) are skipped
69
- rather than retried forever — a permanent repeated failure that is actually correct behaviour
66
+ - **187 stores, up from 69.** Everything rUv ships that has content, pulled in and kept level by
67
+ `scripts/ingest-new-repos.mjs` running nightly, newest-first. Empty repos (`size=0KB`) are skipped
68
+ rather than retried forever — a permanent nightly failure that is actually correct behaviour
70
69
  trains you to ignore the failure line, which is how a real one would hide inside it.
71
70
  - **174 capability cards, up from 39.** Ingesting a repo is not the same as making it reachable: a
72
71
  store with no card is *dark* — valid bytes no by-description query can find. Cards are written
@@ -80,7 +79,7 @@ corpus freshness for uncontrolled source changes.
80
79
  agentdb's binding, so `lesson-bridge --apply` and `learning-replay` wrote into a silent
81
80
  non-persistent fallback. It now resolves an ABI-matched interpreter and fails loudly instead.
82
81
 
83
- ## What's new in 4.3 — it anticipates, and it learns whether it was right
82
+ ## Earlier — 4.2 anticipated, and learned whether it was right
84
83
 
85
84
  **Building toward L4/L5 (3.9.x, dev).** The mechanisms for the top two rungs of the proactivity
86
85
  ladder are built and wired — but they are **not yet verified to 4.0's bar**, which requires all five
@@ -413,7 +412,7 @@ You install once. After that, three mechanisms keep you on the current brain wit
413
412
  `🧠 RuvNet Brain jumped in · guidance only, no source read · v3.4.18-dev`
414
413
  An unearned citation is worse than no citation, so the line may only name a path the tools genuinely returned — and on a prompt where nothing fires, it stays silent rather than manufacture a receipt. The version shown is the one **actually loaded in memory** for this session; if a newer one is staged awaiting a restart, the line says so plainly (`… vX staged, restart to load`). So you never have to wonder whether the brain is on, which version is acting, or whether an answer was grounded or guessed.
415
414
 
416
- - **Separated overnight paths.** The Dream Machine runs evidence-only evaluation with `autoMerge:false`; the optional `com.ruvnet.brain-update` LaunchAgent updates one installed cache from an already-published bundle. The former `com.ruvnet.brain-nightly` source writer was retired on 2026-08-22 because it accumulated generated changes in the primary developer checkout. Author rebuilds are now explicit, clean linked-worktree operations; `self-update.mjs --publish` is refused and only the protected exact-SHA workflow may release. See [Nightly refresh, evaluation, and author rebuilds](docs/NIGHTLY-REFRESH.md).
415
+ - **Nightly publish → `releases/latest` chain** (the protected release workflow). The nightly rebuilds only the repos whose upstream changed, and **if anything was rebuilt** it bumps the product version, cuts a GitHub Release, and advances [`releases/latest`](https://github.com/stuinfla/ruvnet-brain/releases/latest). Plugin and knowledge bundle move under **one** version number, so the heartbeat above picks up both automatically. The exact author-vs-end-user schedules, incremental algorithm, failure behavior, and hosting recommendation are documented in [Nightly refresh and publish](docs/NIGHTLY-REFRESH.md). The retired primary-checkout LaunchAgent is not part of the supported path.
417
416
 
418
417
  ---
419
418
 
package/bin/install.mjs CHANGED
@@ -2245,16 +2245,17 @@ async function doctor() {
2245
2245
  hookResult = await runSelfCheck({ installState: { repos: v.repos, reader: v.reader, mcp: v.mcp } });
2246
2246
  }
2247
2247
 
2248
- // ── THE PERSISTED GROUNDING VERDICT (ADR-058 §D8) — read-only, never re-derived here ────────────
2249
- // bin/install.mjs's own install run is the ONLY writer (right after its real smoke query), so a
2250
- // failed smoke stays non-fatal there. `--doctor` is different: it is the command someone runs
2251
- // SPECIFICALLY TO ASK whether the install is healthy, so this is the one place an unresolved
2252
- // "unproven" verdict DOES gate the exit code — without doctor() re-running a second live query
2253
- // (the live smoke result printed above already updates the SAME file the next real install or
2254
- // search_ruvnet touches; this just reads back whatever the most recent real attempt recorded).
2248
+ // ── THE PERSISTED GROUNDING VERDICT (ADR-058 §D8) — synchronize stronger live proof first ───────
2249
+ // A failed install smoke stays non-fatal there. `--doctor` is different: it is the command someone
2250
+ // runs SPECIFICALLY TO ASK whether the install is healthy, so this is the one place an unresolved
2251
+ // "unproven" verdict DOES gate the exit code. Its successful live citation proof above must clear
2252
+ // an older failure before this read; otherwise one invocation can print both PROVEN and UNPROVEN.
2255
2253
  let groundingUnprovenPersisted = false;
2256
2254
  try {
2257
2255
  const mod = await import(new URL('../scripts/selfcheck.mjs', import.meta.url).href);
2256
+ if (smoke.grounded === true) {
2257
+ mod.writeInstallState({ grounding: 'proven', reason: null, clearedBy: 'doctor-live-proof' });
2258
+ }
2258
2259
  groundingUnprovenPersisted = mod.groundingUnproven(mod.readInstallState());
2259
2260
  if (groundingUnprovenPersisted) {
2260
2261
  console.log(` ${c.yellow('! Grounding UNPROVEN')} (recorded at ${c.bold(mod.installStatePath())}).`);
@@ -2811,7 +2812,16 @@ function runUpdate() {
2811
2812
  console.error(` If you believe a newer build exists, check: ${c.bold('node forge-update.mjs --check')} in ${kbDir}`);
2812
2813
  process.exit(outcome.exitCode);
2813
2814
  }
2814
- if (outcome.fallback) {
2815
+ if (outcome.fallback && FLAG_HOST_SYNC_ONLY) {
2816
+ // Host synchronization has a narrower contract than a full update: it must converge the
2817
+ // executable plugin/spine to the published package even when an optional large KB asset is
2818
+ // missing. The old path fell through to a fresh install here, which required the same missing
2819
+ // zip and stranded every host on its previous generation. Keep the KB failure visible, but
2820
+ // continue to the host-sync transaction; the published-surface/release gates still fail the
2821
+ // release until the signed KB asset exists.
2822
+ warn("the knowledge bundle could not refresh; continuing with executable host synchronization only");
2823
+ updateStatus = 0;
2824
+ } else if (outcome.fallback) {
2815
2825
  warn("\nthe bundle's own updater couldn't complete — falling back to a fresh install of the latest Release (this always works)…\n");
2816
2826
  const self = fileURLToPath(import.meta.url);
2817
2827
  const fr = spawnSync(process.execPath, [self, '--force'], { stdio: 'inherit',
package/console/app.js CHANGED
@@ -3119,10 +3119,10 @@ async function bpFlip(next) {
3119
3119
  has to wonder again whether a piece is missing or merely elsewhere.
3120
3120
 
3121
3121
  THE RULE THAT KEEPS IT HONEST is user-settings.mjs's own: a switch is not shipped until something
3122
- reads it. The remaining schema keys (learningScope, autoApply, newProjectDefaults) exist in the
3123
- settings store and are deliberately NOT rendered as controls, because nothing enforces them yet —
3124
- a row of checkboxes that govern nothing is the single fastest way to make every other switch on
3125
- this page untrustworthy. */
3122
+ reads it. The ordinary schema keys (learningScope, advocacy, autoApply, newProjectDefaults) are
3123
+ rendered by the shared Settings form below and saved through the user-settings writer. Only
3124
+ controls with a real runtime consumer are included; a row that governs nothing would make every
3125
+ other switch on this page untrustworthy. */
3126
3126
  function bpParts(bp) {
3127
3127
  const off = !!bp.off;
3128
3128
  const part = (title, chipText, tone, ...text) => el('div', { class: 'bp-part' },
@@ -4,7 +4,7 @@
4
4
  "generated": "2026-07-15",
5
5
  "schema_version": 1,
6
6
  "sources": {
7
- "prices": "OpenRouter /api/v1/models live catalog, pulled 2026-07-15 (in/out USD per Mtok).",
7
+ "prices": "OpenRouter /api/v1/models live catalog, pulled 2026-09-04 (in/out USD per Mtok).",
8
8
  "rankings": "Artificial Analysis Intelligence Index (artificialanalysis.ai) + Arena/LMArena (arena.ai) — the ONLY independent evaluators carrying current-generation models as of 2026-07-15; each figure cross-verified twice.",
9
9
  "provenance_rule": "rUv ADR-206: vendor-reported scores are optimistic and harness-confounded — trust independent (AA/Arena) numbers, treat vendor self-scaffold SWE-bench/LiveCodeBench figures as noisy features, never as truth.",
10
10
  "benchmark_lag": "The canonical hard coding benchmarks (SWE-bench Verified standardized harness, LiveCodeBench, Aider polyglot) were ALL months stale on 2026-07-15 and carry NONE of these models. The '88.6% / 95% SWE-bench' figures in the press are vendor self-scaffold scores, not the standardized harness — excluded here.",
@@ -55,8 +55,8 @@
55
55
  ],
56
56
  "frontier": {
57
57
  "model": "openai/gpt-5.6-sol",
58
- "in": 2.5,
59
- "out": 15,
58
+ "in": 2,
59
+ "out": 10,
60
60
  "released": "2026-07-09",
61
61
  "rank": "AA Intelligence #2 (59) · AA Coding Index leader (80) · Terminal-Bench 2.1 SOTA · ~1/3 Fable 5's cost/task",
62
62
  "source": "independent (AA)"
@@ -24,23 +24,50 @@ import fs from 'node:fs';
24
24
  import path from 'node:path';
25
25
  import readline from 'node:readline';
26
26
 
27
- /** Parse the reader's stdout into structured citations. Never throws; unparseable input → []. */
27
+ /**
28
+ * Parse the reader's stdout into structured citations. Never throws; unparseable input → [].
29
+ *
30
+ * The reader dumps each hit's full document body inline (`forge-ask-all.mjs`'s "----- full
31
+ * document -----" section), unescaped. A retrieved document can itself legitimately CONTAIN text
32
+ * shaped exactly like this format — this very file's own header comment is an example, and so is
33
+ * any doc, ADR, or transcript that quotes or discusses the reader's output. Two guards keep such
34
+ * look-alike text from being parsed as a real citation: (1) a block's `path`/`title` are read only
35
+ * from the span between its own header and the NEXT header, never past it, so a citation missing
36
+ * one is not silently filled in from something appearing later in the dump; (2) real hits are
37
+ * numbered `#1, #2, …` strictly in order with no repeats — embedded example text does not continue
38
+ * that sequence, so any header whose rank isn't exactly the next expected one is skipped. This is
39
+ * not airtight against a document engineered to predict and spoof the exact next rank (tracked as
40
+ * an open item, not solved here); it closes the realistic case this repo's own docs demonstrate.
41
+ */
28
42
  export function parseCitations(stdout) {
29
43
  const out = [];
30
44
  const text = String(stdout ?? '');
31
45
  const blockRe = /^#(\d+)\s+repo=(\S+)(?:\s+ce=(-?[\d.]+))?(?:\s+vec=(-?[\d.]+))?(?:\s+kind=(\S+))?/gm;
46
+ const nextHeaderRe = /^#\d+\s+repo=\S+/gm;
32
47
  let m;
48
+ let expectedRank = 1;
33
49
  while ((m = blockRe.exec(text)) !== null) {
34
- const rest = text.slice(m.index);
35
- const pathM = /^path\s*:\s*(.+)$/m.exec(rest);
36
- const titleM = /^title\s*:\s*(.+)$/m.exec(rest);
50
+ const rank = Number(m[1]);
51
+ if (rank !== expectedRank) continue; // out-of-sequence header: a look-alike, not a real hit
52
+ const blockStart = m.index + m[0].length;
53
+ nextHeaderRe.lastIndex = blockStart;
54
+ const next = nextHeaderRe.exec(text);
55
+ const block = text.slice(blockStart, next ? next.index : text.length);
56
+ const pathM = /^path\s*:\s*(.+)$/m.exec(block);
57
+ const titleM = /^title\s*:\s*(.+)$/m.exec(block);
58
+ // Only a block that actually resolves to a path fills this rank slot. Advancing on rank match
59
+ // alone (before this check) let a headerless-of-path look-alike fragment (e.g. an incidental
60
+ // "#N repo=..." mention with no path/title following) consume the slot, permanently rejecting
61
+ // the REAL citation at that rank when it appeared later in the stream — a false negative on a
62
+ // genuinely grounded answer, worse than the fabrication this rank check exists to prevent.
37
63
  if (!pathM) continue;
64
+ expectedRank = rank + 1;
38
65
  const repo = m[2];
39
66
  const fullPath = pathM[1].trim();
40
67
  // Strip the repo prefix the reader adds, so the remainder can be matched against the store.
41
68
  const docPath = fullPath.startsWith(`${repo}/`) ? fullPath.slice(repo.length + 1) : fullPath;
42
69
  out.push({
43
- rank: Number(m[1]),
70
+ rank,
44
71
  repo,
45
72
  ce: m[3] !== undefined ? Number(m[3]) : null,
46
73
  vec: m[4] !== undefined ? Number(m[4]) : null,
package/package.json CHANGED
@@ -1,27 +1,36 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.1",
3
+ "version": "4.3.8",
4
4
  "description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ruvnet-brain": "bin/install.mjs"
8
8
  },
9
9
  "scripts": {
10
+ "prepublishOnly": "node scripts/protected-release-invocation.mjs --prepublish-only",
10
11
  "test": "node plugin/test/run-tests.mjs",
11
12
  "release:proof": "node scripts/release-proof.mjs",
12
13
  "benchmark:brain50": "node scripts/brain-latency-50.mjs",
13
14
  "benchmark:novice50": "node scripts/brain-novice-50.mjs",
14
15
  "version:check": "node scripts/sync-version.mjs --check",
16
+ "version:set": "node scripts/set-version.mjs",
17
+ "qa:pr": "node scripts/qa-runner.mjs",
18
+ "qa:release": "node scripts/qa-runner.mjs --release",
19
+ "release:authority": "node scripts/release-authority.mjs",
20
+ "card-from-source": "node scripts/card-from-source.mjs",
21
+ "release-abort-stale": "node scripts/release-abort-stale.mjs",
15
22
  "claims:verify": "node scripts/claims-verify.mjs",
16
23
  "claims:fix": "node scripts/claims-verify.mjs --fix",
17
24
  "version:sync": "node scripts/sync-version.mjs",
25
+ "convergence:write": "node scripts/convergence-manifest.mjs --write",
26
+ "convergence:check": "node scripts/convergence-manifest.mjs",
27
+ "execution-policy:check": "node scripts/execution-policy.mjs '{\"action\":\"delegate\",\"description\":\"architecture audit\",\"nativeHosts\":[\"codex\"]}'",
18
28
  "test:unit": "vitest run tests/unit",
19
29
  "test:mesh": "vitest run tests/mesh",
20
30
  "test:mutation": "vitest run tests/mutation",
21
31
  "test:regression": "vitest run tests/regression",
22
32
  "qe:ux": "node scripts/qe/ux-suite.mjs",
23
33
  "test:cov": "vitest run tests/unit --coverage",
24
- "test:release:preflight": "vitest run tests/unit/public-verification-inputs.test.mjs",
25
34
  "test:all": "npm run test:unit && npm run test:mesh && npm run test:mutation && npm run test:regression && npm run test:integration && npm test",
26
35
  "metaharness:receipts": "node scripts/metaharness-receipts.mjs",
27
36
  "route:cheap": "node scripts/route-cheap.mjs",
@@ -35,9 +44,6 @@
35
44
  "eval:top100": "node scripts/top100-benchmark.mjs",
36
45
  "gists:index": "node scripts/ingest-gists.mjs --index-only",
37
46
  "gists:sync": "node scripts/ingest-gists.mjs && node kb/forge-big.mjs both --dir kb --name ruv-gists",
38
- "gists:rebuild-receipts": "node scripts/rebuild-gists-from-receipts.mjs",
39
- "cards:from-source": "node scripts/card-from-source.mjs",
40
- "release:abort-stale": "node scripts/release-abort-stale.mjs",
41
47
  "test:integration": "vitest run tests/integration",
42
48
  "substitution:check": "node scripts/no-silent-substitution.mjs",
43
49
  "catalog:verify": "node scripts/verify-model-catalog.mjs",
@@ -51,8 +57,6 @@
51
57
  "learning:replay:dry": "node scripts/learning-replay.mjs --dry-run",
52
58
  "wired:check": "node scripts/wired-check.mjs --check",
53
59
  "doc:currency": "node scripts/doc-currency.mjs --check",
54
- "integrity:trace:check": "node scripts/product-integrity-contract.mjs --check-markdown docs/reviews/adr-072-traceability.md",
55
- "integrity:trace:json": "node scripts/product-integrity-contract.mjs",
56
60
  "status:check": "node scripts/status-honesty.mjs",
57
61
  "cap:collect": "node scripts/rerank-cap-eval.mjs --collect",
58
62
  "cap:report": "node scripts/rerank-cap-eval.mjs --report",
@@ -108,7 +112,7 @@
108
112
  "bugs": "https://github.com/stuinfla/ruvnet-brain/issues",
109
113
  "author": "Stuart Kerr",
110
114
  "devDependencies": {
111
- "@metaharness/darwin": "~0.8.0",
115
+ "@metaharness/darwin": "~0.9.2",
112
116
  "@ruvector/rvf": "^0.3.4",
113
117
  "@vitest/coverage-v8": "^4.1.10",
114
118
  "playwright": "^1.62.0",
@@ -116,6 +120,6 @@
116
120
  },
117
121
  "dependencies": {
118
122
  "@metaharness/flywheel": "^0.1.7",
119
- "@metaharness/router": "^0.3.2"
123
+ "@metaharness/router": "^0.4.0"
120
124
  }
121
125
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
3
  "description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
4
- "version": "4.3.1",
4
+ "version": "4.3.8",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.1",
3
+ "version": "4.3.8",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { loadRuntimePreferences, runtimeChildEnv } from '../scripts/runtime-preferences.mjs';
6
+ import { projectDirectory } from '../scripts/project-identity.mjs';
6
7
  import { recordManagedCliObservation, recordRegistryLatestObservation } from '../scripts/capability-claim-evidence.mjs';
7
8
 
8
9
  export const MANAGED_EXECUTABLES = Object.freeze([
@@ -261,6 +262,7 @@ export async function callManagedCli(toolName, args, env = process.env, fetchImp
261
262
  try {
262
263
  const executable = assertExecutable(args?.executable);
263
264
  const argv = literalArgv(args?.argv ?? []);
265
+ const projectRoot = env.RUVNET_BRAIN_PROJECT_DIR || projectDirectory({ env });
264
266
 
265
267
  if (toolName === 'ruvnet_registry_latest') {
266
268
  const packageName = REGISTRY_PACKAGES[executable];
@@ -299,7 +301,7 @@ export async function callManagedCli(toolName, args, env = process.env, fetchImp
299
301
  isError: true,
300
302
  };
301
303
  }
302
- const policy = loadRuntimePreferences({ env, cwd: env.RUVNET_BRAIN_PROJECT_DIR || process.cwd() });
304
+ const policy = loadRuntimePreferences({ env, cwd: projectRoot });
303
305
  if (executable === 'agentic-flow' && policy.values.routing !== 'auto') {
304
306
  return {
305
307
  content: [{
@@ -326,7 +328,7 @@ export async function callManagedCli(toolName, args, env = process.env, fetchImp
326
328
  };
327
329
  }
328
330
  const childEnv = (executable === 'agentic-flow' || executable === 'agentic-qe')
329
- ? runtimeChildEnv({ env, cwd: env.RUVNET_BRAIN_PROJECT_DIR || process.cwd() })
331
+ ? runtimeChildEnv({ env, cwd: projectRoot })
330
332
  : env;
331
333
  const execution = await execute(executable, argv, childEnv);
332
334
  recordManagedCliObservation({ toolName, executable, argv, execution, env });
@@ -31,6 +31,7 @@ import fs from 'node:fs';
31
31
  import path from 'node:path';
32
32
  import { spawnSync } from 'node:child_process';
33
33
  import { fileURLToPath } from 'node:url';
34
+ import { CONTEXT_EVENTS } from './codex-hook-events.mjs';
34
35
 
35
36
  const raw = fs.readFileSync(0, 'utf8');
36
37
  let input = {};
@@ -41,13 +42,10 @@ const event = String(input.hook_event_name || '');
41
42
  let adapted = false;
42
43
  const codexToolName = String(input.tool_name).toLowerCase();
43
44
 
44
- /**
45
- * Events whose output schema defines a *HookSpecificOutputWire with `additionalContext`. Only these
46
- * may carry a hook's prose back to the model.
47
- */
48
- const CONTEXT_EVENTS = new Set([
49
- 'PreToolUse', 'PostToolUse', 'PermissionRequest', 'SessionStart', 'SubagentStart', 'UserPromptSubmit',
50
- ]);
45
+ // CONTEXT_EVENTS (events whose output schema defines a *HookSpecificOutputWire with
46
+ // `additionalContext`) now lives in the pure sibling ./codex-hook-events.mjs — see that file's
47
+ // header for why: this module's top level reads stdin synchronously, which makes it unsafe to
48
+ // import for its constants alone (Dream Cycle 2026-08-30).
51
49
 
52
50
  /** Every file an apply_patch touches, in patch order. Codex patches are routinely multi-file. */
53
51
  export function patchFiles(patch) {
@@ -172,6 +170,13 @@ if (event === 'Stop') {
172
170
  process.exit(0);
173
171
  }
174
172
 
173
+ // Dream Cycle 2026-08-25: this event's schema has nowhere to carry an envelope at all — see
174
+ // CONTEXT_EVENTS above. The `!parsed` branch below already dropped unparseable prose here; a body
175
+ // that happens to emit VALID JSON (e.g. a stray hookSpecificOutput.additionalContext) used to skip
176
+ // that guard and fall through to a verbatim stdout write, which Codex rejects exactly like prose
177
+ // would. No shipped body does this today, but nothing enforced that it couldn't start.
178
+ if (!CONTEXT_EVENTS.has(event)) process.exit(0);
179
+
175
180
  if (!parsed) {
176
181
  // Prose from a shared body. It is only deliverable on an event whose schema has somewhere to put
177
182
  // it; everywhere else it is dropped rather than emitted as output the host will reject.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * codex-hook-events.mjs — the Codex event catalogue, as pure data.
3
+ *
4
+ * Split out of codex-hook-adapter.mjs (Dream Cycle 2026-08-30) because that file's top level reads
5
+ * stdin synchronously (`fs.readFileSync(0, 'utf8')`) the moment it is imported — the same
6
+ * import-time side-effect hazard the 2026-08-26 brain-stamp.mjs finding named (a test importing the
7
+ * side-effecting module directly hangs/misbehaves rather than observing its constants). That is
8
+ * exactly why `tests/unit/codex-claude-hook-parity.test.mjs` never imported the adapter's own
9
+ * CONTEXT_EVENTS and instead carried a hand-copied array — which had already drifted to 4 of the
10
+ * real 6 events, so `PermissionRequest` and `SubagentStart` had zero coverage proving the
11
+ * wrap-in-envelope branch runs for them at all. Extracting the pure data here, with no filesystem or
12
+ * stdin access at import time, lets both the adapter and its test read the same values instead of
13
+ * two copies that only stay in sync by whoever remembers to edit both.
14
+ */
15
+
16
+ /**
17
+ * Events whose output schema defines a *HookSpecificOutputWire with `additionalContext`. Only these
18
+ * may carry a hook's prose back to the model.
19
+ */
20
+ export const CONTEXT_EVENTS = new Set([
21
+ 'PreToolUse', 'PostToolUse', 'PermissionRequest', 'SessionStart', 'SubagentStart', 'UserPromptSubmit',
22
+ ]);
23
+
24
+ /**
25
+ * The complete Codex hook-event catalogue, PascalCase to match every event name used in this file's
26
+ * sibling and its manifest — not this repo's invention. Read from the live host and recorded
27
+ * verbatim (as snake_case) in `plugin/hooks/codex-hooks.json`'s own `description`: "pre_tool_use,
28
+ * permission_request, post_tool_use, pre_compact, post_compact, session_start, session_end,
29
+ * user_prompt_submit, subagent_start, subagent_stop, stop." `codex-hooks.json` currently wires only
30
+ * 7 of these 11 (see its `DECLARED ABSENT` note for the one Claude-Code-only event, `TeammateIdle`,
31
+ * which Codex has no equivalent for at all); the remaining 4 registered-nowhere-yet events
32
+ * (PermissionRequest, PostCompact, SubagentStart, SubagentStop) still pass through the adapter's
33
+ * event classification the moment anything is ever wired to them, so the classification itself is
34
+ * worth proving correct now rather than the day a hook body first reaches one unproven.
35
+ */
36
+ export const ALL_HOST_EVENTS = [
37
+ 'PreToolUse', 'PermissionRequest', 'PostToolUse', 'PreCompact', 'PostCompact',
38
+ 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart', 'SubagentStop', 'Stop',
39
+ ];
@@ -53,7 +53,7 @@ function validateLegacyGistAggregateReceipt({ receipt, passagesFile, expectedIds
53
53
 
54
54
  export function validateGistAggregateReceipt({ receipt, passagesFile, expectedIds = null,
55
55
  sourceObservationSha256 = null }) {
56
- if (receipt?.schemaVersion === 2 && sourceObservationSha256 === null) {
56
+ if (receipt?.schemaVersion === 2) {
57
57
  return validateLegacyGistAggregateReceipt({ receipt, passagesFile, expectedIds });
58
58
  }
59
59
  if (receipt?.schemaVersion !== 3 || receipt?.kind !== 'ruvnet-brain-gist-source-receipts'
@@ -201,7 +201,7 @@ export function generationLedgerBytes(ledger) {
201
201
  return Buffer.from(`${JSON.stringify(ledger, null, 2)}\n`);
202
202
  }
203
203
 
204
- export function validatePublicInventory({ assetsDir, coverage, ledger, installedPublicStores = null }) {
204
+ export function validatePublicInventory({ assetsDir, coverage, ledger, installedPublicStores = null, gistReceipt = null }) {
205
205
  const root = path.resolve(assetsDir);
206
206
  const selected = installedPublicStores === null ? null : [...installedPublicStores].map((store) => String(store).toLowerCase());
207
207
  if (selected && (selected.some((store) => !store) || new Set(selected).size !== selected.length)) {
@@ -239,14 +239,14 @@ export function validatePublicInventory({ assetsDir, coverage, ledger, installed
239
239
  if (gistStores.size !== 1 || !gistStores.has('ruv-gists')) throw new Error('eligible gists must use the ruv-gists aggregate');
240
240
  if (!selectedSet || selectedSet.has('ruv-gists')) {
241
241
  const receiptFile = path.join(root, 'ruv-gists.sources.json');
242
- const receipt = readJson(receiptFile, 'gist aggregate receipt');
242
+ const receipt = gistReceipt || readJson(receiptFile, 'gist aggregate receipt');
243
243
  const passages = path.join(root, 'ruv-gists.passages.jsonl');
244
244
  const ids = gists.map((row) => String(row.key || '').replace(/^gist:/, ''));
245
245
  if (ids.some((id) => !id)) throw new Error('gist coverage row identity is missing');
246
246
  validateGistAggregateReceipt({ receipt, passagesFile: passages, expectedIds: ids,
247
247
  sourceObservationSha256: coverage.sourceObservationSha256 });
248
248
  if (receipt.schemaVersion === 3) evidenceFiles.push(evidenceIdentity(root, passages, 'gist-passages'));
249
- evidenceFiles.push(evidenceIdentity(root, receiptFile, 'gist-receipt'));
249
+ if (!gistReceipt) evidenceFiles.push(evidenceIdentity(root, receiptFile, 'gist-receipt'));
250
250
  }
251
251
  gistAggregate = 'ruv-gists';
252
252
  }
@@ -275,7 +275,7 @@ function resolveCodeRoot() {
275
275
  try {
276
276
  const dev = JSON.parse(fs.readFileSync(DEV, 'utf8'));
277
277
  if (dev && dev.codeRoot && fs.existsSync(path.join(dev.codeRoot, 'scripts'))) {
278
- return { root: dev.codeRoot, source: 'dev' };
278
+ return { root: dev.codeRoot, source: 'dev', version: dev.version || '' };
279
279
  }
280
280
  } catch { /* no dev mode */ }
281
281
  try {
@@ -285,13 +285,13 @@ function resolveCodeRoot() {
285
285
  const real = fs.realpathSync(root);
286
286
  // Containment: only ever execute from the immutable version store.
287
287
  if (!real.startsWith(fs.realpathSync(VERSIONS) + path.sep)) return null;
288
- return { root: real, source: `gen ${active.generation ?? '?'}` };
288
+ return { root: real, source: `gen ${active.generation ?? '?'}`, version: active.version || '' };
289
289
  } catch { return null; }
290
290
  }
291
291
 
292
292
  // Run one hook body. No shell is ever involved: spawnSync with an argument array, interpreter
293
293
  // chosen from the typed table — never from input.
294
- function runHook(file) {
294
+ function runHook(file, activeVersion = '') {
295
295
  if (!fs.existsSync(file)) return 0; // nothing to run — never invent a failure
296
296
  let cmd;
297
297
  if (entry.interpreter === 'node') {
@@ -312,9 +312,11 @@ function runHook(file) {
312
312
  // core also reads the sentinel because the POSIX compatibility launcher and bare installs invoke
313
313
  // it outside this shim. Passing the snapshot means the two readings cannot disagree within one
314
314
  // invocation if the user flips the switch while the hook is mid-run.
315
- const env = (BRAIN_OFF && entry.offBehavior === 'partial')
316
- ? { ...process.env, RUVNET_BRAIN_OFF: '1' }
317
- : process.env;
315
+ const env = {
316
+ ...process.env,
317
+ ...(activeVersion ? { RUVNET_BRAIN_ACTIVE_VERSION: activeVersion } : {}),
318
+ ...(BRAIN_OFF && entry.offBehavior === 'partial' ? { RUVNET_BRAIN_OFF: '1' } : {}),
319
+ };
318
320
  const io = hookInput !== null
319
321
  ? { stdio: ['pipe', 'inherit', 'inherit'], input: hookInput }
320
322
  : { stdio: 'inherit' };
@@ -342,7 +344,7 @@ function dispatchHook() {
342
344
  // codeRoot IS a plugin-payload root (versions/<v>/ mirrors the plugin dir: scripts/, hooks/, mcp/).
343
345
  const spineFile = path.join(spine.root, 'scripts', entry.file);
344
346
  if (fs.existsSync(spineFile)) {
345
- return runHook(spineFile);
347
+ return runHook(spineFile, spine.version);
346
348
  }
347
349
  // Spine resolved but the body file is missing — fall back LOUDLY (finding 25), once per
348
350
  // generation. The key omits entry.file on purpose: one broken generation is ONE piece of news,
@@ -147,7 +147,26 @@ else
147
147
  # they mean; the component that actually creates the queue was not brought along, so the invariant
148
148
  # held for two of three participants and was violated by the one doing the writing. Same shape as
149
149
  # ADR-066: a writer and a reader that disagree about the store make the recording theatre.
150
- DIR="${RUVNET_BRAIN_PROJECT_DIR:-$PWD}/.swarm/ruvnet-brain-learn"
150
+ # RESIDUAL of #134/#104: RUVNET_BRAIN_PROJECT_DIR is never set by real hook dispatch on either
151
+ # host (neither hook-shim.mjs nor codex-hook-adapter.mjs seeds it), so it degraded back to bare
152
+ # $PWD in production. CLAUDE_PROJECT_DIR is the one project-root signal both hosts DO provide on
153
+ # every invocation. Trusted only when $PWD actually lies inside it — the SAME containment rule
154
+ # project-identity.mjs's projectDirectory() applies for the identical reason (#85/#107: an
155
+ # unrelated declared root must never overrule a cwd it does not contain). A plain string-prefix
156
+ # check, not a realpath/inode compare, to honour this hook's own no-process-spawn contract.
157
+ ROOT_DIR="$PWD"
158
+ if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
159
+ CPD="${CLAUDE_PROJECT_DIR%/}"
160
+ # Git Bash presents PWD as /c/... while Node supplies CLAUDE_PROJECT_DIR as C:\\... on
161
+ # Windows. Compare normalized, case-folded spellings so the real project-root signal works
162
+ # on both hosts without spawning a platform-specific path converter.
163
+ _pwd_for_compare="$PWD"
164
+ if [ -n "$(pwd -W 2>/dev/null || true)" ]; then _pwd_for_compare="$(pwd -W)"; fi
165
+ _pwd_cmp=$(printf '%s' "$_pwd_for_compare" | tr '\\\\' '/' | tr '[:upper:]' '[:lower:]')
166
+ _cpd_cmp=$(printf '%s' "$CPD" | tr '\\\\' '/' | tr '[:upper:]' '[:lower:]')
167
+ case "$_pwd_cmp/" in "$_cpd_cmp"/*) ROOT_DIR="$CPD" ;; esac
168
+ fi
169
+ DIR="${RUVNET_BRAIN_PROJECT_DIR:-$ROOT_DIR}/.swarm/ruvnet-brain-learn"
151
170
  fi
152
171
  # PROJECT SCOPE MEANS THE PROJECT MUST HAVE OPTED IN. In project scope $DIR sits under `.swarm`,
153
172
  # which is Ruflo's own convention and is created by `ruflo init` — so its PRESENCE is the project's
@@ -15,6 +15,7 @@ import { execFileSync } from 'node:child_process';
15
15
  import { readStdinBounded } from './hook-input.mjs';
16
16
  import { learningScope, loadRuntimePreferences } from './runtime-preferences.mjs';
17
17
  import { resolveRuflo, RUFLO_MISSING } from './ruflo-bin.mjs';
18
+ import { projectDirectory } from './project-identity.mjs';
18
19
 
19
20
  // ONE BOUNDED LINE ON STDERR. stderr because a SessionEnd hook's stdout is not surfaced, and bounded
20
21
  // because a hook that prints a stack trace on every `/clear` gets muted — and a muted diagnostic is
@@ -23,7 +24,12 @@ import { resolveRuflo, RUFLO_MISSING } from './ruflo-bin.mjs';
23
24
  const warn = (msg) => { try { process.stderr.write(`learn-flush: ${msg}\n`); } catch { /* stderr gone */ } };
24
25
 
25
26
  const HOME = os.homedir();
26
- const PROJECT = process.env.RUVNET_BRAIN_PROJECT_DIR || process.cwd();
27
+ // RESIDUAL of #134/#104: RUVNET_BRAIN_PROJECT_DIR is never set by real hook dispatch on either host,
28
+ // so it degraded back to raw cwd() in production. `projectDirectory()` (project-identity.mjs) is the
29
+ // SAME CLAUDE_PROJECT_DIR-with-containment rule #85/#107 already fixed for the receipt/Console
30
+ // agreement — reused here rather than trusting the variable unconditionally, which would reopen the
31
+ // class of bug #107 was: an unrelated declared root overruling a cwd it does not actually contain.
32
+ const PROJECT = process.env.RUVNET_BRAIN_PROJECT_DIR || projectDirectory({ env: process.env });
27
33
  // ISSUE #139 — this WRITER resolved scope correctly while two READERS hardcoded it, so they agreed
28
34
  // only by coincidence. The resolution moved into runtime-preferences.mjs and all three now call it;
29
35
  // a future scope is one edit, not three. Behaviour here is unchanged by design.
@@ -270,14 +270,29 @@ const HERE = (() => {
270
270
  }
271
271
  return path.basename(d);
272
272
  })();
273
+ /**
274
+ * A "suffix match" for project names, bounded so it cannot fire on a bare shared tail. `a` matches
275
+ * `b` when `b` is a whole path/name SEGMENT suffix of `a` — the character immediately before it must
276
+ * be a separator, never a mid-word letter. Without this, "Sentry" matched inside "WhitSentry" and any
277
+ * project name that merely happens to end another's, which is the exact cross-project leak the
278
+ * 2026-07-22 project-scope fix (this same function, below) was written to close.
279
+ */
280
+ const segmentSuffixMatch = (a, b) => {
281
+ if (a === b) return true;
282
+ if (a.length <= b.length || !a.endsWith(b)) return false;
283
+ return /[-_/]/.test(a[a.length - b.length - 1]);
284
+ };
285
+
273
286
  /** Does this lesson belong to the project we are standing in? Match is loose on purpose — stored
274
- * names carry prefixes like `Code-` that the directory name does not. */
287
+ * names carry prefixes like `Code-` that the directory name does not — but bounded to a real
288
+ * delimiter so it cannot match a bare, accidental shared suffix between unrelated projects. */
275
289
  const isHome = (l) => {
276
290
  const ps = Array.isArray(l.projects) ? l.projects : [];
277
291
  if (!ps.length) return true; // unscoped: applies anywhere, by declaration
278
292
  return ps.some((p) => {
279
293
  const n = String(p).replace(/^Code-/, '');
280
- return n === HERE || String(p) === HERE || HERE.endsWith(n) || n.endsWith(HERE);
294
+ return n === HERE || String(p) === HERE
295
+ || segmentSuffixMatch(HERE, n) || segmentSuffixMatch(n, HERE);
281
296
  });
282
297
  };
283
298
  const isUniversal = (l) => Array.isArray(l.projects) && l.projects.length >= 2;
@@ -205,8 +205,23 @@ if [ "$EVENT" = "PreToolUse-bash" ] && [ -f "$HOOK_INPUT_JS" ]; then
205
205
  # with absolute paths writes) matched NOTHING, while `grep -n "npm publish" docs/` matched, so
206
206
  # reading ABOUT shipping counted as shipping. Quoted regions are stripped first because the
207
207
  # truth-maker is what will EXECUTE — a commit message is not a command.
208
- CMD_EXEC=$(printf '%s' "$CMD" | sed -e 's/"[^"]*"/ /g' -e "s/'[^']*'/ /g")
209
- if printf '%s' "$CMD_EXEC" | grep -qE '\bgit\b[^|;&]*\bpush\b|\b(npm|yarn|pnpm) publish\b|\bgh release create\b|release\.mjs'; then
208
+ # tr runs BEFORE sed, not after: sed matches one line at a time, so a double/single-quoted string
209
+ # that legitimately SPANS a real newline (a multi-line commit message, a heredoc body) would
210
+ # otherwise never get stripped — its opening quote sits on one sed-internal line with no closer,
211
+ # its closing quote on another with no opener — leaking whatever words it quotes (e.g. "npm" and
212
+ # "publish" mentioned in prose) into CMD_EXEC as if they were unquoted. Collapsing newlines/tabs to
213
+ # spaces FIRST makes the whole command one logical line, so the existing quote-stripping regexes
214
+ # then span it correctly, same as they already do for a single-line quoted string. Caught live by
215
+ # an adversarial critic pass on the first cut of this fix (which ran tr after sed): a multi-line
216
+ # `git commit -m "...npm\npublish is unaffected..."` false-positived as a ship command on this side
217
+ # while degradation-watch.mjs's `dependentEvent()` (unmodified, not line-oriented) correctly said no
218
+ # — the exact "two ship definitions disagree" defect this diff exists to close, reopened in a new
219
+ # shape. [[:space:]]+ (not a literal space) then matches degradation-watch.mjs's `\s+` on what's
220
+ # left. 2026-08-27: the two definitions also disagreed on a tab/doubled-space/wrapped-newline
221
+ # `npm publish`/`gh release create`, which the JS side (already `\s+`-tolerant on a plain string)
222
+ # caught and this side missed silently.
223
+ CMD_EXEC=$(printf '%s' "$CMD" | tr '\n\t' ' ' | sed -e 's/"[^"]*"/ /g' -e "s/'[^']*'/ /g")
224
+ if printf '%s' "$CMD_EXEC" | grep -qE '\bgit\b[^|;&]*\bpush\b|\b(npm|yarn|pnpm)[[:space:]]+publish\b|\bgh[[:space:]]+release[[:space:]]+create\b|release\.mjs'; then
210
225
  ARGS+=(--trigger ship)
211
226
  fi
212
227
  fi