ruvnet-brain 4.3.25 → 4.3.26

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 CHANGED
@@ -7,7 +7,7 @@ Created: 2026-06-29 22:36:38 EDT
7
7
 
8
8
  # 🧠 RuvNet Brain
9
9
 
10
- ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.3.25 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.25-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
10
+ ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.3.26 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.26-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
11
11
 
12
12
  **A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
13
13
 
package/console/app.js CHANGED
@@ -2973,6 +2973,53 @@ function bpField(bp) {
2973
2973
  return (bp && Array.isArray(bp.schema) && bp.schema[0]) || null;
2974
2974
  }
2975
2975
 
2976
+ /* ------------------------------------------------------------ inventory */
2977
+
2978
+ // Renders /api/state's `inventory` section: the installed brain's stores BY NAME, joined with their
2979
+ // upstream repo and build commit. Ranked by recency (server-side) — never alphabetical. Read-only.
2980
+ function renderInventory(inv) {
2981
+ const body = $('#body-inventory');
2982
+ if (!body) return;
2983
+ if (!inv || inv.available !== true) {
2984
+ setChips('chips-inventory', [chip('can’t read the manifest', 'nt')]);
2985
+ body.replaceChildren(el('p', { class: 'muted' },
2986
+ inv && inv.reason ? inv.reason : 'The console could not read RVF-GENERATIONS.json from the installed brain, so it is not guessing a list.'));
2987
+ return;
2988
+ }
2989
+ const chips = [chip(`${fmtInt(inv.count)} repositories`, 'green')];
2990
+ if (inv.privateCount > 0) chips.push(chip(`${fmtInt(inv.privateCount)} private`, 'cyan', 'Fenced private stores — never leave this machine'));
2991
+ if (inv.releaseTag) chips.push(chip(`generation ${inv.releaseTag}`, 'grey'));
2992
+ setChips('chips-inventory', chips);
2993
+
2994
+ const mb = inv.totalBytes ? `${(inv.totalBytes / 1e6).toFixed(0)} MB on disk` : null;
2995
+ const summary = el('p', { class: 'muted' },
2996
+ `${fmtInt(inv.count)} searchable stores`,
2997
+ inv.withProvenance != null ? `, ${fmtInt(inv.withProvenance)} with a recorded upstream commit` : '',
2998
+ mb ? `, ${mb}.` : '.');
2999
+
3000
+ const rows = (inv.stores || []).map((s) => el('li', { class: 'inv-row', 'data-name': s.name.toLowerCase() },
3001
+ el('span', { class: 'mono inv-name' }, s.name),
3002
+ s.private ? chip('private', 'cyan') : null,
3003
+ s.sourceRepo && /^https?:\/\//.test(s.sourceRepo)
3004
+ ? el('a', { class: 'inv-src', href: s.sourceRepo, target: '_blank', rel: 'noopener noreferrer' }, s.sourceRepo.replace(/^https?:\/\/(www\.)?github\.com\//, ''))
3005
+ : el('span', { class: 'muted inv-src' }, s.sourceRepo || '—'),
3006
+ el('span', { class: 'mono muted inv-meta' },
3007
+ s.sourceCommit ? `@${s.sourceCommit}` : '', s.builtUtc ? ` · built ${String(s.builtUtc).slice(0, 10)}` : ''),
3008
+ ));
3009
+ const list = el('ul', { class: 'inv-list', 'aria-label': 'Repositories in the brain' }, ...rows);
3010
+ const filter = el('input', {
3011
+ class: 'inv-filter', type: 'search', placeholder: 'Filter by name…', 'aria-label': 'Filter repositories by name',
3012
+ oninput: (e) => {
3013
+ const q = String(e.target.value || '').trim().toLowerCase();
3014
+ let shown = 0;
3015
+ for (const li of list.children) { const hit = !q || li.dataset.name.includes(q); li.hidden = !hit; if (hit) shown += 1; }
3016
+ count.textContent = q ? `${fmtInt(shown)} of ${fmtInt(inv.count)} match` : '';
3017
+ },
3018
+ });
3019
+ const count = el('span', { class: 'muted inv-count', 'aria-live': 'polite' });
3020
+ body.replaceChildren(summary, el('div', { class: 'inv-tools' }, filter, count), list);
3021
+ }
3022
+
2976
3023
  function renderBrainPower(bp) {
2977
3024
  const body = $('#body-brain');
2978
3025
  if (!body) return;
@@ -3663,6 +3710,7 @@ async function loadState({ landed = false } = {}) {
3663
3710
  renderHost(state.host, state.generatedAt);
3664
3711
  const s = state.sections || {};
3665
3712
  renderBrainPower(s.brainPower);
3713
+ renderInventory(s.inventory);
3666
3714
  renderWiring(s.wiring);
3667
3715
  lastMemory = s.memory;
3668
3716
  renderMemory(s.memory);
@@ -108,6 +108,25 @@
108
108
  </div>
109
109
  </section>
110
110
 
111
+ <!-- The one thing this page never showed: WHICH repositories are in the brain, by name. It always
112
+ carried the count and never the list. Read-only mirror of the installed manifest. -->
113
+ <section class="card rail-mirror" id="card-inventory" aria-labelledby="card-inventory-h">
114
+ <div class="qgroup-head">
115
+ <span class="card-ic ic-mirror" aria-hidden="true">
116
+ <svg viewBox="0 0 24 24"><path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h10"/></svg>
117
+ </span>
118
+ <span class="card-head">
119
+ <span class="stage st-mirror">Mirror</span>
120
+ <h2 id="card-inventory-h">What’s in your brain</h2>
121
+ <span class="q">Every repository the brain can search right now, by name — where it came from and when it was built.</span>
122
+ </span>
123
+ <span class="chips" id="chips-inventory"><span class="chip tone-wait">counting…</span></span>
124
+ </div>
125
+ <div class="body" id="body-inventory">
126
+ <div class="skeleton" aria-hidden="true"><div class="sk-bar w40"></div><div class="sk-bar w75"></div></div>
127
+ </div>
128
+ </section>
129
+
111
130
  <!-- ======================================================= HERO -->
112
131
  <section class="intro">
113
132
  <div class="intro-grid">
package/console/style.css CHANGED
@@ -2147,3 +2147,29 @@ details.sub .sub-body > *:first-child { margin-top: 4px; }
2147
2147
  #body-brain .fn-path {
2148
2148
  font-family: var(--mono); font-size: 0.86em; color: var(--faint); overflow-wrap: anywhere;
2149
2149
  }
2150
+
2151
+ /* ------------------------------------------------------------ inventory card
2152
+ The repository list, by name. Same row language as .cap-row (hairline separators, quiet meta),
2153
+ with a type-to-filter above it because 200+ names need a way in. Long upstream URLs wrap rather
2154
+ than overflow the card. */
2155
+ .inv-tools { display: flex; align-items: center; gap: 12px; margin: 10px 0 8px; flex-wrap: wrap; }
2156
+ .inv-filter {
2157
+ flex: 1 1 220px; min-width: 0; padding: 7px 10px; font: inherit; font-size: 13.5px;
2158
+ color: var(--ink); background: var(--panel-2, transparent);
2159
+ border: 1px solid var(--hair-strong); border-radius: 8px;
2160
+ }
2161
+ .inv-filter:focus { outline: 2px solid var(--cyan); outline-offset: 1px; }
2162
+ .inv-count { font-size: 12.5px; }
2163
+ .inv-list { list-style: none; margin: 0; padding: 0; max-height: 28rem; overflow: auto; border-top: 1px solid var(--hair); }
2164
+ .inv-row {
2165
+ display: grid; grid-template-columns: minmax(10ch, 16ch) auto 1fr auto; align-items: baseline; gap: 6px 12px;
2166
+ padding: 7px 4px; border-bottom: 1px solid var(--hair); font-size: 13.5px;
2167
+ }
2168
+ .inv-row[hidden] { display: none; }
2169
+ .inv-name { font-size: 13px; overflow-wrap: anywhere; }
2170
+ .inv-src { min-width: 0; overflow-wrap: anywhere; font-size: 12.5px; }
2171
+ .inv-meta { font-size: 12px; white-space: nowrap; }
2172
+ @media (max-width: 640px) {
2173
+ .inv-row { grid-template-columns: 1fr auto; }
2174
+ .inv-src, .inv-meta { grid-column: 1 / -1; white-space: normal; }
2175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.25",
3
+ "version": "4.3.26",
4
4
  "description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
3
  "description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
4
- "version": "4.3.25",
4
+ "version": "4.3.26",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.25",
3
+ "version": "4.3.26",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -729,6 +729,50 @@ export function reconcileNightly({ choice, schedule }) {
729
729
  };
730
730
  }
731
731
 
732
+ // The one thing the console never showed: WHICH repositories are in the brain, by name. It has
733
+ // always carried the count (206) and never the inventory — the owner asked for this list more than
734
+ // a dozen times. RVF-GENERATIONS.json is the manifest that matches disk exactly (verified 206 = 206);
735
+ // SOURCE.json is the provenance subset (104) that carries the upstream URL. Join on store name,
736
+ // never restate either. Ranked by build recency — never alphabetical (relevance-ordering rule).
737
+ function gatherInventory() {
738
+ const generations = readJSON(path.join(INSTALLED_KB, 'RVF-GENERATIONS.json'));
739
+ const source = readJSON(path.join(INSTALLED_KB, 'SOURCE.json'));
740
+ const stores = generations?.stores && typeof generations.stores === 'object' ? generations.stores : null;
741
+ if (!stores) return { available: false, reason: 'RVF-GENERATIONS.json not readable in the installed brain', count: 0, stores: [] };
742
+ // SOURCE.json has carried `stores` as an array in some generations and a name-keyed object in
743
+ // others; accept both rather than silently joining nothing (measured 2026-09-16: object form,
744
+ // and the array-only read reported withProvenance=0 for a brain with 104 provenance rows).
745
+ const provenance = new Map();
746
+ const sourceRows = Array.isArray(source?.stores) ? source.stores
747
+ : source?.stores && typeof source.stores === 'object'
748
+ ? Object.entries(source.stores).map(([name, row]) => ({ name, ...row })) : [];
749
+ for (const row of sourceRows) {
750
+ if (row?.name) provenance.set(String(row.name).toLowerCase(), row);
751
+ }
752
+ const rows = Object.entries(stores).map(([name, gen]) => {
753
+ const prov = provenance.get(name.toLowerCase());
754
+ return {
755
+ name,
756
+ sourceRepo: prov?.sourceRepo || null,
757
+ sourceCommit: gen?.sourceCommit ? String(gen.sourceCommit).slice(0, 7) : null,
758
+ builtUtc: gen?.builtUtc || null,
759
+ bytes: Number(gen?.bytes) || 0,
760
+ model: gen?.model || null,
761
+ private: prov?.updateManaged === false,
762
+ };
763
+ }).sort((a, b) => String(b.builtUtc || '').localeCompare(String(a.builtUtc || '')) || a.name.localeCompare(b.name));
764
+ return {
765
+ available: true,
766
+ count: rows.length,
767
+ withProvenance: rows.filter((r) => r.sourceRepo).length,
768
+ privateCount: rows.filter((r) => r.private).length,
769
+ brainVersion: generations.brainVersion || null,
770
+ releaseTag: generations.releaseTag || null,
771
+ totalBytes: rows.reduce((sum, r) => sum + r.bytes, 0),
772
+ stores: rows,
773
+ };
774
+ }
775
+
732
776
  function gatherConfig() {
733
777
  const cfg = readJSON(CONFIG_PATH) || {};
734
778
  const credential = openRouterCredentialStatus({ cwd: process.cwd() });
@@ -2268,7 +2312,7 @@ function gatherState(cwd, { fleet = true } = {}) {
2268
2312
  generatedAt: new Date().toISOString(),
2269
2313
  preStateHash,
2270
2314
  host: { user: os.userInfo().username, platform: process.platform, node: process.version, npmPrefix: NPM_PREFIX.replace(SYSTEM_HOME, '~'), brainVersion: brainVersionOnDisk() },
2271
- sections: { wiring, memory, savings, config, userSettings, brainPower, gates, recommendations },
2315
+ sections: { wiring, memory, savings, config, userSettings, brainPower, gates, recommendations, inventory: gatherInventory() },
2272
2316
  };
2273
2317
  // Cache the last good state so repeat page-loads paint instantly, same as the stack audit does.
2274
2318
  // TOKEN is per-server-run and must never touch disk — ?fast=1 splices the live one back in.
@@ -21,6 +21,11 @@ const json = (name, args, options) => JSON.parse(command(name, args, options));
21
21
  // night a corpus round shipped. Every question this provider asks is about the CODE generation, so
22
22
  // it must resolve the latest CODE release rather than read a pointer that now answers differently.
23
23
  const latestCodeTag = () => latestCodeReleaseTag(json('gh', ['api', `repos/${REPO}/releases?per_page=30`]));
24
+ // Two DIFFERENT questions, deliberately: latestCodeTag() is "which CODE generation is newest" (a
25
+ // corpus generation may hold the pointer — designed steady state); githubPointerTag() is "what does
26
+ // GitHub's releases/latest actually point at right now". The publish-github-nonlatest gate needs the
27
+ // second and was reading the first, which made it unsatisfiable for every freshly published release.
28
+ const githubPointerTag = () => maybe(() => json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name, null);
24
29
  const maybe = (callback, fallback = null) => {
25
30
  try { return callback(); } catch { return fallback; }
26
31
  };
@@ -79,14 +84,26 @@ const sha256File = (file) => {
79
84
  return hash.digest('hex');
80
85
  };
81
86
  const assetDigest = (asset) => withTempAsset(asset, sha256File);
87
+ // 4.3.25 run 35056636514: `npm publish` succeeded at ~04:47:31Z and the registry first showed the
88
+ // version at 04:53:40Z (time.modified) — roughly six minutes. The poll gave up at 156s, the job
89
+ // reported FAILURE, and an already-published release was left half-promoted with npm and GitHub
90
+ // naming different generations (the exact #77 split). The action was never the problem; the
91
+ // deadline was shorter than npm's propagation for this package. Ten minutes covers the observed
92
+ // ~6 with margin; the env overrides remain for a runner that knows better.
82
93
  const OBSERVATION_POLICY = {
83
- maxElapsedMs: Number(process.env.RUVNET_NPM_VISIBILITY_TIMEOUT_MS || 180_000),
84
- maxAttempts: Number(process.env.RUVNET_NPM_VISIBILITY_ATTEMPTS || 14),
94
+ maxElapsedMs: Number(process.env.RUVNET_NPM_VISIBILITY_TIMEOUT_MS || 600_000),
95
+ maxAttempts: Number(process.env.RUVNET_NPM_VISIBILITY_ATTEMPTS || 40),
85
96
  initialDelayMs: 1_000,
86
97
  maxDelayMs: 15_000,
87
98
  multiplier: 1.8,
88
99
  jitter: (delay) => Math.floor(Math.random() * Math.min(1_000, delay * 0.2)),
89
100
  };
101
+ // The one command in this file that mutates a public channel gets its stderr in the job log and a
102
+ // timeout sized for a real registry upload. command()'s defaults — stderr piped and dropped, 30s —
103
+ // meant 4.3.25's first run left NO diagnostic between "stage-npm" and the deadline error, and a slow
104
+ // registry would have killed a legitimate publish mid-upload. Named, so the publish call stays one
105
+ // pinnable line for tests/unit/release-lineage + publication-receipt-wiring (one publisher, one tag).
106
+ const PUBLISH_COMMAND_OPTIONS = Object.freeze({ stdio: ['ignore', 'pipe', 'inherit'], timeout: 600_000 });
90
107
 
91
108
  export function selectCurrentReleaseBytes({ remoteBytes, localBytes, generatedBytes, identity }) {
92
109
  const bytes = remoteBytes || localBytes || generatedBytes;
@@ -263,6 +280,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
263
280
  try {
264
281
  const release = draft?.id ? hydratedRelease(releaseById(draft.id)) : null;
265
282
  const latestTag = latestCodeTag();
283
+ const pointerTag = githubPointerTag();
266
284
  const candidate = maybe(() => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']), null);
267
285
  const exactVersion = maybe(() => json('npm', ['view', `${PACKAGE}@${identity.version}`, '--json']), null);
268
286
  const latestVersion = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
@@ -295,8 +313,10 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
295
313
  published: release.draft === false,
296
314
  latest: latestTag === identity.tag,
297
315
  latestTag,
316
+ // What releases/latest actually points at — distinct from `latest` (code recency) above.
317
+ pointerTag,
298
318
  assetsExact: assetsExactFor(release, identity, forceAssets),
299
- } : { tag: null, sha: null, draft: false, published: false, latest: false, assetsExact: false },
319
+ } : { tag: null, sha: null, draft: false, published: false, latest: false, pointerTag, assetsExact: false },
300
320
  publicReceiptExact,
301
321
  publicHostsExact,
302
322
  };
@@ -400,7 +420,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
400
420
  command('npm', ['dist-tag', 'add', `${PACKAGE}@${identity.version}`, `candidate-v${identity.version}`]);
401
421
  return;
402
422
  }
403
- command('npm', ['publish', packagePath, '--tag', `candidate-v${identity.version}`]);
423
+ command('npm', ['publish', packagePath, '--tag', `candidate-v${identity.version}`], PUBLISH_COMMAND_OPTIONS);
404
424
  },
405
425
 
406
426
  async observeNpmCandidate(identity) {
@@ -437,7 +457,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
437
457
  // legitimately holding `releases/latest` is NOT that — it is the designed steady state between
438
458
  // code releases — so the comparison is made against the latest CODE release, and the pointer's
439
459
  // own kind is recorded rather than treated as a conflict.
440
- const pointer = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
460
+ const pointer = githubPointerTag();
441
461
  const current = latestCodeTag();
442
462
  if (current !== expectedPrior && current !== identity.tag) {
443
463
  throw new Error(`refusing GitHub promotion: latest code release is ${current}, expected ${expectedPrior}`);
@@ -194,6 +194,25 @@ const npmCandidateExact = (snapshot, identity) => snapshot?.npm?.candidateVersio
194
194
  const npmLatestIsB = (snapshot, identity) => snapshot?.npm?.latestVersion === identity.version;
195
195
  const githubIsB = (snapshot, identity) => snapshot?.github?.sha === identity.candidateSha
196
196
  && snapshot?.github?.tag === identity.tag;
197
+ // TWO DIFFERENT QUESTIONS ABOUT "LATEST", ANSWERED BY TWO DIFFERENT FIELDS (4.3.25, 2026-09-16).
198
+ //
199
+ // `snapshot.github.latest` is "is this the newest CODE release" — observeGithub derives it from
200
+ // latestCodeReleaseTag(), deliberately, because per ADR-086 S1 a corpus generation may legitimately
201
+ // hold GitHub's releases/latest pointer and a code release must not observe itself as demoted.
202
+ // `snapshot.github.pointerTag` is what releases/latest actually points at.
203
+ //
204
+ // Every promotion, recovery, verification and abort check in this file wants the SECOND question:
205
+ // "has the pointer been moved to B yet". Ten sites read the first. A freshly published code release
206
+ // is ALWAYS the newest code release, so `!latest` was unsatisfiable (publish gate, resume clause —
207
+ // the transaction wedged at github-promote-intent), `latest` was trivially true (promotion looked
208
+ // already done, the pointer never moved, converge and finalize passed with it unmoved), and abort
209
+ // refused because "latest" had moved. Ask the pointer. Snapshots without one (recorded receipts,
210
+ // older providers) fall back to code recency so history stays interpretable.
211
+ const githubPointerIsB = (snapshot, identity) => (snapshot?.github?.pointerTag !== undefined
212
+ ? snapshot.github.pointerTag === identity.tag
213
+ : snapshot?.github?.latest === true);
214
+ const githubPointerTag = (snapshot) => (snapshot?.github?.pointerTag !== undefined
215
+ ? snapshot.github.pointerTag : snapshot?.github?.latestTag);
197
216
 
198
217
  /**
199
218
  * Pure, total next-action selector. Receipt history is an audit chain, never permission to skip a
@@ -206,7 +225,7 @@ export function reduceReleaseState({ lastReceipt, snapshot, identity, prior }) {
206
225
  const npmB = npmLatestIsB(snapshot, identity);
207
226
  const githubB = githubIsB(snapshot, identity);
208
227
  const githubPublished = githubB && snapshot.github?.published === true;
209
- const githubLatest = githubPublished && snapshot.github?.latest === true;
228
+ const githubLatest = githubPublished && githubPointerIsB(snapshot, identity);
210
229
 
211
230
  if (snapshot.npm?.candidateVersion === identity.version && snapshot.npm?.candidateIntegrity
212
231
  && snapshot.npm.candidateIntegrity !== identity.packageIntegrity) {
@@ -338,8 +357,11 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
338
357
  await transition('prepared', { recovered: true });
339
358
  continue;
340
359
  }
360
+ // 4.3.25 run 35109036116: run 2 died after writing github-promote-intent, run 3 found the draft
361
+ // published, the reducer asked for promote-npm — not a legal edge from here. This clause was the
362
+ // only legal exit and its `!latest` was unsatisfiable. Wedged.
341
363
  if (current.state === 'github-promote-intent' && githubIsB(snapshot, identity)
342
- && snapshot.github?.published && !snapshot.github?.latest) {
364
+ && snapshot.github?.published && !githubPointerIsB(snapshot, identity)) {
343
365
  await transition('github-promoted-nonlatest', { github: snapshot.github, recovered: true });
344
366
  continue;
345
367
  }
@@ -348,7 +370,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
348
370
  continue;
349
371
  }
350
372
  if (current.state === 'github-latest-intent' && npmLatestIsB(snapshot, identity)
351
- && githubIsB(snapshot, identity) && snapshot.github?.latest) {
373
+ && githubIsB(snapshot, identity) && githubPointerIsB(snapshot, identity)) {
352
374
  await transition('defaults-promoted', { npm: snapshot.npm, github: snapshot.github, recovered: true });
353
375
  continue;
354
376
  }
@@ -357,7 +379,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
357
379
  throw new Error('npm compensation recovered; resume the same transaction');
358
380
  }
359
381
  if (current.state === 'finalize-intent' && npmCandidateExact(snapshot, identity) && npmLatestIsB(snapshot, identity)
360
- && githubIsB(snapshot, identity) && snapshot.github?.published && snapshot.github?.latest
382
+ && githubIsB(snapshot, identity) && snapshot.github?.published && githubPointerIsB(snapshot, identity)
361
383
  && snapshot.github?.assetsExact) {
362
384
  return append('channels-converged', { verdict: 'PUBLISHED_NOT_VERIFIED', recovered: true,
363
385
  npm: snapshot.npm, github: snapshot.github });
@@ -425,7 +447,10 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
425
447
  await transition('github-promote-intent');
426
448
  await adapter.publishDraftNonLatest(draft, identity);
427
449
  const observed = await adapter.observeSnapshot(identity, draft);
428
- if (!(githubIsB(observed, identity) && observed.github.published && !observed.github.latest)) {
450
+ // Published as B, and the POINTER has not moved to B yet — it is promoted later, in
451
+ // make-github-latest. 4.3.25 run 35057738103 published this correctly and then threw here on
452
+ // `!latest`, which a just-published code release can never satisfy.
453
+ if (!(githubIsB(observed, identity) && observed.github.published && !githubPointerIsB(observed, identity))) {
429
454
  throw new Error('GitHub non-latest publication not observed');
430
455
  }
431
456
  await transition('github-promoted-nonlatest', { github: observed.github });
@@ -446,7 +471,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
446
471
  await transition('github-latest-intent');
447
472
  await adapter.makeGithubLatest(draft, identity, prior.githubLatest);
448
473
  const observed = await adapter.observeSnapshot(identity, draft);
449
- if (!(npmLatestIsB(observed, identity) && observed.github?.latest && githubIsB(observed, identity))) {
474
+ if (!(npmLatestIsB(observed, identity) && githubPointerIsB(observed, identity) && githubIsB(observed, identity))) {
450
475
  throw new Error('provider defaults not jointly observed at candidate B');
451
476
  }
452
477
  await transition('defaults-promoted', { npm: observed.npm, github: observed.github });
@@ -469,7 +494,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
469
494
  }
470
495
  await transition('finalize-intent');
471
496
  const reobserved = await adapter.observeSnapshot(identity, draft, { forceAssets: true });
472
- if (!(npmLatestIsB(reobserved, identity) && reobserved.github?.latest && githubIsB(reobserved, identity))) {
497
+ if (!(npmLatestIsB(reobserved, identity) && githubPointerIsB(reobserved, identity) && githubIsB(reobserved, identity))) {
473
498
  throw new Error('provider defaults drifted before channel convergence');
474
499
  }
475
500
  if (!reobserved.github?.assetsExact || !npmCandidateExact(reobserved, identity)) {
@@ -491,8 +516,12 @@ export async function abortReleaseTransaction({ identity, receipts, reason, auth
491
516
  }
492
517
  const snapshot = await adapter.observeSnapshot(identity);
493
518
  const prior = chain[0]?.observation?.prior;
494
- if (!prior || snapshot.npm?.latestVersion === identity.version || snapshot.github?.latest === true
495
- || snapshot.npm?.latestVersion !== prior.npmLatest || snapshot.github?.latestTag !== prior.githubLatest) {
519
+ // "Both defaults still at the captured prior generation" is a question about the npm `latest`
520
+ // dist-tag and GitHub's releases/latest POINTER. A published-but-not-promoted candidate is the
521
+ // newest CODE release, so the old `latest === true` / `latestTag !== prior` reads refused to abort
522
+ // exactly the transaction that most needs aborting: one wedged after publish, before promotion.
523
+ if (!prior || snapshot.npm?.latestVersion === identity.version || githubPointerIsB(snapshot, identity)
524
+ || snapshot.npm?.latestVersion !== prior.npmLatest || githubPointerTag(snapshot) !== prior.githubLatest) {
496
525
  throw new Error('release abort cannot prove both defaults at captured prior generation');
497
526
  }
498
527
  const receipt = stateReceipt({
@@ -550,7 +579,7 @@ export async function finalizeReleaseTransaction({
550
579
  }
551
580
  const snapshot = await adapter.observeSnapshot(identity, discovered.matchingDrafts?.[0], { forceAssets: true });
552
581
  if (snapshot.readError || !npmLatestIsB(snapshot, identity) || !npmCandidateExact(snapshot, identity)
553
- || !githubIsB(snapshot, identity) || snapshot.github?.published !== true || snapshot.github?.latest !== true
582
+ || !githubIsB(snapshot, identity) || snapshot.github?.published !== true || !githubPointerIsB(snapshot, identity)
554
583
  || snapshot.github?.assetsExact !== true) throw new Error('public channels drifted before install verification');
555
584
  const materialized = await adapter.materializePublicVerificationAggregate({ identity, aggregate });
556
585
  const aggregateAssetSha256 = crypto.createHash('sha256').update(canonicalJson(aggregate)).digest('hex');
@@ -642,7 +671,7 @@ export async function abandonPublicVerificationTransaction({ identity, expected,
642
671
  }
643
672
  const snapshot = await adapter.observeSnapshot(identity, discovered.matchingDrafts?.[0], { forceAssets: true });
644
673
  if (snapshot.readError || !npmLatestIsB(snapshot, identity) || !npmCandidateExact(snapshot, identity)
645
- || !githubIsB(snapshot, identity) || snapshot.github?.published !== true || snapshot.github?.latest !== true
674
+ || !githubIsB(snapshot, identity) || snapshot.github?.published !== true || !githubPointerIsB(snapshot, identity)
646
675
  || snapshot.github?.assetsExact !== true
647
676
  || canonicalJson(snapshot.npm) !== canonicalJson(current.observation.npm)
648
677
  || canonicalJson(snapshot.github) !== canonicalJson(current.observation.github)) {