ruvnet-brain 4.3.28 → 4.3.30

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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/bin/install.mjs +90 -8
  3. package/console/app.js +7 -2
  4. package/kb/corpus-release-identity.mjs +73 -0
  5. package/package.json +4 -3
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/mcp/managed-cli-interface.mjs +83 -9
  9. package/plugin/scripts/advocacy-route.mjs +4 -1
  10. package/plugin/scripts/capacity-aware-parallel-work.mjs +4 -0
  11. package/plugin/scripts/continuation-gate.mjs +71 -0
  12. package/plugin/scripts/decision-gate.mjs +29 -57
  13. package/plugin/scripts/ground-ruvnet.sh +77 -4
  14. package/plugin/scripts/grounding-stamp.sh +34 -5
  15. package/plugin/scripts/grounding-turn-gate.mjs +8 -2
  16. package/plugin/scripts/grounding-turn-mark.mjs +6 -1
  17. package/plugin/scripts/hook-input.mjs +54 -0
  18. package/plugin/scripts/hook-shim.mjs +10 -0
  19. package/plugin/scripts/memory-doctor.mjs +43 -0
  20. package/plugin/scripts/nightly-controller.mjs +6 -1
  21. package/plugin/scripts/project-progression-contract.mjs +17 -0
  22. package/plugin/scripts/project-progression-hook.mjs +18 -5
  23. package/plugin/scripts/project-progression-producer.mjs +16 -12
  24. package/plugin/scripts/ruvnet-gate1-pattern.mjs +17 -0
  25. package/plugin/scripts/session-start-core.mjs +11 -5
  26. package/plugin/scripts/session-start-update-plane.mjs +5 -1
  27. package/plugin/scripts/unprompted-runtime.mjs +29 -8
  28. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +1 -1
  29. package/scripts/adr-072-completion.mjs +2 -1
  30. package/scripts/calibrate-router.mjs +6 -6
  31. package/scripts/corpus-reconcile.mjs +32 -3
  32. package/scripts/correction-detect.mjs +10 -11
  33. package/scripts/dispatch-receipt.mjs +2 -2
  34. package/scripts/dual-host-deliberation.mjs +3 -2
  35. package/scripts/execution-policy.mjs +10 -2
  36. package/scripts/gen-console-images.mjs +0 -1
  37. package/scripts/gen-images.mjs +0 -4
  38. package/scripts/host-install-matrix.mjs +7 -4
  39. package/scripts/independent-review-receipt.mjs +7 -8
  40. package/scripts/ingest-repo.mjs +45 -1
  41. package/scripts/learning-replay-cli.mjs +2 -1
  42. package/scripts/learning-replay-fixture.mjs +2 -1
  43. package/scripts/learnings.mjs +19 -5
  44. package/scripts/lesson-migrate-agentdb.mjs +635 -0
  45. package/scripts/loop-checkpoint.mjs +37 -1
  46. package/scripts/metaharness-receipts.mjs +3 -2
  47. package/scripts/nightly-two-run-proof.mjs +6 -6
  48. package/scripts/nightly-watchdog.mjs +10 -2
  49. package/scripts/onboarding-console.mjs +98 -49
  50. package/scripts/oracle/producer-hosts.mjs +2 -1
  51. package/scripts/private-overlay.mjs +31 -9
  52. package/scripts/public-verification-aggregate.mjs +4 -3
  53. package/scripts/publication-receipt.mjs +9 -0
  54. package/scripts/qe/agentic-qe-4.3.mjs +0 -1
  55. package/scripts/rebuild-gists-from-receipts.mjs +1 -1
  56. package/scripts/reconcile-project.mjs +0 -0
  57. package/scripts/release.mjs +34 -82
  58. package/scripts/retrieval-canary.mjs +3 -2
  59. package/scripts/review-model-defaults.mjs +26 -0
  60. package/scripts/route-cheap.mjs +20 -7
  61. package/scripts/router-utilization.mjs +5 -5
  62. package/scripts/rvf-generation.mjs +68 -2
  63. package/scripts/single-source-check.mjs +270 -0
  64. package/scripts/subscription-hosts.mjs +4 -0
  65. package/scripts/sync-version.mjs +22 -0
  66. package/scripts/trismart.mjs +3 -3
  67. package/scripts/wired-check.mjs +40 -4
  68. package/console/assets/memory.webp +0 -0
  69. package/plugin/scripts/version-bump-gate.sh +0 -124
  70. package/scripts/qe/aggregate-4.3.mjs +0 -42
  71. package/scripts/release-convergence-watchdog.mjs +0 -112
  72. package/scripts/stamp-existing-rvf-generations.mjs +0 -53
  73. package/scripts/verify-channels.mjs +0 -196
