Files
bench/install.py
T
istosandClaude Fable 5 cc931a9174 Start cleanly when bench is the project itself
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>
2026-07-29 19:55:57 +02:00

73 lines
2.4 KiB
Python

#!/usr/bin/env python3
"""Wire the task manager into the project it sits in.
python3 .task-manager/install.py # apply (idempotent)
python3 .task-manager/install.py --dry-run # report only, change nothing
Vendor-specific wiring belongs to the configured agent adapter: this script
resolves the adapter (BOARD_AGENT_ADAPTER in manager/local/.env, default
"claude"; local/adapters/ overrides core/adapters/) and runs its `wire`
executable against the project root. Safe to run any time — after dropping
.task-manager/ into a new repo, and after every update.sh.
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
TM = Path(__file__).resolve().parent
LOCAL = TM / "manager" / "local"
CORE = TM / "manager" / "core"
def _project_root() -> Path:
"""The host project's root: the git toplevel seen from the manager's
directory (same resolution as config._repo_root). Vendored installs
drop .task-manager/.git at clone time, so this finds the host repo;
when the manager IS the repo (self-hosted), it finds that repo itself.
No git → fall back to the vendored-layout assumption, the parent."""
try:
out = subprocess.check_output(
["git", "-C", str(TM), "rev-parse", "--show-toplevel"],
text=True, stderr=subprocess.DEVNULL,
).strip()
if out:
return Path(out)
except (subprocess.CalledProcessError, OSError):
pass
return TM.parent
PROJECT = _project_root()
def adapter_name() -> str:
if os.environ.get("BOARD_AGENT_ADAPTER"):
return os.environ["BOARD_AGENT_ADAPTER"]
env_file = LOCAL / ".env"
if env_file.is_file():
for line in env_file.read_text(encoding="utf-8").splitlines():
key, _, value = line.strip().partition("=")
if key.strip() == "BOARD_AGENT_ADAPTER" and value.strip():
return value.strip().strip("'\"")
return "claude"
def main() -> int:
name = adapter_name()
for base in (LOCAL / "adapters", CORE / "adapters"):
wire = base / name / "wire"
if wire.is_file():
return subprocess.call(
[sys.executable, str(wire), str(PROJECT), *sys.argv[1:]])
print(f"agent adapter '{name}' has no wire script — looked in "
f"{LOCAL / 'adapters' / name} and {CORE / 'adapters' / name}.")
return 1
if __name__ == "__main__":
sys.exit(main())