Skip to content

fix(kanban): branch dispatched worktrees from fresh remote tip - #61626

Open
Mourey wants to merge 2 commits into
NousResearch:mainfrom
Mourey:fix/kanban-worktree-base-freshness
Open

fix(kanban): branch dispatched worktrees from fresh remote tip#61626
Mourey wants to merge 2 commits into
NousResearch:mainfrom
Mourey:fix/kanban-worktree-base-freshness

Conversation

@Mourey

@Mourey Mourey commented Jul 9, 2026

Copy link
Copy Markdown

Problem

The interactive hermes -w path branches new worktrees from the freshly-fetched remote tip via _resolve_worktree_base (in cli.py), gated by the worktree_sync config (default on), with a fail-soft fallback to local HEAD. The kanban dispatch path never inherited this: _ensure_git_worktree in hermes_cli/kanban_db.py hardcoded "HEAD" for the new-branch case.

The standalone ~/.hermes/hermes-agent clone is updated only by hermes update, not every session, so its local HEAD can lag origin/main by many commits. A dispatched card branched from that stale HEAD roots 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 local main that never learned about the merge → textual merge conflicts against a moved origin/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.

  • 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 the symbol from the shared module — behavior and the public name are unchanged.
  • _ensure_git_worktree resolves the base ref for the new-branch case (gated by worktree_sync, defensively defaulting to True on a 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.

Offline / no-remote / detached-HEAD all still work — they fall back to HEAD exactly as before.

Tests

New tests/hermes_cli/test_kanban_worktree_base.py (7 cases) proves, against real scratch git repos:

  • a new-branch worktree contains a remote-only commit → it branched from the fetched tip, not stale local HEAD, when worktree_sync is on;
  • offline / unusable-ref cases still succeed by falling back to HEAD (mirrors _setup_worktree's retry);
  • worktree_sync: false branches from local HEAD;
  • the existing-branch resume path is unchanged.

Verified green:

  • tests/hermes_cli/test_kanban_worktree_base.py — 7 passed
  • tests/cli/test_worktree_sync_base.py (interactive-path regression) — 5 passed
  • tests/hermes_cli/test_kanban_cli_dispatch_passthrough.py — 4 passed
  • tests/hermes_cli/test_kanban_db.py (the changed module) — 223 passed

🤖 Generated with Claude Code

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/cron Cron scheduler and job management comp/cli CLI entry point, hermes_cli/, setup wizard labels Jul 9, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:54 invokes git fetch without 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 to HEAD as described.
  • tests/hermes_cli/test_kanban_worktree_base.py:125-140 cannot expose that case because the fixture's local HEAD and stale origin/main initially resolve to the same commit.

Suggested changes

  • Require a successful fetch before returning either remote base ref; otherwise continue resolution and use HEAD as the final fallback.
  • Make the failed-fetch test diverge local HEAD from the stale tracking ref and assert the resulting worktree selects local HEAD.

Automated hermes-sweeper review.

Comment thread hermes_cli/worktree_sync.py Outdated
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 11, 2026
@Mourey
Mourey force-pushed the fix/kanban-worktree-base-freshness branch from 63779c5 to 42bc6d5 Compare July 20, 2026 11:43
@Mourey

Mourey commented Jul 20, 2026

Copy link
Copy Markdown
Author

Rebased onto current main (was ~1500 commits behind; now conflict-free and mergeable).

Only conflict was an import-line adjacency in cli.pymain added CLIBillingMixin where this branch adds worktree_sync; kept both. Diffstat is unchanged from the original commit.

Still needed on current main: _ensure_git_worktree continues to hardcode "HEAD" for the new-branch case (hermes_cli/kanban_db.py:5753), so dispatched worktrees still branch from the standalone clone's stale local HEAD rather than the fetched remote tip.

Verified against current main: tests/hermes_cli/test_kanban_worktree_base.py + tests/tools/test_kanban_tools.py — 121 passed.

@PRATHAMESH75

Copy link
Copy Markdown
Contributor

This PR fixes #68201 (kanban _ensure_git_worktree bases new worktree branches on a possibly-parked local HEAD, so a primary checkout left on an unmerged branch contaminates worker branches). It ports the interactive hermes -w base-freshness logic into the dispatch path: _resolve_worktree_base is extracted into a shared hermes_cli/worktree_sync.py (so kanban_db need not import cli), and the new-branch case branches from the freshly-resolved remote tip — gated by worktree_sync, defaulting to True on a config error — with a fail-soft retry from HEAD if the resolved base fails once. The existing-branch resume path is unchanged. That directly resolves the issue's root cause.

Note the issue suggested probing origin/HEAD; this PR instead reuses the repo's existing _resolve_worktree_base (fetched remote tip), which gives parity with the interactive path and is config-gated — a better tradeoff than a second bespoke probe. Offline / no-remote / detached-HEAD fall back to HEAD as before, covered by the 7 new cases in test_kanban_worktree_base.py. For context: upstream #50355 fixed only the interactive cli.py path, not this dispatch path, so this remains needed.

@brandonedley

Copy link
Copy Markdown

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. _ensure_git_worktree basing on HEAD means "whatever branch the primary checkout happens to be parked on." Our primary clone was sitting on an unmerged PR branch, so every worker branch cut during that window was born containing that PR's diff. The workers then opened PRs carrying changes nobody on those tasks had written. That reads as a correctness bug rather than a diff-size annoyance — the resulting PRs looked legitimate and the extra content was only caught downstream.

A gap in my version that yours closes. Mine resolved origin/HEAD via git rev-parse --abbrev-ref origin/HEAD and used it directly. That's a local remote-tracking ref, so with no fetch it can be arbitrarily stale — I fixed the "wrong branch" half and left the "old commit" half open. Your step-1/step-2 fetch is the part I got wrong.

Two details in your patch that look right to me from having run the narrower version in production:

  • Applying it to both cli.py:_setup_worktree and kanban_db.py:_ensure_git_worktree — I only patched the kanban path, and the CLI path has the identical exposure.
  • The @{upstream} step ordering. A deliberate feature-branch worktree should track its own remote rather than snapping to the default branch, and checking upstream before origin/HEAD is what preserves that.

The fail-soft retry in _ensure_git_worktree (resolved ref fails → retry from local HEAD with a warning) is the detail I'd have gotten wrong; a partial fetch leaving an unusable remote ref is exactly the edge my simpler version would have hard-failed on.

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.

@alt-glitch alt-glitch added the needs-decision Awaiting maintainer decision before any implementation label Jul 29, 2026
Mourey and others added 2 commits August 6, 2026 09:33
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants