status: distinguish stopped runs in text output to match JSON and list
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
調査の方向性
src/bmad_loop/cli.py の7740〜7747行付近にある cmd_status のテキスト分岐を読み、src/bmad_loop/documents.py の346〜355行付近にあるJSONステータスビルダーと状態の扱いを比較してください。停止した実行を再現し、既存のステータステストを調べてください。テキストステータスが停止した実行を識別し、完了済みのステータスは変更されず、他のステータス表示と一致することが完了条件です。
索引モデルが issue の本文から書いたものです。
説明
Summary
Enhancement request: distinguish a recorded stop in the text output of bmad-loop status. After a clean bmad-loop stop, text status prints in progress (or interrupted), while state.json records stopped: true and status --json and list report stopped. The CLI reference documents the current text fallback; this request changes that documented presentation so an operator can distinguish a stopped, resumable run from an active or interrupted run without switching status surfaces. The missing crashed arm is source-derived and was not exercised.
This is not specific to tickets mode: cmd_status builds the status line from the run state the same way for every run type. The repro uses a tickets-mode project only because the test suite's helpers give a zero-token sandbox for it.
Environment
bmad-loop a5a8f097, Windows 11 Enterprise 10.0.26200, PowerShell 7.6.6 (SHELL unset), Python 3.13.2, git 2.49.0.windows.1; real bmad-loop run, stop, status and list CLI calls, sessions served by bmad-loop's MockAdapter (zero tokens). Linux: the Ubuntu/WSL transcript below also shows stopped=True with text status in progress (or interrupted); Linux status --json and list output are not included in that excerpt.
Steps to reproduce
Run from PowerShell 7 with SHELL removed. From a bmad-loop checkout at a5a8f097 after uv sync --all-extras, save the script below as repro.py in the checkout root and run:
Remove-Item Env:SHELL -ErrorAction SilentlyContinue
$env:PYTHONUTF8 = '1'; $env:PYTHONPATH = 'tests'
uv run python repro.py
The script builds committed v7 projects with one ticket (1.1) under $env:TEMP\bmad-loop-repro (override with REPRO_ROOT) and runs two cases, each through the real CLI in its own process with a scripted build session (no LLM). Positive control: bmad-loop run --story 1.1 runs to the end (--story checks no epic, so the run finishes rather than pausing at the epic boundary). Trigger: the same run with a [verify] command that sleeps 40 s; once it is running, bmad-loop stop <run> (a hard stop). For each run it prints the state.json flags, the status line, the status field of status --json, and the run's list row.
# bmad-loop repro: run from a bmad-loop checkout (uv sync --all-extras), PowerShell 7:
# Remove-Item Env:SHELL -ErrorAction SilentlyContinue; $env:PYTHONUTF8='1'; $env:PYTHONPATH='tests'; uv run python repro.py
import json, os, subprocess, sys, time
from pathlib import Path
from conftest import EntrySpec, EpicSpec, install_bmad_v7, install_build_auto_v7_skill, write_ticket_tree
ROOT = Path(os.environ.get("REPRO_ROOT") or Path(os.environ.get("TEMP", "/tmp")) / "bmad-loop-repro")
ROOT.mkdir(parents=True, exist_ok=True)
os.environ.pop("SHELL", None)
# Keep the user's global git config out of the sandbox, as the test suite's fixtures do.
os.environ["GIT_CONFIG_GLOBAL"] = str(ROOT / "no-such-gitconfig")
os.environ["XDG_CONFIG_HOME"] = str(ROOT)
def git(root, *args):
return subprocess.run(["git", "-C", str(root), *args], check=True, capture_output=True, text=True).stdout
def sandbox(name, policy=""):
"""A committed v7 project with one ticket, `1.1`, in epic `epic-cart`."""
root = ROOT / name
if root.exists():
subprocess.run(["cmd", "/c", "rmdir", "/s", "/q", str(root)] if os.name == "nt" else ["rm", "-rf", str(root)])
root.mkdir(parents=True)
(root / "src.txt").write_text("original\n", encoding="utf-8")
(root / ".gitignore").write_text(".bmad-loop/runs/\n_bmad/custom/*.user.toml\n", encoding="utf-8")
git(root, "init", "-q", "-b", "main")
git(root, "config", "user.email", "t@t")
git(root, "config", "user.name", "t")
write_ticket_tree(install_bmad_v7(root), (EpicSpec(1, "epic-cart", "Cart", (EntrySpec(1, "Cart model"),)),))
install_build_auto_v7_skill(root)
(root / ".bmad-loop").mkdir()
(root / ".bmad-loop" / "policy.toml").write_text(
'[stories]\nsource = "tickets"\n\n[notify]\ndesktop = false\n\n[review]\nenabled = false\n\n' + policy,
encoding="utf-8")
git(root, "add", "-A")
git(root, "commit", "-q", "-m", "sandbox")
return root
def fake_session(spec):
"""A scripted bmad-build-auto (no LLM): writes the plan at `built` and commits its work."""
from bmad_loop.adapters.base import SessionResult
cwd, ref = Path(spec.cwd), spec.env["BMAD_LOOP_STORY_KEY"]
found = json.loads(subprocess.run(
[sys.executable, "-I", "-B", "-X", "utf8", "_bmad/method/scripts/tickets.py", "--project-root", str(cwd), "find", ref],
cwd=cwd, check=True, capture_output=True, text=True).stdout)
plan, base = Path(found["plan"]), git(cwd, "rev-parse", "HEAD").strip()
with open(cwd / "src.txt", "a", encoding="utf-8") as f:
f.write(f"impl {ref}\n")
plan.write_text(f"---\nticket: {found['id']}\nstatus: built\nbaseline_revision: {base}\n---\n\n# plan\n\nwork\n", encoding="utf-8")
git(cwd, "add", "-A")
git(cwd, "commit", "-q", "-m", f"build {ref}")
return SessionResult(status="completed", result_json={"workflow": "auto-dev", "spec_file": str(plan), "status": "built"})
def bmad_loop(root, *argv, background=False, show=True):
"""One real `bmad-loop` CLI call in its own process; every session role is served by
`fake_session` through bmad-loop's MockAdapter, so nothing spawns an agent."""
cmd = [sys.executable, __file__, "--child", argv[0], "--project", str(root), *argv[1:]]
if background:
return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
p = subprocess.run(cmd, capture_output=True, text=True)
if show:
print(f"$ bmad-loop {' '.join(argv)} -> rc={p.returncode}")
print(" " + (p.stdout + p.stderr).strip().replace("\n", "\n "))
return p
if len(sys.argv) > 1 and sys.argv[1] == "--child":
from bmad_loop import cli
from bmad_loop.adapters.mock import MockAdapter
adapter = MockAdapter([fake_session] * 20)
cli._make_adapters = lambda *a, **k: {role: adapter for role in cli.ROLES}
sys.exit(cli.main(sys.argv[2:]))
def run_id(root):
return sorted((root / ".bmad-loop" / "runs").iterdir())[-1].name
def report(root):
rid = run_id(root)
st = json.loads((root / ".bmad-loop" / "runs" / rid / "state.json").read_text(encoding="utf-8"))
print(f"state.json: finished={st.get('finished')} stopped={st.get('stopped')} paused_stage={st.get('paused_stage')}")
text = bmad_loop(root, "status", rid, show=False).stdout
print("status :", next(l for l in text.splitlines() if l.startswith("status:")))
print("status --json: status =", json.loads(bmad_loop(root, "status", "--json", rid, show=False).stdout)["status"])
print("list :", [l for l in bmad_loop(root, "list", show=False).stdout.splitlines() if rid in l][0])
# Positive control: a run that finishes (`--story` checks no epic, so it ends finished, not at the epic boundary).
print("### control: a run that finishes")
root = sandbox("finishes")
bmad_loop(root, "run", "--story", "1.1")
report(root)
# Trigger: a hard `bmad-loop stop` while a [verify] command runs (a 40 s sleep).
print("\n### trigger: a run stopped with `bmad-loop stop`")
root = sandbox("stopped", f"[verify]\ncommands = ['\"{sys.executable}\" .git/slow-verify.py']\n")
(root / ".git" / "slow-verify.py").write_text(
"import os, time\nopen(os.path.join('.git', 'in-verify'), 'w').close()\ntime.sleep(40)\n", encoding="utf-8")
run = bmad_loop(root, "run", "--story", "1.1", background=True)
marker = root / ".git" / "in-verify"
try:
for _ in range(300):
if marker.exists() or run.poll() is not None:
break
time.sleep(0.2)
if not marker.exists() or run.poll() is not None:
raise SystemExit(f"setup failed: the run never reached the verify command (rc={run.poll()})")
bmad_loop(root, "stop", run_id(root))
out, _ = run.communicate(timeout=120)
finally:
if run.poll() is None: # never leave the run (and its sleeping child) behind
subprocess.run(["taskkill", "/PID", str(run.pid), "/T", "/F"] if os.name == "nt"
else ["kill", "-9", str(run.pid)], capture_output=True)
run.communicate()
print(f"run exited rc={run.returncode}: " + out.strip().replace("\n", " | "))
report(root)
Output (Windows, 2/2 identical apart from run ids):
### control: a run that finishes
$ bmad-loop run --story 1.1 -> rc=0
run <run> starting (attach: bmad-loop attach)
run <run>: 1 done, 0 deferred, 0 escalated, 0 tokens
state.json: finished=True stopped=False paused_stage=None
status : status: finished
status --json: status = finished
list : <ref> story finished <run>
### trigger: a run stopped with `bmad-loop stop`
$ bmad-loop stop <run> -> rc=0
run <run> stopped
run exited rc=0: run <run> starting (attach: bmad-loop attach) | run <run>: 0 done, 0 deferred, 0 escalated, 0 tokens
state.json: finished=False stopped=True paused_stage=None
status : status: in progress (or interrupted)
status --json: status = stopped
list : <ref> story stopped <run>
Current vs proposed
| actual | proposed | |
|---|---|---|
status <run> after bmad-loop stop |
status: in progress (or interrupted) |
a stopped status line, matching status --json (stopped), list (stopped) and the documented end state of a hard stop (docs/reference/run-state.md, Stopping a run: "End state: stopped, resumable") |
status <run> after a finished run (control) |
status: finished |
status: finished |
The CLI reference (docs/reference/cli.md, status) lists in progress (or interrupted) as one of the four status lines, so the text itself is documented; the problem is that it is the only status surface that does not report a recorded stop. The derived run status that list and the TUI use: https://github.com/bmad-code-org/bmad-loop/blob/a5a8f09781e151eb5691f55d55316f47f9f16f1c/docs/reference/run-state.md#derived-run-status
This is a presentation enhancement to the documented status command; the reproduction does not show that stop failed or that persisted run state is wrong.
Linux
This excerpt covers the stopped flag and text status line only; it does not establish parity for every status surface.
Ubuntu under WSL (git 2.43.0, tmux 3.4, Python 3.13.2), a run stopped with bmad-loop stop while a [verify] command was running, verbatim (the other runs printed the same status line):
### hard stop while waiting on: verify
$ bmad-loop stop 20261018-160347-4b68 -> rc=0
run exited rc=0:
state: stopped = True | paused_stage = None
paused_reason: None
status: in progress (or interrupted)
Source
src/bmad_loop/cli.py:7740-7747 (a5a8f097), the text branch of cmd_status: finished, paused and a pending graceful stop have an arm; a stopped (or crashed) run falls through to the last line. https://github.com/bmad-code-org/bmad-loop/blob/a5a8f09781e151eb5691f55d55316f47f9f16f1c/src/bmad_loop/cli.py#L7737-L7747
if state.finished:
print("status: finished")
elif state.paused:
print(f"status: PAUSED ({state.paused_stage}) — {display_pause_reason(state)}")
elif graceful_pending:
print("status: in progress — graceful stop pending (will stop after the current item)")
else:
print("status: in progress (or interrupted)")
For contrast, the --json builder (src/bmad_loop/documents.py:346-355) maps both flags: https://github.com/bmad-code-org/bmad-loop/blob/a5a8f09781e151eb5691f55d55316f47f9f16f1c/src/bmad_loop/documents.py#L346-L355
if state.finished:
status = "finished"
elif state.paused:
status = "paused"
elif state.crashed:
status = "crashed"
elif state.stopped:
status = "stopped"
else:
status = "in-progress"
Suggested fix
Not validated: add elif state.crashed: and elif state.stopped: arms to the text branch in the same order as the --json builder (for example status: stopped (resumable)), and add them to the status-line list in docs/reference/cli.md.
- 主要言語
- Python
- スター
- 160
- フォーク
- 68
- 平均マージ
- 1日 6時間
- マージ済み PR(30日)
- 90
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
bmad-code-org/bmad-loop のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
bmad-code-org/bmad-loop#965 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
bmad-code-org/bmad-loop#835 ·
メンテナーはふだん 1 日以内に返信
-
area:adapters area:psmux bug P4
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
bmad-code-org/bmad-loop#673 · コメント 8 件 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 36/100
bmad-code-org/bmad-loop#972 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 1〜2日 初心者へのやさしさ 49/100
bmad-code-org/bmad-loop#971 ·
メンテナーはふだん 1 日以内に返信
bmad-code-org/bmad-loop の issue をすべて見る
似ている issue
-
infrastructure
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
khuisman/mcp-gee-sweet#1152 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
-
backroom bug
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
ditto-assistant/ditto-subnet#3069 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 67/100
open-webui/open-webui#32901 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
0.3.3 bug
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信