fix(kanban): branch dispatched worktrees from fresh remote tip - #61626
fix(kanban): branch dispatched worktrees from fresh remote tip#61626Mourey wants to merge 2 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for carrying the interactive worktree-base behavior into Kanban; the current-main premise is confirmed at hermes_cli/kanban_db.py:5416-5419, which hardcodes HEAD for new branches.
Problems
hermes_cli/worktree_sync.py:54invokesgit fetchwithout checking its return code and immediately returns the upstream as “fetched” at line 55; the default-branch path repeats this at lines 80-81. With an unreachable remote and an existing stale tracking ref, this does not fall back toHEADas described.tests/hermes_cli/test_kanban_worktree_base.py:125-140cannot expose that case because the fixture's localHEADand staleorigin/maininitially resolve to the same commit.
Suggested changes
- Require a successful fetch before returning either remote base ref; otherwise continue resolution and use
HEADas the final fallback. - Make the failed-fetch test diverge local
HEADfrom the stale tracking ref and assert the resulting worktree selects localHEAD.
Automated hermes-sweeper review.
| if upstream and "/" in upstream: | ||
| remote = upstream.split("/", 1)[0] | ||
| # Fetch just that branch; fail-soft if offline. | ||
| _git(["fetch", remote, upstream.split("/", 1)[1]], timeout=30) |
There was a problem hiding this comment.
Please check this fetch result before returning upstream as “(fetched)” on line 55. A nonzero fetch leaves any existing tracking ref usable but stale, so the new Kanban path will not take the advertised HEAD fallback; apply the same check to the default-ref fetch at line 80.
63779c5 to
42bc6d5
Compare
|
Rebased onto current Only conflict was an import-line adjacency in Still needed on current Verified against current |
|
This PR fixes #68201 (kanban Note the issue suggested probing |
|
Independent production corroboration for this, plus a second failure mode that I think strengthens the case. We hit this on a self-hosted fleet running kanban lanes and shipped a narrower local patch on 2026-07-20 (issue #68201 was my writeup of it). Yours is a superset and I'm dropping mine in favour of it — but the symptom we saw was different from the stale-base ergonomics problem described here, and worse: Contamination, not just staleness. A gap in my version that yours closes. Mine resolved Two details in your patch that look right to me from having run the narrower version in production:
The fail-soft retry in Happy to test this against our fleet's lane provisioning if that's useful — we have the reproducing condition (parked primary checkout + concurrent worker spawns) readily available. |
The interactive `hermes -w` path branches new worktrees from the freshly-fetched remote tip via `_resolve_worktree_base` (gated by `worktree_sync`, default on, with a fail-soft fallback to local HEAD). The kanban DISPATCH path never inherited this: `_ensure_git_worktree` hardcoded "HEAD" for the new-branch case, so a dispatched card branched from the standalone clone's (often stale) local HEAD. That roots the new branch on an old merge base, which later surfaces as textual merge conflicts against a moved origin/main and inflates the PR diff. Port the base-freshness logic into the dispatch path: - Extract `_resolve_worktree_base` from cli.py into a new shared module `hermes_cli/worktree_sync.py` (verbatim body + its own module logger). kanban_db must not import cli (cli imports kanban_db, not vice versa), so the helper lives where both can import it. cli.py now imports it and re-exports the same name, keeping behavior and the public symbol identical. - `_ensure_git_worktree` now resolves the base ref for the NEW-branch case (gated by `worktree_sync`, defensively defaulting to True on config error) and branches from it. It mirrors the interactive path's fail-soft retry: if `worktree add <base_ref>` fails and base_ref != HEAD, it retries once from local HEAD before raising, so a fetch hiccup never hard-fails worktree creation. The existing-branch resume path is unchanged. Tests: new `tests/hermes_cli/test_kanban_worktree_base.py` proves the new-branch worktree contains the remote-only commit (branched from the fetched tip, not stale HEAD), the offline/unusable-ref fallbacks still succeed from HEAD, sync-off branches from local HEAD, and the existing-branch resume path is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The offline test asserted the resulting worktree matched local `HEAD`, but the fixture left local `HEAD` and the stale `origin/main` tracking ref pointing at the same commit — so it passed no matter which base resolution picked, and could not detect a regression in the fallback. Advance local `HEAD` past the tracking ref first, then assert the base actually chosen. Current `_resolve_worktree_base` falls back to the cached remote-tracking ref when a fetch fails (HEAD only when no cached ref exists), so the rewritten case pins that contract from the dispatch path and additionally asserts the worktree does NOT inherit the primary checkout's local-only commits. A sibling case covers the no-cached-ref path resolving to `HEAD` without hard-failing worktree creation. Addresses the hermes-sweeper review on NousResearch#61626, which flagged the non-discriminating fixture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
42bc6d5 to
51f4c0a
Compare
Problem
The interactive
hermes -wpath branches new worktrees from the freshly-fetched remote tip via_resolve_worktree_base(incli.py), gated by theworktree_syncconfig (default on), with a fail-soft fallback to localHEAD. The kanban dispatch path never inherited this:_ensure_git_worktreeinhermes_cli/kanban_db.pyhardcoded"HEAD"for the new-branch case.The standalone
~/.hermes/hermes-agentclone is updated only byhermes update, not every session, so its localHEADcan lagorigin/mainby many commits. A dispatched card branched from that staleHEADroots the new branch on an old merge base. The result, observed in production: a later same-file card branches before its predecessor's PR merges, from a localmainthat never learned about the merge → textual merge conflicts against a movedorigin/main, plus an inflated PR diff. Dispatch ordering can't fix a stale merge base.Fix
Port the base-freshness logic the interactive path already has into the dispatch path — a narrow divergence fix, not a new feature.
_resolve_worktree_basefromcli.pyinto a new shared modulehermes_cli/worktree_sync.py(verbatim body + its own module logger).kanban_dbmust not importcli(cliimportskanban_db, not vice-versa), so the helper lives where both can import it.cli.pynow imports the symbol from the shared module — behavior and the public name are unchanged._ensure_git_worktreeresolves the base ref for the new-branch case (gated byworktree_sync, defensively defaulting toTrueon a config error) and branches from it. It mirrors the interactive path's fail-soft retry: ifworktree add <base_ref>fails andbase_ref != "HEAD", it retries once from localHEADbefore raising, so a fetch hiccup never hard-fails worktree creation. The existing-branch resume path is unchanged.Offline / no-remote / detached-HEAD all still work — they fall back to
HEADexactly as before.Tests
New
tests/hermes_cli/test_kanban_worktree_base.py(7 cases) proves, against real scratch git repos:HEAD, whenworktree_syncis on;HEAD(mirrors_setup_worktree's retry);worktree_sync: falsebranches from localHEAD;Verified green:
tests/hermes_cli/test_kanban_worktree_base.py— 7 passedtests/cli/test_worktree_sync_base.py(interactive-path regression) — 5 passedtests/hermes_cli/test_kanban_cli_dispatch_passthrough.py— 4 passedtests/hermes_cli/test_kanban_db.py(the changed module) — 223 passed🤖 Generated with Claude Code