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
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
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
Agent runs are now always worker-owned 'claude -p' subprocesses; tmux
survives only for the interactive /login flow.
- deleted: worker.capture_agent_output/_pane_tail + the capture loop
arm (the empty-/log bug's home), spawn's tmux launch/_claude_command,
the tmux resume/kill branches (the silent-send-keys bug's home),
tmux.session_name/list_sessions, the CLI attach subcommand, the
'runner' setting
- spawn: task is now a hard requirement (headless has no idle REPL) -
enforced in spawn (SpawnError) and the API (400); onboarding seeding
dropped (-p skips the trust dialog)
- resume: single headless path; pre-headless agent rows (no session_id)
degrade to the context-re-injection fresh run
- settings_gen: permissions allowlist is always emitted
- credsync: change-triggered uploads key on .claude/.credentials.json
only (claude touches ~/.claude.json every run - keying on it would
ping-pong uploads between workers); logins still publish explicitly
- cli list: liveness from agent_runs in the DB, not tmux
- tests: spawn/kill/resume re-pointed at the fake_launch seam
(conftest); integration test now drives API -> worker -> real fake
claude subprocess -> events endpoint; README documents the headless
model + multi-worker deployment invariants
Suite 295 green; frontend unchanged since phase 3.
The mise-init agent wedged on launch and the UI reported it green. Three
distinct problems, fixed together:
1. Onboarding wedge (the proximate bug). A freshly-installed claude opens
interactive setup — theme picker, then a folder-trust prompt — before
the REPL. A detached tmux agent has no one to answer it, so it sat on
the theme picker forever while agents.status said 'working'. New
control.claude_config.ensure_onboarded() marks onboarding complete and
trusts the working dir in ~/.claude.json (merge-only, so the login
flow's oauthAccount survives); spawn() calls it before launching.
2. Config-name gate. control/mise.py only recognized `.mise.toml`, so a
repo shipping `mise.toml` (no dot) — or config under `.config/mise/` —
failed the [tasks.test] gate even when healthy. It now accepts the
filenames mise itself reads and scans them all for the test task.
3. "Done" != done (the design gap). A spawned agent's real state lives in
its tmux pane, but the socket is control-container-only, so the API
couldn't see it. The worker now snapshots each working agent's pane
tail (last ~40 lines) into two new agents columns (last_output,
output_at, migration 0007) on its existing poll loop; the API
serializes them and AgentsSection renders a live-output <pre> under
each running agent. A wedged agent now shows the theme picker instead
of a misleading green badge.
Tests: home-dir writes are isolated to tmp in conftest; added coverage for
claude_config seeding/merge, the mise filename set, the worker capture
(including dead-session skip), and the API serialization.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxKY28XKCM6o4ag3nmaVsZ
Some repos an operator wants to manage don't yet define the `.mise.toml`
`[tasks.test]` task the spawn gate hard-requires — a chicken-and-egg,
since you can't run an agent to author that file without it. This adds a
one-click bootstrap.
Ticking "Initialize mise" on the add step enqueues a `mise_init` command
after the clone. The worker launches a dedicated agent that detects the
repo's stack, writes a `.mise.toml` with a canonical `[tasks.test]` task,
and commits + pushes it. That agent runs with the test-task gate off
(creating the task is the point) and a `HANDLER_MISE_INIT` marker on, so
its hooks enforce a bootstrap contract instead of the normal test gate:
- Stop hook blocks the turn until `.mise.toml` defines `[tasks.test]` and
the change is committed (clean tree) and pushed (no commits ahead of an
upstream) — so claude cannot end before the work has actually landed.
- git-push hook lets the bootstrap push through, skipping the test/build
gate (there may be no working suite yet) so the file reaches the remote.
Backend: `mise_init` command type (+ migration 0006), a shared
`control.mise` helper for the test-task check, `spawn(require_tests=,
mise_init=)`, gitops `is_clean`/`ahead_count`, and `init_mise` on the
project-create API (only acts when a git remote exists to push to).
Frontend: the checkbox, plumbed through the store, following the launch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxKY28XKCM6o4ag3nmaVsZ
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