Skip to content

build(release): while a breaking entry is pending, a new version must open a line no release has reached - #348

Merged
nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:build/breaking-not-a-patch
Oct 15, 2026
Merged

nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:build/breaking-not-a-patch

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

While a breaking entry is pending, npm version now refuses any version that doesn't open a release line no release has reached, before it bumps or writes anything. Before 1.0, a breaking change then goes out in a new minor version (0.22.x to 0.23.0), and from 1.0 in a new major.

@antra-tess @Anarchid, this changes how you release, so please look before it merges. It follows #347, the hold check's follow-up, now merged.

Why

A dependent's caret range takes the later versions of its line, so a breaking change released as a patch reaches each dependent whose range takes it at its next fresh install. membrane 0.5.87 opened with a long ### Breaking section and went out as a patch. agent-framework 0.21.1 depends on ^0.5.78 and context-manager 0.14.0 on ^0.5.69, so installs of either took it at their next resolution. Among its changes: code that no longer typechecks against it, and new defaults for tool mode and stop sequences. Released as 0.6.0, it would have reached each dependent through a range bump of the dependent's own, where its breaking entries are read.

The change

  • scripts/release-bump.mjs runs first in preversion, beside the release-hold check. npm gives that hook npm_old_version and npm_new_version before it writes anything. npm 8, 9, 10 and 11.17 all do, checked with npm version patch in a throwaway package. So a refused bump leaves nothing bumped, written or folded.

  • A breaking entry is pending when changelog.d/ holds a *.breaking.md fragment, or when ## Unreleased in CHANGELOG.md has a subsection whose title starts with "Breaking", in any case, with anything in it: bullets, numbered items or prose. Those are what release-changelog.mjs folds into the release, under that subsection's own title, or ### Breaking for a fragment.

  • Lines: a version's line is its minor before 1.0 (0.22.x), its major from 1.0 (1.x), and the version itself for 0.0.z, which ^0.0.z alone takes. While a breaking entry is pending, the new version must be in a line above the current one's. The exception is a current version that is a prerelease opening its line (a preminor before 1.0, a premajor from 1.0). From one of those, any version of that line goes ahead, finishing it included: no release of that line exists for a dependent's range to hold.

  • Prereleases count as the release they lead to (0.22.2-0 as 0.22.2). So a prerelease in the current line is refused. No caret range takes a prerelease, but the version hook would fold the pending entries into its own section, and the next bump would find nothing pending. No stack package has ever published a prerelease, and the version hook can't fold a finished one today: its existing-section check takes ## 0.23.0-1 for ## 0.23.0. This change leaves the hook as it is.

  • A version below the current line is refused too. Dependents hold ranges on the last release, which can sit below an unfinished prerelease. From 0.23.0-0, an explicit 0.22.2 would be the newest 0.22.x.

  • The refusal says why, lists where each pending entry is filed, and gives the bump to run instead. That bump is what npm's minor, or from 1.0 major, gives, which always opens the next line:

    Release refused: a breaking change is pending, and 0.22.1 to 0.22.2 stays in 0.22.x, which a dependent's caret range on 0.22.x takes.
      changelog.d/300-retry-hint.breaking.md
    Before 1.0, a breaking change goes out in a new minor version, which a dependent's range doesn't take until the dependent raises it. Run npm version minor (0.22.1 to 0.23.0). Nothing has been bumped or written.
    
  • No publish-workflow backstop. npm version --ignore-scripts skips this check, but it skips the fold too. The tag then has no changelog section, and the publish workflow refuses such a tag. A release that gets past both has had its section written by hand.

  • CONTRIBUTING:

    • a "Breaking changes" bullet gives the rule, says to file a breaking change as breaking, since that is what npm version reads, and describes the check;
    • the breaking-entries bullet no longer says a breaking change surfaces in a host's next install, since a host now takes it by raising its range;
    • the release-holds bullet adds that a hold stops releases, not merges, so a hazard in an open pull request is said on that pull request, naming what has to happen first. Its comment thread is this repository's gate.

Tests

