Commit Graph

73 Commits

Author SHA1 Message Date
Wyatt 59696d8e93 Merge pull request #29 from 0xWheatyz/claude/lightweight-harness-pi-xp4kl8 2026-08-12 14:59:47 -04:00
Claude 5d3414db3b Add CHANGELOG.md with release + deployment notes for the pi harness
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
2026-08-12 18:45:15 +00:00
Claude e5c8ebfd34 Add web_search / web_fetch tools for pi-harness agents
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
2026-08-12 18:41:41 +00:00
Claude 887576bf22 pi bridge: activate the full built-in tool set (grep/find/ls)
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
2026-08-12 18:27:17 +00:00
Claude a3a5c272a2 Add the pi harness: lightweight local-model agents with full gate parity
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
2026-08-12 18:21:35 +00:00
Wyatt 356fa276a2 Merge pull request #28 from 0xWheatyz/claude/tencentdb-agent-memory-hgqjsy 2026-08-05 13:06:34 -04:00
Claude 82183d9bca Stop tracking the built web UI; build it in the Docker image instead
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
2026-08-05 15:43:19 +00:00
Wyatt 810fb80217 Merge pull request #27 from 0xWheatyz/claude/tencentdb-agent-memory-hgqjsy 2026-08-05 11:33:11 -04:00
Claude dc80e7bc28 Add agent memory: linked note store, MCP server, recall hook, and graph UI
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
2026-08-04 13:49:47 +00:00
Wyatt 682ea20ffc Merge pull request #26 from 0xWheatyz/claude/local-model-tool-calling-pt0xcq 2026-07-30 13:35:04 -04:00
Claude 44632ea771 Extend the model picker to schedules
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
2026-07-29 18:54:31 +00:00
Claude 2d5c0e34d7 Add local model backends: per-spawn dropdown pointing claude at alternative endpoints
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
2026-07-29 18:36:16 +00:00
Wyatt c4e6ae0faa Merge pull request #25 from 0xWheatyz/claude/expo-go-mise-setup-c2rsyv 2026-07-23 13:46:49 -04:00
Claude 962a613cc6 Add the Expo Go dev-server task to .mise.toml
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCxHDoWDBHJ9GN3djVFkpw
2026-07-23 17:45:25 +00:00
Claude 4f74b8313f Revert "Add mise task to start the Expo Go dev server behind a reverse proxy"
This reverts commit 02e43f833f.
2026-07-23 17:45:13 +00:00
Claude 02e43f833f Add mise task to start the Expo Go dev server behind a reverse proxy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCxHDoWDBHJ9GN3djVFkpw
2026-07-23 17:42:42 +00:00
Wyatt 4ef524ab05 Merge pull request #24 from 0xWheatyz/claude/claude-management-page-y7tzmz 2026-07-23 12:36:03 -04:00
Claude 6a14823c26 Add install-from-prompt to the Skills tab
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
2026-07-23 15:06:22 +00:00
Claude 07d8c3aa19 Turn the Claude Login page into a full Claude management page
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
2026-07-23 13:32:41 +00:00
Wyatt 301a697e74 Merge pull request #23 from 0xWheatyz/claude/agent-spawn-git-pull-cf0abm
Fetch origin and cut agent branches from origin/HEAD at spawn
2026-07-23 06:58:35 -04:00
Claude 9ad13b2488 Gate agent completion on commits, pushes, and tests; capture real checkpoints
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
2026-07-23 06:49:39 +00:00
Claude f03b03ab5c Fetch origin and cut agent branches from origin/HEAD at spawn
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
2026-07-23 04:21:13 +00:00
Wyatt 4b1f35df98 Merge pull request #22 from 0xWheatyz/claude/webapp-multi-page-refactor-85v1od
Split the web UI into a page per section
2026-07-22 22:47:46 -04:00
0xWheatyz 28a262c2e8 Merge branch 'main' into claude/webapp-multi-page-refactor-85v1od
# Conflicts:
#	src/handler/api/static/404.html
#	src/handler/api/static/_next/static/QCIYKmybhnvk7okhoCnyi/_buildManifest.js
#	src/handler/api/static/_next/static/QCIYKmybhnvk7okhoCnyi/_ssgManifest.js
#	src/handler/api/static/_next/static/ga21jhjKhYsutf8F3vo_-/_buildManifest.js
#	src/handler/api/static/_next/static/ga21jhjKhYsutf8F3vo_-/_ssgManifest.js
#	src/handler/api/static/_next/static/zI_TsbchBb39diXA-dnfw/_buildManifest.js
#	src/handler/api/static/_next/static/zI_TsbchBb39diXA-dnfw/_ssgManifest.js
#	src/handler/api/static/index.html
#	src/handler/api/static/index.txt
2026-07-22 22:47:27 -04:00
Wyatt d66884bd21 Merge pull request #21 from 0xWheatyz/feat/headless-runner
fix(control,ui): close review findings - heartbeat starvation, resume…
2026-07-22 22:38:56 -04:00
Claude 5d8c62450c Split the web UI into a page per section
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
2026-07-22 19:34:33 +00:00
Wyatt c600ad481e Merge pull request #20 from 0xWheatyz/feat/headless-runner 2026-07-22 15:05:05 -04:00
0xWheatyz 478d178542 fix(control,ui): close review findings - heartbeat starvation, resume race, stale UI writes
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.
2026-07-21 23:41:36 -04:00
0xWheatyz 1517e4dca8 feat!: headless is the only runner - delete the tmux run path (phase 4)
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.
2026-07-21 23:20:39 -04:00
0xWheatyz 6c2e73d4ec feat(control,api,ui): worker liveness + run event stream (phase 3)
- worker: heartbeat every loop pass (workers registry); reaper pass
  every ~15s marks a silent worker's running runs crashed and flips
  agents stuck in 'working' to crashed (paused/blocked keep their
  still-accurate status). Idempotent via finish_run's running-guard;
  any surviving worker can reap; no auto-requeue (half-done runs may
  have pushed). Dead workers' registry rows are dropped once settled.
