Rename open to stalled and autopilot to auto-confirm; add pushed state; declare idempotency; propose maturity and projects model (RBT-WP-0008)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Work Records / validate (push) Successful in 14s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 295952@bnt-lap001
Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
tegwick 2026-09-29 12:19:58 +02:00
parent f39d5cec3c
commit f5619e1a52
19 changed files with 429 additions and 105 deletions

View file

@ -244,8 +244,6 @@ def exercise_update(run: Run, today: date | None = None) -> tuple[str, str]:
raise RunError("every step must be done (not skipped) to exercise the pack")
if run.data.get("deviations"):
raise RunError("a run with deviations (overrides, waivers, push-through) cannot exercise the pack")
if any(v.get("pushed_through") for v in run.data["steps"].values()):
raise RunError("a run with pushed-through steps cannot exercise the pack")
today = today or date.today()
old = run.pack.path.read_text()
new = STATUS_LINE.sub(f"exercise_status: exercised {today.isoformat()} by {run.data['operator']}",
@ -346,8 +344,9 @@ def finalize(run: Run, outcome: str, note: str = "") -> None:
raise RunError("a run succeeds only when every step is done (skipped steps do not count)")
if outcome == "succeeded" and any(g["state"] not in ("passed", "waived") for g in run.data.get("gates", {}).values()):
raise RunError("a run succeeds only when every gate has passed or been waived")
if outcome == "failed" and "failed" not in states:
raise RunError("a run can be finished as failed only when a step has failed; otherwise abandon it")
gstates = [g["state"] for g in run.data.get("gates", {}).values()]
if outcome == "failed" and not ({"failed", "pushed"} & set(states) or {"failed", "pushed"} & set(gstates)):
raise RunError("a run can be finished as failed only when a step or gate has failed or was pushed through; otherwise abandon it")
run.data["outcome"] = outcome
run.data["finished"] = _now()
if note.strip():
@ -356,17 +355,17 @@ def finalize(run: Run, outcome: str, note: str = "") -> None:
def effective_outcome(run: Run, now: datetime | None = None, stale_days: int = STALE_DAYS) -> str:
"""finished outcome, or 'open', or 'abandoned' for an open run untouched for stale_days."""
"""finished outcome, or 'stalled' (unfinished), or 'abandoned' for an unfinished run untouched for stale_days."""
if run.data.get("outcome"):
return run.data["outcome"]
now = now or datetime.now(timezone.utc)
last = datetime.fromisoformat(run.data.get("updated") or run.data["created"])
return "abandoned" if (now - last).days >= stale_days else "open"
return "abandoned" if (now - last).days >= stale_days else "stalled"
def scorecard(pack: Pack, now: datetime | None = None, stale_days: int = STALE_DAYS) -> dict:
"""How often a runbook was tried and how it went, derived from its run records."""
card = {"tried": 0, "succeeded": 0, "failed": 0, "abandoned": 0, "open": 0,
card = {"tried": 0, "succeeded": 0, "failed": 0, "abandoned": 0, "stalled": 0,
"feedback": {"good": 0, "neutral": 0, "poor": 0}}
for rid in list_runs(pack):
r = load(pack, rid)