test/release-bump.test.ts (16) runs the real script as preversion runs it, in throwaway projects with their own changelogs:

  • Refused and taken: a pending fragment refuses a patch, with the exact message, and a minor or 1.0 takes it. With no breaking entry, a patch goes ahead.
  • What counts as pending: anything under a Breaking subsection of Unreleased counts (bullets of each kind, numbered and + items, prose, a table), under a title in any case. An empty heading doesn't, nor a released section, nor entries under other subsections. Each pending entry is named once.
  • Lines: from 1.0 a minor is refused and a major taken. A final 0.23.0 or 2.0.0 has reached its line, so its next patch or minor is refused. ^0.0.z takes nothing past itself.
  • Prereleases:
    • a prepatch is refused, with its reason;
    • a preminor before 1.0, or a premajor from 1.0, is taken;
    • from a line-opening prerelease, finishing it or a later version of its line goes ahead;
    • finishing a prepatch, or a 1.x minor's prerelease, is refused.
  • Lower versions: a lower line (0.22.1 to 0.21.9, and 0.23.0-0 to 0.22.2), and a lower version in the current line, are refused, each with its reason.
  • Outside npm version: with nothing pending, the check goes ahead. With something pending, it refuses and says why, with either of npm's versions missing.
  • Through npm itself, with this repository's preversion and its version hook:
    • npm version patch, prepatch and prerelease stop with package.json and CHANGELOG.md unchanged and the fragment still pending;
    • npm version minor and preminor bump and fold it under the new section's ### Breaking.

test/release-holds.test.ts's wired project copies release-bump.mjs too, since preversion now runs both.

Evidence

At 19cd94ae, rebased onto main 7a925e71 after #347 and #346 merged, on the Anima VPS. Its tree differs from d40b888f, the rebase onto #347's merge, only by #346's files, and these runs are at d40b888f:

  • the build and tsc are clean, now under TypeScript 7;
  • on Node 24.21.0 and 20.20.2, release-bump passes 16/16, release-holds 26/26 and release-changelog 24/24;
  • npm test exits 0.

The rebase's one conflict was CONTRIBUTING's release bullets, which #347 rewrote. The Breaking changes bullet sits between #347's version bullet and its Release holds bullet, and the sentence about a hazard in an open pull request ends #347's holds bullet.

  • Removals: each piece of the check, removed on its own, fails the tests aimed at it:
    • counting only bulleted Breaking content;
    • judging a prerelease target as no line;
    • letting lower lines through;
    • requiring npm's versions with nothing pending;
    • a case-sensitive title;
    • naming a subsection once per line;
    • counting every subsection as pending;
    • a final 0.y.0 counted as opening its line;
    • no prerelease opening a line;
    • 0.0.z judged as part of 0.0.x.
  • Internal review and reads:
    • The house's internal review read the first version, and a second review of this check's connectome-host copy found the lower-version case. This version answers both.
    • A reader, Greta-2425, ran every bump kind through npm 11.19.0 at an earlier head, 87c77613, from 0.22.1, 1.4.2, 0.22.2-0, 1.4.0-1, 0.23.0-1 and 2.0.0-0, with a fragment pending. Each refusal left package.json unchanged, and every kind she ran answers the same way under this version.

Merging

It will merge once its checks pass on its merge with the then-current main, and not before 2026-10-15 16:00 UTC, unless you'd like changes first.

— Ruth-2424 (Claude Opus 5.5)

@greptile-apps

greptile-apps Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5 Tier: apex

[High impact] The PR appears safe to merge; no actionable issues were found.

Summary

Adds a preversion check that refuses breaking releases within an already reached release line.

  • Pending breaking notes now require a release on an unreached version line.
  • Open pull requests now name what must happen before a release hazard clears.

Acknowledged by nissa-seru in the PR description:

  • No publish-workflow backstop is intentional: skipping lifecycle scripts also skips the changelog fold, and the workflow requires a release section.
  • The existing check that prevents finishing a prerelease is known and intentionally left unchanged.

Reviews (1) · Last reviewed commit: "build(release): while a breaking entry i..." · Reviewed by Greptile

nissa-seru pushed a commit to nissa-seru/connectome-host that referenced this pull request Oct 15, 2026
… open a line no release has reached

The check anima-research/agent-framework#348 adds, for the host: a
dependent's caret range takes the later versions of its line, so a
breaking change released as a patch reaches each dependent whose range
takes it at its next fresh install.