- api: GET /projects/{p}/agents/{name}/events - the persisted
  stream-json event log, oldest-first, cursor-paged by row id;
  AgentOut exposes session_id/worker_id
- frontend: Run events panel in the run detail (assistant text, tool
  chips, result footer with cost/turns, runner notices, raw lines),
  cursor-appended on the existing 5s poll; 'Crashed' filter + danger
  badge; crashed agents show their frozen last frame ('last output
  before crash'); static export regenerated

Suite 290 -> 296 green; next build clean.
2026-07-21 23:10:40 -04:00
0xWheatyz 650f376934 feat(control): flag-gated headless runner with cross-worker resume (phase 2)
Wires the phase-1 headless machinery behind runner=headless (default
stays tmux; legacy agents, session_id null, keep the tmux paths):

- spawn: branches tmux vs headless.launch; extracts _agent_env (shared
  with resume - a headless resume is a new process needing identity/
  credential env); headless spawns require a task (no idle-REPL mode),
  enforced at spawn and as a 400 in the API
- resume: headless path materializes the session archive from the DB
  onto whichever worker claimed the command, then claude -p --resume;
  falls back to a fresh session with DB-re-injected context (visible
  worker event) when no transcript survives anywhere; refuses while a
  run is live. Undeliverable resumes now raise -> command FAILED,
  fixing silent input loss on both runners
- kill: headless path flags cancel_requested; the owning supervisor
  SIGTERMs its own child (cross-worker safe)
- worker: stable per-container ids, DB-driven run slots (full workers
  skip claiming spawn/resume/mise_init, leaving them for less-loaded
  workers), credsync refresh in the main loop
- settings_gen: permissions block (defaultMode + allowlist) for
  headless runs - -p auto-denies anything that would prompt; hooks
  remain the hard gate
- credsync + migration 0009 (runtime_secrets): login publishes the
  Fernet-encrypted claude credential bundle; every worker materializes
  it (merge-safe for local trust state); login_submit pinned to the
  login_start worker via commands.target_worker

Suite 270 -> 290 green, including the cross-worker resume linchpin
(clean-HOME materialize + --resume against the fake binary).
2026-07-21 22:56:02 -04:00
0xWheatyz f3acc57015 feat(db,control): dormant headless-runner schema, stream parser, and fixtures (phase 1)
Groundwork for replacing tmux-TUI agent runs with worker-owned
'claude -p --output-format stream-json' subprocesses (Postgres as the
single source of truth; no shared files between workers):

- migration 0008: workers (heartbeat registry), agent_runs (one row per
  headless invocation), agent_events (persisted stream-json event log),
  session_archives (tar.gz'd claude session per agent for cross-worker
  --resume); agents gains session_id/worker_id, commands gains
  target_worker; 'crashed' joins the agent status vocabulary
- control/headless.py: munged-path helper, argv builders, tolerant
  stream parser, archive/materialize round-trip, RunSupervisor + launch
  (nothing calls it yet - runner config still defaults to tmux)
- repository: run/event/archive/worker accessors; claim_next_command
  learns target_worker pinning and type exclusion for full slots
- tests/fixtures/fake_claude.py: scripted stream-json stand-in binary
- scripts/validate_claude_headless.sh: manual real-binary validation
  checklist (resume-on-clean-HOME linchpin, hook behavior under -p)

No behavior change; suite 245 -> 270 green.
2026-07-21 22:41:46 -04:00
Wyatt 06079db821 Merge pull request #19 from 0xWheatyz/fix/worktree-branch-creation
fix(control): create worktree branch when it does not exist
2026-07-21 17:15:55 -04:00
0xWheatyz 40390c0568 fix(control): create worktree branch when it does not exist
`git worktree add <path> <branch>` only checks out an existing ref, so
spawning an agent on a fresh feature branch failed with `fatal: invalid
reference` (exit 128) — the branch spawn asks for never exists yet, since
spawn starts from the remote's latest state.

- Detect whether the branch exists; use `git worktree add -b` to create it
  when it doesn't, and a plain checkout (git DWIMs remote tracking) when it does.
- Slugify the agent name for the worktree directory so free-form names (a PR
  title) no longer put spaces/colons/parens in repo-root paths.
- Surface git's stderr via a WorktreeError instead of a bare "exit status 128";
  spawn re-raises it as SpawnError.

Adds tests/test_worktree.py covering new-branch creation, existing-branch
checkout, slugification, the isolation guard, and the clear-error path.
2026-07-21 17:09:03 -04:00
Wyatt cab1f3148e Merge pull request #18 from 0xWheatyz/claude/handler-mobile-app-design-w1dqny
Add Handler mobile app (React Native/Expo) + Leeworks design system
2026-07-21 16:52:25 -04:00
0xWheatyz c0342a9796 feat(app): replace mock data with live Handler API
Wire the mobile app to the real Handler API instead of the transcribed
prototype data:

- src/api/client.ts: typed client ported from frontend/lib/api.ts (bearer
  auth, AuthError/ApiError, base URL param, allow401 for admin-only resume)
- src/api/format.ts: relative-time + status label/tone/color helpers
- src/state/ServerConfig.tsx: endpoint+token persisted to AsyncStorage
- src/screens/ConnectScreen.tsx: first-open config, validated via
  /health then /projects
- src/state/AppState.tsx: data-driven store polling projects -> agents ->
  checkmarks -> logs every 10s with per-item failure isolation; derives
  fleet counts, waiting list, recent checkmarks, merged log; answer+resume,
  spawn, kill mutations; 401 routes back to ConnectScreen
- screens render live data; detail meta is Started/Status/Tests/Build;
  Pause removed (no endpoint); Kill confirms; log filters are per-project
- delete src/data/mock.ts

tsc clean; Hermes bundle builds (200).
2026-07-21 15:16:56 -04:00
0xWheatyz 678a16605d chore(app): upgrade Expo SDK 52->54, add AsyncStorage
Bumps expo ^54, react-native 0.81.5, react 19.1, @types/react 19.1,
typescript 5.9, and adds babel-preset-expo (no longer transitive in SDK
54) plus @react-native-async-storage/async-storage for persisted server
config. expo-doctor 18/18, tsc clean, Hermes bundle builds.
2026-07-21 15:16:48 -04:00
Claude c49cdda462 ci(mobile): add EAS-based release workflow for the mobile app
- .github/workflows/mobile-release.yml: validate (npm ci + tsc) on every
  run with no secrets, plus an EAS Build job gated on workflow_dispatch or
  a `mobile-v*` tag. Requires only the EXPO_TOKEN secret.
- app/eas.json: development / preview (iOS simulator, no Apple account) /
  production (signed, EAS-managed Apple credentials) build profiles.
2026-07-21 13:20:20 +00:00
Claude da66bf7f2e Merge Handler mobile app (React Native + Expo) design + implementation
Brings in the mobile-app work from the design handoff session as a
sub-project alongside the backend:

- app/     React Native + Expo iOS implementation (components, screens,
           state, theme tokens)
- chats/   design conversation transcript(s)
- project/ Claude Design export (HTML prototype + design-system tokens)

Kept the backend's top-level README.md; the design-handoff instructions
are preserved at project/HANDOFF.md.
2026-07-20 19:31:14 +00:00
Claude daecaa7e6b Implement Handler mobile app (React Native + Expo iOS)
Build the committed 2a interactive prototype from Handler Mobile.dc.html as
a real Expo app: six wired screens (Fleet, Agent detail, Answer, Spawn, Log,
Settings) with the exact state logic ported from the design's renderVals().

- Port Leeworks tokens (colors light/dark, typography, spacing, radii,
  shadows) to typed RN values in src/theme.
- Reimplement the design-system components used by the screens (Button,
  Badge, Icon, Switch, Select, TextField, segmented control, chip, tab bar).
- Shared store mirrors the prototype's single-screen navigation and the
  Waiting -> Running flip when agt-7a1d is answered.
- Real OS status bar / home indicator (safe-area insets) replace the mock
  phone chrome; dark mode follows system appearance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdF2hHVQ4UUBFPtpeMQ81o
2026-07-20 18:48:30 +00:00
Claude Design f143959b52 Claude Design handoff: Handler mobile app design 2026-07-20 18:34:27 +00:00
0xWheatyz 04597a9ed6 ci(docker): publish sortable <timestamp>-<sha> tag for Flux image automation
Both image workflows now emit a <timestamp>-<sha> tag on main pushes
alongside the existing sha-<short>/latest tags. Flux's ImagePolicy needs a
sortable tag to pick the newest build; latest is mutable and sha-<short>
has no ordering, so neither could drive auto-deploy. Same scheme as the
Talos devbox image.

Tag is stamped at build time (UTC); re-running a stale build would sort
newest, so prefer reverting over re-running. api and control build via
separate workflows/policies and can skew if one fails.
2026-07-18 15:24:28 -04:00
Wyatt 8f6bfa3e61 Merge pull request #17 from 0xWheatyz/claude/mise-tooling-repo-init-attcr2
Add mise-init bootstrap agent for automatic tooling setup
2026-07-16 18:18:22 -04:00
Claude 24b8c44451 fix(mise-init): unblock the bootstrap agent + surface live agent output
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
2026-07-16 19:30:58 +00:00
Claude 43e1fa3619 fix(projects): cascade dependents when deleting a project or agent
Removing a repo returned 500. `delete_project` deleted only the projects
row, but `commands`, `agents`, `approvals`, and `schedules` all carry a
foreign key to `projects.id` with no ON DELETE CASCADE — and every real
project has at least the `sync` command queued at registration referencing
it, so Postgres rejected the delete with a ForeignKeyViolation
(commands_project_id_fkey). The existing tests only deleted dependent-free
projects, so it went unnoticed (and SQLite, though it has FK enforcement on
here, was never exercised with a referencing row).

delete_project now clears dependents in FK-safe order: agent-owned rows
(checkmarks before log_entries per the use_alter cycle, approvals authored
by those agents, and shared_context attribution nulled since it's a global
table), then schedules (which reference commands via last_command_id),
then the project-scoped approvals/commands/agents, then the project.

delete_agent had the same latent bug — a spawned agent always accrues a
checkmark + log entries via the hooks, which the log_entries/checkmarks FKs
would block — so it shares the same _purge_agent_dependents helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxKY28XKCM6o4ag3nmaVsZ
2026-07-16 19:05:52 +00:00
Claude 072f63bf2d feat(repos): add an "Initialize mise" option to the add-repo step
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
2026-07-16 18:51:24 +00:00
Wyatt 2ca93a2bc9 Merge pull request #10 from 0xWheatyz/claude/docker-executables-web-login-ybxp9p
fix(login): make the claude web-login flow actually work
2026-07-15 14:01:38 -04:00
Claude cd49dab5a3 feat(login): open the login URL in an OAuth-style popup, not an iframe
claude.com refuses to be embedded in an iframe (X-Frame-Options), so the inline
frame just showed a blocked page. Replace it with a small popup window, like a
"Sign in with Google" flow: the "Log in to Claude" click opens a blank popup
(within the user gesture, so it isn't popup-blocked) and, once login_start
returns the URL, the popup is navigated to it. Buttons to reopen the window or
open the URL in a new tab remain as fallbacks, and the popup is closed on
success/error. README updated to match. Rebuilt static export.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
2026-07-15 16:09:30 +00:00
Claude 1fe260ebe4 fix(login): submit via paste+separate Enter and navigate onboarding
Reproduced the failure against a real claude 2.1 in tmux. Two root causes,
both now fixed (the URL was never wrong — claude genuinely emits
`claude.com/cai/oauth/authorize`, so extraction was fine):

1. Submit race (the actual failure). `send_keys` sent the code and Enter
   together; for a long real code the Enter is processed before Ink commits the
   paste, so nothing submits — the session sits at "Paste code here > ****…",
   exactly what the activity log showed. Fix: deliver the code as a bracketed
   paste (tmux set-buffer/paste-buffer, new tmux.send_text), let it settle, then
   send Enter separately (tmux.send_enter). Verified end-to-end: the separate
   Enter submits and claude proceeds to the exchange.

2. Fragile onboarding. A fresh claude shows a theme picker, then the
   login-method menu, before any URL — the old blind /login+Enter+Enter only
   reached the menu by luck. Fix: start() now reads the pane each pass and reacts
   — accept theme/trust/continue prompts, pick the default subscription option on
   the login-method menu, and send /login once only when already onboarded at the
   REPL.

Also: confirm login by watching ~/.claude.json (where claude stores the account
on Linux) plus a success-text fallback, and fail fast on an "OAuth error /
Press Enter to retry" screen instead of waiting out the poll. Tests updated to
the real TUI screen text. Suite green (200).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
2026-07-15 15:34:43 +00:00
Claude f51a49fb98 fix(login): confirm via credentials file + validate the OAuth URL
Testing showed the wide-window fix captured the full URL, but login still
failed at submit ("login not confirmed"): the old check snapshotted the pane
once after 3s and only matched a few success strings, so an in-progress or
differently-worded exchange read as failure. Two hardening changes:

- login_submit now polls (up to 40s) and confirms by the authoritative signal —
  claude's credentials file changing on disk (any of the known locations /
  ~/.claude/*credential*) — with success-text and clean-exit as fallbacks.
- login_start captures with escape sequences (-e) and accepts only a *complete*
  OAuth URL (https:// + client_id + redirect_uri + state). This recovers the
  real href when claude renders the link as an OSC-8 hyperlink (whose visible
  text can be garbled, e.g. the "ttps://claude.com/cai/..." seen in testing) and
  refuses partial/garbled captures. On timeout the error now includes the actual
  last screen so a wrong menu/onboarding state is diagnosable.

tmux.capture_pane gains an `escapes` flag. Tests cover URL completeness,
OSC-8 href recovery, and credentials-file confirmation. Suite green (199).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
2026-07-13 19:37:20 +00:00