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.
- package/README.md +2 -2
- package/bin/install.mjs +32 -9
- package/kb/forge-update.mjs +1884 -0
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +4 -4
- package/plugin/hooks/hook-contracts.json +7 -7
- package/plugin/hooks/hooks.json +2 -2
- package/plugin/scripts/capability-claim-evidence.mjs +11 -2
- package/plugin/scripts/capacity-aware-parallel-work.mjs +71 -15
- package/plugin/scripts/codex-hook-wrapper.mjs +3 -0
- package/plugin/scripts/completion-claim-evidence.mjs +262 -0
- package/plugin/scripts/continuation-gate.mjs +105 -21
- package/plugin/scripts/continuation-objective.mjs +15 -0
- package/plugin/scripts/decision-gate.mjs +22 -2
- package/plugin/scripts/duplicate-gate.mjs +503 -0
- package/plugin/scripts/grounding-turn-evidence.mjs +339 -0
- package/plugin/scripts/grounding-turn-gate.mjs +77 -25
- package/plugin/scripts/grounding-turn-mark.mjs +61 -14
- package/plugin/scripts/hook-input.mjs +15 -0
- package/plugin/scripts/host-update.mjs +45 -0
- package/plugin/scripts/nightly-scheduler.mjs +34 -0
- package/plugin/scripts/session-start-budget.mjs +1 -0
- package/plugin/scripts/session-start-core.mjs +15 -2
- package/plugin/scripts/session-start-health.mjs +101 -1
- package/plugin/scripts/session-start-update-plane.mjs +71 -1
- package/scripts/approved-runtime.mjs +1 -1
- package/scripts/completion-claim-replay.mjs +114 -0
- package/scripts/corpus-canary.mjs +396 -0
- package/scripts/corpus-dispatch-decision.mjs +2 -2
- package/scripts/corpus-promotion.mjs +49 -0
- package/scripts/corpus-reconcile.mjs +27 -3
- package/scripts/corpus-watchdog.mjs +45 -6
- package/scripts/duplicate-gate-replay.mjs +98 -0
- package/scripts/grounding-turn-replay.mjs +131 -0
- package/scripts/nightly-watchdog.mjs +3 -3
- package/scripts/protected-release-invocation.mjs +1 -1
- package/scripts/release.mjs +180 -77
- package/scripts/single-source-check.mjs +12 -6
- package/scripts/wired-check.mjs +2 -0
package/scripts/release.mjs
CHANGED
|
@@ -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
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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 (!
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
|
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
|
|
499
|
-
//
|
|
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
|
|
564
|
+
corpusFailure(`refusing to stage an incomplete corpus release; expected ${expectedAssets.join(', ')} fully uploaded on a draft`);
|
|
528
565
|
}
|
|
529
566
|
|
|
530
|
-
const
|
|
531
|
-
if (
|
|
532
|
-
corpusFailure(`corpus
|
|
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
|
|
537
|
-
//
|
|
538
|
-
|
|
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
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
84
|
-
|
|
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|
|
|
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: '
|
|
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=="
|
|
164
|
-
|
|
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
|
package/scripts/wired-check.mjs
CHANGED
|
@@ -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 '
|