Agents narrate far more than anyone reads: the transcript is not the
deliverable, and prose there is spent tokens burying information where
no one looks. The new skill routes each kind of output to its store —
work happens through tool calls; a minimized NOTES.md ledger (one
bullet per action, committed with the work) records what happened and
how; problems and causes go to memory; status goes to the final
checkpoint-sized message the Stop hook captures onto the checkmark; and
questions go through the question tool, which reaches the operator as a
push notification and an answer prompt in the web and mobile apps
instead of stalling silently in the transcript.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
Seven skills now travel with Handler itself and are seeded into the
managed skill store when the API boots: gate recovery, the test
authorship standard, checkpoint quality, memory discipline, mise-task
rules, scheduled-run continuity, and secrets hygiene. They cover the
judgment layer the hard gates cannot enforce — the gates check that
tests pass, not that an agent responded to a blocked gate sensibly or
kept credentials out of logs.
Seeding is idempotent by name: existing rows are never touched, so
operator edits and enable/disable choices survive every upgrade;
deleting a built-in restores it as shipped on the next start (disable
is the supported off-switch). Rows are created shared and enabled, so
they sync to every worker like any managed skill and remain
admin-editable from the dashboard or the mobile app. Seeding failures
log and never block the API from serving.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
Settings → Manage now opens the entire admin control surface on the
phone, mirroring the web dashboard page for page:
- Models: full CRUD for model backends incl. write-only API keys
(set/clear), harness selection (claude/pi), enable toggles.
- Skills: create, toggle, delete, expand to read SKILL.md, and
install-from-prompt driven through the command queue.
- Connectors: stdio/http/sse MCP servers with args/env/header parsing.
- Plugins: marketplace plugins pinned to their repo.
- Permissions: default mode + allow/deny/ask rules over the read-only
env baseline.
- Repositories: register in git-server or manual mode (incl. the
mise-init bootstrap), sync, delete.
- Git servers: forge hosts with encrypted tokens and generated deploy
keys (public half selectable for copying).
- Approvals: record operator approve/reject verdicts per branch.
- Shared context: browse and set the cross-agent key/value store.
- Users: invite (with shareable invite links), promote/disable, mint
reset links, delete — the user-accounts feature that just landed.
- Claude login: drive the worker's claude /login flow from the phone.
- Account (Settings): who you're signed in as, change password,
sign out with server-side session revocation.
The connect screen gains the matching gate: email sign-in via
/auth/login (session token stored like the legacy env token),
first-run setup when the server has zero accounts, forgot-password,
and the API-token method as fallback (auto-selected for pre-accounts
servers). Memory gains note authoring + deletion via a new
reloadMemory hook.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
Groundwork for the full admin surface on mobile: the API client gains
the user-account and Claude-management types (skills, connectors,
plugins, permissions, users/auth, owner fields) plus an unauthenticated
authApi helper; AppState exposes the client and the new management
screen names; ManageShell/Field/ErrorNotice and a useResource hook give
the subscreens one shared page/fetch pattern; Settings gains Manage and
Account rows leading to the new hub.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
A new Memory tab loads /memory/graph when opened and lists the
distilled notes newest-first with kind filter chips
(fact/decision/gotcha/runbook); tapping a note expands its body, tags,
and both directions of its links, resolved to note titles. Read-only by
design — authoring stays with the web dashboard's admin surface and the
agents' own MCP server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
A new Schedules tab lists every schedule across projects with its
interval, prompt, next/last run, and role/model badges; the operator
can create one (project, name prefix, interval, role, model backend,
prompt), pause/resume with a switch, and delete with confirmation.
Mirrors the web dashboard's Schedules page, including the model picker
added there. New clock/brain icons join the Lucide subset and the tab
bar grows to four tabs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
Add an Events tab between Checkmark and Log: while the detail (or
answer) screen is open the store polls the cursor-paged
/agents/{name}/events endpoint every 3s and renders the stream-json
events the way the web dashboard does — assistant text as prose, tool
calls as badges, results as a turns/cost footer, worker notices as
danger callouts, raw lines verbatim. The meta card now also shows the
agent's model backend and supervising worker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
Fetch the registered model backends (/claude/models) alongside the
fleet poll and offer the enabled ones in a Model select on the spawn
form, defaulting to the Claude subscription — the mobile counterpart of
the web dashboard's per-spawn dropdown. The select is hidden when no
backends are registered, and Select itself now takes value/label pairs
as well as bare strings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
Port the schema additions the backend gained since the last mobile
release: headless-runner fields on Agent (model_id, session_id,
worker_id) plus the AgentEvent row, model_id on Schedule, the
ClaudeModel backend registry row, and the agent-memory note/link/graph
shapes. Mirrors frontend/lib/api.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
- AuthGate replaces the raw token prompt: first-run setup form (creates
the admin) when no accounts exist, email/password sign-in with a
forgot-password flow, and a collapsible raw-API-token fallback for
legacy/script setups.
- /reset is a public page where invite and password-reset links land;
success stores the fresh session and enters the dashboard.
- Users section (admin-only nav): invite by email (link always shown,
emailed when SMTP is configured), admin/disable toggles, reset links,
and delete with the shared-resources handoff spelled out.
- Sidebar shows who is signed in; sign-out revokes the session
server-side.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ws7xj5Ej623hh4GXQCYYR
24 new tests: setup/login/session lifecycle, invites, resets (with and
without SMTP), admin guards and the last-admin lockout guard, resource
reassignment on user deletion, cross-user 404s on projects/skills/
connectors/plugins/models/schedules/commands/memory, private model
backends rejected at spawn/schedule time, and claude_gen materializing
only shared + owner rows at launch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ws7xj5Ej623hh4GXQCYYR
claude_gen.apply now takes the launching project's owner and materializes
only shared rows plus that user's own; spawn and resume pass it through.
skill_install stamps imported rows with the requesting user from the
command payload (reinstalls keep the existing owner).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ws7xj5Ej623hh4GXQCYYR
Projects, agents, interaction, approvals, schedules, memory, commands,
and the Claude page resources (skills/connectors/plugins/models) now
resolve through the Actor's ownership rules: users see shared rows plus
their own, creates stamp the creating user as owner, mutations require
the owner or an admin, and invisible resources 404. Legacy env tokens
keep their exact historical semantics (all-access; admin token for the
old admin-gated routes). Commands from a user carry a user:<id> audit
label so they can track their own non-project commands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ws7xj5Ej623hh4GXQCYYR
- deps: every request resolves to an Actor — a signed-in user (session
token, hash-stored) or a legacy env token with its historical
semantics. require_admin/require_shared_write now accept admin users.
- /auth: status probe, first-run setup (first account = admin), login,
logout, me, change-password, forgot/reset (one-shot links), and
admin user management with invite links + a last-admin lockout guard.
- Invite/reset links are emailed when SMTP is configured and always
returned to the admin; forgot never reveals account existence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ws7xj5Ej623hh4GXQCYYR
users/auth_sessions/auth_tokens tables plus a nullable owner_user_id on
projects, claude_skills, claude_connectors, claude_plugins, and
claude_models (null = shared/legacy, so upgrades keep behaving as before).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ws7xj5Ej623hh4GXQCYYR
Keep-a-Changelog format, seeded with this PR's release: the pi harness
feature set, the control-image changes (Node 22, pi baked in), the additive
0015_model_harness migration, and a numbered rollout checklist (image order,
env, rollback, volume invariants) so the deployment strategy can be verified
against it after merge. README links to it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KdGv3u3DfTsP1S188KDhVH
pi ships no web tools and claude's WebSearch/WebFetch are Anthropic-server-side
(absent on local endpoints), so these are handler-owned: a new handler.webtool
module (httpx, already a dependency) exposed through the bridge extension the
same way as the memory tools. web_fetch is provider-free — GET, HTML stripped
to readable text, size-capped. web_search resolves SEARXNG_URL, then
BRAVE_SEARCH_API_KEY, then falls back to DuckDuckGo's HTML endpoint with zero
config. Verified live: a pi agent now advertises all 14 tools (7 built-ins +
ask_operator + 4 memory + 2 web).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KdGv3u3DfTsP1S188KDhVH
pi ships seven built-in tools but activates only read/write/edit/bash by
default. The --tools flag can't fix this (it is a strict allowlist that drops
extension tools — verified against a live endpoint), so the bridge calls
setActiveTools with everything registered at session start: all seven built-ins
plus ask_operator and the four memory tools. Verified live: the model now
receives all twelve tool definitions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KdGv3u3DfTsP1S188KDhVH
Model backend rows gain a harness column (claude | pi). A pi-harness row runs
the agent through the pi coding agent instead of the claude binary — pi speaks
the OpenAI Completions API natively, so a bare vLLM/llama.cpp/Ollama endpoint
needs no LiteLLM/claude-code-router translation proxy, and the loop is far
lighter for slow local token throughput. The Claude subscription and existing
claude-harness backends are untouched.
Parity comes from generated per-agent artifacts under ~/.handler-pi (outside
the repo tree, so the clean-tree gate never trips): models.json + settings.json
render the row as a pi provider pinned as the default model; a bundled bridge
extension (pi_bridge.ts) adapts pi's events to the exact stdin/stdout contract
of `python -m handler.hooks` — the Stop/completion gate re-prompts pi with
blockers via a follow-up message, git push runs the test/build/approval gates
and denies on failure, questions defer through an ask_operator tool into the
normal answer/resume flow, and memory recall is injected at session start. The
memory tools are registered natively (pi has no MCP), shelling to a new
`python -m handler.mcpserver --call <tool>` seam that reuses the MCP server's
implementations. Skills reuse the same ~/.claude/skills sync (pi implements the
same SKILL.md standard) plus the repo's committed .claude/skills.
Sessions are single JSONL files pre-assigned via --session, so cross-worker
resume archives/materializes exactly like claude's; the prompt travels on stdin
(pi has no -- separator). The supervisor normalizes pi's event stream on the
fly: assistant message_end feeds last_output, the final agent_end becomes the
run result. The whole chain was validated live against pi 0.84.1 with a stub
OpenAI endpoint: memory injection, push-gate denial (including the protected-
branch approval gate), stop-gate block loop, and ask_operator pause all ran
end to end through the real hooks and DB.
Also: harness selector in the dashboard Models form, pi baked into the control
image (NodeSource 22 for pi's node >= 22.19 floor), PI_BIN override, docs in
docs/local-models.md, fake_pi fixture + 12 tests (361 total green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KdGv3u3DfTsP1S188KDhVH
src/handler/api/static/ was a committed build artifact: Next's content-hashed
chunk names churn on every build, so any two branches touching frontend/ were
guaranteed merge conflicts there, PR diffs drowned in generated churn, and a
forgotten `npm run export` could silently ship a UI older than its source.
- gitignore the export (plus frontend/out and .next were already covered) and
remove the 52 tracked files.
- Dockerfile grows a `ui` stage (npm ci + npm run build) whose output is copied
into the packaged tree before pip install, so the image published by docker.yml
always carries a UI built from exactly that commit's source — the frontend
build is now effectively part of CI with no new workflow.
- .dockerignore excludes frontend artifacts and any stale local export: COPY
into src/handler/api/static merges, so a checkout copy must never leak in.
- pyproject: hatchling skips VCS-ignored files, so `artifacts` re-includes the
export when present; absent it, the wheel builds fine and the API just runs
headless (it only mounts static/ when the directory exists).
- README documents the two build paths (Docker stage vs `npm run export` for
source installs) and the headless fallback.
Verified: wheel with the export present ships all 52 files (memory page
included); wheel without it builds clean and create_app() skips the UI mount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqkoYPX8NAo1V2KyXr1pk
The distilled knowledge layer over the raw log/transcript history, inspired by
TencentDB-Agent-Memory's memory-hub model, adapted to handler's invariants
(state lives only in the database; workers stay stateless and disposable):
- memory_notes + memory_links tables (migration 0014) with a portable DAL:
scoped listing/search (project + global), idempotent linking, and a one-read
graph. Deleting an agent nulls attribution but keeps its notes; deleting a
project removes its notes and edges, leaving global knowledge intact.
- /memory API routes: reads on the normal token, note/link authoring on the
admin token, plus GET /memory/graph for the dashboard.
- Bundled handler-memory MCP server (python -m handler.mcpserver), a
dependency-free stdio JSON-RPC implementation injected into every launch's
--mcp-config ahead of the DB connectors and allowlisted in generated
settings, exposing memory_search / memory_get / memory_save / memory_link.
Identity and DATABASE_URL arrive via the spawn env, same as hooks.
- SessionStart recall hook: injects the most recent notes in scope as
additional context at session start, best-effort, never blocking.
- Memory page in the web UI: a hand-rolled force-directed SVG graph of the
note web (colored by kind, hover highlights, click-through details), plus
note/link authoring — the app's first visualization, no chart dependency.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqkoYPX8NAo1V2KyXr1pk
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
Skill marketplaces (SkillsMP and friends) publish an install prompt meant to
be pasted into an interactive claude, which fetches the skill's files and
places them under a skills directory. Handler has no interactive claude and
its skills are DB rows, so the Skills tab gains an "Install from a marketplace
prompt" card wired to a new skill_install command: the worker runs the pasted
prompt through a one-off headless claude in a throwaway staging directory
(sandboxed by a generated settings.json allowing fetch/clone tooling with
acceptEdits), then imports whatever <skill>/SKILL.md landed as managed rows —
reinstalling a skill updates it in place.
Headless means nobody can answer questions mid-install, so the wrapper prompt
front-loads the answers a human would give: install into the staging dir,
always user scope (Handler distributes skills to workers itself), pick the
instructions' defaults, never stop to ask, and end with a report of the
choices made — surfaced in the command result for after-the-fact review, with
the imported skill editable/disableable in the UI.
Multi-file skills survive the import: a new claude_skill_files table
(migration 0011, alongside the command-type constraint change) captures
auxiliary files (references/, scripts/, ...), the launch-time sync rebuilds
each managed skill dir from them, and skill cards list what a skill ships
with. The one-off run's timeout defaults under worker_stale_after so a slow
install can't get the worker's live runs falsely reaped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f42XmjtVsc3zQ9Dhn6DqZ
The dashboard's Claude page now manages the whole Claude Code install agents
run on, not just the account login:
- Skills: operator-authored SKILL.md rows, synced to each worker's user-level
~/.claude/skills at every launch. Managed dirs carry a .handler-managed
marker so deletions in the UI propagate while hand-installed skills survive.
- Connectors: MCP servers (stdio/http/sse) written per-launch as
.claude/mcp-servers.json and passed to claude via --mcp-config, so nothing
lands in the managed repo's tracked tree.
- Plugins: marketplace-pinned plugins folded into generated settings as
extraKnownMarketplaces + enabledPlugins, installing on boot of headless runs.
- Permissions: defaultMode override plus allow/deny/ask rules merged over the
env baseline into every generated settings.json.
All of it is plain DB state (new claude_skills / claude_connectors /
claude_plugins / claude_config tables, migration 0010) edited through the new
admin-gated /claude/* API routes and applied by the control container at spawn
and resume — changes reach the next launch of every agent with no redeploy.
The login flow moved into the page's Account tab unchanged; /login redirects
to /claude for old bookmarks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f42XmjtVsc3zQ9Dhn6DqZ
Agents could be marked done while leaving work uncommitted or unpushed:
the Stop gate only ran the test suite, and the headless supervisor's
fallback marked any still-working agent done on a clean process exit
even when the Stop gate never recorded a verdict. Checkmarks also only
ever carried hook-written boilerplate, so the webui had no real
checkpoint to show.
The Stop gate now blocks the turn on any of: failing tests, uncommitted
changes, or commits no origin/* ref contains (rev-list --not
--remotes=origin, so it works for the --no-track worktree branches).
All blockers are reported at once; status 'done' only ever accompanies
a fully passing gate. Working dirs that aren't git checkouts, and repos
without an origin remote, skip the git half so local-only projects
can't deadlock. The supervisor's clean-exit fallback now reconciles a
still-working agent to blocked instead of done — done is a gate
verdict, not an exit code (operator cancels still settle as done).
The agent's final message is captured deterministically from the
session transcript onto the checkmark's where_it_stopped, so the
dashboard always shows a real checkpoint regardless of whether the
agent thought to leave one; a blocked checkmark shows the blockers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7mF6qeryi9nJthaxYkfPm
Agents spawned by handler were getting branches several commits behind
main. Two compounding causes: sync_project ran 'git pull --ff-only' in
the project root, which only moves whichever branch the root checkout
happens to be on — an agent parked on a feature branch left origin/*
stale (and the pull's 'no tracking information' failure degraded to an
easy-to-miss sync_note). Then worktree spawns cut new branches from the
root's HEAD, inheriting that stale state.
sync_project now fetches origin (refreshing origin/* regardless of the
checkout), re-pins origin/HEAD, and fast-forwards the checkout only when
it sits on the default branch — a diverged default branch still fails
loudly. New worktree branches are cut from origin/HEAD with --no-track
so they start at the remote default branch's tip and don't adopt it as
upstream.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7mF6qeryi9nJthaxYkfPm
The dashboard was a single route that swapped section components via a
`section` state field. Convert it to the App Router's multi-page model so
each left-nav selection is its own route (/, /repositories, /agents,
/schedules, /approvals, /servers, /activity, /shared, /login), making the
pages modular and independently updatable.
- Move the token gate + store provider + sidebar into a persistent frame
(AppFrame + Shell) rendered by the root layout, so auth, the polling
loop, and shared state survive client-side navigation.
- Sidebar items are now <Link> routes; the active item and the store's
polled section are derived from the URL (lib/nav).
- Each section gets an app/<section>/page.tsx; Runs stays at root and keeps
its full-height split layout, the rest render in the shared scroll frame.
- Emit per-route index.html (trailingSlash) so the FastAPI StaticFiles
mount serves clean slash-terminated URLs with no SPA rewrite.
- Regenerate the bundled static export.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsAeGVadULRzPhV2PRDttM
Fixes from the post-migration code review (3 major, 4 minor):
- worker: heartbeat between every drained command so a long queue can't
starve proof-of-life into a false reap; worker_stale_after default
60s -> 300s (one slow sync/login command must not look like a crash)
- repository.create_run: enforces one running run per agent atomically
(agent-row FOR UPDATE on Postgres; SQLite's single writer suffices) -
two workers claiming resumes for the same agent can no longer both
launch claude on one session; resume surfaces the loss loudly
- headless._settle: upload the final session archive BEFORE marking the
run finished - a resume claimed the instant a run leaves 'running'
materializes from session_archives, and the old order let it race an
incomplete archive into needless context re-injection (found as a
test flake, real in production)
- store.tsx: generation token drops in-flight loadRun writes after the
user switches runs (run A's events/log/checkmark no longer land on
run B), plus id-keyed dedup on event appends from overlapping polls
- credsync: credential files written 0600 from the first byte
- headless: seq counter locked (reader thread + supervisor both emit
events); proc.stdout closed after reader join
- login: submit pins to the latest CLAIMED login_start (a still-running
one previously pinned to the wrong worker)
Suite 296 green (new: create_run conflict coverage); reaper tests track
the new staleness default.