Files
bench/manager/core/board.py
T
istosandClaude Fable 5 e10b5cf1ed Move the workflow brief to AGENTS.md; keep CLAUDE.md as a pointer
AGENTS.md is the cross-vendor name (opencode, Codex, Gemini CLI read it
natively; so does current Claude Code), and the brief goes to every
vendor's agents — an opencode work agent previously launched with no
project brief at all. The content moves verbatim to AGENTS.md at the
root and in manager/local/; each CLAUDE.md becomes a load-bearing
compatibility pointer (@AGENTS.md import) for older Claude Code CLIs.

update.sh's core-owned file list now carries both names, so updating an
old-layout install lands AGENTS.md and replaces the full CLAUDE.md with
the pointer instead of resurrecting it. All four core prompts, README,
the board.py/taskfiles.py docstrings, the task template and the adapter
contract docs now name AGENTS.md; adapters/README.md notes that vendors
reading AGENTS.md from the working tree need no adapter work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 07:35:28 +02:00

80 lines
2.7 KiB
Python

#!/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
agents.py headless work/review agents: launch, reap, stop, diff
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 state
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()
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()