schedules.model_id (migration 0013) pins every fired run of a recurring spawn
to a registered model backend, exactly like a hand-spawned agent: the worker
copies it into each firing's spawn payload, the launched agent records the pin,
and resumes stay on the same backend. The Schedules form gets the same Model
dropdown as the spawn form (Claude subscription by default), with a badge in
the schedules table. Create/update routes fail fast on a missing or disabled
backend so a stale selection bounces immediately instead of every firing
failing asynchronously in Activity; a backend deleted later still fails each
firing visibly rather than silently falling back to the subscription.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzDofD7gP63WpeLG8vEdZu
Operators can register Anthropic-API-compatible endpoints (a local Qwen/Llama
behind LiteLLM or claude-code-router, an LLM gateway) on the dashboard's
Claude -> Models tab and pick one from a Model dropdown when spawning an agent.
The agent still launches as the same claude binary with the same hooks, skills,
connectors, plugins, and gates — only its ANTHROPIC_BASE_URL / ANTHROPIC_MODEL /
ANTHROPIC_AUTH_TOKEN env differs — and it stays pinned to its backend across
resumes. No selection keeps the worker's Claude subscription untouched.
- claude_models table (+ agents.model_id pin), migration 0012
- control.models resolves a row into the launch env (API keys Fernet-encrypted
at rest, decrypted only in the control container; placeholder key when none is
stored so the subscription OAuth token never reaches a local endpoint)
- /claude/models CRUD (admin-gated writes, key never returned), spawn route +
worker + CLI (--model) pass the selection through, fail-fast on missing or
disabled backends
- dashboard: Models tab, spawn-form dropdown, model badge in the agents table
- docs/local-models.md: why bare OpenAI-compatible servers break tool calling
with Qwen-Coder, and working vLLM/LiteLLM/llama.cpp stacks
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzDofD7gP63WpeLG8vEdZu
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.
Phase 2 configures forge for the agents (operator only sets a credential_ref +
optional version pin) and lets them drive a junior→senior→deploy workflow:
- Credential resolution/injection (control/credentials.py): credential_ref pointers
(env:/file:/cmd:) resolved only at spawn, injected as FORGE_TOKEN + host var, with a
forge-host-scoped git credential helper reading the token from env (never on disk / in
the DB). Resolution is a fail-fast spawn gate.
- Role-based forge skills committed into the managed repo (control/skills_gen.py,
`handler forge-init`): forge-junior/senior/deploy + a workflow overview.
- Hard approval gate (hooks/gate.py, approvals table, migration 0002): merge/deploy —
and direct pushes to protected branches — are denied unless a DIFFERENT agent has an
`approved` record for the branch, pinned to the reviewed commit (approved_sha). Senior
records verdicts via `handler approve`/`reject`.
- forge/git seams (control/forge.py, control/gitops.py) matching the Phase 1 seam pattern.
- CI status poller (control/poller.py, `handler poll-ci [--watch]`) backfilling
ci_status/ci_checked_at via `forge ci list`.
- Fix: migrations/env.py commits explicitly after run_migrations — pysqlite on Py 3.12+
was rolling back the final migration's DDL + alembic_version stamp (latent in Phase 1).
Reviewed via a separate code-reviewer pass; gate-bypass and credential-scoping findings
addressed. 106 tests, ruff clean, verified end-to-end against real git + migrations.
Move the original plan-of-action out of README.md into docs/PLAN.md (verbatim,
with Phase 1 checkboxes ticked and MVP-resolved open questions annotated). Replace
the README with a proper project readme grounded in the implemented MVP: what it is,
architecture, install/configure/run, curl walkthrough, control CLI, API reference,
hooks, development, and layout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5ZuS5pV1NS6eKsRZHXonY