ruvnet-brain 4.4.0 → 4.5.0
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 +3 -3
- package/bin/install.mjs +679 -121
- package/console/app.js +178 -81
- package/console/index.html +1 -1
- package/console/install-architecture.html +1 -0
- package/console/scope.css +4 -1
- package/console/style.css +13 -0
- package/console/tips.html +4 -4
- package/kb/brain-profile.mjs +1 -0
- package/kb/corpus-release-identity.mjs +1 -1
- package/kb/forge-update.mjs +41 -17
- package/kb/model-requirements.mjs +4 -1
- package/kb/update-storage-transaction.mjs +79 -0
- package/kb/zip-extract.mjs +22 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +5 -4
- package/plugin/commands/configure.md +5 -4
- package/plugin/commands/rnb-brief.md +41 -0
- package/plugin/commands/rnb.md +80 -0
- package/plugin/commands/rnbc.md +80 -0
- package/plugin/commands/rvbc.md +5 -4
- package/plugin/commands/rvcb.md +5 -4
- package/plugin/commands/whats-new.md +4 -4
- package/plugin/mcp/server.mjs +10 -2
- package/plugin/scripts/advocacy-route.mjs +59 -23
- package/plugin/scripts/anticipate.sh +4 -0
- package/plugin/scripts/brain-confirmation.mjs +258 -0
- package/plugin/scripts/brain-footprint.mjs +494 -0
- package/plugin/scripts/brain-location.mjs +47 -0
- package/plugin/scripts/capability-registry.mjs +11 -1
- package/plugin/scripts/continuity-brief.mjs +324 -0
- package/plugin/scripts/continuity-events.mjs +327 -0
- package/plugin/scripts/continuity-journal.mjs +500 -0
- package/plugin/scripts/decision-gate.mjs +56 -3
- package/plugin/scripts/footprint-io.mjs +186 -0
- package/plugin/scripts/ground-before-write.sh +8 -1
- package/plugin/scripts/ground-ruvnet.sh +103 -12
- package/plugin/scripts/grounding-answer.mjs +2 -1
- package/plugin/scripts/grounding-stamp.sh +3 -0
- package/plugin/scripts/grounding-substance.mjs +1 -1
- package/plugin/scripts/grounding-turn-evidence.mjs +68 -6
- package/plugin/scripts/hook-input.mjs +78 -4
- package/plugin/scripts/kb-copy-proof.mjs +148 -0
- package/plugin/scripts/lesson-bridge.mjs +6 -2
- package/plugin/scripts/nightly-controller.mjs +8 -1
- package/plugin/scripts/node-sqlite.mjs +41 -0
- package/plugin/scripts/package-cards.json +797 -0
- package/plugin/scripts/package-cards.rvf +0 -0
- package/plugin/scripts/package-cards.rvf.idmap.json +1 -0
- package/plugin/scripts/package-cards.rvf.meta.json +1 -0
- package/plugin/scripts/package-recommender-client.mjs +138 -0
- package/plugin/scripts/package-recommender-flag.mjs +30 -0
- package/plugin/scripts/package-recommender.mjs +391 -0
- package/plugin/scripts/project-progression-outbox.mjs +26 -8
- package/plugin/scripts/project-progression-reader.mjs +14 -2
- package/plugin/scripts/project-progression-store.mjs +153 -6
- package/plugin/scripts/protect-brain-state.sh +4 -1
- package/plugin/scripts/session-snapshot-hook.mjs +225 -41
- package/plugin/scripts/session-start-budget.mjs +1 -0
- package/plugin/scripts/session-start-core.mjs +34 -4
- package/plugin/scripts/session-start-health.mjs +7 -1
- package/plugin/scripts/session-start-update-plane.mjs +35 -0
- package/plugin/scripts/turn-outcome-capture.mjs +12 -1
- package/plugin/scripts/unprompted-runtime.mjs +2 -2
- package/plugin/skills/brain-console/SKILL.md +3 -3
- package/plugin/skills/rnbc/SKILL.md +24 -0
- package/plugin/skills/rvbc/SKILL.md +2 -2
- package/scripts/approved-runtime.mjs +2 -2
- package/scripts/ci/warm-brain-models.mjs +28 -0
- package/scripts/codex-hook-trust.mjs +94 -0
- package/scripts/console-instances.mjs +70 -12
- package/scripts/console-runtime-identity.mjs +5 -0
- package/scripts/corpus-canary.mjs +46 -6
- package/scripts/corpus-dispatch-decision.mjs +2 -2
- package/scripts/corpus-promotion.mjs +1 -1
- package/scripts/full-suite-gate.mjs +9 -2
- package/scripts/hook-qualify-hosts.mjs +15 -3
- package/scripts/host-install-matrix.mjs +63 -2
- package/scripts/human-approval-phrases.mjs +46 -0
- package/scripts/installed-brain-health.mjs +53 -0
- package/scripts/move-brain.mjs +310 -0
- package/scripts/onboarding-console.mjs +93 -10
- package/scripts/oracle/abstain-threshold-sweep.mjs +62 -0
- package/scripts/oracle/abstain-trace.mjs +139 -0
- package/scripts/oracle/doc2query-generate.mjs +162 -0
- package/scripts/oracle/doc2query-reach.mjs +110 -0
- package/scripts/oracle/judge-train.mjs +158 -0
- package/scripts/oracle/need-set-split.mjs +48 -0
- package/scripts/oracle/sona-query-adapter-eval.mjs +139 -0
- package/scripts/package-cards.mjs +374 -0
- package/scripts/publication-receipt.mjs +37 -9
- package/scripts/recommendation-e2e.mjs +110 -0
- package/scripts/recommendation-eval.mjs +105 -0
- package/scripts/recommendation-floor.mjs +56 -0
- package/scripts/recommendation-judge-score.mjs +74 -0
- package/scripts/recommendation-latency.mjs +95 -0
- package/scripts/recommendation-real-host-score.mjs +76 -0
- package/scripts/recommendation-real-host.mjs +137 -0
- package/scripts/release-channel-kind.mjs +1 -1
- package/scripts/release-environment-policy.mjs +33 -0
- package/scripts/single-source-check.mjs +15 -10
- package/scripts/sync-commands.mjs +5 -2
- package/scripts/wired-check.mjs +17 -2
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* recommendation-real-host.mjs — the package recommender in a REAL Claude Code session (ADR-093 rev 3).
|
|
4
|
+
*
|
|
5
|
+
* For each selected eval prompt: a fresh `claude -p` with this checkout's UserPromptSubmit runtime as
|
|
6
|
+
* its only hook (route producer only, flag on), a warm search worker in a throwaway brain home, then the
|
|
7
|
+
* model's actual answer. Records whether the hook injected a hint, which packages it carried, and which
|
|
8
|
+
* one (if any) the model named — so the simulated-host judge can be checked against a real host.
|
|
9
|
+
*
|
|
10
|
+
* Isolation, following scripts/hook-qualify-hosts.mjs: --no-session-persistence, --setting-sources ''
|
|
11
|
+
* (no user/project settings, plugins or hooks), --strict-mcp-config (no MCP servers), --tools '' (no
|
|
12
|
+
* tool can touch anything), a temp cwd, every RUVNET_* state path in a temp dir. Auth is the user's own
|
|
13
|
+
* login; the run snapshots ~/.claude mtimes before/after and reports anything that changed.
|
|
14
|
+
*
|
|
15
|
+
* node scripts/recommendation-real-host.mjs --key <e2e judge-key.json> --models <d> --xenova <d> --out <dir>
|
|
16
|
+
* [--n-pos 12 --n-neg-hinted 6 --n-other 4 | --all-blinds] [--max-load 60 --batch 11] [--claude <bin>]
|
|
17
|
+
*/
|
|
18
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import readline from 'node:readline';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
26
|
+
const arg = (n, d) => { const i = process.argv.indexOf(n); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
|
|
27
|
+
const CLAUDE = arg('--claude', path.join(os.homedir(), '.npm-global', 'bin', 'claude'));
|
|
28
|
+
const POS = new Set(['design', 'diagnosis']);
|
|
29
|
+
|
|
30
|
+
/** Deterministic stratified sample from the blind sets of an e2e key. */
|
|
31
|
+
export function stratify(key, { nPos = 12, nNegHinted = 6, nOther = 4, floor = 0 } = {}) {
|
|
32
|
+
const hinted = (k) => k.lane && (k.lane !== 'semantic' || k.topSimilarity >= floor);
|
|
33
|
+
const blind = key.filter((k) => /blind/.test(k.set)).sort((a, b) => a.qid.localeCompare(b.qid));
|
|
34
|
+
const every = (list, n) => (list.length <= n ? list : Array.from({ length: n }, (_, i) => list[Math.floor((i * list.length) / n)]));
|
|
35
|
+
const pos = every(blind.filter((k) => POS.has(k.category) && hinted(k)), nPos);
|
|
36
|
+
const negHinted = every(blind.filter((k) => !POS.has(k.category) && hinted(k)), nNegHinted);
|
|
37
|
+
const other = every(blind.filter((k) => !hinted(k)), nOther);
|
|
38
|
+
return [...pos, ...negHinted, ...other];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Which offered package (by full id or short name) the answer names, if any. Pure. */
|
|
42
|
+
export function mentioned(answer, offered) {
|
|
43
|
+
const text = String(answer || '');
|
|
44
|
+
const sentences = text.split(/(?<=[.!?])\s+/);
|
|
45
|
+
for (const id of offered || []) {
|
|
46
|
+
const short = id.replace(/^@[^/]+\//, '');
|
|
47
|
+
const re = (s) => new RegExp(`(?<![\\w@/-])${s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w/-])`, 'i');
|
|
48
|
+
// A scoped or hyphenated id is unambiguous anywhere. A bare short name ("migration", "typesafe")
|
|
49
|
+
// counts only in a sentence that also names rUv — "write the migration" is not a recommendation.
|
|
50
|
+
if (re(id).test(text) && (id.startsWith('@') || id.includes('-'))) return id;
|
|
51
|
+
if (sentences.some((s) => /\brUv\b|ruvector|ruvnet/i.test(s) && (re(id).test(s) || (short.length >= 5 && re(short).test(s))))) return id;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
57
|
+
if (isMain) {
|
|
58
|
+
const outDir = arg('--out', null);
|
|
59
|
+
if (!outDir || !arg('--key', null)) { console.error('--key and --out are required'); process.exit(2); }
|
|
60
|
+
const key = JSON.parse(fs.readFileSync(arg('--key'), 'utf8'));
|
|
61
|
+
const items = new Map();
|
|
62
|
+
for (const f of ['recommendation-eval.blind.v1.json', 'recommendation-eval.blind.v2.json']) {
|
|
63
|
+
for (const it of JSON.parse(fs.readFileSync(path.join(ROOT, 'evals', f), 'utf8')).items) items.set(`${f}:${it.id}`, it);
|
|
64
|
+
}
|
|
65
|
+
// --all-blinds: every blind prompt, in qid order (the decision run); otherwise the stratified sample.
|
|
66
|
+
const sample = process.argv.includes('--all-blinds')
|
|
67
|
+
? key.filter((k) => /blind/.test(k.set)).sort((a, b) => a.qid.localeCompare(b.qid))
|
|
68
|
+
: stratify(key, { nPos: Number(arg('--n-pos', 12)), nNegHinted: Number(arg('--n-neg-hinted', 6)), nOther: Number(arg('--n-other', 4)), floor: Number(arg('--floor', 0.532)) });
|
|
69
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'reco-host-'));
|
|
70
|
+
const kb = path.join(home, 'kb'); fs.mkdirSync(kb);
|
|
71
|
+
const cwd = path.join(home, 'cwd'); fs.mkdirSync(cwd);
|
|
72
|
+
const marker = path.join(home, 'marker'); fs.writeFileSync(marker, '');
|
|
73
|
+
const brainEnv = { RUVNET_BRAIN_HOME: home, KB_DIR: kb, KB_MODEL_CACHE: arg('--models', ''), XENOVA_PATH: arg('--xenova', '') };
|
|
74
|
+
const worker = spawn(process.execPath, [path.join(ROOT, 'kb', 'forge-mcp-all.mjs')],
|
|
75
|
+
{ env: { PATH: process.env.PATH, HOME: home, ...brainEnv, RUVNET_PACKAGE_RECOMMENDER: '1', RUVNET_BRAIN_IDLE_EXIT_MS: '0' }, stdio: ['pipe', 'pipe', 'ignore'] }); // idle exit off: load-gate waits must not retire the worker mid-run
|
|
76
|
+
for (const sig of ['SIGTERM', 'SIGINT']) process.once(sig, () => { worker.kill('SIGTERM'); fs.rmSync(home, { recursive: true, force: true }); process.exit(1); });
|
|
77
|
+
const rl = readline.createInterface({ input: worker.stdout });
|
|
78
|
+
const waiters = new Map();
|
|
79
|
+
rl.on('line', (l) => { try { const m = JSON.parse(l); waiters.get(m.id)?.(m); } catch { /* not ours */ } });
|
|
80
|
+
const call = (id, method) => new Promise((r) => { waiters.set(id, r); worker.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params: {} })}\n`); });
|
|
81
|
+
const settings = path.join(home, 'settings.json');
|
|
82
|
+
fs.writeFileSync(settings, JSON.stringify({ autoMemoryEnabled: false, hooks: { UserPromptSubmit: [{ hooks: [{ type: 'command', command: `"${process.execPath}" "${path.join(ROOT, 'plugin', 'scripts', 'unprompted-runtime.mjs')}" UserPromptSubmit`, timeout: 3 }] }] } }));
|
|
83
|
+
const rows = [];
|
|
84
|
+
try {
|
|
85
|
+
await call(1, 'initialize');
|
|
86
|
+
const warm = await call(2, 'brain/warmup');
|
|
87
|
+
if (!warm.result?.ready) throw new Error('worker warmup failed');
|
|
88
|
+
// LOAD GATE: never start a batch while the 1-minute load is above --max-load; wait (polling) instead.
|
|
89
|
+
// Each row records the load it ran at, so delivery can be read against load afterwards.
|
|
90
|
+
const maxLoad = Number(arg('--max-load', 1e9));
|
|
91
|
+
const batch = Number(arg('--batch', 11));
|
|
92
|
+
const waitForLoad = async () => {
|
|
93
|
+
let waited = 0;
|
|
94
|
+
while (os.loadavg()[0] > maxLoad) { await new Promise((res) => setTimeout(res, 20_000)); waited += 20; }
|
|
95
|
+
if (waited) console.log(`[load-gate] waited ${waited}s for load < ${maxLoad}`);
|
|
96
|
+
};
|
|
97
|
+
for (const [n, k] of sample.entries()) {
|
|
98
|
+
if (n % batch === 0) await waitForLoad();
|
|
99
|
+
const it = items.get(`${k.set}:${k.id}`);
|
|
100
|
+
const env = {
|
|
101
|
+
...process.env, ...brainEnv, CLAUDE_HOOK: '/usr/bin/true', CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1', RUVNET_PACKAGE_RECOMMENDER: '1',
|
|
102
|
+
RUVNET_UNPROMPTED_PRODUCERS: JSON.stringify([{ argv: [process.execPath, path.join(ROOT, 'plugin', 'scripts', 'advocacy-route.mjs')], feedStdin: true, channels: ['advocacy'] }]),
|
|
103
|
+
RUVNET_ADVOCACY_ROUTE_STATE: path.join(home, `state-${n}.json`), RUVNET_ADVOCACY_OUTCOMES: path.join(home, `outcomes-${n}.jsonl`),
|
|
104
|
+
RUVNET_ADVOCACY_ROUTE_ROOTS: path.join(home, 'none'), RUVNET_SETTINGS_FILE: path.join(home, 'user-settings.json'),
|
|
105
|
+
};
|
|
106
|
+
const r = spawnSync(CLAUDE, ['-p', it.prompt, '--output-format', 'stream-json', '--verbose', '--include-hook-events',
|
|
107
|
+
'--no-session-persistence', '--setting-sources', '', '--settings', settings, '--strict-mcp-config', '--tools', '',
|
|
108
|
+
'--max-turns', '1', '--max-budget-usd', '0.30',
|
|
109
|
+
'--append-system-prompt', 'This is a quick planning exchange with no tools: answer in at most five sentences with how you would approach the request.'],
|
|
110
|
+
{ cwd, env, encoding: 'utf8', timeout: 240_000, maxBuffer: 64e6 });
|
|
111
|
+
let injected = ''; let answer = '';
|
|
112
|
+
for (const line of String(r.stdout || '').split('\n')) {
|
|
113
|
+
let o; try { o = JSON.parse(line); } catch { continue; }
|
|
114
|
+
const blob = JSON.stringify(o);
|
|
115
|
+
// All three advocacy copies: the package lanes AND the closed catalogue ("capability advocacy").
|
|
116
|
+
const m = blob.match(/\[RuvNet Brain — (?:rUv (?:may already ship|already ships) this|capability advocacy)\][^"]*/);
|
|
117
|
+
if (m && !injected) injected = m[0];
|
|
118
|
+
if (o.type === 'result' && typeof o.result === 'string') answer = o.result;
|
|
119
|
+
}
|
|
120
|
+
const offered = injected ? [...new Set([...injected.matchAll(/(?:Consider )?(@[a-z0-9-]+\/[a-z0-9._-]+|[a-z0-9][a-z0-9._-]+) — /gi)].map((x) => x[1]))] : [];
|
|
121
|
+
rows.push({ qid: k.qid, set: k.set, id: k.id, category: k.category, load1m: +os.loadavg()[0].toFixed(1), exit: r.status, injected: Boolean(injected), offered, said: mentioned(answer, offered), answer: answer.slice(0, 1200) });
|
|
122
|
+
console.log(`${k.qid} ${k.category.padEnd(9)} hint=${injected ? 'yes' : 'no '} said=${rows.at(-1).said || '-'}`);
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
worker.kill('SIGTERM');
|
|
126
|
+
// Other sessions write under ~/.claude all the time; what THIS run could own is a project entry for
|
|
127
|
+
// its own temp cwd, so that is checked by name as well as the raw mtime list.
|
|
128
|
+
const ours = [path.join(os.homedir(), '.claude', 'projects')].flatMap((d) => { try { return fs.readdirSync(d).filter((n) => n.includes('reco-host')); } catch { return []; } });
|
|
129
|
+
let inDotClaudeJson = false; try { inDotClaudeJson = fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8').includes(path.basename(home)); } catch { /* absent */ }
|
|
130
|
+
const touched = spawnSync('find', [path.join(os.homedir(), '.claude'), '-newer', marker, '-maxdepth', '3'], { encoding: 'utf8' }).stdout.trim().split('\n').filter(Boolean);
|
|
131
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
132
|
+
fs.writeFileSync(path.join(outDir, 'real-host.json'), JSON.stringify({ claude: spawnSync(CLAUDE, ['--version'], { encoding: 'utf8' }).stdout.trim(), sample: rows.length, projectEntriesForThisRun: ours, cwdRecordedInDotClaudeJson: inDotClaudeJson, modifiedUnderDotClaudeByAnyProcess: touched.length, rows }, null, 1));
|
|
133
|
+
console.log(`project entries for this run under ~/.claude/projects: ${ours.length}; temp cwd in ~/.claude.json: ${inDotClaudeJson}; ~/.claude entries modified by ANY process meanwhile: ${touched.length}`);
|
|
134
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
/** Content-addressed corpus generation: the tag IS the archive's sha256. */
|
|
29
29
|
export const CORPUS_TAG_PATTERN = /^corpus-sha256-[0-9a-f]{64}$/;
|
|
30
|
-
/**
|
|
30
|
+
/** Product (code) release: a plain semver tag. */
|
|
31
31
|
export const CODE_TAG_PATTERN = /^v\d+\.\d+\.\d+$/;
|
|
32
32
|
|
|
33
33
|
export function releaseKind(tag) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// release-environment-policy.mjs — the ONE verdict on the npm-scoped Production environment
|
|
2
|
+
// ("Production – ruvnet-brain"), as GitHub's environments API reports it.
|
|
3
|
+
//
|
|
4
|
+
// The design (CONTRIBUTING.md, "What replaces a human approval"): the environment is a scoping boundary —
|
|
5
|
+
// a branch policy (protected branches only) and admins cannot bypass — and NO person is part of it. A
|
|
6
|
+
// required reviewer coming back (someone re-adds one in the GitHub UI) silently re-inserts a human click
|
|
7
|
+
// into every release; it must turn single-source C3 red, not pass because the other two rules still hold.
|
|
8
|
+
export const PRODUCTION_ENVIRONMENT = 'Production – ruvnet-brain';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {object|null} environment one element of `GET /repos/{o}/{r}/environments` `.environments[]`
|
|
12
|
+
* @returns {{ ok: boolean, problems: string[], detail: string }}
|
|
13
|
+
*/
|
|
14
|
+
export function productionEnvironmentVerdict(environment) {
|
|
15
|
+
if (!environment || typeof environment !== 'object') {
|
|
16
|
+
return { ok: false, problems: [`environment "${PRODUCTION_ENVIRONMENT}" was not found`], detail: 'not found' };
|
|
17
|
+
}
|
|
18
|
+
const rules = Array.isArray(environment.protection_rules) ? environment.protection_rules : [];
|
|
19
|
+
const branchPolicies = rules.filter((rule) => rule?.type === 'branch_policy').length;
|
|
20
|
+
const reviewerRules = rules.filter((rule) => rule?.type === 'required_reviewers');
|
|
21
|
+
const reviewers = reviewerRules.flatMap((rule) => (Array.isArray(rule.reviewers) ? rule.reviewers : [])
|
|
22
|
+
.map((entry) => entry?.reviewer?.login || entry?.reviewer?.slug || entry?.reviewer?.name || entry?.type || 'unknown'));
|
|
23
|
+
const problems = [];
|
|
24
|
+
if (environment.can_admins_bypass !== false) problems.push('admins can bypass the environment (can_admins_bypass is not false)');
|
|
25
|
+
if (branchPolicies === 0) problems.push('no branch_policy rule (any branch could deploy)');
|
|
26
|
+
if (reviewerRules.length) problems.push(`a required reviewer is back on the environment (${reviewers.join(', ') || 'unnamed'}) — no human approves a release`);
|
|
27
|
+
return {
|
|
28
|
+
ok: problems.length === 0,
|
|
29
|
+
problems,
|
|
30
|
+
detail: `can_admins_bypass=${environment.can_admins_bypass}; branch_policy rules: ${branchPolicies}; required_reviewers rules: ${reviewerRules.length}`
|
|
31
|
+
+ (problems.length ? `\n${problems.join('\n')}` : ''),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -15,6 +15,8 @@ import os from 'node:os';
|
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { readCheckpoint, checkpointStaleness } from './loop-checkpoint.mjs';
|
|
18
|
+
import { PRODUCTION_ENVIRONMENT, productionEnvironmentVerdict } from './release-environment-policy.mjs';
|
|
19
|
+
import { approvalHits, LOCAL_INSTRUCTION_DRIFT } from './human-approval-phrases.mjs';
|
|
18
20
|
|
|
19
21
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
22
|
const HOME = os.homedir();
|
|
@@ -33,8 +35,6 @@ const HISTORY = /^(docs\/(adr|ddd|research|audits|reviews|qe)\/|CHANGELOG\.md$|P
|
|
|
33
35
|
// kb/ is corpus content (primers/cards describing OTHER projects' own commands) and docs/issues/
|
|
34
36
|
// are upstream bug reports quoting other configs — both are data, not instructions to this project.
|
|
35
37
|
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;
|
|
38
38
|
const grepIn = (files, re) => files.flatMap((f) => read(f).split('\n')
|
|
39
39
|
.map((l, i) => (re.test(l) ? `${f}:${i + 1}: ${l.trim().slice(0, 140)}` : null)).filter(Boolean));
|
|
40
40
|
const none = (hits) => ({ ok: hits.length === 0, detail: hits.slice(0, 8).join('\n') || 'none' });
|
|
@@ -85,7 +85,10 @@ const checks = [
|
|
|
85
85
|
// The owner is not in the release loop (2026-09-30): the gates are machine gates, and no instruction may
|
|
86
86
|
// route a release through a person clicking in GitHub. B12 applies the same idea to the local CLAUDE.md/AGENTS.md.
|
|
87
87
|
{ id: 'B6', area: 'rules', scope: 'repo', title: 'No instruction puts a human approval click (or a required reviewer) in the release path',
|
|
88
|
-
|
|
88
|
+
// Matched without markdown emphasis; a negation counts only in the matched phrase's own clause
|
|
89
|
+
// (scripts/human-approval-phrases.mjs) — "…; never skip it" does not excuse "the owner approves …".
|
|
90
|
+
run: () => none(instructions.flatMap((f) => read(f).split('\n')
|
|
91
|
+
.map((l, i) => (approvalHits(l).length ? `${f}:${i + 1}: ${l.trim().slice(0, 140)}` : null)).filter(Boolean))) },
|
|
89
92
|
{ id: 'B7', area: 'rules', scope: 'repo', title: 'Model IDs are selected only in their owner modules',
|
|
90
93
|
run: () => {
|
|
91
94
|
const re = /['"`](claude-(fable|opus|sonnet|haiku)-[0-9][a-z0-9.-]*|gpt-[0-9][a-z0-9.-]*)['"`]/;
|
|
@@ -131,9 +134,9 @@ const checks = [
|
|
|
131
134
|
{ id: 'B12', area: 'rules', scope: 'machine', title: 'The local (untracked) CLAUDE.md / AGENTS.md carry no contradicting instruction',
|
|
132
135
|
run: () => {
|
|
133
136
|
const main = path.join(HOME, 'Code/ruvnet-brain');
|
|
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;
|
|
135
137
|
return none(['CLAUDE.md', 'AGENTS.md'].flatMap((f) => readAbs(path.join(main, f)).split('\n')
|
|
136
|
-
.map((l, i) => (
|
|
138
|
+
.map((l, i) => (approvalHits(l, LOCAL_INSTRUCTION_DRIFT, { allowNegation: false }).length
|
|
139
|
+
? `${f}:${i + 1}: ${l.trim().slice(0, 120)}` : null)).filter(Boolean)));
|
|
137
140
|
} },
|
|
138
141
|
|
|
139
142
|
// C — one release path
|
|
@@ -162,12 +165,14 @@ const checks = [
|
|
|
162
165
|
{ id: 'C4', area: 'release', scope: 'machine', title: 'The corpus gist job has its authenticated token (RUVNET_GISTS_TOKEN)',
|
|
163
166
|
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(', ') }; } },
|
|
164
167
|
{ 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)',
|
|
168
|
+
// One verdict (scripts/release-environment-policy.mjs): branch policy present, admins cannot bypass,
|
|
169
|
+
// and NO required reviewer — a reviewer re-added in the GitHub UI re-inserts a human click into every release.
|
|
165
170
|
run: () => {
|
|
166
|
-
const r = spawnSync('gh', ['api', 'repos/stuinfla/ruvnet-brain/environments', '
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return { ok: r.status === 0 &&
|
|
171
|
+
const r = spawnSync('gh', ['api', 'repos/stuinfla/ruvnet-brain/environments'], { encoding: 'utf8' });
|
|
172
|
+
let environment = null;
|
|
173
|
+
try { environment = (JSON.parse(r.stdout).environments || []).find((e) => e?.name === PRODUCTION_ENVIRONMENT) || null; } catch { /* unreadable */ }
|
|
174
|
+
const verdict = productionEnvironmentVerdict(environment);
|
|
175
|
+
return { ok: r.status === 0 && verdict.ok, detail: `${verdict.detail}${r.status === 0 ? '' : ` (${r.stderr.trim()})`}` };
|
|
171
176
|
} },
|
|
172
177
|
|
|
173
178
|
// D — one corpus / update path
|
|
@@ -37,8 +37,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
37
37
|
|
|
38
38
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
39
39
|
const DIR = path.join(ROOT, 'plugin', 'commands');
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// 2026-10-01 (owner): RuvNet Brain abbreviates as RNB, so the console is `/rnbc` and it is the
|
|
41
|
+
// producer now. `/rnb` was added as a short form; the older spellings stay as aliases because people
|
|
42
|
+
// (and the installer's plugin-presence probe, which looks for rvbc.md) still use them.
|
|
43
|
+
export const CANONICAL = 'rnbc.md';
|
|
44
|
+
export const ALIASES = Object.freeze(['rnb.md', 'rvbc.md', 'rvcb.md', 'brain-console.md', 'configure.md']);
|
|
42
45
|
const CHECK = process.argv.includes('--check');
|
|
43
46
|
|
|
44
47
|
/** Split `---\n…\n---\n` frontmatter from the body. Both are returned verbatim. */
|
package/scripts/wired-check.mjs
CHANGED
|
@@ -76,8 +76,23 @@ const STANDALONE = [
|
|
|
76
76
|
+ 'produce-questions, validate-labels) are each wired to a real caller; this driver is the human entry point.'],
|
|
77
77
|
['gate', 'retired automatic-hook helper and manual benchmark retained for explicit human use; no workflow or scheduler invokes this expensive command'],
|
|
78
78
|
['route-gold-rank', 'human-run measurement harness: routes question sets through kb/forge-ask-all.mjs planSourceRoute (no model) and reports where the gold store lands, with Wilson intervals; nothing to schedule'],
|
|
79
|
-
['
|
|
79
|
+
['abstain-trace', 'human-run measurement harness: traces why the reranker abstains on novice needs whose gold repository was searched (pool membership, cross-encoder on production text, best chunk and gold span); minutes of model time, never scheduled'],
|
|
80
|
+
['doc2query-generate', 'build-time generator (ADR-099 arm A): newcomer-style questions per documentation file through the subscription host; run where the corpus is built, never on a customer machine; not yet in the corpus pipeline'],
|
|
81
|
+
['doc2query-reach', 'human-run measurement harness: builds per-store RVF entry indexes from doc2query output and measures how often they bring the gold file into the pool; nothing to schedule'],
|
|
82
|
+
['sona-query-adapter-eval', 'human-run experiment (ADR-099 arm B): trains a SONA MicroLoRA query adapter on the need-set train split and measures held-out dense rank of the gold file; needs @ruvector/sona from a scratch install; nothing to schedule'],
|
|
83
|
+
['judge-train', 'human-run trainer: fits the learned judge (ADR-099 arm C) offline on the need-set train split from recorded cross-encoder pools and reports held-out; writes kb/judge-weights.json only on request; nothing to schedule'],
|
|
84
|
+
['need-set-split', 'human-run measurement tool: writes the frozen, repository-stratified train / held-out split every learning arm (ADR-099) trains and is measured on; nothing to schedule'],
|
|
85
|
+
['abstain-threshold-sweep', 'human-run measurement harness: replays measured runs at other abstain thresholds (no model) and reports confident hits, confident misses, off-topic abstain and held-out routed with Wilson intervals; nothing to schedule'],
|
|
86
|
+
['route-latency-warm','human-run measurement harness: paired warm latency of two or more search runtimes in one process, load-gated, with paired bootstrap intervals; minutes to hours of model time, never scheduled'],
|
|
80
87
|
['route-index-memory', 'human-run measurement harness: retained memory and cold/warm time of the router metadata index (needs node --expose-gc); nothing to schedule'],
|
|
88
|
+
['recommendation-eval', 'human-run measurement harness (ADR-0093): scores the package recommender against evals/recommendation-eval*.json with Wilson intervals; its frozen numbers are asserted by tests/unit/package-recommender.test.mjs, which imports evaluate()'],
|
|
89
|
+
['recommendation-e2e', 'human-run measurement harness (ADR-0093 rev 2): spawns the real search worker in a temp brain home and the real hook producer per eval prompt; load-gated and model-bound, so never scheduled'],
|
|
90
|
+
['recommendation-judge-score', 'human-run measurement harness (ADR-0093 rev 2): scores a host-model judge\'s picks against an e2e judge key with Wilson intervals; nothing to schedule'],
|
|
91
|
+
['recommendation-floor', 'human-run measurement harness (ADR-0093 rev 3): derives the semantic similarity floor on the tuning set only and applies it to judged e2e runs; nothing to schedule'],
|
|
92
|
+
['recommendation-real-host', 'human-run measurement harness (ADR-0093 rev 3): fresh `claude -p` sessions with this checkout\'s hook against a warm worker; spends model budget, never scheduled'],
|
|
93
|
+
['recommendation-real-host-score', 'human-run measurement harness (ADR-0093 rev 3): scores a real-host run and its agreement with the simulated host; nothing to schedule'],
|
|
94
|
+
['recommendation-latency','human-run measurement harness (ADR-0093): paired cold-process latency of advocacy-route with the package flag off vs on; load-sensitive, so never scheduled'],
|
|
95
|
+
['package-cards', 'ADR-0093 (Proposed) package-card generator, run by hand to refresh plugin/scripts/package-cards.json from the installed corpus. NOT YET NIGHTLY: the bundle step that would seal package-cards.json into the signed corpus is ADR-0093 phase 2 and is deliberately unbuilt while the recommender is default-off'],
|
|
81
96
|
['dream-issue-gate','pure Dream Cycle disposition policy; invoked by the external issue adapter, never a GitHub writer'],
|
|
82
97
|
['sync-census', 'explicit maintainer census writer; a destructive source-to-surface refresh is never scheduled'],
|
|
83
98
|
['customer-state-matrix', 'human-run release-qualification harness (2026-09-30): applies ONE published release through the real '
|
|
@@ -116,7 +131,7 @@ const STANDALONE = [
|
|
|
116
131
|
+ '`--window`). Its former automatic 36-hour dump was deliberately retired from the machine-wide '
|
|
117
132
|
+ 'SessionStart hook after 61KB of output hid the current checkpoint; SessionStart now prints the '
|
|
118
133
|
+ 'checkpoint plus a compact lesson index and directs topic recall through `ruflo memory search`.'],
|
|
119
|
-
['onboarding-console', 'human-started local server reached through the shipped `/rvbc`, `/rvcb`, '
|
|
134
|
+
['onboarding-console', 'human-started local server reached through the shipped `/rnbc`, `/rnb`, `/rvbc`, `/rvcb`, '
|
|
120
135
|
+ '`/brain-console`, and `/ruvnet-brain:configure` command documents. The command host executes '
|
|
121
136
|
+ 'those instructions; there is intentionally no in-process source caller for a long-running CLI.'],
|
|
122
137
|
['ingest-meeting', 'one-shot ingestion, run by hand'],
|