Files
bench/manager/core/board.py
T

90 lines
3.2 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Live kanban board for ../tasks/ — board, session timeline, heads-up display.
python3 .task-manager/manager/board.py # serve on :26071, open a browser
python3 .task-manager/manager/board.py --port 9000 --no-open
The manager sits cleanly on top of the tasks/ directory: it reads and moves
task files, but the tasks work as a plain folder kanban without it. See
../AGENTS.md for the workflow and the module map:
config.py paths, stages, launch configuration
state.py shared registries, event persistence, SSE fan-out
taskfiles.py reading/moving task files (the only code touching tasks/)
events.py hook payloads → displayable events, session registry
sync.py origin/main as the shared board: push on move, pull on a beat
agents.py headless work/review agents: launch, reap, stop, diff
2026-08-01 09:43:57 +02:00
phases.py a phase run: its own branch, its members merged into it
watch.py 2s disk poller narrating moves made outside the API
httpd.py HTTP routes, SSE stream, the page itself
.prompts/ agent prompt templates (read fresh on every launch)
Stdlib only, no install.
"""
from __future__ import annotations
import argparse
import errno
import threading
import webbrowser
from http.server import ThreadingHTTPServer
import config
import drive
import events
import github
import httpd
2026-08-01 09:43:57 +02:00
import phases
import state
import sync
import watch
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--port", type=int, default=config.PORT)
parser.add_argument("--no-open", action="store_true", help="don't open a browser")
args = parser.parse_args()
state.serve_port = args.port
url = f"http://127.0.0.1:{args.port}/"
try:
server = ThreadingHTTPServer(("127.0.0.1", args.port), httpd.Handler)
except OSError as exc:
if exc.errno != errno.EADDRINUSE:
raise
# The port is pinned, so this is nearly always the board already running.
print(f"Port {args.port} is already in use — assuming the board is up at {url}")
if not args.no_open:
webbrowser.open(url)
return
# parents=True: local/state/ is gitignored and ships empty, so a fresh
# checkout has neither it nor its children — boot must create the chain.
config.SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
config.AGENT_DIR.mkdir(parents=True, exist_ok=True)
events.load_disk_sessions()
threading.Thread(target=watch.watcher, daemon=True).start()
threading.Thread(target=github.poller, daemon=True).start()
threading.Thread(target=github.reconcile, daemon=True).start()
2026-08-01 09:43:57 +02:00
threading.Thread(target=phases.beat, daemon=True).start()
if config.SYNC:
# Team mode: board commits publish themselves and a beat pulls what
# the other boards published. Off, neither thread nor hook exists.
sync.install()
threading.Thread(target=sync.beat, daemon=True).start()
drive.adopt()
print(f"Task board for {config.TASKS}\n {url}\n Ctrl-C to stop")
if not args.no_open:
webbrowser.open(url)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nstopped")
if __name__ == "__main__":
main()