ruvnet-brain 4.3.37 → 4.3.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +32 -9
  3. package/kb/forge-update.mjs +1884 -0
  4. package/package.json +3 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/hooks/codex-hooks.json +4 -4
  8. package/plugin/hooks/hook-contracts.json +7 -7
  9. package/plugin/hooks/hooks.json +2 -2
  10. package/plugin/scripts/capability-claim-evidence.mjs +11 -2
  11. package/plugin/scripts/capacity-aware-parallel-work.mjs +71 -15
  12. package/plugin/scripts/codex-hook-wrapper.mjs +3 -0
  13. package/plugin/scripts/completion-claim-evidence.mjs +262 -0
  14. package/plugin/scripts/continuation-gate.mjs +105 -21
  15. package/plugin/scripts/continuation-objective.mjs +15 -0
  16. package/plugin/scripts/decision-gate.mjs +22 -2
  17. package/plugin/scripts/duplicate-gate.mjs +503 -0
  18. package/plugin/scripts/grounding-turn-evidence.mjs +339 -0
  19. package/plugin/scripts/grounding-turn-gate.mjs +77 -25
  20. package/plugin/scripts/grounding-turn-mark.mjs +61 -14
  21. package/plugin/scripts/hook-input.mjs +15 -0
  22. package/plugin/scripts/host-update.mjs +45 -0
  23. package/plugin/scripts/nightly-scheduler.mjs +34 -0
  24. package/plugin/scripts/session-start-budget.mjs +1 -0
  25. package/plugin/scripts/session-start-core.mjs +15 -2
  26. package/plugin/scripts/session-start-health.mjs +101 -1
  27. package/plugin/scripts/session-start-update-plane.mjs +71 -1
  28. package/scripts/approved-runtime.mjs +1 -1
  29. package/scripts/completion-claim-replay.mjs +114 -0
  30. package/scripts/corpus-canary.mjs +396 -0
  31. package/scripts/corpus-dispatch-decision.mjs +2 -2
  32. package/scripts/corpus-promotion.mjs +49 -0
  33. package/scripts/corpus-reconcile.mjs +27 -3
  34. package/scripts/corpus-watchdog.mjs +45 -6
  35. package/scripts/duplicate-gate-replay.mjs +98 -0
  36. package/scripts/grounding-turn-replay.mjs +131 -0
  37. package/scripts/nightly-watchdog.mjs +3 -3
  38. package/scripts/protected-release-invocation.mjs +1 -1
  39. package/scripts/release.mjs +180 -77
  40. package/scripts/single-source-check.mjs +12 -6
  41. package/scripts/wired-check.mjs +2 -0
@@ -11,7 +11,7 @@
11
11
  // header called check-only "the DEFINITION OF DONE" and claimed it alone decided "shipped"; that was
12
12
  // never true and duplicated a second, drifting gate list alongside CI's real one). Publishing a
13
13
  // release — creating the GitHub Release, moving the npm dist-tag — happens ONLY inside the
14
- // reviewer-protected `protected-release.yml` workflow, against an exact-SHA CI-sealed candidate;
14
+ // branch-protected `protected-release.yml` workflow, against an exact-SHA CI-sealed candidate;
15
15
  // `--publish` mode is that workflow's publisher (it validates the protected-invocation receipt before
16
16
  // doing anything and refuses outside it), never rebuilds or retests source, and treats the immutable
17
17
  // artifact as the evidence boundary. `--check` mode is a read-only LOCAL PREVIEW for a human on a dev
@@ -65,7 +65,9 @@ const recallNotes = (receipt) => {
65
65
  ];
66
66
  };
67
67
  import { verifyBundle } from './verify-bundle.mjs';
68
- import { CORPUS_GENERATION_FIELD, evaluateCorpusPromotion } from './corpus-promotion.mjs';
68
+ import {
69
+ CORPUS_GENERATION_FIELD, CORPUS_TAG_PATTERN, evaluateCanaryVerdict, evaluateCorpusPromotion, parseCorpusGeneration,
70
+ } from './corpus-promotion.mjs';
69
71
  import { bindCoverageToReceipt, writeCoverageAssets } from './corpus-coverage-sidecar.mjs';
70
72
  import { degradedPublication } from './corpus-store-failure.mjs';
71
73
  import { assertNoNewerCorpusGeneration } from './code-release-corpus.mjs';
