Three layout defects broke ./start.sh in the self-hosted repo, one of which (missing local/state/) broke every fresh vendored install too: - install.py resolved the project as TM.parent, a hardcoded vendored- layout assumption. It now asks git for the toplevel from the manager's directory (same resolution as config._repo_root), falling back to the parent when git is unavailable — vendored installs still find the host repo, self-hosted bench finds itself instead of its parent. - adapters/claude/wire hardcoded ".task-manager/" into the emit hook command and plansDirectory. Both are now derived from the manager's path relative to the project root, so vendored installs keep the .task-manager/ prefix and self-hosted bench gets prefix-free paths. _is_ours also recognises the emit.py suffix, so settings wired with the old literal path count as stale and are repaired idempotently. - board.py and state.py created the sessions/agent dirs without parents=True; local/state/ is gitignored and ships empty, so a virgin checkout died with FileNotFoundError before serving. Boot now creates the whole chain, wiring or no wiring. tests/test_self_hosting.py (stdlib unittest) covers both layouts' wiring, stale-path repair, idempotent re-runs, refusal without .claude/, root resolution with and without git, and an integration boot of board.py from a scratch checkout with no local/state/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
80 lines
2.7 KiB
Python
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
|
|
../CLAUDE.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()
|