ruvnet-brain 4.0.6 → 4.0.8

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 (47) hide show
  1. package/README.md +3 -3
  2. package/bin/install.mjs +118 -9
  3. package/config/model-router/catalog.template.json +17 -2
  4. package/console/app.js +32 -14
  5. package/console/architecture.html +1 -1
  6. package/data/model-catalog.json +54 -0
  7. package/package.json +2 -1
  8. package/plugin/.claude-plugin/plugin.json +1 -1
  9. package/plugin/.codex-plugin/plugin.json +1 -1
  10. package/plugin/hooks/codex-hooks.json +17 -17
  11. package/plugin/hooks/hooks.json +34 -1
  12. package/plugin/scripts/codex-hook-wrapper.mjs +72 -1
  13. package/plugin/scripts/ground-ruvnet.sh +2 -1
  14. package/plugin/scripts/hook-shim.mjs +13 -6
  15. package/plugin/scripts/lesson-provenance.mjs +21 -0
  16. package/plugin/scripts/lesson-store.mjs +33 -3
  17. package/plugin/scripts/route-dispatch.sh +23 -49
  18. package/plugin/scripts/session-snapshot-contract.mjs +100 -0
  19. package/plugin/scripts/session-snapshot-hook.mjs +34 -0
  20. package/plugin/scripts/session-start-core.mjs +25 -7
  21. package/plugin/scripts/swarm-slot-recycler.mjs +89 -0
  22. package/plugin/skills/release-proof/SKILL.md +4 -4
  23. package/plugin/skills/release-proof/scripts/release-proof.mjs +12 -5
  24. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +19 -4
  25. package/scripts/candidate-host-evidence.mjs +49 -0
  26. package/scripts/capability-registry.mjs +3 -3
  27. package/scripts/ci/learning-replay-codex-adapter.mjs +13 -1
  28. package/scripts/learning-replay-contract.mjs +14 -4
  29. package/scripts/learning-replay-execution.mjs +4 -3
  30. package/scripts/learning-replay-fixture.mjs +23 -2
  31. package/scripts/lesson-lifecycle.mjs +2 -1
  32. package/scripts/lesson-ratify.mjs +6 -3
  33. package/scripts/lesson-seed.mjs +21 -16
  34. package/scripts/model-router-catalog.mjs +16 -0
  35. package/scripts/model-router-engine.mjs +4 -1
  36. package/scripts/onboarding-console.mjs +52 -14
  37. package/scripts/provider-availability.mjs +12 -0
  38. package/scripts/publication-receipt.mjs +126 -34
  39. package/scripts/release-authority.mjs +14 -2
  40. package/scripts/release-evidence-aggregate.mjs +55 -0
  41. package/scripts/release-payload.mjs +100 -0
  42. package/scripts/release-transaction-provider.mjs +413 -0
  43. package/scripts/release-transaction.mjs +404 -0
  44. package/scripts/release.mjs +97 -280
  45. package/scripts/selfcheck.mjs +20 -5
  46. package/scripts/session-snapshot-contract.mjs +1 -0
  47. package/scripts/staged-host-verifier.mjs +103 -0
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+
4
+ export const RECEIPT_PREFIX = 'release-transaction-';
5
+ export const TERMINAL_STATES = new Set(['channels-converged', 'aborted']);
6
+
7
+ export const ALLOWED_TRANSITIONS = Object.freeze({
8
+ 'remote-prepared': ['asset-upload-intent', 'manual-intervention-required'],
9
+ 'asset-upload-intent': ['npm-stage-intent', 'manual-intervention-required'],
10
+ 'npm-stage-intent': ['npm-candidate-staged', 'manual-intervention-required'],
11
+ 'npm-candidate-staged': ['remote-materialization-intent', 'manual-intervention-required'],
12
+ 'remote-materialization-intent': ['prepared', 'manual-intervention-required'],
13
+ prepared: ['github-promote-intent', 'manual-intervention-required'],
14
+ 'github-promote-intent': ['github-promoted-nonlatest', 'manual-intervention-required'],
15
+ 'github-promoted-nonlatest': ['npm-promote-intent', 'manual-intervention-required'],
16
+ 'npm-promote-intent': ['npm-promoted', 'compensation-intent', 'manual-intervention-required'],
17
+ 'npm-promoted': ['github-latest-intent', 'compensation-intent', 'manual-intervention-required'],
18
+ 'github-latest-intent': ['defaults-promoted', 'compensation-intent', 'manual-intervention-required'],
19
+ 'compensation-intent': ['compensated', 'manual-intervention-required'],
20
+ compensated: ['github-promote-intent', 'npm-promote-intent', 'manual-intervention-required'],
21
+ 'defaults-promoted': ['finalize-intent', 'manual-intervention-required'],
22
+ 'finalize-intent': ['channels-converged', 'manual-intervention-required'],
23
+ 'manual-intervention-required': [],
24
+ 'channels-converged': [],
25
+ aborted: [],
26
+ });
27
+
28
+ export const canonicalJson = (value) => {
29
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
30
+ if (value && typeof value === 'object') {
31
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
32
+ }
33
+ return JSON.stringify(value);
34
+ };
35
+
36
+ export const receiptPayload = (receipt) => {
37
+ const { signature: _signature, receiptDigest: _digest, ...payload } = receipt;
38
+ return canonicalJson(payload);
39
+ };
40
+
41
+ export const digestReceipt = (receipt) => crypto.createHash('sha256')
42
+ .update(receiptPayload(receipt)).digest('hex');
43
+
44
+ export const transactionIdFor = (identity) => crypto.createHash('sha256').update(canonicalJson({
45
+ schemaVersion: 2,
46
+ repository: identity.repository,
47
+ package: identity.package,
48
+ version: identity.version,
49
+ tag: identity.tag,
50
+ candidateSha: identity.candidateSha,
51
+ payloadId: identity.payloadId || null,
52
+ evidenceDigest: identity.evidenceDigest || null,
53
+ packageIntegrity: identity.packageIntegrity,
54
+ packageSha256: identity.packageSha256 || null,
55
+ packageAssetName: identity.packageAssetName || null,
56
+ bundleSha256: identity.bundleSha256,
57
+ bundleSignatureSha256: identity.bundleSignatureSha256 || null,
58
+ bundleDigestSha256: identity.bundleDigestSha256 || null,
59
+ })).digest('hex');
60
+
61
+ export function signReceipt(receipt, privateKey) {
62
+ const unsigned = { ...receipt, receiptDigest: digestReceipt(receipt) };
63
+ return {
64
+ ...unsigned,
65
+ signature: crypto.sign(null, Buffer.from(canonicalJson(unsigned)), privateKey).toString('base64'),
66
+ };
67
+ }
68
+
69
+ export function verifyReceipt(receipt, publicKey) {
70
+ if (!receipt || ![1, 2].includes(receipt.schemaVersion) || !/^[a-f0-9]{64}$/.test(receipt.transactionId || '')) {
71
+ throw new Error('invalid release transaction receipt');
72
+ }
73
+ const { signature, receiptDigest, ...unsigned } = receipt;
74
+ if (digestReceipt(unsigned) !== receiptDigest) throw new Error('release receipt digest mismatch');
75
+ if (!crypto.verify(null, Buffer.from(canonicalJson({ ...unsigned, receiptDigest })), publicKey, Buffer.from(signature || '', 'base64'))) {
76
+ throw new Error('release receipt signature mismatch');
77
+ }
78
+ return receipt;
79
+ }
80
+
81
+ export function validateReceiptChain(receipts, identity, publicKey) {
82
+ const expectedId = transactionIdFor(identity);
83
+ const sorted = [...receipts].sort((a, b) => a.sequence - b.sequence);
84
+ let previous = null;
85
+ for (let index = 0; index < sorted.length; index += 1) {
86
+ const receipt = verifyReceipt(sorted[index], publicKey);
87
+ if (receipt.transactionId !== expectedId || canonicalJson(receipt.identity) !== canonicalJson(identity)) {
88
+ throw new Error('release receipt identity conflict');
89
+ }
90
+ if (receipt.sequence !== index) throw new Error('release receipt sequence gap or replay');
91
+ if ((receipt.previousReceiptDigest || null) !== (previous?.receiptDigest || null)) {
92
+ throw new Error('release receipt chain conflict');
93
+ }
94
+ previous = receipt;
95
+ }
96
+ return sorted;
97
+ }
98
+
99
+ const stateReceipt = ({ identity, prior, state, observation = {}, privateKey }) => signReceipt({
100
+ schemaVersion: 2,
101
+ transactionId: transactionIdFor(identity),
102
+ sequence: prior ? prior.sequence + 1 : 0,
103
+ previousReceiptDigest: prior?.receiptDigest || null,
104
+ state,
105
+ identity,
106
+ observation,
107
+ createdAt: new Date().toISOString(),
108
+ }, privateKey);
109
+
110
+ const exact = (actual, expected, label) => {
111
+ if (actual !== expected) throw new Error(`${label} mismatch: ${actual ?? '(missing)'} != ${expected}`);
112
+ };
113
+
114
+ const npmCandidateExact = (snapshot, identity) => snapshot?.npm?.candidateVersion === identity.version
115
+ && snapshot?.npm?.candidateIntegrity === identity.packageIntegrity
116
+ && snapshot?.npm?.candidateTagVersion === identity.version;
117
+ const npmLatestIsB = (snapshot, identity) => snapshot?.npm?.latestVersion === identity.version;
118
+ const githubIsB = (snapshot, identity) => snapshot?.github?.sha === identity.candidateSha
119
+ && snapshot?.github?.tag === identity.tag;
120
+
121
+ /**
122
+ * Pure, total next-action selector. Receipt history is an audit chain, never permission to skip a
123
+ * provider postcondition. Every invocation consumes one fresh cross-provider observation.
124
+ */
125
+ export function reduceReleaseState({ lastReceipt, snapshot, identity, prior }) {
126
+ if (!lastReceipt || !snapshot || snapshot.readError) return { action: 'manual', reason: 'provider observation unavailable' };
127
+ const assetsExact = snapshot.github?.assetsExact === true;
128
+ const candidateExact = npmCandidateExact(snapshot, identity);
129
+ const npmB = npmLatestIsB(snapshot, identity);
130
+ const githubB = githubIsB(snapshot, identity);
131
+ const githubPublished = githubB && snapshot.github?.published === true;
132
+ const githubLatest = githubPublished && snapshot.github?.latest === true;
133
+
134
+ if (snapshot.npm?.candidateVersion === identity.version && snapshot.npm?.candidateIntegrity
135
+ && snapshot.npm.candidateIntegrity !== identity.packageIntegrity) {
136
+ return { action: 'manual', reason: 'immutable npm candidate bytes mismatch' };
137
+ }
138
+ if (snapshot.github?.tag === identity.tag && snapshot.github?.sha
139
+ && snapshot.github.sha !== identity.candidateSha) {
140
+ return { action: 'manual', reason: 'GitHub tag resolves to competing SHA' };
141
+ }
142
+ if (snapshot.github?.latestTag && snapshot.github.latestTag !== identity.tag
143
+ && snapshot.github.latestTag !== prior?.githubLatest) {
144
+ return { action: 'manual', reason: 'GitHub latest is neither captured A nor candidate B' };
145
+ }
146
+ if (npmB && snapshot.github?.draft === true) return { action: 'compensate-npm' };
147
+ if (npmB && githubLatest) {
148
+ if (lastReceipt.state === 'channels-converged') {
149
+ return githubPublished && assetsExact && candidateExact
150
+ && snapshot.publicReceiptExact && snapshot.publicHostsExact
151
+ ? { action: 'complete' }
152
+ : { action: 'manual', reason: 'terminal public state drifted' };
153
+ }
154
+ return { action: 'finalize' };
155
+ }
156
+ if (!assetsExact) return { action: 'upload-assets' };
157
+ if (!candidateExact) return { action: 'stage-npm' };
158
+ if (!githubPublished) return { action: 'publish-github-nonlatest' };
159
+ if (!npmB) {
160
+ if (snapshot.npm?.latestVersion !== prior?.npmLatest) {
161
+ return { action: 'manual', reason: 'npm latest is neither captured A nor candidate B' };
162
+ }
163
+ return { action: 'promote-npm' };
164
+ }
165
+ if (!githubLatest) return { action: 'make-github-latest' };
166
+ return { action: 'manual', reason: 'unclassified provider state' };
167
+ }
168
+
169
+ export async function pollObservation(read, predicate, {
170
+ maxElapsedMs = 120_000,
171
+ maxAttempts = 12,
172
+ initialDelayMs = 1_000,
173
+ maxDelayMs = 15_000,
174
+ multiplier = 1.8,
175
+ jitter = () => 0,
176
+ now = () => Date.now(),
177
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
178
+ } = {}) {
179
+ const started = now();
180
+ let delay = initialDelayMs;
181
+ let last;
182
+ let lastError;
183
+ for (let attempt = 1; attempt <= maxAttempts && now() - started <= maxElapsedMs; attempt += 1) {
184
+ try {
185
+ last = await read();
186
+ const elapsedMs = now() - started;
187
+ if (predicate(last) && elapsedMs <= maxElapsedMs) return { value: last, attempt, elapsedMs };
188
+ } catch (error) {
189
+ lastError = error;
190
+ }
191
+ if (attempt === maxAttempts) break;
192
+ const remaining = maxElapsedMs - (now() - started);
193
+ if (remaining <= 0) break;
194
+ const wait = Math.max(0, Math.min(remaining, maxDelayMs, delay + jitter(delay, attempt)));
195
+ await sleep(wait);
196
+ delay = Math.min(maxDelayMs, Math.ceil(delay * multiplier));
197
+ }
198
+ const detail = lastError?.message || canonicalJson(last);
199
+ throw new Error(`visibility deadline exceeded after ${now() - started}ms: ${detail}`);
200
+ }
201
+
202
+ export async function runReleaseTransaction({ identity, assets, adapter, privateKey, publicKey, hostVerifier }) {
203
+ const expectedId = transactionIdFor(identity);
204
+ const discovered = await adapter.discover(identity);
205
+ const competing = discovered.pending?.filter((item) => item.transactionId !== expectedId) || [];
206
+ if (competing.length) throw new Error(`pending release ${competing[0].transactionId} blocks ${expectedId}`);
207
+ if ((discovered.matchingDrafts || []).length > 1) throw new Error('duplicate matching drafts require reconciliation');
208
+
209
+ let chain = validateReceiptChain(discovered.receipts || [], identity, publicKey);
210
+ let current = chain.at(-1) || null;
211
+ const draft = discovered.matchingDrafts?.[0] || await adapter.createDraft(identity);
212
+ const append = async (state, observation = {}) => {
213
+ const recoveryCompensation = state === 'compensation-intent' && current
214
+ && !TERMINAL_STATES.has(current.state) && current.state !== 'manual-intervention-required';
215
+ if (current && !ALLOWED_TRANSITIONS[current.state]?.includes(state) && current.state !== state
216
+ && !recoveryCompensation) {
217
+ throw new Error(`illegal release transition ${current.state} -> ${state}`);
218
+ }
219
+ const receipt = stateReceipt({ identity, prior: current, state, observation, privateKey });
220
+ await adapter.appendReceipt(draft, receipt, `${RECEIPT_PREFIX}${String(receipt.sequence).padStart(4, '0')}.json`);
221
+ const observed = await adapter.readReceipt(draft, receipt.sequence);
222
+ verifyReceipt(observed, publicKey);
223
+ exact(observed.receiptDigest, receipt.receiptDigest, 'remote receipt');
224
+ current = receipt;
225
+ return receipt;
226
+ };
227
+ const transition = async (state, observation = {}) => {
228
+ if (current?.state !== state) await append(state, observation);
229
+ };
230
+
231
+ if (!current) await append('remote-prepared', {
232
+ draftId: draft.id,
233
+ prior: discovered.prior,
234
+ reconciledLegacyTransactions: discovered.legacySettled || [],
235
+ });
236
+ const prior = chain[0]?.observation?.prior || current.observation?.prior || discovered.prior;
237
+ if (!prior?.npmLatest || !prior?.githubLatest) throw new Error('sequence-zero prior generation is incomplete');
238
+
239
+ for (let step = 0; step < 32; step += 1) {
240
+ const snapshot = await adapter.observeSnapshot(identity, draft, {
241
+ forceAssets: current.state === 'finalize-intent' || current.state === 'channels-converged',
242
+ });
243
+ if (current.state === 'channels-converged') {
244
+ const terminal = reduceReleaseState({ lastReceipt: current, snapshot, identity, prior });
245
+ if (terminal.action === 'complete') return current;
246
+ throw new Error(`terminal release drift: ${terminal.reason || terminal.action}`);
247
+ }
248
+ // A process may die after the provider side effect and before its observation receipt. Rebuild
249
+ // that missing receipt from fresh state before asking the reducer for the next command.
250
+ if (current.state === 'npm-stage-intent' && npmCandidateExact(snapshot, identity)) {
251
+ await transition('npm-candidate-staged', { npm: snapshot.npm, recovered: true });
252
+ continue;
253
+ }
254
+ if (current.state === 'remote-materialization-intent' && snapshot.github?.assetsExact
255
+ && npmCandidateExact(snapshot, identity)) {
256
+ await transition('prepared', { recovered: true });
257
+ continue;
258
+ }
259
+ if (current.state === 'github-promote-intent' && githubIsB(snapshot, identity)
260
+ && snapshot.github?.published && !snapshot.github?.latest) {
261
+ await transition('github-promoted-nonlatest', { github: snapshot.github, recovered: true });
262
+ continue;
263
+ }
264
+ if (current.state === 'npm-promote-intent' && npmLatestIsB(snapshot, identity)) {
265
+ await transition('npm-promoted', { npm: snapshot.npm, recovered: true });
266
+ continue;
267
+ }
268
+ if (current.state === 'github-latest-intent' && npmLatestIsB(snapshot, identity)
269
+ && githubIsB(snapshot, identity) && snapshot.github?.latest) {
270
+ await transition('defaults-promoted', { npm: snapshot.npm, github: snapshot.github, recovered: true });
271
+ continue;
272
+ }
273
+ if (current.state === 'compensation-intent' && snapshot.npm?.latestVersion === prior.npmLatest) {
274
+ await transition('compensated', { npm: snapshot.npm, github: snapshot.github, recovered: true });
275
+ throw new Error('npm compensation recovered; resume the same transaction');
276
+ }
277
+ if (current.state === 'finalize-intent' && snapshot.publicReceiptExact && snapshot.publicHostsExact
278
+ && npmCandidateExact(snapshot, identity) && npmLatestIsB(snapshot, identity)
279
+ && githubIsB(snapshot, identity) && snapshot.github?.published && snapshot.github?.latest
280
+ && snapshot.github?.assetsExact) {
281
+ return append('channels-converged', { verdict: 'PASS', recovered: true });
282
+ }
283
+ const decision = reduceReleaseState({ lastReceipt: current, snapshot, identity, prior });
284
+ const stageRecord = {
285
+ event: 'release-stage',
286
+ transactionId: expectedId,
287
+ payloadId: identity.payloadId || null,
288
+ sequence: current.sequence,
289
+ state: current.state,
290
+ action: decision.action,
291
+ };
292
+ console.log(process.env.GITHUB_ACTIONS === 'true'
293
+ ? `::notice title=Release stage ${decision.action}::${JSON.stringify(stageRecord)}`
294
+ : JSON.stringify(stageRecord));
295
+ if (decision.action === 'complete') return current;
296
+ if (decision.action === 'manual') {
297
+ await transition('manual-intervention-required', { reason: decision.reason, snapshot });
298
+ throw new Error(`release requires manual intervention: ${decision.reason}`);
299
+ }
300
+ if (decision.action === 'upload-assets') {
301
+ await transition('asset-upload-intent', { payloadId: identity.payloadId || null });
302
+ await adapter.uploadAssets(draft, assets, identity);
303
+ const observed = await adapter.observeSnapshot(identity, draft);
304
+ if (!observed.github?.assetsExact) throw new Error('staged GitHub payload mismatch');
305
+ await transition('npm-stage-intent', { github: observed.github });
306
+ continue;
307
+ }
308
+ if (decision.action === 'stage-npm') {
309
+ await transition('npm-stage-intent');
310
+ await adapter.stageNpm(identity, assets.packagePath);
311
+ const polled = await pollObservation(
312
+ async () => ({ npm: await adapter.observeNpm(identity) }),
313
+ (value) => npmCandidateExact(value, identity),
314
+ adapter.observationPolicy,
315
+ );
316
+ await transition('npm-candidate-staged', { npm: polled.value.npm, visibility: polled });
317
+ continue;
318
+ }
319
+ if (decision.action === 'publish-github-nonlatest') {
320
+ if (current.state === 'npm-candidate-staged') await transition('remote-materialization-intent');
321
+ if (current.state === 'remote-materialization-intent') {
322
+ const staged = await adapter.materializeStagedAssets(draft, identity);
323
+ try { await adapter.verifyMaterializedPayload?.(staged.assets, identity); } finally { staged.cleanup(); }
324
+ await transition('prepared');
325
+ }
326
+ await transition('github-promote-intent');
327
+ await adapter.publishDraftNonLatest(draft, identity);
328
+ const observed = await adapter.observeSnapshot(identity, draft);
329
+ if (!(githubIsB(observed, identity) && observed.github.published && !observed.github.latest)) {
330
+ throw new Error('GitHub non-latest publication not observed');
331
+ }
332
+ await transition('github-promoted-nonlatest', { github: observed.github });
333
+ continue;
334
+ }
335
+ if (decision.action === 'promote-npm') {
336
+ await transition('npm-promote-intent');
337
+ await adapter.promoteNpm(identity, prior.npmLatest);
338
+ const polled = await pollObservation(
339
+ async () => ({ npm: await adapter.observeNpm(identity) }),
340
+ (value) => npmLatestIsB(value, identity),
341
+ adapter.observationPolicy,
342
+ );
343
+ await transition('npm-promoted', { npm: polled.value.npm, visibility: polled });
344
+ continue;
345
+ }
346
+ if (decision.action === 'make-github-latest') {
347
+ await transition('github-latest-intent');
348
+ await adapter.makeGithubLatest(draft, identity, prior.githubLatest);
349
+ const observed = await adapter.observeSnapshot(identity, draft);
350
+ if (!(npmLatestIsB(observed, identity) && observed.github?.latest && githubIsB(observed, identity))) {
351
+ throw new Error('provider defaults not jointly observed at candidate B');
352
+ }
353
+ await transition('defaults-promoted', { npm: observed.npm, github: observed.github });
354
+ continue;
355
+ }
356
+ if (decision.action === 'compensate-npm') {
357
+ await transition('compensation-intent', { restore: prior.npmLatest });
358
+ await adapter.restoreNpmLatest(prior.npmLatest, identity.version);
359
+ const polled = await pollObservation(
360
+ () => adapter.observeNpm(identity),
361
+ (npm) => npm?.latestVersion === prior.npmLatest,
362
+ adapter.observationPolicy,
363
+ );
364
+ await transition('compensated', { npm: polled.value, github: snapshot.github, visibility: polled });
365
+ throw new Error('npm compensated to captured prior generation; resume the same transaction');
366
+ }
367
+ if (decision.action === 'finalize') {
368
+ if (current.state !== 'defaults-promoted' && current.state !== 'finalize-intent') {
369
+ await transition('defaults-promoted', { npm: snapshot.npm, github: snapshot.github });
370
+ }
371
+ await transition('finalize-intent');
372
+ const final = await adapter.finalize(identity, current, hostVerifier);
373
+ if (final.verdict !== 'PASS') throw new Error('final release convergence failed');
374
+ const reobserved = await adapter.observeSnapshot(identity, draft, { forceAssets: true });
375
+ if (!(npmLatestIsB(reobserved, identity) && reobserved.github?.latest && githubIsB(reobserved, identity))) {
376
+ throw new Error('provider defaults drifted during finalization');
377
+ }
378
+ if (!reobserved.github?.assetsExact || !reobserved.publicReceiptExact || !reobserved.publicHostsExact) {
379
+ throw new Error('public receipt, host, or artifact evidence drifted during finalization');
380
+ }
381
+ return append('channels-converged', final);
382
+ }
383
+ }
384
+ throw new Error('release reducer exceeded its bounded transition count');
385
+ }
386
+
387
+ export async function abortReleaseTransaction({ identity, receipts, reason, authorized, adapter, privateKey, publicKey }) {
388
+ if (!authorized) throw new Error('release abort requires explicit human authorization');
389
+ const chain = validateReceiptChain(receipts, identity, publicKey);
390
+ const current = chain.at(-1);
391
+ if (!current || TERMINAL_STATES.has(current.state)) throw new Error('release transaction is not abortable');
392
+ const snapshot = await adapter.observeSnapshot(identity);
393
+ const prior = chain[0]?.observation?.prior;
394
+ if (!prior || snapshot.npm?.latestVersion === identity.version || snapshot.github?.latest === true
395
+ || snapshot.npm?.latestVersion !== prior.npmLatest || snapshot.github?.latestTag !== prior.githubLatest) {
396
+ throw new Error('release abort cannot prove both defaults at captured prior generation');
397
+ }
398
+ const receipt = stateReceipt({
399
+ identity, prior: current, state: 'aborted',
400
+ observation: { reason, authorized: true, snapshot }, privateKey,
401
+ });
402
+ await adapter.appendReceipt(null, receipt, `${RECEIPT_PREFIX}${String(receipt.sequence).padStart(4, '0')}.json`);
403
+ return receipt;
404
+ }