ruvnet-brain 4.3.38 β 4.3.39
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 +2 -2
- package/kb/forge-update.mjs +18 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +4 -4
- package/plugin/hooks/hook-contracts.json +2 -2
- package/plugin/hooks/hooks.json +2 -2
- package/plugin/scripts/codex-hook-wrapper.mjs +3 -0
- package/scripts/approved-runtime.mjs +1 -1
- package/scripts/corpus-canary.mjs +9 -12
- package/scripts/protected-release-invocation.mjs +1 -1
- package/scripts/release.mjs +3 -3
- package/scripts/single-source-check.mjs +12 -6
package/README.md
CHANGED
|
@@ -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
|
|
|
@@ -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** β 199 public stores Β· 161,
|
|
565
|
+
- β
**The grounding brain is real and proven** β 199 public stores Β· 161,322 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/kb/forge-update.mjs
CHANGED
|
@@ -1073,6 +1073,23 @@ function writeSnapshotReceipt(backupPath, { state, reason = null, recoveryComman
|
|
|
1073
1073
|
*
|
|
1074
1074
|
* @returns {{removed: string[], kept: [string, string][], freed: number}}
|
|
1075
1075
|
*/
|
|
1076
|
+
/**
|
|
1077
|
+
* WHY a guard run failed. forge-guard prints its `[FAIL] ...` lines to STDOUT, and execFileSync puts only
|
|
1078
|
+
* STDERR in error.message, so a refused store used to read "Command failed: node .../forge-guard.mjs --name X"
|
|
1079
|
+
* with the cause dropped (measured 2026-09-30: the customer canary refused a generation and its log did not
|
|
1080
|
+
* say why). Keep the command line, then append the guard's own FAIL lines.
|
|
1081
|
+
*/
|
|
1082
|
+
export function describeGuardFailure(error) {
|
|
1083
|
+
const text = (value) => (value == null ? '' : Buffer.isBuffer(value) ? value.toString('utf8') : String(value));
|
|
1084
|
+
const fails = `${text(error?.stdout)}\n${text(error?.stderr)}`.split('\n')
|
|
1085
|
+
.map((line) => line.trim()).filter((line) => /\[FAIL\]|Error:/.test(line));
|
|
1086
|
+
// Keep the WHOLE message: execFileSync appends the child's stderr on the lines after the command line.
|
|
1087
|
+
const message = String(error?.message || error);
|
|
1088
|
+
const extra = fails.filter((line) => !message.includes(line));
|
|
1089
|
+
const cause = extra.length ? ` -- ${extra.join(' | ').slice(0, 800)}` : '';
|
|
1090
|
+
return `${message.slice(0, 1600)}${cause}`;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1076
1093
|
export function reclaimBackups({
|
|
1077
1094
|
kbDir,
|
|
1078
1095
|
backupsMade = [],
|
|
@@ -1694,7 +1711,7 @@ async function main() {
|
|
|
1694
1711
|
if (phase === 'live') finalVerificationByStore.set(local.kbName, verified);
|
|
1695
1712
|
}
|
|
1696
1713
|
return { valid: true, failures: [] };
|
|
1697
|
-
} catch (error) { return { valid: false, failures: [`forge-guard failed: ${error
|
|
1714
|
+
} catch (error) { return { valid: false, failures: [`forge-guard failed: ${describeGuardFailure(error)}`] }; }
|
|
1698
1715
|
};
|
|
1699
1716
|
let transaction;
|
|
1700
1717
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.39",
|
|
4
4
|
"description": "One-command installer for RuvNet Brain \u2014 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": {
|
|
@@ -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 a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
|
|
4
|
-
"version": "4.3.
|
|
4
|
+
"version": "4.3.39",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"hooks": [
|
|
66
66
|
{
|
|
67
67
|
"type": "command",
|
|
68
|
-
"command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\"
|
|
69
|
-
"timeout":
|
|
68
|
+
"command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 9000 unprompted-speech UserPromptSubmit",
|
|
69
|
+
"timeout": 10
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
"type": "command",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
"type": "command",
|
|
78
|
-
"command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\"
|
|
79
|
-
"timeout":
|
|
78
|
+
"command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 5500 capacity-aware-parallel-work",
|
|
79
|
+
"timeout": 6
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
"type": "command",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
"mode": "blocking",
|
|
180
180
|
"offBehavior": "silence",
|
|
181
181
|
"matcher": "*",
|
|
182
|
-
"timeout":
|
|
182
|
+
"timeout": 9
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
"id": "continuation-gate",
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
"mode": "advisory",
|
|
251
251
|
"offBehavior": "run",
|
|
252
252
|
"matcher": "*",
|
|
253
|
-
"timeout":
|
|
253
|
+
"timeout": 6
|
|
254
254
|
},
|
|
255
255
|
{
|
|
256
256
|
"id": "decision-gate",
|
package/plugin/hooks/hooks.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
{
|
|
21
21
|
"type": "command",
|
|
22
22
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" unprompted-speech UserPromptSubmit",
|
|
23
|
-
"timeout":
|
|
23
|
+
"timeout": 9
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"type": "command",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
{
|
|
31
31
|
"type": "command",
|
|
32
32
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" capacity-aware-parallel-work || true",
|
|
33
|
-
"timeout":
|
|
33
|
+
"timeout": 6
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"type": "command",
|
|
@@ -69,6 +69,9 @@ function timeoutFor(hookId) {
|
|
|
69
69
|
// tinguishable from a crash; 6000ms covers the gate's cap plus this chain's spawn overhead
|
|
70
70
|
// (measured 773β1145ms end-to-end warm, so ~150β400ms of that is the wrapper/adapter/shim).
|
|
71
71
|
if (hookId === 'decision-gate') return 6_000;
|
|
72
|
+
// Advisory capacity hook: 4 of 8 runs were killed at the old 2s host timeout on a loaded machine (measured
|
|
73
|
+
// 2026-09-30). Keep the wrapper's budget below the registration's own 5.5s so the wrapper, not the host, ends it.
|
|
74
|
+
if (hookId === 'capacity-aware-parallel-work') return 5_000;
|
|
72
75
|
if (hookId === 'ground-ruvnet' || hookId === 'unprompted-speech' || hookId === 'continuation-gate') {
|
|
73
76
|
return 8_500;
|
|
74
77
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// scripts/verify-bundle.mjs (:645) and keys/ruvnet-brain-signing.pub.pem (:648) β and the customer
|
|
8
8
|
// updater extracts that archive straight into the user's Claude Code config. So an unattended corpus
|
|
9
9
|
// promotion built at HEAD would ship whatever unreleased executable bytes happen to be on main that
|
|
10
|
-
// night, to every installed client, with no
|
|
10
|
+
// night, to every installed client, with no install-verified code release behind it. That is a code
|
|
11
11
|
// release wearing a corpus release's clothes.
|
|
12
12
|
//
|
|
13
13
|
// The pin closes it by ENFORCED EQUALITY, not by a version string. Dual's correction is explicit:
|
|
@@ -117,10 +117,13 @@ function coverageRows(file) {
|
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
119
|
* Per-store freshness, judged from the generation's own sealed coverage (CORPUS-COVERAGE.json inside
|
|
120
|
-
* the installed tree): the upstream observation must be under 48h old, no store
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
120
|
+
* the installed tree): the upstream observation must be under 48h old, and no store may carry a commit
|
|
121
|
+
* other than the upstream commit observed then. Together those ARE the 48h guarantee. There is
|
|
122
|
+
* deliberately NO rule on a store's own ingestedAt: a store keeps the ingestion time of the last night
|
|
123
|
+
* its upstream moved (measured 2026-09-30: one generation holds ingestedAt from 2026-07-30 to
|
|
124
|
+
* 2026-09-30), so a "moved store must be ingested within 48h of now" rule would wrongly refuse a
|
|
125
|
+
* perfectly fresh generation about three nights after every code release.
|
|
126
|
+
* MISSING/INELIGIBLE rows are the coverage validator's business (it already ran), not a freshness question.
|
|
124
127
|
*/
|
|
125
128
|
export function storeFreshness({ after, before = null, now }) {
|
|
126
129
|
if (!after) return { ok: false, detail: 'CORPUS-COVERAGE.json is missing from the installed tree' };
|
|
@@ -131,7 +134,6 @@ export function storeFreshness({ after, before = null, now }) {
|
|
|
131
134
|
if (age > FRESHNESS_LIMIT_MS || age < 0) problems.push(`upstream observed ${(age / 3_600_000).toFixed(1)}h ago (limit 48h)`);
|
|
132
135
|
const priorCommit = new Map((before?.rows || []).map((row) => [row.key, row.artifact?.sourceCommit || null]));
|
|
133
136
|
const stale = [];
|
|
134
|
-
const late = [];
|
|
135
137
|
let moved = 0;
|
|
136
138
|
for (const row of after.rows) {
|
|
137
139
|
if (row.status === 'INELIGIBLE') continue;
|
|
@@ -139,17 +141,12 @@ export function storeFreshness({ after, before = null, now }) {
|
|
|
139
141
|
const built = row.artifact?.sourceCommit || null;
|
|
140
142
|
if (!upstream || !built) continue;
|
|
141
143
|
if (upstream !== built) { stale.push(row.artifact?.store || row.key); continue; }
|
|
142
|
-
if (before && priorCommit.get(row.key) !== upstream)
|
|
143
|
-
moved += 1;
|
|
144
|
-
const ingested = Date.parse(row.artifact?.ingestedAt);
|
|
145
|
-
if (!Number.isFinite(ingested) || now - ingested > FRESHNESS_LIMIT_MS) late.push(row.artifact?.store || row.key);
|
|
146
|
-
}
|
|
144
|
+
if (before && priorCommit.get(row.key) !== upstream) moved += 1;
|
|
147
145
|
}
|
|
148
146
|
if (stale.length) problems.push(`${stale.length} store(s) built from a commit older than their observed upstream: ${stale.slice(0, 10).join(', ')}`);
|
|
149
|
-
if (late.length) problems.push(`${late.length} moved store(s) ingested more than 48h ago: ${late.slice(0, 10).join(', ')}`);
|
|
150
147
|
return problems.length
|
|
151
148
|
? { ok: false, detail: problems.join('; ') }
|
|
152
|
-
: { ok: true, detail: `observed ${(age / 3_600_000).toFixed(1)}h ago; ${after.rows.length} row(s), 0 stale; ${before ? `${moved} moved since the installed generation, all
|
|
149
|
+
: { ok: true, detail: `observed ${(age / 3_600_000).toFixed(1)}h ago; ${after.rows.length} row(s), 0 stale; ${before ? `${moved} moved since the installed generation, all at their observed upstream commit` : 'no prior coverage to diff against'}` };
|
|
153
150
|
}
|
|
154
151
|
|
|
155
152
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Local publication is deliberately impossible. The one publisher may mutate public channels only
|
|
3
|
-
// when the
|
|
3
|
+
// when the branch-protected GitHub workflow carries a fully valid candidate seal into it.
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
package/scripts/release.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// header called check-only "the DEFINITION OF DONE" and claimed it alone decided "shipped"; that was
|
|
12
12
|
// never true and duplicated a second, drifting gate list alongside CI's real one). Publishing a
|
|
13
13
|
// release β creating the GitHub Release, moving the npm dist-tag β happens ONLY inside the
|
|
14
|
-
//
|
|
14
|
+
// branch-protected `protected-release.yml` workflow, against an exact-SHA CI-sealed candidate;
|
|
15
15
|
// `--publish` mode is that workflow's publisher (it validates the protected-invocation receipt before
|
|
16
16
|
// doing anything and refuses outside it), never rebuilds or retests source, and treats the immutable
|
|
17
17
|
// artifact as the evidence boundary. `--check` mode is a read-only LOCAL PREVIEW for a human on a dev
|
|
@@ -680,7 +680,7 @@ if (CORPUS_SEED) {
|
|
|
680
680
|
console.log(`\n${c.b('RuvNet Brain β release / definition-of-done')} ${c.dim('Β· ' + (PUBLISH ? 'PUBLISH' : 'check-only') + ' Β· shipping ' + V())}\n`);
|
|
681
681
|
|
|
682
682
|
// The local CLI remains useful as a read-only preflight, but publication authority lives only in
|
|
683
|
-
// the
|
|
683
|
+
// the branch-protected workflow. Validate the exact candidate receipt and artifact bytes before
|
|
684
684
|
// any command capable of pushing, tagging, releasing, or publishing can run.
|
|
685
685
|
if (PUBLISH) {
|
|
686
686
|
const protectedInvocation = validateProtectedPublishInvocation({ root: ROOT });
|
|
@@ -782,7 +782,7 @@ if (PUBLISH) {
|
|
|
782
782
|
}
|
|
783
783
|
}
|
|
784
784
|
// ADR-0091 D6.6 β THE BACKWARD-MOVE RACE. Release QE sealed the corpus generation this bundle was
|
|
785
|
-
// built from; publication happens later,
|
|
785
|
+
// built from; publication happens later, in the protected workflow. Clients always accept a code release
|
|
786
786
|
// and drop their corpusGeneration marker when they install one (kb/forge-update.mjs), so publishing
|
|
787
787
|
// a bundle built from generation G after G+1 already shipped rolls every user back one night.
|
|
788
788
|
// Re-resolve with the SAME resolver, before any asset upload, and refuse on any difference -- or on
|
|
@@ -33,6 +33,8 @@ const HISTORY = /^(docs\/(adr|ddd|research|audits|reviews|qe)\/|CHANGELOG\.md$|P
|
|
|
33
33
|
// kb/ is corpus content (primers/cards describing OTHER projects' own commands) and docs/issues/
|
|
34
34
|
// are upstream bug reports quoting other configs β both are data, not instructions to this project.
|
|
35
35
|
const instructions = tracked.filter((f) => /\.md$/.test(f) && !HISTORY.test(f) && !/^(kb|docs\/issues)\//.test(f));
|
|
36
|
+
// Phrases that re-introduce a person as a release gate. Negated statements ("no human approval step") are filtered by the caller.
|
|
37
|
+
const HUMAN_APPROVAL_STEP = /(owner|stuart'?s?|maintainer) (approves?|approval|click|must approve)\b[^.]*(deployment|release|publish|gate)|approves? the `?Production|hand (it|the work) over[^.]*click|required[- ]reviewers?\b|standing authori[sz]ation permits/i;
|
|
36
38
|
const grepIn = (files, re) => files.flatMap((f) => read(f).split('\n')
|
|
37
39
|
.map((l, i) => (re.test(l) ? `${f}:${i + 1}: ${l.trim().slice(0, 140)}` : null)).filter(Boolean));
|
|
38
40
|
const none = (hits) => ({ ok: hits.length === 0, detail: hits.slice(0, 8).join('\n') || 'none' });
|
|
@@ -80,8 +82,10 @@ const checks = [
|
|
|
80
82
|
return none(instructions.flatMap((f) => [...read(f).matchAll(/npm run ([a-z0-9:_-]+)/g)]
|
|
81
83
|
.filter((m) => !scripts.has(m[1])).map((m) => `${f}: npm run ${m[1]}`)));
|
|
82
84
|
} },
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
// The owner is not in the release loop (2026-09-30): the gates are machine gates, and no instruction may
|
|
86
|
+
// route a release through a person clicking in GitHub. B12 applies the same idea to the local CLAUDE.md/AGENTS.md.
|
|
87
|
+
{ id: 'B6', area: 'rules', scope: 'repo', title: 'No instruction puts a human approval click (or a required reviewer) in the release path',
|
|
88
|
+
run: () => none(grepIn(instructions, HUMAN_APPROVAL_STEP).filter((l) => !/\bno (human|reviewer)\b|\bnever\b|\bnot asked\b/i.test(l))) },
|
|
85
89
|
{ id: 'B7', area: 'rules', scope: 'repo', title: 'Model IDs are selected only in their owner modules',
|
|
86
90
|
run: () => {
|
|
87
91
|
const re = /['"`](claude-(fable|opus|sonnet|haiku)-[0-9][a-z0-9.-]*|gpt-[0-9][a-z0-9.-]*)['"`]/;
|
|
@@ -127,7 +131,7 @@ const checks = [
|
|
|
127
131
|
{ id: 'B12', area: 'rules', scope: 'machine', title: 'The local (untracked) CLAUDE.md / AGENTS.md carry no contradicting instruction',
|
|
128
132
|
run: () => {
|
|
129
133
|
const main = path.join(HOME, 'Code/ruvnet-brain');
|
|
130
|
-
const bad = /npx (-y )?(@claude-flow|claude-flow|ruflo)\b|standing authori[sz]ation permits|
|
|
134
|
+
const bad = /npx (-y )?(@claude-flow|claude-flow|ruflo)\b|standing authori[sz]ation permits|owner (approves?|click)|Stuart's approval (of|in GitHub)|approves? the `?Production|hand (it|the work) over[^.]*click|npm run (build|dev|test:integration|test:coverage|test:security)\b|not direct Agent tool/i;
|
|
131
135
|
return none(['CLAUDE.md', 'AGENTS.md'].flatMap((f) => readAbs(path.join(main, f)).split('\n')
|
|
132
136
|
.map((l, i) => (bad.test(l) ? `${f}:${i + 1}: ${l.trim().slice(0, 120)}` : null)).filter(Boolean)));
|
|
133
137
|
} },
|
|
@@ -157,11 +161,13 @@ const checks = [
|
|
|
157
161
|
} },
|
|
158
162
|
{ id: 'C4', area: 'release', scope: 'machine', title: 'The corpus gist job has its authenticated token (RUVNET_GISTS_TOKEN)',
|
|
159
163
|
run: () => { const r = spawnSync('gh', ['secret', 'list', '-R', 'stuinfla/ruvnet-brain'], { encoding: 'utf8' }); return { ok: /^RUVNET_GISTS_TOKEN\b/m.test(r.stdout), detail: r.stdout.split('\n').map((l) => l.split(/\s/)[0]).filter(Boolean).join(', ') }; } },
|
|
160
|
-
{ id: 'C3', area: 'release', scope: 'machine', title: '
|
|
164
|
+
{ id: 'C3', area: 'release', scope: 'machine', title: 'The npm-scoped Production environment is a boundary: branch policy present, admins cannot bypass (no human reviewer is part of the design)',
|
|
161
165
|
run: () => {
|
|
162
166
|
const r = spawnSync('gh', ['api', 'repos/stuinfla/ruvnet-brain/environments', '-q',
|
|
163
|
-
'.environments[]|select(.name=="Production β ruvnet-brain")|[.protection_rules[]?|select(.type=="
|
|
164
|
-
|
|
167
|
+
'.environments[]|select(.name=="Production β ruvnet-brain")|[(.can_admins_bypass==false),([.protection_rules[]?|select(.type=="branch_policy")]|length)]|@json'], { encoding: 'utf8' });
|
|
168
|
+
let noBypass = false; let policies = 0;
|
|
169
|
+
try { [noBypass, policies] = JSON.parse(r.stdout.trim()); } catch { /* unreadable: falls through to fail */ }
|
|
170
|
+
return { ok: r.status === 0 && noBypass === true && policies > 0, detail: `can_admins_bypass=false: ${noBypass}; branch_policy rules: ${policies}${r.status === 0 ? '' : ` (${r.stderr.trim()})`}` };
|
|
165
171
|
} },
|
|
166
172
|
|
|
167
173
|
// D β one corpus / update path
|