ruvnet-brain 4.3.27 → 4.3.29
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 +1 -1
- package/bin/install.mjs +74 -7
- package/console/app.js +7 -2
- package/kb/corpus-release-identity.mjs +73 -0
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/mcp/managed-cli-interface.mjs +83 -9
- package/plugin/scripts/advocacy-route.mjs +4 -1
- package/plugin/scripts/capacity-aware-parallel-work.mjs +4 -0
- package/plugin/scripts/continuation-gate.mjs +71 -0
- package/plugin/scripts/decision-gate.mjs +29 -57
- package/plugin/scripts/ground-ruvnet.sh +77 -4
- package/plugin/scripts/grounding-stamp.sh +34 -5
- package/plugin/scripts/grounding-turn-gate.mjs +8 -2
- package/plugin/scripts/grounding-turn-mark.mjs +6 -1
- package/plugin/scripts/hook-input.mjs +54 -0
- package/plugin/scripts/hook-shim.mjs +10 -0
- package/plugin/scripts/memory-doctor.mjs +43 -0
- package/plugin/scripts/nightly-controller.mjs +6 -1
- package/plugin/scripts/project-progression-contract.mjs +17 -0
- package/plugin/scripts/project-progression-hook.mjs +18 -5
- package/plugin/scripts/project-progression-producer.mjs +16 -12
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +17 -0
- package/plugin/scripts/session-start-core.mjs +11 -5
- package/plugin/scripts/session-start-update-plane.mjs +5 -1
- package/plugin/scripts/unprompted-runtime.mjs +29 -8
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +1 -1
- package/scripts/adr-072-completion.mjs +2 -1
- package/scripts/calibrate-router.mjs +6 -6
- package/scripts/candidate-host-evidence.mjs +44 -16
- package/scripts/corpus-reconcile.mjs +32 -3
- package/scripts/correction-detect.mjs +10 -11
- package/scripts/dispatch-receipt.mjs +2 -2
- package/scripts/dual-host-deliberation.mjs +3 -2
- package/scripts/execution-policy.mjs +10 -2
- package/scripts/gen-console-images.mjs +0 -1
- package/scripts/gen-images.mjs +0 -4
- package/scripts/host-install-matrix.mjs +60 -14
- package/scripts/independent-review-receipt.mjs +7 -8
- package/scripts/ingest-repo.mjs +45 -1
- package/scripts/learning-replay-cli.mjs +2 -1
- package/scripts/learning-replay-fixture.mjs +2 -1
- package/scripts/learnings.mjs +19 -5
- package/scripts/lesson-migrate-agentdb.mjs +635 -0
- package/scripts/loop-checkpoint.mjs +37 -1
- package/scripts/metaharness-receipts.mjs +3 -2
- package/scripts/nightly-two-run-proof.mjs +6 -6
- package/scripts/nightly-watchdog.mjs +10 -2
- package/scripts/onboarding-console.mjs +98 -49
- package/scripts/oracle/producer-hosts.mjs +2 -1
- package/scripts/prepublication-evidence.mjs +8 -0
- package/scripts/private-overlay.mjs +31 -9
- package/scripts/public-verification-aggregate.mjs +4 -3
- package/scripts/publication-receipt.mjs +80 -30
- package/scripts/qe/agentic-qe-4.3.mjs +0 -1
- package/scripts/qe/ux-suite.mjs +17 -5
- package/scripts/rebuild-gists-from-receipts.mjs +1 -1
- package/scripts/reconcile-project.mjs +0 -0
- package/scripts/release-qualification-contract.mjs +11 -1
- package/scripts/release.mjs +34 -82
- package/scripts/retrieval-canary.mjs +3 -2
- package/scripts/review-model-defaults.mjs +26 -0
- package/scripts/route-cheap.mjs +20 -7
- package/scripts/router-utilization.mjs +5 -5
- package/scripts/rvf-generation.mjs +68 -2
- package/scripts/single-source-check.mjs +270 -0
- package/scripts/staged-host-verifier.mjs +2 -1
- package/scripts/subscription-hosts.mjs +4 -0
- package/scripts/sync-version.mjs +22 -0
- package/scripts/trismart.mjs +3 -3
- package/scripts/wired-check.mjs +40 -4
- package/console/assets/memory.webp +0 -0
- package/plugin/scripts/version-bump-gate.sh +0 -124
- package/scripts/qe/aggregate-4.3.mjs +0 -42
- package/scripts/release-convergence-watchdog.mjs +0 -112
- package/scripts/stamp-existing-rvf-generations.mjs +0 -53
- package/scripts/verify-channels.mjs +0 -196
|
@@ -11,6 +11,29 @@ import path from 'node:path';
|
|
|
11
11
|
import { getVersion, getVersionTag } from './version.mjs';
|
|
12
12
|
|
|
13
13
|
export const RVF_GENERATIONS_FILE = 'RVF-GENERATIONS.json';
|
|
14
|
+
export const RUNTIME_LEDGER_KIND = 'ruvnet-brain-runtime-generation-ledger';
|
|
15
|
+
|
|
16
|
+
// S4 (ONE PROVENANCE RECORD) — RECONCILING THE TWO "schemaVersion 2" MEANINGS.
|
|
17
|
+
//
|
|
18
|
+
// Two independent writers had drifted into calling different shapes "schemaVersion 2":
|
|
19
|
+
// "Schema A" — THIS module's writeRvfGeneration wrote schemaVersion 1, no `kind`, no
|
|
20
|
+
// `sourceSnapshot`, one store touched per call (the dev-time incremental refresh ledger).
|
|
21
|
+
// "Schema B" — scripts/build-bundle.mjs's projectStoreViews independently writes
|
|
22
|
+
// `{ schemaVersion: 2, kind: 'ruvnet-brain-runtime-generation-ledger', brainVersion,
|
|
23
|
+
// releaseTag, sourceSnapshot, stores }` (the release-time ledger) — and
|
|
24
|
+
// plugin/scripts/coverage-integrity.mjs's release validation ALREADY requires exactly this
|
|
25
|
+
// shape (`schemaVersion !== 2` / `kind !== 'ruvnet-brain-runtime-generation-ledger'` are hard
|
|
26
|
+
// failures there, confirmed by reading it directly) for the tree an installed brain carries.
|
|
27
|
+
// Schema B is the one kept: it is a strict superset (Schema A's per-store row shape is a subset
|
|
28
|
+
// of Schema B's), and it is what the release-time validator already enforces on every installed
|
|
29
|
+
// tree. Schema A is retired: writeRvfGeneration below now emits Schema B's envelope, carrying
|
|
30
|
+
// `sourceSnapshot` forward from whatever the ledger already had (null until a release build sets
|
|
31
|
+
// it — build-bundle.mjs's own construction is untouched and still wins at release time; this
|
|
32
|
+
// module's schemaVersion bump changes nothing downstream, since nothing reads the dev-time
|
|
33
|
+
// ledger's schemaVersion — verified against every consumer: validateSelectedRvfGenerations/
|
|
34
|
+
// verifyRvfGenerations below check individual fields, never schemaVersion; build-bundle.mjs
|
|
35
|
+
// reads the dev-time ledger via readRvfGenerations only to look up per-store rows by name, then
|
|
36
|
+
// constructs ITS OWN new ledger object from scratch).
|
|
14
37
|
|
|
15
38
|
export function canonicalRvfStores(dir) {
|
|
16
39
|
return fs.readdirSync(dir)
|
|
@@ -42,7 +65,8 @@ export function sha256File(file) {
|
|
|
42
65
|
export function readRvfGenerations(dir) {
|
|
43
66
|
const file = path.join(dir, RVF_GENERATIONS_FILE);
|
|
44
67
|
if (!fs.existsSync(file)) {
|
|
45
|
-
return { schemaVersion:
|
|
68
|
+
return { schemaVersion: 2, kind: RUNTIME_LEDGER_KIND, brainVersion: getVersion(),
|
|
69
|
+
releaseTag: getVersionTag(), sourceSnapshot: null, stores: {} };
|
|
46
70
|
}
|
|
47
71
|
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
48
72
|
parsed.stores ||= {};
|
|
@@ -56,15 +80,22 @@ export function writeRvfGeneration({
|
|
|
56
80
|
model,
|
|
57
81
|
dimensions,
|
|
58
82
|
sourceCommit = null,
|
|
83
|
+
sourceRepo = null,
|
|
84
|
+
sourceDescribe = null,
|
|
59
85
|
builtUtc = new Date().toISOString(),
|
|
60
86
|
previousDir = dir,
|
|
61
87
|
}) {
|
|
62
88
|
const rvfPath = path.join(dir, rvfFile);
|
|
63
89
|
if (!fs.existsSync(rvfPath)) throw new Error(`cannot stamp missing RVF: ${rvfPath}`);
|
|
64
90
|
const manifest = readRvfGenerations(previousDir);
|
|
65
|
-
manifest.schemaVersion =
|
|
91
|
+
manifest.schemaVersion = 2;
|
|
92
|
+
manifest.kind = RUNTIME_LEDGER_KIND;
|
|
66
93
|
manifest.brainVersion = getVersion();
|
|
67
94
|
manifest.releaseTag = getVersionTag();
|
|
95
|
+
// Carried forward, never invented here: this is the git sha of the CODE checkout that
|
|
96
|
+
// assembles a release (build-bundle.mjs's projectStoreViews sets it fresh at release time).
|
|
97
|
+
// The dev-time incremental refresh this function serves does not know that value yet.
|
|
98
|
+
manifest.sourceSnapshot = manifest.sourceSnapshot ?? null;
|
|
68
99
|
manifest.stores[store] = {
|
|
69
100
|
file: rvfFile,
|
|
70
101
|
sha256: sha256File(rvfPath),
|
|
@@ -72,12 +103,47 @@ export function writeRvfGeneration({
|
|
|
72
103
|
model,
|
|
73
104
|
dimensions,
|
|
74
105
|
sourceCommit,
|
|
106
|
+
// S4 (ONE PROVENANCE RECORD): the repo identity a SOURCE.json entry needs, carried alongside
|
|
107
|
+
// the byte identity this ledger has always recorded, so SOURCE.json can be projected entirely
|
|
108
|
+
// FROM this ledger (projectSourceStore below) rather than a caller's own second copy of the
|
|
109
|
+
// same facts. Optional and omitted (not written as null) so a caller that does not pass them
|
|
110
|
+
// gets a ledger row with no dangling nulls.
|
|
111
|
+
...(sourceRepo !== null ? { sourceRepo } : {}),
|
|
112
|
+
...(sourceDescribe !== null ? { sourceDescribe } : {}),
|
|
75
113
|
builtUtc,
|
|
76
114
|
};
|
|
77
115
|
fs.writeFileSync(path.join(dir, RVF_GENERATIONS_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
78
116
|
return manifest.stores[store];
|
|
79
117
|
}
|
|
80
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Project ONE SOURCE.json store entry from its ledger row — the ONE place identity fields
|
|
121
|
+
* (sourceRepo, sourceCommit, sourceDescribe, builtUtc) are read FROM the ledger rather than
|
|
122
|
+
* independently recomputed by a caller (S4: ONE PROVENANCE RECORD).
|
|
123
|
+
*
|
|
124
|
+
* Deliberately minimal: it projects ONLY the identity fields the ledger itself carries.
|
|
125
|
+
* `updater` supplies every NON-identity field a caller's SOURCE.json shape wants
|
|
126
|
+
* (canonicalManifestUrl, canonicalBundleUrl, selfUpdate, builder, updateManaged, origin, …) —
|
|
127
|
+
* spread first so identity fields read from `generation` always win, the same adapter pattern
|
|
128
|
+
* scripts/build-bundle.mjs's projectStoreViews already uses for the release-time case ("only its
|
|
129
|
+
* non-identity updater fields are borrowed per store; sourceCommit/builtUtc are always bound from
|
|
130
|
+
* the selected generation record, never independently trusted from the updater's own copy"). This
|
|
131
|
+
* function does not default those non-identity fields itself: a public-bundle store and a private
|
|
132
|
+
* overlay store want genuinely different shapes (a private store has no selfUpdate command at
|
|
133
|
+
* all, since forge-update.mjs never fetches it) — each caller states what IT needs.
|
|
134
|
+
*/
|
|
135
|
+
export function projectSourceStore(name, generation, updater = {}) {
|
|
136
|
+
if (!generation) throw new Error(`projectSourceStore: no ledger generation record for ${name}`);
|
|
137
|
+
return {
|
|
138
|
+
...updater,
|
|
139
|
+
kbName: updater.kbName || name,
|
|
140
|
+
sourceRepo: generation.sourceRepo ?? updater.sourceRepo ?? null,
|
|
141
|
+
sourceCommit: generation.sourceCommit ?? null,
|
|
142
|
+
sourceDescribe: generation.sourceDescribe ?? updater.sourceDescribe ?? null,
|
|
143
|
+
builtUtc: generation.builtUtc,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
81
147
|
export function verifyRvfGenerations(dir, {
|
|
82
148
|
version = getVersion(),
|
|
83
149
|
releaseTag = getVersionTag(),
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/single-source-check.mjs — the single-source contract, as executable checks.
|
|
3
|
+
//
|
|
4
|
+
// Every entry is one defect found in the 2026-09-26 audit: two or more versions of the same rule,
|
|
5
|
+
// process, code path or fact. Each check's exit is the verdict — nobody's word is. `repo` checks run
|
|
6
|
+
// in CI (canonical-qa) so a contradiction that is removed can never silently come back; `machine`
|
|
7
|
+
// checks describe this maintainer Mac and run with --machine.
|
|
8
|
+
//
|
|
9
|
+
// node scripts/single-source-check.mjs # repo checks (CI)
|
|
10
|
+
// node scripts/single-source-check.mjs --machine # repo + machine checks
|
|
11
|
+
// node scripts/single-source-check.mjs --json
|
|
12
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
13
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { readCheckpoint, checkpointStaleness } from './loop-checkpoint.mjs';
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const HOME = os.homedir();
|
|
21
|
+
const MACHINE = process.argv.includes('--machine');
|
|
22
|
+
const JSON_OUT = process.argv.includes('--json');
|
|
23
|
+
|
|
24
|
+
const git = (...a) => execFileSync('git', a, { cwd: ROOT, encoding: 'utf8', maxBuffer: 1 << 26 }).trim();
|
|
25
|
+
const tracked = git('ls-files').split('\n').filter(Boolean);
|
|
26
|
+
const read = (f) => { try { return readFileSync(path.join(ROOT, f), 'utf8'); } catch { return ''; } };
|
|
27
|
+
const readAbs = (f) => { try { return readFileSync(f, 'utf8'); } catch { return ''; } };
|
|
28
|
+
const json = (s) => { try { return JSON.parse(s); } catch { return {}; } };
|
|
29
|
+
|
|
30
|
+
// Instruction surfaces: files that tell a person or an agent HOW to operate this project.
|
|
31
|
+
// Decision records (ADR/DDD), research notes and append-only logs are history and are excluded.
|
|
32
|
+
const HISTORY = /^(docs\/(adr|ddd|research|audits|reviews|qe)\/|CHANGELOG\.md$|PROGRESS\.md$|plugin\/docs\/RELEASE-NOTES|\.release-evidence\/|tests\/|evals\/)/;
|
|
33
|
+
// kb/ is corpus content (primers/cards describing OTHER projects' own commands) and docs/issues/
|
|
34
|
+
// are upstream bug reports quoting other configs — both are data, not instructions to this project.
|
|
35
|
+
const instructions = tracked.filter((f) => /\.md$/.test(f) && !HISTORY.test(f) && !/^(kb|docs\/issues)\//.test(f));
|
|
36
|
+
const grepIn = (files, re) => files.flatMap((f) => read(f).split('\n')
|
|
37
|
+
.map((l, i) => (re.test(l) ? `${f}:${i + 1}: ${l.trim().slice(0, 140)}` : null)).filter(Boolean));
|
|
38
|
+
const none = (hits) => ({ ok: hits.length === 0, detail: hits.slice(0, 8).join('\n') || 'none' });
|
|
39
|
+
|
|
40
|
+
const pkg = json(read('package.json'));
|
|
41
|
+
const workflows = tracked.filter((f) => /^\.github\/workflows\/.+\.ya?ml$/.test(f));
|
|
42
|
+
|
|
43
|
+
const checks = [
|
|
44
|
+
// A — one copy of the code
|
|
45
|
+
{ id: 'A1', area: 'code', scope: 'repo', title: 'Every module is wired or has a stated reason (the repo\'s own wired-check audit)',
|
|
46
|
+
run: () => { const r = spawnSync(process.execPath, [path.join(ROOT, 'scripts/wired-check.mjs'), '--check'], { cwd: ROOT, encoding: 'utf8' });
|
|
47
|
+
return { ok: r.status === 0, detail: `${r.stdout}${r.stderr}`.split('\n').filter((l) => /UNWIRED|✗/.test(l)).slice(0, 6).join('\n') }; } },
|
|
48
|
+
{ id: 'A2', area: 'code', scope: 'repo', title: 'No byte-identical duplicate files (outside test fixtures)',
|
|
49
|
+
run: () => {
|
|
50
|
+
const seen = new Map(); const dup = [];
|
|
51
|
+
for (const f of tracked) {
|
|
52
|
+
if (!/\.(mjs|js|cjs|sh|md|yml|json)$/.test(f) || /^tests\/|fixtures?\//.test(f)) continue;
|
|
53
|
+
if (f.endsWith('RELEASE-NOTES-4.0.md')) continue; // intentional pair, equality enforced by tests/integration/whats-new-installed.test.mjs
|
|
54
|
+
const t = read(f); if (t.length < 200) continue;
|
|
55
|
+
if (seen.has(t)) dup.push(`${f} == ${seen.get(t)}`); else seen.set(t, f);
|
|
56
|
+
}
|
|
57
|
+
return none(dup);
|
|
58
|
+
} },
|
|
59
|
+
|
|
60
|
+
// B — one version of the rules
|
|
61
|
+
{ id: 'B1', area: 'rules', scope: 'repo', title: 'CONTRIBUTING.md is the only operating rulebook (no parallel process docs)',
|
|
62
|
+
run: () => none(['docs/QA-RELEASE-PROCESS.md', 'docs/NIGHTLY-REFRESH.md'].filter((f) => tracked.includes(f))) },
|
|
63
|
+
{ id: 'B2', area: 'rules', scope: 'repo', title: 'No instruction tells anyone to publish locally',
|
|
64
|
+
run: () => none(grepIn(instructions.filter((f) => f !== 'CONTRIBUTING.md'), /release\.mjs --publish|\bnpm publish\b|gh release create/)
|
|
65
|
+
.filter((l) => !/\b(never|not|unusable|refus|intentionally|blocked|cannot|forbid|only through|provenance|ahead of the last)/i.test(l))) },
|
|
66
|
+
{ id: 'B3', area: 'rules', scope: 'repo', title: 'No instruction uses npx for ruflo / claude-flow (one global ruflo)',
|
|
67
|
+
run: () => none(grepIn(instructions, /npx (-y )?(@claude-flow|claude-flow|ruflo)\b/)
|
|
68
|
+
.filter((l) => !/\bnever\b|console\/CONTRACT\.md:\d+: +"event"/i.test(l))) },
|
|
69
|
+
{ id: 'B4', area: 'rules', scope: 'repo', title: 'No live file cites a workflow that does not exist',
|
|
70
|
+
run: () => {
|
|
71
|
+
const exists = new Set(workflows.map((w) => path.basename(w)));
|
|
72
|
+
const live = [...instructions, ...workflows, ...tracked.filter((f) => /^(scripts|plugin|bin|kb)\/.+\.(mjs|sh)$/.test(f))];
|
|
73
|
+
return none(live.flatMap((f) => [...read(f).matchAll(/\b([a-z0-9-]+\.ya?ml)\b/g)].map((m) => m[1])
|
|
74
|
+
.filter((n) => /release|corpus|publish|nightly|preflight|grading/.test(n) && !exists.has(n))
|
|
75
|
+
.map((n) => `${f}: ${n}`)));
|
|
76
|
+
} },
|
|
77
|
+
{ id: 'B5', area: 'rules', scope: 'repo', title: 'Every `npm run X` in an instruction exists in package.json',
|
|
78
|
+
run: () => {
|
|
79
|
+
const scripts = new Set([...Object.keys(pkg.scripts || {}), ...Object.keys(json(read('kb/package.json')).scripts || {})]);
|
|
80
|
+
return none(instructions.flatMap((f) => [...read(f).matchAll(/npm run ([a-z0-9:_-]+)/g)]
|
|
81
|
+
.filter((m) => !scripts.has(m[1])).map((m) => `${f}: npm run ${m[1]}`)));
|
|
82
|
+
} },
|
|
83
|
+
{ id: 'B6', area: 'rules', scope: 'repo', title: 'No instruction grants standing authority to publish without the owner',
|
|
84
|
+
run: () => none(grepIn(instructions, /standing authori[sz]ation permits|no separate human reviewer click/i)) },
|
|
85
|
+
{ id: 'B7', area: 'rules', scope: 'repo', title: 'Model IDs are selected only in their owner modules',
|
|
86
|
+
run: () => {
|
|
87
|
+
const re = /['"`](claude-(fable|opus|sonnet|haiku)-[0-9][a-z0-9.-]*|gpt-[0-9][a-z0-9.-]*)['"`]/;
|
|
88
|
+
// Owners (the single definition sites) and non-selecting mentions, each with its reason.
|
|
89
|
+
const allowed = new Map([
|
|
90
|
+
['scripts/review-model-defaults.mjs', 'owner: review / dual-host defaults'],
|
|
91
|
+
['scripts/route-cheap.mjs', 'owner: Claude cost ladder'],
|
|
92
|
+
['plugin/scripts/identifier-preflight.mjs', 'doc comment quoting a historical incident'],
|
|
93
|
+
['scripts/goldie-research.mjs', 'family prefix match against the live catalog'],
|
|
94
|
+
['scripts/routing-flywheel.mjs', 'comment example of id normalisation'],
|
|
95
|
+
['scripts/proxy/proxy-verify.mjs', 'standalone ADR-0026 proxy trial tool (wired-check exempt)'],
|
|
96
|
+
]);
|
|
97
|
+
const files = tracked.filter((f) => /^(scripts|plugin|bin|kb)\/.+\.mjs$/.test(f) && !allowed.has(f) && re.test(read(f)));
|
|
98
|
+
return none(files);
|
|
99
|
+
} },
|
|
100
|
+
{ id: 'B8', area: 'rules', scope: 'repo', title: 'ADRs governing the live corpus publisher are not left "Proposed"',
|
|
101
|
+
run: () => none(tracked.filter((f) => /^docs\/adr\/008[56]-/.test(f)).filter((f) => /^status:\s*Proposed/im.test(read(f)))) },
|
|
102
|
+
{ id: 'B9', area: 'rules', scope: 'repo', title: 'All version surfaces carry one version',
|
|
103
|
+
run: () => {
|
|
104
|
+
const v = [['package.json', pkg.version], ['kb/package.json', json(read('kb/package.json')).version],
|
|
105
|
+
['plugin/.claude-plugin/plugin.json', json(read('plugin/.claude-plugin/plugin.json')).version],
|
|
106
|
+
['plugin/.codex-plugin/plugin.json', json(read('plugin/.codex-plugin/plugin.json')).version]].filter(([, x]) => x);
|
|
107
|
+
return { ok: new Set(v.map(([, x]) => x)).size === 1, detail: v.map(([f, x]) => `${f}=${x}`).join(' ') };
|
|
108
|
+
} },
|
|
109
|
+
{ id: 'B10', area: 'rules', scope: 'repo', title: 'Public pages show exactly one current version (no candidate/public split)',
|
|
110
|
+
run: () => none(grepIn(['README.md', 'explainer/index.html'], /candidate preview|public npm v\d/i)) },
|
|
111
|
+
{ id: 'B11', area: 'rules', scope: 'repo', title: 'The ADR index lists every ADR, with the status the ADR itself declares',
|
|
112
|
+
run: () => {
|
|
113
|
+
const idx = read('docs/adr/README.md');
|
|
114
|
+
const adrs = tracked.filter((f) => /^docs\/adr\/\d{4}-.+\.md$/.test(f));
|
|
115
|
+
const missing = adrs.filter((f) => !idx.includes(path.basename(f))).map((f) => `not indexed: ${f}`);
|
|
116
|
+
const drift = adrs.flatMap((f) => {
|
|
117
|
+
const own = (read(f).match(/^status:\s*([A-Za-z]+)/m) || [])[1];
|
|
118
|
+
const statusTable = idx.slice(Math.max(0, idx.indexOf('| ADR | Title | Status |')));
|
|
119
|
+
const row = statusTable.split('\n').find((l) => l.includes(`(${path.basename(f)})`));
|
|
120
|
+
const listed = row && (row.match(/\|\s*([A-Za-z]+)[^|]*\|\s*$/) || [])[1];
|
|
121
|
+
return own && listed && own.toLowerCase() !== listed.toLowerCase() ? [`${path.basename(f)}: ADR says ${own}, index says ${listed}`] : [];
|
|
122
|
+
});
|
|
123
|
+
return none([...missing, ...drift]);
|
|
124
|
+
} },
|
|
125
|
+
|
|
126
|
+
{ id: 'B12', area: 'rules', scope: 'machine', title: 'The local (untracked) CLAUDE.md / AGENTS.md carry no contradicting instruction',
|
|
127
|
+
run: () => {
|
|
128
|
+
const main = path.join(HOME, 'Code/ruvnet-brain');
|
|
129
|
+
const bad = /npx (-y )?(@claude-flow|claude-flow|ruflo)\b|standing authori[sz]ation permits|no separate human reviewer click|npm run (build|dev|test:integration|test:coverage|test:security)\b|not direct Agent tool/i;
|
|
130
|
+
return none(['CLAUDE.md', 'AGENTS.md'].flatMap((f) => readAbs(path.join(main, f)).split('\n')
|
|
131
|
+
.map((l, i) => (bad.test(l) ? `${f}:${i + 1}: ${l.trim().slice(0, 120)}` : null)).filter(Boolean)));
|
|
132
|
+
} },
|
|
133
|
+
|
|
134
|
+
// C — one release path
|
|
135
|
+
{ id: 'C1', area: 'release', scope: 'repo', title: 'Unattended corpus promotion stays disarmed until a code release is install-verified',
|
|
136
|
+
run: () => ({ ok: !tracked.includes('data/approved-runtime.json'), detail: tracked.includes('data/approved-runtime.json') ? 'data/approved-runtime.json present (arms the 07:17 UTC nightly)' : 'absent' }) },
|
|
137
|
+
{ id: 'C2', area: 'release', scope: 'repo', title: 'Every scheduled workflow pages the phone on failure',
|
|
138
|
+
run: () => {
|
|
139
|
+
const alerts = read('.github/workflows/ntfy-alerts.yml');
|
|
140
|
+
return none(workflows.filter((w) => /^\s*schedule:/m.test(read(w)) && !w.endsWith('ntfy-alerts.yml'))
|
|
141
|
+
.map((w) => ((read(w).match(/^name:\s*(.+)$/m) || [])[1] || w).trim().replace(/['"]/g, ''))
|
|
142
|
+
.filter((n) => !alerts.includes(n)));
|
|
143
|
+
} },
|
|
144
|
+
{ id: 'C4', area: 'release', scope: 'machine', title: 'The corpus gist job has its authenticated token (RUVNET_GISTS_TOKEN)',
|
|
145
|
+
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(', ') }; } },
|
|
146
|
+
{ id: 'C3', area: 'release', scope: 'machine', title: 'Publishing requires the owner: Production environment has a required reviewer',
|
|
147
|
+
run: () => {
|
|
148
|
+
const r = spawnSync('gh', ['api', 'repos/stuinfla/ruvnet-brain/environments', '-q',
|
|
149
|
+
'.environments[]|select(.name=="Production – ruvnet-brain")|[.protection_rules[]?|select(.type=="required_reviewers")]|length'], { encoding: 'utf8' });
|
|
150
|
+
return { ok: r.status === 0 && Number(r.stdout.trim()) > 0, detail: `required_reviewers rules: ${r.stdout.trim() || r.stderr.trim()}` };
|
|
151
|
+
} },
|
|
152
|
+
|
|
153
|
+
// D — one corpus / update path
|
|
154
|
+
{ id: 'D1', area: 'corpus', scope: 'repo', title: 'The end-user updater label is defined once; the one necessary copy (standalone runner) matches it',
|
|
155
|
+
run: () => {
|
|
156
|
+
const owner = (read('plugin/scripts/nightly-scheduler.mjs').match(/NIGHTLY_LABEL\s*=\s*'([^']+)'/) || [])[1];
|
|
157
|
+
const copies = tracked.filter((f) => /\.(mjs|sh)$/.test(f) && !/^tests\//.test(f) && f !== 'plugin/scripts/nightly-scheduler.mjs')
|
|
158
|
+
.filter((f) => /\^com\\\.ruvnet\\\.brain-update|'com\.ruvnet\.brain-update'/.test(read(f)));
|
|
159
|
+
// bin/nightly-refresh.mjs is copied to a standalone content-addressed path and executed outside the package, so it cannot import.
|
|
160
|
+
const extra = copies.filter((f) => f !== 'bin/nightly-refresh.mjs');
|
|
161
|
+
// The runner MUST carry the owner's exact label (it validates its own registration against it);
|
|
162
|
+
// testing only files that already contain the label would miss exactly the drifted case.
|
|
163
|
+
const runner = read('bin/nightly-refresh.mjs');
|
|
164
|
+
const esc = owner.replace(/\./g, '\\.');
|
|
165
|
+
const drift = !owner ? ['owner label not found in plugin/scripts/nightly-scheduler.mjs']
|
|
166
|
+
: (!runner.includes(`'${owner}'`) || !runner.includes(`/^${esc}\\.proof-`)) ? [`bin/nightly-refresh.mjs does not carry owner label ${owner}`] : [];
|
|
167
|
+
return none([...extra, ...drift]);
|
|
168
|
+
} },
|
|
169
|
+
|
|
170
|
+
{ id: 'D2', area: 'corpus', scope: 'machine', title: 'Installed knowledge matches the installed plugin version',
|
|
171
|
+
run: () => {
|
|
172
|
+
const tag = String(json(readAbs(path.join(HOME, '.cache/ruvnet-brain/kb/SOURCE.json'))).releaseTag || '').replace(/^v/, '');
|
|
173
|
+
const active = json(readAbs(path.join(HOME, '.cache/ruvnet-brain/active.json'))).version;
|
|
174
|
+
return { ok: !!tag && tag === active, detail: `knowledge ${tag || 'unknown'} vs plugin ${active || 'unknown'}` };
|
|
175
|
+
} },
|
|
176
|
+
{ id: 'D3', area: 'corpus', scope: 'machine', title: 'Exactly one update owner exists, and its last refresh run passed within 26h',
|
|
177
|
+
run: () => {
|
|
178
|
+
const brainJob = spawnSync('launchctl', ['print', `gui/${process.getuid()}/com.ruvnet.brain-update`], { encoding: 'utf8' }).status === 0;
|
|
179
|
+
const akJob = spawnSync('launchctl', ['print', `gui/${process.getuid()}/com.stuartkerr.ak-sync`], { encoding: 'utf8' }).status === 0;
|
|
180
|
+
const dir = path.join(HOME, '.cache/ruvnet-brain/refresh-runs');
|
|
181
|
+
const runs = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.json')).sort() : [];
|
|
182
|
+
const last = runs.length ? json(readAbs(path.join(dir, runs.at(-1)))) : {};
|
|
183
|
+
const fresh = last.startedAt && (Date.now() - Date.parse(last.startedAt)) < 26 * 3600e3;
|
|
184
|
+
const owners = [brainJob && 'brain-update', akJob && 'ak-sync'].filter(Boolean);
|
|
185
|
+
return { ok: owners.length === 1 && fresh && last.terminalVerdict !== 'failed', detail: `owners=[${owners.join(',') || 'none'}] last run ${last.startedAt || 'none'} verdict ${last.terminalVerdict || last.status || 'none'}` };
|
|
186
|
+
} },
|
|
187
|
+
|
|
188
|
+
// E — one hook / context plane
|
|
189
|
+
{ id: 'E1', area: 'hooks', scope: 'machine', title: 'No stale autonomous-loop checkpoint is injected into sessions',
|
|
190
|
+
// H3: judged by the checkpoint's OWN `updatedAt` claim (loop-checkpoint.mjs's checkpointStaleness
|
|
191
|
+
// — the SAME rule ground-ruvnet.sh's `stale` verb uses), never by file mtime. A copy/rsync/restore
|
|
192
|
+
// resets mtime without the checkpoint's content changing, which is exactly the kind of second,
|
|
193
|
+
// drifting definition of "stale" this audit exists to catch — including, previously, in itself.
|
|
194
|
+
run: () => {
|
|
195
|
+
const f = path.join(ROOT, '.ruvnet-brain/checkpoint.json');
|
|
196
|
+
const main = path.join(HOME, 'Code/ruvnet-brain/.ruvnet-brain/checkpoint.json');
|
|
197
|
+
const stale = [f, main].filter((p) => existsSync(p) && checkpointStaleness(readCheckpoint(p)).stale);
|
|
198
|
+
return none(stale);
|
|
199
|
+
} },
|
|
200
|
+
{ id: 'E2', area: 'hooks', scope: 'machine', title: 'Only one hook writes session snapshots (no global + plugin double writer)',
|
|
201
|
+
run: () => { const s = readAbs(path.join(HOME, '.claude/settings.json')); return { ok: !/agentdb-autocapture/.test(s), detail: /agentdb-autocapture/.test(s) ? 'global agentdb-autocapture still registered alongside the plugin session-snapshot' : 'single writer' }; } },
|
|
202
|
+
|
|
203
|
+
{ id: 'E3', area: 'hooks', scope: 'machine', title: 'Lessons live in one store (no .swarm lesson-* rows outside the plugin lesson store)',
|
|
204
|
+
run: () => { const r = spawnSync('ruflo', ['memory', 'list', '--path', path.join(HOME, 'Code/ruvnet-brain/.swarm/memory.db'), '--limit', '500'], { encoding: 'utf8', timeout: 60000 });
|
|
205
|
+
const n = (r.stdout.match(/lesson-/g) || []).length; return { ok: n === 0, detail: `${n} lesson-* rows in project .swarm (plugin store: ~/.config/ruvnet-brain/lessons.json)` }; } },
|
|
206
|
+
{ id: 'E4', area: 'hooks', scope: 'machine', title: 'One session-start continuity restorer (global hook stands down when the plugin is enabled)',
|
|
207
|
+
run: () => { const s = readAbs(path.join(HOME, '.claude/hooks/agentdb-ensure.sh')); return { ok: s.includes('SINGLE CONTINUITY OWNER'), detail: 'agentdb-ensure.sh must stand down its project-state recall' }; } },
|
|
208
|
+
|
|
209
|
+
// F — this Mac
|
|
210
|
+
{ id: 'F1', area: 'machine', scope: 'machine', title: 'No plaintext API keys in ~/.claude/settings.json',
|
|
211
|
+
run: () => none(readAbs(path.join(HOME, '.claude/settings.json')).split('\n')
|
|
212
|
+
.filter((l) => /(api-key-|sk-[A-Za-z0-9]{20,}|_TOKEN"\s*:\s*"[^"$]{16,})/.test(l)).map((l) => l.replace(/:\s*".*"/, ': "<redacted>"').trim())) },
|
|
213
|
+
{ id: 'F2', area: 'machine', scope: 'machine', title: 'Every required scheduled job proves it ran (repo watchdog)',
|
|
214
|
+
run: () => { const r = spawnSync(process.execPath, [path.join(ROOT, 'scripts/nightly-watchdog.mjs')], { cwd: ROOT, encoding: 'utf8', timeout: 60000 }); return { ok: r.status === 0, detail: `${r.stdout}${r.stderr}`.trim().split('\n').slice(-6).join('\n') }; } },
|
|
215
|
+
{ id: 'F3', area: 'machine', scope: 'machine', title: 'Scheduled jobs use the one Node the shell uses (no /usr/local/bin/node v22)',
|
|
216
|
+
run: () => {
|
|
217
|
+
const dir = path.join(HOME, 'Library/LaunchAgents');
|
|
218
|
+
const plists = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.plist')) : [];
|
|
219
|
+
return none(plists.filter((f) => readAbs(path.join(dir, f)).includes('/usr/local/bin/node')));
|
|
220
|
+
} },
|
|
221
|
+
{ id: 'F4', area: 'machine', scope: 'machine', title: 'Permission allow-list names the real Ruflo MCP server',
|
|
222
|
+
run: () => { const s = readAbs(path.join(HOME, '.claude/settings.json')); return { ok: !s.includes('"mcp__ruflo__*"') || s.includes('"mcp__claude-flow__*"'), detail: 'allow list names mcp__ruflo__* but the server is claude-flow' }; } },
|
|
223
|
+
|
|
224
|
+
// G — one release state
|
|
225
|
+
{ id: 'G1', area: 'state', scope: 'machine', title: 'Local main is exactly origin/main',
|
|
226
|
+
run: () => { const m = git('rev-parse', 'main'); const o = git('rev-parse', 'origin/main'); return { ok: m === o, detail: `main ${m.slice(0, 8)} origin ${o.slice(0, 8)}` }; } },
|
|
227
|
+
{ id: 'G2', area: 'state', scope: 'machine', title: 'npm latest, GitHub latest release and main carry the same version',
|
|
228
|
+
run: () => {
|
|
229
|
+
const npm = spawnSync('npm', ['view', 'ruvnet-brain', 'dist-tags.latest'], { encoding: 'utf8' }).stdout.trim();
|
|
230
|
+
const gh = spawnSync('gh', ['api', 'repos/stuinfla/ruvnet-brain/releases/latest', '-q', '.tag_name'], { encoding: 'utf8' }).stdout.trim().replace(/^v/, '');
|
|
231
|
+
const main = json(git('show', 'origin/main:package.json')).version;
|
|
232
|
+
return { ok: npm === gh && gh === main, detail: `npm ${npm} · github ${gh} · main ${main}` };
|
|
233
|
+
} },
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
const selected = checks.filter((c) => c.scope === 'repo' || MACHINE);
|
|
237
|
+
const results = selected.map((c) => { let r; try { r = c.run(); } catch (e) { r = { ok: false, detail: `check crashed: ${e.message}` }; } return { ...c, ...r }; });
|
|
238
|
+
|
|
239
|
+
// R1 — the register may not claim more than the evidence shows. A row in docs/WORK-REGISTER.md whose
|
|
240
|
+
// State is LIVE or STAGED must have every check it names passing (machine checks only in --machine).
|
|
241
|
+
{
|
|
242
|
+
const byId = new Map(results.map((r) => [r.id, r]));
|
|
243
|
+
const scopeOf = new Map(checks.map((c) => [c.id, c.scope]));
|
|
244
|
+
const bad = [];
|
|
245
|
+
for (const line of read('docs/WORK-REGISTER.md').split('\n')) {
|
|
246
|
+
const cells = line.split('|').map((x) => x.trim());
|
|
247
|
+
if (cells.length < 7 || !/^\d+$/.test(cells[1])) continue;
|
|
248
|
+
const [, num, family, , state, checkList] = cells;
|
|
249
|
+
if (!/^(LIVE|STAGED)$/.test(state)) continue;
|
|
250
|
+
const ids = checkList.split(/[ ,]+/).filter(Boolean);
|
|
251
|
+
if (!ids.length) bad.push(`#${num} ${family}: claims ${state} but names no check`);
|
|
252
|
+
for (const id of ids) {
|
|
253
|
+
if (!scopeOf.has(id)) { bad.push(`#${num}: unknown check ${id}`); continue; }
|
|
254
|
+
if (scopeOf.get(id) === 'machine' && !MACHINE) continue;
|
|
255
|
+
const r = byId.get(id);
|
|
256
|
+
if (!r?.ok) bad.push(`#${num} ${family}: claims ${state} but ${id} fails`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
results.push({ id: 'R1', area: 'register', scope: 'repo', title: 'The work register claims nothing its checks do not prove', ok: bad.length === 0, detail: bad.join('\n') || 'none' });
|
|
260
|
+
}
|
|
261
|
+
if (JSON_OUT) console.log(JSON.stringify(results.map(({ run, ...r }) => r), null, 1));
|
|
262
|
+
else {
|
|
263
|
+
for (const r of results) {
|
|
264
|
+
console.log(`${r.ok ? 'PASS' : 'FAIL'} ${r.id.padEnd(4)} [${r.area}] ${r.title}`);
|
|
265
|
+
if (!r.ok) for (const l of String(r.detail).split('\n')) console.log(` ${l}`);
|
|
266
|
+
}
|
|
267
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
268
|
+
console.log(`\n${results.length - failed}/${results.length} pass${MACHINE ? ' (repo + machine)' : ' (repo)'}`);
|
|
269
|
+
}
|
|
270
|
+
process.exitCode = results.every((r) => r.ok) ? 0 : 1;
|
|
@@ -84,7 +84,7 @@ const preparePackage = async ({ packagePath, bundlePath }) => {
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
-
export function stagedHostVerifier({ assets, identity, retrieval }, { runMatrix = runHostMatrixAsync } = {}) {
|
|
87
|
+
export function stagedHostVerifier({ assets, identity, retrieval, sequentialSearches = false }, { runMatrix = runHostMatrixAsync } = {}) {
|
|
88
88
|
return {
|
|
89
89
|
async verify({ source, assets: observedAssets = assets }) {
|
|
90
90
|
if (retrieval) verifyCandidateRetrievalAssets({ retrieval, assets: observedAssets });
|
|
@@ -104,6 +104,7 @@ export function stagedHostVerifier({ assets, identity, retrieval }, { runMatrix
|
|
|
104
104
|
locate,
|
|
105
105
|
temp: prepared.temp,
|
|
106
106
|
retrieval,
|
|
107
|
+
sequentialSearches,
|
|
107
108
|
});
|
|
108
109
|
if (retrieval) verifyCandidateRetrievalAssets({ retrieval, assets: observedAssets });
|
|
109
110
|
if (matrix.verdict !== 'PASS') {
|
|
@@ -14,10 +14,14 @@ export const API_BILLING_ENV = Object.freeze([
|
|
|
14
14
|
'GEMINI_API_KEY',
|
|
15
15
|
'XAI_API_KEY',
|
|
16
16
|
]);
|
|
17
|
+
export const PRIVATE_SIGNING_ENV = Object.freeze([
|
|
18
|
+
'RUVNET_SIGNING_KEY', 'RUVNET_FABLE_REVIEW_SIGNING_KEY', 'RUVNET_ASTRA_REVIEW_SIGNING_KEY',
|
|
19
|
+
]);
|
|
17
20
|
|
|
18
21
|
export function subscriptionOnlyEnv(parent = process.env) {
|
|
19
22
|
const child = { ...parent, RUVNET_SUBSCRIPTION_ONLY: '1' };
|
|
20
23
|
for (const name of API_BILLING_ENV) delete child[name];
|
|
24
|
+
for (const name of PRIVATE_SIGNING_ENV) delete child[name];
|
|
21
25
|
return child;
|
|
22
26
|
}
|
|
23
27
|
|
package/scripts/sync-version.mjs
CHANGED
|
@@ -41,6 +41,14 @@ export function writeExplainerBadgeVersion(s, version) {
|
|
|
41
41
|
return s.replace(/(·\s*v)\d+\.\d+\.\d+(?:-[\w.-]+)?/, `$1${version}`);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// The explainer once showed a second, competing version next to the product version
|
|
45
|
+
// ("candidate preview" ... "public npm vX.Y.Z") — two versions on one public page, exactly the
|
|
46
|
+
// defect single-source-check.mjs check B10 exists to catch (fixed 2026-09-26). This regression
|
|
47
|
+
// guard keeps it from silently coming back.
|
|
48
|
+
export function hasSplitVersionLabel(s) {
|
|
49
|
+
return /candidate preview|public npm v\d/i.test(s);
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
// Each target: a file, a regex to find the version-bearing line, and the corrected line.
|
|
45
53
|
const targets = [
|
|
46
54
|
{ // Codex plugin manifest — same product, same release train as the Claude manifest
|
|
@@ -160,6 +168,20 @@ if (CHECK && fs.existsSync(path.join(ROOT, 'kb/RVF-GENERATIONS.json'))) {
|
|
|
160
168
|
}
|
|
161
169
|
}
|
|
162
170
|
|
|
171
|
+
// Guard: the explainer must never again show a second, competing version next to the product
|
|
172
|
+
// version (the "candidate preview" / "public npm vX.Y.Z" split fixed 2026-09-26 — see
|
|
173
|
+
// hasSplitVersionLabel() above and single-source-check.mjs check B10). This is a content
|
|
174
|
+
// regression, not a mechanical one, so --check only reports it; there is no auto-fix.
|
|
175
|
+
{
|
|
176
|
+
const p = path.join(ROOT, 'explainer/index.html');
|
|
177
|
+
if (fs.existsSync(p) && hasSplitVersionLabel(fs.readFileSync(p, 'utf8'))) {
|
|
178
|
+
if (CHECK) {
|
|
179
|
+
console.error('[version] DRIFT: explainer/index.html shows a second/candidate version label ("candidate preview" / "public npm vX") — the page must show exactly one current version');
|
|
180
|
+
drift++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
163
185
|
// Guard: no stray hardcoded product-version literal anywhere in the source tree — every code path must
|
|
164
186
|
// read getVersion() (or inherit a synced literal via the `targets` above / the README badge). Rather than
|
|
165
187
|
// a fixed 3-file allowlist (which silently missed strays in every other file), walk the WHOLE tree and
|
package/scripts/trismart.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { TRI_HOST_MODEL_IDS } from './review-model-defaults.mjs';
|
|
6
7
|
|
|
7
8
|
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const DUAL = path.join(ROOT, 'dual-host-deliberation.mjs');
|
|
@@ -24,9 +25,8 @@ if (!task) { usage(); process.exit(2); }
|
|
|
24
25
|
|
|
25
26
|
if (dryRun) {
|
|
26
27
|
const providers = mode === 'dual' ? ['claude', 'codex'] : ['claude', 'codex', 'grok'];
|
|
27
|
-
console.log(JSON.stringify({ status: 'ready', mode, providers, models:
|
|
28
|
-
|
|
29
|
-
}, billingPath: 'native subscription/OAuth CLI; API-key variables unset', taskHash: task.length }, null, 2));
|
|
28
|
+
console.log(JSON.stringify({ status: 'ready', mode, providers, models: TRI_HOST_MODEL_IDS,
|
|
29
|
+
billingPath: 'native subscription/OAuth CLI; API-key variables unset', taskHash: task.length }, null, 2));
|
|
30
30
|
process.exit(0);
|
|
31
31
|
}
|
|
32
32
|
|
package/scripts/wired-check.mjs
CHANGED
|
@@ -78,7 +78,6 @@ const STANDALONE = [
|
|
|
78
78
|
['dream-issue-gate', 'pure Dream Cycle disposition policy; invoked by the external issue adapter, never a GitHub writer'],
|
|
79
79
|
['sync-census', 'explicit maintainer census writer; a destructive source-to-surface refresh is never scheduled'],
|
|
80
80
|
['sync-commands', 'explicit maintainer alias synchronizer; run deliberately before release, never from a lifecycle hook'],
|
|
81
|
-
['version-bump-gate', 'retired automatic interceptor; explicit version checks own release validation'],
|
|
82
81
|
['project-progression-checkpoint', 'the body of the shipped `/ruvnet-brain:checkpoint` command '
|
|
83
82
|
+ '(plugin/commands/checkpoint.md:39; added a7167b6b 2026-09-11). The command host executes that '
|
|
84
83
|
+ 'instruction, and this check does not scan command markdown — the same shape as '
|
|
@@ -89,6 +88,10 @@ const STANDALONE = [
|
|
|
89
88
|
+ '.claude/worktrees/ — their package.json copies had been counted as callers all day.'],
|
|
90
89
|
['lesson-seed', 'one-shot seeding, run deliberately by a human'],
|
|
91
90
|
['lesson-ratify', 'the human control surface — a CLI is its entire purpose'],
|
|
91
|
+
['lesson-migrate-agentdb', 'one-time reconciliation between the two AgentDB lesson stores and the '
|
|
92
|
+
+ 'one plugin lesson store, run deliberately by a human/model, never scheduled. Completion is '
|
|
93
|
+
+ 'proven and kept honest by tests/unit/lesson-migrate-agentdb.test.mjs (0 pending rows), a drift '
|
|
94
|
+
+ 'canary rather than automated invocation — the same shape as lesson-seed/lesson-ratify above.'],
|
|
92
95
|
['stamp-sweep', 'ADR-056 §2 — the ONE-TIME backfill half of the stamp rule. A human runs it once '
|
|
93
96
|
+ '(--apply) to reach the files nobody is editing; the ongoing half is the md-stamp PostToolUse '
|
|
94
97
|
+ 'hook, which IS wired. Deliberately not in a gate: it WRITES to documents, and a writer that '
|
|
@@ -109,9 +112,6 @@ const STANDALONE = [
|
|
|
109
112
|
['fix-metaharness-memretrieve', 'one-shot historical repair; kept for the record'],
|
|
110
113
|
['gen-console-images', 'build-time asset generation, run by hand'],
|
|
111
114
|
['adr-backfill', 'one-shot backfill by a human; its result is enforced by adr-format.test.mjs'],
|
|
112
|
-
['stamp-existing-rvf-generations', 'one-shot maintainer migration that binds already-built canonical '
|
|
113
|
-
+ 'RVFs to RVF-GENERATIONS.json and optionally prunes legacy sidecars; build-bundle.mjs consumes '
|
|
114
|
-
+ 'and validates the resulting manifest, so scheduling this destructive migration would be wrong'],
|
|
115
115
|
['release', 'the ship path, run by a human'],
|
|
116
116
|
['execution-preflight', 'external orchestration boundary — invoked by the host before consequential Ruflo/Codex execution; no in-repo caller exists because the host supplies the live Brain and AgentDB receipts'],
|
|
117
117
|
['fix-workstream', 'session-supervised coordination CLI run explicitly by the integration owner or an '
|
|
@@ -230,6 +230,27 @@ const STANDALONE = [
|
|
|
230
230
|
+ 'ground-before-write. Per ADR-0014 ownership moved to the Kling skill (confirmed live: a copy '
|
|
231
231
|
+ 'ships at ~/.claude/skills/klingai/scripts/kling-preflight.sh); NOT currently wired into any '
|
|
232
232
|
+ 'settings.json there either — honestly dormant until a user opts in, not a silent gap'],
|
|
233
|
+
|
|
234
|
+
// H5 (2026-09-26 dead-code audit): decision-gate.mjs's REGISTRY['bash'] was the only production
|
|
235
|
+
// caller of these three — never registered in plugin/hooks/hooks.json or codex-hooks.json
|
|
236
|
+
// (continuity-hook-policy.mjs's own header: "remains reachable through hook-shim's dispatch table
|
|
237
|
+
// by explicit invocation" only). Removing that dead route makes each of these genuinely uncalled
|
|
238
|
+
// by any product path. Not deleted: each exports pure, independently-tested logic with its own
|
|
239
|
+
// passing suite — identifier-preflight.test.mjs / spend-guard.test.mjs import and exercise these
|
|
240
|
+
// functions directly, and tests/unit/lesson-gate.test.mjs imports degradation-watch.mjs's
|
|
241
|
+
// `dependentEvent` to cross-check lesson-hooks.sh's own pattern. A test importing an export is not
|
|
242
|
+
// a product caller (same distinction this file already draws for correction-detect-measure above),
|
|
243
|
+
// so these are honestly STANDALONE rather than silently left to read as still-wired.
|
|
244
|
+
['identifier-preflight', 'H5: orphaned by decision-gate.mjs\'s dead bash route (REGISTRY[\'bash\'] '
|
|
245
|
+
+ 'removed 2026-09-26). Pure `check`/`identifierIn` logic remains directly imported and exercised '
|
|
246
|
+
+ 'by its own tests/unit/identifier-preflight.test.mjs; no product path calls it'],
|
|
247
|
+
['spend-guard', 'H5: orphaned by decision-gate.mjs\'s dead bash route (REGISTRY[\'bash\'] removed '
|
|
248
|
+
+ '2026-09-26). Its logic remains directly imported and exercised by its own '
|
|
249
|
+
+ 'tests/unit/spend-guard.test.mjs; no product path calls it'],
|
|
250
|
+
['degradation-watch', 'H5: orphaned by decision-gate.mjs\'s dead bash route (REGISTRY[\'bash\'] '
|
|
251
|
+
+ 'removed 2026-09-26). Its `dependentEvent` export remains directly imported by '
|
|
252
|
+
+ 'tests/unit/lesson-gate.test.mjs (cross-checked against lesson-hooks.sh\'s own pattern) and by '
|
|
253
|
+
+ 'its own test file; no product path calls it'],
|
|
233
254
|
];
|
|
234
255
|
// REMOVED 2026-07-22, each verified before removal:
|
|
235
256
|
// check-legibility / check-indexation / status-honesty — claimed "invoked from the workflow";
|
|
@@ -889,9 +910,24 @@ export function hookWiringAudit({
|
|
|
889
910
|
const scriptsDir = path.join(repo, 'plugin/scripts');
|
|
890
911
|
let all = [];
|
|
891
912
|
try { all = fs.readdirSync(scriptsDir).filter((f) => /\.(mjs|sh)$/.test(f)); } catch { /* no dir */ }
|
|
913
|
+
// H6 (2026-09-26 dead-code audit): hook-shim.mjs is a DISPATCH TABLE, not a script that
|
|
914
|
+
// unconditionally executes everything it mentions. `scanConfig()` above already proves reachability
|
|
915
|
+
// for a TABLE entry CORRECTLY — only when its id is genuinely dispatched by a real command string in
|
|
916
|
+
// a manifest. Letting hook-shim.mjs ALSO participate as a "from" in the generic fixed-point pass
|
|
917
|
+
// below double-counts every OTHER entry in its TABLE as "spawned", because the pass's own predicate
|
|
918
|
+
// (callerPattern) matches any quoted filename — and every TABLE entry is, by construction, a quoted
|
|
919
|
+
// filename in hook-shim.mjs's source, dispatched or not. Measured: before this exclusion,
|
|
920
|
+
// design-wall.sh, route-dispatch.sh, verify-interface.sh, learn-capture.sh, learn-flush.mjs,
|
|
921
|
+
// md-stamp.mjs, signal-watch.mjs, routing-outcome-capture.mjs and swarm-slot-recycler.mjs all read
|
|
922
|
+
// "wired · via spawned by plugin/scripts/hook-shim.mjs" while zero manifest (hooks.json,
|
|
923
|
+
// codex-hooks.json, this repo's or the user's settings.json) ever dispatches most of their ids —
|
|
924
|
+
// exactly the false positive this check exists to catch. Excluding it here does not remove any
|
|
925
|
+
// GENUINE reachability: every id hook-shim.mjs's TABLE actually dispatches was already added by
|
|
926
|
+
// scanConfig() above, before this loop runs at all.
|
|
892
927
|
for (let i = 0; i < 10; i++) {
|
|
893
928
|
let changed = false;
|
|
894
929
|
for (const from of [...reached.keys()]) {
|
|
930
|
+
if (from === 'hook-shim.mjs') continue;
|
|
895
931
|
const src = sourceCache.commentFree(path.join(scriptsDir, from));
|
|
896
932
|
if (src === null) continue;
|
|
897
933
|
for (const cand of all) {
|
|
Binary file
|