Fresh box — the first GitHub sign-in becomes its owner.
You'll get a short code to type at github.com/login/device
diff --git a/web/app/views/status/_daemon_health.html.erb b/web/app/views/status/_daemon_health.html.erb
new file mode 100644
index 000000000..a701278ed
--- /dev/null
+++ b/web/app/views/status/_daemon_health.html.erb
@@ -0,0 +1,28 @@
+<%# Daemon health card (U5/U6): running/version/consistency from the shared
+ DaemonHealth snapshot, plus a bounded Repair action that posts to the
+ lifecycle route (not the dispatch queue). %>
+<% daemon ||= @daemon %>
+
+
+
Daemon
+ <% if daemon[:running] %>
+ v<%= daemon[:binary_version] %>
+ <% end %>
+
+
+ <% if daemon[:running] %>
+
+ running
+ <% if daemon[:consistent] == false %>
+ binary/version drift detected — repair to match the CLI
+ <% end %>
+ <% else %>
+
+ not running
+ <% end %>
+
+ <% if !container_managed? && (!daemon[:running] || daemon[:consistent] == false) %>
+ <%= button_to "Repair daemon", daemon_repair_path, class: "btn btn-sm",
+ form_class: "inline-form", data: { turbo_submits_with: "Repairing…" } %>
+ <% end %>
+
diff --git a/web/app/views/status/index.html.erb b/web/app/views/status/index.html.erb
index 25bcdf793..6308337e6 100644
--- a/web/app/views/status/index.html.erb
+++ b/web/app/views/status/index.html.erb
@@ -26,6 +26,8 @@
+<%= render "status/daemon_health", daemon: @daemon %>
+
<%# data-turbo-permanent: a morph must never touch the composer — it holds
typed-but-unsent idea text and staged image attachments (Stimulus state
the server can't re-render). %>
diff --git a/web/config/routes.rb b/web/config/routes.rb
index 659576df3..61017cf34 100644
--- a/web/config/routes.rb
+++ b/web/config/routes.rb
@@ -3,6 +3,10 @@ Rails.application.routes.draw do
get "health" => "health#show"
get "up" => "rails/health#show", as: :rails_health_check
+ # Daemon health + bounded repair (U5). The repair is a lifecycle verb,
+ # not a workflow verb — it runs in-process, CSRF-protected + auth-gated.
+ post "daemon/repair" => "daemon#repair", as: :daemon_repair
+
# GitHub device-flow sign-in (RFC 8628). Start is a POST (it creates a
# GitHub device code); the wait page polls the grant.
# Development/test only: the auth seam Capybara logs in through (and dev
diff --git a/web/test/e2e/golden_path_e2e.rb b/web/test/e2e/golden_path_e2e.rb
index 5d6c83021..950354ed5 100644
--- a/web/test/e2e/golden_path_e2e.rb
+++ b/web/test/e2e/golden_path_e2e.rb
@@ -149,8 +149,47 @@ class GoldenPathE2E < ApplicationSystemTestCase
"the fake agent's commit must be a real commit in a real worktree"
end
+ test "local loopback mode: no login, idea auto-dispatches through the daemon" do
+ # U1 / R13 local path: loopback no-auth skips the GitHub owner gate, so
+ # the status grid renders directly (no claim flow) and the daemon still
+ # auto-dispatches a dropped idea — the web↔daemon shared source of truth
+ # the definition-of-done requires, exercised WITHOUT the Docker claim flow.
+ configure_loopback_no_auth!
+
+ visit "/"
+ assert_no_current_path "/login", "loopback no-auth must not redirect to /login"
+ assert_selector "#projects", wait: 15
+
+ # --- Sample idea -------------------------------------------------------
+ fill_in "New idea", with: "Local mode sample idea"
+ find(".composer select[name='project']").find("option[value='#{@project}']").select_option
+ click_button "Add idea"
+ assert_selector ".task-row", text: "Local mode sample idea", wait: 10
+
+ # No manual claim or login: the daemon drives the task forward on its own.
+ assert_selector ".stage-badge", text: "execute", wait: 90
+ assert_selector ".task-meta", text: "Ready to open PR", wait: 90
+ end
+
private
+ # Local loopback no-auth config (U1): the same block `hive setup` /
+ # `hive web install` persist. Unlike configure_owner! (Docker claimable
+ # box), this sets auth_mode=loopback + a loopback bind so require_login
+ # skips the owner gate entirely.
+ def configure_loopback_no_auth!
+ path = File.join(ENV["HIVE_HOME"], "config.yml")
+ data = File.exist?(path) ? YAML.safe_load_file(path) : {}
+ data ||= {}
+ data["web"] = {
+ "origin" => "http://127.0.0.1:4567",
+ "bind" => "127.0.0.1",
+ "auth_mode" => "loopback",
+ "github" => { "client_id" => "client" }
+ }
+ File.write(path, data.to_yaml)
+ end
+
# The status grid is Turbo-replaced while the daemon advances tasks. Read the
# slug from a single current-DOM query instead of retaining a Capybara element.
def task_slug_from_grid!(title, timeout: 10)
diff --git a/web/test/integration/auth_local_mode_test.rb b/web/test/integration/auth_local_mode_test.rb
new file mode 100644
index 000000000..977203f21
--- /dev/null
+++ b/web/test/integration/auth_local_mode_test.rb
@@ -0,0 +1,62 @@
+require "test_helper"
+
+# U1 — local loopback auth policy. When `web.auth_mode: loopback` is set (the
+# mode `hive setup` / `hive web install` write for the first-class local
+# install), the web tier skips the GitHub owner gate entirely: `/` renders
+# without a login. The non-loopback half of the policy is enforced in the CLI
+# (`Hive::Commands::Web#refuse_unsafe_public_bind`), so the Rails tier only
+# needs to prove the config-derived predicate agrees.
+class AuthLocalModeTest < ActionDispatch::IntegrationTest
+ def write_loopback_config!(bind: "127.0.0.1", auth_mode: "loopback", unsafe: false)
+ path = File.join(ENV["HIVE_HOME"], "config.yml")
+ data = File.exist?(path) ? YAML.safe_load_file(path) : {}
+ data ||= {}
+ data["web"] = {
+ "bind" => bind,
+ "origin" => "http://127.0.0.1:4567",
+ "auth_mode" => auth_mode,
+ "unsafe_public_no_auth" => unsafe,
+ "github" => { "client_id" => "client" }
+ }
+ File.write(path, data.to_yaml)
+ end
+
+ test "loopback no-auth renders the status grid without login" do
+ write_loopback_config!
+ get "/"
+ assert_response :success, "loopback no-auth mode must render / without redirecting to /login"
+ end
+
+ test "loopback no-auth does not create a session" do
+ write_loopback_config!
+ get "/"
+ assert_nil session[:github_login], "no-auth mode must not fabricate a login"
+ end
+
+ test "owner mode still redirects unauthenticated requests to login" do
+ write_loopback_config!(auth_mode: "owner")
+ get "/"
+ assert_redirected_to "/login", "owner mode must keep the existing auth gate"
+ end
+
+ test "login page reflects auth disabled in loopback mode" do
+ write_loopback_config!
+ get "/login"
+ assert_response :success
+ assert_match(/auth is disabled/i, response.body)
+ assert_select "form[action='/auth/github']", 0,
+ "the device-flow button must not render when auth is disabled locally"
+ end
+
+ test "config-derived predicate agrees with the CLI refusal guard" do
+ write_loopback_config!
+ cfg = Hive::Config.load_global_web
+ assert Hive::Config.local_web_no_auth?(cfg), "loopback config must resolve to no-auth"
+ refute Hive::Config.web_bind_loopback?("0.0.0.0"),
+ "a non-loopback bind under loopback mode is what the CLI refuses"
+
+ # The CLI guard refuses exactly when the app would otherwise run
+ # ownerless on a public interface.
+ assert Hive::Config.local_web_no_auth?(cfg) && !Hive::Config.web_bind_loopback?("0.0.0.0")
+ end
+end
diff --git a/web/test/integration/daemon_health_test.rb b/web/test/integration/daemon_health_test.rb
new file mode 100644
index 000000000..4f84a5b84
--- /dev/null
+++ b/web/test/integration/daemon_health_test.rb
@@ -0,0 +1,72 @@
+require "test_helper"
+require "hive/pid_file"
+
+class DaemonHealthTest < ActionDispatch::IntegrationTest
+ include Hive::PidFile
+
+ def pid_file
+ File.join(Hive::Paths.state_home, ".daemon.pid")
+ end
+
+ def write_live_pid!
+ FileUtils.mkdir_p(File.dirname(pid_file))
+ File.write(pid_file, pid_file_payload(Process.pid).to_yaml)
+ end
+
+ teardown do
+ DaemonController.repair_runner = nil
+ FileUtils.rm_f(pid_file)
+ end
+
+ test "deep health includes the binary-consistency fields" do
+ write_live_pid!
+ get "/health", params: { deep: "1" }
+ assert_response :success
+ daemon = response.parsed_body.dig("daemon")
+ assert_equal Process.pid, daemon.dig("pid")
+ assert daemon.key?("binary_version"), "deep health must carry the daemon binary version"
+ refute_nil daemon.dig("consistent"),
+ "deep health must carry the binary-consistency verdict (true/false)"
+ end
+
+ test "repair posts the lifecycle commands through the injected runner" do
+ sign_in!(login: "alice")
+ commands = nil
+ DaemonController.repair_runner = ->(cmds) { commands = cmds }
+
+ post "/daemon/repair"
+
+ assert_redirected_to "/"
+ assert_equal 2, commands.size
+ assert_includes commands, [ commands.first.first, "daemon", "install", "--force" ]
+ assert_includes commands, [ commands.first.first, "daemon", "start" ]
+ end
+
+ test "repair renders a typed error when the bounded subprocess fails" do
+ sign_in!(login: "alice")
+ DaemonController.repair_runner = ->(_cmds) { raise Hive::Error, "daemon repair failed: boom" }
+
+ post "/daemon/repair"
+
+ assert_response :unprocessable_entity
+ assert_match(/daemon repair failed/, response.body)
+ end
+
+ test "repair is a no-op under the container supervisor" do
+ sign_in!(login: "alice")
+ commands = nil
+ DaemonController.repair_runner = ->(cmds) { commands = cmds }
+
+ prev = ENV["HIVEBOX_SUPERVISOR_PID"]
+ ENV["HIVEBOX_SUPERVISOR_PID"] = Process.pid.to_s
+ begin
+ post "/daemon/repair"
+ ensure
+ prev.nil? ? ENV.delete("HIVEBOX_SUPERVISOR_PID") : ENV["HIVEBOX_SUPERVISOR_PID"] = prev
+ end
+
+ assert_redirected_to "/"
+ assert_nil commands, "container-managed daemons must not run local lifecycle verbs"
+ assert_match(/container supervisor/, flash[:notice])
+ end
+end
diff --git a/web/test/integration/local_mode_banner_test.rb b/web/test/integration/local_mode_banner_test.rb
new file mode 100644
index 000000000..d05491e14
--- /dev/null
+++ b/web/test/integration/local_mode_banner_test.rb
@@ -0,0 +1,37 @@
+require "test_helper"
+
+# U6 — the local-mode banner must render only in loopback no-auth mode and
+# never leak into the owner-gated (Docker/hivebox) surface.
+class LocalModeBannerTest < ActionDispatch::IntegrationTest
+ def write_web_config!(auth_mode:, owner: "alice")
+ path = File.join(ENV["HIVE_HOME"], "config.yml")
+ data = File.exist?(path) ? YAML.safe_load_file(path) : {}
+ data ||= {}
+ github = { "client_id" => "client" }
+ github["owner"] = owner unless owner.to_s.strip.empty?
+ data["web"] = {
+ "bind" => "127.0.0.1",
+ "origin" => "http://127.0.0.1:4567",
+ "auth_mode" => auth_mode,
+ "unsafe_public_no_auth" => false,
+ "github" => github
+ }
+ File.write(path, data.to_yaml)
+ end
+
+ test "loopback no-auth mode shows the local-mode badge" do
+ write_web_config!(auth_mode: "loopback", owner: "")
+ get "/"
+ assert_response :success
+ assert_match(/local mode — no auth/, response.body)
+ end
+
+ test "owner mode does not show the local-mode badge" do
+ write_web_config!(auth_mode: "owner")
+ sign_in!(login: "alice")
+ get "/"
+ assert_response :success
+ refute_match(/local mode — no auth/, response.body,
+ "the local-mode badge must be absent from the owner-gated surface")
+ end
+end
diff --git a/web/test/system/daemon_health_card_test.rb b/web/test/system/daemon_health_card_test.rb
new file mode 100644
index 000000000..c96df5510
--- /dev/null
+++ b/web/test/system/daemon_health_card_test.rb
@@ -0,0 +1,30 @@
+require "application_system_test_case"
+
+# U6 — the status page daemon health card renders the shared DaemonHealth
+# snapshot (running/version/consistent) and its Repair button posts the
+# bounded lifecycle repair. Runs under Capybara + Playwright (chromium) in CI.
+class DaemonHealthCardTest < ApplicationSystemTestCase
+ teardown do
+ DaemonController.repair_runner = nil
+ end
+
+ test "daemon card renders not-running and repairs through the bounded runner" do
+ sign_in!
+
+ # No daemon pidfile → the card shows "not running" + a Repair button.
+ visit "/"
+ assert_selector "#daemon-health", wait: 5
+ assert_selector "#daemon-health", text: "not running", wait: 5
+
+ commands = nil
+ DaemonController.repair_runner = ->(cmds) { commands = cmds }
+
+ click_button "Repair daemon"
+
+ assert_current_path "/", wait: 5
+ assert_text "Daemon repair completed", wait: 5
+ assert_equal 2, commands.size
+ assert_includes commands, [ commands.first.first, "daemon", "install", "--force" ]
+ assert_includes commands, [ commands.first.first, "daemon", "start" ]
+ end
+end
diff --git a/wiki/commands/setup.md b/wiki/commands/setup.md
new file mode 100644
index 000000000..ccd2ebfb0
--- /dev/null
+++ b/wiki/commands/setup.md
@@ -0,0 +1,59 @@
+---
+title: hive setup
+type: command
+source: lib/hive/commands/setup.rb, lib/hive/commands/setup/{deps,web_bundle,backend_prompt}.rb, lib/hive/daemon/consistency.rb
+created: 2026-08-13
+updated: 2026-08-13
+tags: [command, setup, local, web, daemon, deps]
+---
+
+**TLDR**: `hive setup` is the one-shot provisioning + validation of the
+first-class **local** (non-Docker) install surface (Linux systemd-user /
+macOS launchd). It wires the sub-pieces in dependency order — backend
+selection → dependency check → Hive-owned deps (qmd + web bundle) → daemon
+service → project enrollment → web service — each idempotent and
+fail-fast-with-fix-command. External agent CLIs (`gh` / `claude` / `codex`)
+are **diagnosed, never installed or authenticated**: a missing/unauthed CLI
+is reported `blocked` with the exact fix command and never gates the local
+web bring-up.
+
+## CLI
+
+`hive setup [--json] [--non-interactive]`. Steps, in order:
+
+1. **Backend prompt** — `Setup::BackendPrompt` persists which agents to
+ provision globally (`agents.selected`); non-TTY (or `--non-interactive`)
+ uses the `claude, codex` defaults.
+2. **Deps** — `Setup::Deps` verifies ruby (exact 3.4), git, tmux, gh (auth),
+ claude (≥ 2.1.118), codex (≥ 0.125.0), node/npm, sqlite3. External agent
+ CLIs are diagnose-only.
+3. **Bootstrap** — `qmd` (managed npm install into `
/qmd`) and the
+ web bundle (`Setup::WebBundle`: resolve env → managed → source, else fetch
+ `v`, `bundle install`, `db:prepare`, and record a
+ `.hive-web-version` marker).
+4. **Daemon** — `hive daemon install --force` + `hive daemon status --json`
+ (reads the `consistent` field from the U5 binary-consistency guard).
+5. **Enrollment** — `hive init .` (or `hive daemon enable ` for an
+ already-registered project).
+6. **Web service** — `hive web install` + a deep health probe at
+ `http://127.0.0.1:4567/health?deep=1`.
+
+With `--json` it emits a single `hive-setup.v1` document (`ok`, `backends`,
+`deps[]`, `steps[]`, `web_url`, `warnings[]`). Success = every step passed AND
+every non-blocked dependency is ok; blocked external CLIs are warnings, not
+failures. The loopback web UI then needs no GitHub login (`web.auth_mode:
+loopback`, see [[commands/web]]).
+
+## Tests
+
+- `test/unit/commands/setup/deps_test.rb` — version parsing, gh auth
+ status, exact fix commands, blocked-vs-missing classification.
+- `test/unit/commands/setup_test.rb` — orchestration ordering with injected
+ collaborators, non-TTY defaults, blocked-vs-hard-failure split, envelope.
+- `test/unit/commands/setup/web_bundle_test.rb` — resolution order, marker
+ read/write/mismatch, fetch-failure fix command, stale re-fetch.
+- `test/integration/setup_test.rb` — real subprocess probes against stub
+ `gh`/`claude`/`codex` on PATH (read-only, never modified).
+
+Backlinks: [[commands/web]], [[commands/daemon]], [[modules/config]],
+[[decisions]], [[operating]].
diff --git a/wiki/commands/web.md b/wiki/commands/web.md
index f6f253737..68f950483 100644
--- a/wiki/commands/web.md
+++ b/wiki/commands/web.md
@@ -9,7 +9,9 @@ tags: [command, web, hivebox, rails, turbo]
**TLDR**: `hive web` boots the hivebox web UI — a vanilla **Rails 8** app
(importmap, Turbo, Stimulus, propshaft, solid_cable) living in `web/` at the
-repo root, shipped in the Docker image at `/app/web`. The web tier adds no
+repo root, shipped in the Docker image at `/app/web` OR provisioned into the
+managed `~/.local/share/hive/web` dir by `hive setup` for the first-class
+**local** (non-Docker) mode. The web tier adds no
pipeline logic: status reads call `Hive::Commands::Status#json_payload` (via
`Hive::Web::StatusFeed`), gate approval calls `Hive::Commands::Approve`
in-process, task Drop calls `Hive::Commands::Drop` in-process, stage runs go
@@ -22,21 +24,35 @@ path with separate gates.
## CLI
-`hive web [--bind] [--port]` (defaults from the `web:` config block). The
-command locates the Rails app (`HIVEBOX_WEB_APP_DIR` override, else `web/`
-next to `lib/`), exports `SECRET_KEY_BASE` (derived from the same persisted
+`hive web [--bind] [--port]` (defaults from the `web:` config block) boots the
+server in the foreground; `hive web install|start|stop|status` manage a
+SEPARATE per-user service (systemd-user on Linux, launchd on macOS, via
+`Web::ServiceInstaller`) distinct from `hive daemon` — `status --json` emits
+`hive-web-status.v1` with `app_dir`/`bind`/`port`. The command locates the
+Rails app (`HIVEBOX_WEB_APP_DIR` override, else the managed
+`~/.local/share/hive/web` provisioned by `hive setup`, else `web/` next to
+`lib/`), exports `SECRET_KEY_BASE` (derived from the same persisted
`Hive::Web::SessionSecret` file as before — sessions survive container
recreation), `HIVEBOX_ORIGIN` (extra Action Cable origin allow; same-origin
host traffic is accepted without config), and
`HIVEBOX_STORAGE_DIR` (the solid-stack sqlite files, under
`Hive::Paths.state_home/web-storage` so they live on the `/data` mount), runs
-`bin/rails db:prepare`, then execs `bin/rails server`. Outside the container
-or a source checkout the command exits 1 with guidance — the gem itself does
-not package the Rails app (`test/unit/gemspec_test.rb` pins that).
+`bin/rails db:prepare`, then execs `bin/rails server`. Outside the container,
+a source checkout, or a provisioned managed dir the command exits 1 with
+`run hive setup` guidance — the gem itself does not package the Rails app
+(`test/unit/gemspec_test.rb` pins that).
## Auth
+Two auth modes, selected by `web.auth_mode`: **owner** (default) keeps the
GitHub **device flow** (RFC 8628, see [[decisions]] ADR-036), owner-only.
+**loopback** (written by `hive setup` / `hive web install` for local mode)
+skips the owner gate entirely on a loopback bind — `require_login` returns
+early, the login page shows an "auth disabled locally" notice, and the
+layout header shows a "local mode — no auth" badge. A non-loopback bind under
+`web.auth_mode: loopback` is REFUSED at `hive web` startup (exit 78) unless
+`web.unsafe_public_no_auth: true` is set, so an ownerless public box cannot
+come up by accident. Docker/hivebox stays on `owner` with `0.0.0.0`.
An ownerless box is CLAIMABLE: the first successful device-flow login writes
itself into `web.github.owner` (config-lock-guarded so concurrent first
logins race safely; the claim is logged loudly) — the install path has no
@@ -69,7 +85,12 @@ usable. The local dev/test seam is exempt only for tokenless local sessions.
replace/morph), composer (new idea with image attach: clipboard
paste AND upload button; images become `[imageN]` placeholders and land in
the task's `assets/` dir — `Commands::New`'s TUI contract), per-project
- task rows with stage badges and liveness dots. Live-updates over **Turbo
+ task rows with stage badges and liveness dots, and a daemon health card
+ (`#daemon-health`) rendering the shared `DaemonHealth` snapshot
+ (running / `binary_version` / `consistent`) with a `POST /daemon/repair`
+ button that runs `hive daemon install --force` + `hive daemon start` as a
+ bounded lifecycle subprocess (own process group + timeout; typed error
+ page). Live-updates over **Turbo
Streams**: `StatusBroadcaster` subscribes to `StatusFeed#each_snapshot`;
`StatusFeed` suppresses unchanged snapshots by comparing with only
`generated_at` and `age_seconds` removed while keeping `mtime` /
diff --git a/wiki/decisions.md b/wiki/decisions.md
index 371a107c2..951a07367 100644
--- a/wiki/decisions.md
+++ b/wiki/decisions.md
@@ -3,11 +3,26 @@ title: Architectural Decisions
type: decisions
source: code + author's local planning notes (not committed)
created: 2026-04-25
-updated: 2026-06-16
+updated: 2026-08-13
tags: [decisions, adr]
---
-**TLDR**: ADRs below were authored alongside implementation work. ADR-024 records both the PR-first workflow/stage renumbering and daemon autonomy; ADR-026 covers the Telegram bot mobile surface (subprocess caller for non-state-mutating verbs); ADR-027 records the diagnose-then-act surface for red status rows; ADR-029 records the 7-artifacts stage insertion; ADR-030 records the project-global Claude launch mode plus permission/model/effort follow-ups; **ADR-033 supersedes the subprocess-caller portion of ADR-026 for state-mutating verbs — the bot now writes file-backed dispatch requests that the daemon consumes, making the daemon the sole spawner of `hive run`-class children**; ADR-034 records Hive-owned fallback commits for successful fix-agent edits and pre-fix dirty-worktree snapshots; ADR-035 records hivebox's PTY agent-login relay for paste-back and operator-ward device flows, now also used for `gh auth login`, instead of provider-page proxying; ADR-036 records hivebox's switch to GitHub device-flow sign-in, including ownerless first-login claim (no callback URL, no client secret, no required config edit).
+**TLDR**: ADRs below were authored alongside implementation work. ADR-024 records both the PR-first workflow/stage renumbering and daemon autonomy; ADR-026 covers the Telegram bot mobile surface (subprocess caller for non-state-mutating verbs); ADR-027 records the diagnose-then-act surface for red status rows; ADR-029 records the 7-artifacts stage insertion; ADR-030 records the project-global Claude launch mode plus permission/model/effort follow-ups; **ADR-033 supersedes the subprocess-caller portion of ADR-026 for state-mutating verbs — the bot now writes file-backed dispatch requests that the daemon consumes, making the daemon the sole spawner of `hive run`-class children**; ADR-034 records Hive-owned fallback commits for successful fix-agent edits and pre-fix dirty-worktree snapshots; ADR-035 records hivebox's PTY agent-login relay for paste-back and operator-ward device flows, now also used for `gh auth login`, instead of provider-page proxying; ADR-036 records hivebox's switch to GitHub device-flow sign-in, including ownerless first-login claim (no callback URL, no client secret, no required config edit); ADR-038 records the first-class local (non-Docker) web install/run mode — loopback no-auth default plus web-bundle version pinning.
+
+## ADR-038: First-class local web install/run mode — loopback no-auth default + web-bundle version pinning
+
+**Status:** Active (introduced for the local hive web install/run mode, 2026-08-13).
+
+**Context:** The pieces for a non-Docker web UI already existed (`hive web` boots the Rails app; `hive daemon install` writes/enables a per-user unit; `Setup::BackendPrompt` was scaffolded), but nothing tied them together, nothing provisioned the web bundle on a plain `hive-cli` install, loopback auth was still gated by the GitHub owner flow, and nothing detected a daemon whose unit drifted to a stale binary. Docker/hivebox (ADR-036 owner flow, `0.0.0.0` bind) stays supported.
+
+**Decision:**
+
+1. **Local loopback auth policy.** `web.auth_mode` selects the model: `owner` (default) keeps the GitHub device-flow / owner gate; `loopback` skips the owner gate on a loopback bind (`127.0.0.0/8`, `::1`, `localhost`). A non-loopback bind under `loopback` is refused at `hive web` startup unless `web.unsafe_public_no_auth: true` is set. `owner` is the default so Docker semantics are byte-for-byte preserved; local no-auth is opt-in via `hive setup` / `hive web install`.
+2. **Web bundle version pinning.** The gem does not package the Rails app (ADR-037 stands). Local mode obtains the matching release (`v`), keeps only `web/`, `bundle install`s + `db:prepare`s it, and records a `.hive-web-version` marker so `hive setup` / `hive web` can warn (and re-fetch) when the managed bundle drifts from the installed gem.
+3. **Separate web service.** `hive web install|start|stop|status` manage a `Web::ServiceInstaller` service distinct from the daemon (R7 — never merged locally).
+4. **Daemon binary-consistency guard.** `hive daemon status --json` (and `/health?deep=1`) surface `binary` / `binary_version` / `consistent`, comparing the resolved CLI binary against the installed unit's ExecStart and the running process's `/proc//exe` (+ `--version`). Repair is `hive daemon install --force`.
+
+**Consequences:** `hive setup` is the one-shot local surface. External agent CLIs (`gh`/`claude`/`codex`) are diagnosed with exact fix commands, never installed/authenticated. Docker/hivebox is unaffected (`owner` default + smoke re-run). Windows stays out of scope.
## ADR-037: Hivebox web tier is a vanilla Rails 8 + Turbo app, replacing the Sinatra tier
diff --git a/wiki/gaps.md b/wiki/gaps.md
index 2d71cc615..1ce805a11 100644
--- a/wiki/gaps.md
+++ b/wiki/gaps.md
@@ -47,6 +47,17 @@ Latest refresh note (2026-06-16): the babysitter gh-hostname dry-run audit remai
## Open questions about the codebase
+### 2026-08-13 local web live-provider smoke (R-e2e)
+
+The local web install/run mode (ADR-038) ships a CLI-level e2e scenario
+(`test/e2e/scenarios/local_web_setup.yml`) plus unit/integration coverage, but
+the full live-provider acceptance — a real authenticated `hive setup` bringing
+up `http://127.0.0.1:4567` with a real daemon dispatching a task created in
+the TUI/web and back — is NOT exercised in CI (it needs authenticated
+`claude`/`gh` and a running web server). It remains a documented manual step:
+run `hive setup`, open the web UI, create a task in the TUI, and confirm it
+appears in the web UI and dispatches automatically (and vice versa).
+
### 2026-06-22 dependency-stacking placeholder branch investigation
Branch-creator inventory for the U1-U10 inversion dogfood found no separate
@@ -106,7 +117,7 @@ Residual audits of commits `6a6cf990`, `2d15e9ee`, and `5e8723fa` carried this b
33. **Finalize merged-PR recovery is unit/integration-pinned but not live-smoked.** The merged-error archive recovery change routes whitelisted `8-finalize` `ERROR reason=git_status_failed` / `reason=claude_launch_failed` rows to `Hive::Daemon::PrMergeWatcher`; when GitHub reports the PR as `MERGED`, the watcher dispatches `hive archive --recover-merged-error-reason `, and `Hive::Commands::StageAction` re-confirms the current marker reason plus `Hive::Gh.pr_state(pr_url) == "MERGED"` before moving the task to `9-done`. Commit `118ed2fd` also adds an earlier `Stages::Finalize.pr_already_merged?` short-circuit: if `pr.md` points at a PR that is already `MERGED`, finalize stamps `COMPLETE pr_url=... is_draft=false merged=true` and returns `finalize_already_merged` before auth, git status, body-refresh agent spawn, or `gh pr ready`. `test/unit/daemon/pr_merge_watcher_test.rb`, `test/unit/daemon/dispatcher_test.rb`, `test/unit/gh_test.rb`, `test/integration/run_stage_action_test.rb`, and `test/integration/run_finalize_test.rb` cover the archive command generation, routing, `pr_state` success/error parsing, accept/reject boundaries, GhError fall-through, and direct already-merged finalize completion. This refresh did not find an in-tree artifact showing either live path against GitHub: a daemon observing a red finalized row after a real merge and archiving it, or a normal `hive finalize` run seeing an out-of-band merged PR and surfacing the completed task through `hive status`/TUI/bot.
34. **Claude/tmux orphan-sweep server skip is unit-pinned but not post-fix parallel live-smoked.** Commit `024b29b0` changes `Hive::ClaudeLauncher.sweep_orphan_processes` from a blanket `pkill -f` to `pgrep` plus per-PID `TERM`, skipping matched `tmux` commands because the tmux server can retain the first session's full `new-session ... --add-dir ` argv. `test/unit/stages/brainstorm_tmux_sentinel_test.rb` covers the observed shape: one matched tmux server line plus one matched Claude line must kill only the Claude PID and log `skipped=1`. The 2026-06-11 refreshes did not find an in-tree artifact showing two real Claude/tmux-backed Hive tasks running in parallel after the fix, one finishing, and the sibling session surviving without `tmux_session_terminated`.
35. Hivebox web-tier residuals after the Rails rewrite (ADR-037): browser-level coverage of agents/telegram/repos pages beyond the pipeline system test (the Telegram page now has source-level integration coverage for its first-run setup guide, strict numeric chat-ID validation, and blank/@handle refusals, but no browser/Docker smoke; repos has source-level coverage for the first-run questionnaire, SSH-origin normalization, and non-directory clone-target refusal, but no live GitHub/Docker smoke; task-page red recovery now has source/Rails integration coverage and commit-message live verification, and oversized diff rendering is capped by source/Rails integration coverage, but no checked-in browser-system or Docker artifact); Action Cable behavior under many tabs; diff happy-path tests; cross-round brainstorm answer-numbering semantics (see dispatcher answer_questions); hoisting the action→verb map into the gem (duplicated in Dispatcher and bot NotificationBuilders). Commits `eb971b55`, `463fff29`, `0dea8aa6`, `d7ce55a9`, `70d60980`, `24c41980`, `b47f6627`, `9d0fc9ef`, `65e90ebe`, and `c0630426` add Playwright/system or Rails integration coverage for the task log tail's follow/pause/resume behavior, node-preserving log-frame morph reloads, artifact open-state preservation across pushed morphs, status-grid scroll plus composer draft preservation across a live broadcast, project-rail filtering with URL/composer sync, `+ Add project` routing, and re-application after a live broadcast, Telegram first-timer setup guide open-state/BotFather/userinfobot/three-step rendering and strict chat-ID validation, red-task diagnostic banner plus Retry route queueing, Q&A round replacement without permanent stale forms, finalize-first artifact ordering, chronological ordering for earlier stages, Artifacts-before-Log layout, sanitized markdown rendering, non-directory repo-target refusal, plain-vs-deep health, and bounded diff output. `StatusBroadcaster` is source/model-test pinned for self-healing after a raising broadcast, and commit `65e90ebe` moves the task-page refresh signal before the fallible grid render, but this refresh did not find a focused test or live artifact proving task pages still refresh when the projects partial itself raises. Commit `c52e4e83` styles artifact summaries as filename-tab chrome and rendered markdown as a bordered document panel, but this refresh found no screenshot or visual-regression artifact proving that distinction in a browser. Commit `279a9380` adds `web/script/record_box_demo.rb` for a staged real Rails + daemon + Playwright demo recording, and commit `c0630426` adds a real-resume helper path that reruns a stranded `3-plan` stage through the product CLI before resuming filming, but this refresh only source-inspected the recorder scripts; no checked-in `box-demo` artifact or local run evidence proves the recorder currently completes with Playwright and ffmpeg. Apart from commit `9d0fc9ef`'s live-verified stuck-review recovery note, this refresh also did not find an in-tree live Docker or long-running-agent artifact proving the same behavior against a deployed hivebox while real agents are appending logs/artifacts and status updates.
-36. **Root README/FAQ still mentions "why no built-in web UI".** The committed hivebox work touched packaging and OpenClaw/wiki docs, but the root README still points readers to a FAQ entry framed as "why no built-in web UI" and `docs/faq.md` still says a web UI would add another state surface before the file protocol is finished. This refresh did not edit user-facing README/FAQ content because the request was scoped to the LLM wiki.
+36. ~~**Root README/FAQ still mentions "why no built-in web UI".**~~ — closed 2026-08-13. The local hive web install/run mode (ADR-038) replaced the framing: `docs/faq.md` now explains the local-first web UI (with Docker/hivebox still supported), and `README.md` / `docs/getting-started.md` / `install.md` / `openclaw/skills/hive/SKILL.md` document `hive setup` and the `hive web install|start|stop|status` lifecycle plus the loopback no-auth model.
37. **Hivebox HTTPS-origin push path is source/integration-pinned but not live-Docker-smoked.** Commit `8be458bd` added `ReposController#normalize_origin!`, a Rails integration regression proving an existing `git@github.com:` origin is rewritten to `https://github.com/...`, and a Dockerfile system credential helper for `https://github.com` via `gh auth git-credential`. This refresh did not find an in-tree artifact showing the full Dockerized path after a real Agents-page `gh` login: register/clone a repo whose `gh` config prefers SSH, open a Hive PR, and observe `5-open-pr` push succeeding over the rewritten https origin.
38. **Hivebox Advanced Drop is source/unit/integration-pinned but not live-browser/Docker-smoked.** Commit `4a09cdb9` adds `POST /tasks/:project/:slug/drop`, `TasksController#drop`, `Hive::Web::Dispatcher#drop`, the Advanced Drop card, and tests proving the card is not a primary action, successful posts delete the task folder, and stale `from` stages return 422 without deletion. Existing `Commands::Drop` tests cover agent kill, folder/log/worktree/branch cleanup, draft-PR close, JSON/error contracts, and TUI Shift+X dispatch; commit `65e90ebe` pins the in-process return payload and the clarified `pr_closed` contract (`true` for no recorded PR, `false` only when a recorded PR could not be closed) so the web notice can stay honest. Commit `279a9380` bumps the current `hive-drop` schema to v2 while preserving v1 for pinned validators; commit `c0630426` fixes the copied v1 `$id`/title in `schemas/hive-drop.v2.json` and adds a schema-identity regression covering every exported schema file. This refresh did not find an in-tree artifact showing a real browser confirmation flow against a running hivebox instance or a Dockerized web drop that exercises full cleanup of an active worktree/branch/draft PR.
39. **3-plan terminal-error healer requeue is unit/integration-pinned but not live-smoked.** Commit `5f7ba051` changes `Hive::Daemon::StaleAgentHealer` so `3-plan` `ERROR reason=tmux_session_terminated` / `reason=agent_orphaned` clears also write a dispatch request for `hive plan --project --from 3-plan` (`requestor=healer`, `trigger=terminal_agent_loss`) and log `heal_requeued`. Commit `65e90ebe` adds the distinct `heal_requeue_failed` event when the marker clear succeeded but queue write failed, plus integration coverage proving a real status row feeds the healer and lands an allowlisted dispatch request in `Hive::Daemon::DispatchRequestQueue`. Commit `279a9380` broadens the `3-plan` requeue to every successful terminal `ERROR` clear, including elapsed `limits_reached` cooldown markers, because they leave the same markerless empty `plan.md`; `test/unit/daemon/stale_agent_healer_test.rb` pins the limits path. Commit `c0630426` bumps the dispatch-request schema to v2 so `requestor=healer` is part of the published queue contract, and queue/schema tests track the new const rather than hard-coded v1 fixtures. This refresh did not find an in-tree live artifact showing a daemon observing such a red `3-plan` row, writing the queue file, dispatching the queued rerun, and surfacing either a recovered `WAITING`/`COMPLETE` plan or a bounded red state after repeated real failures.
diff --git a/wiki/index.md b/wiki/index.md
index d59b93e36..4a50cb589 100644
--- a/wiki/index.md
+++ b/wiki/index.md
@@ -10,8 +10,8 @@ tags: [index, wiki]
**TLDR**: Catalog of the LLM-maintained wiki for `hive`.
-Page count: 84
-Updated: 2026-06-25
+Page count: 85
+Updated: 2026-08-13
Folder-as-agent workflow engine: a Ruby 3.4 / Thor CLI control plane where descriptor-backed workflows move task folders through filesystem stages, stage agents run via configurable AgentProfile CLIs (`claude` default, `codex`, `pi`), and `mv` between directories remains the approval primitive. The built-in `coding` workflow drives the nine-stage PR pipeline (`1-inbox` → `2-brainstorm` → `3-plan` → `4-execute` → `5-open-pr` → `6-review` → `7-artifacts` → `8-finalize` → `9-done`), while `content` and project-authored workflows share the same generic runner/status/action machinery. The public release surface is the `hive-cli` rubygem installed through Homebrew, AUR, or `install.sh`, with `hv` as the Apache Hive collision fallback entrypoint, plus the hivebox GHCR Docker image and one-command `hivecli.sh/box` shell / `hivecli.sh/box.ps1` PowerShell installers; `hive web`/hivebox, `hive init` workflow selection and normal-vs-patrol reviewer split, project-global Claude model/effort pins, `hive connect screenote` for OAuth-backed Screenote MCP uploads, `hive patrol` handoff into `6-review`, `hive babysit`, `hive bench submit` for hive-bench corpus submissions, `hive digest` for the daily shipped digest, and the single ClawHub `hive-cli` listing that installs the OpenClaw `/hive` skill are covered by dedicated command/module pages.
@@ -42,6 +42,7 @@ Folder-as-agent workflow engine: a Ruby 3.4 / Thor CLI control plane where descr
- [[commands/rebase-status]] — `wiki/commands/rebase-status.md`
- [[commands/run]] — `wiki/commands/run.md`
- [[commands/screenote]] — `wiki/commands/screenote.md`
+- [[commands/setup]] — `wiki/commands/setup.md`
- [[commands/stage_action]] — `wiki/commands/stage_action.md`
- [[commands/status]] — `wiki/commands/status.md`
- [[commands/tui]] — `wiki/commands/tui.md`
diff --git a/wiki/log.d/20260813T000000Z-local-hive-web-install.md b/wiki/log.d/20260813T000000Z-local-hive-web-install.md
new file mode 100644
index 000000000..49701f6b4
--- /dev/null
+++ b/wiki/log.d/20260813T000000Z-local-hive-web-install.md
@@ -0,0 +1,16 @@
+## [2026-08-13T00:00:00Z] feat — first-class local (non-Docker) Hive web install/run mode
+
+**Action:** Added a first-class local install/run mode for the Hive web UI alongside the existing Docker/hivebox path (which stays supported): loopback no-auth policy (`web.auth_mode`), a managed `hive web install|start|stop|status` lifecycle, a `hive setup` orchestration command, web-bundle provisioning pinned to the gem version, a daemon binary-consistency guard, and web daemon-health/repair surfaces.
+
+**Code:**
+- `lib/hive/config.rb` — `web.auth_mode` / `web.unsafe_public_no_auth` + `local_web_no_auth?` / `web_bind_loopback?` helpers.
+- `lib/hive/commands/web.rb` + `web/service_installer.rb` — lifecycle subcommands + a separate `hive-web` service (systemd/launchd).
+- `lib/hive/commands/setup.rb` + `setup/{deps,web_bundle}.rb` — one-shot provisioning; external agent CLIs are diagnose-only.
+- `lib/hive/daemon/consistency.rb` — binary/version drift probe surfaced in `hive daemon status --json` and `/health?deep=1`.
+- `web/` — loopback auth skip, local-mode banner, daemon health card + `POST /daemon/repair`.
+
+**Validation:**
+- `bundle exec rake test` (gem unit/integration; tmux/hv environmental failures are pre-existing and unchanged).
+- `web` job: `bundle exec rails test` (integration), new system test for CI.
+
+**Backlinks:** [[commands/setup]], [[commands/web]], [[commands/daemon]], [[decisions]] (ADR-038), [[operating]].
diff --git a/wiki/operating.md b/wiki/operating.md
index 2b24d46a7..5e07c70ee 100644
--- a/wiki/operating.md
+++ b/wiki/operating.md
@@ -3,15 +3,36 @@ title: Operating Hive
type: operating
source: README.md, bin/hv, install.sh, lib/hive/commands/daemon.rb, lib/hive/commands/babysit.rb, lib/hive/commands/bot.rb, examples/systemd/, examples/launchd/, openclaw/skills/hive/SKILL.md, openclaw/README.md
created: 2026-05-07
-updated: 2026-06-25
-tags: [operating, daemon, bot, systemd, launchd, install]
+updated: 2026-08-13
+tags: [operating, daemon, bot, systemd, launchd, install, web]
---
-**TLDR**: Day-2 guide for running the hive daemon, experimental PR babysitter, and Telegram bot.
+**TLDR**: Day-2 guide for running the hive daemon, experimental PR babysitter, Telegram bot, and the local web UI.
Covers install-time daemon autostart, per-project daemon/babysitter enrollment, bot token/allowlist setup,
autostart on macOS (launchd) and Linux (systemd), dry-run shakedowns,
log inspection, community support, and how to disable automation mid-flight.
+## Local web UI
+
+For a non-Docker machine, `hive setup` provisions the full local surface
+(Rails web bundle + daemon service + web service) and serves the UI at
+`http://127.0.0.1:4567`. On the loopback bind the UI needs no GitHub login
+(`web.auth_mode: loopback`); a non-loopback bind is refused unless
+`web.unsafe_public_no_auth: true`. The web service is managed SEPARATELY from
+the daemon:
+
+```bash
+hive setup # one-shot: deps → qmd → web bundle → daemon → enroll → web
+hive web install # write/enable the hive-web systemd/launchd unit
+hive web start # run the server detached (pidfile /.web.pid)
+hive web status --json # running/pid/uptime + service install state
+```
+
+`hive daemon status --json` reports `binary` / `binary_version` /
+`consistent`; a `consistent: false` means the unit or running daemon drifted
+from the CLI binary — repair with `hive daemon install --force`. Docker/hivebox
+stays the owner-gated `0.0.0.0` path and is unchanged.
+
## Worktree-first workflow
All new feature, bugfix, or refactor work on `hive` itself starts in