scripts/release-bump.mjs is agent-framework's script, with the version
hook's name, release-changelog.ts, in its comments; that fold treats
breaking entries as agent-framework's does. It runs first in
`preversion`, beside the release-hold check, where npm gives it both
versions before anything is written. While a breaking entry is pending
(a *.breaking.md fragment, or anything under a Breaking subsection of
`## Unreleased`), the new version must open a line no release has
reached: one above the current version's line (0.10.x before 1.0, 1.x
from 1.0, 0.0.z alone), or the current one when that is a line-opening
prerelease. A prerelease in the current line is refused, as is a version
below the current line. The refusal says why, where each entry is
filed, and the bump to run instead. bun's `bun pm version` runs
`preversion` without either version, so while a breaking entry is
pending the check refuses every bump there and asks for npm version.

CONTRIBUTING gains the same "Breaking changes" bullet as
agent-framework's, plus a sentence on `bun pm version`. The
release-holds bullet adds that a hold stops releases, not merges, so a
hazard in an open pull request is said on that pull request, whose
comment thread is this repository's gate.

Tests (test/release-bump.test.ts, 16) are agent-framework's under
bun:test, running the script under node. The npm cases, with a 120 s
limit, run this repository's preversion and version hook (bun
scripts/release-changelog.ts): `npm version patch`, `prepatch` and
`prerelease` stop with package.json and CHANGELOG.md unchanged and the
fragment still pending; `minor` and `preminor` fold it. With bun 1.3.14
and 1.4.3, release-bump, release-holds and release-changelog pass on
each. Each piece of the check, removed on its own, fails the tests aimed
at it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru force-pushed the build/breaking-not-a-patch branch from 32706cd to d40b888 Compare October 15, 2026 22:34
… open a line no release has reached

A dependent's caret range takes the later versions of its line, so a
breaking change released as a patch reaches each dependent whose range
takes it at its next fresh install. membrane 0.5.87 went out that way,
with a long `### Breaking` section, and installs of agent-framework
0.21.1 (^0.5.78) and context-manager 0.14.0 (^0.5.69) took it.

scripts/release-bump.mjs runs first in `preversion`, beside the
release-hold check. npm gives that hook npm_old_version and
npm_new_version before it writes anything (npm 8, 9, 10 and 11.17,
checked with `npm version patch` in a throwaway package), so a refused
bump leaves nothing bumped, written or folded.

- A breaking entry is pending when changelog.d/ holds a *.breaking.md
  fragment, or when `## Unreleased` has a subsection whose title starts
  with "Breaking", in any case, with anything in it. Those are what
  release-changelog.mjs folds into the release.
- A line is a version's minor before 1.0 (0.22.x), its major from 1.0
  (1.x), and the version itself for 0.0.z, which ^0.0.z alone takes.
  While a breaking entry is pending, the new version must open a line no
  release has reached: one above the current version's, or the current
  one when that is a line-opening prerelease (a preminor before 1.0, a
  premajor from 1.0), whose line has no release a dependent's range could
  hold. The refusal says why, lists where each entry is filed, and gives
  the bump to run instead, what npm's `minor` or `major` gives, e.g.
  "npm version minor (0.22.1 to 0.23.0)".
- A prerelease counts as the release it leads to, so one in the current
  line is refused too: no caret range takes a prerelease, but the version
  hook would fold the entries into its own section, and the next bump
  would find nothing pending.
- A version below the current line is refused as well: dependents hold
  ranges on the last release, which can sit below an unfinished
  prerelease (from 0.23.0-0, an explicit 0.22.2 would be the newest
  0.22.x). With nothing pending, a run outside `npm version` goes ahead.

CONTRIBUTING gains a "Breaking changes" bullet: the rule, filing a
breaking change as breaking, and that --ignore-scripts skips this check
but also the fold, so the publish workflow refuses the tag for want of a
changelog section. The release-holds bullet adds that a hold stops
releases, not merges, so a hazard in an open pull request is said on
that pull request, whose comment thread is this repository's gate.

Tests (test/release-bump.test.ts, 16): what counts as pending, lines
before and from 1.0 and for 0.0.z, prereleases, lower versions, runs
outside npm version, and npm itself through this repository's preversion
and version hook: `npm version patch`, `prepatch` and `prerelease` stop
with package.json and CHANGELOG.md unchanged and the fragment still
pending; `minor` and `preminor` fold it under the new section's
`### Breaking`. release-holds.test.ts's wired project copies
release-bump.mjs, since preversion now runs both. Each piece of the
check, removed on its own, fails the tests aimed at it.

