diff --git a/web/app/views/status/index.html.erb b/web/app/views/status/index.html.erb
index 25bcdf79..bf432389 100644
--- a/web/app/views/status/index.html.erb
+++ b/web/app/views/status/index.html.erb
@@ -9,6 +9,14 @@
<% end %>
<%= turbo_stream_from StatusBroadcaster::CHANNEL %>
+
+ Daemon: <%= @daemon_health.state %>
+ <%= @daemon_health.remediation %>
+ <% if local_no_auth? && @daemon_health.state != :match %>
+ <%= button_to "Repair daemon", daemon_repair_path, class: "btn btn-secondary btn-sm" %>
+ <% end %>
+
+
<%# TUI left-pane parity: the rail filters the grid client-side (buttons,
not links — a navigation would discard the permanent composer's typed
text). The controller wraps rail AND grid; it re-applies the filter
diff --git a/web/config/database.yml b/web/config/database.yml
index d1c0e8fd..20d11f48 100644
--- a/web/config/database.yml
+++ b/web/config/database.yml
@@ -26,21 +26,21 @@ test:
#
# Similarly, if you deploy your application as a Docker container, you must
# ensure the database is located in a persisted volume.
-# Production sqlite files live under HIVEBOX_STORAGE_DIR (hive's state
+# Production sqlite files live under HIVE_WEB_STORAGE_DIR (hive's state
# home — the /data mount in the container) so image upgrades keep them.
production:
primary:
<<: *default
- database: <%= ENV.fetch("HIVEBOX_STORAGE_DIR", "storage") %>/production.sqlite3
+ database: <%= ENV.fetch("HIVE_WEB_STORAGE_DIR", ENV.fetch("HIVEBOX_STORAGE_DIR", "storage")) %>/production.sqlite3
cache:
<<: *default
- database: <%= ENV.fetch("HIVEBOX_STORAGE_DIR", "storage") %>/production_cache.sqlite3
+ database: <%= ENV.fetch("HIVE_WEB_STORAGE_DIR", ENV.fetch("HIVEBOX_STORAGE_DIR", "storage")) %>/production_cache.sqlite3
migrations_paths: db/cache_migrate
queue:
<<: *default
- database: <%= ENV.fetch("HIVEBOX_STORAGE_DIR", "storage") %>/production_queue.sqlite3
+ database: <%= ENV.fetch("HIVE_WEB_STORAGE_DIR", ENV.fetch("HIVEBOX_STORAGE_DIR", "storage")) %>/production_queue.sqlite3
migrations_paths: db/queue_migrate
cable:
<<: *default
- database: <%= ENV.fetch("HIVEBOX_STORAGE_DIR", "storage") %>/production_cable.sqlite3
+ database: <%= ENV.fetch("HIVE_WEB_STORAGE_DIR", ENV.fetch("HIVEBOX_STORAGE_DIR", "storage")) %>/production_cable.sqlite3
migrations_paths: db/cable_migrate
diff --git a/web/config/environments/production.rb b/web/config/environments/production.rb
index b991b661..c4ae0f58 100644
--- a/web/config/environments/production.rb
+++ b/web/config/environments/production.rb
@@ -1,4 +1,5 @@
require "active_support/core_ext/integer/time"
+require "uri"
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -82,11 +83,22 @@ Rails.application.configure do
# Only use :id for inspections in production.
config.active_record.attributes_for_inspect = [ :id ]
- # Enable DNS rebinding protection and other `Host` header attacks.
- # config.hosts = [
- # "example.com", # Allow requests from example.com
- # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
- # ]
+ # Enable DNS rebinding protection and other `Host` header attacks. The CLI
+ # resolves a bind once before boot and exports it; never derive this policy
+ # from a request Host header. Docker starts Rails directly through the same
+ # command and retains its explicit GitHub-auth owner gate.
+ if (bind = ENV["HIVE_WEB_BIND"]).to_s != ""
+ hosts = [ bind ]
+ hosts << "localhost" if bind == "127.0.0.1" || bind == "::1" || bind == "localhost"
+ begin
+ origin_host = URI.parse(ENV.fetch("HIVE_WEB_ORIGIN", ENV.fetch("HIVEBOX_ORIGIN", ""))).host
+ hosts << origin_host if origin_host
+ rescue URI::InvalidURIError
+ # Config validation rejects invalid origins before web exec. This guard
+ # keeps an accidental environment override from crashing Rails boot.
+ end
+ config.hosts = hosts.uniq
+ end
#
# Skip DNS rebinding protection for the default health check endpoint.
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
@@ -94,10 +106,10 @@ Rails.application.configure do
# Turbo Streams connect over Action Cable. Same-origin-as-host covers the
# normal case with ZERO config — browse the box at any address and the
# Origin header matches the Host header (also true behind proxies that
- # forward Host). web.origin → HIVEBOX_ORIGIN remains as an explicit
+ # forward Host). web.origin → HIVE_WEB_ORIGIN remains as an explicit
# additional allow for exotic setups where the two genuinely differ;
# without same-origin, an unset origin silently dropped every live
# update on any non-localhost URL — a trap on the install path.
config.action_cable.allow_same_origin_as_host = true
- config.action_cable.allowed_request_origins = [ ENV["HIVEBOX_ORIGIN"] ].compact
+ config.action_cable.allowed_request_origins = [ ENV["HIVE_WEB_ORIGIN"] || ENV["HIVEBOX_ORIGIN"] ].compact
end
diff --git a/web/config/routes.rb b/web/config/routes.rb
index 659576df..230524d0 100644
--- a/web/config/routes.rb
+++ b/web/config/routes.rb
@@ -17,6 +17,8 @@ Rails.application.routes.draw do
root "status#index"
+ post "daemon/repair" => "daemon#repair", as: :daemon_repair
+
post "ideas" => "ideas#create", as: :ideas
# Task pages are addressed by project name + task slug, mirroring the CLI.
diff --git a/web/test/integration/daemon_repair_test.rb b/web/test/integration/daemon_repair_test.rb
new file mode 100644
index 00000000..d5d816c8
--- /dev/null
+++ b/web/test/integration/daemon_repair_test.rb
@@ -0,0 +1,19 @@
+require "test_helper"
+
+class DaemonRepairTest < ActionDispatch::IntegrationTest
+ around do |test|
+ previous = ENV["HIVE_WEB_AUTH"]
+ ENV["HIVE_WEB_AUTH"] = "none"
+ test.call
+ ensure
+ previous.nil? ? ENV.delete("HIVE_WEB_AUTH") : ENV["HIVE_WEB_AUTH"] = previous
+ end
+
+ test "status renders a daemon health card in local mode" do
+ get "/"
+
+ assert_response :success
+ assert_match(/Daemon: stopped/, response.body)
+ assert_select "form[action='/daemon/repair']", 1
+ end
+end
diff --git a/web/test/integration/health_test.rb b/web/test/integration/health_test.rb
index 12d53aee..079d517d 100644
--- a/web/test/integration/health_test.rb
+++ b/web/test/integration/health_test.rb
@@ -27,7 +27,13 @@ class HealthTest < ActionDispatch::IntegrationTest
# real start time passes the same liveness + ownership checks
# `hive daemon status` applies.
FileUtils.mkdir_p(File.dirname(pid_file))
- File.write(pid_file, pid_file_payload(Process.pid).to_yaml)
+ File.write(
+ pid_file,
+ pid_file_payload(
+ Process.pid,
+ identity: { "invoked_binary" => Hive::InvokedBinary.path, "hive_version" => Hive::VERSION }
+ ).to_yaml
+ )
get "/health", params: { deep: "1" }
assert_response :success
assert_equal Process.pid, response.parsed_body.dig("daemon", "pid")
diff --git a/web/test/integration/local_auth_test.rb b/web/test/integration/local_auth_test.rb
new file mode 100644
index 00000000..5559fd2a
--- /dev/null
+++ b/web/test/integration/local_auth_test.rb
@@ -0,0 +1,25 @@
+require "test_helper"
+
+class LocalAuthTest < ActionDispatch::IntegrationTest
+ around do |test|
+ previous = ENV["HIVE_WEB_AUTH"]
+ ENV["HIVE_WEB_AUTH"] = "none"
+ test.call
+ ensure
+ previous.nil? ? ENV.delete("HIVE_WEB_AUTH") : ENV["HIVE_WEB_AUTH"] = previous
+ end
+
+ test "loopback no-auth mode serves status without a GitHub session" do
+ get "/"
+
+ assert_response :success
+ assert_no_match(/Log out/, response.body)
+ assert_no_match(/Continue with GitHub/, response.body)
+ end
+
+ test "login endpoints do not create an owner session in no-auth mode" do
+ get "/login"
+
+ assert_redirected_to "/"
+ end
+end
diff --git a/wiki/architecture.md b/wiki/architecture.md
index 85ff8867..62f1e073 100644
--- a/wiki/architecture.md
+++ b/wiki/architecture.md
@@ -3,11 +3,11 @@ title: Architecture
type: architecture
source: lib/hive/, bin/hive, templates/
created: 2026-04-25
-updated: 2026-06-24
-tags: [architecture, overview]
+updated: 2026-07-18
+tags: [architecture, overview, web, local]
---
-**TLDR**: Hive is a Ruby 3.4 / Thor agent workflow engine over folder-backed state machines. The flagship `coding` workflow is the nine-stage idea-to-PR pipeline, while the built-in `content` workflow and project-authored descriptors run through the same generic workflow/data layer. The CLI dispatches into per-stage runners; stage agents run through configured AgentProfile CLIs inside per-task and per-project locks. Optional long-running surfaces sit beside the CLI: `hive daemon` advances safe tasks automatically, `hive tui` renders a terminal dashboard, `hive bot` turns human-input gates into Telegram interactions, and `hive web` provides the hivebox browser surface. Workflow state has no application database; durable task/project state is the filesystem plus global YAML config, while token-usage metrics use a small SQLite store.
+**TLDR**: Hive is a Ruby 3.4 / Thor agent workflow engine over folder-backed state machines. The flagship `coding` workflow is the nine-stage idea-to-PR pipeline, while the built-in `content` workflow and project-authored descriptors run through the same generic workflow/data layer. The CLI dispatches into per-stage runners; stage agents run through configured AgentProfile CLIs inside per-task and per-project locks. Optional long-running surfaces sit beside the CLI: `hive daemon` advances safe tasks automatically, `hive tui` renders a terminal dashboard, `hive bot` turns human-input gates into Telegram interactions, and `hive web` is a Rails adapter over the same state. `hive setup` makes that adapter first-class for an installed CLI by staging a writable versioned runtime under XDG data while keeping durable web state under XDG state; it starts distinct daemon and web services that use the invoking CLI's XDG snapshot. Hivebox Docker remains a separate `/data`-isolated, GitHub-owner-authenticated deployment. Workflow state has no application database; durable task/project state is the filesystem plus global YAML config, while token-usage metrics use a small SQLite store.
## Layer cake
@@ -241,7 +241,26 @@ sends the next question. The earlier "Codex draft-assist" flow — where
Path A spawned Codex to draft an answer with write-draft/edit/cancel
buttons — has been retired; see [[modules/bot]] and [[state-model]].
-## Hivebox web pipeline
+## Web control planes
+
+Native local mode and hivebox share the command/state adapters, not process
+ownership or storage roots. `hive setup ` provisions the packaged
+Rails source into `${XDG_DATA_HOME}/hive/web/`, with Bundler, assets,
+logs, and tmp files in that writable runtime and durable SQLite/session files
+under XDG state. The Rails app, TUI, daemon, and CLI therefore observe the
+operator's real registry and checked-out repositories. `hive-daemon` and
+`hive-web` are independent systemd-user/launchd units (or daemon detached plus
+foreground web fallback when Linux has no systemd user manager). A PID payload
+records the daemon wrapper/version, allowing the web status card to show
+`match`, `mismatch`, `unknown`, `stopped`, or `unmanaged` and repair a managed
+native daemon without touching the web process. Bind-aware auth is decided by
+the CLI before Rails starts: `auto` is authless only on literal loopback;
+public/unknown binds require GitHub auth unless the operator explicitly opts
+into `--unsafe-no-auth`.
+
+Hivebox remains intentionally different: it uses `HIVEBOX_WEB_APP_DIR`, its
+container supervisor, `/data`, and GitHub owner/device-flow auth. Native web
+repair is disabled there because that supervisor owns daemon restarts.
`hive web` serves a vanilla Rails 8 + Turbo app from `web/` (ADR-037; the
original Sinatra/Puma + SSE tier is gone). Auth is the GitHub device flow
diff --git a/wiki/commands/daemon.md b/wiki/commands/daemon.md
index c84cb08e..5c7c7ed5 100644
--- a/wiki/commands/daemon.md
+++ b/wiki/commands/daemon.md
@@ -3,7 +3,7 @@ title: hive daemon
type: command
source: lib/hive/commands/daemon.rb, lib/hive/daemon/*
created: 2026-05-06
-updated: 2026-06-18
+updated: 2026-07-18
tags: [command, daemon, automation, json]
---
@@ -40,7 +40,7 @@ hive daemon queue [list | show | prune] [--json]
|-----------|----------|
| `start` | Acquires the PID file (`~/Dev/hive/.daemon.pid`); without `--detach` runs in the foreground. With `--detach` calls `Process.daemon(true, true)` and the parent returns immediately. With `--dry-run` logs every dispatch decision but does NOT spawn child `hive ...` processes. Refuses with exit `75 (TEMPFAIL)` if a live daemon already holds the PID file. |
| `stop` | Sends `SIGTERM` to the running daemon's PID. Waits up to `daemon.shutdown_grace_sec` (default 600s) for the daemon to exit, then escalates to `SIGKILL`. Idempotent: `stop` with no PID file exits 0 with `daemon not running` on stderr; a stale PID file (process gone) is removed and the call exits 0. With `--json`, emits a `hive-daemon-stop` envelope (fields: `running`, `was_running`, `stale_pid?`, `reason?` — `pid_reused` / `unverified` for safety bailouts). |
-| `status` | Reports running / not running. Exit code 0 if running, 1 if not. With `--json`, emits a `hive-daemon-status` envelope with `running`, `pid`, `uptime_sec`, `pid_file`, `log_file`, plus the autostart-service state `service_installed`, `service_enabled`, and `unit_path` (read-only probe) so an agent can tell whether `hive daemon install` has run without a mutating call. |
+| `status` | Reports running / not running. Exit code 0 if running, 1 if not. With `--json`, emits `hive-daemon-status.v2`: alongside `running`, `pid`, `uptime_sec`, and service state, `daemon_identity` compares the live PID payload's invoked wrapper/version to the caller and reports `match`, `mismatch`, `unknown`, `stopped`, or `unmanaged` with repair text. v1 remains published for pinned readers. |
| `reload` | Sends `SIGHUP` to the running daemon's PID, which triggers config reload at the next tick boundary. In-flight children continue uninterrupted. Exit 1 if no daemon running. With `--json`, emits a `hive-daemon-reload` envelope (`ok`, `reason`, `pid`, `message`). |
| `tail` | `tail -F` semantics on `~/Dev/hive/logs/daemon.log` (self-implemented; doesn't shell out to the `tail` binary). Exit 1 if the log file doesn't exist. |
| `install` | (Re)writes the platform-native unit file (`~/.config/systemd/user/hive-daemon.service` on Linux, `~/Library/LaunchAgents/local.hive-daemon.plist` on macOS) and starts/enables the service. Installers and agent-assisted setup run this by default so daemon autostart is global install-time infrastructure, independent of any project. Without `--force`, refuses to overwrite a pre-existing unit (preserving operator hand-edits); exit `64` (USAGE) with a message pointing at `--force` so automation can branch without clobbering local changes. With `--force`, saves the previous content to a timestamped `.bak-YYYYMMDDTHHMMSSZ` (rotated, never overwritten) via atomic write, then — only when an existing unit was actually overwritten (the `upgraded` outcome) — restarts the running daemon on Linux / unloads-then-loads on macOS so new `Environment=` lines take effect (a first-time `--force` install with no prior unit just starts/enables, no restart). A service-manager failure (systemctl reload/enable, or launchctl load rejecting the unit) exits `70` (SOFTWARE). A host with no systemd-user manager at all is different: the unit is still written, but autostart cannot be enabled, so it exits `0` with the `unsupported` outcome (and `target_path` set to the written unit) — a known-platform limitation, not a failure. With `--json`, every outcome (success and error) emits a `hive-daemon-install.v1` envelope. Units point at the user-facing wrapper path when installers provide it, so bash/Homebrew installs preserve the GEM_HOME/GEM_PATH wrapper across login/reboot; `hv` invocations remain valid when Apache Hive shadows `hive`. Use this after upgrading hive when the unit template has changed or when autostart needs repair. |
diff --git a/wiki/commands/doctor.md b/wiki/commands/doctor.md
index 81cc8f8b..23c0fcfc 100644
--- a/wiki/commands/doctor.md
+++ b/wiki/commands/doctor.md
@@ -3,7 +3,7 @@ title: hive doctor
type: command
source: lib/hive/commands/doctor.rb, lib/hive/skill_check.rb
created: 2026-05-07
-updated: 2026-06-14
+updated: 2026-07-18
tags: [command, preflight, skills, tmux]
---
@@ -57,6 +57,16 @@ Encoded as the third return of `AgentProfile.new(skill_verifier:)`:
A new agent profile becomes "doctorable" by registering a `Hive::SkillCheck::*` module and passing its `.method(:verify)` into `AgentProfile.new(skill_verifier:)`.
+## Local setup readiness
+
+`Hive::Commands::Setup::Preflight` is the structured counterpart used by
+`hive setup`: it reports Ruby 3.4+, git, tmux, gh, Claude, Codex, Node/npm,
+qmd, SQLite, and the Rails runtime as rows carrying a path/version, ownership,
+severity, and a copyable remediation. It never installs or logs in external
+tools. Only qmd and the staged web runtime are Hive-owned repairs; qmd follows
+the same XDG prefix and `better-sqlite3` rebuild contract as `install.sh` and
+will not overwrite a user-owned `~/.local/bin/qmd`.
+
## JSON envelope (`hive-doctor.v1`)
```json
diff --git a/wiki/commands/setup.md b/wiki/commands/setup.md
new file mode 100644
index 00000000..1e8494d3
--- /dev/null
+++ b/wiki/commands/setup.md
@@ -0,0 +1,37 @@
+---
+title: hive setup
+type: command
+source: lib/hive/commands/setup.rb
+created: 2026-07-18
+tags: [command, setup, web, daemon, local]
+---
+
+**TLDR**: `hive setup [PROJECT_PATH]` is the resumable local-control-plane
+bootstrap. It defaults to the current directory, reports every phase, safely
+repairs Hive-owned qmd/Rails dependencies, initializes or re-enrolls the
+repository, enables daemon participation, starts separate native services, and
+prints the local web URL. It returns non-zero after its summary when an
+external prerequisite still needs operator action.
+
+## Phases
+
+1. Readiness rows for Ruby, git, tmux, agent CLIs, Node/npm, qmd, SQLite, and
+ the Rails runtime.
+2. Global backend selection using [[commands/doctor]]'s agent vocabulary.
+3. qmd and Rails runtime provisioning (the only automatic dependency repairs).
+4. Project initialization/re-enrollment and `daemon.enabled: true`.
+5. Daemon unit install/start, then a deterministic `127.0.0.1:4567` endpoint
+ check before web unit install/start.
+6. Bounded `/health?deep=1` readiness wait.
+
+An already healthy Hive web endpoint is accepted as an idempotent success. An
+unrelated listener on the configured port is never killed or moved aside; setup
+reports the exact `web.port` / `hive web --port PORT` remedy. A customized
+native unit is preserved and reports the `--force` repair command. On Linux
+without systemd-user, setup starts the daemon detached, prints its readiness
+summary/URL, then hands off to foreground `hive web`; that usable fallback is
+explicitly not reboot-persistent.
+
+## Backlinks
+
+- [[commands/web]] · [[commands/daemon]] · [[commands/doctor]]
diff --git a/wiki/commands/web.md b/wiki/commands/web.md
index f6f25373..29a06e3a 100644
--- a/wiki/commands/web.md
+++ b/wiki/commands/web.md
@@ -3,13 +3,15 @@ title: hive web
type: command
source: lib/hive/commands/web.rb, lib/hive/web/, web/, packaging/docker/, .github/workflows/release.yml
created: 2026-06-04
-updated: 2026-06-25
+updated: 2026-07-18
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
+**TLDR**: `hive web` boots Hive's vanilla **Rails 8** app (importmap, Turbo,
+Stimulus, propshaft, solid_cable). `hive setup` stages the packaged app into a
+writable, versioned XDG-data runtime before it is launched from an installed
+gem; Docker continues to use `/app/web` and `/data` through its
+`HIVEBOX_WEB_APP_DIR` override. 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
@@ -23,19 +25,39 @@ 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
+command locates the Rails app in this order: Docker-compatible
+`HIVEBOX_WEB_APP_DIR`, neutral `HIVE_WEB_APP_DIR`, a matching provisioned XDG
+runtime, then a source checkout. If an installed runtime is missing, it exits
+with the exact repair command `hive setup`; it never performs a network bundle
+install while starting a service. It 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
+recreation), `HIVE_WEB_ORIGIN` (with `HIVEBOX_ORIGIN` retained as a Docker alias; extra Action Cable origin allow; same-origin
host traffic is accepted without config), and
-`HIVEBOX_STORAGE_DIR` (the solid-stack sqlite files, under
+`HIVE_WEB_STORAGE_DIR` (with `HIVEBOX_STORAGE_DIR` retained as an alias; 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`.
+
+`hive web install [--force]` writes the separate native `hive-web` unit
+(systemd-user or launchd) without starting it. `hive web start` daemon-reloads
+then enables/starts that unit (or loads the launchd plist); it never touches
+the distinct `hive-daemon` unit. A differing existing unit is preserved unless
+`--force` is specified, which creates a timestamped backup first. Linux hosts
+without systemd-user keep the foreground `hive web` path as the supported
+fallback.
## Auth
+`web.auth` is an explicit `auto`, `none`, or `github` policy. `auto` resolves
+to no login only for literal loopback binds (`127/8`, `::1`, or `localhost`);
+wildcards, LAN addresses, unknown hostnames, and malformed values resolve to
+GitHub auth without DNS lookups. Explicit `none` on a non-loopback bind is
+refused before Rails/database work unless `--unsafe-no-auth` is supplied, and
+that invocation prints a prominent warning. The CLI exports the resolved mode
+to Rails, so application code never infers authentication from request hosts.
+Local no-auth mode keeps CSRF protection but bypasses the owner session gate
+and omits the login/logout chrome. Hivebox explicitly starts `hive web` with
+`--auth github`, preserving its owner-claim contract.
+
GitHub **device flow** (RFC 8628, see [[decisions]] ADR-036), owner-only.
An ownerless box is CLAIMABLE: the first successful device-flow login writes
itself into `web.github.owner` (config-lock-guarded so concurrent first
diff --git a/wiki/gaps.md b/wiki/gaps.md
index 2d71cc61..c139bfd0 100644
--- a/wiki/gaps.md
+++ b/wiki/gaps.md
@@ -106,7 +106,6 @@ 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.
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 d59b93e3..7d3d5757 100644
--- a/wiki/index.md
+++ b/wiki/index.md
@@ -3,7 +3,7 @@ title: hive Wiki
type: index
source: wiki/**/*.md
created: 2026-05-14
-updated: 2026-06-25
+updated: 2026-07-18
tags: [index, wiki]
---
@@ -11,7 +11,7 @@ tags: [index, wiki]
**TLDR**: Catalog of the LLM-maintained wiki for `hive`.
Page count: 84
-Updated: 2026-06-25
+Updated: 2026-07-18
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/20260718T000000Z-local-web-runtime.md b/wiki/log.d/20260718T000000Z-local-web-runtime.md
new file mode 100644
index 00000000..1401e086
--- /dev/null
+++ b/wiki/log.d/20260718T000000Z-local-web-runtime.md
@@ -0,0 +1,14 @@
+---
+title: Package writable local web runtime
+date: 2026-07-18
+pages: [commands/web]
+---
+
+Packaged the Rails control-plane source with `hive-cli` and added
+`Hive::Web::RuntimeLayout` plus `Setup::WebProvisioner`. Local setup now copies
+the immutable payload to `${XDG_DATA_HOME}/hive/web/`, installs the
+locked bundle and Rails artifacts there, stores durable SQLite/session data
+under XDG state, writes a manifest, and atomically promotes only completed
+runtimes. `hive web` prefers this matching runtime while retaining the
+Docker-compatible `HIVEBOX_WEB_APP_DIR` override and its legacy environment
+aliases.
diff --git a/wiki/log.d/20260718T000100Z-local-setup-preflight.md b/wiki/log.d/20260718T000100Z-local-setup-preflight.md
new file mode 100644
index 00000000..54740a7e
--- /dev/null
+++ b/wiki/log.d/20260718T000100Z-local-setup-preflight.md
@@ -0,0 +1,11 @@
+---
+title: Add structured local setup preflight
+date: 2026-07-18
+pages: [commands/doctor]
+---
+
+Added `Setup::Preflight` and `Setup::QmdInstaller` for the local control-plane
+installer. The preflight returns typed, remediation-bearing rows without
+installing or authenticating external commands. The qmd provisioner owns only
+Hive's XDG data prefix, rebuilds `better-sqlite3`, verifies the binary, and
+leaves a conflicting user qmd link untouched.
diff --git a/wiki/log.d/20260718T000200Z-bind-aware-web-auth.md b/wiki/log.d/20260718T000200Z-bind-aware-web-auth.md
new file mode 100644
index 00000000..d1514268
--- /dev/null
+++ b/wiki/log.d/20260718T000200Z-bind-aware-web-auth.md
@@ -0,0 +1,12 @@
+---
+title: Make web authentication bind-aware
+date: 2026-07-18
+pages: [commands/web]
+---
+
+Added the `web.auth` policy (`auto`, `none`, `github`) and a pre-Rails
+loopback classifier. Authless mode is now possible only on verified loopback
+by default; public `none` needs `--unsafe-no-auth`. Rails receives the
+resolved mode through `HIVE_WEB_AUTH`, maintains CSRF, and skips GitHub
+session/owner checks only in the local authless mode. The Docker supervisor
+passes explicit GitHub auth to preserve hivebox behavior.
diff --git a/wiki/log.d/20260718T000300Z-local-web-service.md b/wiki/log.d/20260718T000300Z-local-web-service.md
new file mode 100644
index 00000000..07a7da50
--- /dev/null
+++ b/wiki/log.d/20260718T000300Z-local-web-service.md
@@ -0,0 +1,13 @@
+---
+title: Add independent native web service
+date: 2026-07-18
+pages: [commands/web]
+---
+
+Added `hive web install` and `hive web start` plus independent `hive-web`
+systemd-user/launchd templates. The renderer inherits shared atomic
+write/drift/backup mechanics, resolves the stable invoked Hive wrapper, and
+bakes the calling shell's non-secret XDG environment into the unit. launchd
+uses a clean-exit missing-binary guard to avoid a permanent respawn loop.
+Uninstall now removes the web unit and XDG web runtime while preserving durable
+web state under XDG state.
diff --git a/wiki/log.d/20260718T000400Z-daemon-identity-health.md b/wiki/log.d/20260718T000400Z-daemon-identity-health.md
new file mode 100644
index 00000000..1f5935be
--- /dev/null
+++ b/wiki/log.d/20260718T000400Z-daemon-identity-health.md
@@ -0,0 +1,13 @@
+---
+title: Publish daemon binary identity health
+date: 2026-07-18
+pages: [commands/daemon, commands/web]
+---
+
+Daemon PID payloads now persist the canonical invoked Hive wrapper and version.
+`Daemon::Health` compares them without signalling unknown processes and powers
+the new `hive-daemon-status.v2` `daemon_identity` object. `Daemon::Repair`
+uses the existing managed-unit backup/force semantics and waits for a fresh
+matching identity. The local web status page displays this state and exposes a
+CSRF-protected repair action only in local no-auth mode; Docker stays under
+its container supervisor.
diff --git a/wiki/log.d/20260718T000500Z-local-setup-orchestrator.md b/wiki/log.d/20260718T000500Z-local-setup-orchestrator.md
new file mode 100644
index 00000000..6cd26890
--- /dev/null
+++ b/wiki/log.d/20260718T000500Z-local-setup-orchestrator.md
@@ -0,0 +1,12 @@
+---
+title: Orchestrate local Hive setup
+date: 2026-07-18
+pages: [commands/setup, commands/web, commands/daemon]
+---
+
+Added `hive setup [PROJECT_PATH]` as an idempotent phase-oriented command. It
+continues qmd/runtime provisioning when external preflight rows need action,
+re-enrolls projects without resetting state, starts independently managed
+daemon/web services, detects port conflicts instead of killing listeners, and
+requires deep health before a zero exit. Its structured result prints all
+remediation before returning non-zero for unfinished external prerequisites.
diff --git a/wiki/log.d/20260718T000600Z-local-web-release-docs.md b/wiki/log.d/20260718T000600Z-local-web-release-docs.md
new file mode 100644
index 00000000..a6854694
--- /dev/null
+++ b/wiki/log.d/20260718T000600Z-local-web-release-docs.md
@@ -0,0 +1,13 @@
+---
+title: Document and verify native local web payload
+date: 2026-07-18
+pages: [architecture, commands/web, commands/setup]
+---
+
+The built gem now has an integration assertion for the complete Rails runtime
+and native web service templates, including the executable Rails entry point;
+the release build repeats that artifact check before publishing. User-facing
+install, FAQ, package caveat, and Docker docs now present `hive setup .` as the
+native local path while retaining hivebox as the `/data`-isolated GitHub-auth
+alternative. Managed daemon and web units receive the same non-secret XDG
+environment snapshot as the invoking CLI.