RED QUEEN BUILD PHASE A2: Mission-State- und Work-Package-State-Machine. Minimaler Missions-Metadata-Layer (missions.db) auf nativer Hermes-Kanban-Basis (KanbanMirror best effort). Deterministische Validierung, Idempotenz, STATE_ERROR fail-closed, Restart-Persistenz. 61 deterministische Tests PASS. Keine autonome Orchestrierung/Loops/Cron.
154 lines
5.8 KiB
Python
154 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Red Queen — A2 CLI. Dünner, deterministischer Kommandozeilen-Zugriff auf den
|
|
MissionStore. Maschinenlesbar via `--json`.
|
|
|
|
Hinweis: Der A2-Build definiert KEINE autonome Orchestrierung — dieses CLI ruft
|
|
nur die vom Store bereitgestellten atomaren Operationen auf.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from typing import Optional
|
|
|
|
from rq_mission import MissionStore, RqError
|
|
|
|
|
|
def _store(args) -> MissionStore:
|
|
db = args.db or os.environ.get("RQ_MISSIONS_DB") or "missions.db"
|
|
mirror = args.mirror
|
|
return MissionStore(db, mirror_db=mirror)
|
|
|
|
|
|
def _emit(args, obj) -> None:
|
|
if args.json:
|
|
print(json.dumps(obj, indent=2, ensure_ascii=False))
|
|
else:
|
|
if isinstance(obj, dict) and "id" in obj:
|
|
print(obj["id"])
|
|
else:
|
|
print(json.dumps(obj, ensure_ascii=False))
|
|
|
|
|
|
def _fail(e: RqError, args) -> None:
|
|
if args.json:
|
|
print(json.dumps(e.to_dict(), indent=2))
|
|
else:
|
|
print(f"ERROR [{e.code}]: {e.message}")
|
|
sys.exit(2)
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
p = argparse.ArgumentParser(prog="rq_mission", description="Red Queen A2 Mission/WP CLI")
|
|
p.add_argument("--db", help="Pfad zur missions.db (default: $RQMISSIONS_DB oder missions.db)")
|
|
p.add_argument("--mirror", help="Kanban-DB-Pfad fuer nativen Mirror ('off' deaktiviert)")
|
|
p.add_argument("--json", action="store_true", help="JSON-Ausgabe")
|
|
sub = p.add_subparsers(dest="cmd", required=True)
|
|
|
|
# mission_create
|
|
mc = sub.add_parser("mission_create")
|
|
mc.add_argument("--title", required=True)
|
|
mc.add_argument("--goal")
|
|
mc.add_argument("--scope")
|
|
mc.add_argument("--constraints")
|
|
mc.add_argument("--acceptance")
|
|
mc.add_argument("--id", dest="mission_id")
|
|
mc.add_argument("--actor", default="red-queen")
|
|
|
|
# mission_read / state
|
|
mr = sub.add_parser("mission_read"); mr.add_argument("mission_id")
|
|
ms = sub.add_parser("mission_state"); ms.add_argument("mission_id")
|
|
|
|
# mission transitions
|
|
mt = sub.add_parser("mission_transition")
|
|
mt.add_argument("--mission", dest="mission_id", required=True)
|
|
mt.add_argument("--target", required=True)
|
|
mt.add_argument("--evidence")
|
|
mt.add_argument("--actor", default="red-queen")
|
|
sub.add_parser("mission_pause").add_argument("mission_id")
|
|
sub.add_parser("mission_block").add_argument("mission_id")
|
|
sub.add_parser("mission_resume").add_argument("mission_id")
|
|
sub.add_parser("mission_complete").add_argument("mission_id")
|
|
|
|
# wp_create
|
|
wc = sub.add_parser("wp_create")
|
|
wc.add_argument("--mission", dest="mission_id", required=True)
|
|
wc.add_argument("--title", required=True)
|
|
wc.add_argument("--wp-id", dest="wp_id")
|
|
wc.add_argument("--actor", default="red-queen")
|
|
|
|
wr = sub.add_parser("wp_read"); wr.add_argument("wp_id")
|
|
ws = sub.add_parser("wp_state"); ws.add_argument("wp_id")
|
|
wt = sub.add_parser("wp_transition")
|
|
wt.add_argument("--wp", dest="wp_id", required=True)
|
|
wt.add_argument("--target", required=True)
|
|
wt.add_argument("--evidence")
|
|
wt.add_argument("--actor", default="red-queen")
|
|
wd = sub.add_parser("wp_dependency")
|
|
wd.add_argument("--mission", dest="mission_id", required=True)
|
|
wd.add_argument("--wp", dest="wp_id", required=True)
|
|
wd.add_argument("--depends-on", required=True)
|
|
wb = sub.add_parser("wp_block")
|
|
wb.add_argument("wp_id")
|
|
wb.add_argument("--reason", default="blocked")
|
|
wc2 = sub.add_parser("wp_complete")
|
|
wc2.add_argument("wp_id")
|
|
wc2.add_argument("--result")
|
|
return p
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
try:
|
|
s = _store(args)
|
|
if args.cmd == "mission_create":
|
|
r = s.mission_create(args.title, goal=args.goal, scope=args.scope,
|
|
constraints=args.constraints,
|
|
acceptance_criteria=args.acceptance,
|
|
mission_id=args.mission_id, actor=args.actor)
|
|
elif args.cmd == "mission_read":
|
|
r = s.mission_read(args.mission_id)
|
|
elif args.cmd == "mission_state":
|
|
r = s.mission_state(args.mission_id)
|
|
elif args.cmd == "mission_transition":
|
|
r = s.mission_transition(args.mission_id, args.target, actor=args.actor, evidence=args.evidence)
|
|
elif args.cmd == "mission_pause":
|
|
r = s.mission_pause(args.mission_id)
|
|
elif args.cmd == "mission_block":
|
|
r = s.mission_block(args.mission_id)
|
|
elif args.cmd == "mission_resume":
|
|
r = s.mission_resume(args.mission_id)
|
|
elif args.cmd == "mission_complete":
|
|
r = s.mission_complete(args.mission_id)
|
|
elif args.cmd == "wp_create":
|
|
r = s.wp_create(args.mission_id, args.title, wp_id=args.wp_id, actor=args.actor)
|
|
elif args.cmd == "wp_read":
|
|
r = s.wp_read(args.wp_id)
|
|
elif args.cmd == "wp_state":
|
|
r = s.wp_state(args.wp_id)
|
|
elif args.cmd == "wp_transition":
|
|
r = s.wp_transition(args.wp_id, args.target, actor=args.actor, evidence=args.evidence)
|
|
elif args.cmd == "wp_dependency":
|
|
r = s.wp_dependency(args.mission_id, args.wp_id, args.depends_on)
|
|
elif args.cmd == "wp_block":
|
|
r = s.wp_block(args.wp_id, reason=args.reason)
|
|
elif args.cmd == "wp_complete":
|
|
r = s.wp_complete(args.wp_id, result=args.result)
|
|
else:
|
|
_emit(args, {"error": f"unknown command {args.cmd}"})
|
|
return 1
|
|
_emit(args, r)
|
|
return 0
|
|
except RqError as e:
|
|
_fail(e, args)
|
|
except Exception as e: # noqa
|
|
_fail(RqError("INTERNAL_ERROR", str(e)), args)
|
|
return 2 # unreachable normally; _fail exits
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|