Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

status: distinguish stopped runs in text output to match JSON and list

オープン 初心者向け
#973 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
69/100
issue の種類
機能追加
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
python
領域
cli

調査の方向性

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

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

bmad-code-org/bmad-loop のほかの issue

bmad-code-org/bmad-loop の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。