Commit Graph

62 Commits

Author SHA1 Message Date
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
Claude 882a071521 fix(login): capture the full claude auth URL on a wide tmux window
Testing the web login surfaced a truncated authorization URL
(…client_id=9d1c250a-e61b-44d9-88) and a "missing redirect_uri" error: the
login session ran at the default 80 columns, so claude clipped the long URL and
capture-pane read it back cut off.

- Launch the login session with a very wide, tall window (500x50) via new
  optional width/height on tmux.new_session, so claude prints the URL on one
  unclipped line.
- Harden URL extraction to stop at box-drawing glyphs (U+2500–U+257F) in case
  the TUI renders the link flush against a border.

Tests: assert the wide window is requested, and that extraction keeps a full
redirect_uri/PKCE URL intact and strips a trailing box border. Suite green (197).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
2026-07-13 19:19:19 +00:00
Wyatt 32327a18c4 Merge pull request #9 from 0xWheatyz/claude/docker-executables-web-login-ybxp9p 2026-07-13 14:13:24 -04:00
Claude 1a3e832ba9 fix(docker): pin mise apt arch via dpkg, not TARGETARCH default
The arm64 control image build failed installing mise: it pulled the amd64
.deb (dpkg "architecture (amd64) does not match system (arm64)"). Cause: the
`ARG TARGETARCH=amd64` default overrode the per-platform value buildx injects,
so the arm64 stage wrote an amd64 apt source. Derive the arch from
`dpkg --print-architecture` (the image's own arch) instead — correct on both
platforms and independent of buildx arg wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
2026-07-13 18:00:58 +00:00
Claude e96bf25165 fix(docker): build forge with Go 1.26+ (golang:1 base)
The control image build failed: forge v0.6.0 requires Go >= 1.26 but the
forge-builder stage pinned golang:1.22 (GOTOOLCHAIN=local, so no auto-fetch).
Track the latest stable Go via golang:1-bookworm and set GOTOOLCHAIN=auto so a
future forge release needing a newer toolchain resolves itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
2026-07-13 17:51:44 +00:00
Claude fa2e97130d feat: bundle agent executables + web-driven claude login
Two changes so an operator can stand up and authenticate Handler entirely
from the browser, with a self-contained control image.

Bundle executables in the control image (Dockerfile.control)
- Node.js (NodeSource) + the Claude Code CLI, mise (official apt repo), and
  forge (git-pkgs/forge, built in a Go stage) join the existing git/tmux/ssh.
  No more bring-your-own binaries: live agent spawning, the verification gate,
  CI resolution, and the login flow all work out of the box. Installed under
  /usr so the /var/lib/handler VOLUME never masks them; mise apt source pinned
  to $TARGETARCH for the multi-arch (amd64/arm64) build.

Claude login from the web UI
- New login_start / login_submit command types (migration 0005) drive the
  interactive `claude /login` through the same enqueue→worker handoff every
  other control action uses — the API container has no claude binary.
- control/login.py opens `claude` in a dedicated tmux session, sends /login,
  selects the subscription account, and scrapes the claude.com authorization
  URL (tmux.capture_pane, -pJ so a wrapped URL rejoins); a second command feeds
  back the pasted code. Fully mockable via the tmux seam.
- API: POST /login/start, POST /login/submit (admin-gated).
- Dashboard: a "Claude Login" pane — a button that starts the flow, embeds the
  URL in an iframe (with a new-tab fallback, since claude.com may refuse
  framing), and takes the code to finish.

Also un-ignores frontend/lib/ (a broad Python `lib/` rule was swallowing the
UI's own api client + formatters, breaking rebuilds from a fresh clone) and
reconstructs those two source files; rebuilt static export committed.

Tests: control/login unit tests (tmux faked), worker dispatch, and API route
tests. Full suite green (195 tests), ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
2026-07-13 17:45:58 +00:00
Wyatt 5c68c8d47b Merge pull request #8 from 0xWheatyz/claude/git-server-agent-scheduling-f4bfq7
Git servers own their credentials: tokens, SSH keys, and schedules
2026-07-10 15:23:01 -04:00
Claude 71a7550f48 feat: git servers own credentials, one-line project adds with auto-clone, and scheduled agents
Git servers (forge_hosts) become full credential owners:
- an encrypted forge token (Fernet, HANDLER_SECRET_KEY) stored per server and
  never returned by the API (has_token flag only); used automatically by every
  project on that host and addressable as db:host:<hostname> — the reserved
  db: credential scheme is now live
- a per-server ed25519 SSH deploy key: generated server-side, public half
  shown in the dashboard to paste into the forge, private half encrypted at
  rest and materialized 0600 only in the control container (GIT_SSH_COMMAND /
  core.sshCommand)

Project registration gets a git-server mode: pick a registered server, type
owner/name, and the API derives the remote (ssh when the server has a deploy
key, https otherwise), computes root_dir under PROJECTS_ROOT, and enqueues a
new 'sync' command the worker executes (clone, or ff-only pull). Spawn always
pulls first, so runs start from the remote's latest state; POST /projects/:p/sync
and 'handler sync' re-pull on demand.

Schedules: recurring agent spawns (prefix, prompt, interval, role). The worker
fires due schedules as ordinary queued spawn commands with timestamped agent
names, so runs are fresh stateless agents and appear in the Activity audit
trail; missed intervals collapse into one catch-up run.

Dashboard: Git Servers pane shows the SSH public key (copy button) and takes a
write-only token; Repositories gains the server-first add form and a Pull now
button; new Schedules pane. Rebuilt static export. Also restores the missing
frontend/lib (api client + format helpers) the components import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XY1tEhQZXHZ5wci7dLc7rM
2026-07-10 19:17:12 +00:00
Wyatt 7399315185 Replace Alpine frontend with Next.js Claude Activity Dashboard (#7)
Rebuild the bundled web UI as a Next.js (React + TypeScript) static export
implementing the Claude Activity Dashboard design: a left-nav "Control Center"
hub over Runs, Repositories, Agents, Approvals, Git Servers, Activity, and
Shared, styled with the Leeworks design-system tokens (flat, dark, border-led).

The dashboard is a pure client of the existing API (same contract as curl):
the browser prompts for the token once, stores it in localStorage, attaches it
to every call, and renders all API values as React text so agent-authored
strings stay inert. Control actions enqueue a command and poll it to a terminal
state, matching the worker model.

The build output is committed to src/handler/api/static/ so the wheel ships it
and FastAPI serves it same-origin. app.py now mounts the export at "/" after the
API routers (a non-shadowing fallback: unmatched paths 404, no SPA rewrite).
UI-serving tests updated for the export; frontend source lives in frontend/.


Claude-Session: https://claude.ai/code/session_01ATgVWRjFzG8nHEnwgZpJWD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 14:34:43 -04:00
Wyatt 6668d14c34 Merge pull request #6 from 0xWheatyz/claude/handler-docker-container-u3qauh 2026-07-10 12:49:40 -04:00
Claude 4f05d09c2b feat(web): fully web-managed control plane via a DB command queue
Make credentials/hosts, projects, agents, and approvals manageable from the
dashboard. The API and control layer are separate containers, so the API can't
run control actions directly (no git/tmux/claude, doesn't own the tmux
sessions). Instead the API enqueues a command and a worker in the control
container executes it and writes the result back.

Data model (migration 0003):
- `commands` queue/audit table; `forge_hosts` registry; `approvals` gains a
  nullable approver id + `actor` so operator verdicts are first-class.

Control worker:
- `control/worker.py` claims commands and dispatches to the existing control
  functions (spawn/kill/resume/record_approval/write_skills/poller.sweep),
  plus a periodic CI sweep. New `handler worker` CLI subcommand; it becomes the
  control image's default command (subsumes `poll-ci --watch`).

API:
- `require_admin` gate + `ADMIN_TOKEN`; project GET/PATCH/DELETE; agent
  spawn/kill/delete; resume now enqueues (fixes a cross-container bug where the
  API tried to send tmux keys to a session in the control container); new
  approvals/commands/hosts routes; forge-init and poll-ci enqueue endpoints.

Credentials/hosts:
- host->token-env lookup consults the `forge_hosts` registry first (built-in
  map is the fallback); `resolve()` refactored to a scheme dispatch reserving
  `db:` for a future encrypted store. Web input restricts credential_ref to
  env:/file:/db: (cmd: stays CLI-only — it would run arbitrary commands).

Dashboard:
- New tabs for projects, agents (spawn/kill with live command-status polling),
  approvals, hosts, and an activity/audit view; shared context is now writable.

Tests: +33 (queue atomicity, worker dispatch, CRUD, hosts, admin gating,
cmd: rejection, host-aware credentials, and an API->queue->worker->spawn
end-to-end). README gains a Web management section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrhrBToauu4L2qG6jdnuFP
2026-07-10 16:32:45 +00:00
Wyatt 7bc31f9f76 Merge pull request #5 from 0xWheatyz/claude/handler-docker-container-u3qauh 2026-07-10 11:46:36 -04:00