ruvnet-brain 4.3.3 → 4.3.9
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 +1 -1
- package/bin/install.mjs +8 -7
- package/data/model-catalog.json +3 -3
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/learn-flush.mjs +1 -1
- package/plugin/skills/release-proof/SKILL.md +19 -10
- package/plugin/skills/release-proof/references/receipt-contract.md +18 -8
- package/scripts/candidate-host-evidence.mjs +9 -2
- package/scripts/execution-policy.mjs +49 -4
- package/scripts/execution-preflight.mjs +18 -0
- package/scripts/host-install-matrix.mjs +247 -40
- package/scripts/integration-evidence.mjs +73 -0
- package/scripts/nightly-wrapper.sh +4 -3
- package/scripts/onboarding-console.mjs +3 -3
- package/scripts/prepublication-evidence.mjs +174 -0
- package/scripts/protected-release-invocation.mjs +32 -4
- package/scripts/public-verification-aggregate.mjs +24 -53
- package/scripts/public-verification-lane.mjs +49 -4
- package/scripts/publication-receipt.mjs +66 -23
- package/scripts/qa-runner.mjs +1 -0
- package/scripts/refresh-model-catalog.mjs +32 -3
- package/scripts/release-authority.mjs +19 -7
- package/scripts/release-convergence-watchdog.mjs +10 -22
- package/scripts/release-publication-handoff.mjs +101 -0
- package/scripts/release-transaction.mjs +1 -1
- package/scripts/release.mjs +14 -6
- package/scripts/source-coverage.mjs +11 -2
- package/scripts/staged-host-verifier.mjs +2 -2
- package/scripts/wired-check.mjs +1 -0
|
@@ -133,7 +133,7 @@ const identityOf = (leaf) => ({
|
|
|
133
133
|
releaseTransactionId: leaf.releaseTransactionId,
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
export function buildPublicVerificationAggregate(leaves
|
|
136
|
+
export function buildPublicVerificationAggregate(leaves) {
|
|
137
137
|
if (!Array.isArray(leaves) || leaves.length !== PUBLIC_VERIFICATION_OS.length * PUBLIC_VERIFICATION_MODES.length) {
|
|
138
138
|
throw new Error('public verification requires exactly nine leaves');
|
|
139
139
|
}
|
|
@@ -168,25 +168,11 @@ export function buildPublicVerificationAggregate(leaves, reviews) {
|
|
|
168
168
|
if (metrics.recallAt10 < 0.98 || metrics.deltaCitationRate !== 1 || metrics.skipped || metrics.unknown) {
|
|
169
169
|
throw new Error('public verification aggregate retrieval acceptance failed');
|
|
170
170
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (byModel.size !== REQUIRED_REVIEW_MODELS.length || REQUIRED_REVIEW_MODELS.some((model) => !byModel.has(model))) {
|
|
177
|
-
throw new Error('required independent review models are missing or duplicated');
|
|
178
|
-
}
|
|
179
|
-
const firstReview = byModel.get(REQUIRED_REVIEW_MODELS[0]);
|
|
180
|
-
const semanticIdentity = validateReviewOracleAgainstPlan(firstReview, first.retrievalPlan);
|
|
181
|
-
for (const model of REQUIRED_REVIEW_MODELS) {
|
|
182
|
-
const review = byModel.get(model);
|
|
183
|
-
const semantic = validateReviewOracleAgainstPlan(review, first.retrievalPlan);
|
|
184
|
-
if (review.sourceSha !== identity.sourceSha || review.artifactSha256 !== identity.artifactSha256
|
|
185
|
-
|| review.payloadId !== identity.payloadId || review.productContractSha256 !== firstReview.productContractSha256
|
|
186
|
-
|| review.rubricSha256 !== firstReview.rubricSha256
|
|
187
|
-
|| semantic.recordSetSha256 !== semanticIdentity.recordSetSha256) {
|
|
188
|
-
throw new Error(`${model} review identity, rubric, or oracle identity differs`);
|
|
189
|
-
}
|
|
171
|
+
const oracle = first.retrievalPlan.oracle;
|
|
172
|
+
if (!oracle || !HEX64.test(String(oracle.receiptSha256 || ''))
|
|
173
|
+
|| !HEX64.test(String(oracle.queryStoreSetSha256 || ''))
|
|
174
|
+
|| oracle.queryStoreSetSha256 !== digest(first.retrievalPlan.denominator.eligibleStores)) {
|
|
175
|
+
throw new Error('sealed retrieval oracle identity is incomplete');
|
|
190
176
|
}
|
|
191
177
|
return {
|
|
192
178
|
schemaVersion: 1,
|
|
@@ -194,17 +180,11 @@ export function buildPublicVerificationAggregate(leaves, reviews) {
|
|
|
194
180
|
identity,
|
|
195
181
|
coverage: first.coverage,
|
|
196
182
|
lanes: required.map((lane) => ({ lane, leafSha256: byLane.get(lane).leafSha256 })),
|
|
197
|
-
|
|
198
|
-
score: byModel.get(model).score })),
|
|
199
|
-
evidence: { leaves: required.map((lane) => structuredClone(byLane.get(lane))),
|
|
200
|
-
reviews: REQUIRED_REVIEW_MODELS.map((model) => structuredClone(byModel.get(model))) },
|
|
201
|
-
productContractSha256: firstReview.productContractSha256,
|
|
202
|
-
rubricSha256: firstReview.rubricSha256,
|
|
183
|
+
evidence: { leaves: required.map((lane) => structuredClone(byLane.get(lane))) },
|
|
203
184
|
retrievalOracle: {
|
|
204
|
-
oracleReceiptSha256:
|
|
205
|
-
queryStoreSetSha256:
|
|
206
|
-
recordCount:
|
|
207
|
-
recordSetSha256: semanticIdentity.recordSetSha256,
|
|
185
|
+
oracleReceiptSha256: oracle.receiptSha256,
|
|
186
|
+
queryStoreSetSha256: oracle.queryStoreSetSha256,
|
|
187
|
+
recordCount: first.retrievalPlan.denominator.eligibleStores.length,
|
|
208
188
|
},
|
|
209
189
|
metrics,
|
|
210
190
|
verdict: 'PASS',
|
|
@@ -212,8 +192,8 @@ export function buildPublicVerificationAggregate(leaves, reviews) {
|
|
|
212
192
|
};
|
|
213
193
|
}
|
|
214
194
|
|
|
215
|
-
export function signPublicVerificationAggregate({ leaves
|
|
216
|
-
const payload = buildPublicVerificationAggregate(leaves
|
|
195
|
+
export function signPublicVerificationAggregate({ leaves }, privateKey) {
|
|
196
|
+
const payload = buildPublicVerificationAggregate(leaves);
|
|
217
197
|
const aggregateSha256 = digest(payload);
|
|
218
198
|
const signed = { ...payload, aggregateSha256 };
|
|
219
199
|
return { ...signed, signature: crypto.sign(null, Buffer.from(canonicalJson(signed)), privateKey).toString('base64') };
|
|
@@ -227,10 +207,10 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
|
|
|
227
207
|
}
|
|
228
208
|
const payload = aggregatePayload(aggregate);
|
|
229
209
|
if (digest(payload) !== aggregate.aggregateSha256) throw new Error('public verification aggregate digest mismatch');
|
|
230
|
-
if (!Array.isArray(aggregate.evidence?.leaves)
|
|
231
|
-
throw new Error('public verification aggregate lacks raw leaf
|
|
210
|
+
if (!Array.isArray(aggregate.evidence?.leaves)) {
|
|
211
|
+
throw new Error('public verification aggregate lacks raw leaf evidence');
|
|
232
212
|
}
|
|
233
|
-
const rebuilt = buildPublicVerificationAggregate(aggregate.evidence.leaves
|
|
213
|
+
const rebuilt = buildPublicVerificationAggregate(aggregate.evidence.leaves);
|
|
234
214
|
if (canonicalJson(rebuilt) !== canonicalJson(payload)) {
|
|
235
215
|
throw new Error('public verification aggregate differs from rebuilt raw evidence');
|
|
236
216
|
}
|
|
@@ -249,17 +229,10 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
|
|
|
249
229
|
|| aggregate.coverage.gistCurrent !== aggregate.coverage.gistTotal) {
|
|
250
230
|
throw new Error('public verification aggregate evidence is incomplete');
|
|
251
231
|
}
|
|
252
|
-
if (aggregate.reviews?.length !== REQUIRED_REVIEW_MODELS.length
|
|
253
|
-
|| REQUIRED_REVIEW_MODELS.some((model) => !aggregate.reviews.some((row) => row.model === model
|
|
254
|
-
&& Number.isInteger(row.score) && row.score >= 95 && HEX64.test(String(row.receiptSha256 || ''))))
|
|
255
|
-
|| !HEX64.test(String(aggregate.productContractSha256 || '')) || !HEX64.test(String(aggregate.rubricSha256 || ''))) {
|
|
256
|
-
throw new Error('public verification aggregate reviews are incomplete');
|
|
257
|
-
}
|
|
258
232
|
if (aggregate.retrievalOracle?.oracleReceiptSha256 !== aggregate.evidence.leaves[0].retrievalPlan.oracle.receiptSha256
|
|
259
233
|
|| aggregate.retrievalOracle?.queryStoreSetSha256 !== aggregate.evidence.leaves[0].retrievalPlan.oracle.queryStoreSetSha256
|
|
260
|
-
|| aggregate.retrievalOracle?.recordCount !== aggregate.evidence.leaves[0].retrievalPlan.denominator.eligibleStores.length
|
|
261
|
-
|
|
262
|
-
throw new Error('public verification aggregate retrieval oracle review is incomplete');
|
|
234
|
+
|| aggregate.retrievalOracle?.recordCount !== aggregate.evidence.leaves[0].retrievalPlan.denominator.eligibleStores.length) {
|
|
235
|
+
throw new Error('public verification aggregate retrieval oracle identity is incomplete');
|
|
263
236
|
}
|
|
264
237
|
if (expectedIdentity && canonicalJson(aggregate.identity) !== canonicalJson(expectedIdentity)) {
|
|
265
238
|
throw new Error('public verification aggregate identity differs from the release transaction');
|
|
@@ -272,13 +245,13 @@ function parseCliArgs(argv) {
|
|
|
272
245
|
for (let index = 0; index < argv.length; index += 2) {
|
|
273
246
|
const flag = argv[index];
|
|
274
247
|
const value = argv[index + 1];
|
|
275
|
-
if (!['--lanes', '--
|
|
276
|
-
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --
|
|
248
|
+
if (!['--lanes', '--out'].includes(flag) || !value) {
|
|
249
|
+
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file>');
|
|
277
250
|
}
|
|
278
251
|
parsed[flag.slice(2)] = value;
|
|
279
252
|
}
|
|
280
|
-
if (!parsed.lanes || !parsed.
|
|
281
|
-
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --
|
|
253
|
+
if (!parsed.lanes || !parsed.out || Object.keys(parsed).length !== 2) {
|
|
254
|
+
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file>');
|
|
282
255
|
}
|
|
283
256
|
return parsed;
|
|
284
257
|
}
|
|
@@ -314,12 +287,11 @@ function readLaneLeaves(directory) {
|
|
|
314
287
|
return PUBLIC_VERIFICATION_OS.flatMap((osName) => byOs.get(osName));
|
|
315
288
|
}
|
|
316
289
|
|
|
317
|
-
export function generatePublicVerificationAggregate({ lanesDirectory,
|
|
290
|
+
export function generatePublicVerificationAggregate({ lanesDirectory, outputFile, privateKey }) {
|
|
318
291
|
if (!privateKey) throw new Error('RUVNET_SIGNING_KEY is required');
|
|
319
292
|
if (fs.existsSync(outputFile)) throw new Error(`refusing to overwrite existing aggregate: ${outputFile}`);
|
|
320
293
|
const leaves = readLaneLeaves(lanesDirectory);
|
|
321
|
-
const
|
|
322
|
-
const aggregate = signPublicVerificationAggregate({ leaves, reviews }, privateKey);
|
|
294
|
+
const aggregate = signPublicVerificationAggregate({ leaves }, privateKey);
|
|
323
295
|
fs.writeFileSync(outputFile, `${JSON.stringify(aggregate, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
324
296
|
return aggregate;
|
|
325
297
|
}
|
|
@@ -328,12 +300,11 @@ async function main() {
|
|
|
328
300
|
const options = parseCliArgs(process.argv.slice(2));
|
|
329
301
|
const aggregate = generatePublicVerificationAggregate({
|
|
330
302
|
lanesDirectory: options.lanes,
|
|
331
|
-
reviewsDirectory: options.reviews,
|
|
332
303
|
outputFile: options.out,
|
|
333
304
|
privateKey: process.env.RUVNET_SIGNING_KEY,
|
|
334
305
|
});
|
|
335
306
|
process.stdout.write(`${JSON.stringify({ ok: true, aggregateSha256: aggregate.aggregateSha256,
|
|
336
|
-
leaves: aggregate.metrics.leaves
|
|
307
|
+
leaves: aggregate.metrics.leaves })}\n`);
|
|
337
308
|
}
|
|
338
309
|
|
|
339
310
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
@@ -24,6 +24,32 @@ const HEX40 = /^[a-f0-9]{40}$/;
|
|
|
24
24
|
const HEX64 = /^[a-f0-9]{64}$/;
|
|
25
25
|
const normalizedDigest = (value) => String(value || '').replace(/^sha256:/, '');
|
|
26
26
|
|
|
27
|
+
function failedCanaryCases(retrieval) {
|
|
28
|
+
return retrieval.cases.filter(({ cohort, status, retrievalHit, citationResolved }) =>
|
|
29
|
+
status !== 'COMPLETED' || retrievalHit !== true || (cohort === 'delta' && citationResolved !== true));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function publicVerificationFailure({ os, identity, failures, completedLeaves }) {
|
|
33
|
+
const payload = {
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
kind: 'ruvnet-brain-public-verification-failure',
|
|
36
|
+
os,
|
|
37
|
+
sourceSha: identity.candidateSha,
|
|
38
|
+
artifactSha256: identity.packageSha256,
|
|
39
|
+
bundleSha256: identity.bundleSha256,
|
|
40
|
+
failures: failures.map(({ mode, reason, retrieval }) => ({
|
|
41
|
+
mode,
|
|
42
|
+
reason,
|
|
43
|
+
metrics: retrieval.metrics,
|
|
44
|
+
failedCaseIds: failedCanaryCases(retrieval).map(({ id }) => id),
|
|
45
|
+
failedCases: failedCanaryCases(retrieval),
|
|
46
|
+
retrievalReceiptSha256: retrieval.receiptSha256,
|
|
47
|
+
})),
|
|
48
|
+
completedLeaves: completedLeaves.map(({ mode, leafSha256 }) => ({ mode, leafSha256 })),
|
|
49
|
+
};
|
|
50
|
+
return { ...payload, failureSha256: digest(payload) };
|
|
51
|
+
}
|
|
52
|
+
|
|
27
53
|
function validateReleaseIdentity(identity) {
|
|
28
54
|
if (identity?.repository !== 'stuinfla/ruvnet-brain' || identity?.package !== 'ruvnet-brain'
|
|
29
55
|
|| !HEX40.test(String(identity.candidateSha || '')) || !HEX64.test(String(identity.payloadId || ''))
|
|
@@ -118,6 +144,7 @@ export async function createPublicVerificationLane({
|
|
|
118
144
|
releaseTransactionId: transactionIdFor(identity),
|
|
119
145
|
};
|
|
120
146
|
const leaves = [];
|
|
147
|
+
const failures = [];
|
|
121
148
|
for (const mode of PUBLIC_VERIFICATION_MODES) {
|
|
122
149
|
const installed = publication.installed?.[RECEIPT_MODE_NAMES[mode]];
|
|
123
150
|
if (installed?.status !== 'PASS' || installed.doctorExit !== 0 || installed.version !== identity.version
|
|
@@ -132,10 +159,16 @@ export async function createPublicVerificationLane({
|
|
|
132
159
|
search: ({ query, k }) => adapter.searchInstalled({ mode, query, k }),
|
|
133
160
|
citationResolver: (matched, expected) => adapter.resolveInstalledCitation({ mode, matched, expected }),
|
|
134
161
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
162
|
+
let verifiedRetrieval;
|
|
163
|
+
try {
|
|
164
|
+
verifiedRetrieval = validateRetrievalCanaryReceipt(
|
|
165
|
+
retrieval,
|
|
166
|
+
{ plan: retrievalPlan, requireAcceptance: true },
|
|
167
|
+
);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
failures.push({ mode, reason: error.message, retrieval });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
139
172
|
leaves.push(createPublicVerificationLeaf({
|
|
140
173
|
...common,
|
|
141
174
|
os,
|
|
@@ -152,6 +185,11 @@ export async function createPublicVerificationLane({
|
|
|
152
185
|
unknown: 0,
|
|
153
186
|
}));
|
|
154
187
|
}
|
|
188
|
+
if (failures.length) {
|
|
189
|
+
const error = new Error(`retrieval canary acceptance failed for ${failures.map(({ mode }) => mode).join(', ')}`);
|
|
190
|
+
error.publicVerificationFailure = publicVerificationFailure({ os, identity, failures, completedLeaves: leaves });
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
155
193
|
if (canonicalJson(leaves.map(({ mode }) => mode)) !== canonicalJson(PUBLIC_VERIFICATION_MODES)) {
|
|
156
194
|
throw new Error('public verification lane modes are incomplete');
|
|
157
195
|
}
|
|
@@ -211,6 +249,13 @@ export async function generatePublicVerificationLane({
|
|
|
211
249
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
212
250
|
fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
213
251
|
return receipt;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error.publicVerificationFailure && !fs.existsSync(output)) {
|
|
254
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
255
|
+
fs.writeFileSync(output, `${JSON.stringify(error.publicVerificationFailure, null, 2)}\n`,
|
|
256
|
+
{ flag: 'wx', mode: 0o600 });
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
214
259
|
} finally {
|
|
215
260
|
await adapter.dispose?.();
|
|
216
261
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
@@ -29,30 +29,62 @@ const DEADLINE_MS = 30_000;
|
|
|
29
29
|
const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
30
30
|
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
31
31
|
const receiptDigest = (candidate) => String(candidate?.artifact?.sha256 || '').replace(/^sha256:/, '');
|
|
32
|
-
|
|
32
|
+
export function commandInvocation(name, args, { platform = process.platform, env = process.env } = {}) {
|
|
33
|
+
if (platform === 'win32' && /^(?:npm|npx)$/i.test(name)) return {
|
|
34
|
+
executable: env.ComSpec || 'cmd.exe', args: ['/d', '/c', `${name}.cmd`, ...args],
|
|
35
|
+
};
|
|
36
|
+
return { executable: name, args };
|
|
37
|
+
}
|
|
33
38
|
function command(name, args, options = {}) {
|
|
34
|
-
const
|
|
39
|
+
const { platform = process.platform, ...spawnOptions } = options;
|
|
40
|
+
const invocation = commandInvocation(name, args, { platform, env: spawnOptions.env || process.env });
|
|
41
|
+
const result = spawnSync(invocation.executable, invocation.args, { encoding: 'utf8', ...spawnOptions });
|
|
35
42
|
if (result.error || result.status !== 0) {
|
|
36
43
|
const detail = String(result.stderr || result.stdout || result.error?.message || '').trim();
|
|
37
44
|
throw new Error(`${name} ${args.join(' ')} failed: ${detail || `exit ${result.status}`}`);
|
|
38
45
|
}
|
|
39
46
|
return String(result.stdout || '').trim();
|
|
40
47
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
try {
|
|
48
|
+
function locate(name, { platform = process.platform } = {}) {
|
|
49
|
+
const query = platform === 'win32' ? (name === 'node' ? 'node.exe' : `${name}.cmd`) : name;
|
|
50
|
+
try {
|
|
51
|
+
return command(platform === 'win32' ? 'where.exe' : 'which', [query], { platform })
|
|
52
|
+
.split(/\r?\n/).find(Boolean) || null;
|
|
53
|
+
} catch { return null; }
|
|
44
54
|
}
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
export function createIsolatedPath(mode, temp, { platform = process.platform, env = process.env,
|
|
56
|
+
resolve = (name) => locate(name, { platform }) } = {}) {
|
|
47
57
|
const bin = path.join(temp, `bin-${mode}`);
|
|
48
58
|
fs.mkdirSync(bin);
|
|
49
59
|
const hosts = mode === 'claudeOnly' ? ['claude'] : mode === 'codexOnly' ? ['codex'] : ['claude', 'codex'];
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
const names = platform === 'win32' ? ['npm', ...hosts] : ['node', 'npm', ...hosts];
|
|
61
|
+
for (const name of names) {
|
|
62
|
+
const target = resolve(name);
|
|
52
63
|
if (!target) throw new Error(`${name} CLI unavailable for ${mode} public host fixture`);
|
|
53
|
-
|
|
64
|
+
if (platform === 'win32') {
|
|
65
|
+
fs.writeFileSync(path.join(bin, `${name}.cmd`), `@call "${target}" %*\r\n`, { flag: 'wx' });
|
|
66
|
+
} else {
|
|
67
|
+
fs.symlinkSync(target, path.join(bin, name));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (platform !== 'win32') return `${bin}:/usr/bin:/bin`;
|
|
71
|
+
const node = resolve('node');
|
|
72
|
+
if (!node) throw new Error(`node CLI unavailable for ${mode} public host fixture`);
|
|
73
|
+
return [bin, path.dirname(node), path.join(env.SystemRoot || 'C:\\Windows', 'System32'),
|
|
74
|
+
path.join(env.ProgramFiles || 'C:\\Program Files', 'Git', 'usr', 'bin')].join(path.win32.delimiter);
|
|
75
|
+
}
|
|
76
|
+
export function stageVerifiedBundle({ bundlePath, bundleSha256, packageRoot }) {
|
|
77
|
+
const source = path.resolve(bundlePath || '');
|
|
78
|
+
let stat;
|
|
79
|
+
try { stat = fs.lstatSync(source); } catch { throw new Error('verified public bundle is missing'); }
|
|
80
|
+
if (!stat.isFile() || stat.isSymbolicLink() || sha256(source) !== bundleSha256) {
|
|
81
|
+
throw new Error('verified bundle digest mismatch before installation');
|
|
54
82
|
}
|
|
55
|
-
|
|
83
|
+
const destination = path.join(packageRoot, 'dist', 'ruvnet-brain.zip');
|
|
84
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
85
|
+
fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
|
|
86
|
+
if (sha256(destination) !== bundleSha256) throw new Error('staged verified bundle digest mismatch');
|
|
87
|
+
return destination;
|
|
56
88
|
}
|
|
57
89
|
|
|
58
90
|
async function download(url, destination, headers = {}) {
|
|
@@ -134,7 +166,8 @@ export function assertInstalledPayload(sourceRoot, installedRoot) {
|
|
|
134
166
|
return checked;
|
|
135
167
|
}
|
|
136
168
|
|
|
137
|
-
function rpcSearch(server, env, query, timeoutMs = DEADLINE_MS) {
|
|
169
|
+
export function rpcSearch(server, env, query, k = 5, timeoutMs = DEADLINE_MS) {
|
|
170
|
+
if (!Number.isSafeInteger(k) || k < 1 || k > 50) throw new Error(`invalid search result count: ${k}`);
|
|
138
171
|
return new Promise((resolve, reject) => {
|
|
139
172
|
const child = spawn(process.execPath, [server], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
140
173
|
let stdout = '';
|
|
@@ -175,7 +208,7 @@ function rpcSearch(server, env, query, timeoutMs = DEADLINE_MS) {
|
|
|
175
208
|
const listed = await call(2, 'tools/list');
|
|
176
209
|
if (!listed.result?.tools?.some((tool) => tool.name === 'search_ruvnet')) throw new Error('installed MCP does not advertise search_ruvnet');
|
|
177
210
|
const started = performance.now();
|
|
178
|
-
const searched = await call(3, 'tools/call', { name: 'search_ruvnet', arguments: { query, k
|
|
211
|
+
const searched = await call(3, 'tools/call', { name: 'search_ruvnet', arguments: { query, k } });
|
|
179
212
|
const broadMs = Math.round(performance.now() - started);
|
|
180
213
|
const text = (searched.result?.content || []).map((item) => item.text || '').join('\n');
|
|
181
214
|
if (searched.error || searched.result?.isError || /search_ruvnet error:/i.test(text)) throw new Error(`installed Brain search failed: ${text.slice(0, 400)}`);
|
|
@@ -213,14 +246,17 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
|
|
|
213
246
|
return { path: destination, tag: release.tag_name, sha: tagCommit(root, tag) };
|
|
214
247
|
},
|
|
215
248
|
|
|
216
|
-
async installHosts({ artifactPath, artifactSha256, version }) {
|
|
249
|
+
async installHosts({ artifactPath, artifactSha256, bundlePath, bundleSha256, version }) {
|
|
217
250
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-install-'));
|
|
218
251
|
installTemp = temp;
|
|
219
252
|
const packageRoot = path.join(temp, 'package');
|
|
220
253
|
command('tar', ['-xzf', artifactPath, '-C', temp]);
|
|
254
|
+
stageVerifiedBundle({ bundlePath, bundleSha256, packageRoot });
|
|
221
255
|
const sealedPlugin = path.join(packageRoot, 'plugin');
|
|
222
256
|
const results = {};
|
|
223
257
|
let bundle = null;
|
|
258
|
+
const sharedModelCache = path.join(temp, 'models-cache');
|
|
259
|
+
fs.mkdirSync(sharedModelCache, { recursive: true });
|
|
224
260
|
// Derived from HOST_MODES, so a fourth host shape is added in ONE place and this loop
|
|
225
261
|
// cannot fall behind the staged-side check the way it did.
|
|
226
262
|
for (const mode of HOST_MODES.map((m) => RECEIPT_MODE_NAMES[m])) {
|
|
@@ -233,26 +269,27 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
|
|
|
233
269
|
const env = {
|
|
234
270
|
...process.env,
|
|
235
271
|
HOME: home,
|
|
272
|
+
...(process.platform === 'win32' ? { USERPROFILE: home } : {}),
|
|
236
273
|
CODEX_HOME: codexHome,
|
|
237
274
|
RUVNET_BRAIN_HOME: brainHome,
|
|
238
275
|
RUVNET_BRAIN_KB: kb,
|
|
276
|
+
KB_MODEL_CACHE: sharedModelCache,
|
|
239
277
|
// Env comes from the ONE variant table (host-install-matrix VARIANTS.published), not a
|
|
240
278
|
// second hand-written copy — this file's own header promises "ONE doctor rule and ONE mode
|
|
241
279
|
// vocabulary, shared with the staged-side check", and the copy had already drifted: it
|
|
242
280
|
// omitted the Codex hook-trust bypass the staged side carries, which failed the seal on
|
|
243
281
|
// every release.
|
|
244
282
|
...VARIANTS.published.env({ packageRoot }),
|
|
283
|
+
RUVNET_STRICT_INSTALL: '0',
|
|
245
284
|
CI: 'true',
|
|
246
|
-
PATH:
|
|
285
|
+
PATH: createIsolatedPath(mode, temp),
|
|
247
286
|
};
|
|
248
287
|
const installer = path.join(packageRoot, 'bin', 'install.mjs');
|
|
249
288
|
command(process.execPath, [
|
|
250
289
|
installer, '--yes', '--force', '--version', `v${version}`,
|
|
251
290
|
'--no-nightly-prompt', '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline',
|
|
291
|
+
'--no-selfcheck', '--no-verify',
|
|
252
292
|
], { env, cwd: packageRoot, timeout: 1_200_000, maxBuffer: 32 * 1024 * 1024 });
|
|
253
|
-
command(process.execPath, [installer, '--doctor', '--hooks'], {
|
|
254
|
-
env, cwd: packageRoot, timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
|
|
255
|
-
});
|
|
256
293
|
|
|
257
294
|
const verified = {};
|
|
258
295
|
if (mode !== 'codexOnly') {
|
|
@@ -270,7 +307,11 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
|
|
|
270
307
|
throw new Error(`${mode} installed public Brain bundle version mismatch`);
|
|
271
308
|
}
|
|
272
309
|
bundle ||= { brainVersion: source.brainVersion, releaseTag: source.releaseTag };
|
|
273
|
-
const searched = await rpcSearch(findMcpServer(home), env,
|
|
310
|
+
const searched = await rpcSearch(findMcpServer(home), env,
|
|
311
|
+
'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS);
|
|
312
|
+
command(process.execPath, [installer, '--doctor', '--hooks'], {
|
|
313
|
+
env, cwd: packageRoot, timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
|
|
314
|
+
});
|
|
274
315
|
results[mode] = {
|
|
275
316
|
status: 'PASS', doctorExit: 0, version, artifactSha256,
|
|
276
317
|
functionalSearch: true, searchMs: searched.broadMs, hostsOnPath: mode,
|
|
@@ -291,16 +332,17 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
|
|
|
291
332
|
if (rvfs.length === 0) throw new Error('installed public Brain has no ruvnet-brain self RVF');
|
|
292
333
|
const server = path.join(installContext.home, '.claude', 'ruvnet-brain', 'mcp', 'server.mjs');
|
|
293
334
|
if (!fs.existsSync(server)) throw new Error('installed persistent MCP server is missing');
|
|
294
|
-
const result = await rpcSearch(server, installContext.env,
|
|
335
|
+
const result = await rpcSearch(server, installContext.env,
|
|
336
|
+
'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS);
|
|
295
337
|
const readiness = readJson(path.join(installContext.brainHome, 'mcp-readiness.json'));
|
|
296
338
|
if (readiness.state !== 'ready') throw new Error(`installed Brain readiness is ${readiness.state || 'missing'}`);
|
|
297
339
|
return { status: 'PASS', selfStore: true, broadMs: result.broadMs, deadlineMs: DEADLINE_MS };
|
|
298
340
|
},
|
|
299
341
|
|
|
300
|
-
async searchInstalled({ mode, query }) {
|
|
342
|
+
async searchInstalled({ mode, query, k }) {
|
|
301
343
|
const context = installContexts.get(mode);
|
|
302
344
|
if (!context) throw new Error(`${mode} public host is not installed`);
|
|
303
|
-
const result = await rpcSearch(findMcpServer(context.home), context.env, query, DEADLINE_MS);
|
|
345
|
+
const result = await rpcSearch(findMcpServer(context.home), context.env, query, k, DEADLINE_MS);
|
|
304
346
|
return parseCitations(result.text).map((citation) => ({
|
|
305
347
|
repo: citation.repo.toLowerCase(),
|
|
306
348
|
path: citation.docPath,
|
|
@@ -400,7 +442,8 @@ export async function generatePublicationReceipt({
|
|
|
400
442
|
if (npm.version !== candidate.version || (npm.sha && npm.sha !== candidate.sha)) throw new Error('npm public identity mismatch');
|
|
401
443
|
if (github.tag !== candidate.tag || github.sha !== candidate.sha) throw new Error('GitHub public identity mismatch');
|
|
402
444
|
|
|
403
|
-
const installed = await adapter.installHosts({ artifactPath: npm.path, artifactSha256: digest,
|
|
445
|
+
const installed = await adapter.installHosts({ artifactPath: npm.path, artifactSha256: digest,
|
|
446
|
+
bundlePath: githubBundle.path, bundleSha256: githubBundleDigest, version: candidate.version });
|
|
404
447
|
const brain = await adapter.probeBrain({ sha: candidate.sha, artifactSha256: digest, version: candidate.version });
|
|
405
448
|
const surface = await adapter.probePublishedSurface({ sha: candidate.sha, artifactSha256: digest, version: candidate.version });
|
|
406
449
|
const publication = {
|
package/scripts/qa-runner.mjs
CHANGED
|
@@ -33,6 +33,7 @@ const lanes = [
|
|
|
33
33
|
'tests/unit/codex-claude-hook-parity.test.mjs',
|
|
34
34
|
'tests/unit/brain-stamp-resolve-built-from-sha.test.mjs',
|
|
35
35
|
'tests/unit/qa-runner-concurrency.test.mjs',
|
|
36
|
+
'tests/unit/execution-preflight.test.mjs',
|
|
36
37
|
'--reporter=dot']],
|
|
37
38
|
['mesh', 'npm', ['run', 'test:mesh']],
|
|
38
39
|
['plugin', 'npm', ['test']],
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// scripts/refresh-model-catalog.mjs — pulls the LIVE OpenRouter model catalog and writes the committed
|
|
3
|
-
// snapshot data/openrouter-catalog-snapshot.json (with a pulledAt stamp)
|
|
3
|
+
// snapshot data/openrouter-catalog-snapshot.json (with a pulledAt stamp) and synchronizes the catalog's
|
|
4
|
+
// factual prices. verify-model-catalog.mjs then
|
|
4
5
|
// enforces, offline in CI, that every model in data/model-catalog.json exists and is priced correctly
|
|
5
|
-
// against this snapshot — and that the snapshot is fresh. Run
|
|
6
|
+
// against this snapshot — and that the snapshot is fresh. Run weekly (the anti-rot mechanism) + on demand.
|
|
6
7
|
// ADR-0016.
|
|
7
8
|
//
|
|
8
9
|
// It ALSO flags drift so a new flagship (e.g. a GPT-5.6-class release) surfaces instead of rotting:
|
|
@@ -45,6 +46,30 @@ export function detectDrift(catalog, models) {
|
|
|
45
46
|
return missing;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** Synchronize factual catalog prices from the same live map that backs the snapshot. */
|
|
50
|
+
export function syncCatalogPrices(catalog, models, pulledAt) {
|
|
51
|
+
const changes = [];
|
|
52
|
+
for (const [provider, entry] of Object.entries(catalog.providers || {})) {
|
|
53
|
+
if (entry.aliasOf) continue;
|
|
54
|
+
for (const tier of ['frontier', 'mid', 'cheap']) {
|
|
55
|
+
const candidate = entry[tier];
|
|
56
|
+
if (!candidate?.model) continue;
|
|
57
|
+
const live = models[candidate.model]
|
|
58
|
+
|| (!candidate.model.includes('/') ? models[`anthropic/${candidate.model}`] : null);
|
|
59
|
+
if (!live) continue;
|
|
60
|
+
if (candidate.in !== live.in || candidate.out !== live.out) {
|
|
61
|
+
changes.push({ provider, tier, model: candidate.model, from: { in: candidate.in, out: candidate.out }, to: live });
|
|
62
|
+
candidate.in = live.in;
|
|
63
|
+
candidate.out = live.out;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (catalog._meta?.sources) {
|
|
68
|
+
catalog._meta.sources.prices = `OpenRouter /api/v1/models live catalog, pulled ${pulledAt.slice(0, 10)} (in/out USD per Mtok).`;
|
|
69
|
+
}
|
|
70
|
+
return changes;
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
export function shapeSnapshot(models, pulledAt) {
|
|
49
74
|
return { _meta: { source: 'OpenRouter /api/v1/models (live metadata)', pulledAt, modelCount: Object.keys(models).length }, models };
|
|
50
75
|
}
|
|
@@ -73,7 +98,8 @@ async function main() {
|
|
|
73
98
|
|
|
74
99
|
const catalog = JSON.parse(fs.readFileSync(CATALOG, 'utf8'));
|
|
75
100
|
const missing = detectDrift(catalog, models);
|
|
76
|
-
const
|
|
101
|
+
const pulledAt = new Date().toISOString();
|
|
102
|
+
const snapshot = shapeSnapshot(models, pulledAt);
|
|
77
103
|
const next = JSON.stringify(snapshot, null, 2) + '\n';
|
|
78
104
|
|
|
79
105
|
if (check) {
|
|
@@ -86,8 +112,11 @@ async function main() {
|
|
|
86
112
|
return;
|
|
87
113
|
}
|
|
88
114
|
|
|
115
|
+
const priceChanges = syncCatalogPrices(catalog, models, pulledAt);
|
|
89
116
|
fs.writeFileSync(SNAPSHOT, next);
|
|
117
|
+
fs.writeFileSync(CATALOG, JSON.stringify(catalog, null, 2) + '\n');
|
|
90
118
|
console.log(`✓ wrote ${SNAPSHOT.replace(ROOT + '/', '')} — ${Object.keys(models).length} live models, pulledAt ${snapshot._meta.pulledAt}`);
|
|
119
|
+
console.log(`✓ synchronized ${priceChanges.length} catalog price field(s) from the same live response`);
|
|
91
120
|
if (missing.length) {
|
|
92
121
|
console.log(`\n⚠ DRIFT: ${missing.length} catalog model(s) no longer in the live catalog — update data/model-catalog.json:`);
|
|
93
122
|
for (const m of missing) console.log(' - ' + m);
|
|
@@ -11,7 +11,7 @@ const CANONICAL_PUBLISHERS = new Set([
|
|
|
11
11
|
'scripts/release.mjs',
|
|
12
12
|
'scripts/release-transaction-provider.mjs',
|
|
13
13
|
]);
|
|
14
|
-
const SOURCE_EXTENSIONS = new Set(['.mjs', '.js', '.cjs', '.sh']);
|
|
14
|
+
const SOURCE_EXTENSIONS = new Set(['.mjs', '.js', '.cjs', '.sh', '.yml', '.yaml']);
|
|
15
15
|
|
|
16
16
|
function executableSource(source) {
|
|
17
17
|
const withoutBlocks = source.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
@@ -28,7 +28,7 @@ const ACTIONS = [
|
|
|
28
28
|
jsPatterns: [
|
|
29
29
|
/\[\s*['"]release['"]\s*,\s*['"]create['"]/,
|
|
30
30
|
],
|
|
31
|
-
shellPatterns: [/^\s*gh\s+release\s+create\b/m],
|
|
31
|
+
shellPatterns: [/^\s*(?:-\s*run:\s*)?gh\s+release\s+create\b/m],
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
action: 'github-release-update',
|
|
@@ -36,7 +36,7 @@ const ACTIONS = [
|
|
|
36
36
|
/['"]PATCH['"][\s\S]{0,120}releases\//,
|
|
37
37
|
/\[\s*['"]release['"]\s*,\s*['"]edit['"]/,
|
|
38
38
|
],
|
|
39
|
-
shellPatterns: [/^\s*gh\s+(?:release\s+edit|api\s+.*releases\/.*PATCH)\b/m],
|
|
39
|
+
shellPatterns: [/^\s*(?:-\s*run:\s*)?gh\s+(?:release\s+edit|api\s+.*releases\/.*PATCH)\b/m],
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
action: 'npm-publish',
|
|
@@ -44,7 +44,7 @@ const ACTIONS = [
|
|
|
44
44
|
/(?:execFileSync|spawnSync)\(\s*['"]npm['"]\s*,\s*\[\s*['"]publish['"]/,
|
|
45
45
|
/runOrDie\(\s*['"]npm publish['"]/,
|
|
46
46
|
],
|
|
47
|
-
shellPatterns: [/^\s*npm\s+publish\b/m],
|
|
47
|
+
shellPatterns: [/^\s*(?:-\s*run:\s*)?npm\s+publish\b/m],
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
action: 'npm-dist-tag',
|
|
@@ -52,7 +52,7 @@ const ACTIONS = [
|
|
|
52
52
|
/(?:execFileSync|spawnSync)\(\s*['"]npm['"]\s*,\s*\[\s*['"]dist-tag['"]\s*,\s*['"]add['"]/,
|
|
53
53
|
/runOrDie\(\s*['"]npm dist-tag[^'"]*['"]/,
|
|
54
54
|
],
|
|
55
|
-
shellPatterns: [/^\s*npm\s+dist-tag\s+add\b/m],
|
|
55
|
+
shellPatterns: [/^\s*(?:-\s*run:\s*)?npm\s+dist-tag\s+add\b/m],
|
|
56
56
|
},
|
|
57
57
|
];
|
|
58
58
|
|
|
@@ -60,7 +60,10 @@ export function detectPublisherActions(file, source) {
|
|
|
60
60
|
const relative = file.split(path.sep).join('/');
|
|
61
61
|
if (CANONICAL_PUBLISHERS.has(relative)) return [];
|
|
62
62
|
const executable = executableSource(source);
|
|
63
|
-
const
|
|
63
|
+
const extension = path.extname(relative);
|
|
64
|
+
const kind = ['.sh', '.yml', '.yaml'].includes(extension) || relative.startsWith('package.json#scripts.')
|
|
65
|
+
? 'shellPatterns'
|
|
66
|
+
: 'jsPatterns';
|
|
64
67
|
return ACTIONS
|
|
65
68
|
.filter((action) => action[kind].some((pattern) => pattern.test(executable)))
|
|
66
69
|
.map(({ action }) => ({ file: relative, action }));
|
|
@@ -79,14 +82,23 @@ function sourceFiles(root) {
|
|
|
79
82
|
visit(path.join(root, 'scripts'));
|
|
80
83
|
visit(path.join(root, 'deploy'));
|
|
81
84
|
visit(path.join(root, 'bin'));
|
|
85
|
+
visit(path.join(root, '.github', 'workflows'));
|
|
82
86
|
return files;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
export function findUnauthorizedPublishers(root = ROOT) {
|
|
86
|
-
|
|
90
|
+
const findings = sourceFiles(root).flatMap((absolute) => {
|
|
87
91
|
const relative = path.relative(root, absolute).split(path.sep).join('/');
|
|
88
92
|
return detectPublisherActions(relative, fs.readFileSync(absolute, 'utf8'));
|
|
89
93
|
});
|
|
94
|
+
const packageFile = path.join(root, 'package.json');
|
|
95
|
+
if (fs.existsSync(packageFile)) {
|
|
96
|
+
const pkg = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
|
|
97
|
+
for (const [name, command] of Object.entries(pkg.scripts || {})) {
|
|
98
|
+
findings.push(...detectPublisherActions(`package.json#scripts.${name}`, String(command)));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return findings;
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
export function main(root = ROOT) {
|
|
@@ -11,18 +11,16 @@
|
|
|
11
11
|
* for a month.
|
|
12
12
|
*
|
|
13
13
|
* WHAT IT WILL NOT DO. It does not publish. `scripts/self-update.mjs:56` refuses `--publish` and
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* still runs inside the workflow exactly as designed. Bypassing them to "get it done while nobody
|
|
18
|
-
* is looking" would be the worst possible reading of an instruction to finish the job.
|
|
14
|
+
* `release-candidate-preflight.yml` owns the one-time long lanes and `protected-release.yml` is the
|
|
15
|
+
* sole publication controller. This watchdog is report-only: it never dispatches either phase and
|
|
16
|
+
* cannot manufacture typed candidate evidence.
|
|
19
17
|
*
|
|
20
18
|
* IT IS A NO-OP UNLESS EVERY PRECONDITION HOLDS. It runs from the nightly, unattended, for weeks.
|
|
21
19
|
* A watchdog that acts on a partial picture is worse than no watchdog, so it refuses on anything
|
|
22
20
|
* unexpected and says why. The default outcome is "nothing happened, here is the reason".
|
|
23
21
|
*
|
|
24
22
|
* node scripts/release-convergence-watchdog.mjs # report only, never acts
|
|
25
|
-
* node scripts/release-convergence-watchdog.mjs --dispatch #
|
|
23
|
+
* node scripts/release-convergence-watchdog.mjs --dispatch # compatibility flag; still reports only
|
|
26
24
|
*/
|
|
27
25
|
import { execFileSync } from 'node:child_process';
|
|
28
26
|
import fs from 'node:fs';
|
|
@@ -99,21 +97,11 @@ const runsFor = (wf) => JSON.parse(tryShy(() => sh('gh', ['run', 'list', '--repo
|
|
|
99
97
|
const greenAt = (wf) => runsFor(wf).find((r) => r.headSha === originMain && r.conclusion === 'success');
|
|
100
98
|
|
|
101
99
|
const ci = greenAt('ci');
|
|
102
|
-
const aggregate = greenAt('release-aggregate');
|
|
103
100
|
if (!ci) stand_down(`no successful exact-SHA \`ci\` run at ${originMain.slice(0, 7)} yet`);
|
|
104
|
-
if (!aggregate) stand_down(`no successful exact-SHA \`release-aggregate\` run at ${originMain.slice(0, 7)} yet`);
|
|
105
101
|
|
|
106
|
-
log(`
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ── 6. Dispatch THE SANCTIONED PUBLISHER. Nothing here publishes; the workflow does. ─────────────
|
|
113
|
-
sh('gh', ['workflow', 'run', 'protected-release.yml', '--repo', REPO,
|
|
114
|
-
'-f', `candidate_sha=${originMain}`,
|
|
115
|
-
'-f', `version=${version}`,
|
|
116
|
-
'-f', `release_qe_run_id=${ci.databaseId}`,
|
|
117
|
-
'-f', `aggregate_run_id=${aggregate.databaseId}`]);
|
|
118
|
-
log(`DISPATCHED protected-release for ${version}. The workflow owns every safety gate from here.`);
|
|
119
|
-
log('Verify afterwards with: node scripts/published-surface-probe.mjs (D-version-coherence must PASS).');
|
|
102
|
+
log(`PRECONDITIONS VISIBLE — candidate=${originMain.slice(0, 7)} version=${version} ci=${ci.databaseId}`);
|
|
103
|
+
// ── 6. Never dispatch a publisher from the watchdog. ───────────────────────────────────────────
|
|
104
|
+
// Only protected-release may derive the complete typed evidence chain and cross Production.
|
|
105
|
+
// This unattended observer cannot turn visible preconditions into release authority.
|
|
106
|
+
log(`PRECONDITIONS ONLY — no candidate artifact inferred and no action taken for ${version} at ${originMain}`);
|
|
107
|
+
log('Run release-candidate-preflight on release/** once; fast-forward that exact SHA to main; then protected-release imports release-candidate-<SHA> without a run ID.');
|