Skip to content

fix(runs,tui): tag project ownership with a transportable digest - #523

Merged
pbean merged 4 commits into
bmad-code-org:mainfrom
dracic:fix/419-transportable-project-tag
Aug 11, 2026
Merged

fix(runs,tui): tag project ownership with a transportable digest#523
pbean merged 4 commits into
bmad-code-org:mainfrom
dracic:fix/419-transportable-project-tag

Conversation

@dracic

@dracic dracic commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The project tag stored the resolved path, and psmux refuses a value that cannot cross its CLI->server control line verbatim (#418). Filter those refusal shapes through Path.resolve() on Windows and one survives: a spaced UNC share, \srv\share name\proj. There every session went untagged, and untagged is weak ownership twice over (#419) — it dies with the run dir, so an orphaned session leaked once clean removed the directory, and it proves ownership by run-id collision on disk rather than by identity, so a reused --run-id let one project prune another's session.

project_tag now returns a 16-hex sha256 prefix of the resolved path, which clears the transport gate by construction. Both read sites (runs.prunable_sessions, the TUI's ctl-window prune) compare against accepted_tags — digest plus the legacy resolved-path form — so a tag written by an earlier release still proves ownership: the ctl session is long-lived and shared across projects, and without legacy acceptance its windows would read as another project's after an upgrade.

The transport gate itself is unchanged — it remains the general contract for @ session options; it is simply no longer the project tag's expected path. The untagged run-dir fallback also stays, for state whose tag write failed or predates a working one.

Tests: tag shape is hex whatever the path (with the refused UNC literal asserted as the premise) and clears the psmux gate; legacy path tags stay owned and foreign paths/digests stay foreign on both read sites; existing prune-partition and ctl-window tests pass unedited.

Closes #419

Summary by CodeRabbit

  • Bug Fixes
    • Improved project ownership tagging for sessions and control windows, including projects with unusual path characters.
    • Prevented transport corruption and ownership collisions between different projects.
    • Preserved recognition and safe pruning of existing sessions using legacy path-based tags.
    • Ensured untagged windows continue to require a project-local run directory before pruning.
    • Clarified that unsupported session option values are rejected and read as unset.

The project tag stored the resolved path, and psmux refuses a value that
cannot cross its CLI->server control line verbatim. Filter those refusal
shapes through Path.resolve() on Windows and one survives: a spaced UNC
share, \srv\share name\proj. There every session went untagged, and
untagged is weak ownership twice over — it dies with the run dir, so an
orphaned session leaked once `clean` removed the directory, and it proves
ownership by run-id collision on disk rather than by identity, so a reused
--run-id let one project prune another's session.

project_tag now returns a sha256 prefix of the resolved path, which clears
the transport gate by construction. Read sites compare against accepted_tags
so a tag written by an earlier release still proves ownership: the ctl
session is long-lived and shared across projects, and without that its
windows would read as another project's after an upgrade.

The gate itself is unchanged — it remains the general contract for `@`
session options, it is simply no longer the project tag's expected path.

Closes bmad-code-org#419
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pbean, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 37 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1eb051aa-cec5-496d-be29-0ed3bafbc6de

📥 Commits

Reviewing files that changed from the base of the PR and between aa617fc and 468709f.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src/bmad_loop/tui/launch.py
  • tests/test_runs.py
  • tests/test_tui_launch.py

Walkthrough

Project ownership tags now use transport-safe 16-character SHA-256 digests. Session and window pruning accepts both digest tags and legacy path tags. Untagged-state fallback behavior remains unchanged.

Changes

Project ownership tagging

Layer / File(s) Summary
Digest tag generation and transport contract
src/bmad_loop/runs.py, src/bmad_loop/adapters/psmux_backend.py, docs/multiplexer-backends.md, tests/test_runs.py, tests/test_psmux_backend.py, src/bmad_loop/adapters/tmux_base.py
Resolved project paths now produce fixed-length hexadecimal digests. Transport rejection reads as unset. Tests cover unusual paths, digest uniqueness, and transport behavior.
Current and legacy tag pruning
src/bmad_loop/runs.py, src/bmad_loop/tui/launch.py, tests/test_runs.py, tests/test_tui_launch.py
Pruning accepts current digest tags and legacy path tags. Untagged sessions and windows retain their existing run-directory handling. Tests cover legacy ownership and foreign tags.
Release documentation
CHANGELOG.md
The changelog describes digest-based ownership, transport safety, and legacy-tag compatibility.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: pbean

Poem

A rabbit marks each project bright,
With digest tags that travel right.
Old path tags still pass the gate,
Foreign sessions meet their fate.
Safe pruning keeps the burrow light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: replacing project ownership tags with a transportable digest.
Linked Issues check ✅ Passed The PR implements #419's preferred transportable digest tags and legacy path-tag acceptance to prevent ownership failures.
Out of Scope Changes check ✅ Passed The changes remain within the linked issue scope and include related documentation, comments, tests, and compatibility updates.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bmad_loop/adapters/psmux_backend.py (1)

519-527: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Raise when session-option cleanup fails.

_write_scoped() only prints a warning when set-option -u fails. The existing value can then remain set. Line 523 incorrectly states that the option reads as unset. A stale non-empty project tag prevents the untagged ownership fallback and can leave a session unprunable.

  • src/bmad_loop/adapters/psmux_backend.py#L519-L527: Propagate a typed TmuxError when the unset write fails. State that the option reads as unset only after a successful cleanup.
  • docs/multiplexer-backends.md#L79-L88: Document the successful-cleanup behavior and the loud failure path.

As per coding guidelines, “At I/O boundaries, fail loudly: use typed escalation rather than bare except; observation may degrade, but repair writes must raise.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bmad_loop/adapters/psmux_backend.py` around lines 519 - 527, The cleanup
path in _write_scoped must propagate a typed TmuxError when unsetting the
non-transportable session option fails, and only report the option as unset
after successful cleanup; update src/bmad_loop/adapters/psmux_backend.py lines
519-527 accordingly. Document this successful-cleanup behavior and loud failure
path in docs/multiplexer-backends.md lines 79-88.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/bmad_loop/adapters/psmux_backend.py`:
- Around line 519-527: The cleanup path in _write_scoped must propagate a typed
TmuxError when unsetting the non-transportable session option fails, and only
report the option as unset after successful cleanup; update
src/bmad_loop/adapters/psmux_backend.py lines 519-527 accordingly. Document this
successful-cleanup behavior and loud failure path in
docs/multiplexer-backends.md lines 79-88.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f896a50b-5761-42d6-887b-6f6f1b59e301

📥 Commits

Reviewing files that changed from the base of the PR and between 99fa36e and 42643ac.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • docs/multiplexer-backends.md
  • src/bmad_loop/adapters/psmux_backend.py
  • src/bmad_loop/runs.py
  • src/bmad_loop/tui/launch.py
  • tests/test_psmux_backend.py
  • tests/test_runs.py
  • tests/test_tui_launch.py

@dracic

dracic commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai The _write_scoped finding is rejected — the never-raise contract there is deliberate, not a gap:

  • Not silent. A failed set-option -u prints its own second warning (_write_scoped, psmux_backend.py:542-545). The comment block above it (:530-538) documents the warn-never-raise choice: these verbs are best-effort because raising out of set_session_option would abort a run launch over tag hygiene. That matches the TerminalMultiplexer ABC contract, which explicitly permits option-write refusal as degradation — this is an observation-side boundary, not a repair write.
  • The branch is unreachable for the only real caller. @bmad_project is the only @ session option ever written, and this PR's digest clears the transport gate by construction — that is the point of the change. Hitting the described state needs a future non-transportable @ option, plus a pre-seeded foreign value under that exact key in the user's psmux config, plus that one -u write failing after the session-create writes succeeded.
  • Failure direction is safe. A stale foreign tag makes the prune skip the session — a leak with two stderr warnings, never a wrong kill. psmux: the session-level project tag may bleed the same way the window tag did (#310) #320 documented unset-on-refusal as the correct degradation for exactly this reason; escalating the free to a raise would trade a bounded leak for a failed launch.

The one fair nit — the refusal warning states "the option reads as unset" before the free has succeeded — is contradicted visibly by the second warning whenever the free fails, so we're leaving the wording as is.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@dracic Understood. The never-raise behavior is intentional and conforms to the TerminalMultiplexer degradation contract.

A failed cleanup emits a second warning. It can only preserve a stale foreign tag, which makes pruning skip the session. It cannot cause an incorrect kill. The digest-based @bmad_project tag also prevents this branch for the current caller.

I withdraw the finding.

🐇


✏️ Learnings added
Learnt from: dracic
URL: https://github.com/bmad-code-org/bmad-loop/pull/523

Timestamp: 2026-08-10T12:26:13.686Z
Learning: In `src/bmad_loop/adapters/psmux_backend.py`, `_write_scoped` intentionally treats failed option writes and cleanup (`set-option -u`) as warn-never-raise operations. `TerminalMultiplexer.set_session_option` permits write refusal as degradation because raising can abort a run launch. If cleanup fails, the stale foreign project tag makes pruning skip the session rather than kill it. The only current `@` session option, `bmad_project`, uses a transport-safe digest tag.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@pbean

pbean commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@pbean

pbean commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Bookkeeping, no change requested here: I filed #526 to carry forward the two pieces of #419
this PR does not implement, so the parent's auto-close on merge does not orphan them.

Neither is a defect in this diff — direction (1) is what this PR set out to do, and #419 itself
says (1) and (3) are not exclusive.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 42643acbbf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

bmad-code-org#518 landed on main and rewrote `runs.project_tag` for the same class of
defect from the other end: a project path the *listing* cannot carry was
conditionally percent-encoded, while every transportable path stayed
byte-identical so stored tags kept comparing equal.

The digest subsumes that. Encoding answered only the listing round trip, so
a path the listing carries but psmux's control line refuses — the spaced UNC
share this branch exists for — still went untagged. A 16-hex digest clears
both transports by construction, and the compatibility objection encoding was
shaped around is answered on the read side by `accepted_tags`.

So the encoding half of bmad-code-org#518 is removed, not merged alongside: `_survives_listing`,
`_TAG_ENCODED_PREFIX` and the now-dead `sys`/`quote` imports go with it. bmad-code-org#518's
other halves are independent and untouched — the bounded field split in
`BaseTmuxBackend.list_windows`, the whole-run-id compare, and the bmad-code-org#482 control
window identity work.

Test coverage is ported rather than dropped: bmad-code-org#518's line-separator family now
asserts the digest shape and a single-row round trip against the same
`_SEP_VALUES` table, and the surrogate case was already covered by this
branch's transportability test. The `tmux_base` bounded-split comment no longer
claims PROJECT_OPTION holds a path, and states the bound as the seam's standing
contract, since no field a caller requests today can hold a tab.

CHANGELOG: the two Unreleased entries describing the same tag are folded into
one, since both are unreleased and the second described a mechanism this merge
removes.
@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Merged main into this branch (maintainer edit on the fork). The conflict was not textual — #518 landed while this was open and rewrote runs.project_tag for the same class of defect from the other end, so the two designs had to be reconciled rather than merged.

The two designs. #518 returns the resolved path byte-identically when it survives a listing round trip, and percent-encodes (%enc%…) only the paths it cannot — a line separator, or a byte invalid in the filesystem encoding. This branch returns a 16-hex sha256 of the resolved path unconditionally, plus accepted_tags() for legacy path acceptance.

Why the digest wins. Encoding answers only the listing half. The path this branch exists for — a spaced UNC share — passes _survives_listing (a space is neither a separator nor a bad byte), so project_tag handed the raw path to psmux's _transportable gate, which refuses it for holding \\ and a space. Verified against merged main (2b13893):

transportable=False  '\\\\srv\\share name\\proj'      <- #523's motivating path, still refused
_survives_listing=True                               <- so #518 leaves it raw, never encoded

The session therefore still went untagged after #518. A digest clears both transports by construction — no separator, no tab, no byte outside ASCII, and it satisfies the control-line gate. The compatibility objection encoding was shaped around (rewriting every tag strands the ones already stored on live sessions and windows) is answered on the read side by accepted_tags instead.

What was removed: _survives_listing, _TAG_ENCODED_PREFIX, and the sys/quote imports that died with them. What was not: #518's other halves are independent and untouched — the bounded field split in BaseTmuxBackend.list_windows, the whole-run-id compare, and the #482 control-window identity work.

Coverage is ported, not dropped. #518's line-separator family now asserts the digest shape and a single-row round trip over the same _SEP_VALUES table (test_project_tag_carries_a_path_the_listing_cannot_carry); the surrogate case was already covered here by test_project_tag_is_transportable_whatever_the_path. Ablating the digest back to a raw path fails 12 of those cases.

accepted_tags deliberately stays two members — a %enc%-spelled tag is not accepted. Only a path the listing could not carry was ever spelled that way, that spelling never reached a release, and an unaccepted tag reads as foreign, which skips a session rather than pruning it. Fail-safe, and it clears on the next tag write.

The two Unreleased CHANGELOG entries describing this same tag are folded into one, since both are unreleased and the second described the mechanism this merge removes. The tmux_base bounded-split comment no longer claims PROJECT_OPTION holds a path.

Post-merge on this head: full suite 4946 passed / 0 failed, pyright 0 errors, trunk check --all clean.

@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa617fcdf9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/bmad_loop/runs.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/test_tui_launch.py (1)

895-897: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Two docstrings still describe the superseded percent-encoding design.

runs.project_tag now hashes the resolved path and does not encode it. Its own docstring states that the digest "subsumes the conditional percent-encoding this function briefly applied".

Two claims here contradict that:

  • Line 896-897 states that the separators are carried "by project_tag's encoding", and that "two different mechanisms carry them". Under the digest there is one mechanism: no spelling of the project path reaches the listing at all.
  • Line 917 states "project_tag encodes the tag instead".

Both tests still pass, because a hex digest survives the listing. Only the stated reason is wrong. The parametrization at lines 879-883 stays valuable as a path-input matrix, so keep it.

📝 Proposed docstring correction
-    Parametrized over all six on purpose: two different mechanisms carry them —
-    the tab by the backends' bounded field split, the separators by project_tag's
-    encoding — so one spelling passing says nothing about another."""
+    Parametrized over all six on purpose: each is a byte a resolved project path
+    can legally hold, and project_tag hashes the path rather than carrying it, so
+    none of them reaches the listing. The matrix pins that the digest is the
+    single mechanism, rather than one spelling happening to survive."""
-    Reach and scoping are not a trade: project_tag encodes the tag instead, so
-    the comparison stays exact and this row is simply not ours.
+    Reach and scoping are not a trade: project_tag hashes the resolved path, so
+    the tag is listing-safe by construction, the comparison stays exact, and this
+    row is simply not ours.

Also applies to: 917-918

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_tui_launch.py` around lines 895 - 897, Update the docstrings for
the parametrized tests around the path-input matrix and the `project_tag`
behavior to remove references to percent-encoding or multiple mechanisms.
Explain that `project_tag` hashes the resolved path into a digest, so no path
spelling or separators reach the listing; preserve the existing parametrization
and test behavior.
src/bmad_loop/tui/launch.py (1)

382-382: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use runs.accepted_tags(project) in ctl_window_id

_ctl_window_candidates accepts both current digest and legacy resolved-path tags, but ctl_window_id compares only the current digest. A legacy-tagged control window is therefore prunable but unreachable by attach_plan and kill_ctl_window. Use tag in runs.accepted_tags(project).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bmad_loop/tui/launch.py` at line 382, Update ctl_window_id to use
runs.accepted_tags(project) when matching control-window tags, allowing both
current digest and legacy resolved-path tags; replace the single-tag comparison
while preserving the existing attach_plan and kill_ctl_window lookup behavior.
🧹 Nitpick comments (2)
tests/test_runs.py (1)

648-649: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Keep Windows coverage for valid separator values.

The function-level win32 skip disables every parameter case. If the supported Windows filesystem accepts any of \x85, \u2028, or \u2029, this skips valid regression coverage. Confirm the supported filename rules, then skip only invalid parameter values.

The PR objective requires transport coverage for Windows paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_runs.py` around lines 648 - 649, Replace the function-level
Windows skip on the parametrized separator test with per-parameter skipping
based on the supported Windows filename rules, retaining coverage for valid
values such as \x85, \u2028, or \u2029. Preserve the existing _SEP_VALUES and
_SEP_IDS parameterization and ensure Windows transport-path coverage remains
enabled for accepted separators.
CHANGELOG.md (1)

191-206: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Condense the entry and use an imperative lead.

As per coding guidelines, CHANGELOG.md entries must remain “terse, scannable, and imperative.” This entry is a long implementation narrative and starts with a declarative sentence. Keep it under Unreleased, but summarize the digest behavior, legacy-tag compatibility, and pruning result in a short release note.

Suggested shorter entry
- **A project path the multiplexer cannot carry no longer strands the scans over it (`#419`).**
- ...
+ **Use a 16-character SHA-256 prefix for project ownership tags (`#419`).**
+ Preserve legacy path tags during pruning. Prevent transport failures from leaving
+ sessions unowned or crossing project boundaries.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 191 - 206, Rewrite the changelog entry for issue
`#419` under Unreleased as a short, scannable imperative release note. Summarize
that path tags now use a 16-hex digest, legacy path-shaped tags remain
supported, and pruning correctly retains and removes owned sessions/windows
without the current implementation narrative.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/bmad_loop/tui/launch.py`:
- Line 382: Update ctl_window_id to use runs.accepted_tags(project) when
matching control-window tags, allowing both current digest and legacy
resolved-path tags; replace the single-tag comparison while preserving the
existing attach_plan and kill_ctl_window lookup behavior.

In `@tests/test_tui_launch.py`:
- Around line 895-897: Update the docstrings for the parametrized tests around
the path-input matrix and the `project_tag` behavior to remove references to
percent-encoding or multiple mechanisms. Explain that `project_tag` hashes the
resolved path into a digest, so no path spelling or separators reach the
listing; preserve the existing parametrization and test behavior.

---

Nitpick comments:
In `@CHANGELOG.md`:
- Around line 191-206: Rewrite the changelog entry for issue `#419` under
Unreleased as a short, scannable imperative release note. Summarize that path
tags now use a 16-hex digest, legacy path-shaped tags remain supported, and
pruning correctly retains and removes owned sessions/windows without the current
implementation narrative.

In `@tests/test_runs.py`:
- Around line 648-649: Replace the function-level Windows skip on the
parametrized separator test with per-parameter skipping based on the supported
Windows filename rules, retaining coverage for valid values such as \x85,
\u2028, or \u2029. Preserve the existing _SEP_VALUES and _SEP_IDS
parameterization and ensure Windows transport-path coverage remains enabled for
accepted separators.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ae3bac30-e86d-497c-a5f5-5ede661d4e3f

📥 Commits

Reviewing files that changed from the base of the PR and between 42643ac and aa617fc.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • src/bmad_loop/adapters/psmux_backend.py
  • src/bmad_loop/adapters/tmux_base.py
  • src/bmad_loop/runs.py
  • src/bmad_loop/tui/launch.py
  • tests/test_psmux_backend.py
  • tests/test_runs.py
  • tests/test_tui_launch.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/test_psmux_backend.py
  • src/bmad_loop/adapters/psmux_backend.py
  • src/bmad_loop/runs.py

`ctl_window_id` compared the stored window tag against `project_tag(project)`
alone, so a control window tagged before the digest read as foreign and `a`
and `x` could no longer reach this project's own orchestrator — while
`_ctl_window_candidates`, which accepts the legacy tag, would happily prune
the same window. Reachable and pruned-but-unreachable are the wrong pair.

The site is one main added (bmad-code-org#482 window identity) after this branch forked, so
the branch's read-side update never covered it and the merge did not conflict.
Both readers now go through `accepted_tags`; the writers keep minting the
current digest. Scoping is unchanged: a nonempty tag outside the accepted set
is still foreign, so a stop cannot cross a project boundary.

Found by codex review on the merge commit.
@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 52 minutes.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 351ffba31a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Two test docstrings still explained the tag's safety as percent-encoding, and
one credited the tab case to the backends' bounded split. Under the digest
neither spelling of the path reaches the listing, so there is one mechanism,
not two. Both tests passed either way — only the stated reason was wrong.

The bmad-code-org#419 CHANGELOG entry is cut roughly in half: the transports and the
user-facing consequence stay, the blow-by-blow of which byte defeats which
parse goes. Headline keeps its noun-phrase form, which is the section's
convention (Unreleased opens 54x "A", 22x "The", 7x "An", 0x imperative).

Also narrows a skip reason that overclaimed: NEL, U+2028 and U+2029 are not
illegal in win32 names, so the skip rests on the property being a POSIX-name
concern rather than on a filesystem rule.

Found by CodeRabbit review on the merge commit.
@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@coderabbitai Triaged all four findings from the aa617fc review. Three applied in 468709f, one declined.

1. ctl_window_id should use accepted_tags (Major) — already fixed in 351ffba. Correct, and codex found the same defect independently. That review was against aa617fc, which predates the fix. Both readers now go through accepted_tags; the inventory is symmetric — writers generic.py:422 and launch.py:766 mint the digest, readers runs.py:381, launch.py:382, launch.py:662 accept current + legacy.

2. Stale percent-encoding docstrings (Minor) — valid, fixed. Good catch, and it found real drift my own sweep missed: I had grepped for the identifiers (_survives_listing, _TAG_ENCODED_PREFIX, %enc%) but not the concept, so prose saying "project_tag's encoding" and "project_tag encodes the tag instead" survived. Rewrote both. I kept your framing that the digest is the single mechanism, but phrased the parametrization rationale so it still says what a revert would break, since that is what makes the matrix worth keeping.

3. CHANGELOG length (Trivial) — the length half is valid, the imperative half is declined. Entry is cut roughly in half; the transports and the user-facing consequence stay, the byte-by-byte parse narrative goes.

The imperative-headline part is a false positive against this repo's actual convention. Measured over the Unreleased section:

54 A     22 The     7 An     ...     0 imperative

Rewriting this one headline to "Use a 16-character SHA-256 prefix…" would make it the sole outlier in ~90 entries. AGENTS.md's "terse, scannable, imperative" governs the body, not the bolded headline. Noun-phrase headline retained deliberately.

4. Per-parameter Windows skip (Trivial) — declined, with one correction taken. You are right that \x85, U+2028 and U+2029 are not illegal in win32 names, and my skip reason overclaimed that they were — narrowed it to rest on the property being a POSIX-name concern instead.

Not splitting the skip per parameter, though: it would encode an assertion about which of these bytes a Windows filesystem accepts, which I cannot validate from this machine, and a wrong guess turns a green Windows job red for a reason unrelated to the tag. The sibling test on main (test_a_delimiter_in_the_project_path_does_not_hide_its_own_window) uses the same function-level skip over an overlapping parameter set, so this stays consistent with the established pattern rather than diverging on an unverifiable rule. The digest's Windows behaviour is covered by test_project_tag_is_transportable_whatever_the_path, which runs everywhere.

On 468709f: 4947 passed / 0 failed, pyright 0 errors, trunk check --all clean.

@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== relevant files =='
fd -a -t f '^(runs|launch|test_runs|test_tui_launch|AGENTS|CHANGELOG)\.(py|md)$|^AGENTS\.md$|^CHANGELOG\.md$' . \
  | sort

printf '%s\n' '== recent relevant commits =='
git show --no-patch --oneline 351ffba 468709f aa617fc 2>&1 || true

printf '%s\n' '== outlines =='
for file in \
  src/bmad_loop/runs.py \
  src/bmad_loop/tui/launch.py \
  tests/test_runs.py \
  tests/test_tui_launch.py
do
  if [ -f "$file" ]; then
    echo "-- $file"
    ast-grep outline "$file" --items all --type function --match 'accepted_tags|project_tag|prunable_sessions|ctl_window_id|test_project_tag|test_prunable|test_a_delimiter' || true
  fi
done

printf '%s\n' '== ownership readers/writers and stale encoding prose =='
rg -n -C 4 --glob '*.py' \
  'accepted_tags\s*\(|project_tag\s*\(|bmad_project|PROJECT_OPTION|encoding|encodes|percent' \
  src/bmad_loop tests || true

printf '%s\n' '== changelog unreleased and guidance =='
if [ -f AGENTS.md ]; then sed -n '1,220p' AGENTS.md; fi
if [ -f CHANGELOG.md ]; then
  sed -n '1,180p' CHANGELOG.md
fi

Length of output: 174438


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== relevant commit summaries =='
git show --no-patch --format='%h %s' 351ffba 468709f aa617fc

printf '%s\n' '== runs ownership implementation =='
sed -n '330,410p' src/bmad_loop/runs.py

printf '%s\n' '== TUI ownership readers and writer =='
sed -n '350,400p' src/bmad_loop/tui/launch.py
sed -n '635,680p' src/bmad_loop/tui/launch.py
sed -n '745,780p' src/bmad_loop/tui/launch.py

printf '%s\n' '== all accepted_tags call sites =='
rg -n -C 3 'accepted_tags\s*\(' src/bmad_loop/runs.py src/bmad_loop/tui/launch.py

printf '%s\n' '== revised test documentation and Windows skip =='
sed -n '620,735p' tests/test_runs.py
rg -n -C 5 'skipif|skip\(|POSIX.*name|delimiter.*project.*path' tests/test_runs.py tests/test_tui_launch.py

printf '%s\n' '== digest changelog entry and nearby headline convention =='
rg -n -C 4 -i 'digest|psmux.*tag|project.*tag|ownership tag' CHANGELOG.md
sed -n '1,90p' CHANGELOG.md

printf '%s\n' '== diff of applied follow-up commit =='
git show --format=fuller --stat --find-renames 468709f
git show --format= --unified=5 468709f -- \
  src/bmad_loop/runs.py src/bmad_loop/tui/launch.py tests/test_runs.py tests/test_tui_launch.py CHANGELOG.md

Length of output: 57395


@pbean Confirmed.

  • ctl_window_id, _ctl_window_candidates, and prunable_sessions now use runs.accepted_tags(project). The legacy-path compatibility rule is consistent across the ownership readers.
  • Commit 468709f removes the stale percent-encoding explanations. The remaining test prose correctly describes the digest as the single tag mechanism.
  • The shortened #419 entry keeps the required behavior and compatibility details. Its noun-phrase headline matches the surrounding Unreleased convention. I accept the decline of the imperative-headline point.
  • The Windows skip reason is now accurate. Keeping the function-level skip is consistent with the related test and does not require unverified per-character Windows filename assertions.

I verified the source and documentation changes. I did not independently run the reported test, pyright, or trunk commands.


✏️ Learnings added
Learnt from: pbean
URL: https://github.com/bmad-code-org/bmad-loop/pull/523

Timestamp: 2026-08-11T05:52:58.875Z
Learning: In this repository, `CHANGELOG.md` uses noun-phrase bold headlines in the `Unreleased` section. Although `AGENTS.md` requires changelog entries to be terse, scannable, and imperative, apply the imperative guidance to entry content without making a noun-phrase headline an outlier.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 468709f000

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@pbean
pbean merged commit 814f0a2 into bmad-code-org:main Aug 11, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants