mooting 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mooting/__init__.py +3 -0
- mooting/cli.py +617 -0
- mooting/console.py +1184 -0
- mooting/doctor.py +139 -0
- mooting/drivers/__init__.py +4 -0
- mooting/drivers/base.py +194 -0
- mooting/drivers/fake.py +65 -0
- mooting/drivers/registry.py +37 -0
- mooting/drivers/spawn.py +462 -0
- mooting/install.py +75 -0
- mooting/mcp_server.py +299 -0
- mooting/minutes.py +182 -0
- mooting/models.py +73 -0
- mooting/schema.sql +181 -0
- mooting/setup.py +167 -0
- mooting/store.py +986 -0
- mooting/supervisor.py +799 -0
- mooting/tui.py +942 -0
- mooting-0.1.0.dist-info/METADATA +185 -0
- mooting-0.1.0.dist-info/RECORD +24 -0
- mooting-0.1.0.dist-info/WHEEL +5 -0
- mooting-0.1.0.dist-info/entry_points.txt +2 -0
- mooting-0.1.0.dist-info/licenses/LICENSE +21 -0
- mooting-0.1.0.dist-info/top_level.txt +1 -0
mooting/__init__.py
ADDED
mooting/cli.py
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"""The human's surface.
|
|
2
|
+
|
|
3
|
+
Deliberately a separate surface from the MCP tools. Agents get `mooting_say`,
|
|
4
|
+
`mooting_propose`, `mooting_vote`; the human gets those *plus* `approve`/`reject`,
|
|
5
|
+
which exist nowhere in the agent-facing tool list. Same board, different powers,
|
|
6
|
+
and the difference is structural rather than a matter of prompt discipline.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import asyncio
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .store import Store, StoreError, connect
|
|
18
|
+
|
|
19
|
+
DIM_, RESET_ = "\033[2m", "\033[0m"
|
|
20
|
+
|
|
21
|
+
# Windows consoles default to cp950 here; council traffic is Chinese from day one.
|
|
22
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
23
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
24
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _board(args: argparse.Namespace) -> Store:
|
|
28
|
+
return connect(Path(args.db) if args.db else None, init=getattr(args, "_init", False))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _human(board: Store, name: str | None) -> str:
|
|
32
|
+
"""Resolve who 'you' are, and refuse to let a human command run as an agent."""
|
|
33
|
+
who = name or os.environ.get("MOOTING_HUMAN") or os.environ.get("MOOT_HUMAN")
|
|
34
|
+
if not who:
|
|
35
|
+
humans = [a["name"] for a in board.agents() if a["kind"] == "human"]
|
|
36
|
+
if len(humans) == 1:
|
|
37
|
+
return humans[0]
|
|
38
|
+
raise SystemExit("who are you? pass --as <name> or set MOOTING_HUMAN")
|
|
39
|
+
if not board.is_human(who):
|
|
40
|
+
raise SystemExit(f"{who!r} is not a human seat; agent seats cannot use this command")
|
|
41
|
+
return who
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ------------------------------------------------------------------- commands
|
|
45
|
+
|
|
46
|
+
def cmd_init(args) -> int:
|
|
47
|
+
args._init = True
|
|
48
|
+
board = _board(args)
|
|
49
|
+
board.add_agent(args.human, "human", display=args.human)
|
|
50
|
+
print(f"board at {board.path}")
|
|
51
|
+
print(f"human seat: {args.human}")
|
|
52
|
+
print("next: mooting agents add claude claude --cwd .")
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cmd_setup(args) -> int:
|
|
57
|
+
"""One command that gets a council standing."""
|
|
58
|
+
from .setup import run
|
|
59
|
+
return run(args.db, assume_yes=args.yes)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def cmd_agents_add(args) -> int:
|
|
63
|
+
board = _board(args)
|
|
64
|
+
cfg = {"cwd": os.path.abspath(args.cwd)} if args.cwd else {}
|
|
65
|
+
if args.model:
|
|
66
|
+
cfg["model"] = args.model
|
|
67
|
+
if args.effort:
|
|
68
|
+
cfg["effort"] = args.effort
|
|
69
|
+
if args.capability:
|
|
70
|
+
cfg["capability"] = args.capability
|
|
71
|
+
if args.arg:
|
|
72
|
+
# Escape hatch for machine-local quirks -- a broken plugin to switch off,
|
|
73
|
+
# a flag a newer CLI needs. Keeping these per-seat means the drivers stay
|
|
74
|
+
# general instead of accumulating one user's environment.
|
|
75
|
+
cfg["extra_argv"] = args.arg
|
|
76
|
+
board.add_agent(args.name, args.kind, driver=args.driver, driver_cfg=cfg)
|
|
77
|
+
print(f"seat {args.name} ({args.kind}, driver={board.agent(args.name)['driver']})")
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_agents_ls(args) -> int:
|
|
82
|
+
board = _board(args)
|
|
83
|
+
for a in board.agents():
|
|
84
|
+
print(f"{a['name']:<12} {a['kind']:<10} driver={a['driver']:<11} "
|
|
85
|
+
f"{'enabled' if a['enabled'] else 'disabled'}")
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cmd_agents_rm(args) -> int:
|
|
90
|
+
board = _board(args)
|
|
91
|
+
a = board.agent(args.name)
|
|
92
|
+
counts = {"seats": len(board.q("SELECT 1 FROM seats WHERE agent = ?", (args.name,))),
|
|
93
|
+
"messages": len(board.q("SELECT 1 FROM messages WHERE author = ?", (args.name,)))}
|
|
94
|
+
if not args.yes:
|
|
95
|
+
print(f"would remove seat `{a['name']}` ({a['kind']})")
|
|
96
|
+
print(f" sits on {counts['seats']} topic(s); "
|
|
97
|
+
f"{counts['messages']} message(s) it wrote stay on the board")
|
|
98
|
+
print("re-run with --yes")
|
|
99
|
+
return 1
|
|
100
|
+
got = board.delete_agent(args.name)
|
|
101
|
+
print(f"removed `{args.name}` (was on {got['seats']} topic(s); "
|
|
102
|
+
f"{got['messages']} message(s) kept)")
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def cmd_topic_new(args) -> int:
|
|
107
|
+
board = _board(args)
|
|
108
|
+
brief = args.brief
|
|
109
|
+
if brief == "-":
|
|
110
|
+
brief = sys.stdin.read()
|
|
111
|
+
seats = [s.strip() for s in args.seats.split(",") if s.strip()]
|
|
112
|
+
who = _human(board, args.as_)
|
|
113
|
+
if who not in seats:
|
|
114
|
+
seats.append(who) # the human always holds a seat on their own topic
|
|
115
|
+
tid = board.open_topic(args.slug, args.title, brief, who, seats=seats,
|
|
116
|
+
max_rounds=args.rounds, max_turns=args.turns, mode=args.mode,
|
|
117
|
+
effort=args.effort, manager=args.manager)
|
|
118
|
+
print(f"topic #{tid} `{args.slug}` opened with seats: {', '.join(seats)}")
|
|
119
|
+
print(f"next: mooting run {args.slug}")
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def cmd_topic_rm(args) -> int:
|
|
124
|
+
board = _board(args)
|
|
125
|
+
t = board.topic(int(args.slug) if args.slug.isdigit() else args.slug)
|
|
126
|
+
trees = board.orphan_worktrees(int(t["id"]))
|
|
127
|
+
if not args.yes:
|
|
128
|
+
print(f"would delete `{t['slug']}` — {t['title']}")
|
|
129
|
+
print(f" {len(board.transcript(int(t['id'])))} message(s), "
|
|
130
|
+
f"{len(board.tasks(int(t['id'])))} task(s), "
|
|
131
|
+
f"{len(board.proposals(int(t['id'])))} proposal(s)")
|
|
132
|
+
for w in trees:
|
|
133
|
+
print(f" leaves a worktree on disk: {w}")
|
|
134
|
+
print("re-run with --yes to actually delete it")
|
|
135
|
+
return 1
|
|
136
|
+
counts = board.delete_topic(int(t["id"]))
|
|
137
|
+
print(f"deleted `{t['slug']}` ({counts['messages']} messages, "
|
|
138
|
+
f"{counts['tasks']} tasks, {counts['proposals']} proposals)")
|
|
139
|
+
for w in trees:
|
|
140
|
+
print(f" worktree left on disk: {w}")
|
|
141
|
+
print(f" git worktree remove {w}")
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def cmd_reset(args) -> int:
|
|
146
|
+
"""Clear the board. Seats survive unless you ask for everything."""
|
|
147
|
+
board = _board(args)
|
|
148
|
+
topics = board.topics()
|
|
149
|
+
trees = [w for t in topics for w in board.orphan_worktrees(int(t["id"]))]
|
|
150
|
+
if not args.yes:
|
|
151
|
+
print(f"would delete {len(topics)} topic(s) and all their messages, "
|
|
152
|
+
f"tasks and proposals:")
|
|
153
|
+
for t in topics[:12]:
|
|
154
|
+
print(f" {t['slug']:<22} {t['title'][:44]}")
|
|
155
|
+
if len(topics) > 12:
|
|
156
|
+
print(f" … and {len(topics) - 12} more")
|
|
157
|
+
if args.all:
|
|
158
|
+
print(f"and {len(board.agents())} registered seat(s)")
|
|
159
|
+
for w in trees:
|
|
160
|
+
print(f" leaves a worktree on disk: {w}")
|
|
161
|
+
print("re-run with --yes to actually do it")
|
|
162
|
+
return 1
|
|
163
|
+
n = board.clear_topics()
|
|
164
|
+
print(f"cleared {n} topic(s)")
|
|
165
|
+
if args.all:
|
|
166
|
+
with board.tx() as c:
|
|
167
|
+
c.execute("DELETE FROM agents WHERE kind != 'human'")
|
|
168
|
+
print("removed every agent seat; human seats kept")
|
|
169
|
+
for w in trees:
|
|
170
|
+
print(f" worktree left on disk: {w} -> git worktree remove {w}")
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def cmd_ls(args) -> int:
|
|
175
|
+
board = _board(args)
|
|
176
|
+
for t in board.topics(args.status):
|
|
177
|
+
openp = len(board.proposals(t["id"], status="open"))
|
|
178
|
+
flag = f" ⟨{openp} awaiting you⟩" if openp else ""
|
|
179
|
+
print(f"#{t['id']:<3} {t['slug']:<24} {t['status']:<9} "
|
|
180
|
+
f"r{t['round'] + 1}/{t['max_rounds']} {t['title']}{flag}")
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def cmd_show(args) -> int:
|
|
185
|
+
board = _board(args)
|
|
186
|
+
t = board.topic(int(args.topic) if args.topic.isdigit() else args.topic)
|
|
187
|
+
print(f"# {t['title']} (`{t['slug']}`, {t['status']}, round {t['round'] + 1}/{t['max_rounds']})\n")
|
|
188
|
+
print(t["brief"] + "\n")
|
|
189
|
+
for s in board.seats(t["id"]):
|
|
190
|
+
print(f" {s['agent']:<10} {s['kind']:<9} {s['state']:<8} {s['turns_used']}/{s['max_turns']}")
|
|
191
|
+
print("\n" + "-" * 60 + "\n")
|
|
192
|
+
for m in board.transcript(t["id"]):
|
|
193
|
+
tag = "" if m["kind"] == "say" else f" [{m['kind']}]"
|
|
194
|
+
print(f"#{m['id']} {m['author']}{tag} {m['created_at']}")
|
|
195
|
+
print(m["body"].strip() + "\n")
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def cmd_say(args) -> int:
|
|
200
|
+
"""A human joining the discussion -- not consuming an agent's metered turn."""
|
|
201
|
+
board = _board(args)
|
|
202
|
+
who = _human(board, args.as_)
|
|
203
|
+
t = board.topic(int(args.topic) if args.topic.isdigit() else args.topic)
|
|
204
|
+
body = sys.stdin.read() if args.body == "-" else args.body
|
|
205
|
+
mid = board.post(int(t["id"]), who, body, count_turn=False)
|
|
206
|
+
print(f"posted #{mid} as {who}")
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def cmd_ask(args) -> int:
|
|
211
|
+
"""@ one agent directly. The human's version of an @mention."""
|
|
212
|
+
board = _board(args)
|
|
213
|
+
who = _human(board, args.as_)
|
|
214
|
+
t = board.topic(int(args.topic) if args.topic.isdigit() else args.topic)
|
|
215
|
+
question = sys.stdin.read() if args.question == "-" else args.question
|
|
216
|
+
mid = board.ask(int(t["id"]), who, args.agent, question)
|
|
217
|
+
print(f"asked {args.agent} (#{mid}). They are next in line.")
|
|
218
|
+
print(f"next: mooting nudge {t['slug']} {args.agent} (or `mooting run {t['slug']}`)")
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def cmd_install(args) -> int:
|
|
223
|
+
from .install import install_seat
|
|
224
|
+
board = _board(args)
|
|
225
|
+
names = [n.strip() for n in args.agents.split(",")] if args.agents else [a["name"] for a in board.agents()]
|
|
226
|
+
rc = 0
|
|
227
|
+
for name in names:
|
|
228
|
+
try:
|
|
229
|
+
ok, detail = install_seat(board, name, dry_run=args.dry_run)
|
|
230
|
+
except StoreError as exc:
|
|
231
|
+
ok, detail = False, str(exc)
|
|
232
|
+
print(f"[{'ok' if ok else 'FAIL'}] {name:<12} {detail}")
|
|
233
|
+
rc |= 0 if ok else 1
|
|
234
|
+
return rc
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def cmd_minutes(args) -> int:
|
|
238
|
+
"""Write the meeting out as markdown."""
|
|
239
|
+
from .minutes import default_path, render
|
|
240
|
+
board = _board(args)
|
|
241
|
+
t = board.topic(int(args.topic) if args.topic.isdigit() else args.topic)
|
|
242
|
+
text = render(board, int(t["id"]), transcript=not args.decisions_only)
|
|
243
|
+
if args.out == "-":
|
|
244
|
+
print(text)
|
|
245
|
+
return 0
|
|
246
|
+
path = Path(args.out) if args.out else Path(default_path(board, int(t["id"])))
|
|
247
|
+
path.write_text(text, encoding="utf-8")
|
|
248
|
+
print(f"wrote {path} ({len(text.splitlines())} lines)")
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def cmd_conclude(args) -> int:
|
|
253
|
+
"""Close the meeting and write its minutes in one step."""
|
|
254
|
+
from .minutes import default_path, render
|
|
255
|
+
board = _board(args)
|
|
256
|
+
who = _human(board, args.as_)
|
|
257
|
+
t = board.topic(int(args.topic) if args.topic.isdigit() else args.topic)
|
|
258
|
+
tid = int(t["id"])
|
|
259
|
+
|
|
260
|
+
# Only an undecided proposal blocks: it is the chair's own outstanding duty.
|
|
261
|
+
# A question one agent left hanging for another is recorded, not a veto.
|
|
262
|
+
undecided = board.proposals(tid, status="open")
|
|
263
|
+
if undecided and not args.force:
|
|
264
|
+
print(f"`{t['slug']}` has {len(undecided)} decision(s) still waiting on you:")
|
|
265
|
+
for p in undecided:
|
|
266
|
+
print(f" proposal #{p['id']} {p['title']}")
|
|
267
|
+
print("rule on them first, or --force to close with them unresolved")
|
|
268
|
+
return 1
|
|
269
|
+
for m in board.open_mentions(tid):
|
|
270
|
+
print(f"left unanswered: {m['asker']} asked {m['target']} — "
|
|
271
|
+
f"{' '.join(m['question'].split())[:70]}")
|
|
272
|
+
|
|
273
|
+
board.conclude(tid, who, args.note or "")
|
|
274
|
+
text = render(board, tid, transcript=not args.decisions_only)
|
|
275
|
+
path = Path(args.out) if args.out else Path(default_path(board, tid))
|
|
276
|
+
path.write_text(text, encoding="utf-8")
|
|
277
|
+
print(f"concluded `{t['slug']}` and wrote {path}")
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def cmd_tasks(args) -> int:
|
|
282
|
+
board = _board(args)
|
|
283
|
+
t = board.topic(int(args.topic) if args.topic.isdigit() else args.topic)
|
|
284
|
+
rows = board.tasks(int(t["id"]), status=args.status)
|
|
285
|
+
if not rows:
|
|
286
|
+
print("no tasks")
|
|
287
|
+
return 0
|
|
288
|
+
for r in rows:
|
|
289
|
+
print(f"#{r['id']:<3} [{r['status']:<11}] {r['assignee']:<10} {r['title']}")
|
|
290
|
+
if r["branch"]:
|
|
291
|
+
print(f" {DIM_}branch {r['branch']} worktree {r['worktree']}{RESET_}")
|
|
292
|
+
if r["result"]:
|
|
293
|
+
print(f" {r['result'].strip()[:300]}")
|
|
294
|
+
return 0
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def cmd_proposals(args) -> int:
|
|
298
|
+
board = _board(args)
|
|
299
|
+
tid = None
|
|
300
|
+
if args.topic:
|
|
301
|
+
tid = int(board.topic(int(args.topic) if args.topic.isdigit() else args.topic)["id"])
|
|
302
|
+
for p in board.proposals(tid, status=args.status):
|
|
303
|
+
votes = board.votes(p["id"])
|
|
304
|
+
tally = ", ".join(f"{v['agent']}:{v['stance']}" for v in votes) or "no votes"
|
|
305
|
+
print(f"#{p['id']} [{p['status']}] {p['title']} — by {p['author']} ({tally})")
|
|
306
|
+
if args.full:
|
|
307
|
+
print(" " + p["body"].strip().replace("\n", "\n "))
|
|
308
|
+
for v in votes:
|
|
309
|
+
if v["rationale"]:
|
|
310
|
+
print(f" · {v['agent']} {v['stance']}: {v['rationale']}")
|
|
311
|
+
return 0
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _decide(args, approve: bool) -> int:
|
|
315
|
+
board = _board(args)
|
|
316
|
+
who = _human(board, args.as_)
|
|
317
|
+
board.decide(args.proposal_id, who, approve, args.message or "")
|
|
318
|
+
p = board.proposal(args.proposal_id)
|
|
319
|
+
print(f"proposal #{p['id']} {p['status']} by {who}")
|
|
320
|
+
if board.topic(int(p["topic_id"]))["status"] == "paused":
|
|
321
|
+
print(f"topic is paused — `mooting run {board.topic(int(p['topic_id']))['slug']}` to continue")
|
|
322
|
+
return 0
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def cmd_approve(args) -> int:
|
|
326
|
+
return _decide(args, True)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def cmd_reject(args) -> int:
|
|
330
|
+
return _decide(args, False)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _drivers(board: Store, names: list[str] | None = None):
|
|
334
|
+
from .drivers.registry import build_drivers
|
|
335
|
+
return build_drivers(board, names)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def cmd_run(args) -> int:
|
|
339
|
+
from .supervisor import Caps, Supervisor
|
|
340
|
+
board = _board(args)
|
|
341
|
+
t = board.topic(int(args.topic) if args.topic.isdigit() else args.topic)
|
|
342
|
+
if t["status"] == "paused" and not args.resume:
|
|
343
|
+
print(f"topic is paused ({t['slug']}). Re-run with --resume once you have ruled.")
|
|
344
|
+
return 1
|
|
345
|
+
if args.resume:
|
|
346
|
+
board.set_topic_status(int(t["id"]), "open", _human(board, args.as_), "resumed by human")
|
|
347
|
+
if args.rounds:
|
|
348
|
+
with board.tx() as c:
|
|
349
|
+
c.execute("UPDATE topics SET max_rounds = max_rounds + ? WHERE id = ?",
|
|
350
|
+
(args.rounds, int(t["id"])))
|
|
351
|
+
|
|
352
|
+
caps = Caps(max_turns_per_seat=args.max_turns, max_wakes_per_agent_per_hour=args.max_wakes,
|
|
353
|
+
effort=args.effort or "low")
|
|
354
|
+
sup = Supervisor(board, _drivers(board), caps,
|
|
355
|
+
turn_taking="sequential" if args.sequential else "concurrent")
|
|
356
|
+
reason = asyncio.run(sup.run_topic(int(t["id"])))
|
|
357
|
+
print(f"\n== stopped: {reason}")
|
|
358
|
+
for p in board.proposals(int(t["id"]), status="open"):
|
|
359
|
+
print(f" awaiting you: #{p['id']} {p['title']}")
|
|
360
|
+
print(f" mooting approve {p['id']} -m '...' | mooting reject {p['id']} -m '...'")
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def cmd_nudge(args) -> int:
|
|
365
|
+
"""Wake one seat by hand. The manual equivalent of everything the supervisor
|
|
366
|
+
does -- which is the point: the board works without the daemon."""
|
|
367
|
+
from .supervisor import Supervisor
|
|
368
|
+
board = _board(args)
|
|
369
|
+
t = board.topic(int(args.topic) if args.topic.isdigit() else args.topic)
|
|
370
|
+
sup = Supervisor(board, _drivers(board, [args.agent]))
|
|
371
|
+
result = asyncio.run(sup.wake_seat(int(t["id"]), args.agent))
|
|
372
|
+
print(f"{args.agent}: {'ok' if result.ok else 'FAILED — ' + result.detail}")
|
|
373
|
+
if result.tail and args.verbose:
|
|
374
|
+
print(result.tail)
|
|
375
|
+
return 0 if result.ok else 1
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def cmd_prompt(args) -> int:
|
|
379
|
+
"""Print exactly what a seat would be told. Costs nothing; use it before
|
|
380
|
+
spending a real turn on a prompt that turns out to be wrong."""
|
|
381
|
+
from .supervisor import Supervisor
|
|
382
|
+
board = _board(args)
|
|
383
|
+
t = board.topic(int(args.topic) if args.topic.isdigit() else args.topic)
|
|
384
|
+
text, cursor = Supervisor(board, {}).build_prompt(int(t["id"]), args.agent)
|
|
385
|
+
print(text)
|
|
386
|
+
print(f"\n--- would advance {args.agent} to cursor {cursor} on success ---")
|
|
387
|
+
return 0
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def cmd_console(args) -> int:
|
|
391
|
+
"""One terminal where every agent's reply lands and you can talk back."""
|
|
392
|
+
from .console import run_console
|
|
393
|
+
board = _board(args)
|
|
394
|
+
who = _human(board, args.as_)
|
|
395
|
+
topic = args.topic
|
|
396
|
+
if not topic:
|
|
397
|
+
live = [t for t in board.topics() if t["status"] in {"open", "paused"}
|
|
398
|
+
and not t["slug"].startswith("doctor-")]
|
|
399
|
+
# An empty board opens fine: /new works from inside.
|
|
400
|
+
topic = live[0]["slug"] if live else None
|
|
401
|
+
board.close()
|
|
402
|
+
return run_console(args.db, topic, who)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def cmd_tui(args) -> int:
|
|
406
|
+
"""One screen: transcript, seats, tasks, and an input that talks and rules."""
|
|
407
|
+
from .tui import run_tui
|
|
408
|
+
board = _board(args)
|
|
409
|
+
who = _human(board, args.as_)
|
|
410
|
+
topic = args.topic
|
|
411
|
+
if not topic:
|
|
412
|
+
live = [t for t in board.topics() if t["status"] in {"open", "paused"}
|
|
413
|
+
and not t["slug"].startswith("doctor-")]
|
|
414
|
+
# An empty board opens fine: /new works from inside.
|
|
415
|
+
topic = live[0]["slug"] if live else None
|
|
416
|
+
board.close()
|
|
417
|
+
return run_tui(args.db, topic, who)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def cmd_watch(args) -> int:
|
|
421
|
+
"""Read-only tail, for a second terminal beside the one driving."""
|
|
422
|
+
from .console import Console
|
|
423
|
+
board = _board(args)
|
|
424
|
+
who = _human(board, args.as_)
|
|
425
|
+
board.close()
|
|
426
|
+
c = Console(args.db, args.topic, who)
|
|
427
|
+
try:
|
|
428
|
+
c._poll()
|
|
429
|
+
except KeyboardInterrupt:
|
|
430
|
+
c.stop.set()
|
|
431
|
+
return 0
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def cmd_doctor(args) -> int:
|
|
435
|
+
from .doctor import run_doctor
|
|
436
|
+
return asyncio.run(run_doctor(_board(args), only=args.only, timeout=args.timeout))
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# --------------------------------------------------------------------- parser
|
|
440
|
+
|
|
441
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
442
|
+
ap = argparse.ArgumentParser(prog="mooting", description=__doc__.splitlines()[0])
|
|
443
|
+
ap.add_argument("--db", help="board path (default ./.mooting/board.db, or $MOOTING_DB)")
|
|
444
|
+
ap.add_argument("--as", dest="as_", help="act as this human seat (or $MOOTING_HUMAN)")
|
|
445
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
446
|
+
|
|
447
|
+
p = sub.add_parser("setup", help="find your CLIs, seat them, wire them up, prove it works")
|
|
448
|
+
p.add_argument("-y", "--yes", action="store_true",
|
|
449
|
+
help="take every default; seat every CLI found")
|
|
450
|
+
p.set_defaults(fn=cmd_setup)
|
|
451
|
+
|
|
452
|
+
p = sub.add_parser("init", help="create a board and your human seat")
|
|
453
|
+
p.add_argument("--human", default=os.environ.get("MOOTING_HUMAN")
|
|
454
|
+
or os.environ.get("MOOT_HUMAN", "human"))
|
|
455
|
+
p.set_defaults(fn=cmd_init)
|
|
456
|
+
|
|
457
|
+
ag = sub.add_parser("agents", help="manage seats").add_subparsers(dest="sub", required=True)
|
|
458
|
+
p = ag.add_parser("add")
|
|
459
|
+
p.add_argument("name")
|
|
460
|
+
p.add_argument("kind", choices=["claude", "codex", "copilot", "gemini", "agy", "human", "external"])
|
|
461
|
+
p.add_argument("--cwd", help="repo the agent works in")
|
|
462
|
+
p.add_argument("--model")
|
|
463
|
+
p.add_argument("--driver", choices=["stdio_json", "acp", "spawn", "none"])
|
|
464
|
+
p.add_argument("--effort", choices=["low", "medium", "high"],
|
|
465
|
+
help="reasoning effort for this seat; the dominant latency knob")
|
|
466
|
+
p.add_argument("--capability", choices=["deliberate", "execute"],
|
|
467
|
+
help="execute lets this seat edit files, but only for an approved "
|
|
468
|
+
"task on a work topic (default: deliberate)")
|
|
469
|
+
p.add_argument("--arg", action="append",
|
|
470
|
+
help="extra argv passed to this CLI every wake (repeatable)")
|
|
471
|
+
p.set_defaults(fn=cmd_agents_add)
|
|
472
|
+
ag.add_parser("ls").set_defaults(fn=cmd_agents_ls)
|
|
473
|
+
p = ag.add_parser("rm", help="remove a seat from the registry")
|
|
474
|
+
p.add_argument("name")
|
|
475
|
+
p.add_argument("--yes", action="store_true", help="actually do it")
|
|
476
|
+
p.set_defaults(fn=cmd_agents_rm)
|
|
477
|
+
|
|
478
|
+
tp = sub.add_parser("topic", help="manage topics").add_subparsers(dest="sub", required=True)
|
|
479
|
+
p = tp.add_parser("new")
|
|
480
|
+
p.add_argument("slug")
|
|
481
|
+
p.add_argument("--title", required=True)
|
|
482
|
+
p.add_argument("--brief", required=True, help="the question; '-' reads stdin")
|
|
483
|
+
p.add_argument("--seats", required=True, help="comma-separated agent names")
|
|
484
|
+
p.add_argument("--rounds", type=int, default=3)
|
|
485
|
+
p.add_argument("--turns", type=int, default=6, help="per-seat turn ceiling")
|
|
486
|
+
p.add_argument("--effort", choices=["low", "medium", "high"],
|
|
487
|
+
help="override seat effort for this topic (low is ~9x faster)")
|
|
488
|
+
p.add_argument("--mode", choices=["debate", "discuss", "work"], default="debate",
|
|
489
|
+
help="debate: find the flaw (default). discuss: build on each "
|
|
490
|
+
"other. work: a manager assigns tasks and the team does them")
|
|
491
|
+
p.add_argument("--manager", help="work mode: the seat that plans and reviews")
|
|
492
|
+
p.set_defaults(fn=cmd_topic_new)
|
|
493
|
+
|
|
494
|
+
p = tp.add_parser("rm", help="delete one topic and everything in it")
|
|
495
|
+
p.add_argument("slug")
|
|
496
|
+
p.add_argument("--yes", action="store_true", help="actually do it")
|
|
497
|
+
p.set_defaults(fn=cmd_topic_rm)
|
|
498
|
+
|
|
499
|
+
p = sub.add_parser("reset", help="clear every topic (seats are kept)")
|
|
500
|
+
p.add_argument("--all", action="store_true",
|
|
501
|
+
help="also remove every agent seat, leaving a bare board")
|
|
502
|
+
p.add_argument("--yes", action="store_true", help="actually do it")
|
|
503
|
+
p.set_defaults(fn=cmd_reset)
|
|
504
|
+
|
|
505
|
+
p = sub.add_parser("ls", help="list topics")
|
|
506
|
+
p.add_argument("--status")
|
|
507
|
+
p.set_defaults(fn=cmd_ls)
|
|
508
|
+
|
|
509
|
+
p = sub.add_parser("show", help="print a topic's transcript")
|
|
510
|
+
p.add_argument("topic")
|
|
511
|
+
p.set_defaults(fn=cmd_show)
|
|
512
|
+
|
|
513
|
+
p = sub.add_parser("say", help="join the discussion yourself")
|
|
514
|
+
p.add_argument("topic")
|
|
515
|
+
p.add_argument("body", help="'-' reads stdin")
|
|
516
|
+
p.set_defaults(fn=cmd_say)
|
|
517
|
+
|
|
518
|
+
p = sub.add_parser("ask", help="@ one agent directly for its opinion")
|
|
519
|
+
p.add_argument("topic")
|
|
520
|
+
p.add_argument("agent")
|
|
521
|
+
p.add_argument("question", help="'-' reads stdin")
|
|
522
|
+
p.set_defaults(fn=cmd_ask)
|
|
523
|
+
|
|
524
|
+
p = sub.add_parser("install", help="register the MCP server with codex/gemini/agy (one-time)")
|
|
525
|
+
p.add_argument("agents", nargs="?", help="comma-separated; default all seats")
|
|
526
|
+
p.add_argument("--dry-run", action="store_true", help="print the command instead of running it")
|
|
527
|
+
p.set_defaults(fn=cmd_install)
|
|
528
|
+
|
|
529
|
+
p = sub.add_parser("conclude", help="close a meeting and write its minutes")
|
|
530
|
+
p.add_argument("topic")
|
|
531
|
+
p.add_argument("note", nargs="?", help="your closing words, recorded as the conclusion")
|
|
532
|
+
p.add_argument("-o", "--out")
|
|
533
|
+
p.add_argument("--decisions-only", action="store_true")
|
|
534
|
+
p.add_argument("--force", action="store_true",
|
|
535
|
+
help="close it with proposals or questions still open")
|
|
536
|
+
p.set_defaults(fn=cmd_conclude)
|
|
537
|
+
|
|
538
|
+
p = sub.add_parser("minutes", help="write the meeting out as markdown")
|
|
539
|
+
p.add_argument("topic")
|
|
540
|
+
p.add_argument("-o", "--out", help="file to write ('-' for stdout)")
|
|
541
|
+
p.add_argument("--decisions-only", action="store_true",
|
|
542
|
+
help="skip the transcript; keep the rulings and the work log")
|
|
543
|
+
p.set_defaults(fn=cmd_minutes)
|
|
544
|
+
|
|
545
|
+
p = sub.add_parser("tasks", help="the work plan and where each task has got to")
|
|
546
|
+
p.add_argument("topic")
|
|
547
|
+
p.add_argument("--status")
|
|
548
|
+
p.set_defaults(fn=cmd_tasks)
|
|
549
|
+
|
|
550
|
+
p = sub.add_parser("proposals", help="what is waiting on you")
|
|
551
|
+
p.add_argument("topic", nargs="?")
|
|
552
|
+
p.add_argument("--status", default="open")
|
|
553
|
+
p.add_argument("--full", action="store_true")
|
|
554
|
+
p.set_defaults(fn=cmd_proposals)
|
|
555
|
+
|
|
556
|
+
for name, fn, helptext in (("approve", cmd_approve, "approve a proposal"),
|
|
557
|
+
("reject", cmd_reject, "reject a proposal")):
|
|
558
|
+
p = sub.add_parser(name, help=helptext)
|
|
559
|
+
p.add_argument("proposal_id", type=int)
|
|
560
|
+
p.add_argument("-m", "--message", help="your rationale, recorded on the board")
|
|
561
|
+
p.set_defaults(fn=fn)
|
|
562
|
+
|
|
563
|
+
p = sub.add_parser("run", help="drive the debate until it needs you")
|
|
564
|
+
p.add_argument("topic")
|
|
565
|
+
p.add_argument("--resume", action="store_true", help="unpause first")
|
|
566
|
+
p.add_argument("--rounds", type=int, default=0, help="with --resume: grant N more rounds")
|
|
567
|
+
p.add_argument("--max-turns", type=int, default=6, dest="max_turns")
|
|
568
|
+
p.add_argument("--max-wakes", type=int, default=30, dest="max_wakes",
|
|
569
|
+
help="per agent per hour; a failed wake still counts")
|
|
570
|
+
p.add_argument("--effort", choices=["low", "medium", "high"],
|
|
571
|
+
help="council-wide effort for this run (default low)")
|
|
572
|
+
p.add_argument("--sequential", action="store_true",
|
|
573
|
+
help="one seat at a time so each sees the last; slower by ~N x")
|
|
574
|
+
p.set_defaults(fn=cmd_run)
|
|
575
|
+
|
|
576
|
+
p = sub.add_parser("nudge", help="wake one seat by hand")
|
|
577
|
+
p.add_argument("topic")
|
|
578
|
+
p.add_argument("agent")
|
|
579
|
+
p.add_argument("-v", "--verbose", action="store_true")
|
|
580
|
+
p.set_defaults(fn=cmd_nudge)
|
|
581
|
+
|
|
582
|
+
p = sub.add_parser("prompt", help="show what a seat would be told, without waking it")
|
|
583
|
+
p.add_argument("topic")
|
|
584
|
+
p.add_argument("agent")
|
|
585
|
+
p.set_defaults(fn=cmd_prompt)
|
|
586
|
+
|
|
587
|
+
p = sub.add_parser("console", help="live council view: watch replies and talk back")
|
|
588
|
+
p.add_argument("topic", nargs="?", help="default: the most recent open topic")
|
|
589
|
+
p.set_defaults(fn=cmd_console)
|
|
590
|
+
|
|
591
|
+
p = sub.add_parser("tui", help="full-screen session: talk, watch the work, rule")
|
|
592
|
+
p.add_argument("topic", nargs="?", help="default: the most recent open topic")
|
|
593
|
+
p.set_defaults(fn=cmd_tui)
|
|
594
|
+
|
|
595
|
+
p = sub.add_parser("watch", help="read-only live tail of a topic")
|
|
596
|
+
p.add_argument("topic")
|
|
597
|
+
p.set_defaults(fn=cmd_watch)
|
|
598
|
+
|
|
599
|
+
p = sub.add_parser("doctor", help="smoke-test each CLI driver end to end")
|
|
600
|
+
p.add_argument("--only", help="comma-separated agent names")
|
|
601
|
+
p.add_argument("--timeout", type=float, default=180.0)
|
|
602
|
+
p.set_defaults(fn=cmd_doctor)
|
|
603
|
+
|
|
604
|
+
return ap
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def main(argv: list[str] | None = None) -> int:
|
|
608
|
+
args = build_parser().parse_args(argv)
|
|
609
|
+
try:
|
|
610
|
+
return args.fn(args)
|
|
611
|
+
except StoreError as exc:
|
|
612
|
+
print(f"mooting: {exc}", file=sys.stderr)
|
|
613
|
+
return 1
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
if __name__ == "__main__": # pragma: no cover
|
|
617
|
+
raise SystemExit(main())
|