@@ -139,6 +141,68 @@ const compareCodeTags = (left, right) => {
139
141
  return Math.sign(a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
140
142
  };
141
143
 
144
+ /** The one gh seam both corpus entry points use (RUVNET_GH_COMMAND / RUVNET_GH_SCRIPT for tests). */
145
+ function corpusGh(env, run) {
146
+ const ghCommand = env.RUVNET_GH_COMMAND || 'gh';
147
+ const ghPrefix = env.RUVNET_GH_SCRIPT ? [env.RUVNET_GH_SCRIPT] : [];
148
+ const gh = (args) => run(ghCommand, [...ghPrefix, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
149
+ const ghJson = (args, label) => {
150
+ const result = gh(args);
151
+ if (result.error || result.status !== 0) {
152
+ corpusFailure(`cannot read ${label} (${String(result.error?.message || result.stderr || result.stdout || '').trim() || `gh exited ${result.status}`})`);
153
+ }
154
+ try { return JSON.parse(String(result.stdout || 'null')); }
155
+ catch (error) { corpusFailure(`cannot parse ${label} (${error.message})`); }
156
+ };
157
+ return { gh, ghJson };
158
+ }
159
+
160
+ /**
161
+ * PUBLISH-TIME RE-RESOLVE, before anything is written. Preparation takes hours and the customer canary
162
+ * adds more; a code release may have been published meanwhile. The newest code release is selected
163
+ * exactly as scripts/approved-runtime.mjs selects it (non-draft, non-prerelease vX.Y.Z, highest).
164
+ */
165
+ function assertApprovedRuntimeIsNewest({ ghJson, repo, approvedTag, target }) {
166
+ const listed = ghJson(['release', 'list', '--repo', repo, '--limit', '200', '--json', 'tagName,isDraft,isPrerelease'], 'the code release list');
167
+ const [newest] = (Array.isArray(listed) ? listed : [])
168
+ .filter((row) => !row?.isDraft && !row?.isPrerelease && CODE_TAG.test(String(row?.tagName || '')))
169
+ .map((row) => row.tagName).sort((a, b) => compareCodeTags(b, a));
170
+ if (!newest) corpusFailure(`no published code release is listed on ${repo}; the approved runtime ${approvedTag} cannot be confirmed`);
171
+ const order = compareCodeTags(newest, approvedTag);
172
+ if (order > 0) {
173
+ throw new CorpusSuperseded(`code release ${newest} was published after this corpus was built at ${approvedTag}; `
174
+ + 'promoting it would put an older runtime over the live code release. The next night builds at the newer runtime.');
175
+ }
176
+ if (order < 0) corpusFailure(`approved runtime ${approvedTag} is newer than every published code release (newest ${newest})`);
177
+ const commit = ghJson(['api', `repos/${repo}/commits/${approvedTag}`], `the commit of ${approvedTag}`);
178
+ if (String(commit?.sha || '').toLowerCase() !== target) {
179
+ corpusFailure(`target ${target} is not the source of the approved runtime ${approvedTag} (${commit?.sha || 'unknown'})`);
180
+ }
181
+ }
182
+
183
+ /** What releases/latest is right now, or null when the repository has none. */
184
+ function readCurrentLatest(gh, repo) {
185
+ const latestView = gh(['release', 'view', '--json', 'tagName,body', '--repo', repo]);
186
+ if (!latestView.error && latestView.status === 0) {
187
+ let currentLatest;
188
+ try { currentLatest = JSON.parse(String(latestView.stdout || 'null')); }
189
+ catch (error) { corpusFailure(`cannot read the current latest release (${error.message})`); }
190
+ if (!currentLatest || typeof currentLatest.tagName !== 'string') corpusFailure('current latest release carries no tag name');
191
+ return currentLatest;
192
+ }
193
+ const latestError = String(latestView.error?.message || latestView.stderr || latestView.stdout || '');
194
+ if (!/(release not found|no release found)/i.test(latestError)) {
195
+ corpusFailure(`cannot determine the current latest release (${latestError.trim() || `gh exited ${latestView.status}`})`);
196
+ }
197
+ return null;
198
+ }
199
+
200
+ function latestTagNow(gh, repo) {
201
+ const latestNow = gh(['api', `repos/${repo}/releases/latest`]);
202
+ if (latestNow.error || latestNow.status !== 0) return null;
203
+ try { return JSON.parse(String(latestNow.stdout || 'null'))?.tag_name ?? null; } catch { return null; }
204
+ }
205
+
142
206
  export async function runProtectedCorpusSeed({
143
207
  argv = process.argv.slice(2),
144
208
  env = process.env,
@@ -155,7 +219,15 @@ export async function runProtectedCorpusSeed({
155
219
  // construction; this refuses the combined invocation outright so the two modes can never share one
156
220
  // process even if a future workflow edit put them in the same job.
157
221
  if (argv.includes('--publish')) corpusFailure('--corpus-seed cannot be combined with --publish; corpus routing must never enter product publication');
158
- const promoteLatest = argv.includes('--promote-latest');
222
+ // THE PRODUCER CANNOT DECLARE SUCCESS UNLESS THE CONSUMER ACCEPTED (customer canary). There is no
223
+ // longer any single invocation that both publishes a corpus and moves releases/latest: a customer
224
+ // candidate is STAGED (--stage-candidate: a signed, public, non-latest prerelease), a clean customer
225
+ // install applies it (scripts/corpus-canary.mjs), and only --promote-staged with that verdict moves
226
+ // latest. The old one-shot flag is refused outright so no stale workflow text can bypass the canary.
227
+ if (argv.includes('--promote-latest')) {
228
+ corpusFailure('--promote-latest was removed: stage with --stage-candidate, then promote with --promote-staged and the customer canary verdict');
229
+ }
230
+ const customerCandidate = argv.includes('--stage-candidate');
159
231
 
160
232
  const tag = cliArg(argv, '--corpus-tag');
161
233
  const bundleFile = cliArg(argv, '--corpus-bundle');
@@ -208,7 +280,7 @@ export async function runProtectedCorpusSeed({
208
280
  });
209
281
  if (ancestry.error || ancestry.status !== 0) corpusFailure(`target ${target} is not an ancestor of this run's GITHUB_SHA ${env.GITHUB_SHA}`);
210
282
  const approvedTag = cliArg(argv, '--approved-tag');
211
- if (promoteLatest) {
283
+ if (customerCandidate) {
212
284
  if (!CODE_TAG.test(String(approvedTag || ''))) corpusFailure('customer promotion requires --approved-tag vX.Y.Z (the approved runtime this corpus was built at)');
213
285
  if (receipt.archiveManifestReleaseTag !== approvedTag) {
214
286
  corpusFailure(`the archive ships runtime ${receipt.archiveManifestReleaseTag}, not the approved runtime ${approvedTag}`);
@@ -366,7 +438,7 @@ export async function runProtectedCorpusSeed({
366
438
  const signatureFile = `${bundleFile}.sig`;
367
439
  const digestFile = `${bundleFile}.sha256`;
368
440
  const generation = String(receipt.createdAt || '');
369
- if (promoteLatest) {
441
+ if (customerCandidate) {
370
442
  for (const [label, file] of [['detached signature', signatureFile], ['sha256 sidecar', digestFile]]) {
371
443
  if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
372
444
  corpusFailure(`customer corpus promotion requires a ${label} beside the archive (${path.basename(file)} missing) — the updater fails closed without it`);
@@ -380,40 +452,11 @@ export async function runProtectedCorpusSeed({
380
452
  if (!Number.isFinite(Date.parse(generation))) corpusFailure('corpus receipt createdAt is not a readable generation timestamp');
381
453
  }
382
454
 
383
- const ghCommand = env.RUVNET_GH_COMMAND || 'gh';
384
- const ghPrefix = env.RUVNET_GH_SCRIPT ? [env.RUVNET_GH_SCRIPT] : [];
385
- const gh = (args) => run(ghCommand, [...ghPrefix, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
386
- const ghJson = (args, label) => {
387
- const result = gh(args);
388
- if (result.error || result.status !== 0) {
389
- corpusFailure(`cannot read ${label} (${String(result.error?.message || result.stderr || result.stdout || '').trim() || `gh exited ${result.status}`})`);
390
- }
391
- try { return JSON.parse(String(result.stdout || 'null')); }
392
- catch (error) { corpusFailure(`cannot parse ${label} (${error.message})`); }
393
- };
455
+ const { gh, ghJson } = corpusGh(env, run);
394
456
 
395
- if (promoteLatest) {
396
- // PUBLISH-TIME RE-RESOLVE, before anything is written. Preparation takes hours; a code release
397
- // may have been published meanwhile. The newest code release is selected exactly as
398
- // scripts/approved-runtime.mjs selects it (non-draft, non-prerelease vX.Y.Z, highest version).
399
- // Its signed install aggregate was re-verified for --approved-tag by the workflow step that built
400
- // the runtime pin moments ago; what can change after that is only WHICH release is newest.
401
- const listed = ghJson(['release', 'list', '--repo', repo, '--limit', '200', '--json', 'tagName,isDraft,isPrerelease'], 'the code release list');
402
- const [newest] = (Array.isArray(listed) ? listed : [])
403
- .filter((row) => !row?.isDraft && !row?.isPrerelease && CODE_TAG.test(String(row?.tagName || '')))
404
- .map((row) => row.tagName).sort((a, b) => compareCodeTags(b, a));
405
- if (!newest) corpusFailure(`no published code release is listed on ${repo}; the approved runtime ${approvedTag} cannot be confirmed`);
406
- const order = compareCodeTags(newest, approvedTag);
407
- if (order > 0) {
408
- throw new CorpusSuperseded(`code release ${newest} was published after this corpus was built at ${approvedTag}; `
409
- + 'promoting it would put an older runtime over the live code release. The next night builds at the newer runtime.');
410
- }
411
- if (order < 0) corpusFailure(`approved runtime ${approvedTag} is newer than every published code release (newest ${newest})`);
412
- const commit = ghJson(['api', `repos/${repo}/commits/${approvedTag}`], `the commit of ${approvedTag}`);
413
- if (String(commit?.sha || '').toLowerCase() !== target) {
414
- corpusFailure(`target ${target} is not the source of the approved runtime ${approvedTag} (${commit?.sha || 'unknown'})`);
415
- }
416
- }
457
+ // Its signed install aggregate was re-verified for --approved-tag by the workflow step that built the
458
+ // runtime pin moments ago; what can change after that is only WHICH release is newest.
459
+ if (customerCandidate) assertApprovedRuntimeIsNewest({ ghJson, repo, approvedTag, target });
417
460
 
418
461
  const viewArgs = ['release', 'view', tag, '--json', 'tagName', '--repo', repo];
419
462
  const view = gh(viewArgs);
@@ -423,7 +466,7 @@ export async function runProtectedCorpusSeed({
423
466
 
424
467
  const receiptSha256 = sha256File(receiptFile);
425
468
 
426
- if (!promoteLatest) {
469
+ if (!customerCandidate) {
427
470
  // BOOTSTRAP/RECOVERY seeds stay exactly as ADR-086's original contract left them: an immutable
428
471
  // prerelease that never touches releases/latest. Dual's C4 resolution (S1) narrows the change to
429
472
  // CUSTOMER releases — "Bootstrap-only releases may remain prereleases."
@@ -465,23 +508,16 @@ export async function runProtectedCorpusSeed({
465
508
  // (kb/forge-update.mjs:1275 fetches `${url}.sig`, :1284-1285 exits 4 when verification fails).
466
509
  // scripts/verify-channels.mjs checks exactly these two things (checks 3 and 4) against the live
467
510
  // endpoints, and is the owner's post-publish acceptance gate.
468
- const latestView = gh(['release', 'view', '--json', 'tagName,body', '--repo', repo]);
469
- let currentLatest = null;
470
- if (!latestView.error && latestView.status === 0) {
471
- try { currentLatest = JSON.parse(String(latestView.stdout || 'null')); }
472
- catch (error) { corpusFailure(`cannot read the current latest release (${error.message})`); }
473
- if (!currentLatest || typeof currentLatest.tagName !== 'string') corpusFailure('current latest release carries no tag name');
474
- } else {
475
- const latestError = String(latestView.error?.message || latestView.stderr || latestView.stdout || '');
476
- if (!/(release not found|no release found)/i.test(latestError)) {
477
- corpusFailure(`cannot determine the current latest release (${latestError.trim() || `gh exited ${latestView.status}`})`);
478
- }
479
- }
511
+ //
512
+ // STAGED, NOT PROMOTED (customer canary). This path ends with a signed, public PRERELEASE that
513
+ // releases/latest cannot resolve to. The ordering check still runs here so a candidate that could
514
+ // never be promoted is not staged at all; it runs again at promotion time.
515
+ const currentLatest = readCurrentLatest(gh, repo);
480
516
  const promotion = evaluateCorpusPromotion({ tag, generation, currentLatest });
481
517
  if (!promotion.allowed) corpusFailure(promotion.reason);
482
518
 
483
519
  const notes = [
484
- 'RuvNet Brain corpus generation — signed, content-addressed, and promoted to latest.',
520
+ 'RuvNet Brain corpus generation — signed and content-addressed. Staged as a prerelease; promoted to latest only after a clean customer install applied it.',
485
521
  `${CORPUS_GENERATION_FIELD} ${generation}`,
486
522
  `Archive SHA-256: ${archiveSha256}`,
487
523
  `Receipt SHA-256: ${receiptSha256}`,
@@ -495,8 +531,9 @@ export async function runProtectedCorpusSeed({
495
531
  // ASSETS COMPLETE BEFORE PROMOTION. `gh release create` uploads assets AFTER the release exists, so
496
532
  // creating a non-draft release directly opens a window in which releases/latest resolves to a
497
533
  // release with no archive — every polling client in that window fails or, worse, half-downloads.
498
- // Create as a draft (invisible to releases/latest), prove all four assets landed, and only then
499
- // flip draft off and claim latest in one edit.
534
+ // Create as a draft (invisible to releases/latest), prove every asset landed, and only then flip
535
+ // draft off AS A PRERELEASE — public, so an anonymous customer install can download it exactly as it
536
+ // downloads latest, and never latest, so no customer receives it until the canary has applied it.
500
537
  // Both reports ride with every corpus release for the same reason they ride with a seed: a customer
501
538
  // (or the next night's dispatcher) that downloads the archive must be able to reverify it against
502
539
  // the identity it was actually measured under — the blocking recall gate AND the C3 diagnostic it
@@ -524,43 +561,109 @@ export async function runProtectedCorpusSeed({
524
561
  catch (error) { corpusFailure(`cannot read the draft corpus release (${error.message})`); }
525
562
  const uploaded = (draft?.assets || []).filter((asset) => asset?.state === 'uploaded' && Number.isSafeInteger(asset.size) && asset.size > 0);
526
563
  if (draft?.isDraft !== true || JSON.stringify(uploaded.map((asset) => asset.name).sort()) !== JSON.stringify(expectedAssets)) {
527
- corpusFailure(`refusing to promote an incomplete corpus release; expected ${expectedAssets.join(', ')} fully uploaded on a draft`);
564
+ corpusFailure(`refusing to stage an incomplete corpus release; expected ${expectedAssets.join(', ')} fully uploaded on a draft`);
528
565
  }
529
566
 
530
- const promote = gh(['release', 'edit', tag, '--repo', repo, '--draft=false', '--latest', '--prerelease=false']);
531
- if (promote.error || promote.status !== 0) {
532
- corpusFailure(`corpus promotion to latest failed (${String(promote.error?.message || promote.stderr || promote.stdout || '').trim()})`);
567
+ const stage = gh(['release', 'edit', tag, '--repo', repo, '--draft=false', '--prerelease', '--latest=false']);
568
+ if (stage.error || stage.status !== 0) {
569
+ corpusFailure(`corpus staging failed (${String(stage.error?.message || stage.stderr || stage.stdout || '').trim()})`);
533
570
  }
534
571
 
535
572
  // `isLatest` is NOT a `gh release view` field (gh 2.101.0: "Unknown JSON field"; it exists only on
536
- // `gh release list`), so asking for it made this confirmation fail against the real CLI every time.
537
- // Latest-ness is read from the one authoritative endpoint instead: releases/latest must BE this tag.
538
- // tests/unit/gh-json-fields.test.mjs checks every --json field list against the captured real CLI.
573
+ // `gh release list`), so latest-ness is read from the one authoritative endpoint: releases/latest
574
+ // must NOT be this tag. tests/unit/gh-json-fields.test.mjs checks every --json field list.
575
+ const finalView = gh(['release', 'view', tag, '--json', 'tagName,isDraft,isPrerelease,assets', '--repo', repo]);
576
+ if (finalView.error || finalView.status !== 0) corpusFailure('cannot confirm the staged corpus release');
577
+ let staged;
578
+ try { staged = JSON.parse(String(finalView.stdout || 'null')); }
579
+ catch (error) { corpusFailure(`cannot read the staged corpus release (${error.message})`); }
580
+ const stagedAssets = (staged?.assets || []).map((asset) => asset?.name).sort();
581
+ if (staged?.tagName !== tag || staged.isDraft !== false || staged.isPrerelease !== true
582
+ || latestTagNow(gh, repo) === tag || JSON.stringify(stagedAssets) !== JSON.stringify(expectedAssets)) {
583
+ corpusFailure('corpus release did not reach a complete, public, non-latest prerelease state');
584
+ }
585
+
586
+ return {
587
+ tag, target, repository: repo, archiveSha256, receiptSha256, staged: true, promoted: false,
588
+ generation, currentLatest: currentLatest?.tagName || null,
589
+ };
590
+ }
591
+
592
+ /**
593
+ * PROMOTION — the only code path that moves releases/latest to a corpus generation, and it requires
594
+ * the consumer's consent: a PASS verdict from scripts/corpus-canary.mjs for THIS run, over exactly the
595
+ * asset digests still on the staged prerelease. Everything that could have changed since staging is
596
+ * re-proved: the protected environment, the checkout, the approved runtime still being the newest code
597
+ * release (else CorpusSuperseded), the release still being the staged prerelease, and the ordering
598
+ * against whatever is latest now.
599
+ */
600
+ export async function runProtectedCorpusPromotion({
601
+ argv = process.argv.slice(2),
602
+ env = process.env,
603
+ root = ROOT,
604
+ run = (command, args, options) => spawnSync(command, args, { encoding: 'utf8', ...options }),
605
+ } = {}) {
606
+ const environmentFailures = validateProtectedPublishEnvironment(env);
607
+ if (environmentFailures.length) corpusFailure(environmentFailures.join('; '));
608
+ if (argv.includes('--publish')) corpusFailure('--corpus-seed cannot be combined with --publish; corpus routing must never enter product publication');
609
+ const tag = cliArg(argv, '--corpus-tag');
610
+ const verdictFile = cliArg(argv, '--canary-verdict');
611
+ const target = cliArg(argv, '--target');
612
+ const approvedTag = cliArg(argv, '--approved-tag');
613
+ const repo = cliArg(argv, '--repo') || env.GITHUB_REPOSITORY;
614
+ if (!CORPUS_TAG_PATTERN.test(String(tag || ''))) corpusFailure('corpus tag must be corpus-sha256- followed by 64 lowercase hex characters');
615
+ if (repo !== env.GITHUB_REPOSITORY || repo !== 'stuinfla/ruvnet-brain') corpusFailure('repository does not match the protected workflow');
616
+ if (!CODE_TAG.test(String(approvedTag || ''))) corpusFailure('promotion requires --approved-tag vX.Y.Z (the approved runtime this corpus was built at)');
617
+ const head = String(run('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).stdout || '').trim();
618
+ if (!isHex(target, 40) || target !== head || !isHex(env.GITHUB_SHA, 40)) corpusFailure('target must exactly equal HEAD (and GITHUB_SHA must be a commit)');
619
+ const ancestry = run('git', ['merge-base', '--is-ancestor', target, env.GITHUB_SHA], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
620
+ if (ancestry.error || ancestry.status !== 0) corpusFailure(`target ${target} is not an ancestor of this run's GITHUB_SHA ${env.GITHUB_SHA}`);
621
+ let verdict;
622
+ try {
623
+ if (!verdictFile || !path.isAbsolute(verdictFile)) throw new Error('--canary-verdict must be an absolute file');
624
+ verdict = JSON.parse(fs.readFileSync(verdictFile, 'utf8'));
625
+ } catch (error) {
626
+ corpusFailure(`no customer consent: the canary verdict is unreadable (${error.message})`);
627
+ }
628
+ // Local refusal first: a FAIL verdict never reaches the network at all.
629
+ if (verdict?.verdict !== 'PASS') corpusFailure(`no customer consent: the customer canary reported ${verdict?.verdict || '(no verdict)'}`);
630
+
631
+ const { gh, ghJson } = corpusGh(env, run);
632
+ assertApprovedRuntimeIsNewest({ ghJson, repo, approvedTag, target });
633
+ const release = ghJson(['release', 'view', tag, '--json', 'tagName,isDraft,isPrerelease,assets,body', '--repo', repo], `the staged release ${tag}`);
634
+ if (release?.tagName !== tag || release.isDraft !== false || release.isPrerelease !== true) {
635
+ corpusFailure(`${tag} is not a staged (public, non-draft) prerelease; refusing to promote it`);
636
+ }
637
+ const consent = evaluateCanaryVerdict({ verdict, tag, runId: env.GITHUB_RUN_ID, runAttempt: env.GITHUB_RUN_ATTEMPT,
638
+ approvedTag, releaseAssets: (release.assets || []).map((asset) => ({ name: asset?.name, digest: asset?.digest ?? null })) });
639
+ if (!consent.allowed) corpusFailure(consent.reason);
640
+ const generation = parseCorpusGeneration(release.body)?.value;
641
+ if (!generation) corpusFailure(`${tag} carries no readable "${CORPUS_GENERATION_FIELD}" ordering key`);
642
+ const currentLatest = readCurrentLatest(gh, repo);
643
+ const promotion = evaluateCorpusPromotion({ tag, generation, currentLatest });
644
+ if (!promotion.allowed) corpusFailure(promotion.reason);
645
+
646
+ const promote = gh(['release', 'edit', tag, '--repo', repo, '--prerelease=false', '--latest']);
647
+ if (promote.error || promote.status !== 0) {
648
+ corpusFailure(`corpus promotion to latest failed (${String(promote.error?.message || promote.stderr || promote.stdout || '').trim()})`);
649
+ }
539
650
  const finalView = gh(['release', 'view', tag, '--json', 'tagName,isDraft,isPrerelease,assets', '--repo', repo]);
540
651
  if (finalView.error || finalView.status !== 0) corpusFailure('cannot confirm the promoted corpus release');
541
652
  let promoted;
542
653
  try { promoted = JSON.parse(String(finalView.stdout || 'null')); }
543
654
  catch (error) { corpusFailure(`cannot read the promoted corpus release (${error.message})`); }
544
- const latestNow = gh(['api', `repos/${repo}/releases/latest`]);
545
- let latestTag = null;
546
- if (!latestNow.error && latestNow.status === 0) {
547
- try { latestTag = JSON.parse(String(latestNow.stdout || 'null'))?.tag_name ?? null; } catch { latestTag = null; }
548
- }
549
- const promotedAssets = (promoted?.assets || []).map((asset) => asset?.name).sort();
550
- if (promoted?.tagName !== tag || promoted.isDraft !== false || latestTag !== tag
551
- || promoted.isPrerelease !== false || JSON.stringify(promotedAssets) !== JSON.stringify(expectedAssets)) {
655
+ const names = (assets) => (assets || []).map((asset) => asset?.name).sort();
656
+ if (promoted?.tagName !== tag || promoted.isDraft !== false || promoted.isPrerelease !== false
657
+ || latestTagNow(gh, repo) !== tag || JSON.stringify(names(promoted.assets)) !== JSON.stringify(names(release.assets))) {
552
658
  corpusFailure('corpus release did not reach a complete, non-draft, non-prerelease latest state');
553
659
  }
554
-
555
- return {
556
- tag, target, repository: repo, archiveSha256, receiptSha256, promoted: true,
557
- generation, supersededLatest: currentLatest?.tagName || null,
558
- };
660
+ return { tag, target, repository: repo, promoted: true, generation, consent: consent.reason,
661
+ supersededLatest: currentLatest?.tagName || null };
559
662
  }
560
663
 
561
664
  if (CORPUS_SEED) {
562
665
  try {
563
- const result = await runProtectedCorpusSeed();
666
+ const result = process.argv.includes('--promote-staged') ? await runProtectedCorpusPromotion() : await runProtectedCorpusSeed();
564
667
  console.log(JSON.stringify({ ok: true, mode: 'corpus-seed', ...result }, null, 2));
565
668
  } catch (error) {
566
669
  console.error(error.message);
@@ -577,7 +680,7 @@ if (CORPUS_SEED) {
577
680
  console.log(`\n${c.b('RuvNet Brain — release / definition-of-done')} ${c.dim('· ' + (PUBLISH ? 'PUBLISH' : 'check-only') + ' · shipping ' + V())}\n`);
578
681
 
579
682
  // The local CLI remains useful as a read-only preflight, but publication authority lives only in
580
- // the reviewer-protected workflow. Validate the exact candidate receipt and artifact bytes before
683
+ // the branch-protected workflow. Validate the exact candidate receipt and artifact bytes before
581
684
  // any command capable of pushing, tagging, releasing, or publishing can run.
582
685
  if (PUBLISH) {
583
686
  const protectedInvocation = validateProtectedPublishInvocation({ root: ROOT });
@@ -679,7 +782,7 @@ if (PUBLISH) {
679
782
  }
680
783
  }
681
784
  // ADR-0091 D6.6 — THE BACKWARD-MOVE RACE. Release QE sealed the corpus generation this bundle was
682
- // built from; publication happens later, after owner approval. Clients always accept a code release
785
+ // built from; publication happens later, in the protected workflow. Clients always accept a code release
683
786
  // and drop their corpusGeneration marker when they install one (kb/forge-update.mjs), so publishing
684
787
  // a bundle built from generation G after G+1 already shipped rolls every user back one night.
685
788
  // Re-resolve with the SAME resolver, before any asset upload, and refuse on any difference -- or on
@@ -33,6 +33,8 @@ const HISTORY = /^(docs\/(adr|ddd|research|audits|reviews|qe)\/|CHANGELOG\.md$|P
33
33
  // kb/ is corpus content (primers/cards describing OTHER projects' own commands) and docs/issues/
34
34
  // are upstream bug reports quoting other configs — both are data, not instructions to this project.
35
35
  const instructions = tracked.filter((f) => /\.md$/.test(f) && !HISTORY.test(f) && !/^(kb|docs\/issues)\//.test(f));
36
+ // Phrases that re-introduce a person as a release gate. Negated statements ("no human approval step") are filtered by the caller.
37
+ const HUMAN_APPROVAL_STEP = /(owner|stuart'?s?|maintainer) (approves?|approval|click|must approve)\b[^.]*(deployment|release|publish|gate)|approves? the `?Production|hand (it|the work) over[^.]*click|required[- ]reviewers?\b|standing authori[sz]ation permits/i;
36
38
  const grepIn = (files, re) => files.flatMap((f) => read(f).split('\n')
37
39
  .map((l, i) => (re.test(l) ? `${f}:${i + 1}: ${l.trim().slice(0, 140)}` : null)).filter(Boolean));
38
40
  const none = (hits) => ({ ok: hits.length === 0, detail: hits.slice(0, 8).join('\n') || 'none' });
@@ -80,8 +82,10 @@ const checks = [
80
82
  return none(instructions.flatMap((f) => [...read(f).matchAll(/npm run ([a-z0-9:_-]+)/g)]
81
83
  .filter((m) => !scripts.has(m[1])).map((m) => `${f}: npm run ${m[1]}`)));
82
84
  } },
83
- { 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
+ // The owner is not in the release loop (2026-09-30): the gates are machine gates, and no instruction may
86
+ // route a release through a person clicking in GitHub. B12 applies the same idea to the local CLAUDE.md/AGENTS.md.
87
+ { id: 'B6', area: 'rules', scope: 'repo', title: 'No instruction puts a human approval click (or a required reviewer) in the release path',
88
+ run: () => none(grepIn(instructions, HUMAN_APPROVAL_STEP).filter((l) => !/\bno (human|reviewer)\b|\bnever\b|\bnot asked\b/i.test(l))) },
85
89
  { id: 'B7', area: 'rules', scope: 'repo', title: 'Model IDs are selected only in their owner modules',
86
90
  run: () => {
87
91
  const re = /['"`](claude-(fable|opus|sonnet|haiku)-[0-9][a-z0-9.-]*|gpt-[0-9][a-z0-9.-]*)['"`]/;
@@ -127,7 +131,7 @@ const checks = [
127
131
  { id: 'B12', area: 'rules', scope: 'machine', title: 'The local (untracked) CLAUDE.md / AGENTS.md carry no contradicting instruction',
128
132
  run: () => {
129
133
  const main = path.join(HOME, 'Code/ruvnet-brain');
130
- const bad = /npx (-y )?(@claude-flow|claude-flow|ruflo)\b|standing authori[sz]ation permits|no separate human reviewer click|npm run (build|dev|test:integration|test:coverage|test:security)\b|not direct Agent tool/i;
134
+ const bad = /npx (-y )?(@claude-flow|claude-flow|ruflo)\b|standing authori[sz]ation permits|owner (approves?|click)|Stuart's approval (of|in GitHub)|approves? the `?Production|hand (it|the work) over[^.]*click|npm run (build|dev|test:integration|test:coverage|test:security)\b|not direct Agent tool/i;
131
135
  return none(['CLAUDE.md', 'AGENTS.md'].flatMap((f) => readAbs(path.join(main, f)).split('\n')
132
136
  .map((l, i) => (bad.test(l) ? `${f}:${i + 1}: ${l.trim().slice(0, 120)}` : null)).filter(Boolean)));
133
137
  } },
@@ -157,11 +161,13 @@ const checks = [
157
161
  } },
158
162
  { id: 'C4', area: 'release', scope: 'machine', title: 'The corpus gist job has its authenticated token (RUVNET_GISTS_TOKEN)',
159
163
  run: () => { const r = spawnSync('gh', ['secret', 'list', '-R', 'stuinfla/ruvnet-brain'], { encoding: 'utf8' }); return { ok: /^RUVNET_GISTS_TOKEN\b/m.test(r.stdout), detail: r.stdout.split('\n').map((l) => l.split(/\s/)[0]).filter(Boolean).join(', ') }; } },
160
- { id: 'C3', area: 'release', scope: 'machine', title: 'Publishing requires the owner: Production environment has a required reviewer',
164
+ { id: 'C3', area: 'release', scope: 'machine', title: 'The npm-scoped Production environment is a boundary: branch policy present, admins cannot bypass (no human reviewer is part of the design)',
161
165
  run: () => {
162
166
  const r = spawnSync('gh', ['api', 'repos/stuinfla/ruvnet-brain/environments', '-q',
163
- '.environments[]|select(.name=="Production – ruvnet-brain")|[.protection_rules[]?|select(.type=="required_reviewers")]|length'], { encoding: 'utf8' });
164
- return { ok: r.status === 0 && Number(r.stdout.trim()) > 0, detail: `required_reviewers rules: ${r.stdout.trim() || r.stderr.trim()}` };
167
+ '.environments[]|select(.name=="Production – ruvnet-brain")|[(.can_admins_bypass==false),([.protection_rules[]?|select(.type=="branch_policy")]|length)]|@json'], { encoding: 'utf8' });
168
+ let noBypass = false; let policies = 0;
169
+ try { [noBypass, policies] = JSON.parse(r.stdout.trim()); } catch { /* unreadable: falls through to fail */ }
170
+ return { ok: r.status === 0 && noBypass === true && policies > 0, detail: `can_admins_bypass=false: ${noBypass}; branch_policy rules: ${policies}${r.status === 0 ? '' : ` (${r.stderr.trim()})`}` };
165
171
  } },
166
172
 
167
173
  // D — one corpus / update path
@@ -77,6 +77,8 @@ const STANDALONE = [
77
77
  ['gate', 'retired automatic-hook helper and manual benchmark retained for explicit human use; no workflow or scheduler invokes this expensive command'],
78
78
  ['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
+ ['grounding-turn-replay', 'human-run measurement harness for grounding-turn-gate.mjs (ADR-0030 #1 and shadow #2/#3): replays real transcripts read-only through the same pure functions the hooks call, sharing completion-claim-replay.mjs\'s turn walkers; nothing to schedule'],
81
+ ['duplicate-gate-replay','human-run tuning harness for plugin/scripts/duplicate-gate.mjs: replays the gate over git history, read-only, when its thresholds are re-tuned; it imports the gate\'s own scoring functions, so there is no second copy to drift and nothing to schedule'],
80
82
  ['sync-commands', 'explicit maintainer alias synchronizer; run deliberately before release, never from a lifecycle hook'],
81
83
  ['project-progression-checkpoint', 'the body of the shipped `/ruvnet-brain:checkpoint` command '
82
84
  + '(plugin/commands/checkpoint.md:39; added a7167b6b 2026-09-11). The command host executes that '