Skip to content

fix: report worktree_seed entries skipped as no-ops#233

Merged
pbean merged 1 commit into
bmad-code-org:mainfrom
Polloinfilzato:fix/warn-on-skipped-worktree-seed
Jul 21, 2026
Merged

fix: report worktree_seed entries skipped as no-ops#233
pbean merged 1 commit into
bmad-code-org:mainfrom
Polloinfilzato:fix/warn-on-skipped-worktree-seed

Conversation

@Polloinfilzato

@Polloinfilzato Polloinfilzato commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What

provision_worktree now returns the seed_files entries it skipped because the destination
already existed, and the engine journals them as worktree-seed-skipped.

No behaviour change — nothing new is copied.

Why

Fixes #230.

Copy-when-absent is right for a file the checkout legitimately carries, and
test_provision_worktree_seed_does_not_clobber_existing states the reason: no diff is merged
back.

For a directory entry the same rule is over-broad. A worktree checks out tracked files, so a
seed directory with any tracked child already exists — and the entry is then skipped whole,
including the children that are absent and would clobber nothing. worktree_seed = ["_bmad"]
with _bmad/custom tracked is the natural value to write, and it copies nothing at all.

Nothing reported it: the local seeded list only feeds the git-exclude patterns, so the skip
left no trace in stdout, the journal, or validate. The configuration reads as applied and is
not. In my case the isolated sessions re-created the missing tree themselves, once per story, so
every run stayed green while paying for the repair.

How

  • provision_worktree gained a list[str] return: entries whose source existed but whose
    destination did too.
  • The engine journals them under worktree-seed-skipped, alongside worktree-opened.
  • Reported, not printed. The docstring is explicit that this function is quiet because it
    runs inside the engine loop under a TUI, so a print would corrupt the display — the journal
    is where this signal belongs, next to worktree-open-failed and sprint-status-unknown-keys.
  • not src.exists() and dst.exists() are now separate branches. A missing source is a
    different case (already covered by its own test) and is not a no-op worth reporting.
  • Glob matches are deliberately not reported. A plugin's seed_globs pattern is expected to
    hit paths the checkout already carries, so that skip is routine rather than a misconfiguration.
    The signal stays specific to hand-written worktree_seed entries.

I stopped at reporting rather than the alternative in #230 (recursing into an existing directory
with per-child copy-when-absent). That one changes behaviour and _copy_traversable's file
branch would need a per-child dst.exists() guard to keep the no-clobber property — happy to
follow up with it if you'd prefer that shape.

Testing

Three tests added in tests/test_install.py:

  • a skipped file entry is reported;
  • a seed directory with a tracked child is reported and its absent children stay absent —
    the case from the issue, pinned as current behaviour;
  • a successful seed reports nothing, and a missing source is not reported either.

Existing seeding tests pass unchanged, including
test_provision_worktree_seed_does_not_clobber_existing, so the no-clobber guarantee is intact.

  • uv run pytest -q2678 passed, 23 skipped
  • trunk fmt and trunk check → no issues

One unrelated note: tests/test_engine.py::test_rollback_or_pause_resolved_auto_recovers fails
on my machine because it matches git's English "did not match" while my git is localised. It
fails the same way on a clean main and passes under LC_ALL=C, so it is untouched by this
change — but the suite isn't locale-independent. Happy to open that separately if it's useful.


Written with AI assistance, reviewed line by line before submitting.

Summary by CodeRabbit

  • Bug Fixes

    • Improved worktree setup reporting when seed files are skipped because destination paths already exist.
    • Added journal events identifying skipped seed entries for better visibility.
    • Ensured directory seeds are handled consistently without copying unintended files.
  • Tests

    • Added coverage for skipped, copied, directory, and missing seed-file scenarios.

provision_worktree returns the seed_files entries that named a real source
but were skipped because the destination already existed, and the engine
journals them as worktree-seed-skipped.

Copy-when-absent is right for a file the checkout legitimately carries. For
a DIRECTORY entry it is skipped whole the moment any child is tracked — and
a worktree checks out tracked files, so `worktree_seed = ["_bmad"]` where
_bmad/custom is tracked copies nothing at all, including the children that
are absent and would clobber nothing. Nothing reported it: the local
`seeded` list only feeds the git-exclude patterns.

Reporting rather than printing because provision_worktree is quiet by
contract (it runs under the TUI), so the journal is where the signal
belongs. Glob-expanded matches are deliberately excluded: a plugin's glob is
expected to hit paths the checkout already carries, so that skip is routine.

Behaviour is unchanged — nothing new is copied.

Fixes bmad-code-org#230
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7b7b6321-49bb-4b08-8f2b-ccdb7bf1963a

📥 Commits

Reviewing files that changed from the base of the PR and between 03d7297 and 7a19205.

📒 Files selected for processing (3)
  • src/bmad_loop/engine.py
  • src/bmad_loop/install.py
  • tests/test_install.py

Walkthrough

Worktree provisioning now reports seed entries skipped because destinations already exist. Isolated runs capture these entries and append a worktree-seed-skipped journal event. Tests cover file skips, directory skips, successful copies, and missing sources.

Changes

Worktree seed reporting

Layer / File(s) Summary
Seed skip contract and behavior
src/bmad_loop/install.py, tests/test_install.py
provision_worktree returns skipped seed paths while preserving no-clobber behavior, with tests covering files, directories, copied content, and absent sources.
Isolated-run journal integration
src/bmad_loop/engine.py
_run_isolated records non-empty skipped seed results in a worktree-seed-skipped journal event.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Engine._run_isolated
  participant provision_worktree
  participant Journal
  Engine._run_isolated->>provision_worktree: provision worktree seeds
  provision_worktree-->>Engine._run_isolated: return skipped entries
  Engine._run_isolated->>Journal: append worktree-seed-skipped event
