Download cli/qa_cycle.py from LoveLogicAI/kanbot-backend: direct link, hf CLI and curl.
- Browser
- Download file 4.24 kB
-
https://huggingface.co/spaces/LoveLogicAI/kanbot-backend/resolve/main/cli/qa_cycle.py
- Command line
-
hf download hf://spaces/LoveLogicAI/kanbot-backend/cli/qa_cycle.py
-
curl -L -o qa_cycle.py https://huggingface.co/spaces/LoveLogicAI/kanbot-backend/resolve/main/cli/qa_cycle.py
4.24 kB
| #!/usr/bin/env python3 | |
| """One card, two modes. The QA cycle between dry-run and a live cockpit. | |
| python -m cli.qa_cycle dry # propose and park; the shell does not run | |
| python -m cli.qa_cycle live # run the plan in .sandbox/, critic passes to done | |
| The provider is forced to mock so this never calls OMP, Hermes, or OpenClaw. | |
| The card is deleted before exit, including on failure. Refuses to start when a | |
| conductor or reaper is already running, so a QA card cannot race the live board. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| MODE = sys.argv[1] if len(sys.argv) > 1 else "" | |
| if MODE not in ("dry", "live"): | |
| print("usage: python -m cli.qa_cycle dry|live", file=sys.stderr) | |
| raise SystemExit(2) | |
| # Set before any KanBot import. workers reads DRY_RUN at import, and the repo | |
| # .env must not override this process (setdefault only fills what is missing). | |
| os.environ["KANBOT_DRY_RUN"] = "1" if MODE == "dry" else "0" | |
| os.environ["KANBOT_PROVIDER"] = "mock" | |
| os.environ["KANBOT_SWARM_N"] = "1" | |
| def _foreign_workers() -> list[str]: | |
| found = subprocess.run( | |
| ["pgrep", "-fl", "core.conductor|core.reaper"], | |
| capture_output=True, text=True, | |
| ) | |
| if found.returncode != 0: | |
| return [] | |
| return [line for line in found.stdout.splitlines() if "qa_cycle" not in line] | |
| async def _cycle() -> dict: | |
| from core import db | |
| from core.mcp_server import create_card, move_card | |
| from core.workers import DRY_RUN, claim_and_execute, run_critic | |
| if MODE == "dry" and not DRY_RUN: | |
| raise RuntimeError("dry cycle imported workers with DRY_RUN off") | |
| if MODE == "live" and DRY_RUN: | |
| raise RuntimeError("live cycle imported workers with DRY_RUN on") | |
| created = json.loads(await create_card( | |
| title="QA e2e sandbox ping", | |
| spec="Print a marker through the sandbox and post it as the artifact.", | |
| role="executor", | |
| acceptance_criteria=["the sandbox ran"], | |
| tags=["qa"], | |
| )) | |
| cid = created["id"] | |
| try: | |
| promoted = json.loads(await move_card( | |
| cid, "ready", actor="qa", reason="qa promote")) | |
| if promoted.get("rejected") or promoted.get("error"): | |
| raise RuntimeError(f"promote failed: {promoted}") | |
| card = await db.get_card(cid) | |
| await claim_and_execute(card) | |
| card = await db.get_card(cid) | |
| kinds = [entry.kind for entry in card.thread] | |
| if MODE == "dry": | |
| if card.column.value != "blocked": | |
| raise RuntimeError(f"dry-run left the card in {card.column.value}") | |
| if "tool_call" not in kinds or "tool_result" in kinds: | |
| raise RuntimeError(f"dry-run thread kinds: {kinds}") | |
| else: | |
| if card.column.value != "review": | |
| raise RuntimeError(f"live execution left the card in {card.column.value}") | |
| if "tool_result" not in kinds or "artifact" not in kinds: | |
| raise RuntimeError(f"live thread kinds: {kinds}") | |
| blob = "\n".join(entry.content for entry in card.thread) | |
| if "mock" not in blob: | |
| raise RuntimeError("sandbox stdout did not land on the thread") | |
| await run_critic(card) | |
| card = await db.get_card(cid) | |
| if card.column.value != "done": | |
| raise RuntimeError(f"critic left the card in {card.column.value}") | |
| return {"ok": True, "mode": MODE, "id": cid, "column": card.column.value} | |
| finally: | |
| pool = await db.pool() | |
| async with pool.acquire() as conn: | |
| await conn.execute("DELETE FROM cards WHERE id=$1", cid) | |
| await pool.close() | |
| def main() -> int: | |
| busy = _foreign_workers() | |
| if busy: | |
| print(json.dumps({"ok": False, "error": "conductor or reaper is running", | |
| "procs": busy})) | |
| return 3 | |
| try: | |
| report = asyncio.run(_cycle()) | |
| except Exception as exc: # noqa: BLE001 — one JSON line for the QA runner | |
| print(json.dumps({"ok": False, "mode": MODE, "error": f"{type(exc).__name__}: {exc}"})) | |
| return 1 | |
| print(json.dumps(report)) | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |