Skip to content

fix(cli): surface unknown liveness in resolve/delete/archive (win32) - #39

Merged
pbean merged 8 commits into
bmad-code-org:mainfrom
dracic:fix/gh-38-cli-unknown-liveness
Jul 3, 2026
Merged

fix(cli): surface unknown liveness in resolve/delete/archive (win32)#39
pbean merged 8 commits into
bmad-code-org:mainfrom
dracic:fix/gh-38-cli-unknown-liveness

Conversation

@dracic

@dracic dracic commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What

Routes the CLI's resolve / delete / archive commands through a tri-state liveness read so a live-but-unreadable engine pid (unknown) is surfaced instead of silently read as dead — bringing the CLI to parity with the TUI half of the fix.

Why

On native Windows a running engine whose identity is unreadable (psutil.create_time()ERROR_ACCESS_DENIED) reads as tri-state unknown. The CLI gated only on the strict boolean runs.engine_alive, which collapses unknown to False, so resolve could launch a driver against a possibly-live engine (double-drive) and delete/archive proceeded with no warning while the engine may still be writing the run dir. POSIX is unaffected.

Fixes #38

How

  • Added runs.engine_liveness (plus a shared probe_liveness body) wrapping ProcessHost.liveness_of, so the CLI reads tri-state liveness without importing TUI modules; unknown is preserved and an unexpected probe failure degrades to unknown, never a false dead.
  • resolve now refuses on alive or unknown (both possibly-live); delete/archive warn engine may still be live (unverifiable pid) on unknown but still proceed — an unverifiable/recycled pid must never block cleanup forever. A single liveness sample drives both the warning and the strict block, so a mid-check identity flip can't fire one without the other.
  • Routed tui.data.liveness through the shared probe_liveness, keeping one copy of the pid-liveness logic and reading the pid file once.

Testing

pytest tests/test_runs.py tests/test_cli.py tests/test_tui_data.py tests/test_process_host.py — added cases for the tri-state read (no-pid→dead, alive, reused→dead, unreadable→unknown, probe-raises→unknown), resolve refusing on unknown, delete/archive warn-but-proceed on unknown, and the TUI delegation regression. Full suite green (1303 passed; the only failures are a pre-existing skill-sync drift, unrelated to this change).

Summary by CodeRabbit

  • New Features
    • Added a --force option to resolve, allowing operators to continue when engine liveness is unverifiable (unknown) while still blocking provably-live engines.
  • Bug Fixes
    • Engine liveness is now tri-state (live, dead, unverifiable), improving how resume/resolve/delete/archive/cleanup decisions are made.
    • delete, archive, clean, and cleanup/TUI flows now warn for unverifiable PIDs and proceed safely when appropriate.
    • resume now blocks provably-live engines, but warns and recovers when liveness is unverifiable.
  • Tests
    • Updated and expanded CLI/TUI/run liveness coverage for the new tri-state behavior and --force handling.

@coderabbitai

coderabbitai Bot commented Jul 2, 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: 44 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

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

Run ID: fa916460-af5e-4bbc-82a3-fec16bcaf884

📥 Commits

Reviewing files that changed from the base of the PR and between 8a49cea and eeb1f87.

📒 Files selected for processing (6)
  • src/automator/cli.py
  • src/automator/runs.py
  • src/automator/tui/app.py
  • tests/test_cli.py
  • tests/test_runs.py
  • tests/test_tui_app.py

Walkthrough

Adds tri-state engine liveness handling across runs, CLI commands, and TUI cleanup paths, with unknown states now warning instead of failing in several flows. Tests were updated to cover alive, dead, and unknown outcomes.

Changes

Tri-state liveness propagation

Layer / File(s) Summary
Core liveness helpers and tests
src/automator/runs.py, src/automator/process_host.py, tests/test_runs.py
Adds engine_liveness and probe_liveness, centralizes ProcessHost.alive_and_ours on tri-state liveness, and updates run-level tests for dead, alive, and unknown outcomes.
CLI liveness guards and tests
src/automator/cli.py, tests/test_cli.py, tests/test_cleanup.py
Updates resume, resolve, delete, archive, cleanup, and clean command flows to warn or gate on unknown liveness, and refreshes CLI coverage for alive and unknown cases.
TUI liveness and cleanup warnings
src/automator/tui/data.py, src/automator/tui/app.py, src/automator/tui/launch.py, tests/test_tui_data.py, tests/test_tui_app.py, tests/test_process_host.py
Routes TUI liveness through the shared probe helper and adds cleanup-session warnings for unverifiable engine PIDs, with matching TUI and host-behavior tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: pbean

