ruvnet-brain 3.9.134-dev → 4.0.2
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/.claude-plugin/marketplace.json +14 -0
- package/README.md +5 -5
- package/bin/install.mjs +382 -36
- package/console/CONTRACT.md +172 -0
- package/console/activity.js +753 -0
- package/console/app.js +4189 -0
- package/console/architecture.html +1221 -0
- package/console/assets/depth-1.webp +0 -0
- package/console/assets/depth-2.webp +0 -0
- package/console/assets/depth-3.webp +0 -0
- package/console/assets/harness-vs-plain.svg +259 -0
- package/console/assets/hero.webp +0 -0
- package/console/assets/memory.webp +0 -0
- package/console/assets/metaharness.svg +247 -0
- package/console/index.html +777 -0
- package/console/install-architecture.html +162 -0
- package/console/install-mockup.html +543 -0
- package/console/style.css +2144 -0
- package/console/tips.css +926 -0
- package/console/tips.html +858 -0
- package/console/tips.js +128 -0
- package/docs/RELEASE-NOTES-4.0.md +88 -0
- package/kb/model-requirements.mjs +37 -6
- package/kb/zip-extract.mjs +53 -14
- package/keys/ruvnet-brain-signing.pub.pem +3 -0
- package/package.json +14 -22
- package/plugin/.claude-plugin/marketplace.json +14 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/.codex-plugin/plugin.json +21 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/commands/brain-console.md +16 -0
- package/plugin/commands/configure.md +33 -0
- package/plugin/commands/rvbc.md +79 -0
- package/plugin/commands/rvcb.md +16 -0
- package/plugin/commands/whats-new.md +57 -0
- package/plugin/hooks/codex-hooks.json +160 -0
- package/plugin/hooks/hook-contracts.json +77 -0
- package/plugin/hooks/hooks.json +202 -0
- package/plugin/mcp/managed-cli-interface.mjs +47 -4
- package/plugin/mcp/server.mjs +56 -6
- package/plugin/scripts/anticipate.sh +534 -0
- package/plugin/scripts/codex-hook-adapter.mjs +96 -0
- package/plugin/scripts/continuation-gate.mjs +267 -0
- package/plugin/scripts/design-wall.sh +137 -0
- package/plugin/scripts/detach.mjs +182 -0
- package/plugin/scripts/first-session-worker.mjs +38 -0
- package/plugin/scripts/gate-receipt.sh +35 -0
- package/plugin/scripts/ground-before-write.sh +199 -0
- package/plugin/scripts/ground-ruvnet.sh +517 -0
- package/plugin/scripts/grounding-stamp.sh +113 -0
- package/plugin/scripts/grounding-substance.mjs +595 -0
- package/plugin/scripts/hijack-ruvnet.sh +81 -0
- package/plugin/scripts/hook-input.mjs +558 -0
- package/plugin/scripts/hook-shim-bash.mjs +55 -0
- package/plugin/scripts/hook-shim.mjs +303 -0
- package/plugin/scripts/host-update.mjs +58 -0
- package/plugin/scripts/kling-preflight.sh +146 -0
- package/plugin/scripts/learn-capture.sh +173 -0
- package/plugin/scripts/learn-flush.mjs +155 -0
- package/plugin/scripts/lesson-hooks.sh +213 -0
- package/plugin/scripts/md-stamp.mjs +219 -0
- package/plugin/scripts/protect-brain-state.sh +84 -0
- package/plugin/scripts/route-dispatch.sh +147 -0
- package/plugin/scripts/routing-outcome-capture.mjs +89 -0
- package/plugin/scripts/runtime-preferences.mjs +269 -0
- package/plugin/scripts/session-start-core.mjs +477 -0
- package/plugin/scripts/session-start.sh +13 -0
- package/plugin/scripts/signal-watch.mjs +193 -0
- package/plugin/scripts/unprompted-runtime.mjs +377 -0
- package/plugin/scripts/update-apply.mjs +419 -0
- package/plugin/scripts/verify-interface.sh +53 -0
- package/plugin/scripts/version-bump-gate.sh +112 -0
- package/plugin/skills/brain-build/SKILL.md +123 -0
- package/plugin/skills/brain-console/SKILL.md +22 -0
- package/plugin/skills/brain-prompt/SKILL.md +83 -0
- package/plugin/skills/brain-score/SKILL.md +101 -0
- package/plugin/skills/release-proof/SKILL.md +81 -0
- package/plugin/skills/release-proof/agents/openai.yaml +4 -0
- package/plugin/skills/release-proof/references/receipt-contract.md +38 -0
- package/plugin/skills/release-proof/scripts/release-proof.mjs +210 -0
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +121 -0
- package/plugin/skills/ruvnet-brain/SKILL.md +234 -0
- package/plugin/skills/rvbc/SKILL.md +23 -0
- package/plugin/skills/savings/SKILL.md +46 -0
- package/plugin/skills/whats-new/SKILL.md +22 -0
- package/scripts/adr-backfill.mjs +107 -0
- package/scripts/advocacy-outcomes.mjs +808 -0
- package/scripts/agentdb-context.mjs +216 -0
- package/scripts/agentdb-fleet-doctor.mjs +101 -0
- package/scripts/ascii-drift.mjs +236 -0
- package/scripts/behavioral-l1-l4.mjs +210 -0
- package/scripts/brain-capability-check.mjs +72 -0
- package/scripts/brain-grade-groundtruth.mjs +100 -0
- package/scripts/brain-latency-50.mjs +227 -0
- package/scripts/brain-novice-50.mjs +189 -0
- package/scripts/brain-stamp.mjs +94 -0
- package/scripts/brain-state.mjs +212 -0
- package/scripts/build-bundle.mjs +522 -0
- package/scripts/build-concepts.mjs +132 -0
- package/scripts/build-l2.mjs +71 -0
- package/scripts/build-primer.mjs +73 -0
- package/scripts/build-symbols.mjs +68 -0
- package/scripts/calibrate-router.mjs +97 -0
- package/scripts/capability-audit.mjs +321 -0
- package/scripts/capability-registry.mjs +876 -0
- package/scripts/check-indexation.mjs +108 -0
- package/scripts/check-legibility.mjs +189 -0
- package/scripts/ci/build-fixture-kb.mjs +67 -0
- package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
- package/scripts/ci/learning-replay-recorder.mjs +59 -0
- package/scripts/ci/mutate-hook-timeout.mjs +70 -0
- package/scripts/ci/stranger-fixture-stage.mjs +17 -0
- package/scripts/ci/stranger-scenario.mjs +228 -0
- package/scripts/ci/stranger-timeout.mjs +25 -0
- package/scripts/ci-verdict.mjs +29 -0
- package/scripts/claims-verify.mjs +710 -0
- package/scripts/clear-claude-tmp.sh +31 -0
- package/scripts/console-engine.mjs +434 -0
- package/scripts/console-engine.test.mjs +125 -0
- package/scripts/corpus-qa.mjs +250 -0
- package/scripts/correction-detect-embed.mjs +346 -0
- package/scripts/correction-detect-measure.mjs +270 -0
- package/scripts/correction-detect.mjs +686 -0
- package/scripts/count-chunks.mjs +54 -0
- package/scripts/described-questions.json +30 -0
- package/scripts/design-grade.mjs +58 -0
- package/scripts/dev-plugin-link.sh +105 -0
- package/scripts/distill-project.mjs +200 -0
- package/scripts/doc-currency.mjs +801 -0
- package/scripts/eval-brain.mjs +244 -0
- package/scripts/fix-metaharness-memretrieve.mjs +121 -0
- package/scripts/full-hints.mjs +87 -0
- package/scripts/gate.sh +39 -0
- package/scripts/gates.mjs +146 -0
- package/scripts/gen-console-images.mjs +54 -0
- package/scripts/gen-images.mjs +47 -0
- package/scripts/git-clone-refresh.mjs +52 -0
- package/scripts/git-hooks/pre-push +126 -0
- package/scripts/goal-match.mjs +398 -0
- package/scripts/goldie-research.mjs +223 -0
- package/scripts/goldie-weekly.sh +67 -0
- package/scripts/health-repair.mjs +250 -0
- package/scripts/helix-scenario-questions.json +10 -0
- package/scripts/ingest-gists.mjs +230 -0
- package/scripts/ingest-meeting.mjs +115 -0
- package/scripts/ingest-repo.mjs +79 -0
- package/scripts/install-npx-witness.sh +49 -0
- package/scripts/issue-fix.mjs +639 -0
- package/scripts/issue-watch.mjs +276 -0
- package/scripts/issue4-close-note.md +31 -0
- package/scripts/key-canary.mjs +91 -0
- package/scripts/latency-to-surface.mjs +233 -0
- package/scripts/learning-enable.mjs +380 -0
- package/scripts/learning-replay.mjs +1570 -0
- package/scripts/learnings.mjs +62 -0
- package/scripts/lesson-gate.mjs +680 -0
- package/scripts/lesson-lifecycle.mjs +449 -0
- package/scripts/lesson-promote.mjs +262 -0
- package/scripts/lesson-ratify.mjs +98 -0
- package/scripts/lesson-seed.mjs +252 -0
- package/scripts/lesson-store.mjs +447 -0
- package/scripts/loop-checkpoint.mjs +86 -0
- package/scripts/memdb-health.sh +14 -0
- package/scripts/memory-doctor.mjs +271 -0
- package/scripts/model-catalog.mjs +79 -0
- package/scripts/nightly-controller.mjs +66 -0
- package/scripts/nightly-gists.sh +72 -0
- package/scripts/nightly-wrapper.sh +180 -0
- package/scripts/notify.sh +12 -0
- package/scripts/npx-witness.sh +56 -0
- package/scripts/onboarding-console.mjs +2749 -0
- package/scripts/private-fence.mjs +69 -0
- package/scripts/proactivity-metrics.mjs +118 -0
- package/scripts/proof-questions.json +56 -0
- package/scripts/prove.mjs +95 -0
- package/scripts/proxy/claude-proxied.sh +57 -0
- package/scripts/proxy/proxy-revert.sh +59 -0
- package/scripts/proxy/proxy-up.sh +60 -0
- package/scripts/proxy/proxy-verify.mjs +142 -0
- package/scripts/published-surface-probe.mjs +241 -0
- package/scripts/qe/card-lane-gate.mjs +162 -0
- package/scripts/qe/session-start-gate.mjs +229 -0
- package/scripts/qe/ux-suite.mjs +323 -0
- package/scripts/reconcile-project.mjs +0 -0
- package/scripts/record-lesson.mjs +113 -0
- package/scripts/refresh-model-catalog.mjs +99 -0
- package/scripts/release-proof.mjs +9 -0
- package/scripts/release-vector.mjs +281 -0
- package/scripts/release.mjs +395 -0
- package/scripts/remedy-registry.mjs +247 -0
- package/scripts/rerank-cap-eval.mjs +265 -0
- package/scripts/rerank-cap-warm-ab.mjs +129 -0
- package/scripts/route-cheap.mjs +20 -15
- package/scripts/router-utilization.mjs +182 -0
- package/scripts/routing-flywheel.mjs +596 -0
- package/scripts/rvf-generation.mjs +104 -0
- package/scripts/rvf-index-audit.mjs +138 -0
- package/scripts/self-update.mjs +508 -0
- package/scripts/selfcheck.mjs +7 -1
- package/scripts/sign-bundle.mjs +69 -0
- package/scripts/signal-watch.mjs +171 -0
- package/scripts/stack-sync.mjs +469 -0
- package/scripts/stamp-existing-rvf-generations.mjs +53 -0
- package/scripts/stamp-sweep.mjs +144 -0
- package/scripts/status-honesty.mjs +102 -0
- package/scripts/sync-version.mjs +217 -0
- package/scripts/token-report.mjs +102 -0
- package/scripts/top100-benchmark.mjs +479 -0
- package/scripts/top100-corpus.mjs +112 -0
- package/scripts/top100-semantic-assertions.mjs +449 -0
- package/scripts/update-apply.mjs +9 -0
- package/scripts/upgrade-notice.mjs +14 -0
- package/scripts/verify-bundle.mjs +51 -0
- package/scripts/verify-channels.mjs +184 -0
- package/scripts/verify-model-catalog.mjs +104 -0
- package/scripts/verify-nightly-close-issue4.sh +31 -0
- package/scripts/version.mjs +40 -0
- package/scripts/wired-check.mjs +864 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Bind every existing canonical RVF to the current Brain release without re-embedding. Optional
|
|
3
|
+
// legacy pruning is fail-closed: the canonical passages/meta and big RVF sidecars must exist first.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { writeRvfGeneration } from './rvf-generation.mjs';
|
|
9
|
+
|
|
10
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const KB = path.join(ROOT, 'kb');
|
|
12
|
+
const PRUNE = process.argv.includes('--prune-legacy');
|
|
13
|
+
const stores = fs.readdirSync(KB)
|
|
14
|
+
.filter((file) => file.endsWith('.big.rvf'))
|
|
15
|
+
.map((file) => file.slice(0, -'.big.rvf'.length))
|
|
16
|
+
.sort();
|
|
17
|
+
|
|
18
|
+
let pruned = 0;
|
|
19
|
+
for (const store of stores) {
|
|
20
|
+
const embedFile = path.join(KB, `${store}.big.rvf.embed.json`);
|
|
21
|
+
const idmapFile = path.join(KB, `${store}.big.rvf.idmap.json`);
|
|
22
|
+
const passagesFile = path.join(KB, `${store}.passages.jsonl`);
|
|
23
|
+
const metaFile = path.join(KB, `${store}.meta.json`);
|
|
24
|
+
for (const required of [embedFile, idmapFile, passagesFile, metaFile]) {
|
|
25
|
+
if (!fs.existsSync(required)) throw new Error(`${store}: missing ${path.basename(required)}; refusing to stamp/prune`);
|
|
26
|
+
}
|
|
27
|
+
const embed = JSON.parse(fs.readFileSync(embedFile, 'utf8'));
|
|
28
|
+
const generation = writeRvfGeneration({
|
|
29
|
+
dir: KB,
|
|
30
|
+
store,
|
|
31
|
+
model: embed.model,
|
|
32
|
+
dimensions: embed.dimensions,
|
|
33
|
+
sourceCommit: null,
|
|
34
|
+
});
|
|
35
|
+
console.log(`[generation] ${store}: ${generation.sha256.slice(0, 16)}… ${generation.bytes} bytes`);
|
|
36
|
+
|
|
37
|
+
if (PRUNE) {
|
|
38
|
+
for (const legacy of [
|
|
39
|
+
`${store}.rvf`,
|
|
40
|
+
`${store}.rvf.idmap.json`,
|
|
41
|
+
`${store}.rvf.embed.json`,
|
|
42
|
+
`${store}.big.passages.jsonl`,
|
|
43
|
+
`${store}.big.meta.json`,
|
|
44
|
+
]) {
|
|
45
|
+
const file = path.join(KB, legacy);
|
|
46
|
+
if (fs.existsSync(file)) {
|
|
47
|
+
fs.rmSync(file);
|
|
48
|
+
pruned++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
console.log(`[generation] stamped ${stores.length} canonical RVFs${PRUNE ? `; pruned ${pruned} legacy files` : ''}`);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/stamp-sweep.mjs — ADR-056 §2. The ONE-TIME half of the owner's first rule.
|
|
3
|
+
//
|
|
4
|
+
// node scripts/stamp-sweep.mjs report: every authored .md, its stamp state, its verdict
|
|
5
|
+
// node scripts/stamp-sweep.mjs --apply write the stamps git can prove
|
|
6
|
+
// node scripts/stamp-sweep.mjs --json machine-readable, same evaluation
|
|
7
|
+
//
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// WHY THIS EXISTS SEPARATELY FROM THE HOOK. `plugin/scripts/md-stamp.mjs` maintains stamps when a
|
|
10
|
+
// file is edited. That is necessary and it is not sufficient, and the adversarial duel on ADR-056
|
|
11
|
+
// said why in one sentence:
|
|
12
|
+
//
|
|
13
|
+
// "Insert-on-touch fires only when a file IS edited — so it reaches actively-edited files, the
|
|
14
|
+
// ones least likely to be stale, and never reaches a stale file, BY DEFINITION OF STALE."
|
|
15
|
+
//
|
|
16
|
+
// The owner's rule is about the documents sitting on disk TODAY: "so I know if they're stale or
|
|
17
|
+
// current actually in there so that I can read it, not just you." Measured 2026-07-27: 166 of 239
|
|
18
|
+
// .md files carry no stamp. The hook would have reached approximately none of them. This reaches
|
|
19
|
+
// them once; the hook keeps them true afterwards.
|
|
20
|
+
//
|
|
21
|
+
// ONE IMPLEMENTATION, TWO CALLERS. Every placement decision lives in md-stamp.mjs (`ensureStamp`,
|
|
22
|
+
// `stampInsertionPoint`, `hasStamp`) and is imported here. A second copy of "where does the stamp
|
|
23
|
+
// go" is the exact drift this whole ADR is about.
|
|
24
|
+
//
|
|
25
|
+
// NOTHING IS EVER INVENTED — the load-bearing rule, inherited verbatim from doc-currency.mjs:
|
|
26
|
+
// · no git history -> SKIP and say so. A file git cannot date does not get a date.
|
|
27
|
+
// · dirty working tree -> SKIP. The last commit's date is not the date of the current contents,
|
|
28
|
+
// and stamping it would assert a freshness that is not true.
|
|
29
|
+
// · already stamped -> SKIP, byte-for-byte untouched.
|
|
30
|
+
// · unrecognised prologue -> SKIP. MDX exports, HTML comments, license banners: an insertion that
|
|
31
|
+
// corrupts someone's document is worse than a document with no date.
|
|
32
|
+
//
|
|
33
|
+
// GENERATED MARKDOWN IS NOT AN AUTHORED DOCUMENT. 96 of the 166 unstamped files live under kb/,
|
|
34
|
+
// dist/ and .agentic-qe/logs/. Stamping machine output is noise wearing the costume of signal, and
|
|
35
|
+
// it would bury the ~70 files where the stamp actually carries information.
|
|
36
|
+
|
|
37
|
+
import fs from 'node:fs';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { spawnSync } from 'node:child_process';
|
|
40
|
+
import { fileURLToPath } from 'node:url';
|
|
41
|
+
import { ensureStamp, hasStamp, stampInsertionPoint } from '../plugin/scripts/md-stamp.mjs';
|
|
42
|
+
|
|
43
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
export const REPO_ROOT = path.resolve(HERE, '..');
|
|
45
|
+
|
|
46
|
+
// Directories whose markdown is GENERATED, vendored, or archival — never authored by a human here.
|
|
47
|
+
export const EXCLUDED = [
|
|
48
|
+
'node_modules', '.git', 'kb', 'dist', 'clones', 'archive', '.agentic-qe',
|
|
49
|
+
'coverage', '.next', 'build', 'tmp', '.swarm',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function git(root, args) {
|
|
53
|
+
const r = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
54
|
+
return { ok: r.status === 0, out: (r.stdout || '').trim() };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Every authored .md tracked by git. Untracked files are out of scope — git cannot date them. */
|
|
58
|
+
export function authoredDocs(root = REPO_ROOT) {
|
|
59
|
+
const ls = git(root, ['ls-files', '--', '*.md']);
|
|
60
|
+
if (!ls.ok) return [];
|
|
61
|
+
return ls.out.split('\n').filter(Boolean).filter((rel) => {
|
|
62
|
+
const segs = rel.split('/');
|
|
63
|
+
return !segs.some((s) => EXCLUDED.includes(s));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Dates git can PROVE for this path. Absent is absent; nothing is inferred from a neighbour. */
|
|
68
|
+
export function gitDates(root, rel) {
|
|
69
|
+
const created = git(root, ['log', '--follow', '--diff-filter=A', '--format=%cs', '-1', '--', rel]);
|
|
70
|
+
const updated = git(root, ['log', '-1', '--format=%cs', '--', rel]);
|
|
71
|
+
const dirty = git(root, ['status', '--porcelain', '--', rel]);
|
|
72
|
+
return {
|
|
73
|
+
created: created.ok && /^\d{4}-\d{2}-\d{2}$/.test(created.out) ? created.out : null,
|
|
74
|
+
updated: updated.ok && /^\d{4}-\d{2}-\d{2}$/.test(updated.out) ? updated.out : null,
|
|
75
|
+
dirty: Boolean(dirty.out),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function evaluate(root = REPO_ROOT, docs = authoredDocs(root)) {
|
|
80
|
+
const rows = [];
|
|
81
|
+
for (const rel of docs) {
|
|
82
|
+
const abs = path.join(root, rel);
|
|
83
|
+
let content;
|
|
84
|
+
try { content = fs.readFileSync(abs, 'utf8'); } catch { continue; }
|
|
85
|
+
|
|
86
|
+
if (hasStamp(content)) { rows.push({ rel, verdict: 'already-stamped' }); continue; }
|
|
87
|
+
|
|
88
|
+
const at = stampInsertionPoint(content);
|
|
89
|
+
if (!at) { rows.push({ rel, verdict: 'skip-prologue', why: 'prologue shape not recognised — refusing to insert' }); continue; }
|
|
90
|
+
|
|
91
|
+
const d = gitDates(root, rel);
|
|
92
|
+
if (d.dirty) { rows.push({ rel, verdict: 'skip-dirty', why: 'working tree modified — the last commit date is not the date of these contents' }); continue; }
|
|
93
|
+
if (!d.updated) { rows.push({ rel, verdict: 'skip-no-history', why: 'no commits — no date can be derived, and none will be invented' }); continue; }
|
|
94
|
+
|
|
95
|
+
const next = ensureStamp(content, { updated: d.updated, created: d.created });
|
|
96
|
+
if (next === content) { rows.push({ rel, verdict: 'no-change' }); continue; }
|
|
97
|
+
rows.push({ rel, verdict: 'would-stamp', placement: at.kind, updated: d.updated, created: d.created, next });
|
|
98
|
+
}
|
|
99
|
+
return rows;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function main() {
|
|
103
|
+
const argv = process.argv.slice(2);
|
|
104
|
+
const apply = argv.includes('--apply');
|
|
105
|
+
const asJson = argv.includes('--json');
|
|
106
|
+
const rows = evaluate();
|
|
107
|
+
|
|
108
|
+
if (asJson) {
|
|
109
|
+
console.log(JSON.stringify(rows.map(({ next, ...r }) => r), null, 2));
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const by = (v) => rows.filter((r) => r.verdict === v);
|
|
114
|
+
const todo = by('would-stamp');
|
|
115
|
+
|
|
116
|
+
console.log(`\nstamp sweep — ${rows.length} authored document(s) under ${REPO_ROOT}\n`);
|
|
117
|
+
for (const r of todo) {
|
|
118
|
+
const c = r.created && r.created !== r.updated ? `, created ${r.created}` : '';
|
|
119
|
+
console.log(` ${apply ? '[stamped] ' : '[would stamp]'} ${r.rel} — updated ${r.updated}${c} (derived-from-git, ${r.placement})`);
|
|
120
|
+
if (apply) {
|
|
121
|
+
try { fs.writeFileSync(path.join(REPO_ROOT, r.rel), r.next); }
|
|
122
|
+
catch (e) { console.log(` [failed] ${r.rel}: ${e.message}`); }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const skipped = [...by('skip-prologue'), ...by('skip-dirty'), ...by('skip-no-history')];
|
|
127
|
+
if (skipped.length) {
|
|
128
|
+
console.log(`\n ${skipped.length} deliberately NOT stamped — no date will be invented:\n`);
|
|
129
|
+
for (const s of skipped) console.log(` ○ ${s.rel}\n ${s.why}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`\nsummary: ${by('already-stamped').length} already stamped · ${todo.length} `
|
|
133
|
+
+ `${apply ? 'stamped' : 'stampable'} · ${skipped.length} skipped (honestly)`);
|
|
134
|
+
if (!apply && todo.length) console.log('\ndry run — nothing written. Re-run with --apply to write them.');
|
|
135
|
+
console.log('Dates are only ever copied out of git; nothing is reconstructed.\n');
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isMain() {
|
|
140
|
+
try { return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); }
|
|
141
|
+
catch { return false; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (isMain()) process.exit(main());
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/status-honesty.mjs — THE ANTI-FAKING GATE (ADR-0024). No script may ASSERT a terminal
|
|
3
|
+
// success status; success must be DERIVED from a verifiable artifact.
|
|
4
|
+
//
|
|
5
|
+
// WHY (Stuart, 2026-07-18, after the word "faking" kept coming back): "The fact that you're still
|
|
6
|
+
// creating code in a sloppy way that allows it to fake results is toxic and needs to be blown out of
|
|
7
|
+
// the system." The historical sin: issue-fix.mjs once hardcoded status:'completed' for issues it
|
|
8
|
+
// never touched — no branch, no comment, no artifact. The class, per rUv's own architecture:
|
|
9
|
+
// - a status FIELD can be forged; only RE-DERIVATION from the real artifact catches it
|
|
10
|
+
// (ruvector proof-gate #506: "a structural scan cannot catch a structurally-valid forgery");
|
|
11
|
+
// - verification must pin to the trusted source, never the self-asserted field
|
|
12
|
+
// (ruflo signed-artifact: "an attacker controls that field; pinning to it is a no-op").
|
|
13
|
+
//
|
|
14
|
+
// LAYER 1 (this file): a lexical scanner over every automation script. It flags any WRITE of a
|
|
15
|
+
// terminal success literal (status:'completed', state:"ok", outcome:'success', …) whose statement
|
|
16
|
+
// shows NO derivation marker — no captured exit code, no comparison, no ternary on a runtime result,
|
|
17
|
+
// no registered verifier call. A comment cannot satisfy it; only a derivation expression can.
|
|
18
|
+
// SCOPE IS DELIBERATE: terminal RECEIPT/STATUS tokens only (the historical sin's exact shape), not
|
|
19
|
+
// generic `ok: true` returns — a gate that cries wolf gets disabled, which is worse than no gate.
|
|
20
|
+
// KNOWN RESIDUAL (documented, accepted): a lexical layer can be gamed by computed keys or an
|
|
21
|
+
// always-true verifier — that is why Layer 2 (behavioral fixtures in derived-status.test.mjs)
|
|
22
|
+
// executes the real wrapper and the real outcome-verifier, and why the known-bad fixture must FAIL
|
|
23
|
+
// on every run. Text can lie; execution can't.
|
|
24
|
+
//
|
|
25
|
+
// Usage: node scripts/status-honesty.mjs exit 0 = clean, 1 = violations (printed)
|
|
26
|
+
// imported by tests/unit/derived-status.test.mjs (scanSource / scanRepo)
|
|
27
|
+
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
|
|
34
|
+
// Terminal success literals — the exact shape of the historical sin. Word-bounded, quote-flexible.
|
|
35
|
+
const SUCCESS_WRITES = [
|
|
36
|
+
/\bstatus\s*[:=]\s*['"](completed|success|succeeded|done|fixed|shipped|passed)['"]/,
|
|
37
|
+
/\bstate\s*[:=]\s*['"](ok|completed|success|succeeded|passed)['"]/,
|
|
38
|
+
/\boutcome\s*[:=]\s*['"](success|succeeded|completed)['"]/,
|
|
39
|
+
/"state"\s*:\s*"(ok|completed|success)"/, // literal JSON in sh heredocs/printf
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Derivation markers: evidence the assignment is CONDITIONAL on a runtime result. The registry is
|
|
43
|
+
// extended by PR — a new receipt-writer fails this gate by default until its derivation is named.
|
|
44
|
+
const DERIVATION_MARKERS = [
|
|
45
|
+
'=== 0', '!== 0', '== 0', '!= 0', '-eq 0', '-ne 0', // exit-code comparisons (js + sh)
|
|
46
|
+
'? ', // ternary on a captured result (paired with the comparisons above in practice)
|
|
47
|
+
'.has(', // SUCCESS_OUTCOMES.has(...) — the issue-fix standard
|
|
48
|
+
'r.status', 'res.status', 'res.ok', 'r.ok', '.exitCode', 'exit_code', 'status ===', 'code -eq',
|
|
49
|
+
'remoteBranchExists', 'installedVersion', 'verifyOutcome', 'delivered', 'succeeded ?', 'sent ===',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/** Scan one source text. Returns violations: [{line, text}] — success literal with no derivation. */
|
|
53
|
+
export function scanSource(src, _name = '(inline)') {
|
|
54
|
+
const violations = [];
|
|
55
|
+
const lines = src.split('\n');
|
|
56
|
+
for (let i = 0; i < lines.length; i++) {
|
|
57
|
+
const line = lines[i];
|
|
58
|
+
const t = line.trim();
|
|
59
|
+
if (t.startsWith('//') || t.startsWith('#') || t.startsWith('*')) continue; // comments assert nothing
|
|
60
|
+
if (!SUCCESS_WRITES.some((re) => re.test(line))) continue;
|
|
61
|
+
// The statement window: this line ± 2 (multi-line object literals / sh case arms).
|
|
62
|
+
const windowText = lines.slice(Math.max(0, i - 2), i + 3).join('\n');
|
|
63
|
+
if (DERIVATION_MARKERS.some((m) => windowText.includes(m))) continue;
|
|
64
|
+
violations.push({ line: i + 1, text: t.slice(0, 160) });
|
|
65
|
+
}
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const SCAN_DIRS = ['scripts', 'plugin/scripts', 'scripts/git-hooks'];
|
|
70
|
+
const EXT = /\.(mjs|cjs|js|sh)$/;
|
|
71
|
+
const EXEMPT = new Set([
|
|
72
|
+
// the scanner itself (its pattern table would self-flag) and test fixtures (known-bad lives there)
|
|
73
|
+
'scripts/status-honesty.mjs',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/** Scan the repo. Returns [{file, violations}] for files with hits. */
|
|
77
|
+
export function scanRepo(root = ROOT) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const dir of SCAN_DIRS) {
|
|
80
|
+
const abs = path.join(root, dir);
|
|
81
|
+
if (!fs.existsSync(abs)) continue;
|
|
82
|
+
for (const f of fs.readdirSync(abs)) {
|
|
83
|
+
if (!EXT.test(f)) continue;
|
|
84
|
+
const rel = path.join(dir, f);
|
|
85
|
+
if (EXEMPT.has(rel)) continue;
|
|
86
|
+
const v = scanSource(fs.readFileSync(path.join(root, rel), 'utf8'), rel);
|
|
87
|
+
if (v.length) out.push({ file: rel, violations: v });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
94
|
+
const bad = scanRepo();
|
|
95
|
+
if (bad.length) {
|
|
96
|
+
console.error('status-honesty: ASSERTED (underived) terminal success literals found:\n');
|
|
97
|
+
for (const b of bad) for (const v of b.violations) console.error(` ${b.file}:${v.line} ${v.text}`);
|
|
98
|
+
console.error('\nA success status must DERIVE from a verifiable artifact (exit code, re-checked branch/comment, re-read file). Name the derivation in the same statement, or record the honest failure.');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
console.log('status-honesty: clean — every terminal success literal is derivation-conditioned.');
|
|
102
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// sync-version.mjs — propagate the ONE source-of-truth version (plugin.json) into the files that must
|
|
2
|
+
// carry a literal, and (with --check) fail if any surface has drifted. SEC-0010 #2 / ADR-0009 decision 1.
|
|
3
|
+
//
|
|
4
|
+
// node scripts/sync-version.mjs # write plugin.json's version into all inheriting files
|
|
5
|
+
// node scripts/sync-version.mjs --check # CI mode: exit 1 if any surface disagrees (writes nothing)
|
|
6
|
+
//
|
|
7
|
+
// The ONE hand-edited number is plugin/.claude-plugin/plugin.json `version`. Bump it there, run this,
|
|
8
|
+
// and every other surface follows. `--check` in CI makes drift impossible to merge.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { getVersion } from './version.mjs';
|
|
13
|
+
import { getVersionTag } from './version.mjs';
|
|
14
|
+
import { verifyRvfGenerations } from './rvf-generation.mjs';
|
|
15
|
+
|
|
16
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const CHECK = process.argv.includes('--check');
|
|
18
|
+
const V = getVersion();
|
|
19
|
+
const TODAY = new Date().toISOString().slice(0, 10);
|
|
20
|
+
let drift = 0;
|
|
21
|
+
|
|
22
|
+
export function readLockVersion(s) {
|
|
23
|
+
const doc = JSON.parse(s);
|
|
24
|
+
const top = doc.version ?? null;
|
|
25
|
+
const root = doc.packages?.['']?.version ?? null;
|
|
26
|
+
return top === root ? top : `mixed(${top}, ${root})`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeLockVersion(s, version) {
|
|
30
|
+
const doc = JSON.parse(s);
|
|
31
|
+
doc.version = version;
|
|
32
|
+
if (doc.packages?.['']) doc.packages[''].version = version;
|
|
33
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Each target: a file, a regex to find the version-bearing line, and the corrected line.
|
|
37
|
+
const targets = [
|
|
38
|
+
{ // Codex plugin manifest — same product, same release train as the Claude manifest
|
|
39
|
+
file: 'plugin/.codex-plugin/plugin.json',
|
|
40
|
+
get: (s) => (JSON.parse(s).version),
|
|
41
|
+
set: (s) => s.replace(/("version"\s*:\s*)"[^"]+"/, `$1"${V}"`),
|
|
42
|
+
},
|
|
43
|
+
{ // npm installer package — inherits the product version (publish in lockstep)
|
|
44
|
+
file: 'package.json',
|
|
45
|
+
get: (s) => (JSON.parse(s).version),
|
|
46
|
+
set: (s) => s.replace(/("version"\s*:\s*)"[^"]+"/, `$1"${V}"`),
|
|
47
|
+
},
|
|
48
|
+
{ // npm lock metadata is part of the packed candidate lineage too
|
|
49
|
+
file: 'package-lock.json',
|
|
50
|
+
get: readLockVersion,
|
|
51
|
+
set: (s) => writeLockVersion(s, V),
|
|
52
|
+
},
|
|
53
|
+
{ // the brain bundle's product-version stamp (corpus provenance stays in SOURCE.json separately)
|
|
54
|
+
file: 'data/manifest.json',
|
|
55
|
+
get: (s) => { const m = s.match(/"brainVersion"\s*:\s*"([^"]+)"/); return m ? m[1] : null; },
|
|
56
|
+
set: (s) => s.replace(/("brainVersion"\s*:\s*)"[^"]+"/, `$1"${V}"`),
|
|
57
|
+
},
|
|
58
|
+
{ // the MCP server's advertised version
|
|
59
|
+
file: 'kb/package.json',
|
|
60
|
+
get: (s) => (JSON.parse(s).version),
|
|
61
|
+
set: (s) => s.replace(/("version"\s*:\s*)"[^"]+"/, `$1"${V}"`),
|
|
62
|
+
},
|
|
63
|
+
{ // the human primer's header stamp — shipped in the bundle, read by real users
|
|
64
|
+
// (drifted to v2.0.0 during the 2.0.1 release because no target covered it — 2026-07-10 gremlin hunt)
|
|
65
|
+
file: 'primer/ruvnet-primer.md',
|
|
66
|
+
get: (s) => { const m = s.match(/Brain version: v([^\s·`]+)/); return m ? m[1] : null; },
|
|
67
|
+
set: (s) => s.replace(/(Brain version: v)[^\s·`]+/, `$1${V}`),
|
|
68
|
+
},
|
|
69
|
+
{ // the live explainer page carries the version twice (JSON-LD softwareVersion + the status badge).
|
|
70
|
+
// get() returns the value only when BOTH agree; a mismatch reports as drift too.
|
|
71
|
+
file: 'explainer/index.html',
|
|
72
|
+
get: (s) => {
|
|
73
|
+
const ld = s.match(/"softwareVersion"\s*:\s*"([^"]+)"/)?.[1] ?? null;
|
|
74
|
+
const badge = s.match(/·\s*v([0-9][^\s<]*)</)?.[1] ?? null;
|
|
75
|
+
return ld === badge ? ld : `mixed(${ld}, ${badge})`;
|
|
76
|
+
},
|
|
77
|
+
set: (s) => s
|
|
78
|
+
.replace(/("softwareVersion"\s*:\s*)"[^"]+"/, `$1"${V}"`)
|
|
79
|
+
.replace(/(·\s*v)[0-9][^\s<]*</, `$1${V}<`),
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
for (const t of targets) {
|
|
84
|
+
const p = path.join(ROOT, t.file);
|
|
85
|
+
if (!fs.existsSync(p)) continue;
|
|
86
|
+
const s = fs.readFileSync(p, 'utf8');
|
|
87
|
+
const cur = t.get(s);
|
|
88
|
+
if (cur === V) continue;
|
|
89
|
+
if (CHECK) { console.error(`[version] DRIFT: ${t.file} = ${cur}, expected ${V}`); drift++; }
|
|
90
|
+
else { fs.writeFileSync(p, t.set(s)); console.log(`[version] ${t.file}: ${cur} -> ${V}`); }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The installed knowledge bundle and the checksum-bound RVF generation ledger are release
|
|
94
|
+
// surfaces too. They are generated artifacts, so tolerate absence in a source-only checkout; when
|
|
95
|
+
// present they must carry the same bare field and v-prefixed tag as npm/GitHub.
|
|
96
|
+
for (const rel of ['kb/RVF-GENERATIONS.json']) {
|
|
97
|
+
const p = path.join(ROOT, rel);
|
|
98
|
+
if (!fs.existsSync(p)) continue;
|
|
99
|
+
const doc = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
100
|
+
const expectedTag = getVersionTag();
|
|
101
|
+
const bad = doc.brainVersion !== V || doc.releaseTag !== expectedTag;
|
|
102
|
+
if (!bad) continue;
|
|
103
|
+
if (CHECK) {
|
|
104
|
+
console.error(`[version] DRIFT: ${rel} = ${doc.brainVersion}/${doc.releaseTag}, expected ${V}/${expectedTag}`);
|
|
105
|
+
drift++;
|
|
106
|
+
} else {
|
|
107
|
+
doc.brainVersion = V;
|
|
108
|
+
doc.releaseTag = expectedTag;
|
|
109
|
+
fs.writeFileSync(p, `${JSON.stringify(doc, null, 2)}\n`);
|
|
110
|
+
console.log(`[version] ${rel}: -> ${V}/${expectedTag}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (CHECK && fs.existsSync(path.join(ROOT, 'kb/RVF-GENERATIONS.json'))) {
|
|
115
|
+
const kbDir = path.join(ROOT, 'kb');
|
|
116
|
+
const requiredStores = fs.readdirSync(kbDir)
|
|
117
|
+
.filter((file) => file.endsWith('.big.rvf'))
|
|
118
|
+
.map((file) => file.slice(0, -'.big.rvf'.length));
|
|
119
|
+
// CI/source-only clones intentionally omit the gitignored RVF binaries. In that shape, validate
|
|
120
|
+
// the committed ledger's version fields but defer byte checks to bundle assembly, where the RVFs
|
|
121
|
+
// are present. A checkout containing any canonical RVF remains fail-closed for every ledger row.
|
|
122
|
+
const { failures } = verifyRvfGenerations(kbDir, {
|
|
123
|
+
requiredStores,
|
|
124
|
+
allowMissingFiles: requiredStores.length === 0,
|
|
125
|
+
});
|
|
126
|
+
for (const failure of failures) {
|
|
127
|
+
console.error(`[version] RVF GENERATION DRIFT: ${failure}`);
|
|
128
|
+
drift++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// README: the heading badge encodes the product version twice — the human-readable alt text
|
|
133
|
+
// ("RuvNet Brain version X — updated Y") and the shields.io URL token ("version_X-updated_Y", with
|
|
134
|
+
// hyphens escaped as `--`). --check flags version drift; write-mode now FIXES it in place (version
|
|
135
|
+
// only — the nightly publisher owns the `updated_` timestamp), so `--check` and a plain write agree.
|
|
136
|
+
{
|
|
137
|
+
const p = path.join(ROOT, 'README.md');
|
|
138
|
+
const s = fs.readFileSync(p, 'utf8');
|
|
139
|
+
const m = s.match(/version_([0-9][^-\s)]*(?:--[^-\s)]*)*)-updated_/); // inside the badge URL
|
|
140
|
+
const badgeVer = m ? m[1].replace(/--/g, '-') : null;
|
|
141
|
+
if (badgeVer !== V) {
|
|
142
|
+
if (CHECK) { console.error(`[version] DRIFT: README badge = ${badgeVer}, expected ${V}`); drift++; }
|
|
143
|
+
else {
|
|
144
|
+
const vBadge = V.replace(/-/g, '--'); // shields.io escapes a literal hyphen as `--`
|
|
145
|
+
const next = s
|
|
146
|
+
// URL token: version_<ver>-updated_<timestamp> → swap <ver>, keep the timestamp
|
|
147
|
+
.replace(/(version_)[0-9][^-\s)]*(?:--[^-\s)]*)*(-updated_)/, `$1${vBadge}$2`)
|
|
148
|
+
// alt text: "RuvNet Brain version <ver> — updated <timestamp>" → swap <ver>, keep the timestamp
|
|
149
|
+
.replace(/(RuvNet Brain version )\S+( — updated )/, `$1${V}$2`);
|
|
150
|
+
if (next !== s) { fs.writeFileSync(p, next); console.log(`[version] README badge: ${badgeVer} -> ${V}`); }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Guard: no stray hardcoded product-version literal anywhere in the source tree — every code path must
|
|
156
|
+
// read getVersion() (or inherit a synced literal via the `targets` above / the README badge). Rather than
|
|
157
|
+
// a fixed 3-file allowlist (which silently missed strays in every other file), walk the WHOLE tree and
|
|
158
|
+
// scan source files, skipping the regenerable / external / VCS dirs. A quoted vX.Y.Z / X.Y.Z(-dev)? literal
|
|
159
|
+
// is flagged when it IS the current product version (bare or `v`-tagged) OR is a `-dev` prerelease literal —
|
|
160
|
+
// i.e. anything that is meant to be the product version, not just the `-dev` form the old guard caught.
|
|
161
|
+
// A line can opt out with `sync-version-ignore` (one documented last-ditch fallback); the version toolchain
|
|
162
|
+
// itself is exempt because it legitimately embeds version strings in doc examples + unit-test fixtures.
|
|
163
|
+
if (CHECK) {
|
|
164
|
+
// `worktrees` (2026-07-27): parallel agents run in git worktrees under .claude/worktrees/, which
|
|
165
|
+
// is GITIGNORED (.gitignore:110) but still on disk — and this walker reads the FILESYSTEM, not
|
|
166
|
+
// git's index. So every agent worktree's copy of the repo got scanned for version literals and
|
|
167
|
+
// the pre-push gate refused a legitimate push over version strings inside transient checkouts
|
|
168
|
+
// that ship to nobody. A shipping-surface scan must not see a scratch copy of itself.
|
|
169
|
+
const SKIP_DIRS = new Set(['node_modules', 'clones', 'dist', '.git', 'worktrees']);
|
|
170
|
+
const CODE_EXT = new Set(['.mjs', '.js', '.cjs', '.ts', '.sh']);
|
|
171
|
+
const EXEMPT = new Set([ // the version machinery defines/tests these literals — linting them = false positive
|
|
172
|
+
'scripts/version.mjs', 'scripts/version.test.mjs', 'scripts/sync-version.mjs', 'scripts/self-update.mjs',
|
|
173
|
+
// The unit tests moved from scripts/ to tests/unit/ but this list did not follow them, so `--check`
|
|
174
|
+
// has been failing on its OWN test fixtures — a gate nobody could pass, which is a gate nobody heeds.
|
|
175
|
+
'tests/unit/version.test.mjs', 'tests/unit/sync-version-drift.test.mjs', 'tests/unit/self-update-plan.test.mjs',
|
|
176
|
+
// Asserts stripTag('v1.14.1-dev') === '1.14.1-dev' — the literals ARE the fixture.
|
|
177
|
+
'tests/unit/version-tag-vs-field.test.mjs',
|
|
178
|
+
]);
|
|
179
|
+
const litRe = /['"`](v?\d+\.\d+\.\d+(?:-dev)?)['"`]/g;
|
|
180
|
+
|
|
181
|
+
const walk = (dir) => {
|
|
182
|
+
const files = [];
|
|
183
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
184
|
+
const abs = path.join(dir, ent.name);
|
|
185
|
+
if (ent.isDirectory()) { if (!SKIP_DIRS.has(ent.name)) files.push(...walk(abs)); }
|
|
186
|
+
else if (ent.isFile() && CODE_EXT.has(path.extname(ent.name))) files.push(abs);
|
|
187
|
+
}
|
|
188
|
+
return files;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const strays = [];
|
|
192
|
+
for (const abs of walk(ROOT)) {
|
|
193
|
+
// EXEMPT keys are forward-slash; path.relative() yields backslashes on Windows, which made the
|
|
194
|
+
// guard flag its own exempt fixtures there — the same gate-nobody-can-pass failure as above.
|
|
195
|
+
const rel = path.relative(ROOT, abs).split(path.sep).join('/');
|
|
196
|
+
if (EXEMPT.has(rel)) continue;
|
|
197
|
+
fs.readFileSync(abs, 'utf8').split('\n').forEach((ln, i) => {
|
|
198
|
+
if (ln.includes('sync-version-ignore')) return;
|
|
199
|
+
for (const mm of ln.matchAll(litRe)) {
|
|
200
|
+
const lit = mm[1].replace(/^v/, '');
|
|
201
|
+
if (lit === V || lit.endsWith('-dev')) strays.push(`${rel}:${i + 1}: ${ln.trim()}`);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (strays.length) {
|
|
206
|
+
console.error(`[version] DRIFT: ${strays.length} stray hardcoded version literal(s) — read getVersion() instead:`);
|
|
207
|
+
for (const s of strays) console.error(` ${s}`);
|
|
208
|
+
drift++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (CHECK) {
|
|
213
|
+
if (drift) { console.error(`\n[version] ${drift} surface(s) drifted from the source of truth (${V}). Run: node scripts/sync-version.mjs`); process.exit(1); }
|
|
214
|
+
console.log(`[version] all surfaces agree on ${V} ✓`);
|
|
215
|
+
} else {
|
|
216
|
+
console.log(`[version] synced everything to ${V} (source: plugin/.claude-plugin/plugin.json)`);
|
|
217
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/token-report.mjs — the read side of the token meter (ADR-0011 token_cost_efficiency).
|
|
3
|
+
//
|
|
4
|
+
// The brain injects context via hooks (ground-ruvnet.sh / session-start.sh) and serves retrieval
|
|
5
|
+
// via MCP (forge-mcp-all.mjs); each of those appends one JSON line per fire to
|
|
6
|
+
// .ruvnet-brain/token-ledger.jsonl with the REAL measured size of what it emitted. This script
|
|
7
|
+
// aggregates that ledger into a plain-text table: per class, the fire count and the p50/p95 byte
|
|
8
|
+
// sizes plus an estimated token total — HONESTLY labeled an estimate (tokens ≈ bytes/4; we measure
|
|
9
|
+
// bytes exactly, we do not run a tokenizer). Zero dependencies, Node built-ins only.
|
|
10
|
+
//
|
|
11
|
+
// node scripts/token-report.mjs # reads ./.ruvnet-brain/token-ledger.jsonl
|
|
12
|
+
// node scripts/token-report.mjs --ledger <path> # explicit ledger (tests use this)
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
|
|
17
|
+
const argv = process.argv.slice(2);
|
|
18
|
+
const ledgerIdx = argv.indexOf('--ledger');
|
|
19
|
+
// The canonical ledger is user-level (issue #36 — writing per-CWD scattered hidden directories into
|
|
20
|
+
// users' project trees). A legacy per-project ledger is still READ when one exists and the canonical
|
|
21
|
+
// one does not, so nobody's existing measurements silently vanish in the move.
|
|
22
|
+
const CANONICAL_LEDGER = path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'ruvnet-brain', 'token-ledger.jsonl');
|
|
23
|
+
const LEGACY_LEDGER = path.join(process.cwd(), '.ruvnet-brain', 'token-ledger.jsonl');
|
|
24
|
+
const LEDGER =
|
|
25
|
+
ledgerIdx !== -1 && argv[ledgerIdx + 1]
|
|
26
|
+
? argv[ledgerIdx + 1]
|
|
27
|
+
: (fs.existsSync(CANONICAL_LEDGER) || !fs.existsSync(LEGACY_LEDGER) ? CANONICAL_LEDGER : LEGACY_LEDGER);
|
|
28
|
+
|
|
29
|
+
// Nearest-rank percentile on a pre-sorted ascending array — the standard textbook definition,
|
|
30
|
+
// chosen because it always returns a value that actually occurred (no interpolation to invent one).
|
|
31
|
+
function percentile(sortedAsc, p) {
|
|
32
|
+
if (sortedAsc.length === 0) return 0;
|
|
33
|
+
const rank = Math.max(1, Math.ceil((p / 100) * sortedAsc.length));
|
|
34
|
+
return sortedAsc[rank - 1];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const estTokens = (bytes) => Math.round(bytes / 4);
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(LEDGER)) {
|
|
40
|
+
console.log(`meter: no data yet (${LEDGER} not found — it appears after the first hook/MCP fire)`);
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// One group per class: hook lines group by their prompt-class, MCP lines by tool name — the label
|
|
45
|
+
// says which side of the brain spent the bytes.
|
|
46
|
+
const groups = new Map();
|
|
47
|
+
let skipped = 0;
|
|
48
|
+
for (const line of fs.readFileSync(LEDGER, 'utf8').split('\n')) {
|
|
49
|
+
if (!line.trim()) continue;
|
|
50
|
+
let e;
|
|
51
|
+
try { e = JSON.parse(line); } catch { skipped++; continue; }
|
|
52
|
+
const bytes = Number(e?.bytes);
|
|
53
|
+
if (!e || !e.source || !Number.isFinite(bytes)) { skipped++; continue; }
|
|
54
|
+
const label = e.source === 'mcp' ? `mcp:${e.tool || 'unknown'}` : `hook:${e.class || 'unknown'}`;
|
|
55
|
+
if (!groups.has(label)) groups.set(label, []);
|
|
56
|
+
groups.get(label).push(bytes);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (groups.size === 0) {
|
|
60
|
+
console.log(`meter: no data yet (${LEDGER} has no valid entries)`);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const rows = [...groups.entries()]
|
|
65
|
+
.map(([label, list]) => {
|
|
66
|
+
const sorted = [...list].sort((a, b) => a - b);
|
|
67
|
+
const total = sorted.reduce((s, b) => s + b, 0);
|
|
68
|
+
return {
|
|
69
|
+
label,
|
|
70
|
+
count: sorted.length,
|
|
71
|
+
p50: percentile(sorted, 50),
|
|
72
|
+
p95: percentile(sorted, 95),
|
|
73
|
+
total,
|
|
74
|
+
tokens: estTokens(total),
|
|
75
|
+
};
|
|
76
|
+
})
|
|
77
|
+
.sort((a, b) => b.total - a.total);
|
|
78
|
+
|
|
79
|
+
const grandBytes = rows.reduce((s, r) => s + r.total, 0);
|
|
80
|
+
const grandCount = rows.reduce((s, r) => s + r.count, 0);
|
|
81
|
+
|
|
82
|
+
// Plain fixed-width table — no dependencies, pipes cleanly.
|
|
83
|
+
const cols = [
|
|
84
|
+
['class', (r) => r.label],
|
|
85
|
+
['count', (r) => String(r.count)],
|
|
86
|
+
['p50 bytes', (r) => String(r.p50)],
|
|
87
|
+
['p95 bytes', (r) => String(r.p95)],
|
|
88
|
+
['total bytes', (r) => String(r.total)],
|
|
89
|
+
['~tokens (bytes/4)', (r) => String(r.tokens)],
|
|
90
|
+
];
|
|
91
|
+
const widths = cols.map(([h, f]) => Math.max(h.length, ...rows.map((r) => f(r).length)));
|
|
92
|
+
const fmt = (cells) => cells.map((cell, i) => String(cell).padEnd(widths[i])).join(' ');
|
|
93
|
+
|
|
94
|
+
console.log(`token meter — ${LEDGER}`);
|
|
95
|
+
console.log(fmt(cols.map(([h]) => h)));
|
|
96
|
+
console.log(fmt(widths.map((w) => '-'.repeat(w))));
|
|
97
|
+
for (const r of rows) console.log(fmt(cols.map(([, f]) => f(r))));
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(
|
|
100
|
+
`session total: ${grandCount} injections, ${grandBytes} bytes ≈ ${estTokens(grandBytes)} tokens (estimate: bytes/4)`,
|
|
101
|
+
);
|
|
102
|
+
if (skipped > 0) console.log(`(${skipped} malformed line(s) skipped)`);
|