Lossless Changelog
fleet

Twelve repos back at parity — and the release the marketplace couldn't see

metafetch 0.1.7 was tagged, released, and attested. Obsidian's directory still read 0.1.6, because it reads the default branch and the default branch was six commits behind. Every repo in the family is now development = main = master.

Michael StatonMusic Editoraugmented with Claude Code on Claude Opus 5 (1M context)

Why Care?

You can do everything right on a release and still ship it into a void.

metafetch 0.1.7 was tagged, built in a clean runner, signed with artifact attestations, and published with hand-written release notes. All correct. And Obsidian's Community Plugins directory still showed 0.1.6 — because the directory reads manifest.json at the HEAD of a repo's default branch, that repo's default branch is master, and master was six commits behind development.

Nothing was broken. No build failed, no check went red. The release simply wasn't where anyone was looking.

That turned out to be the whole family's shape: every repo's main and master sat 1–6 commits behind development, and content-farm's sat 15 behind. All twelve are now at parity, and master serves 0.1.7.

What's New?

  • Twelve repos at development = main = master. All clean fast-forwards — ahead was 0 everywhere, so nothing merged, nothing forced, no history rewritten.

  • Release narratives reach the splash for the first time. They used to live in a root-level release-notes/ folder that nothing rolled up.

  • Eleven submodule pointers bumped, including two vendored upstreams.

  • The Stenographer share imagery is on the CDN, with its URLs written down rather than left in a terminal scrollback.

The branch tiers were lying

development → main → master is the model, and the tiers had quietly stopped meaning anything:

Repomain/master behind development
content-farm15
metafetch6
cite-wide, image-gin, perplexed4
file-transporter, filestarter, grab-reference, lmstud-yo, plunk-it2
stenographer, image-wrangler1

Two details made this safe rather than exciting. Every branch was strictly behind — ahead was 0 on all of them, so each promotion was a fast-forward with nothing to reconcile. And the order was submodules first, parent last: promote the parent first and its main points at submodule commits that only exist on those submodules' development branches. The SHAs still resolve, but the tiers would be claiming something was promoted when it wasn't.

Worth knowing that the default branch is not uniform across the family, and it decides what the outside world reads:

Default branchRepos
mastercite-wide, image-gin, metafetch, file-transporter, filestarter, grab-reference, lmstud-yo, plunk-it
mainperplexed
developmentstenographer, image-wrangler

Release narratives were invisible to the splash

Each plugin kept two documents per release: a frontmattered narrative at changelog/releases/<version>.md for the site, and a frontmatter-free copy at release-notes/<version>.md that only the release workflow read. Deliberately — perplexed's release page literally closed by linking to "the full marketing-grade release narrative with deeper context."

The rollup only walks changelog/, so anything in release-notes/ was never seen. Consolidating to one file per release fixed that as a side effect: the narratives are now inside changelog/, and the workflow strips the frontmatter on its way to GitHub instead of us maintaining a second copy.

Merging perplexed's three duplicate pairs had to be done by hand — neither copy was a superset. The release page carried the install steps, the asset verification and the plugin-family table; the narrative carried the deeper per-feature sections and "What's next". Both halves survive.

On the splash side, changelogSchema now declares three fields that arrive with these documents. date_last_updated was the one that mattered: release docs carry it, the schema didn't declare it, so it came through .passthrough() as a raw string — precisely the hazard the comment above the editorial date pair warns about, where a date chain hands a string to .toISOString(). category and release_tag are declared so a releases-only filter can rely on them instead of fishing through passthrough data.

The /public path that answers 200 and renders nothing

The Stenographer share images went to ImageKit, and the reference file recording their URLs exists because of a trap worth stating plainly:

*.jpg is Git LFS in this repo, and GitHub Pages serves the LFS pointer file rather than the image. A site-relative /public path answers 200 with content-type: image/jpeg and about 131 bytes of text. It looks healthy to a link checker and renders as nothing to a human.

That is the entire reason this imagery is CDN-hosted. It isn't a preference, and it was only recorded in a comment inside one frontmatter block — now it's in splash/assets/ogimages-imagekit-paths.md alongside all twelve in-use URLs.

Alt text is written for Stenographer's six, after looking at each one; they turn out to be six distinct compositions rather than crops of a single render. Perplexed's six are marked as still needing alt rather than being given plausible-sounding text — alt written without seeing the image satisfies a linter while telling a screen-reader user nothing.

Under the hood

A frontmatter audit ran across the whole tree — eleven plugin repos plus the parent, +289/-0 lines. Purely additive: site_uuid and hex_code minted on documents that had no stable identity, plus the editorial date pair where it was missing. No deletions, no reordering, no body text touched, per the context-vigilance drift policy.

rollup-sync now reports the split — wrote N files (X release narratives, Y dated entries) — so a run that silently stops pulling releases is visible rather than buried in a total. No path change was needed; the Content API walk was already recursive and already preserved the subpath.

The vendored submodules turned out to be three different shapes, which is worth writing down because "just pull them" is wrong for two of the three:

ModulePoints atAccessUpdate path
obsidian-textgenerator-pluginupstream directlyREADplain git pull
obsidian-gitour fork of Vinzent03/…ADMINgh repo sync per branch
obsidian-task-managerour fork of antoneheyward/…ADMINalready level

Pulling a fork reaches our copy, not the parent — which is why obsidian-git reported "already up to date" while sitting 185 commits behind upstream. Its master and main synced cleanly; development carried one Lossless commit (adding the plugin's own generated obsidian_askpass.sh to .gitignore) that a --force sync would have discarded, so upstream was merged in instead.

That ignore rule is doing real work, incidentally: the plugin generates the file and tries to self-exclude via .git/info/exclude, which doesn't survive being a submodule. Upstream has no such entry, so it's a one-line PR waiting to be filed.

What's Next

/llms.txt and /llms-full.txt are already live and already index release narratives — 8 of them at last check, from perplexed, image-gin and cite-wide. Metafetch's two join on the next pnpm rollup:sync.

Still open: a way to highlight releases in the splash feed and filter the changelog down to just releases. The data layer is ready for both — every release document carries category: Release and a typed release_tag. The open question is whether releases should keep appearing inline among the dated ship notes, or get a surface of their own.