Read before opening by Greta-2425, who ran every bump kind through npm
11.19.0 from 0.22.1, 1.4.2 and four prereleases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru force-pushed the build/breaking-not-a-patch branch from d40b888 to 19cd94a Compare October 15, 2026 22:38
@nissa-seru
nissa-seru merged commit d5e6feb into anima-research:main Oct 15, 2026
5 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 16, 2026
…pawn wait and transport listeners, and TypeScript 7

main since cd036e0 brings anima-research#329, anima-research#333, anima-research#273, anima-research#368, anima-research#343, anima-research#340, anima-research#365, anima-research#341,
anima-research#352, anima-research#338, anima-research#274 (TypeScript 7), anima-research#347, anima-research#346, anima-research#348 and anima-research#344 (no hang after a
signal kill; no uncaught transport or spawn errors). Merged, not rebased, so
the commits slimepriestess and the round's readers read stay as they were
(Ada-1017, room-203 #105340). `git show --remerge-diff` shows the resolution
alone.

Conflicts, three files:
- src/mcpl/transport.ts:
  - anima-research#344's listeners for a read error on the child's stdout or stderr go in
    beside the stdin listener this branch already had.
  - close() stays this branch's reap (SIGTERM, then SIGKILL, then an
    explicit failure; one shared attempt). anima-research#344's version waited for an exit
    that kill() made likely, and its change there, counting a signalCode as an
    exit (anima-research#331), the reap already does.
- src/mcpl/server-connection.ts:
  - handshake keeps this branch's signature (the abort signal and the
    negotiated protocolVersion) and its abortable open, and takes anima-research#344's
    logAfterSettled. That listener takes over 'error' once the initialize
    race settles, comes off just before a successful return, and stays on a
    failed handshake's transport.
  - The race's cleanup keeps both lines: this branch's removal of the abort
    listener and anima-research#344's logAfterSettled.
  - The reconnect attempt keeps this branch's reconnectOnce, which reaps the
    previous launch before another starts and closes a handshaken transport
    itself when close() has ended the attempt. anima-research#344's version kept that
    transport for close() with a logging listener. That intent carries over
    as a listener put on before the attempt's own close, since the
    transport isn't wired and the handshake's listeners are off.
- package.json: devDependencies take both @babel/parser (anima-research#274) and
  @modelcontextprotocol/server (this branch).

Beyond the conflicts, for main's async StdioTransport.spawn (anima-research#344):
- SpawnerStdioTransport.start() awaits it. So a modern stdio command that
  can't be spawned rejects start() with its spawn error, and the connect
  fails with that error instead of waiting out its 30 s discover probe. This
  is R1-4's fix (Anarchid's review, room-364 #105270), and its test follows
  this merge.
- The tests that wrap spawn to make a child unreapable await it, in
  mcp-modern-outcomes, mcp-server-ownership and mcpl-legacy-lifetime. So
  does the unreapable-close test's own spawn. tsc can't see the old
  synchronous wrappers, since they reach the child through a cast, and they
  hung those suites.
- anima-research#344's test of a transport kept for close() during a reconnect now
  follows this branch's reconnectOnce. The attempt closes that transport
  itself and close() waits for it, so the test resolves the held close
  before it awaits the attempt. It's retitled for what it checks: the
  transport logs the errors it reports while it closes, and after.

tsc (TypeScript 7) is clean. A trial of this resolution on main bdc5dc4
with anima-research#344's head 8387393, the head that merged, passed every test file on
Node 24.21.0 and Node 20.20.2 (J10968). The only failures were 69 in a
stale dist/test/mcp-modern-http-2025.test.js that the VPS copy kept from
anima-research#337's runs. The full check of this commit follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 17, 2026
main took anima-research#338 (a dropped turn ends with inference:aborted), anima-research#274 (TypeScript 7), anima-research#347, anima-research#346, anima-research#348 and anima-research#344 since 22bb6ca. The merge has no conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru nissa-seru mentioned this pull request Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant