Description
GenericAdapter.wait_for_completion (src/automator/adapters/generic.py) can report a dev session completed from the idle-polling shortcut while the native agent window is still alive. Once an idle-grace window is armed, each silent tick does a non-blocking result-artifact check and returns completed the moment the artifact exists (generic.py:238-245, and again at grace expiry, generic.py:260-267) — neither path confirms the window has actually exited (alive is computed at generic.py:221 but only gates the crash branch). The stall fallback shares the hole: _final (generic.py:361-362) upgrades to completed on artifact presence alone.
Three aggravating facts (verified on current main):
- The premature
completed kills the live agent. AgentAdapter.run (src/automator/adapters/base.py:91-96) runs finally: self.kill(handle) — the moment the wait returns, the window is killed. So this is not just a race with post-session reads: the agent is terminated mid-turn, before its remaining commits/cleanup/transcript flush.
- The artifact check runs before the pane-activity re-arm (
generic.py:238 vs generic.py:251), so even a session that is visibly streaming output — demonstrably working — is declared complete and killed on the next idle tick.
- Re-driven specs widen the race to most of the session.
devcontract.find_result_artifact (devcontract.py:193-211) qualifies any spec-*.md carrying an ## Auto Run Result heading with mtime ≥ launch. Escalation re-arm (reset_spec_status) rewrites only the frontmatter status: and leaves the stale ## Auto Run Result section in the file, so on a re-drive the spec qualifies as "terminal" from the agent's first mid-session save — not just in the final minutes.
The engine then enters post-session handling (spec-frontmatter reconcile, sprint sync, verify_dev, usage tally) against files the killed-or-still-writing agent left half-finished; a mis-read there feeds the destructive auto-rollback path.
Surfaced while prototyping the native-Windows multiplexer backend (slower ConPTY timing widens the race), but the bug is platform-neutral — the same interleaving exists on POSIX.
Steps to reproduce
Timing race, so conceptual repro:
- Start a dev session; let it end a turn without a terminal artifact (awaiting a long test, or an early turn-end) → the result-less Stop arms the idle-grace window (
generic.py:322).
- Let the stall nudge re-invoke it (
generic.py:278); the agent resumes working in a new turn while stall_deadline stays armed.
- Mid-turn the agent appends
## Auto Run Result to the spec (a tool call, typically before final prose/cleanup) — or, on a re-driven escalated story, simply saves any edit to a spec that still carries the stale section.
- The next idle tick finds the artifact →
wait_for_completion returns completed, run()'s finally kills the still-working window, and the engine post-processes half-written state.
Expected behavior
Stop events and window-death are the only authoritative completion signals. A terminal artifact observed while the window is still alive is advisory: note it, keep waiting, and complete only once the Stop lands or the window dies. The stall fallback in _final should likewise accept a result only when the window is no longer alive.
Actual behavior
The idle-tick and grace-expiry paths return completed on artifact presence alone; the session window — possibly mid-turn and streaming — is then killed by run()'s finally, and the engine post-processes (and can roll back) a half-written attempt.
Proposed resolution
- Gate the idle/grace terminal-artifact shortcuts on
not self._window_alive(handle); while the window is alive they only re-arm/annotate. (The normal Stop-path completion — artifact + Stop with the window alive — is untouched; artifacts are written by tool calls inside turns, so a legitimate artifact cannot first appear after the final Stop beyond the existing 15 s result grace.)
_final gains an accept_result flag set to "window not alive" for the stall fallback, so a stall verdict can't silently upgrade to completed under a live window.
- Consider stripping the stale
## Auto Run Result section when a spec is re-armed for re-drive, so a re-driven spec can't qualify as terminal from its first save.
- Keep Stop + window-death as the authoritative paths; add focused tests: an idle-tick result under a live window must not complete the session, and a grace-expiry result under a live window must not complete it.
Which area is this for?
CLI adapters & profiles
bmad-auto Version
0.7.12
Which coding CLI are you using?
Claude (claude)
Operating System
Other (platform-neutral; observed timing widens on Windows/ConPTY, same race on Linux/macOS)
Confirm
Description
GenericAdapter.wait_for_completion(src/automator/adapters/generic.py) can report a dev sessioncompletedfrom the idle-polling shortcut while the native agent window is still alive. Once an idle-grace window is armed, each silent tick does a non-blocking result-artifact check and returnscompletedthe moment the artifact exists (generic.py:238-245, and again at grace expiry,generic.py:260-267) — neither path confirms the window has actually exited (aliveis computed atgeneric.py:221but only gates the crash branch). The stall fallback shares the hole:_final(generic.py:361-362) upgrades tocompletedon artifact presence alone.Three aggravating facts (verified on current
main):completedkills the live agent.AgentAdapter.run(src/automator/adapters/base.py:91-96) runsfinally: self.kill(handle)— the moment the wait returns, the window is killed. So this is not just a race with post-session reads: the agent is terminated mid-turn, before its remaining commits/cleanup/transcript flush.generic.py:238vsgeneric.py:251), so even a session that is visibly streaming output — demonstrably working — is declared complete and killed on the next idle tick.devcontract.find_result_artifact(devcontract.py:193-211) qualifies anyspec-*.mdcarrying an## Auto Run Resultheading with mtime ≥ launch. Escalation re-arm (reset_spec_status) rewrites only the frontmatterstatus:and leaves the stale## Auto Run Resultsection in the file, so on a re-drive the spec qualifies as "terminal" from the agent's first mid-session save — not just in the final minutes.The engine then enters post-session handling (spec-frontmatter reconcile, sprint sync,
verify_dev, usage tally) against files the killed-or-still-writing agent left half-finished; a mis-read there feeds the destructive auto-rollback path.Surfaced while prototyping the native-Windows multiplexer backend (slower ConPTY timing widens the race), but the bug is platform-neutral — the same interleaving exists on POSIX.
Steps to reproduce
Timing race, so conceptual repro:
generic.py:322).generic.py:278); the agent resumes working in a new turn whilestall_deadlinestays armed.## Auto Run Resultto the spec (a tool call, typically before final prose/cleanup) — or, on a re-driven escalated story, simply saves any edit to a spec that still carries the stale section.wait_for_completionreturnscompleted,run()'sfinallykills the still-working window, and the engine post-processes half-written state.Expected behavior
Stop events and window-death are the only authoritative completion signals. A terminal artifact observed while the window is still alive is advisory: note it, keep waiting, and complete only once the Stop lands or the window dies. The stall fallback in
_finalshould likewise accept a result only when the window is no longer alive.Actual behavior
The idle-tick and grace-expiry paths return
completedon artifact presence alone; the session window — possibly mid-turn and streaming — is then killed byrun()'sfinally, and the engine post-processes (and can roll back) a half-written attempt.Proposed resolution
not self._window_alive(handle); while the window is alive they only re-arm/annotate. (The normal Stop-path completion — artifact + Stop with the window alive — is untouched; artifacts are written by tool calls inside turns, so a legitimate artifact cannot first appear after the final Stop beyond the existing 15 s result grace.)_finalgains anaccept_resultflag set to "window not alive" for the stall fallback, so a stall verdict can't silently upgrade tocompletedunder a live window.## Auto Run Resultsection when a spec is re-armed for re-drive, so a re-driven spec can't qualify as terminal from its first save.Which area is this for?
CLI adapters & profiles
bmad-auto Version
0.7.12
Which coding CLI are you using?
Claude (claude)
Operating System
Other (platform-neutral; observed timing widens on Windows/ConPTY, same race on Linux/macOS)
Confirm