2026-07-29 17:57:07 +02:00
|
|
|
#!/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
|
2026-07-30 07:35:28 +02:00
|
|
|
../AGENTS.md for the workflow and the module map:
|
2026-07-29 17:57:07 +02:00
|
|
|
|
|
|
|
|
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
|
2026-07-30 09:47:38 +02:00
|
|
|
sync.py origin/main as the shared board: push on move, pull on a beat
|
2026-07-29 17:57:07 +02:00
|
|
|
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
|
2026-07-29 17:57:07 +02:00
|
|
|
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
|
2026-07-29 17:57:07 +02:00
|
|
|
import state
|
2026-07-30 09:47:38 +02:00
|
|
|
import sync
|
2026-07-29 17:57:07 +02:00
|
|
|
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
|
|
|
|
|
|
2026-07-29 19:55:57 +02:00
|
|
|
# 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)
|
2026-07-29 17:57:07 +02:00
|
|
|
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()
|
2026-07-30 09:47:38 +02:00
|
|
|
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()
|
2026-07-29 17:57:07 +02:00
|
|
|
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()
|