ruvnet-brain 4.3.37 → 4.3.38

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 (35) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +32 -9
  3. package/kb/forge-update.mjs +1867 -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/hook-contracts.json +5 -5
  8. package/plugin/scripts/capability-claim-evidence.mjs +11 -2
  9. package/plugin/scripts/capacity-aware-parallel-work.mjs +71 -15
  10. package/plugin/scripts/completion-claim-evidence.mjs +262 -0
  11. package/plugin/scripts/continuation-gate.mjs +105 -21
  12. package/plugin/scripts/continuation-objective.mjs +15 -0
  13. package/plugin/scripts/decision-gate.mjs +22 -2
  14. package/plugin/scripts/duplicate-gate.mjs +503 -0
  15. package/plugin/scripts/grounding-turn-evidence.mjs +339 -0
  16. package/plugin/scripts/grounding-turn-gate.mjs +77 -25
  17. package/plugin/scripts/grounding-turn-mark.mjs +61 -14
  18. package/plugin/scripts/hook-input.mjs +15 -0
  19. package/plugin/scripts/host-update.mjs +45 -0
  20. package/plugin/scripts/nightly-scheduler.mjs +34 -0
  21. package/plugin/scripts/session-start-budget.mjs +1 -0
  22. package/plugin/scripts/session-start-core.mjs +15 -2
  23. package/plugin/scripts/session-start-health.mjs +101 -1
  24. package/plugin/scripts/session-start-update-plane.mjs +71 -1
  25. package/scripts/completion-claim-replay.mjs +114 -0
  26. package/scripts/corpus-canary.mjs +399 -0
  27. package/scripts/corpus-dispatch-decision.mjs +2 -2
  28. package/scripts/corpus-promotion.mjs +49 -0
  29. package/scripts/corpus-reconcile.mjs +27 -3
  30. package/scripts/corpus-watchdog.mjs +45 -6
  31. package/scripts/duplicate-gate-replay.mjs +98 -0
  32. package/scripts/grounding-turn-replay.mjs +131 -0
  33. package/scripts/nightly-watchdog.mjs +3 -3
  34. package/scripts/release.mjs +177 -74
  35. package/scripts/wired-check.mjs +2 -0
@@ -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);
@@ -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 '