Poem

A bunny saw a PID and gave a soft sniff,
“Alive, dead, or unknown?” — now that’s the new riff.
With warnings for ghosts and a shared little probe,
The cleanup hops onward across every globe. 🐇

🚥 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 clearly matches the main CLI change: surfacing unknown liveness in resolve, delete, and archive on Windows.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@augmentcode

augmentcode Bot commented Jul 2, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Updates the CLI to use a tri-state engine liveness probe on Windows so a live-but-unreadable engine pid reports unknown (and is surfaced to operators) instead of being silently treated as dead, preventing accidental double-driving and unsafe destructive actions.

Changes:

  • Added runs.engine_liveness() plus a shared runs.probe_liveness() wrapper around ProcessHost.liveness_of() to standardize the tri-state probe.
  • CLI resume now blocks on alive and warns (but proceeds) on unknown as the recovery path that rewrites engine.pid.
  • CLI resolve now blocks on alive and blocks on unknown unless the operator confirms via --force.
  • CLI delete/archive/cleanup/clean warn when liveness is unknown but continue (so unverifiable pids don’t deadlock cleanup).
  • Session pruning now returns (killed, live, unknown) from a single partition sample; CLI/TUI messaging is derived from that same sample to avoid TOCTOU drift.
  • TUI data.liveness() delegates to the shared probe (single pid-file read) and degrades ProcessHostError to unknown; the TUI cleanup flow notifies when pruned sessions had an unverifiable pid.
  • Expanded unit tests across CLI/TUI/runs/process-host layers for tri-state behavior, --force flows, and misconfiguration/probe-failure handling.

Technical Notes: POSIX behavior is intended to remain unchanged; the new unknown state is primarily for native Windows where process identity reads can fail with ERROR_ACCESS_DENIED.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode 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.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/automator/cli.py
Comment thread src/automator/cli.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.

🧹 Nitpick comments (1)
src/automator/runs.py (1)

180-187: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Blind except Exception also swallows ProcessHostError misconfiguration.

The try wraps both get_process_host() and .liveness_of(...). A ProcessHostError from get_process_host() (e.g. an invalid BMAD_AUTO_PROCESS_HOST override) is a hard misconfiguration, not a flaky per-pid probe failure — but it's silently downgraded to "unknown" here, which could hide a broken deployment for a long time (every liveness check just looks "unverifiable"). Ruff also flags this as BLE001 (blind except).

Consider narrowing the catch to the probe call only, or letting ProcessHostError propagate.

♻️ Proposed narrowing
 def probe_liveness(pid: int, identity: float | None) -> str:
+    host = get_process_host()  # let ProcessHostError (misconfiguration) propagate
     try:
-        return get_process_host().liveness_of(pid, identity)
-    except Exception:
+        return host.liveness_of(pid, identity)
+    except Exception:  # noqa: BLE001 - deliberately fail-open to 'unknown', see docstring
         return "unknown"
