Skip to content

Release v0.79.0 - #4181

Merged
max-sixty merged 2 commits into
mainfrom
release
Sep 18, 2026
Merged

max-sixty merged 2 commits into
mainfrom
release

Conversation

@max-sixty

@max-sixty max-sixty commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner

Cuts 0.79.0 from 75baa2cd3. Minor bump: cargo semver-checks reports five breaking library lints, so patch is disallowed pre-1.0.

Two commits — the changelog and version bump, plus one fix the release's data-loss review turned up.

The cache fix

has-added-changes and merge-add-probe are keyed on the branch and target tips with no TTL and no invalidation, so an entry answers for whatever code wrote it until its key recurs — which on a finished branch is never.

#4146 moved every diff wt parses onto plumbing so display config can no longer change what it reads. The porcelain reads it replaced wrote those two kinds, and under diff.relative or submodule.<name>.ignore they could record a branch's changes as absent. Nothing clears the cache on upgrade, so a poisoned entry outlives the fix: wt remove reads the old answer and deletes the branch without -D.

The exposure is narrow — it needs one of those two settings, and for diff.relative also a run from a subdirectory the branch's changes sit outside. The commits stay in the object store; it's the ref that goes. But nothing about the upgrade clears it, so for an affected repository the fix in #4146 does nothing until the cache is discarded. Reproduced against the fixed binary with a hand-written pre-upgrade entry:

✓ Removed branch feature (no added changes on main, ⊂)
branch still present? DELETED

CACHE_EPOCH in src/cache.rs versions what the cached values mean. ensure_epoch runs from cache_dir, the one funnel every read and write passes through, and discards a tree stamped with a different epoch. The lock spans the removal, not just the bookkeeping, because wt list fans the SHA-keyed reads over a thread pool. The sweep attempts every kind and returns the first error, so one undeletable directory doesn't strand the rest in read_dir order. Both wt config state clear and wt config state cache clear drop the stamp with the entries it describes, so a cleared tree re-stamps on the next command rather than vouching for whatever lands there next.

UNVERSIONED_KINDS exempts summary, ci-status and pr-number, so an upgrade doesn't charge a model call per branch and a forge round-trip for a diff fix. ci-status and pr-number hold the forge's own answer, so no change to how wt computes things can make them wrong. summary is exempt on weaker grounds, stated as such in the docstring: its key hashes the diff, but the prompt template and llm::prepare_diff's filtering sit downstream of that hash, so rewording the prompt does leave a stale summary — accepted because the value is display-only, and closing it means hashing the rendered prompt rather than bumping the epoch.

Known residual, deliberately deferred: the stamp records the last worktrunk to check a tree, not what wrote each entry, so a 0.78.0 binary run afterwards writes porcelain answers this one trusts. Closing that means putting the epoch in each entry's path, which invalidates by construction and costs the same in a later release. It is named in the CACHE_EPOCH comment.

TestRepo::assemble stamps the tree it builds, since a test that seeds .git/wt/cache/ by hand means entries this version produced. test_remove_branch_only_unmerged_ignores_cache_from_an_older_epoch covers the upgrade, a table-driven unit test covers all three stamp states and the exemption, and both clear handlers are pinned against dropping their clear_epoch call. Each assertion was checked under mutation rather than left to pass by construction.

Checks

cargo run -- hook pre-merge --yes green (4925 tests). This PR runs the full cross-platform matrix itself — full-tests on macOS, Linux, Windows and git 2.43.0, plus feature-powerset, nix-flake and the release targets — and nightly.yaml re-runs on the merged tip before the tag, since the green run on the cut-from tip predates the cache fix. Changelog verified over three passes against the diffs, the pre-change files, the rendered docs, and a forced cargo-semver-checks run.

This was written by Claude Code on behalf of max-sixty

🤖 Generated with Claude Code