@@ -1,30 +1,38 @@
1
1
  #!/usr/bin/env node
2
- // scripts/release.mjs — the DEFINITION OF DONE. The only path to the word "shipped."
2
+ // scripts/release.mjs — the ONLY publisher, plus a local preview of the CI qualification gate.
3
3
  //
4
4
  // WHY (2026-07-17, Stuart): "You should be able to take the applied knowledge and build it into a set
5
5
  // of criteria that you always use, not a bunch of suggestions you choose to ignore." Every failure
6
6
  // this session was an ASSUMPTION that survived because the check was a suggestion, not a gate. This
7
- // script turns the checklist into a gate: it runs the criteria in order, STOPS on the first failure,
8
- // and only prints "SHIPPED" when every channel a user touches is proven current and working. There is
9
- // no "I think it's fine" — there is pass or fail.
7
+ // script turns the checklist into a gate: it runs the criteria in order and STOPS on the first
8
+ // failure. There is no "I think it's fine" — there is pass or fail.
10
9
  //
11
- // Check-only mode evaluates source. Publish mode consumes a CI-sealed package and receipt, then
12
- // performs only the staged transaction and public verification. It never rebuilds or retests the
13
- // source: the immutable artifact is the evidence boundary.
10
+ // WHAT EACH MODE ACTUALLY DOES (rewritten 2026-09-26, consolidation/single-source — the previous
11
+ // header called check-only "the DEFINITION OF DONE" and claimed it alone decided "shipped"; that was
12
+ // never true and duplicated a second, drifting gate list alongside CI's real one). Publishing a
13
+ // release — creating the GitHub Release, moving the npm dist-tag — happens ONLY inside the
14
+ // reviewer-protected `protected-release.yml` workflow, against an exact-SHA CI-sealed candidate;
15
+ // `--publish` mode is that workflow's publisher (it validates the protected-invocation receipt before
16
+ // doing anything and refuses outside it), never rebuilds or retests source, and treats the immutable
17
+ // artifact as the evidence boundary. `--check` mode is a read-only LOCAL PREVIEW for a human on a dev
18
+ // branch: it runs the exact same release-qualification gate CI enforces
19
+ // (`scripts/release-qualification.mjs` + `scripts/release-qualification-contract.mjs`, invoked the
20
+ // same way `canonical-qa.yml`/`ci.yml` invoke it) plus the one-publisher check
21
+ // (`scripts/release-authority.mjs`). There is exactly ONE definition of "release-qualified" — the one
22
+ // CI enforces — and this is a preview of it, not a second, independent one.
14
23
  //
15
24
  // Usage:
16
- // node scripts/release.mjs --check # run every gate READ-ONLY (no publish) — the pre-flight
17
- // node scripts/release.mjs --publish # publish the exact CI-sealed artifact
25
+ // node scripts/release.mjs --check # preview the SAME qualification gate CI enforces
26
+ // node scripts/release.mjs --publish # the protected workflow's publisher; not for manual use
18
27
  // node scripts/release.mjs # same as --check
19
28
  //
20
29
  // The gates, in order (fail fast):
21
- // A. version single-source-of-truth agrees (sync-version --check)
22
- // B. full test suite green (npm test — the 60/60)
23
- // C. narrative + unit gates (vitest) incl. the tag/entity-aware "What's new" check
30
+ // A. version single-source-of-truth agrees (sync-version --check) + one protected publisher
31
+ // B. release qualification — scripts/release-qualification.mjs, invoked exactly as CI invokes it
24
32
  // D. [--publish only] stage and promote the exact package plus signed RVF bundle
25
- // E. [--check only] verify current public channels; publish verifies them inside transaction finalization
26
33
 
27
34
  import path from 'node:path';
35
+ import os from 'node:os';
28
36
  import { fileURLToPath } from 'node:url';
29
37
  import { spawnSync, execFileSync } from 'node:child_process';
30
38
  import fs from 'node:fs';
