Skip to content

v0.118.0

Choose a tag to compare

@smm-h smm-h released this 28 Aug 09:26
· 2112 commits to main since this release

The conversion, ownership and ledger release: repository conversions become verified move-outs and move-ins on releasables with lineage records; workspaces gain single-owner attribution with a mandatory root member and derived CI filters; release archives become the anchored ledger every range, status and undo read; the publication reconciler repairs tags and Releases from recorded facts; mirrors carry release tags and Releases and can be promoted to standalone repos.

Context

This release ships one campaign. The old conversion commands copied part of a
project's state and left both repositories incoherent; workspace ownership was
implicit and a root member owned nothing; what a release WAS lived only in git
tags; and mirrors were code copies that could not release. The campaign
replaces all four foundations: conversions verify tree identity and transplant
everything (state, anchors, tags, lineage); every file has exactly one owning
member and CI triggering derives from ownership plus dependencies; the
archived release files carry the CI-verified commit and tree hashes and are
the authority every consumer reads; and the reconciler and mirror publication
repair or extend the published surface only from recorded facts, refusing
anything no record explains. Implicit-mode workspaces and the migrate/extract
commands they used are gone -- deferred workspaces pin 0.117.2 per their own
conversion plans, and scripts/migrate_workspace_model.py migrates the rest.

Breaking

  • Every workspace must now declare a root member. workspace.toml declares the repository root as a member (path = ".", named root), which owns every tracked file no other member claims. The loader refuses a workspace without one, one whose root member is named anything else, one that gives another member the reserved name, one carrying a watch key, an implicit-mode workspace with no [[releasables]], and a releasable owning the root member without an explicit tag_format -- each error naming its own remedy.
  • Commands run at a monorepo workspace root now resolve to the root member. The workspace root is that member's own directory, so rlsbl status, changelog add and the rest report on it instead of refusing with "not inside any project". release resume and the changelog checks still treat standing there as "somewhere in this workspace" and look across every releasable.
  • rlsbl release run at a monorepo workspace root now requires --releasable <name>. The root directory names the whole workspace rather than one releasable, so the invocation has to say which one; without the flag the command errors and lists the declared releasables. Running inside a member directory is unchanged, and passing the flag there (or in a standalone repository) is refused rather than ignored.
  • The workspace watch key is gone from every surface. rlsbl monorepo add --watch no longer exists, rlsbl monorepo status no longer renders a Watch column, and every error message and doc page that told you to adjust watch patterns now points at the derived filters: block instead. The loader still refuses a workspace that carries the key, naming the member and the line to delete.
  • Workspace member paths must be unique. The loader refuses a workspace whose members claim the same path (including two spellings of one path, and two root members) instead of letting declaration order decide which member owns a file.
  • rlsbl monorepo extract is one command over the releasable, and it completes the move. monorepo extract-releasable is gone and monorepo extract now takes a releasable name instead of a package name -- a releasable is the portable unit, since it owns the version, changelog, release archives and tag scheme a package alone does not. The conversion now verifies each member's tree object survives the history filter, transplants the releasable's whole release state (remapping the release anchors and changelog hashes onto the rewritten commits), keeps one boundary alias tag at the current version, writes a lineage record explaining the conversion, and removes the departed members and their state from the source in one commit. It previews the entire plan under --dry-run and refuses -- before writing anything -- a mirrored releasable, one owning the root member, an inbound dependency (naming the rlsbl rewrite command that severs it), and a missing saferm unless --delete-with-rm is passed. Being destructive to the repository you are standing in, it now asks for confirmation.
  • The release archives are now the authority on what shipped. The unreleased range and every coverage figure are bounded by the highest archived release this checkout contains, not by git describe, so a deleted or moved tag no longer erases a release from the range and a monorepo member with a mismatched tag glob no longer reports all of history as unreleased. A truncated history is a hard error naming the deepen remedy instead of silently reporting "never released". The --json payloads of rlsbl status and rlsbl unreleased carry latest_release, latest_release_in_checkout and range_anchor_* in place of the former tag and commits_ahead_tag keys.
  • rlsbl monorepo status now reports each row's latest release from that project's release archives -- the Tag column is now Released and shows the version, annotated when the checkout does not contain it -- and both its coverage figures and rlsbl monorepo release init's unreleased-commit counts measure from the archived release rather than from the newest matching git tag.
  • Release-ref verification now covers every released version. The local-tag, remote-tag and github-release checks are replaced by one unpublished-refs check. The old three looked only at the current version's primary tag; the new one asks the release ledger which versions shipped and verifies every ref each of them owns -- primary tag, Go companion tags, and alias tags a rename or conversion recorded -- exists locally, exists on origin, and points at the commit the release anchored. It reports the three cases distinctly and names rlsbl release reconcile as the remedy.
  • rlsbl monorepo migrate-releasable is removed. It migrated a monorepo from per-package release state into the releasable model, and that state cannot exist any more: a workspace with no [[releasables]] section is refused at load. A workspace that still needs the conversion runs it under rlsbl 0.117.2, the last version that reads such a workspace.
  • The per-package batch release form ([packages.<name>]) is removed. A batch release file declares [releasables.<name>] sections only; a file carrying a [packages] section is refused with the rewrite it needs. This follows the workspace loader's refusal of workspaces that declare no [[releasables]] -- there is no second release mode left for the per-package form to describe.
  • The subtree mirror's destination moved from the member to the releasable. subtree_remote is now a key on a [[releasables]] entry in workspace.toml, not on a [[projects]] member; a member still carrying it is a hard error naming the exact edit, and a releasable with more than one member may not declare one at all -- a mirror carries one subtree. rlsbl monorepo add no longer takes --subtree-remote: declare the binding beside the releasable it belongs to.

