ruvnet-brain 4.3.1 โ 4.3.3
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 +12 -13
- package/bin/install.mjs +10 -1
- package/console/app.js +4 -4
- package/kb/verify-citation.mjs +32 -5
- package/package.json +12 -9
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/mcp/managed-cli-interface.mjs +4 -2
- package/plugin/scripts/codex-hook-adapter.mjs +12 -7
- package/plugin/scripts/codex-hook-events.mjs +39 -0
- package/plugin/scripts/coverage-integrity.mjs +4 -4
- package/plugin/scripts/hook-shim.mjs +9 -7
- package/plugin/scripts/learn-capture.sh +20 -1
- package/plugin/scripts/learn-flush.mjs +7 -1
- package/plugin/scripts/lesson-gate.mjs +17 -2
- package/plugin/scripts/lesson-hooks.sh +17 -2
- package/plugin/scripts/session-start-core.mjs +47 -11
- package/scripts/brain-stamp-resolve.mjs +22 -0
- package/scripts/brain-stamp.mjs +5 -3
- package/scripts/build-bundle.mjs +41 -4
- package/scripts/ci/step-watchdog.mjs +86 -0
- package/scripts/convergence-manifest.mjs +66 -0
- package/scripts/distill-project.mjs +22 -4
- package/scripts/eval-brain.mjs +13 -3
- package/scripts/execution-policy.mjs +66 -0
- package/scripts/git-hooks/pre-push +16 -0
- package/scripts/health-repair.mjs +5 -1
- package/scripts/public-verification-aggregate.mjs +3 -1
- package/scripts/public-verification-inputs.mjs +5 -3
- package/scripts/qa-runner.mjs +71 -0
- package/scripts/qe/agentic-qe-4.3.mjs +195 -0
- package/scripts/qe/aggregate-4.3.mjs +42 -0
- package/scripts/record-lesson.mjs +28 -6
- package/scripts/release-projection.mjs +76 -32
- package/scripts/release.mjs +6 -11
- package/scripts/retrieval-canary.mjs +14 -6
- package/scripts/set-version.mjs +31 -0
- package/scripts/verify-channels.mjs +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# ๐ง RuvNet Brain
|
|
6
6
|
|
|
7
|
-
### ๐ง RuvNet Brain โ [](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)
|
|
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
|
-
##
|
|
59
|
+
## Earlier โ 4.2 loaded what rUv ships, without being asked
|
|
60
60
|
|
|
61
|
-
**The corpus
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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.**
|
|
68
|
-
`scripts/ingest-new-repos.mjs
|
|
69
|
-
rather than retried forever โ a permanent
|
|
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
|
-
##
|
|
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
|
-
- **
|
|
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
|
@@ -2811,7 +2811,16 @@ function runUpdate() {
|
|
|
2811
2811
|
console.error(` If you believe a newer build exists, check: ${c.bold('node forge-update.mjs --check')} in ${kbDir}`);
|
|
2812
2812
|
process.exit(outcome.exitCode);
|
|
2813
2813
|
}
|
|
2814
|
-
if (outcome.fallback) {
|
|
2814
|
+
if (outcome.fallback && FLAG_HOST_SYNC_ONLY) {
|
|
2815
|
+
// Host synchronization has a narrower contract than a full update: it must converge the
|
|
2816
|
+
// executable plugin/spine to the published package even when an optional large KB asset is
|
|
2817
|
+
// missing. The old path fell through to a fresh install here, which required the same missing
|
|
2818
|
+
// zip and stranded every host on its previous generation. Keep the KB failure visible, but
|
|
2819
|
+
// continue to the host-sync transaction; the published-surface/release gates still fail the
|
|
2820
|
+
// release until the signed KB asset exists.
|
|
2821
|
+
warn("the knowledge bundle could not refresh; continuing with executable host synchronization only");
|
|
2822
|
+
updateStatus = 0;
|
|
2823
|
+
} else if (outcome.fallback) {
|
|
2815
2824
|
warn("\nthe bundle's own updater couldn't complete โ falling back to a fresh install of the latest Release (this always works)โฆ\n");
|
|
2816
2825
|
const self = fileURLToPath(import.meta.url);
|
|
2817
2826
|
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
|
|
3123
|
-
|
|
3124
|
-
a
|
|
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' },
|
package/kb/verify-citation.mjs
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
35
|
-
|
|
36
|
-
const
|
|
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
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.3",
|
|
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": {
|
|
@@ -12,16 +12,24 @@
|
|
|
12
12
|
"benchmark:brain50": "node scripts/brain-latency-50.mjs",
|
|
13
13
|
"benchmark:novice50": "node scripts/brain-novice-50.mjs",
|
|
14
14
|
"version:check": "node scripts/sync-version.mjs --check",
|
|
15
|
+
"version:set": "node scripts/set-version.mjs",
|
|
16
|
+
"qa:pr": "node scripts/qa-runner.mjs",
|
|
17
|
+
"qa:release": "node scripts/qa-runner.mjs --release",
|
|
18
|
+
"release:authority": "node scripts/release-authority.mjs",
|
|
19
|
+
"card-from-source": "node scripts/card-from-source.mjs",
|
|
20
|
+
"release-abort-stale": "node scripts/release-abort-stale.mjs",
|
|
15
21
|
"claims:verify": "node scripts/claims-verify.mjs",
|
|
16
22
|
"claims:fix": "node scripts/claims-verify.mjs --fix",
|
|
17
23
|
"version:sync": "node scripts/sync-version.mjs",
|
|
24
|
+
"convergence:write": "node scripts/convergence-manifest.mjs --write",
|
|
25
|
+
"convergence:check": "node scripts/convergence-manifest.mjs",
|
|
26
|
+
"execution-policy:check": "node scripts/execution-policy.mjs '{\"action\":\"delegate\",\"description\":\"architecture audit\",\"nativeHosts\":[\"codex\"]}'",
|
|
18
27
|
"test:unit": "vitest run tests/unit",
|
|
19
28
|
"test:mesh": "vitest run tests/mesh",
|
|
20
29
|
"test:mutation": "vitest run tests/mutation",
|
|
21
30
|
"test:regression": "vitest run tests/regression",
|
|
22
31
|
"qe:ux": "node scripts/qe/ux-suite.mjs",
|
|
23
32
|
"test:cov": "vitest run tests/unit --coverage",
|
|
24
|
-
"test:release:preflight": "vitest run tests/unit/public-verification-inputs.test.mjs",
|
|
25
33
|
"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
34
|
"metaharness:receipts": "node scripts/metaharness-receipts.mjs",
|
|
27
35
|
"route:cheap": "node scripts/route-cheap.mjs",
|
|
@@ -35,9 +43,6 @@
|
|
|
35
43
|
"eval:top100": "node scripts/top100-benchmark.mjs",
|
|
36
44
|
"gists:index": "node scripts/ingest-gists.mjs --index-only",
|
|
37
45
|
"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
46
|
"test:integration": "vitest run tests/integration",
|
|
42
47
|
"substitution:check": "node scripts/no-silent-substitution.mjs",
|
|
43
48
|
"catalog:verify": "node scripts/verify-model-catalog.mjs",
|
|
@@ -51,8 +56,6 @@
|
|
|
51
56
|
"learning:replay:dry": "node scripts/learning-replay.mjs --dry-run",
|
|
52
57
|
"wired:check": "node scripts/wired-check.mjs --check",
|
|
53
58
|
"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
59
|
"status:check": "node scripts/status-honesty.mjs",
|
|
57
60
|
"cap:collect": "node scripts/rerank-cap-eval.mjs --collect",
|
|
58
61
|
"cap:report": "node scripts/rerank-cap-eval.mjs --report",
|
|
@@ -108,7 +111,7 @@
|
|
|
108
111
|
"bugs": "https://github.com/stuinfla/ruvnet-brain/issues",
|
|
109
112
|
"author": "Stuart Kerr",
|
|
110
113
|
"devDependencies": {
|
|
111
|
-
"@metaharness/darwin": "~0.
|
|
114
|
+
"@metaharness/darwin": "~0.9.2",
|
|
112
115
|
"@ruvector/rvf": "^0.3.4",
|
|
113
116
|
"@vitest/coverage-v8": "^4.1.10",
|
|
114
117
|
"playwright": "^1.62.0",
|
|
@@ -116,6 +119,6 @@
|
|
|
116
119
|
},
|
|
117
120
|
"dependencies": {
|
|
118
121
|
"@metaharness/flywheel": "^0.1.7",
|
|
119
|
-
"@metaharness/router": "^0.
|
|
122
|
+
"@metaharness/router": "^0.4.0"
|
|
120
123
|
}
|
|
121
124
|
}
|
|
@@ -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.
|
|
4
|
+
"version": "4.3.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
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 =
|
|
316
|
-
|
|
317
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
|
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
|
|
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
|
-
|
|
209
|
-
|
|
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
|