@max-sixty
max-sixty force-pushed the release branch 2 times, most recently from 8ae98a8 to 055bbed Compare September 18, 2026 01:27

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.git/wt/cache/.epoch is a new file Worktrunk creates, and it isn't in the FAQ's What files does Worktrunk create? inventory. Section 4 enumerates every .git/wt/ path down to .git/wt/cache/picker-preview/*.json and closes with "No files outside the six sections above", and CLAUDE.md's Data Safety section names that inventory as what new code changing this surface gets reviewed against. The row is also where to say the stamp deliberately survives wt config state clear — a leading-dot name has no extension(), so clear_json_files skips it — since the same section tells users that command "removes all repository data". Happy to push the row if you want it.

Three more inline: the failed-removal arm, what the whole-tree wipe costs for two of the kinds, and one changelog line that lost its meaning on the way out of the commit message.

Comment thread src/cache.rs
Comment thread src/cache.rs Outdated
Comment thread CHANGELOG.md Outdated

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The epoch is the right shape for this — one funnel, one constant, and the residual named where the next bumper reads it. Two gaps, both about where the stamp's behavior did and didn't get wired through; inline.

Comment thread src/commands/config/state.rs Outdated
Comment thread CHANGELOG.md

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One new gap, on the funnel itself: cache_dir is now a mutation, and wt config state get's inspection reads go through it — so the command documented as pure inspection performs the wipe and then reports the tree it just emptied. Inline.

Comment thread src/cache.rs

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UNVERSIONED_KINDS exemption settles the summary/ci-status overcharge at the root rather than in the changelog wording, and clear_epoch in handle_state_clear_all closes the stronger of the two clears. Two things on the new code, both inline.

I ran the two new tests under mutation. The integration test holds up: drop the .epoch removal so the poisoned entry is trusted, and it fails with ✓ Removed branch feature-unmerged (no added changes on main, ⊂). The unit test's new exemption assertion does not — it stays green with the UNVERSIONED_KINDS check removed from discard_governed_kinds entirely.

The state get thread from the last pass narrows but doesn't close: summaries and CI status now survive an inspection read, while sha_cache::count_all still reports the counts the command left behind and # Reading vs resolving still says the aggregate get never persists.

Comment thread src/cache.rs Outdated
Comment thread src/cache.rs Outdated

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The per-kind sweep and the clear_epoch drop in handle_state_clear_all close what the last pass raised. Two gaps on the new code, both inline: the clear_epoch calls have nothing pinning them, and the exemption docstring states a rule its own third entry doesn't meet.

Comment thread src/commands/config/state.rs
Comment thread src/cache.rs Outdated
Comment thread src/cache.rs Outdated
@max-sixty
max-sixty force-pushed the release branch 2 times, most recently from ce892d5 to 26e06e4 Compare September 18, 2026 03:49

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The threads from the last four passes all close: the per-kind sweep carries on past a failure, the exemption test seeds off cache_root so the mutation fails, both clear handlers drop the stamp and both are pinned, and the UNVERSIONED_KINDS rule covers pr-number. Two new things, both inline — a changelog gap that will otherwise surface after merge, and one clause of that rule that summary doesn't meet.

On the data-loss gate for the release: discard_governed_kinds is a new fs::remove_dir_all that takes any non-exempt directory it finds, but its root is .git/wt/cache/ and src/cache.rs holds the only join("cache") in the tree — staging/promote and staging/relocate sit one level up at .git/wt/, out of reach. Everything it can delete is regenerable, so the hold doesn't apply.

Comment thread src/cache.rs Outdated
Comment thread CHANGELOG.md
- **The picker demo shows the CI and summary columns filling in**: `alt-p` widens the rows, the cells land behind them, then it pages a diff, a PR's comment thread, and the PR itself. The homepage omnibus demo opens on the picker. [Docs](https://worktrunk.dev/switch/#interactive-picker) ([#4178](https://github.com/max-sixty/worktrunk/pull/4178))

- **The command pages got a sweep**: help text drops internals, deprecated behavior, and edge cases the command reports itself when they happen — `wt step diff`'s "How it works", the `wt hook --var` paragraph, and the `wt config state ci-status` page among them. Every config sample now renders the file it belongs in as its tab. ([#4157](https://github.com/max-sixty/worktrunk/pull/4157), [#4153](https://github.com/max-sixty/worktrunk/pull/4153), [#4173](https://github.com/max-sixty/worktrunk/pull/4173), [#4177](https://github.com/max-sixty/worktrunk/pull/4177))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#4180 landed on main after the cut-from tip 75baa2cd3, so it is already an ancestor of this release commit and ships in 0.79.0 — but nothing here covers it. This is the release skill's step 12 case: the merge squashes onto whatever main tip exists at merge time, so the drift ships whether or not the changelog names it. Folding it in now costs a line; catching it at step 12 costs a follow-up squash PR and another pass over the coverage list.

Whether a Further-reading removal clears the bar for an entry is your call — the section it would join already covers doc-site sweeps of comparable weight. If it does:

Suggested change
- **The homepage and README drop "Further reading"**: the section listed five external links; the one about Worktrunk itself — @DevOpsToolbox's video — moves into Next steps, beside the other things to do after installing. ([#4180](https://github.com/max-sixty/worktrunk/pull/4180))

max-sixty and others added 2 commits September 17, 2026 21:04
`has-added-changes` and `merge-add-probe` are keyed on the branch and
target tips with no TTL and no invalidation rule, so an entry answers
for whatever code wrote it until its key recurs — which on a finished
branch is never.

#4146 moved every diff `wt` parses onto plumbing so display config can
no longer change what it reads. The porcelain reads it replaced wrote
those two kinds, and under `diff.relative` or `submodule.<name>.ignore`
they recorded a branch with changes as having none. Nothing clears the
cache on upgrade, so the first `wt remove` after installing the fix
still reads the old answer and deletes an unmerged branch without
`--force`, reporting `no added changes on main`. Reproduced against the
fixed binary with a hand-written pre-upgrade entry.

`CACHE_EPOCH` in `src/cache.rs` versions what the cached values mean.
`ensure_epoch` runs from `cache_dir`, the one funnel every read and
write passes through, so no caller reaches a stale entry ahead of the
check; a tree stamped with a different epoch is discarded whole. That
makes a future generator change one constant bump rather than a rename
per affected kind. The stamp is a non-`.json` file at the cache root, so
the per-kind sweeps behind `wt config state cache clear` leave it alone.

`TestRepo::assemble` stamps the tree it builds: a test that seeds
`.git/wt/cache/` by hand means entries this version produced, and
unstamped they would be swept before the test read them.
`test_remove_branch_only_unmerged_ignores_cache_from_an_older_epoch`
covers the upgrade, and fails without the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@max-sixty
max-sixty merged commit f813615 into main Sep 18, 2026
41 checks passed
@max-sixty
max-sixty deleted the release branch September 18, 2026 04:20
@max-sixty max-sixty mentioned this pull request Sep 20, 2026
max-sixty added a commit that referenced this pull request Sep 20, 2026
0.79.0 was prepared and merged in #4181 but never tagged, so it never shipped.
Cargo.toml and Cargo.lock already carry the version, so this reuses it and folds
the thirteen commits that landed since into the same changelog section.

New entries cover the worktree-side rename fixes (#4190, #4191), the per-project
unknown-field warning (#4123), and the unterminated code fence in the picker's
preview (#4183, combined with #4178's column fix).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
max-sixty added a commit that referenced this pull request Sep 20, 2026
Removes `CACHE_EPOCH` and the machinery around it, added in #4181.

**Time-sensitive:** 0.79.0 is merged to `main` but not tagged and not on
crates.io, so the epoch has never reached a user. Landing this before
the tag means it never ships.

## What it was protecting

Two corner cases in 0.78.0 made `wt remove` delete an unmerged branch
without `-D`, each needing two conditions at once. Reproduced against
the v0.78.0 binary:

- `diff.relative` set **and** the command run from a subdirectory the
branch's changes sat entirely outside. Run from the repository root with
the same config, 0.78.0 correctly retains the branch.
- `submodule.<name>.ignore` set (git config or `.gitmodules`) **and** a
submodule bump as the branch's only change.

In both, 0.78.0 reports `✓ Removed branch feature (no added changes on
main, ⊂)` and 0.79.0 retains it. #4146 fixed how the answer is computed,
but 0.78.0 cached its wrong answer keyed on the branch and target tips
with no expiry, so the fix can't reach an entry already on disk.

## Why it goes

The epoch fires at most once per repository, on the first command after
upgrading, and pays off only for a user who hit one of those corner
cases *and* has a live cached key *and* then removes that branch.
Against that it carried a stamp file, a tree wipe, a global mutex on
every cache read and write, a documented hole when two versions share a
repository, and a failure path where an undeletable directory left the
reader with exactly the entries the wipe existed to discard.

So nothing replaces it: a repository that hit one of the corner cases
under 0.78.0 keeps that cached answer after upgrading, exactly as it
would have kept it on 0.78.0. `wt config state cache clear` drops it for
anyone who asks. The changelog entry describes both corner cases and
stays descriptive — it names no remediation.

## No new negative behavior

0.78.0 and 0.79.0 read any entry already on disk identically — across
`v0.78.0..HEAD` the key functions, the kind set, the consumers of both
integration probes, and `check_integration`'s priority order are all
unchanged, and no cached struct changed shape. They diverge only on a
cache miss, where 0.79.0 is right and 0.78.0 was wrong. So 0.79.0
without the epoch is a strict improvement on 0.78.0, and at worst a tie.

## Also

- **Release skill:** two rules. A correctness or data-loss entry states
the conditions the bug needed, joined with "and", established by
reproducing against the previous release rather than read off the diff —
without that, an entry like this one reads as though it fired for
everyone. And entries describe: imperative guidance only for a material
issue that affects a sizable share of users, which is rare.
- **`src/summary.rs`:** keeps the caveat that the summary cache hashes
the diff while the prompt sits downstream of that hash, which previously
lived only in the docstring this removes.
- **`CLAUDE.md`:** records that not versioning the cache is the
decision, and that a generator change says in its PR what the old
answers get wrong.

> _This was written by Claude Code on behalf of max-sixty_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants