Skip to content

Plumb paths.project through worktree provisioning so repo_root != project is supported, and retire the refusal #443

Description

@pbean

Supersedes #414, and replaces the refusal that closed it in the short term.

#414 offered two fix shapes: (1) plumb paths.project into provision_worktree for the reads that mirror what init/validate touch, keeping repo_root for git operations; or (2) refuse the combination until (1) exists. The forward-port of the 0.9.1 hotfix took (2)bmadconfig.worktree_isolation_conflict now refuses isolation = "worktree" under a repo_root override at validate, at run/sweep/resume, in the auto-triggered child sweep, in the dry-run banner and in the TUI's pre-launch guard. That was a deliberate scope decision, not a verdict on the design: shape (1) rewrites every disk read in provision_worktree, and it has an open design question shape (2) does not.

This issue is shape (1). Landing it deletes the refusal, and that deletion is part of the work — see "What this removes" below.

The open design question

bmadconfig.load_paths requires project/_bmad/bmm/config.yaml (bmadconfig.py:72-74), so the _bmad/ surface is under project by definition and repo_root/_bmad/ generally does not exist. But "which root wins" is not one answer — it is one answer per surface, and each has to be decided rather than defaulted:

  • the hook relay is pointed at, never copied: init writes project/.bmad-loop/bmad_loop_hook.py (install.py), and provisioning bakes an absolute path from repo_root into the worktree's hook config (worktree_flow.py:142-143). This one is unambiguous — the relay only exists under project.
  • skill trees and _bmad/custom/ overrides are copied from repo_root (worktree_flow.py:238-245, 262-266) and probed under project by missing_base_skills / resolve_review_layers. The preflight is the authority on which layers a run gets, so these follow project.
  • seed_files / seed_globs (worktree_flow.py:196-200) use repo_root as both the copy source and the is_relative_to containment base. These are operator-configured and may legitimately name paths outside project but inside the git root — this is the one where repo_root is arguably still right, and changing it is a behaviour change for existing monorepo users.
  • the worktree destination: a worktree of /mono carries app/, so anything resolving inside the new checkout needs project.relative_to(repo_root) applied. ProjectPaths.rebased (bmadconfig.py:43-63) currently sets project == repo_root == new_root and rebases the artifact dirs relative to the old project — it encodes the assumption that the two are equal, so it cannot express the divergent case at all and has to change with the rest.

What stays on repo_root

Everything that is genuinely a git operation, and this half should not move: workspace.py wholesale (worktree add/remove/prune, branch delete), merge-back and teardown in worktree_flow.py (389, 613, 730) and sweep.py:666, recovery_flow.py:172's workspace.root != self.paths.repo_root isolation predicate, engine.py's discard_worktree, and cli.py's clean-tree gates (1023, 1261), _reconcile_stale (179), --reverify (1813, 1855), the confirm commit (1984) and cmd_clean (2385).

Two pre-existing inconsistencies to settle while here

Both are visible only under a divergent pair, and both currently read a third thing — the raw --project dir — rather than either root:

  • cli.cmd_validate's verify.worktree_clean(project) (cli.py:268) and tui/app.py::_guarded's verify.worktree_clean(self.project) gate the wrong tree, while cmd_run/cmd_sweep gate paths.repo_root. Under an override, validate and the TUI check a directory that may not be a git root at all.
  • [verify] commands run in paths.project (verify.py:1815, 1847, 1887) but cli._reverify runs them in repo_root — the same commands, two cwds.

What this removes

Landing this should delete, not adapt:

  • bmadconfig.worktree_isolation_conflict and all five of its call sites (cli._reject_isolation_conflict at cmd_run/cmd_sweep/_resume_paused_run, the RuntimeError raise in _sweep_factory.factory, and the cmd_validate finding);
  • the policy.isolation-repo-root id from checks.VALIDATE_CHECKS;
  • the _warn_preflight_would_abort insert(0, conflict) leg and the extra paths parameter it needed;
  • the tui/app.py::_guarded refusal block and its except (BmadConfigError, PolicyError, OSError) fall-through;
  • the mutual-exclusion sentences in README.md (Worktree isolation section) and docs/FEATURES.md (Git worktree isolation section);
  • the guard-dependent paragraph in install.py's seed-completeness helper docstring, which currently justifies reading repo_root and paths.project as one by pointing at these guards;
  • the tests that pin the refusal in tests/test_bmadconfig.py, tests/test_cli.py and tests/test_tui_app.py.

worktree_isolation_conflict's docstring names this issue and says it exists to be deleted by it.

Coverage gap to close

No test drives provisioning with repo_root != project (tests/test_bmadconfig.py only asserts the key is parsed). The refusal's tests construct the divergent pair but stop at the refusal, so the seeding path itself has never run under it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:configBMAD config resolution and the setup skillarea:engineOrchestrator engine and run lifecyclebugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions