Skip to content

fix(kanban): stop decompose siblings sharing one worktree checkout - #61907

Closed
ahmadashfq wants to merge 1 commit into
NousResearch:mainfrom
ahmadashfq:fix/kanban-decompose-worktree-isolation
Closed

fix(kanban): stop decompose siblings sharing one worktree checkout#61907
ahmadashfq wants to merge 1 commit into
NousResearch:mainfrom
ahmadashfq:fix/kanban-decompose-worktree-isolation

Conversation

@ahmadashfq

@ahmadashfq ahmadashfq commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Restores the documented one-worktree-per-task guarantee for decompose fan-outs. Today, decompose children inherit the root's literal workspace_path, and _resolve_worktree_workspace's existing-checkout shortcut reuses that directory on whatever branch is checked out — so siblings (which can be promoted and dispatched concurrently, max_in_progress unlimited by default) end up working in one directory on the first sibling's branch: cross-task commits land on the wrong wt/<id> branch, and concurrent siblings share one index/working tree with no lock. Observed in production 2026-07-10.

Related Issue

Fixes #61911

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • hermes_cli/kanban_db.pydecompose_triage_task: worktree-kind children no longer inherit the root's literal path (each child materializes its own <repo>/.worktrees/<child-id> at dispatch); dir/scratch inheritance unchanged.
  • hermes_cli/kanban_db.py_resolve_worktree_workspace: when the requested path is an existing checkout of a different branch, fall back to a fresh <repo>/.worktrees/<task-id> (heals rows already carrying a shared inherited path). Same-branch reuse untouched; degenerate cases (no repo anchor / occupied path IS the task's own canonical worktree) keep the legacy reuse rather than failing dispatch.
  • tests/hermes_cli/test_kanban_worktree_isolation.py — new, 5 cases.

How to Test

  1. Bind a board to a git repo (hermes kanban boards set-default-workdir <slug> <repo>), create a triage card, decompose it into 2+ children with no dependency edges.
  2. Without this patch: child A materializes the shared path on wt/<A>; child B resolves the same directory still on wt/<A> and works there. With it: each child gets .worktrees/<child-id> on its own branch.
  3. python -m pytest tests/hermes_cli/test_kanban_worktree_isolation.py tests/hermes_cli/test_kanban_db.py tests/hermes_cli/test_kanban_decompose_db.py -q → 240 passed.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass — ran the targeted kanban suites above (240 passed, 0 failed); a full-tests/ local run did not complete in my environment
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15 (Darwin 25.5), Python 3.11.14

Documentation & Housekeeping

  • I've updated relevant documentation — N/A: this restores the behavior kanban.md already documents (worktree → one per task id)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no config changes)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) — pure pathlib + git -C subprocess calls, no shell strings, no platform-specific paths
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A (no tool surface changes)

🤖 Generated with Claude Code

Decompose children inherit the root's literal workspace_path (NousResearch#37172),
so every sibling of a worktree-kind root points at the SAME checkout.
_resolve_worktree_workspace's existing-checkout shortcut then reuses
that directory on whatever branch is currently checked out, ignoring
the task's own branch_name. Net effect: sibling workers — which can be
promoted and dispatched concurrently — run in one directory on the
first sibling's branch, with no lock. Work lands on the wrong task's
branch (provenance corruption) and concurrent siblings trample each
other's index/tree.

Fix, two layers:
- decompose_triage_task: worktree-kind children no longer inherit the
  root's literal path; each child materializes its own
  <repo>/.worktrees/<child-id> at dispatch (dir/scratch inheritance
  unchanged — children legitimately share those).
- _resolve_worktree_workspace: when the requested path is an existing
  checkout of a DIFFERENT branch, fall back to a fresh
  <repo>/.worktrees/<task-id> instead of silently reusing it (heals
  rows that already carry a shared path). Same-branch reuse and the
  no-repo/own-path degenerate cases keep the legacy behaviour.

Tests: tests/hermes_cli/test_kanban_worktree_isolation.py (5); full
test_kanban_db.py + test_kanban_decompose_db.py suites pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ahmadashfq
ahmadashfq force-pushed the fix/kanban-decompose-worktree-isolation branch from 1d9d4c8 to fd8dadf Compare July 10, 2026 06:37
@alt-glitch alt-glitch added type/bug Something isn't working comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have labels Jul 10, 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 tracing the existing-checkout shortcut and adding coverage for stale shared paths. The current-main premise is real: decompose_triage_task() copies the root path at hermes_cli/kanban_db.py:5107-5111, while _resolve_worktree_workspace() reuses an existing linked checkout at hermes_cli/kanban_db.py:5485-5487.

Problems

  • The new workspace_path = None branch loses the repository anchor for roots configured with an explicit worktree path. A child without workspace_path must have board.default_workdir; otherwise current resolution raises at hermes_cli/kanban_db.py:5448-5459. This turns a valid explicit-worktree decomposition on a board without a default into a dispatch failure.

Suggested changes

  • Preserve a repository anchor derived from the root path, rather than its concrete linked checkout, so each child resolves to its own .worktrees/<child-id> without depending on board configuration.
  • Add an end-to-end decomposition plus dispatch test for that no-board-default case; the new test currently checks only the stored NULL path.

Automated hermes-sweeper review.

Comment thread hermes_cli/kanban_db.py
child_ws_kind = child.get("workspace_kind") or root_ws_kind
if child.get("workspace_path"):
child_ws_path = child.get("workspace_path")
elif child_ws_kind == "worktree":

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.

Clearing the path also discards the only repo anchor for roots created with an explicit worktree:<path> on a board without default_workdir. _resolve_worktree_workspace() rejects that NULL child path at current-main hermes_cli/kanban_db.py:5454-5459; derive and persist the root repository anchor instead of the root's concrete checkout.

Kyzcreig added a commit to ANG-Ventures/hermes-agent that referenced this pull request Jul 10, 2026
…ee reuse (Greptile #276)

Two review findings from PR #276:

P1 — board-context leak: decompose_triage_task looked up the board's
default_workdir via ambient get_current_board(), which can disagree with
the board the caller's connection is scoped to (a concurrent boards
switch or an outer HERMES_KANBAN_BOARD override) — silently upgrading
children into an unrelated board's workdir. Thread 'board' explicitly:
kanban_decompose.decompose_task resolves the active board ONCE up front
and passes it to connect_closing() AND decompose_triage_task(), matching
how every other board-aware function threads board.

P2 — _ensure_git_worktree reused an existing linked worktree without
checking its branch. A previous failed/interrupted dispatch can leave
the canonical <repo>/.worktrees/<id> on a stale branch; the fresh-per-
task fallback (NousResearch#61907) would then silently run on the wrong branch.
Verify the checkout's branch on reuse and realign (checkout/-b) when it
differs, raising if the switch fails.

Tests: test_decompose_explicit_board_beats_ambient_current_board (P1),
test_ensure_git_worktree_realigns_stale_reused_branch +
_same_branch_reuse_is_noop (P2). Both new tests fail without their fix
(gold-standard verified). Full kanban DB + decompose + worktree suites
green (258 passed).
Kyzcreig added a commit to ANG-Ventures/hermes-agent that referenced this pull request Jul 10, 2026
…ree isolation (#276)

* fix(kanban): inherit board default_workdir when decomposing scratch-workspace tasks

When a triage task is decomposed and its workspace is the default scratch
(no explicit path), auto-decomposed children ended up in the kanban
scratch workspaces folder, causing workers to not find project code and
attempt to git-clone the project.

Fix: in `decompose_triage_task`, after reading the root's workspace, if
it's `scratch` with no explicit path, fall back to the board's
`default_workdir` and upgrade workspace_kind to `dir` for both the
children and the root orchestration task.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 62f73b7)

Co-authored-by: charles <charles@oytaub.home>

* fix(kanban): stop decompose siblings sharing one worktree checkout

Decompose children inherit the root's literal workspace_path (NousResearch#37172),
so every sibling of a worktree-kind root points at the SAME checkout.
_resolve_worktree_workspace's existing-checkout shortcut then reuses
that directory on whatever branch is currently checked out, ignoring
the task's own branch_name. Net effect: sibling workers — which can be
promoted and dispatched concurrently — run in one directory on the
first sibling's branch, with no lock. Work lands on the wrong task's
branch (provenance corruption) and concurrent siblings trample each
other's index/tree.

Fix, two layers:
- decompose_triage_task: worktree-kind children no longer inherit the
  root's literal path; each child materializes its own
  <repo>/.worktrees/<child-id> at dispatch (dir/scratch inheritance
  unchanged — children legitimately share those).
- _resolve_worktree_workspace: when the requested path is an existing
  checkout of a DIFFERENT branch, fall back to a fresh
  <repo>/.worktrees/<task-id> instead of silently reusing it (heals
  rows that already carry a shared path). Same-branch reuse and the
  no-repo/own-path degenerate cases keep the legacy behaviour.

Tests: tests/hermes_cli/test_kanban_worktree_isolation.py (5); full
test_kanban_db.py + test_kanban_decompose_db.py suites pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit fd8dadf)

Co-authored-by: Ahmad <ahmad@madsgency.com>

* fix(kanban): resolve active board default_workdir on decompose + repro tests

The scratch-root board default_workdir fallback (cherry-pick 77b131f40,
upstream NousResearch#42660) called read_board_metadata() with no arg, which
hardcodes the 'default' board — so a triage task on a NAMED board with
a default_workdir never got upgraded and its decomposed children still
landed in disjoint per-task scratch dirs. Resolve the ACTIVE board via
get_current_board(), mirroring the board-resolution create_task() uses
for dir/worktree tasks.

Adds tests/hermes_cli/test_kanban_decompose_workdir_repro.py: the
@SenorStefan repro (X, 2026-05-30) reproduced on a named board, asserting
decomposed siblings resolve to the same board project dir so a dependent
child can read its predecessor's output. These fail without the
active-board resolution above; the no-default board case stays scratch.

* fix(kanban): thread board through decompose + verify branch on worktree reuse (Greptile #276)

Two review findings from PR #276:

P1 — board-context leak: decompose_triage_task looked up the board's
default_workdir via ambient get_current_board(), which can disagree with
the board the caller's connection is scoped to (a concurrent boards
switch or an outer HERMES_KANBAN_BOARD override) — silently upgrading
children into an unrelated board's workdir. Thread 'board' explicitly:
kanban_decompose.decompose_task resolves the active board ONCE up front
and passes it to connect_closing() AND decompose_triage_task(), matching
how every other board-aware function threads board.

P2 — _ensure_git_worktree reused an existing linked worktree without
checking its branch. A previous failed/interrupted dispatch can leave
the canonical <repo>/.worktrees/<id> on a stale branch; the fresh-per-
task fallback (NousResearch#61907) would then silently run on the wrong branch.
Verify the checkout's branch on reuse and realign (checkout/-b) when it
differs, raising if the switch fails.

Tests: test_decompose_explicit_board_beats_ambient_current_board (P1),
test_ensure_git_worktree_realigns_stale_reused_branch +
_same_branch_reuse_is_noop (P2). Both new tests fail without their fix
(gold-standard verified). Full kanban DB + decompose + worktree suites
green (258 passed).

---------

Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
Co-authored-by: charles <charles@oytaub.home>
Co-authored-by: Ahmad <ahmad@madsgency.com>
@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
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #70177 — your commit was cherry-picked onto current main with your authorship preserved in git history (merge SHA 65d42e3). Thanks for both halves: the fan-out fix AND the resolution-time healing for rows already carrying a shared path — the wrong-branch fallback with the degenerate-case guards was exactly the right shape. Live E2E confirmed decomposed siblings now materialize their own worktrees on their own branches, and a legacy shared row heals to a fresh checkout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management 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.

kanban: decompose siblings share one worktree checkout and run on each other's branches

3 participants