Add a no-build, same-origin web frontend so an operator can open a URL,
see every agent's state, and answer a paused question with no terminal
(Phase 3 DoD). The UI is a client of the existing API — no endpoint,
schema, or auth change — so the 106 existing tests pass unchanged.
- app.py serves the bundled UI from / and /static, gated on UI_ENABLED
(default on); optional CORS_ORIGINS (default empty => no middleware)
for hosting the UI on a separate origin. Dedicated /static prefix +
explicit / route so API routes are never shadowed. Zero new runtime
deps (StaticFiles/CORSMiddleware ship with Starlette).
- static/: vanilla fetch + plain CSS + vendored alpine.min.js (v3.14.8,
no CDN). Token captured once into localStorage; all API values render
via x-text (never x-html) to block agent-authored markup injection.
Project switcher, agent list, checkmark panel, paginated log, shared
feed, and Answer / Answer & Resume. Polling scoped to the selected
agent to avoid an N+1 over the fleet.
- config.py: ui_enabled, cors_origins (+ cors_origin_list); documented
in .env.example.
- tests/test_api_ui.py: serving, unauthenticated shell, non-shadowing
401 regression, CORS toggle, UI_ENABLED=false. 114 tests, ruff clean.
The static assets ship in the wheel by default (they live inside the
packaged src/handler tree) — no force-include needed.
Implements the Phase 1 MVP from the README: a stateless control layer + HTTP
API over a centralized database, with hook-enforced test/push gates.
- DB layer: SQLAlchemy Core, one schema rendering both Postgres (BIGSERIAL /
TIMESTAMPTZ / JSONB) and SQLite (INTEGER PK / TEXT / JSON) via portable types;
native ON CONFLICT DO UPDATE checkmark upsert on both dialects.
- Alembic dual-dialect migrations (render_as_batch for SQLite); tests run a real
`alembic upgrade head`.
- FastAPI: projects/agents/checkmark/log/answer/resume + shared log/context
routes, single global bearer token, higher-trust token gating shared-context
writes, project isolation on every route.
- Hooks (`python -m handler.hooks <event>`): Stop test gate (block on red),
PreToolUse AskUserQuestion defer + `git push` gate (tests then throwaway
build), Notification generic webhook (no-op without WEBHOOK_URL). Identity via
env injected at spawn; verify is the mock seam.
- Control CLI: spawn/list/attach/kill, hard `.mise.toml [tasks.test]` gate,
generated per-agent settings.json, identity + DATABASE_URL injected via tmux;
tmux is the mock seam.
- 45 tests (SQLite), ruff clean. Live claude/tmux/mise spawning deferred behind
the mocked seams.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5ZuS5pV1NS6eKsRZHXonY