ruvnet-brain 4.0.6 → 4.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +3 -3
  2. package/bin/install.mjs +112 -7
  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/hooks.json +34 -1
  11. package/plugin/scripts/ground-ruvnet.sh +2 -1
  12. package/plugin/scripts/hook-shim.mjs +13 -6
  13. package/plugin/scripts/lesson-provenance.mjs +21 -0
  14. package/plugin/scripts/lesson-store.mjs +33 -3
  15. package/plugin/scripts/route-dispatch.sh +23 -49
  16. package/plugin/scripts/session-snapshot-contract.mjs +100 -0
  17. package/plugin/scripts/session-snapshot-hook.mjs +34 -0
  18. package/plugin/scripts/session-start-core.mjs +25 -7
  19. package/plugin/scripts/swarm-slot-recycler.mjs +89 -0
  20. package/plugin/skills/release-proof/SKILL.md +4 -4
  21. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +19 -4
  22. package/scripts/capability-registry.mjs +3 -3
  23. package/scripts/lesson-lifecycle.mjs +2 -1
  24. package/scripts/lesson-ratify.mjs +6 -3
  25. package/scripts/lesson-seed.mjs +21 -16
  26. package/scripts/model-router-catalog.mjs +16 -0
  27. package/scripts/model-router-engine.mjs +4 -1
  28. package/scripts/onboarding-console.mjs +52 -14
  29. package/scripts/provider-availability.mjs +12 -0
  30. package/scripts/release-authority.mjs +14 -2
  31. package/scripts/release-transaction-provider.mjs +236 -0
  32. package/scripts/release-transaction.mjs +214 -0
  33. package/scripts/release.mjs +67 -279
  34. package/scripts/session-snapshot-contract.mjs +1 -0
  35. package/scripts/staged-host-verifier.mjs +106 -0
