- a5/rq_heartbeat.py: HeartbeatStore + Heartbeat.run_tick (thin scheduler/resume layer) - a5/rq_heartbeat_cli.py: status/tick/enable/disable/resume/approve/deny/priority - a5/test_a5.py: 31 deterministic tests (tick-lock incl. ownership, eligibility, kill-switch, approval, circuit, git-conflict, bounded single A4) - a5/DESIGN.md, a5/README.md, a5/scripts/a5_heartbeat_tick.sh - a2/rq_mission.py: add read-only mission_list() for A5 enumeration (additive) - Fresh checker: PASS after tick-lock ownership repair (Regressionschutz lock_owned)
150 lines
4.7 KiB
Python
150 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Red Queen — A5: Heartbeat CLI (CONTROLLED HEARTBEAT & RESUME v1).
|
|
|
|
Operational Commands (§52):
|
|
status Read-only: Kill-Switch, letzter Tick, Missionen, offene Gates.
|
|
tick [MISSION_ID] EIN bounded Heartbeat-Zyklus (Kill-Switch, Lock, Eligibility, A4).
|
|
enable Kill-Switch ON (bewusst; autonome A4-Mutation erlaubt).
|
|
disable Kill-Switch OFF (Fail-closed: keine A4-Mutation).
|
|
resume --mission ID Explizite Resume einer PAUSED-Mission markieren.
|
|
approve --gate GATE_ID [--by NAME --evidence TEXT]
|
|
deny --gate GATE_ID [--by NAME --evidence TEXT]
|
|
priority --mission ID --prio N
|
|
status Registry-/Tick-Log-Anzeige.
|
|
|
|
Alle Kommandos sind deterministisch und laufen nie zyklisch. Kein Root.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_HERE = Path(__file__).resolve().parent
|
|
_REPO = _HERE.parent
|
|
for _p in (str(_REPO), str(_REPO / "a2"), str(_REPO / "a3"), str(_REPO / "a4"), str(_HERE)):
|
|
if _p not in sys.path:
|
|
sys.path.insert(0, _p)
|
|
|
|
from a4.rq_orchestrator import Orchestrator # noqa: E402
|
|
from a5.rq_heartbeat import Heartbeat, HeartbeatStore # noqa: E402
|
|
|
|
|
|
def _default_paths():
|
|
base = os.environ.get("RQ_DATA", str(_HERE))
|
|
return {
|
|
"registry": os.path.join(base, "heartbeat.db"),
|
|
"mission": os.path.join(base, "missions.db"),
|
|
"safety": os.path.join(base, "safety.db"),
|
|
}
|
|
|
|
|
|
def _make_hb(args) -> Heartbeat:
|
|
p = _default_paths()
|
|
orch = None
|
|
if args.execute:
|
|
orch = Orchestrator(mission_db=p["mission"], safety_db=p["safety"])
|
|
return Heartbeat(p["registry"], p["mission"], p["safety"], orchestrator=orch)
|
|
|
|
|
|
def _cmd_tick(args):
|
|
hb = _make_hb(args)
|
|
res = hb.run_tick(mission_id=args.mission, execute=args.execute)
|
|
print(json.dumps(res, indent=2, default=str))
|
|
return 0
|
|
|
|
|
|
def _cmd_status(args):
|
|
reg = HeartbeatStore(_default_paths()["registry"])
|
|
print(f"kill_switch: {'ON' if reg.kill_switch_on() else 'OFF'}")
|
|
lt = reg.last_tick()
|
|
if lt:
|
|
print("last_tick:", json.dumps(lt, default=str))
|
|
else:
|
|
print("last_tick: none")
|
|
return 0
|
|
|
|
|
|
def _cmd_switch(args, value: bool):
|
|
reg = HeartbeatStore(_default_paths()["registry"])
|
|
reg.set_kill_switch(value)
|
|
print(f"kill_switch -> {'ON' if value else 'OFF'}")
|
|
return 0
|
|
|
|
|
|
def _cmd_resume(args):
|
|
reg = HeartbeatStore(_default_paths()["registry"])
|
|
reg.set_resume_pending(args.mission, True)
|
|
print(f"resume_pending -> {args.mission}")
|
|
return 0
|
|
|
|
|
|
def _cmd_approve(args):
|
|
reg = HeartbeatStore(_default_paths()["registry"])
|
|
out = reg.approval_grant(args.gate_id, args.by, args.evidence)
|
|
print(json.dumps(out, default=str))
|
|
return 0
|
|
|
|
|
|
def _cmd_deny(args):
|
|
reg = HeartbeatStore(_default_paths()["registry"])
|
|
out = reg.approval_deny(args.gate_id, args.by, args.evidence)
|
|
print(json.dumps(out, default=str))
|
|
return 0
|
|
|
|
|
|
def _cmd_priority(args):
|
|
reg = HeartbeatStore(_default_paths()["registry"])
|
|
reg.set_config(f"priority:{args.mission}", str(args.prio))
|
|
print(f"priority[{args.mission}] -> {args.prio}")
|
|
return 0
|
|
|
|
|
|
def build_parser():
|
|
ap = argparse.ArgumentParser(prog="rq-heartbeat", description="A5 Heartbeat CLI")
|
|
ap.add_argument("--execute", action="store_true",
|
|
help="(tick) erlaubt A4-Mutation; ohne ist tick read-only")
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
|
|
p = sub.add_parser("tick")
|
|
p.add_argument("--mission", default=None)
|
|
p.set_defaults(func=_cmd_tick)
|
|
|
|
sub.add_parser("status").set_defaults(func=_cmd_status)
|
|
sub.add_parser("enable").set_defaults(func=lambda a: _cmd_switch(a, True))
|
|
sub.add_parser("disable").set_defaults(func=lambda a: _cmd_switch(a, False))
|
|
|
|
p = sub.add_parser("resume")
|
|
p.add_argument("--mission", required=True)
|
|
p.set_defaults(func=_cmd_resume)
|
|
|
|
p = sub.add_parser("approve")
|
|
p.add_argument("--gate", dest="gate_id", required=True)
|
|
p.add_argument("--by", default="christian")
|
|
p.add_argument("--evidence", default="manual approval")
|
|
p.set_defaults(func=_cmd_approve)
|
|
|
|
p = sub.add_parser("deny")
|
|
p.add_argument("--gate", dest="gate_id", required=True)
|
|
p.add_argument("--by", default="christian")
|
|
p.add_argument("--evidence", default="manual deny")
|
|
p.set_defaults(func=_cmd_deny)
|
|
|
|
p = sub.add_parser("priority")
|
|
p.add_argument("--mission", required=True)
|
|
p.add_argument("--prio", type=int, required=True)
|
|
p.set_defaults(func=_cmd_priority)
|
|
|
|
return ap
|
|
|
|
|
|
def main():
|
|
args = build_parser().parse_args()
|
|
return args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|