● Zion Boggan
repos/Zion Pm/archive/old-conductor-sketch/bin/pm.py
zionboggan.com ↗
133 lines · python
History for this file →
1
"""Zion PM control CLI. Pure local state ops (no model calls) - safe to spam.
2
 
3
Usage:
4
  pm add "<task>" [--project P] [--type code|research|outreach|browser|ops|misc]
5
  pm ls [--all]
6
  pm show <id>
7
  pm approvals
8
  pm approve <id> | pm reject <id> [--reason "..."]
9
  pm status
10
  pm activity [N]
11
  pm tick
12
  pm start | pm stop
13
"""
14
import json, os, sys, time, subprocess, argparse, uuid
15
 
16
PM = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
ST = os.path.join(PM, "state")
18
INBOX = os.path.join(PM, "inbox")
19
 
20
def _load(p, default):
21
    try:
22
        with open(p) as f: return json.load(f)
23
    except Exception: return default
24
 
25
def _save(p, obj):
26
    tmp = p + ".tmp"
27
    with open(tmp, "w") as f: json.dump(obj, f, indent=2)
28
    os.replace(tmp, p)
29
 
30
def _ts(): return time.strftime("%Y-%m-%dT%H:%M:%S")
31
 
32
def _log(event, **kw):
33
    line = {"ts": _ts(), "event": event, **kw}
34
    with open(os.path.join(ST, "activity.jsonl"), "a") as f:
35
        f.write(json.dumps(line) + "\n")
36
 
37
def cmd_add(a):
38
    t = _load(os.path.join(ST, "tasks.json"), {"tasks": []})
39
    tid = "t-" + uuid.uuid4().hex[:8]
40
    task = {"id": tid, "title": a.task, "project": a.project, "type": a.type,
41
            "status": "open", "created": _ts(), "artifacts": [], "worker": None}
42
    t["tasks"].append(task)
43
    _save(os.path.join(ST, "tasks.json"), t)
44
    with open(os.path.join(INBOX, tid + ".task"), "w") as f:
45
        f.write(a.task + "\n")
46
    _log("task_added", id=tid, title=a.task)
47
    print(f"added {tid}: {a.task}")
48
 
49
def cmd_ls(a):
50
    t = _load(os.path.join(ST, "tasks.json"), {"tasks": []})
51
    for x in t["tasks"]:
52
        if a.all or x["status"] not in ("done", "cancelled"):
53
            print(f"{x['id']}  [{x['status']:>10}]  {x.get('project') or '-':<14} {x['title']}")
54
 
55
def cmd_show(a):
56
    t = _load(os.path.join(ST, "tasks.json"), {"tasks": []})
57
    for x in t["tasks"]:
58
        if x["id"] == a.id: print(json.dumps(x, indent=2)); return
59
    print("not found", file=sys.stderr); sys.exit(1)
60
 
61
def cmd_approvals(a):
62
    o = _load(os.path.join(ST, "outbox.json"), {"pending": [], "history": []})
63
    if not o["pending"]: print("no pending approvals"); return
64
    for p in o["pending"]:
65
        print(f"--- {p['id']}  ({p['kind']})  task={p.get('task')}")
66
        print(f"    {p.get('preview','')}")
67
 
68
def _move_outbox(oid, decision, reason):
69
    o = _load(os.path.join(ST, "outbox.json"), {"pending": [], "history": []})
70
    keep, hit = [], None
71
    for p in o["pending"]:
72
        (hit := p) if p["id"] == oid else keep.append(p)
73
    if not hit: print("not found", file=sys.stderr); sys.exit(1)
74
    hit["decision"] = decision; hit["decided_at"] = _ts(); hit["reason"] = reason
75
    o["pending"] = keep; o["history"].append(hit)
76
    _save(os.path.join(ST, "outbox.json"), o)
77
    _log("outbox_" + decision, id=oid, kind=hit["kind"])
78
    return hit
79
 
80
def cmd_approve(a):
81
    hit = _move_outbox(a.id, "approved", a.reason)
82
    print(f"approved {a.id}. run `pm flush` (or the conductor will) to execute.")
83
 
84
def cmd_reject(a):
85
    _move_outbox(a.id, "rejected", a.reason); print(f"rejected {a.id}")
86
 
87
def cmd_flush(a):
88
    subprocess.run(["bash", os.path.join(PM, "bin", "flush-outbox.sh")])
89
 
90
def cmd_status(a):
91
    c = _load(os.path.join(ST, "conductor_state.json"), {})
92
    print(json.dumps(c, indent=2))
93
    wdir = os.path.join(PM, "workers")
94
    running = [d for d in os.listdir(wdir) if os.path.isdir(os.path.join(wdir, d))
95
               and not os.path.exists(os.path.join(wdir, d, "result.json"))] if os.path.isdir(wdir) else []
96
    print("running workers:", running or "none")
97
 
98
def cmd_activity(a):
99
    p = os.path.join(ST, "activity.jsonl")
100
    if not os.path.exists(p): print("(no activity)"); return
101
    lines = open(p).read().splitlines()[-a.n:]
102
    for l in lines: print(l)
103
 
104
def cmd_tick(a):
105
    subprocess.run(["bash", os.path.join(PM, "bin", "conductor.sh"), "--once"])
106
 
107
def cmd_start(a):
108
    subprocess.run(["bash", os.path.join(PM, "bin", "conductor.sh"), "--daemon"])
109
 
110
def cmd_stop(a):
111
    subprocess.run(["tmux", "kill-session", "-t", "zion-pm"])
112
    print("stopped zion-pm loop")
113
 
114
def main():
115
    ap = argparse.ArgumentParser(prog="pm")
116
    sub = ap.add_subparsers(dest="cmd", required=True)
117
    s = sub.add_parser("add"); s.add_argument("task"); s.add_argument("--project", default=None)
118
    s.add_argument("--type", default="misc"); s.set_defaults(fn=cmd_add)
119
    s = sub.add_parser("ls"); s.add_argument("--all", action="store_true"); s.set_defaults(fn=cmd_ls)
120
    s = sub.add_parser("show"); s.add_argument("id"); s.set_defaults(fn=cmd_show)
121
    s = sub.add_parser("approvals"); s.set_defaults(fn=cmd_approvals)
122
    s = sub.add_parser("approve"); s.add_argument("id"); s.add_argument("--reason", default=""); s.set_defaults(fn=cmd_approve)
123
    s = sub.add_parser("reject"); s.add_argument("id"); s.add_argument("--reason", default=""); s.set_defaults(fn=cmd_reject)
124
    s = sub.add_parser("flush"); s.set_defaults(fn=cmd_flush)
125
    s = sub.add_parser("status"); s.set_defaults(fn=cmd_status)
126
    s = sub.add_parser("activity"); s.add_argument("n", nargs="?", type=int, default=20); s.set_defaults(fn=cmd_activity)
127
    s = sub.add_parser("tick"); s.set_defaults(fn=cmd_tick)
128
    s = sub.add_parser("start"); s.set_defaults(fn=cmd_start)
129
    s = sub.add_parser("stop"); s.set_defaults(fn=cmd_stop)
130
    a = ap.parse_args(); a.fn(a)
131
 
132
if __name__ == "__main__":
133
    main()