Skip to content

🐛 fix(kanban): keep an edit-in-place repo-root dir card in place, not a worktree - #113

Merged
cwest merged 1 commit into
cwest/integrationfrom
wt/t_c2348da0-fix-kanban-m-a-dir-workspace-card-on-an
Aug 4, 2026
Merged

🐛 fix(kanban): keep an edit-in-place repo-root dir card in place, not a worktree#113
cwest merged 1 commit into
cwest/integrationfrom
wt/t_c2348da0-fix-kanban-m-a-dir-workspace-card-on-an

Conversation

@cwest

@cwest cwest commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Why

_resolve_dir_workspace (hermes_cli/kanban_db.py) redirects any dir
workspace whose workspace_path is a git repo ROOT to a per-task linked worktree
at <repo>/.worktrees/<task-id>, on a wt/<task-id> branch. That redirect is
correct for a shared deploy clone (~/src/office, pinned to main so a
post-merge git pull --ff-only can fast-forward): handing the shared clone to a
worker let the worker git checkout <topic> in place and break the fast-forward.

The same redirect is wrong for an EDIT-IN-PLACE repo. ~/.hermes is the
working tree of cwest/hermes-config: the checkout IS the live running install,
changes land directly in it, and an hourly backup_commit cron owns commit+push
to main. There is no PR flow for this repo. Because ~/.hermes is a repo root,
a card correctly filed dir @ ~/.hermes is silently converted into a wt/*
worktree; the worker then commits to that branch and opens an unwanted PR, and a
stray worktree + branch is left in the live install. The card was filed right —
the resolver produced the wrong vehicle. Observed three times (most recently
2026-08-04: two cards materialized at ~/.hermes/.worktrees/<id> and produced an
unwanted, closed PR against cwest/hermes-config).

What

Teach _resolve_dir_workspace that some repo roots are EDIT-IN-PLACE and must be
returned as-is: no worktree redirect, no branch, and no default-branch/upstream
guard (that guard is meaningless for a checkout that is itself the deploy target).
Every other dir card keeps today's redirect-and-guard behavior exactly.

  • New declaration module hermes_cli/edit_in_place_repos.py — a single source
    of truth: edit_in_place_roots() resolves the explicit set of edit-in-place
    roots to real absolute paths, and is_edit_in_place_root(path) is the predicate
    both the resolver and the homestead filing side assert against.
  • _resolve_dir_workspace consults is_edit_in_place_root(repo_root) before
    the redirect/guard and returns the repo root unchanged when it matches.
  • _resolve_worktree_workspace refuses a worktree-kind card explicitly aimed
    at an edit-in-place root (see decision below).

Design decision #1 — where the declaration lives, and how it avoids a heuristic

The list lives in core (hermes_cli/edit_in_place_repos.py), because the
redirect logic being fixed is in core (kanban_db.py) and the homestead filing
side runs on the same machine/pythonpath and can simply import it. This mirrors
the merge_authority.py precedent (a fact scripts assert against, not prose
restated across skills) — the difference is that merge authority keys on repo
slugs, while edit-in-place keys on local paths, resolved at dispatch time.

The declared fact is "the Hermes home checkout is edit-in-place." It resolves to
real paths via get_default_hermes_root() (honours HERMES_HOME for Docker /
custom / profiled installs) and ~/.hermes under $HOME — the exact two roots
_card_requires_pr already uses for its edit-in-place exclusion, so the two agree
by construction. This is not one of the banned heuristics ("path is under
$HOME/.hermes", "repo has no remote", "a cron touches it"): it is a declared,
enumerated set resolved to concrete paths at call time. Resolving at call time
(not import time) is what makes it profile-correct without a new HERMES_* env var
(repo policy: .env is secrets only; behavioral settings are declared code).

The uncommitted homestead sketch ~/.hermes/scripts/repo_vehicle.py is superseded
by this module and can be removed.

Note (out of scope here, worth a follow-up): _card_requires_pr still computes the
same two roots inline. It could consume edit_in_place_repos.edit_in_place_roots()
to collapse the duplication, but the card scoped _card_requires_pr as
already-correct and out of bounds, so this PR leaves it untouched and only ensures
the new module's root set is identical to it.

Design decision #2worktree-kind aimed at an edit-in-place root: REFUSE

A worktree card explicitly aimed at an edit-in-place root is refused at
resolution time with a clear ValueError, not silently coerced to the in-place
path. Rationale: asking for an isolated worktree/branch on a checkout that IS the
deploy target is contradictory — the card should be workspace_kind='dir'.
Refusing surfaces the filing mistake loudly; coercing would mask it and leave the
caller believing they got an isolated worktree. The error message names the fix
("File this card with workspace_kind='dir' instead").

Done when — evidence

RED-first tests (tests/hermes_cli/test_kanban_edit_in_place_workspace.py)

Both directions, since the risk is a carve-out that is too wide:

  • dir on an edit-in-place root resolves to that exact path; asserts no
    .worktrees/<id> created and no branch set. (Failed before the fix.)
  • Full dispatch path (dispatch_once → claim → resolve → spawn): a dir card
    on an edit-in-place root reaches the worker with the in-place workspace and no
    branch_name persisted; declared anchor unchanged on the row.
  • The guard does not run for an edit-in-place root even on a detached HEAD.
  • worktree-kind on an edit-in-place root is refused.
  • Regression fence (must pass before AND after): a NON-edit-in-place repo root is
    still redirected to <repo>/.worktrees/<id> and the default-branch/upstream
    guard still raises. The pre-existing office-clone fences in
    test_kanban_dir_workspace_worktree.py and
    test_kanban_dir_workspace_worker_run.py are unchanged and green.

Execution proof against the real defect

Isolated probe (temp kanban DB, scrubbed env — the live board and install were
never mutated):

== declaration ==
edit_in_place_roots(): ['/Users/caseywest/.hermes']
is_edit_in_place_root(~/.hermes): True

== AFTER (fixed code), dir @ /Users/caseywest/.hermes ==
resolved workspace: /Users/caseywest/.hermes
== is it the live install itself? == True
== no per-task worktree created: True
== git worktree list UNCHANGED: True

== BEFORE (carve-out disabled — simulates pre-fix, on a mirror clone) ==
resolved workspace (pre-fix): .../hermes-config-mirror/.worktrees/t_d80c6e12
== pre-fix REDIRECTS to <repo>/.worktrees/<id>: True

== the REAL /Users/caseywest/.hermes was NEVER touched by this proof ==
== live worktree list identical to baseline: True

~/.hermes is its own git toplevel and is_edit_in_place_root is True, so the
redirect the mirror demonstrates is the identical redirect that fired against
~/.hermes pre-fix — now suppressed.

Full suite

scripts/run_tests.sh run on this branch and on the merge base (cwest/integration
head). The failing set is identical on both — all pre-existing,
host/environment-specific (approval clusters, computer-use, ACP MCP e2e collection
errors, gateway platform callbacks, live-system guard, anthropic adapter, qwen-oauth
resolution, WSL/service-manager). Zero new failures introduced by this change;
none of the failing files reference the changed code.

Scope

Primary file: hermes_cli/kanban_db.py (_resolve_dir_workspace,
_resolve_worktree_workspace). New: hermes_cli/edit_in_place_repos.py,
tests/hermes_cli/test_kanban_edit_in_place_workspace.py. No change to the office
/ dev-checkout behavior, _card_requires_pr, the kanban CLI, or the backup cron.
No new env var.

… a worktree

`_resolve_dir_workspace` redirected any `dir` workspace whose path is a git
repo ROOT to a per-task `<repo>/.worktrees/<id>` worktree on a `wt/*` branch.
That is correct for a shared deploy clone (isolates the worker from a clone
pinned to its deploy branch), but wrong for an EDIT-IN-PLACE checkout that is
itself the deploy target: `~/.hermes` (the working tree of the live config
repo) is edited in place with an hourly backup cron owning commit+push, and
has no PR flow. Redirecting it made the worker commit to a branch and open an
unwanted PR, leaving a stray worktree + branch in the live install.

Add a declared single source of truth, `hermes_cli/edit_in_place_repos.py`
(`edit_in_place_roots()` / `is_edit_in_place_root()`), resolving the Hermes
home checkout to real paths at call time — a declaration, not a heuristic, and
no new env var. `_resolve_dir_workspace` returns an edit-in-place root
unchanged (no worktree, no branch, guard skipped); every other repo root keeps
the redirect-and-guard behavior. `_resolve_worktree_workspace` refuses a
worktree card aimed at an edit-in-place root as a caller error rather than
silently coercing it.

RED-first tests cover both directions: an edit-in-place dir card resolves in
place through the full dispatch path with no branch persisted, and a
non-edit-in-place repo root still redirects and still guards (the regression
fence for the deploy-clone bug).
@cwest
cwest marked this pull request as ready for review August 4, 2026 13:48

@cwest cwest left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

No changes needed.

The carve-out lands where it should: after the repo-root check and before the fast-forward guard, so an edit-in-place root returns unchanged with no worktree, no branch, and no guard run, while every other repo root falls through to the existing redirect-and-guard path untouched.

I checked the part that matters most here — that the carve-out isn't too wide. is_edit_in_place_root resolves to ~/.hermes alone (the shared root, correctly derived even under a profile home like ~/.hermes/profiles/lamport). ~/src/office and ~/src/hermes-agent both classify False, so they keep redirecting and guarding exactly as before; a subdirectory such as ~/.hermes/skills also classifies False, since only the root itself is the deploy checkout. Exact-root equality is the right test for this predicate, distinct from _card_requires_pr's subtree exclusion — the docstring calls that out.

The tests genuinely catch the defect rather than restating it. Neutering the predicate to always-False drops the five carve-out tests to red while the four regression-fence tests stay green, which is the shape you want: the fence doesn't depend on the carve-out being active. The worktree-kind refusal is a clear error naming the fix rather than a silent coercion.

Ran the targeted file plus both fence files: 25 passed, 0 failed. All CI checks are green (mergeStateStatus CLEAN); the skipped Docker/JS/Docs contexts are the expected skips for a Python-only change. Commit is signed and correctly authored.

One wording nit for later, not blocking: the PR body describes the root set as identical to _card_requires_pr's exclusion. It isn't literally — one is exact-root, the other subtree — though both are correct for their purpose.

@cwest
cwest merged commit f4ab539 into cwest/integration Aug 4, 2026
37 checks passed
@cwest
cwest deleted the wt/t_c2348da0-fix-kanban-m-a-dir-workspace-card-on-an branch August 4, 2026 13:51
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.

1 participant