🤖 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/automator/runs.py` around lines 180 - 187, The probe_liveness helper
currently catches every Exception around both get_process_host() and
liveness_of(), which hides ProcessHostError misconfiguration as "unknown".
Narrow the try/except in probe_liveness so get_process_host() is called outside
the blanket handler, and only the actual liveness_of(pid, identity) probe is
downgraded to "unknown"; alternatively, let ProcessHostError propagate unchanged
while preserving the tri-state behavior for transient probe failures.

Source: Linters/SAST tools

🤖 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.

Nitpick comments:
In `@src/automator/runs.py`:
- Around line 180-187: The probe_liveness helper currently catches every
Exception around both get_process_host() and liveness_of(), which hides
ProcessHostError misconfiguration as "unknown". Narrow the try/except in
probe_liveness so get_process_host() is called outside the blanket handler, and
only the actual liveness_of(pid, identity) probe is downgraded to "unknown";
alternatively, let ProcessHostError propagate unchanged while preserving the
tri-state behavior for transient probe failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 65118ce4-5e83-45be-9402-04ddf5079def

📥 Commits

Reviewing files that changed from the base of the PR and between c2b6dd4 and ba22162.

📒 Files selected for processing (6)
  • src/automator/cli.py
  • src/automator/runs.py
  • src/automator/tui/data.py
  • tests/test_cli.py
  • tests/test_runs.py
  • tests/test_tui_data.py

resolve now refuses on a live-but-unverifiable pid (tri-state `unknown`),
closing the double-drive hole; delete/archive warn that the pid is
unverifiable before cleanup but still proceed (`unknown` must never block
cleanup). Adds runs.engine_liveness / probe_liveness (wrapping
ProcessHost.liveness_of) so the CLI reads tri-state liveness without a TUI
import, and routes tui.data.liveness through the shared probe. POSIX
unaffected.

Fixes bmad-code-org#38
@dracic
dracic force-pushed the fix/gh-38-cli-unknown-liveness branch from ba22162 to 7619ead Compare July 2, 2026 20:09
@dracic

dracic commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews — addressed in the latest push:

  • probe_liveness blind except (CodeRabbit): fixed. get_process_host() is now called outside the try, so a ProcessHostError misconfiguration (e.g. an invalid BMAD_AUTO_PROCESS_HOST) propagates loudly instead of being masked as unknown on every check. Only the actual liveness_of() probe still fails open to unknown. Added a regression test (test_engine_liveness now asserts the misconfig propagates).

  • resolve message overstates unknown (augment): fixed. The gate fires on alive or unknown, so the message is now "run … may still be live — stop it first" (accurate for both; the action is unchanged).

  • delete/archive "(unverifiable pid)" wording (augment): left as-is. That block message ("… is still live …") only prints on a confirmed alive engine; the unknown path prints the separate, already-hedged warning "engine may still be live (unverifiable pid)". So the cause attribution is correct where each message fires.

CI is green across all lanes (incl. Windows 3.11–3.14).

pbean and others added 3 commits July 2, 2026 14:11
…verable

resolve: split the single != "dead" gate. A provably-live engine always
blocks (--force never bypasses it); 'unknown' blocks unless the operator
vouches with the new --force. Without an escape hatch a squatted pid with
an unreadable identity would lock resolve out forever: `stop` cannot
verify or clear an unverifiable pid (stop_run's alive_and_ours guard reads
it as not-ours and falls back to mark-stopped without touching engine.pid),
so the old "stop it first" remedy looped back to the same refusal. The TUI
blocks this case with no override, so CLI --force is the only
unknown-resolve escape on any surface.

resume: gate cmd_resume (not _resume_paused_run — that helper is also
resolve's already-gated re-arm path). 'alive' blocks outright: resume
rewrites engine.pid and kills the agent session, double-driving a live
run; the pre-existing hole made the resolve gate trivially bypassable.
'unknown' warns but proceeds — resume is the recovery path that rewrites
engine.pid, so it must stay usable when liveness is unverifiable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… pids in cleanup/clean

Pre-PR, tui.data.liveness had get_process_host() inside its try, so a
ProcessHostError (bad BMAD_AUTO_PROCESS_HOST) read as 'unknown'.
probe_liveness deliberately propagates it — right for CLI decision paths
(main()'s backstop turns it into a clean error), wrong for display: the
dashboard poll worker has no except and Textual's exit_on_error takes the
whole app down on the first tick, and `list` fails outright where it used
to render UNKNOWN rows. data.liveness now catches ProcessHostError and
degrades to 'unknown'; the runs layer keeps failing loud.

cleanup/clean kept collapsing 'unknown' to not-alive with no warning while
delete/archive gained one: prunable_sessions now returns the unknown
subset (classification unchanged — unknown never blocks cleanup) so
cmd_cleanup and the TUI cleanup worker can flag possibly-live sessions
before killing them, and cmd_clean warns per reclaimed run whose liveness
is unverifiable. Residual accepted gap: window-only ctl prune stays on the
boolean gate with no per-window warning (documented inline) — the
session-level warning covers the operator surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two probes encoded the identical decision table by hand (pid<=0,
identity-None degrade, identity match, reused/unreadable), and the test
fake carried a third copy — a drift risk where the binary path (stop,
prune, reclaim) and the tri-state path (resolve/delete/archive gates, TUI)
could silently disagree about the same engine. alive_and_ours is now
exactly liveness_of(pid, identity) == "alive" (provably equivalent on all
branches: gone/reused/unreadable were already not-ours), and _FakeHost
subclasses ProcessHost so both derivations are inherited rather than
mirrored. Also drops a stale engine_alive comment in the delete test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pbean

pbean commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

@dracic thanks — this is a solid implementation of #38, and the review confirmed it does exactly what the issue asked with no POSIX behavior change (the full suite is green on Linux). I ran a deep review pass over it and, rather than round-trip a list of change requests, pushed three commits to your branch (maintainer edits are enabled) addressing what the review surfaced. Summary:

502c5e8 — tri-state resolve/resume gates. Two issues with the != "dead" resolve gate as it stood:

  1. Its remedy couldn't work: for a squatted pid with an unreadable identity, stop takes the mark-stopped fallback (alive_and_ours reads the squatter as not-ours) and engine.pid is never deleted — so "stop it first" looped back to the same refusal forever, with no escape. resolve now distinguishes the states: provably-alive always blocks (never bypassable), unknown blocks unless the operator vouches with a new --force, and the refusal message gives honest recovery guidance.
  2. resume had no liveness gate at all, making the resolve gate trivially bypassable — and resume rewrites engine.pid and kills the agent session, i.e. the exact double-drive the gate exists to prevent. It now blocks on alive and warns-but-proceeds on unknown (it must stay usable there: rewriting the pid file is the recovery path for the unverifiable-pid state).

58d17b2 — misconfig degrade + cleanup warnings. probe_liveness letting ProcessHostError propagate is right for CLI decision paths, but pre-PR the TUI caught it (get_process_host() was inside the old try in data.liveness): post-PR a bad BMAD_AUTO_PROCESS_HOST crashed the whole TUI on the first dashboard poll tick (Textual worker exit_on_error) and broke list. data.liveness now catches ProcessHostError'unknown' at the display layer only. Also extended your delete/archive warning to the other cleanup surfaces: prunable_sessions returns the unknown subset (classification unchanged — unknown still never blocks cleanup) so cleanup (CLI + TUI) and clean flag possibly-live engines before acting.

d9f5dbe — dedup. alive_and_ours is now literally liveness_of(pid, identity) == "alive" (provably equivalent on every branch), and the test _FakeHost subclasses ProcessHost so both derivations are inherited instead of hand-copied — the binary and tri-state probes can no longer drift apart.

Full suite: 1314 passed, 1 skipped; trunk clean. Happy to walk through any of it — and if you disagree with a direction (the --force semantics in particular), say so and we can adjust before merge.

…win32 CI)

alive_and_ours now routes through liveness_of, which probes is_alive when
identity reads None. test_alive_and_ours_matches_only_same_identity stubbed
only identity, so on the native-Windows lanes the real
PosixProcessHost.is_alive fixture hit os.kill(4242, 0) → WinError 87
(production is unaffected: win32 selects WindowsProcessHost/psutil). Stub
is_alive like the adjacent liveness_of test does and assert not-ours for
both sub-branches — gone, and live-but-unreadable ('unknown' must never
read as ours on the strict path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

🧹 Nitpick comments (1)
src/automator/cli.py (1)

857-877: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Good centralization — single liveness sample drives both warn and block.

_stop_or_block_live_engine avoids a warn/block split-brain by sampling engine_liveness once and branching off the same value, which is a clean fix for the identity-flip race the docstring calls out.

One gap: I don't see a test exercising the force=True + alive path where runs.stop_run raises StopRunError/ProcessHostError (only the happy-path force test is present in tests/test_cli.py). Since this helper is now shared by both delete and archive, a regression here silently breaks both commands' force-stop error handling.

🧪 Suggested test to add coverage for the stop-error path
def test_delete_force_stop_error_propagates(tmp_path, monkeypatch, capsys):
    from automator import runs

    monkeypatch.setattr(runs, "engine_liveness", lambda _rd: "alive")
    def _raise(_rd):
        raise runs.StopRunError("boom")
    monkeypatch.setattr(runs, "stop_run", _raise)
    run_dir = _make_run_with_state(tmp_path, "r1")
    assert cli.main(["delete", "--project", str(tmp_path), "r1", "--force"]) == 1
    assert "boom" in capsys.readouterr().err
    assert run_dir.exists()
🤖 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/automator/cli.py` around lines 857 - 877, The shared liveness helper
`_stop_or_block_live_engine` is not covered for the `force=True` path when
`runs.stop_run` fails. Add a test in `tests/test_cli.py` that stubs
`runs.engine_liveness` to return `"alive"` and makes `runs.stop_run` raise
`runs.StopRunError` (and/or `ProcessHostError`), then assert
`cli.main(["delete", ..., "--force"])` returns 1 and the error is printed. This
should exercise the shared helper used by both `delete` and `archive` so the
stop-error handling remains correct.
🤖 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.

