# Operations ## Layout - `cmd/byre`: `byre serve` runs the server; `byre run` drives one Claude session in the current directory from the terminal. - `api/openapi.yaml` is the API source of truth. `go generate ./internal/api` regenerates `internal/api/api.gen.go`, whose types are also the store's model types. - `internal/store`: SQLite (WAL). Every mutation and its event commit in one transaction; events are published to SSE subscribers in `seq` order. - `internal/server`: HTTP handlers, identity, SSE. - `internal/git`: worktree create and remove. - `internal/agents`: the agent session manager. It owns the running Claude processes and records their activity as events. - `internal/claude`: the Claude Code stream-json client (see `docs/claude-protocol.md`). - `internal/report`: the `report` MCP tool and the Stop hook that requires it each turn. - `web/`: the SPA, embedded into the binary. `byre serve --dev` serves `./web` from disk instead (run from the repo root). ## Data State lives in the `byre` directory under Go's `os.UserConfigDir()` (on macOS, `~/Library/Application Support/byre`): `byre.db` and `worktrees//`. Task branches are `byre/` in the project's repo. ## Running as a service The listen address is `listenAddr` in `cmd/byre/serve.go`; it binds loopback only. ``` go install ./cmd/byre cp deploy/run.fc.byre.plist ~/Library/LaunchAgents/ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/run.fc.byre.plist tailscale serve --bg --https= http:// ``` byre gets its own HTTPS port on the machine's `ts.net` name rather than a path under another service: the SPA and API use absolute paths, which break under a path prefix. `tailscale serve status` shows the URL and what else the machine serves. After rebuilding: `go install ./cmd/byre && launchctl kickstart -k gui/$(id -u)/run.fc.byre`. Logs go to `~/Library/Logs/byre.log`. Clients reach `tailscale serve` over HTTP/2; it proxies to byre over HTTP/1.1 and passes SSE through unbuffered. ## Identity `tailscale serve` adds `Tailscale-User-Login` and `Tailscale-User-Name` to proxied requests. The server accepts API requests only from loopback and only with both headers; anything else gets 401 or 403. The SPA's static files need no identity. Users are created on first request; each sets their commit identity with `PUT /api/me`. To call the API locally without Tailscale, send the headers yourself: ``` curl -H 'Tailscale-User-Login: you@example.com' -H 'Tailscale-User-Name: You' http:///api/me ``` ## Agent sessions `POST /api/tasks/{id}/sessions` starts a Claude session in the task's worktree and sends the prompt as its first turn; `POST /api/sessions/{id}/messages` starts later turns and `POST /api/sessions/{id}/interrupt` ends the current one. A task has at most one session that isn't `completed`. The session's commits use the git identity of the user who created it, so that user must have set one with `PUT /api/me`. byre chooses the Claude session ID (`vendor_session_id`) up front, so every session can be resumed. Processes start on demand: the first message after a restart resumes the session with `--resume`. Sessions are `running` from the moment a turn is sent until its `result` arrives; on startup, every session still marked `running` was cut off mid-turn, and byre resumes it and sends a message telling the agent to re-check the working tree. Stopping the server kills all agent processes. Completing a task kills its sessions and marks them `completed`. Agent activity becomes events with `agent_session_id` set: `user.message`, `agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`, `agent.report`, `turn.ended`, and `session.state`. Anything else Claude Code emits (rate-limit status, task progress, state changes) is recorded unchanged as `agent.vendor`. ## Inbox Agents ask questions with the `ask_human` MCP tool; a malformed call (no `why_blocked`, fewer than two options, a recommendation that isn't one of the options, an unknown attachment kind, a foreign `thread_id`) is rejected with a message saying what to fix, and nothing is filed. Each `report` call and each agent error (a turn that ends in error without a human interrupt, a process that dies mid-turn) also files an item. `GET /api/inbox` is one query over open, unsnoozed items, sorted by priority (`blocked`, `gate`, `review`, `fyi`) then age; `project_id` and `task_id` narrow it to a project or task view, and `state` and `include_snoozed` widen it. Blocking questions and errors are `blocked`; reports and non-blocking questions are `fyi`. `POST /api/inbox/responses` answers any number of items in one submission. Response types and what they do to the item: | type | allowed on | item becomes | |---|---|---| | `option` (with optional note in `text`) | questions | `answered` | | `free_form` | questions | `answered` | | `question` (a follow-up from the human) | questions | stays `open` | | `redirect` | any | `redirected` | | `snooze` | any | stays `open`, hidden until `snooze_until` | | `dismiss` | reports, errors | `dismissed` | Answers reach the agent as one message, listing each answer under the question or report it responds to, once the session is idle and none of its blocking questions are still waiting for the human. A follow-up question tells the agent to reply with `ask_human` using the item's `thread_id`; the new item supersedes the open one in that thread. Every item change is an `inbox.created` or `inbox.updated` event carrying the whole item, including who resolved it. ## Events `GET /api/events` is server-sent events: `id` is the event's `seq`, `data` the Event JSON. Reconnecting with `Last-Event-ID` (EventSource does this itself) or `?after=` replays everything after that seq before going live. A client that falls further behind than its subscriber buffer (`subscribe` in `internal/server/hub.go`) is disconnected and catches up by reconnecting.