+
+
Daemon
+
+ <%= daemon["ready"] ? "Ready" : (daemon["running"] ? "Running with service issue" : "Stopped") %>
+ · service <%= daemon["service_enabled"] ? "enabled" : "not enabled" %>
+ · drift <%= daemon["drift"] %>
+
+
+ Installed: <%= daemon["installed_executable"] || "not detected" %>
+ (<%= daemon["installed_version"] || "unknown version" %>)
+ Current: <%= daemon["current_executable"] %>
+ (<%= daemon["current_version"] %>)
+
+ <% if daemon["drift_message"].present? %>
+
Why: <%= daemon["drift_message"] %>
+ <% end %>
+ <% if daemon["last_maintenance"] %>
+
Last action: <%= daemon.dig("last_maintenance", "message") %>
+ <% end %>
+
+
+ <% repair_recommended = daemon["drift"] != "none" ||
+ daemon["service_installed"] != true ||
+ daemon["service_enabled"] != true %>
+ <% if repair_recommended %>
+
Recommended: repair the service definition with the current Hive executable.
+ <%= button_to "Repair service", daemon_maintenance_path("repair"),
+ method: :post, class: "btn btn-sm",
+ form: { data: { turbo_confirm: "Repair the daemon service? This refuses while agents are active." } } %>
+ <% else %>
+
Recommended: restart the healthy service only if its process needs refreshing.
+ <%= button_to "Restart", daemon_maintenance_path("restart"),
+ method: :post, class: "btn btn-ghost btn-sm",
+ form: { data: { turbo_confirm: "Restart the Hive daemon? This refuses while agents are active." } } %>
+ <% end %>
+
+
diff --git a/web/app/views/status/index.html.erb b/web/app/views/status/index.html.erb
index 25bcdf79..cc48afd0 100644
--- a/web/app/views/status/index.html.erb
+++ b/web/app/views/status/index.html.erb
@@ -26,6 +26,8 @@
+<%= render "status/daemon", daemon: @daemon_report %>
+
<%# 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/database.yml b/web/config/database.yml
index d1c0e8fd..256fb134 100644
--- a/web/config/database.yml
+++ b/web/config/database.yml
@@ -26,21 +26,22 @@ 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
-# home — the /data mount in the container) so image upgrades keep them.
+# Production sqlite files prefer the canonical local-web storage variable and
+# retain HIVEBOX_STORAGE_DIR as the Docker/source compatibility alias.
+<% hive_web_storage = ENV["HIVE_WEB_STORAGE_DIR"] || ENV["HIVEBOX_STORAGE_DIR"] || "storage" %>
production:
primary:
<<: *default
- database: <%= ENV.fetch("HIVEBOX_STORAGE_DIR", "storage") %>/production.sqlite3
+ database: <%= hive_web_storage %>/production.sqlite3
cache:
<<: *default
- database: <%= ENV.fetch("HIVEBOX_STORAGE_DIR", "storage") %>/production_cache.sqlite3
+ database: <%= hive_web_storage %>/production_cache.sqlite3
migrations_paths: db/cache_migrate
queue:
<<: *default
- database: <%= ENV.fetch("HIVEBOX_STORAGE_DIR", "storage") %>/production_queue.sqlite3
+ database: <%= hive_web_storage %>/production_queue.sqlite3
migrations_paths: db/queue_migrate
cable:
<<: *default
- database: <%= ENV.fetch("HIVEBOX_STORAGE_DIR", "storage") %>/production_cable.sqlite3
+ database: <%= hive_web_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..5cb41acd 100644
--- a/web/config/environments/production.rb
+++ b/web/config/environments/production.rb
@@ -99,5 +99,14 @@ Rails.application.configure do
# 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
+
+ # Host authorization remains active in every security mode. IPAddr matches
+ # the complete loopback ranges (including 127.0.0.2), while non-loopback
+ # binds add only their explicitly configured host.
+ require "hive/web/host_authorization"
+ bind = ENV["HIVE_WEB_BIND"] || ENV["HIVEBOX_BIND"] || "127.0.0.1"
+ origin = ENV["HIVE_WEB_ORIGIN"] || ENV["HIVEBOX_ORIGIN"]
+ config.hosts = Hive::Web::HostAuthorization.allowed_hosts(bind: bind, origin: origin)
end
diff --git a/web/config/initializers/hive.rb b/web/config/initializers/hive.rb
index f2792b55..7aa6f994 100644
--- a/web/config/initializers/hive.rb
+++ b/web/config/initializers/hive.rb
@@ -10,3 +10,4 @@ require "hive/web/telegram_validator"
require "hive/web/telegram_tester"
require "hive/commands/init"
require "hive/commands/approve"
+require "hive/pid_file"
diff --git a/web/config/initializers/hive_web_environment.rb b/web/config/initializers/hive_web_environment.rb
new file mode 100644
index 00000000..07f85c15
--- /dev/null
+++ b/web/config/initializers/hive_web_environment.rb
@@ -0,0 +1,6 @@
+require "ipaddr"
+
+Rails.application.config.x.hive_web_local_mode =
+ ENV.fetch("HIVE_WEB_LOCAL_MODE", "false") == "true"
+Rails.application.config.x.hive_web_bind =
+ ENV["HIVE_WEB_BIND"] || ENV["HIVEBOX_BIND"] || "127.0.0.1"
diff --git a/web/config/routes.rb b/web/config/routes.rb
index 659576df..d1bdb4de 100644
--- a/web/config/routes.rb
+++ b/web/config/routes.rb
@@ -16,6 +16,9 @@ Rails.application.routes.draw do
post "logout" => "sessions#destroy", as: :logout
root "status#index"
+ post "daemon/:operation" => "daemon#maintain",
+ as: :daemon_maintenance,
+ constraints: { operation: /repair|restart/ }
post "ideas" => "ideas#create", as: :ideas
diff --git a/web/test/controllers/daemon_controller_test.rb b/web/test/controllers/daemon_controller_test.rb
new file mode 100644
index 00000000..0b6c22eb
--- /dev/null
+++ b/web/test/controllers/daemon_controller_test.rb
@@ -0,0 +1,24 @@
+require "test_helper"
+
+class DaemonControllerTest < ActionDispatch::IntegrationTest
+ test "maintenance routes are POST-only and closed to fixed actions" do
+ sign_in!
+
+ get "/daemon/restart"
+ assert_response :not_found
+
+ post "/daemon/arbitrary"
+ assert_response :not_found
+ end
+
+ test "status page exposes bounded daemon actions" do
+ sign_in!
+
+ get "/"
+
+ assert_response :success
+ assert_select "section.daemon-card"
+ assert_select "form[action='/daemon/repair']"
+ assert_select "form[action='/daemon/restart']", count: 0
+ end
+end
diff --git a/web/test/integration/local_loopback_auth_test.rb b/web/test/integration/local_loopback_auth_test.rb
new file mode 100644
index 00000000..77bd2274
--- /dev/null
+++ b/web/test/integration/local_loopback_auth_test.rb
@@ -0,0 +1,37 @@
+require "test_helper"
+
+class LocalLoopbackAuthTest < ActionDispatch::IntegrationTest
+ setup do
+ @old_mode = Rails.application.config.x.hive_web_local_mode
+ @old_bind = Rails.application.config.x.hive_web_bind
+ Rails.application.config.x.hive_web_local_mode = true
+ Rails.application.config.x.hive_web_bind = "127.0.0.1"
+ end
+
+ teardown do
+ Rails.application.config.x.hive_web_local_mode = @old_mode
+ Rails.application.config.x.hive_web_bind = @old_bind
+ end
+
+ test "genuine loopback request bypasses login in local mode" do
+ get "/", env: { "REMOTE_ADDR" => "127.0.0.1" }
+
+ assert_response :success
+ end
+
+ test "forwarded header cannot spoof loopback peer" do
+ get "/",
+ headers: { "X-Forwarded-For" => "127.0.0.1" },
+ env: { "REMOTE_ADDR" => "203.0.113.9" }
+
+ assert_redirected_to "/login"
+ end
+
+ test "non-loopback configured bind disables bypass" do
+ Rails.application.config.x.hive_web_bind = "0.0.0.0"
+
+ get "/", env: { "REMOTE_ADDR" => "127.0.0.1" }
+
+ assert_redirected_to "/login"
+ end
+end
diff --git a/web/test/integration/production_host_authorization_test.rb b/web/test/integration/production_host_authorization_test.rb
new file mode 100644
index 00000000..b9f7766d
--- /dev/null
+++ b/web/test/integration/production_host_authorization_test.rb
@@ -0,0 +1,32 @@
+require "test_helper"
+require "action_dispatch"
+require "rack/mock"
+require "hive/web/host_authorization"
+
+class ProductionHostAuthorizationTest < ActiveSupport::TestCase
+ def authorized_app(bind: "127.0.0.1", origin: nil)
+ hosts = Hive::Web::HostAuthorization.allowed_hosts(bind: bind, origin: origin)
+ ActionDispatch::HostAuthorization.new(
+ ->(_env) { [ 200, { "content-type" => "text/plain" }, [ "ok" ] ] },
+ hosts
+ )
+ end
+
+ test "accepts any loopback address while rejecting an unconfigured host" do
+ app = authorized_app
+
+ assert_equal 200, Rack::MockRequest.new(app).get("/", "HTTP_HOST" => "127.0.0.2").status
+ assert_equal 403, Rack::MockRequest.new(app).get("/", "HTTP_HOST" => "attacker.example").status
+ end
+
+ test "non-loopback mode retains host authorization for the configured origin" do
+ app = authorized_app(bind: "0.0.0.0", origin: "https://hive.internal.example")
+
+ assert_equal 200,
+ Rack::MockRequest.new(app).get("/", "HTTP_HOST" => "192.0.2.10").status
+ assert_equal 200,
+ Rack::MockRequest.new(app).get("/", "HTTP_HOST" => "hive.internal.example").status
+ assert_equal 403,
+ Rack::MockRequest.new(app).get("/", "HTTP_HOST" => "spoofed.example").status
+ end
+end
diff --git a/wiki/commands/daemon.md b/wiki/commands/daemon.md
index c84cb08e..013d17d8 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-23
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` | Uses `Hive::Daemon::StatusReport` for process and service state, installed/current executable and version, readiness, and drift (`none`, `path`, `version`, `unparseable`, `unreadable`, or `not_applicable`). Exit code 0 only when running. The same report is consumed directly by Rails, without global stdout capture. |
| `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. |
@@ -48,6 +48,12 @@ hive daemon queue [list | show | prune] [--json]
| `disable` | Same shape as `enable`, sets `daemon.enabled: false`. The next dispatcher tick honours the change automatically (per-tick enable-cache invalidation); `hive daemon reload` is optional for instant pickup. |
| `queue` | Read-only inspection of the dispatch-request queue the bot/web producers and `3-plan` healer write and the daemon consumes. Runs in the CLI process (no daemon contact); reads the same `/dispatch_requests/` directory. Current pending request files use `hive-dispatch-request.v2`, whose `requestor` enum is `bot|healer`; older/wrong versions are reported as malformed and pruned like other bad files. `list` (default) prints each pending request with `request_id age project/slug verb` plus `[EXPIRED]` / `[NOT-ALLOWLISTED]` flags and any malformed files. `show ` dumps one request's full payload (errors with exit 1 if the id is unknown; missing id is a USAGE error). `prune` removes expired + malformed request files (the daemon also does this lazily on its own tick) and reports the count. With `--json`, emits a `hive-daemon-queue.v1` envelope (`action`, `requests[]`, `request`, `malformed[]`, `pruned_count`). Unknown actions, missing `show` request ids, and unexpected queue-command exceptions emit the schema's `ErrorPayload` arm with `ok:false`, `error_kind` (`unknown_action` / `missing_request_id` / `internal`), and `message` before exiting non-zero. Claimed in-flight requests (`*.json.claimed`) are intentionally not listed — they are daemon-managed; see [[modules/daemon]] §"At-most-once dispatch via atomic claim". |
+The local status page exposes only fixed, confirmed `repair` and `restart`
+POST actions. Repair calls the current service installer directly, so it works
+even when the daemon queue is down; arbitrary action names and command
+arguments are never accepted. The managed web service preserves
+`HIVE_INVOKED_BIN`, keeping repair aligned with the exact CLI used by setup.
+
## Global Digest
Daily digest scheduling is global config, not project enrollment:
diff --git a/wiki/commands/setup.md b/wiki/commands/setup.md
new file mode 100644
index 00000000..bd430f78
--- /dev/null
+++ b/wiki/commands/setup.md
@@ -0,0 +1,45 @@
+---
+title: hive setup
+type: command
+source: lib/hive/commands/setup.rb, lib/hive/setup/, schemas/hive-setup.v1.json
+created: 2026-07-23
+updated: 2026-07-23
+tags: [command, setup, diagnostics, web, daemon]
+---
+
+**TLDR**: `hive setup` is the idempotent Linux/macOS local-mode orchestrator.
+It diagnoses prerequisites, bootstraps only Hive-owned QMD and the matching
+Rails bundle, installs the daemon and web as separate per-user services using
+the exact invoked binary, enrolls the current repository, and waits for
+`http://127.0.0.1:4567/health`.
+
+## Phases
+
+The fixed order is diagnostics, QMD bootstrap, verified web bundle, daemon
+install/readiness, repository enrollment, web install/readiness. A mandatory
+diagnostic failure prevents service mutations. Each completed safe phase is
+retained so rerunning setup can resume without destructive initialization or
+unnecessary service churn.
+
+Diagnostics cover supported Linux/macOS, Ruby 3.4, git, tmux, Node/npm, SQLite,
+cosign, QMD, Rails bundle state, and installed/authenticated `gh`, Claude, and Codex.
+Every probe is timeout-bounded. Setup never installs or authenticates
+operator-owned tools; failure results contain exact remediation.
+Nested initialization, service, Bundler, and Rails output is isolated from
+stdout under `--json`, leaving one schema-valid document.
+
+## Options
+
+- `--no-bootstrap` reports missing Hive-owned dependencies without installing.
+- `--no-init` skips current-repository initialization/enrollment.
+- `--no-service` prepares local mode but does not install/start the web user
+ service; the result prints the exact `hive web` foreground command.
+- `--json` emits the `hive-setup.v1` phase/check envelope and uses the same
+ overall success decision as the process exit.
+
+Windows is explicitly unsupported. A host without systemd-user or launchd can
+finish all foreground prerequisites, but default setup does not claim managed
+readiness; it returns the exact fallback
+`hive daemon start --detach && hive web`.
+
+See [[commands/web]], [[commands/daemon]], [[operating]], and [[testing]].
diff --git a/wiki/commands/web.md b/wiki/commands/web.md
index f6f25373..f607d797 100644
--- a/wiki/commands/web.md
+++ b/wiki/commands/web.md
@@ -3,13 +3,16 @@ 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-23
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 **Rails 8** UI locally from a
+version-matched managed release bundle, while retaining source-checkout and
+Docker/hivebox execution modes.
+`hive web install|start|stop|status` manages a distinct systemd-user/launchd
+service. Local mode shares the CLI/TUI/daemon XDG configuration and real
+repositories; Docker remains the `/data`-isolated alternative. 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,17 +25,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] [--unsafe]` is always foreground. It uses an
+in-tree app when present and otherwise verifies/installs the matching release
+bundle under `Hive::Paths.data_home/web`. `hive web install|start|stop|status`
+operates the independent user service and reports running separately from HTTP
+readiness. The command 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).
+`HIVE_WEB_STORAGE_DIR`/legacy `HIVEBOX_STORAGE_DIR` (the solid-stack SQLite
+files under `Hive::Paths.state_home/web-storage`), runs `bin/rails db:prepare`,
+then execs `bin/rails server`. Canonical `HIVE_WEB_*` variables win over
+legacy `HIVEBOX_*` aliases. The gem remains lean; releases publish
+`hive-web-.tar.gz` beside the gem and signed checksum metadata.
+Default acquisition fails closed unless cosign authenticates that checksum
+manifest. Custom acquisition is available only through the paired
+`HIVE_WEB_BUNDLE_URL` and `HIVE_WEB_BUNDLE_SHA256` variables. Download and
+bundle/assets/database preparation all have hard deadlines, and reuse requires
+a complete executable bundle rather than only a matching version marker.
+
+## Local security
+
+The default bind is `127.0.0.1:4567`. Login bypass requires local-loopback mode,
+a configured loopback bind, and an actual loopback request peer; forwarding
+headers cannot manufacture the bypass and Rails Host authorization remains
+active for the full loopback range and explicitly configured bind/origin. A
+non-loopback bind is refused unless the GitHub device-flow client is configured
+or the operator explicitly supplies `--unsafe`; an ownerless installation with
+the client configured remains claimable on first login. Unsafe mode remains
+visible in startup and status warnings.
## Auth
diff --git a/wiki/gaps.md b/wiki/gaps.md
index 2d71cc61..781a2acd 100644
--- a/wiki/gaps.md
+++ b/wiki/gaps.md
@@ -47,6 +47,16 @@ Latest refresh note (2026-06-16): the babysitter gh-hostname dry-run audit remai
## Open questions about the codebase
+### 2026-07-23 local web platform verification
+
+The managed bundle, loopback policy, systemd-user/launchd rendering, setup
+orchestration, drift reporting, and installed-artifact boot have automated
+source/fixture coverage. The release gate boots the built gem and matching web
+artifact on Linux and macOS. There is not yet an in-repository artifact from a
+clean interactive user account proving both real service managers through the
+entire install/start/status/repair/stop/uninstall sequence, nor a published
+release asset for version 0.3.2 at implementation time.
+
### 2026-06-22 dependency-stacking placeholder branch investigation
Branch-creator inventory for the U1-U10 inversion dogfood found no separate
diff --git a/wiki/index.md b/wiki/index.md
index d59b93e3..302d3ce3 100644
--- a/wiki/index.md
+++ b/wiki/index.md
@@ -3,15 +3,15 @@ title: hive Wiki
type: index
source: wiki/**/*.md
created: 2026-05-14
-updated: 2026-06-25
+updated: 2026-07-23
tags: [index, wiki]
---
**TLDR**: Catalog of the LLM-maintained wiki for `hive`.
-Page count: 84
-Updated: 2026-06-25
+Page count: 85
+Updated: 2026-07-23
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/20260723-local-web-install.md b/wiki/log.d/20260723-local-web-install.md
new file mode 100644
index 00000000..87d22a71
--- /dev/null
+++ b/wiki/log.d/20260723-local-web-install.md
@@ -0,0 +1,11 @@
+## 2026-07-23 — First-class local Hive web install
+
+- Added `hive setup` diagnostics and idempotent Linux/macOS orchestration.
+- Added verified, version-matched Rails release bundles with safe staged
+ extraction and XDG-local dependency/state separation.
+- Added foreground `hive web` plus independent systemd-user/launchd
+ install/start/stop/status lifecycle at `127.0.0.1:4567`.
+- Added loopback peer enforcement, non-loopback auth/unsafe policy, daemon
+ binary/version drift reporting, and fixed repair/restart web actions.
+- Added installed-artifact release gates, shared-state E2E coverage, schemas,
+ operator docs, and Docker/hivebox compatibility coverage.
diff --git a/wiki/log.d/20260723T213750Z-local-web-review-hardening.md b/wiki/log.d/20260723T213750Z-local-web-review-hardening.md
new file mode 100644
index 00000000..018d9cd9
--- /dev/null
+++ b/wiki/log.d/20260723T213750Z-local-web-review-hardening.md
@@ -0,0 +1,18 @@
+---
+date: 2026-07-23
+slug: local-web-review-hardening
+---
+
+- Hardened [[commands/web]] release acquisition with mandatory cosign
+ authentication, bounded downloads/preparation, complete-bundle reuse checks,
+ canonical storage precedence, and source/Docker-compatible Bundler behavior.
+- Made managed web status reflect the installed definition and distinguish
+ manager, unit, process, port, HTTP, and configuration-drift failures.
+- Added safe shared daemon repair/restart primitives, active-agent refusal,
+ persisted maintenance results, and action-specific status UI guidance.
+- Made [[commands/setup]] enrollment use the registered project name, isolated
+ JSON stdout, added phase remediation, rediscovered Hive-managed QMD, and
+ documented the exact no-service-manager foreground fallback.
+- Expanded package and E2E acceptance to exercise real HTTP visibility,
+ automatic daemon pickup, native service lifecycle, maintenance, and safe
+ uninstall.
diff --git a/wiki/operating.md b/wiki/operating.md
index 2b24d46a..20f43edd 100644
--- a/wiki/operating.md
+++ b/wiki/operating.md
@@ -3,7 +3,7 @@ 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
+updated: 2026-07-23
tags: [operating, daemon, bot, systemd, launchd, install]
---
@@ -94,6 +94,41 @@ Fresh installs use XDG locations:
| Cache | `~/.cache/hive/` |
| User binary symlink | `~/.local/bin/hive` |
+## First-class local web
+
+From the repository to enroll:
+
+```bash
+hive setup
+hive web status
+```
+
+The default setup installs the separate daemon and web user services and waits
+for `http://127.0.0.1:4567/health`. It uses the invoked Hive binary in both
+definitions. Bare `hive web` is foreground; managed lifecycle is:
+
+```bash
+hive web install
+hive web start
+hive web status --json
+hive web stop
+hive daemon repair --json
+hive daemon restart --json
+```
+
+Immutable web files live in `${XDG_DATA_HOME}/hive/web` and
+`${XDG_DATA_HOME}/hive/web-gems`; SQLite and mutable Rails state live in
+`${XDG_STATE_HOME}/hive/web-storage`. `hive uninstall` removes the managed app
+and dependencies but preserves storage. Only `--force-purge-state` deletes it.
+If web service deregistration fails, uninstall preserves the runtime as well,
+so a still-loaded unit cannot enter a missing-executable crash loop.
+
+Local login bypass is loopback-only and verifies the actual peer. A public bind
+requires a configured GitHub device-flow client or explicit `--unsafe`; a fresh
+ownerless installation with that client remains claimable. Host authorization
+stays enabled in every mode. Docker/hivebox continues to use `/data` and the
+lower-priority `HIVEBOX_*` compatibility variables.
+
`HIVE_HOME` remains a legacy/test override. Project state stays at
`/.hive-state/`; install and uninstall do not move completed pipeline
work.
diff --git a/wiki/testing.md b/wiki/testing.md
index 3ee77fea..78faab6e 100644
--- a/wiki/testing.md
+++ b/wiki/testing.md
@@ -3,7 +3,7 @@ title: Testing
type: reference
source: test/, Rakefile, bin/hive-eval, .rubocop.yml, .github/workflows/ci.yml, .github/workflows/release.yml, config/brakeman.ignore
created: 2026-04-25
-updated: 2026-06-25
+updated: 2026-07-23
tags: [test, minitest, fixtures]
---
@@ -15,6 +15,27 @@ tags: [test, minitest, fixtures]
bundle exec rake test
```
+The local web artifact gate runs:
+
+```bash
+packaging/smoke-local-web.sh
+```
+
+It builds and installs the gem outside the checkout, creates or consumes the
+matching Rails archive, runs real bundle/assets/database preparation with
+XDG-local dependencies and storage, boots foreground `hive web`, checks
+`/health`, then runs `hive setup --no-init` through the native user service
+manager, checks readiness, exercises daemon repair/restart, stops web, and
+verifies uninstall removed the units/runtime. Release CI runs the same script
+on Linux/systemd-user and macOS/launchd against the exact build artifacts
+before publication.
+
+`test/e2e/scenarios/local_web_shared_state.yml` pins that the CLI and Rails
+observe the same registered project/task tree through a real HTTP request, then
+starts the daemon and waits for its automatic dispatch event. Docker/hivebox
+tests remain independent regression gates for `/data`, legacy environment
+aliases, authentication, and Host authorization.
+
## Coverage
```bash