Worktree spawns now fetch only. The agent's branch is cut from
origin/* and the root checkout - which may hold the operator's own
work - is never fast-forwarded, merged, or re-parked by a spawn. A
failed fetch on a worktree spawn is fatal instead of a silent note:
the contract is 'starts at the remote's latest push', and cutting a
branch from stale refs would break it quietly.
Hardening around the same contract: the branch start falls back
origin/HEAD -> origin/main -> origin/master when the head pin is
missing, and a stale agent/<name> branch left behind by a deleted
agent is reset to the remote tip (-B) instead of silently shadowing
it - while -B's refusal to move a branch checked out elsewhere keeps
in-flight agents protected. Root/subdir placements (schedule firings,
mise-init) keep the fast-forward behavior for their shared tree, as
does the explicit sync command.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
A spawn with no explicit placement ran the agent in the shared
project-root checkout. The root only fast-forwards while it is parked
on the default branch, so the moment one agent left it on a feature
branch, every later no-placement spawn — the mobile app always, the web
form whenever the branch field was blank — started from a stale tree:
the operator's freshly pushed commit was fetched into origin/* but
invisible to the agent.
Operator spawns on a git root now default to a worktree on
agent/<name>, cut from origin/HEAD like any explicit worktree spawn, so
a new agent always starts at the remote's latest push and gets the
per-agent isolation the README promises. Schedule firings opt out
(auto_worktree=False): their continuity convention is a state file
living in the root tree across runs. The mise-init bootstrap and
non-git roots keep root placement unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
The Running / Waiting / Done cards now navigate to a new agent-list
screen pre-filtered to that bucket, using exactly the same grouping as
the counts so the number tapped is the number listed. The list shows
every agent row the API knows — an agent that hasn't dropped a
checkmark yet is visible the moment it spawns, with a status badge, its
age, and a live last-output line while it works. Rows open the agent
detail screen, whose back button now returns to wherever the detail was
opened from (fleet or the list).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
Settings → Manage → Activity lists every control command with status
filter chips, when/where it ran (project, agent, claiming worker or
'unclaimed'), and tap-to-expand result/error text; a Sweep CI button
enqueues the global poll. The list auto-refreshes every 5s while open,
so a login_start or spawn can be watched to completion — this was the
one web dashboard page missing from the phone, and exactly the view
needed to diagnose a stuck command.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
Phase 4 (1517e4d) deleted the tmux launch path and with it the only
caller of claude_config.ensure_onboarded, so agent working dirs -
every fresh worktree is a brand-new path - were never pre-trusted in
~/.claude.json. Headless 'claude -p' runs then wedge or refuse on the
workspace-trust dialog with nobody at a TTY to accept it.
Spawn and resume now mark onboarding complete and trust the working
dir right before launch, next to the settings/claude_gen
materialization. Resume matters independently: a cross-worker resume
can land in a container whose ~/.claude.json has never seen the dir.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
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