@@ -500,71 +508,22 @@ if (!PUBLISH) {
500
508
  runOrDie('version sync', process.execPath, ['scripts/sync-version.mjs', '--check']);
501
509
  runOrDie('one protected publisher', process.execPath, ['scripts/release-authority.mjs']);
502
510
 
503
- // WIRED-CHECK — refuses to ship a module with zero callers.
511
+ // RELEASE QUALIFICATION — ONE definition of "release-qualified", the one CI enforces.
504
512
  //
505
- // Added 2026-07-22 after this project shipped built-tested-unwired code SEVEN times in one session
506
- // (capability-registry, capability-audit, lesson-gate's five triggers, anticipate.sh,
507
- // advocacy-outcomes, lesson-promote's demotion, continuation-gate's global path). Every one had
508
- // passing tests, because a test imports the module directly — the one caller that proves nothing
509
- // about whether the product uses it. Every one was found by a human running grep, hours later.
510
- //
511
- // Seven repetitions of one mistake is not a discipline problem; discipline is what failed. So it
512
- // becomes a gate, on the ship path, where this repo's gates run 8/8 against prose's 0/6.
513
- runOrDie('wired (no orphan modules)', process.execPath, ['scripts/wired-check.mjs', '--check']);
514
-
515
- // THE NORTH-STAR PROMOTION VECTOR — strict/check-only releases may not average one broken or
516
- // unknown invariant into a pass. The separately authorized stabilization class makes no 95 claim;
517
- // it retains every safety, test, artifact, publication, and post-publication gate below while the
518
- // promotion program remains open. Derive this only from the already-validated sealed receipt, never
519
- // from a free-standing environment toggle.
520
- if (protectedReleaseMode === 'strict') {
521
- runOrDie('release vector (all critical invariants PASS)', process.execPath, ['scripts/release-vector.mjs']);
522
-
523
- // The Top-100 corpus spans naive through expert prompts and grades semantic clauses, citations,
524
- // abstention, and latency. A manual-only benchmark is a report; a strict release-path benchmark
525
- // is a guarantee. The benchmark itself fails closed unless all 100 canonical questions run.
526
- runOrDie('Top-100 source-grounded recall contract', process.execPath, ['scripts/top100-benchmark.mjs', '--no-write']);
527
- } else {
528
- console.log(c.y(' strict >=95 promotion gates: NOT CLAIMED (sealed stabilization; scoreClaimed:false)'));
529
- }
530
-
531
- // A2. Stable Spine restart classifier (ADR-023, red-team finding 18): diff the boot-frozen SHELL
532
- // (hooks.json, hook-shim, MCP server, .mcp.json, skills/, commands/) against the previous release
533
- // tag and SAY OUT LOUD whether this release needs a restart. The classification is computed, never
534
- // remembered — the same shellDiff logic runs client-side in update-apply.mjs at every flip, so the
535
- // user-facing nag stays honest even if this print is ignored. Informational at ship time; the
536
- // releasing human sees exactly which shell files changed.
537
- step('A2', 'Stable Spine — does this release change the boot-frozen shell? (requiresRestart classifier)');
513
+ // Rewritten 2026-09-26 (consolidation/single-source). Before this change, check-only mode ran its
514
+ // OWN separate gate list (npm test, vitest tests/unit, release-vector.mjs, top100-benchmark.mjs,
515
+ // wired-check.mjs, a Stable-Spine restart print, and a live verify-channels.mjs walk) that had
516
+ // drifted from — and duplicated — the actual contract CI enforces in
517
+ // scripts/release-qualification-contract.mjs. Two lists of "what counts as qualified" is exactly how
518
+ // a local PASS stops meaning what CI's PASS means. There is now one contract; this runs it locally,
519
+ // read-only, the same way canonical-qa.yml's `qualify-development` job runs it on every push/PR.
520
+ step('B', 'release qualification — the same gate CI enforces (release-qualification.mjs)');
538
521
  {
539
- const { execFileSync } = await import('node:child_process');
540
- const SHELL = ['plugin/hooks/hooks.json', 'plugin/scripts/hook-shim.mjs', 'plugin/mcp/server.mjs', 'plugin/.mcp.json', 'plugin/skills', 'plugin/commands'];
541
- let prevTag = '';
542
- try { prevTag = execFileSync('git', ['describe', '--tags', '--abbrev=0'], { encoding: 'utf8' }).trim(); } catch { /* no tags yet */ }
543
- if (!prevTag) {
544
- console.log(c.dim(' no previous release tag — classifier has no baseline (first spine release: requiresRestart=true by definition)'));
545
- } else {
546
- let changed = [];
547
- try {
548
- const out = execFileSync('git', ['diff', '--name-only', `${prevTag}..HEAD`, '--', ...SHELL], { encoding: 'utf8' }).trim();
549
- changed = out ? out.split('\n') : [];
550
- } catch { /* diff failure = unknown; say so, never guess green */ changed = ['(diff failed — treat as changed)']; }
551
- if (changed.length) {
552
- console.log(` ${c.y('requiresRestart: TRUE')} — shell changed vs ${prevTag}:`);
553
- for (const f of changed) console.log(` · ${f}`);
554
- console.log(c.dim(' users get ONE honest restart notice (session-start reads active.json.shellChanged); everything else is live.'));
555
- } else {
556
- console.log(` ${c.g('requiresRestart: false')} — no shell change vs ${prevTag}; this release goes fully live with zero restarts.`);
557
- }
522
+ const reportDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-brain-release-qualification-'));
523
+ const reportPath = path.join(reportDir, 'source-qualification.json');
524
+ runOrDie('release qualification (source)', process.execPath,
525
+ ['scripts/release-qualification.mjs', '--suite', 'source', '--report', reportPath]);
558
526
  }
559
- }
560
-
561
- // B. the full brain test suite (the 60/60)
562
- step('B', 'full test suite (npm test)');
563
- runOrDie('npm test', 'npm', ['test']);
564
-
565
- // C. unit gates — narrative-version (tag/entity aware), claims, etc.
566
- step('C', 'unit gates (vitest) — narrative version, claims, guards');
567
- runOrDie('vitest unit', 'npx', ['vitest', 'run', 'tests/unit']);
568
527
  }