Nitpick comments:
In `@src/automator/cli.py`:
- Around line 857-877: The shared liveness helper `_stop_or_block_live_engine`
is not covered for the `force=True` path when `runs.stop_run` fails. Add a test
in `tests/test_cli.py` that stubs `runs.engine_liveness` to return `"alive"` and
makes `runs.stop_run` raise `runs.StopRunError` (and/or `ProcessHostError`),
then assert `cli.main(["delete", ..., "--force"])` returns 1 and the error is
printed. This should exercise the shared helper used by both `delete` and
`archive` so the stop-error handling remains correct.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 24fc2b8d-9013-481f-930a-c51cd4d78fc3

📥 Commits

Reviewing files that changed from the base of the PR and between ba22162 and d9f5dbe.

📒 Files selected for processing (11)
  • src/automator/cli.py
  • src/automator/process_host.py
  • src/automator/runs.py
  • src/automator/tui/app.py
  • src/automator/tui/data.py
  • src/automator/tui/launch.py
  • tests/test_cleanup.py
  • tests/test_cli.py
  • tests/test_runs.py
  • tests/test_tui_app.py
  • tests/test_tui_data.py
✅ Files skipped from review due to trivial changes (1)
  • src/automator/tui/launch.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/automator/tui/data.py

@pbean

