fix(kanban): stop decompose siblings sharing one worktree checkout - #61907
fix(kanban): stop decompose siblings sharing one worktree checkout#61907ahmadashfq wants to merge 1 commit into
Conversation
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>
1d9d4c8 to
fd8dadf
Compare
teknium1
left a comment
There was a problem hiding this comment.
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 = Nonebranch loses the repository anchor for roots configured with an explicit worktree path. A child withoutworkspace_pathmust haveboard.default_workdir; otherwise current resolution raises athermes_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.
| 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": |
There was a problem hiding this comment.
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.
…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).
…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>
|
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. |
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_progressunlimited by default) end up working in one directory on the first sibling's branch: cross-task commits land on the wrongwt/<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
Changes Made
hermes_cli/kanban_db.py—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/scratchinheritance 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
hermes kanban boards set-default-workdir <slug> <repo>), create a triage card, decompose it into 2+ children with no dependency edges.wt/<A>; child B resolves the same directory still onwt/<A>and works there. With it: each child gets.worktrees/<child-id>on its own branch.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
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass — ran the targeted kanban suites above (240 passed, 0 failed); a full-tests/local run did not complete in my environmentDocumentation & Housekeeping
worktree→ one per task id)cli-config.yaml.exampleif I added/changed config keys — N/A (no config changes)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/Apathlib+git -Csubprocess calls, no shell strings, no platform-specific paths🤖 Generated with Claude Code