#!/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 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 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() 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()