Skip to content

provision_worktree crashes on a dangling symlink in a skill tree #416

Description

@pbean

Found while reviewing PR #406 (the skills seed gate). Not a regression from that PR — the copy loop is byte-identical on release/0.9.x.

What happens

provision_worktree's BASE_SKILLS copy loop (src/bmad_loop/install.py) skips a destination that already exists:

for skill in BASE_SKILLS:
    dst = tree_dir / skill
    if dst.exists():
        continue
    src = (repo_root / tree / skill).resolve()
    if not src.is_relative_to(repo_root) or not src.is_dir():
        continue
    _copy_traversable(src, dst)

Path.exists() follows symlinks, so a dangling symlink answers False — but the path is still occupied, and the copy then raises.

Measured

Worktree carries a tracked dangling symlink at <tree>/bmad-review; the repo has a real dir there:

dst.exists() on dangling symlink -> False | is_symlink -> True
provision_worktree -> RAISED FileExistsError: [Errno 17] File exists: .../wt/.claude/skills/bmad-review

Why it matters

Provisioning has no failure vocabulary of its own — every containment violation in that loop is a bare continue, and the two seed-shortfall conditions report through the worktree-seed-skipped channel so the engine can decide escalate-vs-defer. An unhandled FileExistsError bypasses all of it and takes the run out through a path the state machine never sees.

The trigger is ordinary: a committed symlink whose target is only present on the machine that authored it (a shared BMad install that moved) is dangling in every other checkout, and git stores and checks out the dangling link happily.

Shape of a fix

Test dst.is_symlink() or dst.exists() rather than dst.exists() alone, and decide deliberately which side of the skip a dangling link belongs on. Note the same .exists() reasoning is used for MODULE_SKILLS one loop above.

Related: the seed-shortfall reporting in that loop is base_skills_seed_incomplete in the same file.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: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