Loading

Suggested reviewers: pbean

Poem

A rabbit found seeds by the worktree door,
“Already here!”—so it copied no more.
It logged every skip with a hop and a cheer,
While fresh little files appeared bright and clear.
🐇🌱

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: reporting skipped worktree_seed entries as no-ops.
Linked Issues check ✅ Passed The PR matches issue #230's minimal fix by surfacing skipped seed entries without changing the existing copy behavior.
Out of Scope Changes check ✅ Passed All changes stay within worktree seeding and journaling, plus tests covering the same behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pbean pbean left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Validated line-by-line against main plus a sweep over every consumer surface — approving.

What I checked beyond the diff:

  • The return-type change is inert everywhere else: _run_isolated is the only production caller, and no existing test consumes the return.
  • The new journal kind can't break a consumer: there is no closed event-name enum anywhere — diagnose histograms kinds via an open Counter, the TUI journal view falls back to dim styling for unknown kinds, and the CLI resume readers only special-case their own kinds. No test pins an exact journal-kind sequence. In diagnose, entries routes through sanitize.scrub_json and relative seed paths don't trip the leak guard.
  • No false-positive path: the one thing that could have made this event misleading is a re-provision of an already-seeded worktree reporting every entry as skipped. That can't happen — resume reconstructs the workspace via _reopen_unit without provisioning, and open_unit_workspace always mounts fresh (git worktree add refuses an existing path; that failure defers with worktree-open-failed). Every reported skip is genuine.
  • Cross-platform: a pure pathlib branch split, and reporting the TOML-authored relative entries (rather than OS-native paths) is the right call. Full suite green locally (2694 passed) and on CI's Linux + real-Windows legs.

Two non-blocking notes:

  1. The signal isn't quite "specific to hand-written worktree_seed entries": the seed list the engine passes also carries adapter-profile seed_files (under scm.seed_adapter_defaults, e.g. .mcp.json from the claude profile) and plugin seeds — so a project that tracks one of those files journals a skip for it once per story. Benign and honest; just noting the commit message's claim only holds vs. globs. Tagging entry origin could be a future refinement if this ever gets noisy.
  2. Nothing load-bearing enumerates journal kinds, so no doc change is required. docs/tui-guide.md's journal section is the natural home for a one-liner if you want one — equally happy to take that as a follow-up.

On your two offers: filing both as follow-up issues — one for the option-B shape from #230 (per-child copy-when-absent), which is yours if you'd like it; and one for the locale failure you hit. On the latter, it's better than you thought: the English match lives in production, not the test — safe_rollback's preserve-restore tolerates the benign checkout failure by substring-matching "did not match", so under a localized git that benign no-op raises GitError in the rollback path. Good catch.

Thanks for an unusually well-argued issue + PR pair.

@pbean
pbean merged commit a7fba70 into bmad-code-org:main Jul 21, 2026
9 checks passed
dracic pushed a commit to dracic/bmad-auto that referenced this pull request Jul 22, 2026
…atching English (bmad-code-org#236)

safe_rollback tolerated a benign `git checkout <snapshot> -- <preserve-dir>`
no-op (a preserve dir holding only untracked files) by substring-matching git's
English "pathspec ... did not match" message. `_run_git` ran git with the
inherited environment, so under a localized git (e.g. LANG=it_IT.UTF-8) the
message is translated, the substring never matches, and the benign no-op raises
GitError — turning a resolvable re-drive into the rollback-pause path.

Force LC_ALL=C at the sole git spawn point (_run_git) so every message the
orchestrator inspects stays stable English, making the whole class of
locale-fragile parsing impossible rather than patching the one instance. The
locale is merged last so it wins over both the inherited environment and the
explicit env the two _git_env callers pass (their throwaway GIT_INDEX_FILE /
synthetic commit identity vars are preserved by the spread).

Regression coverage spies on the actual subprocess env rather than the parent
LANG, so the guarantee holds even on a CI box with no non-English locale
catalogs installed (where a plain LANG=it_IT test would emit English anyway and
pass without the fix).

Refs bmad-code-org#233.
Polloinfilzato added a commit to Polloinfilzato/bmad-loop that referenced this pull request Jul 22, 2026
A `worktree_seed` entry naming a DIRECTORY was a total no-op whenever its
destination existed — and it always does, because a worktree checks out the
tracked children. The gitignored children that are absent, and would clobber
nothing, were skipped along with them (bmad-code-org#230). bmad-code-org#233 made that skip visible;
this makes it stop happening.

`_copy_traversable` gains an opt-in `skip_existing` guard so the no-clobber
property holds at FILE granularity, and returns whether it wrote anything. The
guard is opt-in rather than baked in because `install_into --force-skills`
rmtree's the destination precisely to overwrite it; only the seeding call
passes it, so every other call site is byte-identical.

Reporting follows the same rule as before, one level down: an entry that seeded
even one child is applied configuration and is no longer reported skipped-whole,
while an entry that still copied nothing stays reported. Per-child skips are not
reported — the checkout is expected to carry its tracked children, the same
routine-noise argument that excluded globs in bmad-code-org#233. A partially-seeded entry
still gets its exclude pattern written, so the newly seeded children stay out of
the unit's `git add -A`.

`seed_globs` deliberately keeps its whole-entry behaviour: bmad-code-org#235 scopes this to
`worktree_seed`.

Fixes bmad-code-org#235.
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.

A worktree_seed directory entry is skipped entirely when any child is tracked

2 participants