@@ -0,0 +1,236 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { RECEIPT_PREFIX, TERMINAL_STATES } from './release-transaction.mjs';
7
+
8
+ const REPO = 'stuinfla/ruvnet-brain';
9
+ const PACKAGE = 'ruvnet-brain';
10
+
11
+ const command = (name, args, options = {}) => execFileSync(name, args, {
12
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...options,
13
+ }).trim();
14
+ const json = (name, args, options) => JSON.parse(command(name, args, options));
15
+ const maybe = (callback, fallback = null) => {
16
+ try { return callback(); } catch { return fallback; }
17
+ };
18
+
19
+ const tagSha = (tag, root) => {
20
+ const rows = command('git', ['ls-remote', 'origin', `refs/tags/${tag}`, `refs/tags/${tag}^{}`], { cwd: root })
21
+ .split('\n').filter(Boolean).map((line) => line.split(/\s+/));
22
+ return rows.find(([, ref]) => ref?.endsWith('^{}'))?.[0] || rows[0]?.[0] || '';
23
+ };
24
+
25
+ const assetBytes = (asset) => {
26
+ const result = spawnSync('gh', ['api', asset.url, '-H', 'Accept: application/octet-stream'], {
27
+ encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'],
28
+ });
29
+ if (result.error || result.status !== 0) throw new Error(`cannot download transaction asset ${asset.name}`);
30
+ return Buffer.from(result.stdout);
31
+ };
32
+ const assetReceipt = (asset) => JSON.parse(assetBytes(asset).toString('utf8'));
33
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
34
+
35
+ export function liveReleaseProvider({
36
+ root = process.cwd(), candidateReceipt = null, publicationReceipt = null,
37
+ } = {}) {
38
+ let releases = [];
39
+ let activeDraft = null;
40
+ const refresh = () => {
41
+ releases = json('gh', ['api', `repos/${REPO}/releases?per_page=100`]);
42
+ return releases;
43
+ };
44
+ const releaseById = (id) => json('gh', ['api', `repos/${REPO}/releases/${id}`]);
45
+ const receiptsFor = (release) => (release?.assets || [])
46
+ .filter(({ name }) => name.startsWith(RECEIPT_PREFIX) && name.endsWith('.json'))
47
+ .map(assetReceipt);
48
+ const materializeRemoteAssets = (draft, identity) => {
49
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-release-staged-'));
50
+ try {
51
+ const release = releaseById(draft.id);
52
+ const required = [
53
+ ['bundlePath', 'ruvnet-brain.zip'],
54
+ ['bundleSignaturePath', 'ruvnet-brain.zip.sig'],
55
+ ['bundleDigestPath', 'ruvnet-brain.zip.sha256'],
56
+ ];
57
+ const assets = {};
58
+ for (const [key, name] of required) {
59
+ const remote = release.assets?.find((asset) => asset.name === name);
60
+ if (!remote) throw new Error(`staged GitHub asset missing: ${name}`);
61
+ const file = path.join(temp, name);
62
+ fs.writeFileSync(file, assetBytes(remote), { flag: 'wx', mode: 0o600 });
63
+ assets[key] = file;
64
+ }
65
+ if (sha256(fs.readFileSync(assets.bundlePath)) !== identity.bundleSha256) {
66
+ throw new Error('staged GitHub bundle digest does not match transaction identity');
67
+ }
68
+ const packed = json('npm', [
69
+ 'pack', `${PACKAGE}@candidate-v${identity.version}`, '--json', '--pack-destination', temp,
70
+ ]);
71
+ if (!Array.isArray(packed) || packed.length !== 1 || !packed[0].filename) {
72
+ throw new Error('npm candidate pack did not return exactly one artifact');
73
+ }
74
+ assets.packagePath = path.join(temp, packed[0].filename);
75
+ const integrity = `sha512-${crypto.createHash('sha512').update(fs.readFileSync(assets.packagePath)).digest('base64')}`;
76
+ if (integrity !== identity.packageIntegrity) throw new Error('staged npm package integrity mismatch');
77
+ return { assets, cleanup: () => fs.rmSync(temp, { recursive: true, force: true }) };
78
+ } catch (error) {
79
+ fs.rmSync(temp, { recursive: true, force: true });
80
+ throw error;
81
+ }
82
+ };
83
+
84
+ return {
85
+ async discover(identity) {
86
+ const all = refresh();
87
+ const matchingDrafts = all.filter((release) => release.tag_name === identity.tag
88
+ && release.target_commitish === identity.candidateSha).map((release) => ({
89
+ id: release.id, tag: release.tag_name, sha: release.target_commitish, draft: release.draft,
90
+ }));
91
+ activeDraft = matchingDrafts[0] || null;
92
+ const receipts = matchingDrafts.length === 1 ? receiptsFor(releaseById(matchingDrafts[0].id)) : [];
93
+ const latestByTransaction = new Map();
94
+ for (const receipt of all.flatMap((release) => receiptsFor(release))) {
95
+ const prior = latestByTransaction.get(receipt.transactionId);
96
+ if (!prior || receipt.sequence > prior.sequence) latestByTransaction.set(receipt.transactionId, receipt);
97
+ }
98
+ const pending = [...latestByTransaction.values()]
99
+ .filter((receipt) => !TERMINAL_STATES.has(receipt.state));
100
+ const npmLatest = maybe(() => command('npm', ['view', `${PACKAGE}@latest`, 'version']), null);
101
+ const githubLatest = maybe(() => json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name, null);
102
+ return { matchingDrafts, receipts, pending, prior: { npmLatest, githubLatest } };
103
+ },
104
+
105
+ async createDraft(identity) {
106
+ const release = json('gh', [
107
+ 'api', '-X', 'POST', `repos/${REPO}/releases`,
108
+ '-f', `tag_name=${identity.tag}`, '-f', `target_commitish=${identity.candidateSha}`,
109
+ '-f', `name=${identity.tag} staged candidate`, '-F', 'draft=true', '-F', 'prerelease=false',
110
+ ]);
111
+ activeDraft = { id: release.id, tag: release.tag_name, sha: release.target_commitish };
112
+ return activeDraft;
113
+ },
114
+
115
+ async appendReceipt(draft, receipt, name) {
116
+ const anchor = draft || activeDraft;
117
+ if (!anchor) throw new Error('no draft anchor for transaction receipt');
118
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-release-receipt-'));
119
+ const file = path.join(temp, name);
120
+ try {
121
+ fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
122
+ command('gh', ['release', 'upload', anchor.tag, file, '--repo', REPO]);
123
+ } finally {
124
+ fs.rmSync(temp, { recursive: true, force: true });
125
+ }
126
+ },
127
+
128
+ async readReceipt(draft, sequence) {
129
+ const name = `${RECEIPT_PREFIX}${String(sequence).padStart(4, '0')}.json`;
130
+ const release = releaseById((draft || activeDraft).id);
131
+ const asset = release.assets?.find((item) => item.name === name);
132
+ if (!asset) throw new Error(`remote receipt ${name} is missing after upload`);
133
+ return assetReceipt(asset);
134
+ },
135
+
136
+ async uploadAssets(draft, assets) {
137
+ const paths = [assets.bundlePath, assets.bundleSignaturePath, assets.bundleDigestPath, assets.packagePath];
138
+ for (const file of paths) {
139
+ const release = releaseById(draft.id);
140
+ const existing = release.assets?.find(({ name }) => name === path.basename(file));
141
+ if (existing) {
142
+ if (sha256(assetBytes(existing)) !== sha256(fs.readFileSync(file))) {
143
+ throw new Error(`refusing to replace staged asset with different bytes: ${existing.name}`);
144
+ }
145
+ continue;
146
+ }
147
+ command('gh', ['release', 'upload', draft.tag, file, '--repo', REPO]);
148
+ }
149
+ },
150
+
151
+ async materializeStagedAssets(draft, identity) {
152
+ return materializeRemoteAssets(draft, identity);
153
+ },
154
+
155
+ async stageNpm(identity, packagePath) {
156
+ const existing = maybe(() => command('npm', ['view', `${PACKAGE}@${identity.version}`, 'dist.integrity']), null);
157
+ if (existing) return;
158
+ command('npm', ['publish', packagePath, '--tag', `candidate-v${identity.version}`]);
159
+ },
160
+
161
+ async observeNpmCandidate(identity) {
162
+ const metadata = json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']);
163
+ return { version: metadata.version, integrity: metadata.dist?.integrity, tag: `candidate-v${identity.version}` };
164
+ },
165
+
166
+ async publishDraftNonLatest(draft) {
167
+ command('gh', ['api', '-X', 'PATCH', `repos/${REPO}/releases/${draft.id}`, '-F', 'draft=false', '-f', 'make_latest=false']);
168
+ },
169
+
170
+ async observeGithub(identity) {
171
+ const release = json('gh', ['api', `repos/${REPO}/releases/tags/${identity.tag}`]);
172
+ const latest = maybe(() => json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name, null);
173
+ return { tag: release.tag_name, sha: tagSha(identity.tag, root), latest: latest === identity.tag };
174
+ },
175
+
176
+ async promoteNpm(identity) {
177
+ command('npm', ['dist-tag', 'add', `${PACKAGE}@${identity.version}`, 'latest']);
178
+ },
179
+ async observeNpmLatest() {
180
+ return { version: command('npm', ['view', `${PACKAGE}@latest`, 'version']) };
181
+ },
182
+ async makeGithubLatest(draft) {
183
+ command('gh', ['api', '-X', 'PATCH', `repos/${REPO}/releases/${draft.id}`, '-f', 'make_latest=true']);
184
+ },
185
+ async observeGithubLatest() {
186
+ return { tag: json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name };
187
+ },
188
+ async restoreNpmLatest(prior, expected) {
189
+ const current = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
190
+ if (current !== expected) throw new Error(`refusing compensation: npm latest is ${current}, expected ${expected}`);
191
+ command('npm', ['dist-tag', 'add', `${PACKAGE}@${prior}`, 'latest']);
192
+ },
193
+ async finalize(identity, receipt, hostVerifier) {
194
+ const staged = materializeRemoteAssets(activeDraft, identity);
195
+ let hosts;
196
+ try {
197
+ hosts = await hostVerifier.verify({ source: 'final', identity, assets: staged.assets });
198
+ } finally {
199
+ staged.cleanup();
200
+ }
201
+ if (hosts.verdict !== 'PASS') return { verdict: 'FAIL', hosts };
202
+ if (!candidateReceipt || !publicationReceipt) {
203
+ throw new Error('final convergence requires candidate and publication receipt paths');
204
+ }
205
+ if (!fs.existsSync(path.resolve(root, publicationReceipt))) {
206
+ const publication = spawnSync(process.execPath, [
207
+ 'scripts/publication-receipt.mjs', '--candidate', candidateReceipt, '--out', publicationReceipt,
208
+ ], { cwd: root, encoding: 'utf8', timeout: 1_200_000 });
209
+ if (publication.error || publication.status !== 0) {
210
+ return { verdict: 'FAIL', hosts, publicationError: String(publication.stderr || publication.error?.message) };
211
+ }
212
+ }
213
+ const seal = spawnSync(process.execPath, [
214
+ 'scripts/release-proof.mjs', '--candidate', candidateReceipt, '--publication', publicationReceipt,
215
+ ], { cwd: root, encoding: 'utf8', timeout: 300_000 });
216
+ if (seal.error || seal.status !== 0) {
217
+ return { verdict: 'FAIL', hosts, sealError: String(seal.stderr || seal.error?.message) };
218
+ }
219
+ const channels = spawnSync(process.execPath, ['scripts/verify-channels.mjs'], {
220
+ cwd: root, encoding: 'utf8', timeout: 600_000,
221
+ });
222
+ if (channels.error || channels.status !== 0) {
223
+ return { verdict: 'FAIL', hosts, channelError: String(channels.stderr || channels.error?.message) };
224
+ }
225
+ const probe = spawnSync(process.execPath, ['scripts/published-surface-probe.mjs', '--json'], {
226
+ cwd: root, encoding: 'utf8', timeout: 600_000,
227
+ });
228
+ let observed = null;
229
+ try { observed = JSON.parse(probe.stdout || ''); } catch {}
230
+ return {
231
+ verdict: probe.status === 0 && observed?.verdict === 'PASS' ? 'PASS' : 'FAIL',
232
+ hosts, surface: observed, publicationReceipt, previousReceiptDigest: receipt.receiptDigest,
233
+ };
234
+ },
235
+ };
236
+ }
@@ -0,0 +1,214 @@
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
+ const canonical = (value) => {
8
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
9
+ if (value && typeof value === 'object') {
10
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
11
+ }
12
+ return JSON.stringify(value);
13
+ };
14
+
15
+ export const receiptPayload = (receipt) => {
16
+ const { signature: _signature, receiptDigest: _digest, ...payload } = receipt;
17
+ return canonical(payload);
18
+ };
19
+
20
+ export const digestReceipt = (receipt) => crypto.createHash('sha256')
21
+ .update(receiptPayload(receipt)).digest('hex');
22
+
23
+ export const transactionIdFor = (identity) => crypto.createHash('sha256').update(canonical({
24
+ repository: identity.repository,
25
+ package: identity.package,
26
+ version: identity.version,
27
+ tag: identity.tag,
28
+ candidateSha: identity.candidateSha,
29
+ packageIntegrity: identity.packageIntegrity,
30
+ bundleSha256: identity.bundleSha256,
31
+ })).digest('hex');
32
+
33
+ export function signReceipt(receipt, privateKey) {
34
+ const unsigned = { ...receipt, receiptDigest: digestReceipt(receipt) };
35
+ return {
36
+ ...unsigned,
37
+ signature: crypto.sign(null, Buffer.from(canonical(unsigned)), privateKey).toString('base64'),
38
+ };
39
+ }
40
+
41
+ export function verifyReceipt(receipt, publicKey) {
42
+ if (!receipt || receipt.schemaVersion !== 1 || !/^[a-f0-9]{64}$/.test(receipt.transactionId || '')) {
43
+ throw new Error('invalid release transaction receipt');
44
+ }
45
+ const { signature, receiptDigest, ...unsigned } = receipt;
46
+ if (digestReceipt(unsigned) !== receiptDigest) throw new Error('release receipt digest mismatch');
47
+ if (!crypto.verify(null, Buffer.from(canonical({ ...unsigned, receiptDigest })), publicKey, Buffer.from(signature || '', 'base64'))) {
48
+ throw new Error('release receipt signature mismatch');
49
+ }
50
+ return receipt;
51
+ }
52
+
53
+ export function validateReceiptChain(receipts, identity, publicKey) {
54
+ const expectedId = transactionIdFor(identity);
55
+ const sorted = [...receipts].sort((a, b) => a.sequence - b.sequence);
56
+ let previous = null;
57
+ for (let index = 0; index < sorted.length; index += 1) {
58
+ const receipt = verifyReceipt(sorted[index], publicKey);
59
+ if (receipt.transactionId !== expectedId || canonical(receipt.identity) !== canonical(identity)) {
60
+ throw new Error('release receipt identity conflict');
61
+ }
62
+ if (receipt.sequence !== index) throw new Error('release receipt sequence gap or replay');
63
+ if ((receipt.previousReceiptDigest || null) !== (previous?.receiptDigest || null)) {
64
+ throw new Error('release receipt chain conflict');
65
+ }
66
+ previous = receipt;
67
+ }
68
+ return sorted;
69
+ }
70
+
71
+ const stateReceipt = ({ identity, prior, state, fence, observation = {}, privateKey }) => signReceipt({
72
+ schemaVersion: 1,
73
+ transactionId: transactionIdFor(identity),
74
+ sequence: prior ? prior.sequence + 1 : 0,
75
+ previousReceiptDigest: prior?.receiptDigest || null,
76
+ state,
77
+ fence,
78
+ identity,
79
+ observation,
80
+ createdAt: new Date().toISOString(),
81
+ }, privateKey);
82
+
83
+ const exact = (actual, expected, label) => {
84
+ if (actual !== expected) throw new Error(`${label} mismatch: ${actual ?? '(missing)'} != ${expected}`);
85
+ };
86
+
87
+ export async function runReleaseTransaction({ identity, assets, adapter, privateKey, publicKey, hostVerifier }) {
88
+ const expectedId = transactionIdFor(identity);
89
+ const discovered = await adapter.discover(identity);
90
+ const competing = discovered.pending?.filter((item) => item.transactionId !== expectedId) || [];
91
+ if (competing.length) throw new Error(`pending release ${competing[0].transactionId} blocks ${expectedId}`);
92
+ if ((discovered.matchingDrafts || []).length > 1) throw new Error('duplicate matching drafts require reconciliation');
93
+
94
+ let chain = validateReceiptChain(discovered.receipts || [], identity, publicKey);
95
+ let current = chain.at(-1) || null;
96
+ const fence = current?.fence || discovered.fence || crypto.randomUUID();
97
+ const draft = discovered.matchingDrafts?.[0] || await adapter.createDraft(identity, fence);
98
+ const completed = new Set(chain.map(({ state }) => state));
99
+ const append = async (state, observation = {}) => {
100
+ const receipt = stateReceipt({ identity, prior: current, state, fence, observation, privateKey });
101
+ await adapter.appendReceipt(draft, receipt, `${RECEIPT_PREFIX}${String(receipt.sequence).padStart(4, '0')}.json`);
102
+ const observed = await adapter.readReceipt(draft, receipt.sequence);
103
+ verifyReceipt(observed, publicKey);
104
+ exact(observed.receiptDigest, receipt.receiptDigest, 'remote receipt');
105
+ current = receipt;
106
+ completed.add(state);
107
+ return receipt;
108
+ };
109
+ const intend = async (state, observation = {}) => {
110
+ if (!completed.has(state)) await append(state, observation);
111
+ };
112
+
113
+ if (!current) await append('remote-prepared', { draftId: draft.id, prior: discovered.prior });
114
+ if (TERMINAL_STATES.has(current.state)) return current;
115
+ if (current.fence !== fence) throw new Error('stale release transaction fence');
116
+
117
+ if (!completed.has('local-hosts-verified')) {
118
+ await intend('asset-upload-intent');
119
+ await adapter.uploadAssets(draft, assets, identity);
120
+ await intend('host-verification-intent', { source: 'sealed-local-assets' });
121
+ const localHosts = await hostVerifier.verify({ source: 'local', identity, assets });
122
+ if (localHosts.verdict !== 'PASS') throw new Error('local staged host verification failed');
123
+ await append('local-hosts-verified', { hosts: localHosts });
124
+ }
125
+
126
+ if (!completed.has('npm-candidate-staged')) {
127
+ await intend('npm-stage-intent');
128
+ await adapter.stageNpm(identity, assets.packagePath);
129
+ const stagedNpm = await adapter.observeNpmCandidate(identity);
130
+ exact(stagedNpm.version, identity.version, 'npm candidate version');
131
+ exact(stagedNpm.integrity, identity.packageIntegrity, 'npm candidate integrity');
132
+ await append('npm-candidate-staged', { npm: stagedNpm });
133
+ }
134
+
135
+ if (!completed.has('prepared')) {
136
+ await intend('remote-host-verification-intent');
137
+ const staged = await adapter.materializeStagedAssets(draft, identity);
138
+ try {
139
+ const remoteHosts = await hostVerifier.verify({ source: 'staged', identity, assets: staged.assets, draft });
140
+ if (remoteHosts.verdict !== 'PASS') throw new Error('remote staged host verification failed');
141
+ await append('prepared', { hosts: remoteHosts });
142
+ } finally {
143
+ staged.cleanup();
144
+ }
145
+ }
146
+
147
+ if (!completed.has('github-promoted-nonlatest')) {
148
+ await intend('github-promote-intent');
149
+ await adapter.publishDraftNonLatest(draft, identity);
150
+ const github = await adapter.observeGithub(identity);
151
+ exact(github.sha, identity.candidateSha, 'GitHub candidate SHA');
152
+ if (github.latest) throw new Error('GitHub candidate advanced latest before npm convergence');
153
+ await append('github-promoted-nonlatest', { github });
154
+ }
155
+
156
+ if (!completed.has('npm-promoted')) {
157
+ await intend('npm-promote-intent');
158
+ try {
159
+ await adapter.promoteNpm(identity);
160
+ } catch (error) {
161
+ throw new Error(`npm promotion pending for ${identity.version}: ${error.message}`);
162
+ }
163
+ const npmLatest = await adapter.observeNpmLatest();
164
+ exact(npmLatest.version, identity.version, 'npm latest');
165
+ await append('npm-promoted', { npm: npmLatest });
166
+ }
167
+
168
+ // A failed GitHub-latest promotion is compensated back to A. The historical npm-promoted
169
+ // receipt remains true, but is no longer the current external state, so a clean-runner retry
170
+ // must explicitly re-promote B before it may retry GitHub.
171
+ if (current.state === 'compensated') {
172
+ await append('npm-repromote-intent');
173
+ await adapter.promoteNpm(identity);
174
+ const npmLatest = await adapter.observeNpmLatest();
175
+ exact(npmLatest.version, identity.version, 'npm latest after compensation retry');
176
+ await append('npm-repromoted', { npm: npmLatest });
177
+ }
178
+
179
+ if (!completed.has('defaults-promoted')) {
180
+ await intend('github-latest-intent');
181
+ try {
182
+ await adapter.makeGithubLatest(draft, identity);
183
+ } catch (error) {
184
+ const observed = await adapter.observeNpmLatest();
185
+ if (observed.version !== identity.version) throw new Error('npm latest changed during compensation');
186
+ await intend('compensation-intent', { restore: discovered.prior?.npmLatest });
187
+ await adapter.restoreNpmLatest(discovered.prior?.npmLatest, identity.version);
188
+ exact((await adapter.observeNpmLatest()).version, discovered.prior?.npmLatest, 'npm compensation');
189
+ await append('compensated', { reason: error.message });
190
+ throw new Error(`GitHub latest promotion failed; npm compensated: ${error.message}`);
191
+ }
192
+ const githubLatest = await adapter.observeGithubLatest();
193
+ exact(githubLatest.tag, identity.tag, 'GitHub latest');
194
+ await append('defaults-promoted', { github: githubLatest });
195
+ }
196
+
197
+ await intend('finalize-intent');
198
+ const final = await adapter.finalize(identity, current, hostVerifier);
199
+ if (final.verdict !== 'PASS') throw new Error('final release convergence failed');
200
+ return append('channels-converged', final);
201
+ }
202
+
203
+ export async function abortReleaseTransaction({ identity, receipts, reason, authorized, adapter, privateKey, publicKey }) {
204
+ if (!authorized) throw new Error('release abort requires explicit human authorization');
205
+ const chain = validateReceiptChain(receipts, identity, publicKey);
206
+ const current = chain.at(-1);
207
+ if (!current || TERMINAL_STATES.has(current.state)) throw new Error('release transaction is not abortable');
208
+ const receipt = stateReceipt({
209
+ identity, prior: current, state: 'aborted', fence: current.fence,
210
+ observation: { reason, authorized: true }, privateKey,
211
+ });
212
+ await adapter.appendReceipt(null, receipt, `${RECEIPT_PREFIX}${String(receipt.sequence).padStart(4, '0')}.json`);
213
+ return receipt;
214
+ }