Update docs for event socket mirroring and add doc update convention
README: mirror push now goes through event socket, not inline in
the hook. Updated status to reflect mirroring is working.

PLAN: step 5 and locked-in decisions updated to describe the
hook → socket → serve architecture.

config: per-repo config is now read by quire serve over the event
socket, not by the hook directly.

AGENTS: added convention requiring doc updates alongside behavior
changes.

Assisted-by: GLM-5.1 via pi
change uozozulykqvyrvksmmxyuqzwupwyzwvk
commit 47bd2f564d41ae46d26752563528e01c6c4a1563
author Alpha Chen <alpha@kejadlen.dev>
date
parent uqwzkqux
diff --git a/AGENTS.md b/AGENTS.md
index 93a0090..ddb0419 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -7,3 +7,14 @@ The backlog lives in the `ranger` CLI, not GitHub Issues. Use `ranger` to read a
 ## Before committing
 
 Always run `just all` and verify everything passes before committing. No exceptions — this is not optional. If you commit without running it, you will break the build.
+
+## Updating docs
+
+When changing behavior (mirroring, config, hook dispatch, Docker layout, CI workflows), update the corresponding docs in the same commit. The docs to check:
+
+- `README.md` — feature descriptions and project status.
+- `docs/PLAN.md` — build sequence, architecture, and locked-in design decisions.
+- `docs/config.md` — config file schemas and how they're loaded.
+- `.github/workflows/` — workflow changes must be self-documenting (comments on permissions, triggers).
+
+If you're unsure whether a doc needs updating, it probably does.
diff --git a/README.md b/README.md
index 324ea77..e86a22e 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ A Rust binary that runs in a Docker container, fronted by the host's sshd and a
 
 - **Git hosting over SSH**, via the host's sshd dispatching into the container. Explicit repo creation (`ssh git@host quire repo new <name>`).
 - **A read-only web view** for browsing README, tree, history, blame, diffs, and refs.
-- **Automatic mirroring to GitHub** on push, when configured per-repo. A single GitHub PAT lives in global config and rides on the push as an `http.extraHeader` — no per-repo deploy keys, no agent socket plumbing.
+- **Automatic mirroring to GitHub** on push, when configured per-repo. The post-receive hook sends a push event over a Unix domain socket to `quire serve`, which looks up mirror config and runs the push in-process. A single GitHub PAT lives in global config and rides on the push as an `http.extraHeader` — no per-repo deploy keys, no agent socket plumbing. Mirror failures surface in server logs, not the pusher's terminal.
 - **Fennel-based CI** (Fennel is a Lisp that compiles to Lua), with pipelines defined in `.quire/ci.fnl`. Unsandboxed by default since every pipeline is code I've written; a bubblewrap-based opt-in is available for the day quire ever runs code I haven't.
 - **Email notifications** for CI failures, recoveries, and mirror-push failures. SMTP via `msmtp`; plain text; per-repo config for what to send and to whom.
 
@@ -45,4 +45,4 @@ Host-side config (sshd_config block, Caddyfile, docker-compose file) lives on th
 
 ## Status
 
-Early development. SSH dispatch, repo management, and Fennel config loading work; web view, CI, mirroring, and notifications are still ahead. See `PLAN.md` for the build sequence and open questions.
+Early development. SSH dispatch, repo management, Fennel config loading, and mirror push via event socket work; web view, CI, and notifications are still ahead. See `PLAN.md` for the build sequence and open questions.
diff --git a/docs/PLAN.md b/docs/PLAN.md
index 6d21d75..305be7c 100644
--- a/docs/PLAN.md
+++ b/docs/PLAN.md
@@ -143,7 +143,7 @@ Write the quire binary's `hook` subcommand as a no-op that logs what it was invo
 
 Per-repo config checked into the repo at `.quire/config.fnl` with a `mirror` key (GitHub remote URL); quire reads it from the bare repo via `git show HEAD:.quire/config.fnl`. A matching private key at `<repo>.git/quire/mirror-deploy-key` (generated by `quire mirror add <remote-url>`, which also prints the public key for the user to paste into GitHub's deploy-keys UI).
 
-Post-receive hook reads the config, invokes `git push --mirror` with `GIT_SSH_COMMAND="ssh -i /var/quire/repos/<repo>.git/quire/mirror-deploy-key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"`. Simple, no agent forwarding, no host↔container socket plumbing.
+Post-receive hook sends a JSON push event over `/var/quire/server.sock` to `quire serve`. The server listener parses the event, looks up the repo's mirror config, resolves the GitHub token from global config, and runs `git push` in a spawned blocking task. Mirror failures surface in the server's logs. If `quire serve` isn't running, the hook prints a warning to stderr and exits cleanly (no push is created).
 
 Pre-receive: if a mirror is configured, test-run a low-cost git operation against the remote (probably `git ls-remote`) to verify the deploy key still works. If it fails, reject with a clear message. Per-repo override (`accept_without_mirror = true`) for the rare case where you want to push without syncing.
 
@@ -205,7 +205,7 @@ Keyboard navigation in the web UI. Atom feeds for recent commits (public, subjec
 - **Trust the proxy-injected identity header.** `Remote-User` is trusted because the reverse proxy is the only ingress. Proxy must strip any client-supplied version before injecting its own — this is the security-critical invariant.
 - **Explicit repo creation, not implicit on first push.** `ssh git@host quire new <n>`. No magic, no shims parsing first pushes.
 - **Hooks via `hook.<n>.command` config.** Git 2.54+ (the version we build into the container image). No shim scripts on disk; `hook.<n>.command = /usr/local/bin/quire hook <n>`. Set at creation time.
-- **Mirror push is synchronous in post-receive.** Slow GitHub = slow push. Worth it; silent drift is the worst outcome.
+- **Mirror push runs inside `quire serve` via event socket.** The post-receive hook sends a JSON push event over a Unix domain socket (`/var/quire/server.sock`) to `quire serve`, which looks up the repo's mirror config and runs the push in-process. This trades synchronous push-for-push blocking for architectural cleanliness: the hook exits fast, and mirror failures surface in the server's logs, not the pusher's terminal. When the server isn't running, the hook prints a warning and exits cleanly.
 - **No reverse-direction mirroring.** quire is the source of truth; GitHub is the replica.
 - **CI pipelines are Fennel macros, not data tables.** The whole point is real code. Shared steps can be factored into `.quire/lib/*.fnl` and `require`'d.
 - **One level of repo grouping max.** `foo.git` and `work/foo.git` are fine. `a/b/c.git` is rejected.
diff --git a/docs/config.md b/docs/config.md
index cadae50..26a4744 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -33,10 +33,14 @@ file surfaces as a Fennel parse or eval error with source labels.
 ## Per-repo config
 
 Lives at `.quire/config.fnl` *checked into the repo* — quire reads it
-from `HEAD` of the bare repo via `git show HEAD:.quire/config.fnl`
-whenever a hook needs it. Repos without the file (or without a given
-key) get defaults; this is a no-op for the post-receive hook, not an
-error.
+from `HEAD` of the bare repo via `git show HEAD:.quire/config.fnl`.
+Repos without the file (or without a given key) get defaults; this is
+a no-op, not an error.
+
+The post-receive hook does not read this config directly. Instead it
+sends a JSON push event to `quire serve` over `/var/quire/server.sock`.
+The server reads the config to find mirror settings and dispatch the
+push. See `src/bin/quire/commands/serve.rs` for the full path.
 
 | Key            | Type     | Required | Purpose                                                                       |
 |----------------|----------|----------|-------------------------------------------------------------------------------|