Skip to content

feat(config): flag stale default-branch cache in wt config state (#3471) - #3478

Merged
max-sixty merged 2 commits into
mainfrom
fix/issue-3471-default-branch-drift-29400360636
Jul 15, 2026
Merged

feat(config): flag stale default-branch cache in wt config state (#3471)#3478
max-sixty merged 2 commits into
mainfrom
fix/issue-3471-default-branch-drift-29400360636

Conversation

@worktrunk-bot

Copy link
Copy Markdown
Collaborator

Closes #3471.

What

wt config state's DEFAULT BRANCH section now calls out when the cached worktrunk.default-branch has drifted from git's local <remote>/HEAD — the scenario from #3471, where a GitHub default-branch rename plus git remote set-head origin -a leaves an existing clone silently branching wt switch --create off the stale base.

DEFAULT BRANCH
  old-default
▲ Cached branch differs from origin/HEAD (main)
↳ To adopt it, run wt config state default-branch set main; to re-detect, run wt config state default-branch clear

The check is local-only (reads the <remote>/HEAD symref — no git ls-remote, no network) and fires only when both the cache and <remote>/HEAD resolve and differ. No drift → just the value, as before.

Why on inspection, not per-command

As noted in the triage comment, worktrunk.default-branch doubles as the storage for an explicit user override (wt config state default-branch set). A per-command warning would fire on every command for anyone who intentionally pinned a non-origin/HEAD default. Surfacing it only when the user inspects state (matching the warning-placement guidance for "issues that persist until the user fixes them") sidesteps that entirely, which is what your wt config show / or similar suggestion pointed at.

Placement note: I put this in wt config state rather than wt config show because config state is where the default-branch cache is actually displayed (the DEFAULT BRANCH section), and the existing StaleDefaultBranch error already routes users to wt config state default-branch set. config show has no default-branch section today. Happy to move or mirror it into config show if you'd prefer it there.

Changes

  • Repository::remote_head() — new local-only accessor returning the primary remote's HEAD as (remote, branch); never queries the remote or infers, never persists.
  • wt config state (table): drift warning + set/clear hints in the DEFAULT BRANCH section.
  • wt config state get --format=json: new remote_head_branch field so scripts can detect drift too.
  • Docs: a note in the default-branch detection section explaining the cache isn't re-validated and how the drift call-out reconciles it.

Tests

  • test_state_show_default_branch_drift_warns — cache stale vs origin/HEAD → warning renders.
  • test_state_show_default_branch_no_drift_when_matching — cache matches → no warning.
  • test_state_get_json_reports_remote_head_branch — JSON carries both values.

Verified end-to-end against a scratch repo reproducing the exact #3471 steps.

`worktrunk.default-branch` is cached without re-validating against
origin/HEAD on every command (a deliberate perf choice), so a
default-branch rename followed by `git remote set-head origin -a` is
never noticed once the cache is warm — `wt switch --create` keeps
branching from the stale base (#3471).

Surface the drift on inspection instead of on every command (the key
doubles as a user override, so a per-command warning would be noise):
`wt config state`'s DEFAULT BRANCH section now compares the cached value
against git's local `<remote>/HEAD` and, when they differ, prints a
warning plus set/clear remediation hints. The check is local-only
(reads the symref, no `git ls-remote`) and fires only when both resolve.

`Repository::remote_head()` exposes the primary remote's local HEAD as
`(remote, branch)`; the state JSON gains a `remote_head_branch` field so
scripts can detect the drift too.
Isolated `)?;` closings on the multi-line writeln! calls left the error
-propagation region on a line with no hit region, tripping codecov/patch.
Bind the messages and emit with a single-line writeln!, matching the
render_shell_status pattern. Output is byte-identical.

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The drift check is clean — remote_head() reuses primary_remote()/local_default_branch(), stays local-only (no ls-remote), and correctly returns None (no false warning) when <remote>/HEAD is unset. One convention nit inline, plus one design point for a human call.

Override vs. drift framing (for @max-sixty to weigh): since worktrunk.default-branch doubles as an intentional override, a user who deliberately ran default-branch set develop while origin/HEADmain now sees, on every wt config state, "Cached branch differs from origin/HEAD (main)" plus a hint to set main — i.e. advice to clobber their deliberate choice. The code comment acknowledges the doubling; the open question is whether the wording ("differs" / "To adopt it") should soften when the value looks like an intentional pin. Not blocking — just flagging the UX for the maintainer's call.

"Cached branch differs from <bold>{remote}/HEAD</> (<bold>{remote_head}</>)"
));
let hint = hint_message(cformat!(
"To adopt it, run <underline>wt config state default-branch set {remote_head}</>; to re-detect, run <underline>wt config state default-branch clear</>"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The writing-user-outputs skill flags this pattern: hints render as a separate message from the warning above, so "it" (the remote's HEAD branch named on the warning line) has no local referent — Avoid pronouns with cross-message referents … Don't use pronouns like "it" that refer to something mentioned in the error message. Naming {remote}/HEAD directly makes the hint self-contained (remote is already in scope):

Suggested change
"To adopt it, run <underline>wt config state default-branch set {remote_head}</>; to re-detect, run <underline>wt config state default-branch clear</>"
"To adopt <bold>{remote}/HEAD</>, run <underline>wt config state default-branch set {remote_head}</>; to re-detect, run <underline>wt config state default-branch clear</>"

@max-sixty
max-sixty merged commit d80cc80 into main Jul 15, 2026
39 checks passed
@max-sixty
max-sixty deleted the fix/issue-3471-default-branch-drift-29400360636 branch July 15, 2026 09:18
@max-sixty max-sixty mentioned this pull request Jul 15, 2026
max-sixty added a commit that referenced this pull request Jul 15, 2026
Release v0.68.0. Highlights: on-demand picker preview tabs (#3439),
bare-repo project config read from the object store when the default
branch is checked out nowhere (#3462), shell detection via the process
tree (#3455), and `wt config state` flagging a stale default-branch
cache (#3478). Full details in CHANGELOG.md.

> _This was written by Claude Code on behalf of max_
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.

wt's cached worktrunk.default-branch doesn't refresh when origin/HEAD changes

2 participants