Skip to content

An unreadable repo skill dir aborts provision_worktree mid-loop and leaves the worktree short #417

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. Sibling of #416, same loop.

What happens

provision_worktree's BASE_SKILLS copy loop calls _copy_traversable(src, dst) with no IO error handling. A repo-side skill dir the process cannot read raises out of provisioning, part-way through the loop.

Measured

Repo skill dir chmod 000 on bmad-review-edge-case-hunter, everything else normal:

provision_worktree -> RAISED PermissionError: [Errno 13] Permission denied:
    .../repo/.claude/skills/bmad-review-edge-case-hunter
worktree tree after the raise -> [bmad-build-auto, bmad-dev-auto, bmad-loop-resolve,
    bmad-loop-setup, bmad-loop-sweep, bmad-review-adversarial-general,
    bmad-review-edge-case-hunter]
BASE_SKILLS expected      -> [bmad-build-auto, bmad-dev-auto, bmad-review,
    bmad-review-adversarial-general, bmad-review-edge-case-hunter,
    bmad-review-verification-gap]

Two distinct problems in that output:

  1. The loop aborts, so bmad-review and bmad-review-verification-gap — later in iteration order, nothing wrong with either — are never copied.
  2. The victim is left half-copied. bmad-review-edge-case-hunter exists in the worktree as a directory whose contents are incomplete. Every seed check in this area asks is_dir(), so a partial skill reads as present: base_skills_seed_incomplete cannot report it, and the run-start preflight already passed against the repo. That is the shape the renderer-scripts sentinel exists to catch for _bmad/scripts/ — a partial unit being worse than an absent one.

Why it matters

Same as #416: this escapes the worktree-seed-skipped channel entirely, so the engine never gets to choose escalate-vs-defer, and it is an environment fault — the seed reads the same repo for every story, so no repair session fixes it and no later story escapes it.

Unreadable skill trees are not exotic: a shared machine-wide BMad install owned by another user, or a root-owned install read by an unprivileged run, produces exactly this.

Shape of a fix

Decide the doctrine deliberately — this is the "observation degrades, repair raises" question. If provisioning is repair, the raise wants to be a typed fault the engine can journal and pause on rather than a bare OSError through the state machine; if it degrades, the per-skill failure wants to land in the skipped-seed channel and let the existing gate name it. Either way the half-copied destination should not be left where an is_dir() probe reads it as complete.

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