569
528
 
570
529
  // D. One remotely durable, staged release transaction (ADR-062 / DDD-0015). GitHub remains a draft
@@ -634,13 +593,6 @@ if (PUBLISH) {
634
593
  step('D', 'remote staged release transaction — SKIPPED (check-only; pass --publish to publish)');
635
594
  }
636
595
 
637
- // Check-only diagnoses the currently public channels. During publication, transaction finalization
638
- // performs this walk once, then creates and verifies the publication receipt before convergence.
639
- if (!PUBLISH) {
640
- step('E', 'verify-channels — the live walk of every user path');
641
- runOrDie('verify-channels', process.execPath, ['scripts/verify-channels.mjs']);
642
- }
643
-
644
596
  if (PUBLISH) {
645
597
  console.log(`\n${c.y(c.b('PUBLISHED, NOT VERIFIED'))}`);
646
598
  } else {
@@ -594,7 +594,7 @@ export function validateRetrievalCanaryReceipt(receipt, { plan, requireAcceptanc
594
594
  }
595
595
 
596
596
  export async function runRetrievalCanaries({ plan, sourceSha, artifactSha256, candidateArchiveSha256,
597
- search, citationResolver, concurrency = 3 } = {}) {
597
+ search, citationResolver, concurrency = 3, searchTimeoutMs = null } = {}) {
598
598
  validateRetrievalCanaryPlan(plan);
599
599
  if (!HEX40.test(String(sourceSha || '')) || !HEX64.test(String(artifactSha256 || ''))
600
600
  || !HEX64.test(String(candidateArchiveSha256 || '')) || sourceSha !== plan.candidate.sourceSha
@@ -609,7 +609,8 @@ export async function runRetrievalCanaries({ plan, sourceSha, artifactSha256, ca
609
609
  for (let index = next++; index < plan.cases.length; index = next++) {
610
610
  const canary = plan.cases[index];
611
611
  try {
612
- const rows = resultRows(await search({ query: canary.query, k: 10 }));
612
+ const rows = resultRows(await search({ query: canary.query, k: 10,
613
+ ...(Number.isFinite(searchTimeoutMs) && searchTimeoutMs > 0 ? { timeoutMs: searchTimeoutMs } : {}) }));
613
614
  const top = rows.slice(0, 10);
614
615
  const rank = top.findIndex((row) => String(row?.repo || '').toLowerCase() === canary.expected.repo
615
616
  && expectedSources(canary.expected).some((source) => row?.path === source.path));
@@ -0,0 +1,26 @@
1
+ // scripts/review-model-defaults.mjs — single source for the model IDs this repo SELECTS to run its
2
+ // own release-review and dual/tri-host deliberation pipelines (single-source contract B7, see
3
+ // scripts/single-source-check.mjs). Every value below is copied verbatim from wherever it used to be
4
+ // hard-coded — no ID has changed, so importing these constants changes no behavior.
5
+ //
6
+ // Two independent generations exist because they were pinned on different dates for different gates;
7
+ // they are NOT the same fact and are kept as two separate exports rather than merged into one.
8
+
9
+ // ── Legacy release-review pair (ADR-072 completion gate + public verification aggregate). ─────────
10
+ // Pinned identically, and independently, in three files before this consolidation:
11
+ // adr-072-completion.mjs (REQUIRED_REVIEWERS), independent-review-receipt.mjs
12
+ // (ALLOWED_INDEPENDENT_REVIEWERS) and public-verification-aggregate.mjs (REQUIRED_REVIEW_MODELS).
13
+ export const CLAUDE_FABLE_5_ID = 'claude-fable-5';
14
+ export const GPT_5_6_SOL_ID = 'gpt-5.6-sol';
15
+ export const LEGACY_REVIEW_MODEL_IDS = Object.freeze([CLAUDE_FABLE_5_ID, GPT_5_6_SOL_ID]);
16
+ export const LEGACY_REVIEWER_IDENTITIES = Object.freeze([
17
+ Object.freeze({ identity: CLAUDE_FABLE_5_ID, model: CLAUDE_FABLE_5_ID, provider: 'firstParty' }),
18
+ Object.freeze({ identity: GPT_5_6_SOL_ID, model: GPT_5_6_SOL_ID, provider: 'openai' }),
19
+ ]);
20
+
21
+ // ── Native subscription-CLI dual/tri-host pair, verified against the live hosts 2026-09-10/13. ─────
22
+ // Pinned identically, and independently, in three files before this consolidation:
23
+ // dual-host-deliberation.mjs (TOP_SUBSCRIPTION_MODELS), oracle/producer-hosts.mjs (PRODUCER_MODELS)
24
+ // and trismart.mjs's --dry-run summary.
25
+ export const DUAL_HOST_MODEL_IDS = Object.freeze({ claude: 'claude-fable-5-1', codex: 'gpt-6-astra' });
26
+ export const TRI_HOST_MODEL_IDS = Object.freeze({ ...DUAL_HOST_MODEL_IDS, grok: 'grok-4.6' });
@@ -39,10 +39,23 @@ export const PRICING = {
39
39
  'deepseek/deepseek-v4-flash': { in: 0.077, out: 0.154 }, // verified 2026-07-12 OpenRouter /models live; successor to deepseek-chat (which resolves to legacy V3)
40
40
  'x-ai/grok-4.5': { in: 2.0, out: 6.0 }, // verified 2026-07-12 OpenRouter /models live; mid-priced frontier-adjacent
41
41
  };
42
+ // Named handles for the Claude tier ids below, so every OTHER file that needs one of these ids
43
+ // imports it by NAME instead of retyping the literal a second time (single-source contract B7 —
44
+ // calibrate-router.mjs, dispatch-receipt.mjs, router-utilization.mjs and metaharness-receipts.mjs
45
+ // all used to hard-code their own copies of these same strings).
46
+ export const CLAUDE_MODEL_IDS = Object.freeze({
47
+ haiku: 'claude-haiku-4.5',
48
+ sonnet: 'claude-sonnet-5',
49
+ opus: 'claude-opus-4.8',
50
+ fable: 'claude-fable-5',
51
+ opus5: 'claude-opus-5',
52
+ opus5Fast: 'claude-opus-5-fast',
53
+ });
54
+
42
55
  // Frontier = the most capable model you'd otherwise reach for. Fable 5 leads the Claude 5 family
43
56
  // (2× Opus 4.8 per token — see CLAUDE_TIERS below), so it is the honest "instead of" baseline: every
44
57
  // $ the cascade saves is measured against what Fable 5 would have cost on the same tokens.
45
- export const FRONTIER = { name: 'claude-fable-5', in: 10.0, out: 50.0 };
58
+ export const FRONTIER = { name: CLAUDE_MODEL_IDS.fable, in: 10.0, out: 50.0 };
46
59
 
47
60
  // Claude tiers — $/Mtok, verified live from the OpenRouter /models API 2026-07-13.
48
61
  // These are NOT routed through here (Claude Code's own Agent/Task tool spawns them). They are priced
@@ -51,10 +64,10 @@ export const FRONTIER = { name: 'claude-fable-5', in: 10.0, out: 50.0 };
51
64
  // whole router looked unused. It WAS unused; it was also unmeasurable. Both had to be fixed.
52
65
  // The spread is the whole argument: fable-5 costs 10x haiku-4.5 for identical mechanical work.
53
66
  export const CLAUDE_TIERS = {
54
- 'claude-haiku-4.5': { in: 1.0, out: 5.0 },
55
- 'claude-sonnet-5': { in: 2.0, out: 10.0 },
56
- 'claude-opus-4.8': { in: 5.0, out: 25.0 },
57
- 'claude-fable-5': { in: 10.0, out: 50.0 },
67
+ [CLAUDE_MODEL_IDS.haiku]: { in: 1.0, out: 5.0 },
68
+ [CLAUDE_MODEL_IDS.sonnet]: { in: 2.0, out: 10.0 },
69
+ [CLAUDE_MODEL_IDS.opus]: { in: 5.0, out: 25.0 },
70
+ [CLAUDE_MODEL_IDS.fable]: { in: 10.0, out: 50.0 },
58
71
  // OPUS 5 ADDED 2026-08-08, and its absence was silently costing every receipt.
59
72
  //
60
73
  // dispatch-receipt refuses to price an unknown model ("refusing to invent savings"), which is the
@@ -67,8 +80,8 @@ export const CLAUDE_TIERS = {
67
80
  // standing rule — never recalled, never inferred from the 4.8 row:
68
81
  // anthropic/claude-opus-5 in $5.00/Mtok out $25.00/Mtok
69
82
  // anthropic/claude-opus-5-fast in $10.00/Mtok out $50.00/Mtok
70
- 'claude-opus-5': { in: 5.0, out: 25.0 },
71
- 'claude-opus-5-fast': { in: 10.0, out: 50.0 },
83
+ [CLAUDE_MODEL_IDS.opus5]: { in: 5.0, out: 25.0 },
84
+ [CLAUDE_MODEL_IDS.opus5Fast]: { in: 10.0, out: 50.0 },
72
85
  };
73
86
 
74
87
  /** Price lookup across both tables. Unknown model → null (never invent a savings number). */
@@ -20,7 +20,7 @@
20
20
  // Usage: node scripts/router-utilization.mjs [--json]
21
21
 
22
22
  import fs from 'node:fs';
23
- import { FRONTIER, priceOf, receiptsPath } from './route-cheap.mjs';
23
+ import { CLAUDE_MODEL_IDS, FRONTIER, priceOf, receiptsPath } from './route-cheap.mjs';
24
24
 
25
25
  // Band = a human-legible grouping over the continuous complexity/cost axis (ADR-149: "tier_label is
26
26
  // metadata, not control flow"). The four bands mirror router-optimizer.mjs. Known models are mapped
@@ -28,15 +28,15 @@ import { FRONTIER, priceOf, receiptsPath } from './route-cheap.mjs';
28
28
  const BAND_BY_MODEL = {
29
29
  'agent-booster': 'mechanical',
30
30
  'inclusionai/ling-2.6-flash': 'cheap',
31
- 'claude-haiku-4.5': 'cheap',
31
+ [CLAUDE_MODEL_IDS.haiku]: 'cheap',
32
32
  'deepseek/deepseek-chat': 'cheap',
33
33
  'deepseek/deepseek-v4-flash': 'cheap',
34
34
  'meta-llama/llama-3.3-70b-instruct': 'mid',
35
35
  'openai/gpt-4.1': 'mid',
36
36
  'x-ai/grok-4.5': 'mid',
37
- 'claude-sonnet-5': 'mid',
38
- 'claude-opus-4.8': 'frontier',
39
- 'claude-fable-5': 'frontier',
37
+ [CLAUDE_MODEL_IDS.sonnet]: 'mid',
38
+ [CLAUDE_MODEL_IDS.opus]: 'frontier',
39
+ [CLAUDE_MODEL_IDS.fable]: 'frontier',
40
40
  };
41
41
  export const BAND_ORDER = ['mechanical', 'cheap', 'mid', 'frontier'];
42
42
  const BAND_LABEL = { mechanical: 'Mechanical', cheap: 'Cheap', mid: 'Mid', frontier: 'Frontier' };
@@ -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: 1, brainVersion: getVersion(), releaseTag: getVersionTag(), stores: {} };
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 = 1;
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(),