pbean commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Follow-up: the first CI round failed on all four native-Windows lanes — test_alive_and_ours_matches_only_same_identity stubbed identity but not is_alive, and the derived alive_and_ours now probes is_alive on the unreadable-identity branch, so the real PosixProcessHost.is_alive fixture hit os.kill(4242, 0)WinError 87 on win32 runners (production unaffected — win32 selects WindowsProcessHost/psutil). Fixed in 6a08feb by stubbing is_alive like the adjacent liveness_of test and asserting not-ours for both sub-branches (gone, and live-but-unreadable). CI is now fully green across all lanes: https://github.com/bmad-code-org/bmad-auto/actions/runs/28622823911

@pbean

pbean commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

augment review

@augmentcode augmentcode 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.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/automator/tui/app.py
if unknown:
self.call_from_thread(
self.notify,
f"{len(unknown)} pruned session(s) had an unverifiable engine pid "

@augmentcode augmentcode Bot Jul 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

src/automator/tui/app.py:566: unknown is sampled from runs.prunable_sessions() before runs.prune_sessions() re-partitions; if liveness changes between calls, this notification can claim “pruned session(s) had …” for sessions that weren’t actually pruned. Consider tying the warning list/count to the sessions actually killed (or wording it as “sessions eligible for pruning had …”).

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

pbean and others added 2 commits July 2, 2026 15:21
prune_sessions now returns (killed, unknown) from its single internal
partition sample, and the TUI cleanup worker drops its separate
prunable_sessions pre-sample. Previously a liveness flip between the two
samples could make the 'pruned session(s) had an unverifiable engine pid'
notification name sessions that were never pruned (or miss ones that
were). Same single-sample principle as _stop_or_block_live_engine;
cmd_cleanup keeps its pre-prune warning, which is state-descriptive by
design and needed for --dry-run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_stop_or_block_live_engine's force=True + alive branch returns 1 when
stop_run raises, but only the happy-path force was tested; a regression
would silently break both commands' force-stop error handling. Covers
StopRunError via delete and ProcessHostError via archive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pbean

pbean commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Addressed the two new review comments in caa3eb9 + 8a49cea:

caa3eb9 — cleanup unknown-pid warning tied to the pruned partition (augment, tui/app.py:566). The TOCTOU was real: the notification's unknown set came from a prunable_sessions() sample taken before prune_sessions() re-partitioned internally, so a liveness flip between the two samples could make "pruned session(s) had an unverifiable engine pid" name sessions that were never pruned (or miss ones that were). Rather than soften the wording, prune_sessions now returns (killed, unknown) from its own single partition sample and the TUI worker drops its pre-sample — same "one sample drives both the warning and the action" principle as _stop_or_block_live_engine (502c5e8). cmd_cleanup keeps its pre-prune warning: it's needed for --dry-run and its wording describes state rather than action, so it survives the race either way. Regression test: test_prune_sessions_returns_unknown_from_same_sample.

8a49cea — force-stop error-path tests (CodeRabbit, cli.py:857-877). Added test_delete_force_stop_error_blocks (StopRunError via delete) and test_archive_force_stop_error_blocks (ProcessHostError via archive) — both caught exception types and both commands' wiring through the shared helper, asserting exit 1, the error on stderr, and the run dir untouched.

Full suite green locally (1317 passed) and trunk clean.

@pbean

pbean commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

@CodeRabbit review
augment review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

@pbean Thanks for the detailed writeup and for pushing the fixes directly — I'll kick off a fresh review pass over the latest commits (tri-state resolve/resume gates, misconfig degrade, cleanup warnings, dedup, and the TOCTOU fix) now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@augmentcode augmentcode 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.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/automator/cli.py Outdated
print(f"leaving {len(live)} live session(s) untouched")
return 0
killed = runs.prune_sessions(project)
killed, _ = runs.prune_sessions(project)

@augmentcode augmentcode Bot Jul 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

src/automator/cli.py:931: cmd_cleanup() warns from a prunable_sessions() sample but then prunes via a fresh prune_sessions() sample, so the “unverifiable pid” warnings (and live count) can drift from what was actually killed/left if liveness changes between calls. Consider deriving the warning set (and any related messaging) from the unknown returned by prune_sessions() on the non---dry-run path so the operator-facing output always matches the performed actions.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

…ition

prune_sessions now returns the full (killed, live, unknown) partition and
cmd_cleanup routes both paths through a single call (dry_run= suppresses
the kill). Previously the unknown warnings and the 'left N live
session(s) untouched' count came from a prunable_sessions sample taken
before prune_sessions re-partitioned, so a liveness flip between the two
samples could report a pruned session as left untouched or kill a
newly-unknown session without warning. Completes the single-sample story
from the TUI fix; the pre-sample rationale (needed for --dry-run)
dissolves now that the dry-run path gets the same partition back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pbean

pbean commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Addressed the augment follow-up in eeb1f87:

cmd_cleanup double-sample (augment, cli.py:931). Fair catch — my earlier state-not-action rationale covered the unknown warnings but not the live count: "left N live session(s) untouched" is an action claim printed from a pre-prune sample, so a live→dead flip between the two samples could report a pruned session as left untouched (and a dead→unknown flip could kill a session with no warning). Rather than warn from the returned unknown on just the non-dry-run path, prune_sessions now returns the full (killed, live, unknown) partition and cmd_cleanup routes both paths through a single call (dry_run= suppresses the kill) — every message now derives from the same sample that drove the action, and the CLI no longer touches prunable_sessions directly. Net simplification; this also retires the "needed for --dry-run" argument, since the dry-run path gets the same partition back.

Full suite green locally (1317 passed) and trunk clean.

@pbean

pbean commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

augment review

@augmentcode augmentcode 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.

Review completed. No suggestions at this time.

Comment augment review to trigger a new review at any time.

@pbean
pbean merged commit bde29e2 into bmad-code-org:main Jul 3, 2026
11 checks passed
@dracic
dracic deleted the fix/gh-38-cli-unknown-liveness branch July 3, 2026 05:35
@pbean pbean mentioned this pull request Jul 3, 2026
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.

CLI resolve/delete/archive: surface unknown liveness (win32) — TUI/CLI parity follow-up to #36

2 participants