Features

  • rlsbl rewrite go-module-path. Renames a Go module path across a repository: the module-path tokens in every go.mod and every Go import site under the old path, located by the tree-sitter import scanner and rewritten line-anchored. --dry-run prints a per-file plan with occurrence counts, and an apply refuses if a count moved since the preview.
  • rlsbl rewrite uv-path-sources. Converts path- and workspace-sourced Python dependencies into registry constraints floored at the version uv.lock resolves, covering [project].dependencies, optional-dependency extras and PEP 735 dependency groups, deleting the matching [tool.uv.sources] entry and declaring each converted name in internal_dep_floors. A locked version PyPI does not have -- or a registry probe that fails to answer -- is a hard error naming the release-first remedy, so an unsatisfiable floor is never written.
  • A committed support matrix, and a check that keeps it fresh. rlsbl/data/support-matrix.json records what every release target supports on every axis, plus each check's target scope and the pipeline table, all derived from the registries rather than written down beside them. The new target-matrix-fresh check (error, project + preflight) fails when the committed file no longer matches a regeneration, so a stale matrix blocks the release instead of shipping. Regenerate with uv run python scripts/generate_support_matrix.py.
  • Monorepo CI router path filters are now derived from the workspace. A member's filter is its own territory plus the territories of everything it depends on (transitively, every dependency scope), the workspace-root manifests and lockfiles, and the tool's own machinery; the root member gets everything minus the other members' territories. Nothing is declared per project any more, and the generated router now sets predicate-quantifier: some-with-excludes so those exclusions actually apply.
  • New check router-filters-fresh. The generated ci-router.yml's filters: block must match a fresh derivation from the workspace. A stale block silently under-triggers a member's CI, and a member whose job concludes skipped on the commit a release tags cannot pass the publish check. Runs under rlsbl check --tag workspace and in the release preflight.
  • New script. scripts/backfill_release_anchors.py backfills release archives in one reviewed pass: it anchors every archived version to the commit it shipped from, stamps the strictspec format_version gate onto archives written before the gate existed, materializes archives for released versions that never had one, and records a permanent unanchorable = true marker on a version whose commit cannot be recovered from any source.
  • New script. scripts/set_archived_descriptions.py rewrites the description field of archived release files from a reviewed JSON map, unlocking and relocking each 0444 archive, with a mandatory plan and a written-count assertion.
  • Docs. The release-workflow page documents how to backfill an existing repository's release anchors, and the unanchorable marker that permanently records a version whose commit could not be recovered.
  • rlsbl monorepo absorb is rebuilt: an absorbed repository arrives as a releasable, and a crashed absorb is finished by re-running it. It previews the whole conversion under --dry-run and refuses before writing anything -- a colliding tag name, a version the destination already released, a mixed tag scheme (naming the new --tag-format), a dirty tree. The source's tags are no longer fetched at all, so a tag this repository already owns can never be moved or deleted; the imported tags are created at the rewritten commits, with one boundary alias at the current version. The arriving release state -- changelog, release archives with their anchors, config and version -- moves into the releasable's state directory with every hash and anchor remapped and each recorded tree verified against the rewritten history, and the run ends with the member scaffolded, the CI router re-synced, the snapshot regenerated and a lineage record explaining all of it.
  • Repository conversions are documented. A new guide covers rlsbl monorepo extract and rlsbl monorepo absorb end to end: the releasable as the portable unit, every refusal, the tag translation and boundary-alias policy, tree and release-anchor verification, the manual procedure for splitting one member out of a shared releasable, re-run healing, and what lineage records hold.
  • New dep-locks check: a lockfile that no longer resolves its manifest is an error. A dependency added, removed or re-constrained after the lock was written leaves uv sync and npm ci installing the old resolution. The check compares uv.lock's entry for the project against pyproject.toml, package-lock.json's root entry against package.json, and go.sum's coverage of go.mod; each finding names the relock command. Structural and offline -- no package manager is invoked.
  • New go-module-identity check: a go.mod whose module path no longer names the repository is an error. A renamed repository, a new owner, or an absorb that moved a module under a subdirectory leaves a module path the toolchain can no longer fetch. The expected path comes from the origin remote plus the module's directory (/vN major suffixes accepted), and every finding names the rlsbl rewrite go-module-path invocation that fixes it. A repository with no origin skips rather than guessing an identity.
  • New strictspec-generated-floor check: a strictspec floor below a generated validator's GENERATED_BY is an error. Generated validators pair with the exact strictspec release that produced them at import time, so a floor a consumer can resolve below it ships an artifact whose validators cannot import -- invisible locally, because the repo's own lock resolves the newer release. The comparison is at full patch precision against the files strictspec.toml declares, which dep-floors (major.minor, against the lock) cannot see.
  • Three new networked release checks. ci-publish-secrets verifies that every Actions secret the configured CI publish pipelines authenticate with exists on the GitHub repo -- each pipeline declares its own: NPM_TOKEN for an npm pipeline, the Central Portal credentials and GPG signing key (SONATYPE_USERNAME, SONATYPE_PASSWORD, GPG_SIGNING_KEY, GPG_SIGNING_KEY_PASSWORD) for a maven-central one, HEX_API_KEY for a hex one, and nothing for a pypi pipeline, which publishes through OIDC -- so a missing credential is found before the release tags rather than when the publish job dies with ENEEDAUTH. old-repo-archived reads the lineage record's absorb conversions and reports a source repository still active on GitHub, naming the gh repo archive command. go-deprecation-published reads recorded go-module-path transitions and reports an old module path the proxy still serves with no // Deprecated: notice. All three carry the release tag so offline runs stay offline, and all three are fail-closed: a probe that cannot answer is an error, never a pass.
  • rlsbl release reconcile now reconciles everything a release publishes, not just the tags a rewrite moved. It observes origin once and judges every subject -- each released version's full ref set and its GitHub Release -- against four merged records rather than one: safegit's rewrite journal, the release ledger's anchors, the lineage records, and the committed scrub archives. Because the last three are committed, a fresh clone that never saw the journal can still explain a scrub it carries the archive for. The verdicts are materialize (origin is missing a ref the ledger records, or a released version has no GitHub Release), already-correct, re-point-with-lease, refuse-foreign and refuse-identity-mismatch. One ref origin holds that no record explains aborts the whole reconcile and nothing anywhere is repaired, and Go refuses to recreate a tag for a version released under a module path the repository has since changed. Consent is file-driven and the choice is required: --plan writes .rlsbl/releases/reconcile-plan.toml with a digest of the world it judged, and --apply re-observes and refuses when that world has moved.
  • rlsbl monorepo mirror now reconciles the mirror's release tags too, not just its branch. A mirror could be perfectly converged on main and carry none of the tags its releasable has released -- a mirror bound after the fact, a tag push that failed at release time, a mirror that was reset. The preview now names every released version the mirror is missing, and an apply materializes it from the release ledger: the tag at the subtree split of that version's anchor commit, and the mirror's own GitHub Release with the version's notes. The scaffold commit also moves identity manifests onto the mirror's identity (a mirrored Go module's go.mod named the monorepo, so it could not be fetched from the mirror), and a mirror's scaffold no longer renders a publish workflow -- a mirror's Releases come from the release flow, never from CI of its own.
  • rlsbl monorepo extract now promotes a mirrored releasable instead of refusing it. A releasable bound to a subtree mirror used to be a hard refusal: the mirror derives from the monorepo, so extracting would leave it deriving from a subtree that no longer exists. The same command now runs a different engine when a mirror binding exists -- it adopts the mirror's history rather than filtering a second one out of the monorepo, so the extracted repository keeps the commit ids consumers already resolve. Every changelog hash and release anchor is translated through the monorepo-to-mirror subtree-split correspondence, each released version's tag is materialized at the split of its anchor, and the correspondence itself is written into the new repository's lineage record so it can explain its own hashes without the monorepo. Deleting the monorepo's copy is justified by tree-hash equality against the mirror's pre-scaffold commit: a mirror that is behind stops the promotion and names the command that converges it.
  • The docs now state which part of rlsbl writes which git ref namespace. One table, in CLAUDE.md, the README and the release-workflow page: origin's branch heads are written by releases and nothing else; origin's tags and their GitHub Releases by the release's tag step and rlsbl release reconcile, both composing the Release through one module; a subtree mirror's branch by the mirror reconciler's converge; a mirror's tags and Releases by the mirror publication module; and rewritten history by rlsbl release scrub, the one sanctioned rewrite write.
  • New script. scripts/migrate_workspace_model.py applies the mechanical half of the workspace-model conversion to one repository: it declares the root member in the kind the operator names (--root-dev-node, or --root-releasable <name> --tag-format <fmt>), deletes every retired watch key, moves a member-level subtree_remote onto the releasable that owns the mirror, then loads the result and prints any remaining loader error verbatim as the list only an operator can resolve. It edits the file raw, so it reads a workspace the loader itself refuses; it is idempotent, previewable with --dry-run, and runs the release-anchor backfill afterwards.

Fixes

  • rlsbl monorepo absorb --dry-run no longer aborts before it can preview anything: its source-repo precondition check read the worktree with a command the preview refused to run.
  • strictspec floor corrected. Installing rlsbl could resolve a strictspec older than its generated validators require, making every rlsbl command fail at import with a runtime-version error. The declared floor now matches what the validators call for.
  • monorepo mirror --dry-run renders a plan again. The preview used to stop at step 1 with "branched on unsettled value" because observation's git subtree split and inspection clone were recorded instead of run; both are now allowlisted observes, and the plan no longer advertises scratch directories no apply would create.
  • Ancestry checks no longer trust a shallow repository's "no". With both commits present but the connecting history truncated by a shallow fetch, git answers "not an ancestor" to a question it cannot follow to the end; rlsbl now treats that as undetermined, so the mirror tripwire, the release's CI-verified candidate check and release resume refuse instead of acting on a wrong answer.
  • Mirror honesty on unanswerable history. When git cannot determine whether mirror commits descend from the current subtree split (pruned or unfetched objects), monorepo mirror now reports a distinct lineage-undetermined verdict with a fetch/deepen remedy instead of accusing the mirror of foreign commits.
  • workspace-unregistered no longer reports a member's declared target path subdirectory (e.g. {"name": "npm", "path": "npm"}) as an unregistered project
  • Similarly-named Go modules no longer confuse strictcli detection. A project importing a module whose path merely begins with another module's path (github.com/o/foo-extras next to github.com/o/foo) was matched as importing the latter, which could name the wrong main package as a repo's CLI entry point.
  • The pgdesign target no longer silently detects an undeclared schema/ subdirectory: detection looks only at the directory it is handed, and a schema in a subdirectory is declared with an explicit target path ({"name": "pgdesign", "path": "schema"}). Version and schema resolution raise a hard error naming that remedy instead of guessing.
  • Commits that only touch rlsbl's own generated files no longer need changelog coverage: the exempt set now also covers .rlsbl/bases/, .rlsbl/lint/ and the generated .github/workflows/ci-router.yml, alongside the changelog and release state it already covered.
  • rlsbl release yank no longer passes over a target in silence. A target with no registry-removal action is now reported explicitly by name instead of relying on a fallthrough in the command's name-based dispatch.
  • A project whose target has no built-in test runner no longer records a passing test step. The release's test step, rlsbl check --tag quality and the pre-push workspace test check now report an explicit skip naming the target instead of silently succeeding, and test-suite-workspace lists affected projects it could not test rather than dropping them from its count.
  • rlsbl rewrite go-module-path no longer skips whole directories. The sweep visited everything except vendor/ and .git/ as documented, but inherited the linters' build-output exclusions, so Go packages in ordinary directories named build, dist, static, public, assets or node_modules were silently left un-renamed while the command reported success.
  • Go imports written as raw strings are no longer invisible. An import written with backquotes instead of double quotes compiles identically, but the Go import scanner skipped it -- so rlsbl rewrite go-module-path left it un-renamed, and the dead-package and strictcli detectors did not count it as a reference.
  • A workspace root member now owns its files. A member declared at path = "." computed a prefix no git path could ever match, so it owned nothing: its changelog coverage was silently vacuous and only watch patterns gave it any territory. Every tracked file now belongs to exactly one member -- the most specific declared path wins, and the root member owns whatever no other member claims.
  • An aborted rlsbl rewrite now says what it already wrote. The count-mismatch abort read as if nothing had been written; it now enumerates the files or dependencies applied before the failure (which stay on disk -- there is no rollback) and states that a re-run re-plans from the current tree. A planned file that disappears between the preview and the apply is reported as an error naming the file instead of a traceback.
  • The pipelines and targets documentation no longer describes things that do not exist. The pipelines page claimed publish and asset steps were decided by publish and build_assets target capabilities that no target ever declared and no code ever read; it now describes what actually decides a publish step. The targets page claimed the name-consistency check skips targets that cannot read a name, when it asks every detected target and reports the ones that answered nothing.
  • rlsbl release yank probes and yanks subdirectory targets correctly. A target declared with a path was probed against the project root's manifest instead of its own, so its publication status could come from the wrong package entirely.
  • name-consistency reports non-answering targets when names disagree. A detected target that reports no package name is now named on the mismatch branch as well as the pass branch, so the mismatch detail no longer reads as the complete set of detected targets.
  • rlsbl status --registry distinguishes "no version API" from a failed query. Targets whose ecosystem has no latest-version endpoint (zig, docker, plain and the rest) no longer print (query failed), which read as a network problem; they name the missing API and skip the request. The JSON payload gains a NO_REGISTRY_API drift value.
  • The documentation tables no longer import rlsbl to render. The release-target table, the check-vs-target matrix, the pipeline table and the target count now read the committed support matrix, so building the docs needs the repository rather than an installed rlsbl -- the import path that once failed a release when the documentation environment lost its rlsbl overlay.
  • The targets and checks pages stop restating what the tools already generate. The detection-files, ecosystem-classification and capability tables on the release-targets page were hand-typed duplicates of columns the generated table already renders; the support axes are now a rendered table covering every axis rather than five of them. Several pages also carried counts that had drifted from the registries -- 18 ecosystems against 17 registered, 12 changelog checks against 11 -- and now describe without counting.
  • rlsbl monorepo release run tags every package again in packages mode. It looked for each member's release state beside the package instead of under the member's releasable, so it treated each one as already finished: no tags, no finalized changelogs, and a success report.
  • rlsbl monorepo check-names no longer queries registries about dev nodes. A dev node publishes nothing, so it has no registry identity to check; every workspace has at least one once its root member is a dev node.
  • rlsbl dev install --all no longer fails on dev nodes. A dev node ships nothing, so there is nothing to install from it; the run used to abort with "no targets detected". Naming one through --include still selects it.
  • Changelog coverage no longer exempts nested lookalikes of root-only tool files. A path named .rlsbl-monorepo/... or .github/workflows/ci-router.yml below the repository root is a hand-written file with an owner, not rlsbl's own bookkeeping, so commits touching only such files need changelog coverage again.
  • A broken workspace.toml now surfaces its loader error instead of looking like a repository with no releasables. Releasable-config resolution swallowed every exception and answered "no releasable", so a workspace the loader had refused produced a generated publish router with unrendered template variables and no explanation.
  • Releases no longer abort when a finalize file is unchanged. A commit now names only the files whose staging actually changes the tree, so a byte-identical CHANGELOG.md regeneration no longer makes safegit refuse the release's finalize commit.
  • Undo audit file is no longer silently discarded. A corrupted undo-audit.json was previously overwritten with a fresh single-record array, losing the record of every past undo; rlsbl release undo now reports it as an error and leaves the file untouched for repair.
  • Fixed: an unreadable workspace manifest no longer narrows the generated CI router's paths filters. A manifest no dependency scanner could parse contributed no edges, so rlsbl monorepo sync wrote a filter missing a dependency territory and exited 0 -- and router-filters-fresh passed, because it re-derived from the same broken manifest. Changes to a dependency could then stop triggering its dependents' CI. Sync, the freshness check and the release-time window simulation now refuse, naming each project and file, and workspace-unbuildable runs in the release preflight.
  • Fixed: router-filters-fresh now compares the generated CI router's whole filters block against a fresh derivation, so a filter entry deleted by hand is reported (missing, extra, stale and differing entries are each named by member). Previously the check only walked the committed entries, and a member whose entry had been removed -- whose CI therefore never ran again -- passed silently.
  • The empty-candidate-window refusal no longer offers excluded paths. The remediation list now separates negated filter patterns under a 'Not counting' heading instead of telling the operator to commit under paths the filter excludes.
  • Undo refuses without its audit record. rlsbl release undo no longer deletes the GitHub Release, the tags and the release commits when its undo-audit.json cannot be written -- an unwritable or malformed audit file now refuses the whole operation before anything is destroyed, naming the file and the repair.
  • Release-file errors name the file. A schema or validation failure on a release file now says which file failed (<path>: <diagnostic>) instead of reporting only the field, for both single-project and batch release files.
  • rlsbl release run now refuses an editable unreleased.toml carrying an unanchorable marker, alongside the existing refusal of hand-authored candidate_sha/tree_hashes.
  • rlsbl watch now names the commit it is watching by the release that commit shipped, read from the project's release archives, so a deleted or moved tag no longer changes the run label or the release page the success notification links to.
  • --dry-run no longer reports a destroyed tag for a healthy release. Previewing a release of an already-released project aborted with a destroyed-tag diagnosis and three recovery options, none of which applied — the preview could not answer whether the current version's tag existed and that unanswerable read was treated as "no tag". Whether a version already shipped is now read from the release archives, and the guard still fires for a genuinely destroyed tag.
  • rlsbl monorepo extract names the edit that actually severs an inbound dependency. The remedy is read from the depending member's own manifests -- pyproject.toml, package.json, go.mod -- instead of the dependency scanner's classification, so a Python edge declared through [tool.uv.sources] no longer gets a Go remedy pointing at a go.mod that does not exist, an npm edge gets the exact package.json edit, and an edge declared in two places gets both edits.
  • rlsbl release undo now determines which release it is undoing from the project's release archives instead of git describe, so a release whose tag was already partly deleted is still undoable and a hand-made tag can no longer nominate a release that never happened. Its search for the previous release's boundary uses that release's recorded commit, so a deleted predecessor tag no longer silently widens it.
  • rlsbl monorepo extract refuses a member it cannot carry, before it touches anything. A submodule under a departing member used to be discovered at the source-side commit -- after the member's files were already deleted -- because the commit tool refuses a gitlink path; it is now a refusal at preview naming the submodule. A member path with nothing tracked at HEAD is refused by name too, instead of surfacing a raw git rev-parse failure.
  • rlsbl monorepo extract now checks that historical releases survive the rewrite. Each archived release anchor records the git tree of every path that version shipped; those hashes are content-addressed, so the extract compares its recomputed value against the recorded one instead of silently overwriting it. A disagreement is a hard error naming the archive, the path and both hashes, raised while the source is still untouched.
  • An extract no longer leaves a released version's changelog contradicting itself, and a rewritten changelog keeps its own permissions. When the history filter prunes every commit a released version's entries referenced, that version's .md is regenerated from what survived plus its archived release file -- so the section keeps its description and says "No user-facing changes" instead of listing entries the JSONL no longer holds -- and the run names the affected versions in its next steps. Separately, rlsbl changelog remap (and the remap inside the extract) pinned mode 0600 on every file it rewrote, so an ordinary 644 changelog silently became readable only by its owner.
  • The target support matrix now covers every per-target fact, not just the ones named like one. Completeness is stated by exclusion: every public attribute of the release-target protocol must be either a matrix axis or an explicitly excluded operation with a reason, so a fact can no longer escape the matrix by how it was spelled. Three axes join the documented tables: whether a target's publisher is bound to a repository, where such a publisher is registered, and what a user is told to run to create a project of that target.
  • A non-ASCII or awkwardly named path no longer corrupts a commit rlsbl makes. The working-tree probe reads git's porcelain in NUL-separated mode, so a path holding non-ASCII characters, spaces, tabs, quotes or backslashes arrives exactly as it is on disk instead of in git's escaped spelling -- which named nothing when handed back to a commit, failing mid-operation after earlier steps had already run.
  • rlsbl rewrite uv-path-sources now works for uv workspace members: when the target directory has no uv.lock of its own, the floor is read from the lock of the uv workspace root that claims it (a lock beside the manifest still wins, and neither location having one is still a hard error, now naming both probed paths).
  • rlsbl dev sync and rlsbl dev status refuse an overlay that cannot mean what it says. An overlay naming a package this workspace itself builds, or pointing at a path inside this repository, is now a hard error explaining which one it hit: the first would leave which copy of a member the tests import to install order, and the second makes the environment depend on a tree that resolves on no other machine.
  • rlsbl monorepo extract no longer makes a config file owner-only. Both configs it writes -- the extracted repository's merged .rlsbl/config.json and the dependency-floor declaration it adds to every releasable that stays -- pinned mode 0600, so an ordinary 644 config silently became readable only by its owner on the way through. A rewrite now keeps the mode the file already had, and a fresh one is written through the umask.
  • A repository absorbed into a workspace can be extracted back out. The absorb keeps the source's own tag beside the imported one at the current version, and the extract then translates that same version back to that same name -- which it refused as a tag collision, so a round trip was impossible. A tag standing at exactly the commit a translation would produce is now recognized as the boundary alias it is: kept, not re-created, and never a refusal. A tag of that name at a DIFFERENT commit is still a hard error.
  • A release no longer garbles non-ASCII paths. Every working-tree read in the release flow goes through one git status --porcelain -z helper, so a file whose name holds a non-ASCII character, a space, a tab, a quote or a backslash is named exactly as it is on disk -- in the not-clean refusal, in the selfdoc and hook snapshots, and in the files the release commits. Previously git's C-escaped spelling ("pkg\\303\\251/x.txt") reached those places and named nothing any filesystem has.
  • changelog generate commits every file it generated. The sweep that decides which regenerated files to commit now reads the working tree in the NUL-separated form and asks for untracked files individually, so a per-version .md under a not-yet-tracked .rlsbl/changes/ is committed instead of silently skipped, and a path outside plain ASCII is named as it is on disk rather than in git's escaped spelling.
  • monorepo cleanup no longer leaves a member's deletions uncommitted. When a member package's directory holds a non-ASCII character, a space or a quote, the deletions are now committed under the member's real path instead of git's escaped spelling -- which named nothing, so the files were removed on disk while the tree stayed dirty and the command still reported a successful commit.
  • monorepo sync auto-commits into the workspace it was given. Sync resolved its git repository from the process working directory instead of the workspace root it was handed, so a caller pointing it at another workspace could not use auto-commit -- git refused the paths as outside the repository. The commit now runs in the workspace being synced.
  • monorepo absorb and monorepo extract now name a command that exists when a releasable has no release state. Both refusals pointed at monorepo migrate-releasable, which is gone. They now say to run rlsbl monorepo sync -- which creates the state directory, its version file and its changelog for every declared releasable -- and to set the real version there first if the releasable has already shipped.
  • The monorepo guide describes only the releasable batch form. The batch-release section's examples and prose named per-package [packages.<name>] sections, which no longer parse; they now show [releasables.<name>] and explain that a releasable is the unit a batch releases.
  • A repository that released but was never backfilled now says so. When the release ledger holds no archive at all but the repository carries version tags under its own tag scheme, rlsbl hard-errors with the backfill instruction instead of reporting the entire history as unreleased. A project before its first release -- no version tags -- is unaffected.
  • Re-running an interrupted rlsbl monorepo absorb completes it instead of corrupting it. A changelog entry with no id is no longer migrated twice, a re-run aimed at a different --releasable is refused instead of silently skipping the version-overlap check, and a heal writes the version the first run recorded rather than re-reading it from the source (which a fork could have moved).
  • An unreadable release archive now fails rlsbl check --tag release instead of aborting the run. The unpublished-refs check reported the unreadable archive and then tried to finalize a pass, which the check framework refuses -- taking every other check down with it. It now fails cleanly, naming the archive.
  • Undoing a release no longer destroys changelog entries written after it. Un-finalizing a version now merges its entries back into unreleased.jsonl ahead of whatever accumulated since the release, instead of renaming the released file over them.
  • rlsbl check --tag workspace no longer demands a Go companion tag that the release never creates. When a releasable's own tag format is already the Go module proxy scheme (path/v{version}), the primary tag IS the proxy tag, so no companion exists -- the go-companion-tags check now derives its expectations from the same authority the release flow tags from.
  • rlsbl monorepo extract prints its Trusted Publishing reminder on the real run. The next-step naming PyPI's repository registration appeared under --dry-run but was dropped by the apply, exactly when the operator needed it. The tag summary also no longer claims the destination "keeps this releasable's tag format" for a releasable that owns no tag yet, where the format does change.
  • rlsbl watch labels a commit with its release from anywhere in the project. The label was read from a path relative to the working directory, so watching from a subdirectory -- or from inside a monorepo package whose release archives live under its releasable -- showed the short commit hash instead of the version it shipped.
  • rlsbl status now says when the latest release's commit is not recoverable. A version recorded as unanchorable was printed as a bare version number, indistinguishable from one whose commit the checkout contains.
  • release undo reworked around the real shape of a release. Its commits are found through the release archives rather than by walking down from the tag, so a tag sitting above the version-bump commit -- the normal shape of a resumed release -- no longer leaves undo reporting success with the version files still bumped, and a version-bump commit that cannot be found is a hard error naming the range searched. A conflicted revert now aborts and stops, committing and pushing nothing further, instead of being concluded by the next step's commit and pushed. Work above the released commit that would collide with the revert is refused by name before anything is deleted. And --dry-run lists every commit the real run makes, including the audit record and the two restore commits.
  • Concurrent changelog add runs no longer mislead about what was recorded. When two adds race, the loser's commit finds nothing left to stage and fails -- which was reported as a bare commit failure while the entry was in fact safely committed by the winner. An add now asks git what happened: a recorded entry is stated as such, and an entry that no commit carries is a hard error naming the file and the commit to run, instead of a warning under an "Added entry" success line. A changelog file whose last line lost its newline no longer swallows the entry appended after it.
  • A workspace that does not load is now an error, not a wrong answer. rlsbl unreleased and rlsbl watch silently fell back to the member package's own (empty) changelog directory when the workspace could not be read, and reported an unreleased range and coverage figure for a project they had failed to identify. The failure now surfaces.
  • The release-workflow guide no longer tells you to push after rlsbl release undo. The command pushes the reverted branch itself -- it must, or the remote keeps serving the release the undo just removed locally -- and the instruction sent readers to a manual push that a release-branch repository refuses.
  • dep-floors now polices a uv workspace member's floors. A member has no uv.lock of its own -- one lock at the workspace root resolves every member -- so the check reported "no uv.lock" and passed with zero comparisons for every member of every flat uv workspace. It now resolves the lock's location the same way rlsbl rewrite uv-path-sources does, and an unreadable lock is an error instead of an absence.
  • A Go module path with a capital letter is now probed correctly. rlsbl release yank and rlsbl status --registry asked the Go module proxy for the latest version at an unescaped URL, which the proxy does not serve, so a published module with an uppercase letter in its path reported as never published.
  • workspace-unbuildable now recognizes a uv workspace whose members use a ** glob. The glob was expanded non-recursively, so packages/** behaved like packages/* and a member one directory deeper did not resolve up to the root -- the check then synced the workspace project by project and reported "no root uv workspace". Every uv-workspace lookup in rlsbl now goes through one locator that reads the globs the way uv does, and resolves symlinked checkouts to the same workspace as direct ones.
  • A passing dep-floors check now shows what it compared. The pypi and npm paths said nothing on success, so a passing run on a polyglot project printed only the Go note and read as if the pyproject.toml-floor-against-uv.lock comparison had never run. Each compared dependency is now named with its declared constraint and the version the lock resolves, and a manifest declaring none of the enforced dependencies says so.
  • rlsbl release undo and rlsbl release yank now ask the Go module proxy too. A Go version's publication status was decided from one signal -- whether its tag exists on the git remote -- so a tag that was deleted after someone had already fetched the module read as "never published", while proxy.golang.org went on serving it permanently. The proxy is now a second evidence source: it serving a version blocks the operation, and its NOT serving one is recorded as inconclusive rather than as absence, because the proxy indexes lazily and its lag must never be what clears a deletion.
  • A history rewrite no longer breaks the release ledger. rlsbl release scrub remapped the JSONL changelog hashes through the rewrite and moved the tags, but never the release archives, which record which commit each version shipped from. Afterwards the tag pointed at the rewritten commit while the archive still named the old one, so every command that reads the ledger -- rlsbl status, rlsbl unreleased, the changelog coverage check, the release itself -- hard-errored with a message accusing the tag of having moved, which was the one thing the scrub had repaired. The archives now go through the same commit map, their tree hashes are recomputed at the rewritten commits and any change to a released tree is reported, and the move is recorded in the lineage record so a fresh clone can explain it without safegit's journal. A rewrite performed outside rlsbl is healed the same way from the journal.
  • A scrub no longer destroys a pre-release version's GitHub Release, and every Release it recreates is a complete one. rlsbl release scrub deleted each affected Release before reading the version off its tag, and a -rc.1 tag failed that read -- so the Release was deleted and never recreated. The version is now read first (pre-release tags included), a tag that is not a version tag at all is skipped with its Release untouched, and the rebuilt Release carries the same body the release flow writes: the version's changelog section, its rlsbl-ci-sha marker taken from the release ledger's anchor, and the pre-release flag its version earns.
  • rlsbl release reconcile no longer proposes to create Releases it simply did not list. The GitHub Release listing is capped, and a truncated answer looked exactly like a complete one -- so in a repository with more Releases than the cap, the unlisted ones were judged absent. A listing that comes back at the cap is now a hard error naming it.
  • rlsbl release reconcile --apply now performs exactly what the plan named. It re-derived the work from a fresh observation, and the plan's world_digest covers the remote only -- so a purely local change between plan and apply (a tag fetched or created since) silently enlarged the acted-on set and force-pushed a subject nobody previewed. An apply now refuses, naming the subject, when the fresh observation grows one the plan does not cover or when a planned subject's verdict, lease or target moved; planned items that became correct on their own are reported as no-ops. --plan also writes its plan file when it found nothing, so applying it is a clean no-op instead of an instruction to run the plan you just ran.
  • A release now really updates its subtree mirror. The mirror step pushed a temporary split branch to the mirror's main WITHOUT a force, which only ever succeeded on a mirror that had no scaffold commit on it -- so every release after the first reported a failed step and the mirror's branch never moved. The step now converges the branch through the mirror reconciler (which observes the remote, refuses a mirror carrying hand-authored work, force-pushes with a lease and re-applies the scaffold layer) and publishes the version's tag and GitHub Release through the same module rlsbl monorepo mirror uses, at the subtree split of the commit CI verified rather than at whatever the branch tip happens to be. Both steps stay non-fatal: a failure records its marker, exits nonzero, and names the command that heals it.
  • rlsbl release reconcile can now repair the situation it was written for in a project that has release archives. After an out-of-band history rewrite the archives still named commits the repository no longer had, so every released ref read as disagreeing with the ledger and the tripwire aborted the whole reconcile -- refusing exactly the repair it exists to perform. The ledger's stale anchors are now moved through the same records that explain the rewrite (safegit's journal, a lineage anchor-remap event, or a committed scrub archive) and committed, before a single verdict is computed. A dangling anchor no record explains is still a hard error, and --dry-run writes nothing while still previewing the verdicts a real run would produce.
  • A scrub's summary now reports the GitHub Releases it actually recreated. It counted the rewritten tags that looked like released versions instead, so a tag carrying no Release still added to the total.
  • release scrub no longer strands a tag without its Release. The GitHub Release for each rewritten tag is now updated in place -- notes, rlsbl-ci-sha marker and pre-release flag -- instead of being deleted and recreated, so a transient failure leaves the existing Release intact. A moved tag that carries no Release gets one created.
  • A mirror convergence no longer needs a machine-wide git identity. The reconciler clones the mirror to commit its scaffold layer, and that clone inherits none of the monorepo's local user.name/user.email -- so on a machine with no global identity the commit failed after the split had already been force-pushed, leaving the mirror stripped of its scaffold layer. The clone now carries the monorepo's identity.
  • A publish workflow can no longer survive on a subtree mirror. A mirror never releases itself, but two shapes slipped past: a publish.yml left in an older scaffold layer, and one that rode in through the subtree split because the member's own directory carries it. Every convergence now sweeps publish workflows out of the mirror clone, and a tip still carrying one is reported scaffold-stale instead of converged. The member keeps its own copy in the monorepo.
  • One underivable released version no longer stops the whole mirror command. A release anchored at a commit the subtree split cannot answer for -- typically an anchor predating the member's directory -- raised out of observation, so the branch never converged and no other version was judged. That version is now reported as unanchored with the reason, and everything else reconciles as usual.
  • Promoting a mirrored releasable now asks the mirror contract first. Extracting a mirrored releasable adopts the mirror's history, and the only check was tree equality -- so a pair of hand-authored commits on the mirror that cancel each other out passed and became the extracted repository's permanent history. The reconciler's own classification runs first: foreign commits or an undetermined lineage refuse the promotion, naming the commits.
  • The "who writes which ref namespace" tables now tell the truth. They claimed exactly one writer per namespace and that nothing else was written at all, while release undo, release edit, deprecate, yank, changelog amend|edit and rename-releasable all write those namespaces by design. Each table now names the routine writer and enumerates the sanctioned repair and retraction surfaces beside it.
  • release scrub's help no longer claims it recreates GitHub Releases. Scrub and release reconcile rewrite each tag's Release document in place and create only the absent ones -- they have not deleted a Release in some time, so the help described a failure window that no longer exists.
  • The mirror chapter documents the unanchored tag state and the release flow's own mirror steps. The tag table stopped at present-versus-missing, and nothing said that releasing a mirrored releasable converges the mirror and publishes the version's tag and Release on it non-fatally.
  • The conversions chapter now explains that a mirrored releasable is promoted, not filtered. It described extraction as always running git-filter-repo, and its refusal table listed the missing-tool precondition for conversions that never invoke the tool.
  • Subdirectory targets get CI that runs in their own directory. A project declaring a single target with a subdirectory path was scaffolded CI that ran at the repository root. For a pgdesign schema in a subdirectory this meant the schema validated locally and failed on the first CI push, since pgdesign check resolves its project from the working directory. Root-path targets are unaffected and no longer pick up a needless working-directory line.
  • The CLI help no longer states a wrong number of release targets. The top-level help claimed 17 while the monorepo group claimed 18, and dev install hand-listed which targets each install mode supports. All of these counts and lists are now derived from the target registry, so they cannot disagree with each other or go stale when a target is added.
  • The docs no longer claim 18 release targets, and no longer mention rlsbl config init. Three pages hand-typed a target count one higher than the support matrix carries, and the configuration reference credited .rlsbl/config.json to a command that was removed.
  • The status and unreleased page descriptions no longer say the commands answer from the last git tag. Both answer from the release the ledger anchors the checkout to, and their documentation still described the old behavior.
  • The scrub's step list no longer says it recreates GitHub Releases. Step 8 rewrites each affected tag's Release document in place and creates only an absent one, which is what the guide now describes.
  • Two batch-release error messages no longer call the plan file a "sidecar". rlsbl monorepo release run now says "Missing plan file" and "delete the plan file and re-run" when it refuses a batch whose companion unreleased.plan.json is missing or stale.
  • README corrections. Its per-tag check table is now rendered from the check registry instead of hand-typed counts that had drifted far below what rlsbl check runs; it no longer documents rlsbl scaffold --force or --no-tag, neither of which exists (the live opt-outs are --no-auto-commit and --no-auto-tag); and its release-flow summary describes the real ordering -- the candidate commit is pushed untagged, CI is awaited in-process, and the tag goes on the CI-verified commit.
  • rlsbl monorepo extract now rewrites the clone in a single git-filter-repo run, so a single-member extract's release anchors, changelog hashes and tags remap correctly on filter-repo releases that do not compose commit maps across chained runs (2.38, the Ubuntu 24.04 package). Previously the hoist ran as a second invocation and every original commit silently missed: anchors were left as recorded and changelog entries were dropped as pruned.