diff --git a/docs/content/crux/readme.md b/docs/content/crux/readme.md
index d0b6fd57..935fe34a 100644
--- a/docs/content/crux/readme.md
+++ b/docs/content/crux/readme.md
@@ -72,6 +72,18 @@ tar xzf crux_0.1.0_linux_amd64.tar.gz
./crux --version
```
+That archive example is for Linux on amd64. On macOS choose `darwin_arm64`
+for Apple Silicon or `darwin_amd64` for Intel from the same release. Keep the
+signature-verification command unchanged. macOS includes `shasum`, so verify
+the selected archive against its signed checksum before extracting it:
+
+```sh
+# Apple Silicon example; use darwin_amd64 in both places on Intel
+grep 'crux_0.1.0_darwin_arm64.tar.gz$' checksums.txt | shasum -a 256 -c -
+tar xzf crux_0.1.0_darwin_arm64.tar.gz
+./crux --version
+```
+
The container image is signed the same way:
```sh
@@ -117,6 +129,12 @@ crux kick scout "look at issue 42" # send it work
crux stop scout # stop it
```
+While `crux run` is still active, `crux ps` should list `scout`; `crux logs
+scout -n 50` should show the agent's startup or prompt. Output varies by
+backend. If startup fails, check `command -v tmux` and `command -v copilot`,
+then run and authenticate the agent CLI directly before retrying. Do not send
+the example issue task until you have a repository and task you intend it to work on.
+
Agent files live under `--workdir` (default `$HIVE_WORK_DIR`, then
`$HOME/.crux`). While `crux run` is in the foreground it opens a small
control socket in that folder, and `ps`, `logs`, `kick` and `stop` use it
@@ -127,6 +145,11 @@ to reach the live agent.
`crux serve` keeps the runtime up and exposes it over HTTP+JSON, so other
programs (or you, from another shell) can drive it:
+The example assumes `/run/crux` already exists and is writable by the service
+user; it is not normally writable by an ordinary macOS or Linux user.
+For a local trial, use a socket path in a private directory you own instead
+and set `CRUX_SERVER` to that same path. Do not make a shared socket world-writable.
+
```sh
# Serve on a unix socket. A unix socket is a file on disk that two
# programs on the same machine use to talk to each other.
@@ -206,6 +229,14 @@ The design record is
## What crux exposes
+**Go integrator reference starts here.** If you only want to operate agents,
+the [commands](#commands), [service setup](#run-it-as-a-service), and
+[runbooks](https://github.com/hivecommons/crux/blob/main/runbooks/README.md) are enough. The remaining interfaces and routes
+are for programs embedding or calling Crux.
+
+
+Advanced: Go interfaces, HTTP routes, host callbacks, and packages
+
Crux has three layers you can build on. Each one is a Go package in this
repository. The first is the Go interface. The second is the same thing
over HTTP. The third is what crux asks the host for in return.
@@ -394,6 +425,8 @@ a refusal.
- `hivecap`: the runtime-side adapters for the hive callback.
- `serve`, `cli`, `cmd`: the HTTP server, the `crux` command, and the binary.
+
+
## Contributing
The Go code in this repository is not edited here. It is a copy of
diff --git a/docs/content/dibs/mcp-registry.md b/docs/content/dibs/mcp-registry.md
index aa18e21f..95f4fa05 100644
--- a/docs/content/dibs/mcp-registry.md
+++ b/docs/content/dibs/mcp-registry.md
@@ -2,6 +2,17 @@
# Publishing Dibs to the MCP Registry
+This is an operator procedure for publishing Dibs' listing in the Model
+Context Protocol (MCP) registry, where agent clients discover the service.
+To use Dibs from an agent, follow [client setup](/docs/dibs/readme#submit-ideas-from-your-agent)
+instead; publishing a listing is not required.
+
+Before starting, have an up-to-date Dibs checkout, Git, GitHub CLI (`gh`),
+`curl`, and `jq`; authenticate `gh` with an account allowed to run repository
+workflows. DNS (Domain Name System) authentication also requires permission
+to manage `hivecommons.dev` DNS records and the repository's Actions secrets.
+Run checkout-relative commands from the repository root.
+
The root [`server.json`](https://github.com/hivecommons/dibs/blob/main/server.json) prepares the remote-only listing
`dev.hivecommons/dibs`, using the namespace approved in
[issue #375](https://github.com/hivecommons/dibs/issues/375#issuecomment-6215095421).
@@ -43,11 +54,14 @@ gh workflow run mcp-registry-publish.yml --repo hivecommons/dibs
### DNS authentication key
-Authentication is DNS-based: the apex TXT record on `hivecommons.dev`
+Authentication is DNS-based: the apex TXT record (a text record on the bare
+domain, not a subdomain) on `hivecommons.dev`
(`v=MCPv1; k=ed25519; p=`) must match the Ed25519 private key
(64 hex chars, as produced by the registry's
[DNS authentication instructions](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/authentication.mdx#dns-authentication))
-stored in the `MCP_REGISTRY_PRIVATE_KEY` secret. To rotate, generate a new
+stored in the `MCP_REGISTRY_PRIVATE_KEY` secret. Ed25519 is the signing-key
+type; the public key goes in DNS, while the private key stays secret.
+To rotate, generate a new
key pair, replace the TXT record (remove the old one — a stale record is tried
first and breaks verification), then update the secret.
diff --git a/docs/content/dibs/readme.md b/docs/content/dibs/readme.md
index 18aecaca..9949f6bb 100644
--- a/docs/content/dibs/readme.md
+++ b/docs/content/dibs/readme.md
@@ -8,7 +8,7 @@
[](https://github.com/hivecommons/dibs/blob/main/LICENSE)
-Marketplace of ideas for connecting ideators with hive-managed repositories.
+A place to offer software ideas to projects and retain credit for them.
> **Your idea, your credit, their code.**
>
@@ -16,16 +16,18 @@ Marketplace of ideas for connecting ideators with hive-managed repositories.
Dibs is a two-sided marketplace that connects **ideators** — people who call
dibs on ideas: great insight, but no time, tooling, or inclination to write
-code — with **hive-managed repositories** whose AI agent swarms are hungry
-for well-formed work.
+code — with **hive-managed repositories**: projects that use Hive to run
+coding agents under maintainer-chosen rules.
1. **Ideators post ideas** — public or private — in plain language / markdown.
2. **Repos opt in** to receive ideas, declaring topics and appetite.
-3. **An LLM matches** ideas to repos that want them; both sides must say yes.
+3. **A language model (LLM) matches** ideas to repos that want them; both the
+ idea contributor and the receiving project must accept the match.
4. On **mutual acceptance**, Dibs hands the ideator a **prefilled GitHub
issue** — they file it themselves, under their own GitHub account, so the
- issue is natively attributed to them (label `ideated`). The hive's AI
- agents implement it. Dibs is just the matchmaker: it never files the
+ issue is natively attributed to them (label `ideated`). The receiving project
+ can then have its agents work on it under its own review rules. In this
+ default flow Dibs is just the matchmaker: it never files the
issue for you.
## Ideators are first-class contributors
@@ -43,7 +45,35 @@ public.
Every implemented idea traces back to its ideator: the GitHub issue credits
the idea's author, the resulting work carries that provenance, and the public
**credit wall** (`/api/credits`, "Credit wall" in the UI) lists every ideator
-whose idea shipped. An idea is a contribution.
+with a settled idea: a GitHub issue URL has been recorded. This credits the
+proposal, not proof that its implementation shipped or its issue was closed.
+Settled ideas appear regardless of their visibility setting: settling a private
+idea makes its credit facts (author, title/TLDR, target, issue URL, and time)
+public, but not its body or private detail endpoint.
+
+## Try Dibs in your browser
+
+No local installation or Go toolchain is needed. Open
+[dibs.hivecommons.dev](https://dibs.hivecommons.dev) to browse the public
+surface; posting needs a Hive Hub sign-in and filing the issue needs a GitHub account.
+
+1. Sign in and describe your idea in plain language. Choose public or private
+ deliberately; unsettled private ideas are not included in the public wall.
+2. Review suggested repositories and offer the idea to a suitable project.
+ For Hive-managed targets, both sides accept before issue filing.
+3. Open the prefilled GitHub issue, review it, and file it yourself.
+4. Confirm the resulting issue URL in Dibs. This step is **settlement**:
+ it records the handoff, not completion of the proposed work. Confirming also
+ publishes the credit facts above, even for an idea marked private.
+
+Non-Hive GitHub targets can skip the receiving-project acceptance step.
+For settlement and privacy details, see the [consumer API](/docs/dibs/credit-wall-api).
+To submit from a coding agent instead, see [MCP setup](#submit-ideas-from-your-agent).
+
+## Maintainer and developer reference
+
+The following sections cover running Dibs, its API, and local development,
+not prerequisites for using the hosted site.
## Architecture
@@ -297,6 +327,12 @@ still call `prompts/get` directly.
## Development
+Requires Git and Go 1.27 or newer. Clone
+`https://github.com/hivecommons/dibs.git`, then run these commands from the
+checkout's top-level `dibs` folder. For authenticated local use, follow
+[Running locally with sign-in](#running-locally-with-sign-in); fakehub is
+for development only, not a production authentication service.
+
```sh
go test -race -count=1 ./...
go run ./cmd/dibs # serves on :8080 at /
diff --git a/docs/content/dibs/roadmap.md b/docs/content/dibs/roadmap.md
index eee1ed6d..fcbfe16e 100644
--- a/docs/content/dibs/roadmap.md
+++ b/docs/content/dibs/roadmap.md
@@ -2,7 +2,8 @@
# Dibs Roadmap
-Dibs is a marketplace for turning public ideas into claimable, settleable work.
+Dibs connects idea contributors to projects. A project can accept an idea;
+**settlement** records the resulting GitHub issue URL, not a completed implementation.
This roadmap gives maintainers, ideators, and adopters a shared view of where
the project is headed next.
@@ -10,23 +11,23 @@ the project is headed next.
- End-to-end ideation, matching, and settlement flows for GitHub issue-backed
work.
-- Intake support for common document formats with bounded PDF extraction and
- malformed-input recovery.
+- Idea intake from common document formats, with limits on PDF text extraction
+ and recovery when an uploaded document is malformed.
- Repository registry, credit-wall mechanics, notifications, and guided server
flows covered by automated tests.
## Near term
1. **Trust and abuse controls:** improve spam resistance for public idea intake,
- including clearer moderation states and rate/quality signals for ideators.
+ including clearer moderation states and rate/quality signals for idea contributors.
See the [proposed idea-intake trust design](https://github.com/hivecommons/dibs/blob/main/docs/idea-intake-trust.md);
implementation follows in separate issues.
-2. **Forge and repo coverage:** define what it takes to support non-GitHub issue
+2. **Forge and repo coverage:** define what it takes to support issue hosts other than GitHub
targets while growing the repository registry beyond hive-managed projects.
3. **Credit-wall integrations:** the [consumer API reference](/docs/dibs/credit-wall-api)
documents eligibility, settlement state, rewards, and current experimental
stability levels. Next, define compatibility guarantees and stabilize the
- downstream contract.
+ promises made to applications that consume this API.
## Deferred / non-goals
diff --git a/docs/content/hive/acmm-policy-matrix.md b/docs/content/hive/acmm-policy-matrix.md
index 169683e3..200beaf2 100644
--- a/docs/content/hive/acmm-policy-matrix.md
+++ b/docs/content/hive/acmm-policy-matrix.md
@@ -125,6 +125,39 @@ Existing autonomous lanes can open issues, create PRs, and auto-merge on green C
| operations (paused) | full | `operations-full.md` |
| brainstorm | advisory | `brainstorm-advisory.md` |
+## What hive will say on GitHub at each level
+
+ACMM constrains *agents*; the platform sweeps (sentinel, hold guard, stage
+labels, supersession, …) are not agents and used to run unconditionally
+over `project.repos`. Since the write-back containment gate
+(`pkg/writeback`, see
+[operator-reference.md](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md#what-hive-writes-to-github-and-when))
+each *unsolicited* write-back kind has its own floor, and on a PR hive did
+not open it additionally needs the review opt-in label
+(`review.require_labels`, default `hive-review`). Hive-authored PRs and
+issues, `/hive` command replies and dashboard-triggered actions are never
+gated. Defaults (override under `writeback.kinds.`):
+
+| Write-back kind | L1–L3 | L4 | L5 | L6 |
+| --- | --- | --- | --- | --- |
+| `sentinel` comment + `sentinel-alert` label | dashboard only | dashboard only | opt-in label only | all PRs |
+| `hold_gate_notice` (mergeability / drift) | — | hive PRs only | hive PRs only | hive PRs only |
+| `stage_label`, `needs_decision_comment` on foreign PRs | internal tracking | internal tracking | opt-in label | opt-in label |
+| `supersession_notice`, `duplicate_fix_notice`, `reporter_trust_hold`, `covered_by_pr_label`, `issue_close`, `parent_issue_edit`, `gate_comment` | — | — | opt-in label | opt-in label |
+| `supersession_close`, `contributor_adopt`, `ci_rerun` | — | — | — | opt-in label |
+| `verdict_label` | L3: opt-in label | opt-in label | opt-in label | opt-in label |
+| `claim_comment` | — | opt-in label | opt-in label | opt-in label |
+| `welcome_comment` | — | — | opt-in label | all items |
+| `issue_file` (new hive-authored issues) | L3+ | yes | yes | yes |
+
+In practice: **L4 means hive files issues and talks only on its own PRs**;
+**L5 adds comments and labels on PRs a maintainer labelled `hive-review`**;
+**L6 lets sentinel and welcome comments reach unlabelled PRs**. A repo
+listed only so an agent can file regression issues should carry
+`writeback.per_repo..mode: issues-only`, which holds at every level.
+`backfill: false` (default) additionally keeps every kind off items that
+predate hive's first sight of, or write access to, the repo.
+
## Example config alignment
`src/hive.yaml.example` uses the built-in `quality` lane for automated test suites, coverage gates, and regression checks. There is no built-in `tester` lane in the ACMM packs, known-agent defaults, or shipped policy templates; operators who want a separate tester must define it as a custom agent with explicit metadata and a policy template.
diff --git a/docs/content/hive/agent-configuration.md b/docs/content/hive/agent-configuration.md
index c883c99b..e4c061fd 100644
--- a/docs/content/hive/agent-configuration.md
+++ b/docs/content/hive/agent-configuration.md
@@ -129,7 +129,7 @@ older overlays, but new configs should use `bob.session_label`.
# launch_cmd, prompt, tools, and skills at launch.
```
-The dashboard API can update this field through [`POST /api/effort/{agent}/{effort}`](https://github.com/hivecommons/hive/blob/v5/src/docs/api-reference.md#agents-and-controls).
+The dashboard API can update this field through [`POST /api/effort/{agent}/{effort}`](https://github.com/hivecommons/hive/blob/v5/src/docs/api-reference/agents-and-controls.md).
The dashboard shows a reasoning-effort dropdown next to the model picker for every method that has an effort control (`codex`, `agy`, `claude`, and `omp`). The value is validated at set time against the method's own set, so a `codex`-only level such as `minimal` is refused for a `claude` agent rather than stored and dropped at launch. For `omp`, the dropdown is per selected model: Hive reads `omp models --json` and uses that model's `thinking` levels, hiding the control for models that report no thinking support. Picking `default` clears the stored value; for `claude` and `omp` that means no effort flag at all and the CLI's own default. The dropdown persists through the same path as the model picker (`hive.yaml` plus the per-agent overlay under `agents_dir`), which is the durable path on a hosted spoke: a hand edit of the agent's file is reverted by the next sync, the dashboard write is not. Each dropdown change is recorded in the audit trail as `set_reasoning_effort` with the new value, like the other per-agent setting changes. The effort the agent was actually launched with is what the run rows and the `- hive:` PR trailer report, so a `claude` agent at `xhigh` shows `effort=xhigh` there and an agent with no stored effort shows none.
@@ -1074,6 +1074,31 @@ The `hive/tracking` label also satisfies the tracker-issue check used by `/contr
**Not the same thing as the contribute filters.** `hub.contribute_labels_mode` + its label list gate which issues are *handed out to external contributors* over `/contribute` — they have never gated the hive's **own** agents, so an operator who allow-listed a queue label there (a common setup for routing labeled issues to contributors) still had a hive whose own scanner could work every other open issue. `project.issue_filter.require_labels` is the agent-side gate; configure both if you want the same label to govern both lanes.
+## Write-back containment (`writeback`)
+
+ACMM levels and label policy decide what *agents* may do. The `writeback`
+block decides what the *platform* may say on GitHub without being asked:
+sentinel alerts, hold-gate notices, stage labels, supersession and
+duplicate notices, reporter-trust holds, and so on. Each kind has a minimum
+level and, on PRs hive did not open, needs the review opt-in label
+(`review.require_labels`, fallback `hive-review`):
+
+```yaml
+writeback:
+ unsolicited:
+ min_level: 5
+ require_labels: true
+ backfill: false
+ per_repo:
+ torch-spyre/torch-spyre: { mode: issues-only }
+ kinds:
+ sentinel: { min_level: 5, require_label: true, unlabeled_min_level: 6 }
+```
+
+Agent kicks on items already labelled for hive, hive's own PRs and issues,
+and `/hive` command replies are never gated. The default matrix per level
+is in [acmm-policy-matrix.md](/docs/hive/acmm-policy-matrix#what-hive-will-say-on-github-at-each-level).
+
## When to add what
| Add… | …when |
diff --git a/docs/content/hive/contributor-relay.md b/docs/content/hive/contributor-relay.md
index 310ba332..f4c8c436 100644
--- a/docs/content/hive/contributor-relay.md
+++ b/docs/content/hive/contributor-relay.md
@@ -1263,7 +1263,7 @@ That last line types a fresh prompt into a pane whose CLI is still mid-turn, int
**A blip is not an abandonment until the grace window says so.** The re-adoption check above is zero-width for a `1006`: the hub processes the dead socket the instant its read fails, while the relay does not even start redialing for `BASE_RECONNECT_DELAY_MS` (1 s). So on a real network blip the release always won the race, and every blip wrote an `abandoned_disconnect` run row plus a "released: connection lost" activity row for a task that resumed one second later — 11 phantom rows across 17 completed tasks in one 3.5-hour run ([#7838](https://github.com/hivecommons/hive/issues/7838)). The visible booking now waits `HIVE_CONTRIBUTE_DISCONNECT_GRACE` (default 5 s); at the deadline the hub re-runs the re-adoption check and also asks whether a completion/failure row has since been written for the task id, and writes nothing if either is true. The short release cooldown — the double-assign hedge — is still booked immediately and still withdrawn by the resume, so the window is covered either way.
-**Who wrote the PR does not change the stand-in rule.** Clanker PRs are adoptable on disconnect regardless of the owner's repo role: once the nudge window passes with the contributor's hive gone, the hub's hive adopts a maintainer's `author/clanker` PR exactly as it adopts anyone else's (`governor.labels.stage.adoption.maintainer_policy`, default `adopt`; `ask` parks it for an owner's Adopt, `never` opts out).
+**Who wrote the PR does not change the stand-in rule.** Clanker PRs are adoptable on disconnect regardless of the owner's repo role: once the nudge window passes with the contributor's clanker gone, the hub's hive adopts a maintainer's `author/clanker` PR exactly as it adopts anyone else's (`governor.labels.stage.adoption.maintainer_policy`, default `adopt`; `ask` parks it for an owner's Adopt, `never` opts out).
**The lease has to outlive the hub process.** Leases used to live only in the hub's memory, so a restart — which self-upgrade rolls ([#5391](https://github.com/hivecommons/hive/issues/5391)) make routine rather than rare — emptied the registry while every relay carried on working. After the roll *no* in-flight resume could match, and each one produced the same four lines above, this time for a reason the relay could do nothing about ([#5681](https://github.com/hivecommons/hive/issues/5681)). Observed 2026-09-02: revoked at 14:24:40, the same issue reassigned to the same relay at 14:24:44, discarding two and a half minutes of a turn that was progressing normally. The registry is now written to `/data/contributors/task-leases.json` (owner-only) on every assignment, renewal and release, and read back at startup, so a restart is just a longer-than-usual disconnect.
diff --git a/docs/content/hive/dashboard-sections.md b/docs/content/hive/dashboard-sections.md
index 2652a64a..c305482d 100644
--- a/docs/content/hive/dashboard-sections.md
+++ b/docs/content/hive/dashboard-sections.md
@@ -46,7 +46,8 @@ How many issues and pull requests Hive is tracking right now, and how many of th
- **Total open issues** and **Total open PRs** count every open issue and every open pull request in the repositories you selected. Draft pull requests count too.
- When the ForgeState cache (`forgestate.enabled: true`) has finished its first full scan of a repository, the two totals are read from that cache instead of from the GitHub search API. The cache is kept current by webhook events and its own low-rate refresh, so the totals do not cost a `/search/issues` request on every status refresh. The actionable, held, blocked and outside parts still come from Hive's own classification; whatever the cache counts that the classification has not placed yet lands in Outside's **hold-adjacent/other** part, so the sum always matches the total.
- If GitHub rate-limits or fails the count request, the tiles keep the last good numbers and show **Counts stale since — serving last-good** instead of dropping to zero. The status payload carries the same moment as `staleSince` (shared with the enumeration last-good marker), and each repository as `countsStale` / `countsStaleSince`.
-- The two totals are the count of the items actually listed in the status snapshot (one canonical `openItems` set per refresh, built server-side from the enumeration or ForgeState cache items plus the last-good copy). The small `issues/PRs` counter beside **Overview** in the left menu and the **Hive activity monitor** list and header are read from that same set, so all three always agree; hover the counter to see the whole sum.
+- The two totals are the count of the items actually listed in the status snapshot (one canonical `openItems` set per refresh, built server-side from the enumeration or ForgeState cache items plus the last-good copy). The small `issues/PRs` counter beside **Overview** in the left menu and the **Hive activity monitor** list and header are read from that same set; hover the counter to see the whole sum.
+- **Why the two left-menu counters differ.** The **Overview** counter prints that set as *items*: `33/13` is 33 open issues and 13 open pull requests. The **Hive activity monitor** counter prints the same set as *rows*: every open issue is a row, and a pull request gets its own row only when it is not drawn under a listed issue (one with no linked issue, or whose issue is closed or not listed). So `37` beside the activity monitor is `33 issues + 4 PRs on their own row`, with the other 9 PRs shown on their issue's row. Hover either counter: the Overview tooltip names its two halves and says how the activity-monitor counter is made; the activity-monitor tooltip and the list header spell out the sum (`37 rows (33 issues + 4 PRs on their own row) · 3 need attention · 9 PRs shown on their issue's row`). Neither number is wrong; they are two views of one population.
- When a repository reports more open items than the snapshot can list (for example a cache total that has run ahead of the item list), the extra are never added to the totals; a small **+N not yet listed** badge beside the tiles names them until the next refresh lists them.
- After a restart Hive serves the snapshot it saved on disk; it is marked stale (**Counts stale since** badge) until the first fresh enumeration lands, so a pre-restart number is never shown as current.
- Each total is written as a sum, for example `10 = 2 actionable + 0 held + 1 blocked + 7 outside`.
@@ -648,7 +649,9 @@ More about the line:
A progress strip per open issue and pull request showing which step of the pipeline it is on and who must act next.
-Fed by `GET /api/stage-tracker`. Every issue and pull request follows the same pipeline, named once in the column header above the list: `Open → Actionable (010) → Claimed (020) → PR in progress (030/040) → CI / Review (050) → Merge gate (060/080/095/100) → ✓ merged`. Each entry is a title line over a thin chevron strip. Only the current step is coloured: it is filled amber and carries the step name in words; once the PR is merged the final ✓ chevron is the lit one, in green. Motion is reserved for what is running or needs a hand — the lit chevron falls into one of three buckets (`stageTrackerMotion()` in `index.html`): **static** (steady): Open, Open · on hold (any hold label wins over every other signal), Actionable (010), Claimed (020), PR in progress / Covered by PR (030/040) by a human author, Merged, and any unrecognised stage; **active** (gentle pulse): the hive is acting — 030/040 by a hive or clanker author, 050 review pending, 060 reviewed, 070 fixing, 074 adopted by hive, 080 merge gate, 095 pre-merge soak, 100 merge queue; **needs-human** (slower amber→red breathe): a person must move — 015 triage needed, 045/085 needs decision, 071 needs author, 072 returned to contributor, 090 ready for a human merge, or a `stalled` / `blocked` / `needs-human` / `needs-decision` / `needs-triage` modifier on any stage. Under `prefers-reduced-motion` nothing animates; a needs-human step keeps a steady red tint so it still stands out. Every other step is a low-contrast shape with a small grey glyph (circle, bolt, lock, branch, eye, shield, check) — passed steps a subtle neutral fill, steps ahead a faint outline. Side stages are not extra steps: `070 Fixing` (the author's move; the `needs-rebase` chip next to it says what is being fixed — the stage labeller always parks a needs-rebase PR at 070/071/072, never at CI / Review), `071 Needs author` (human-authored), `072 Returned to contributor` (clanker-authored) and `074 Adopted by hive` light *CI / Review* with their own name (an adopted PR stays on 074 through the hive's pushes going pending, green or red again, and moves on only when *Merge gate* takes it); `085 Needs human decision` (PR-only) and `090 Ready for human merge` light *Merge gate* with their own name (the 085 tooltip says what must be decided); an issue parked on a person wears `045 Needs human decision` and lights *Actionable* (the issue side) with that name — a pre-045 issue still wearing 085 is drawn the same way until the reconciler re-stages it; an actionable issue no maintainer has looked at yet wears `015 Triage needed` (carrying `needs-triage`, or held in the reporter-trust gate's `reporter_triage` bucket — never derived from who filed it) and lights *Actionable* as "Triage needed" (a human's move) — so the Actionable column reads "Actionable", "Triage needed" or "Needs human decision", and the Open column is untouched. An issue carrying a hold-class label (`hold`, `do-not-merge*`, `hive-pause/`) is never parked: it sits at *Open* as "Open · on hold" even beside `needs-human`, and the reconciler removes any 045 the hold caught it with ([#13012](https://github.com/hivecommons/hive/issues/13012)). The lit chevron ends with an ink-coloured icon of whose move it is — person (human), robot (clanker) or hexagon (hive): the claim holder on Claimed, the PR author on PR in progress and Returned to contributor, hive on CI / Review, Adopted by hive and Ready for auto merge, a human on Needs human decision and Ready for human merge. A row at `090 Ready for human merge` always says *why* a person and not the hive merges it (#14658): the lit word ends `· workflow files`, `· tests removed`, `· manual-merge policy`, `· branch protection`, `· merge method`, `· auto-merge off`, `· untrusted author`, `· unauthorized`, `· reviewer deferred` or `· policy` (payload `human_merge_reason`, a `prstage.HumanMergeReason*` code derived from the stage record's lane reason — no second store), the chevron tooltip leads with the one-sentence `human_merge_reason_text` ("Changes .github/workflows/** — the hive never merges workflow changes …; click Merge on GitHub"), the manual-merge word reads `Awaiting human merge · workflow files · approved by @login`, the search / needs-attention headline carries the same tail, and a PR nested under its issue mirrors `human_approver`, `manual_merge`, `human_merge_reason` and `human_merge_reason_text` onto its `linked_prs` entry so the sub-row says the same. `040 Covered by PR` shares the *PR in progress* column with 030; `060 Reviewed`, `080 Awaiting merge admission` and `100 Merge queue` all light *Merge gate* as "Ready for auto merge" (the chevron tooltip says "not yet admitted — " for 060/080 and "admitted, merging" for 100); `095 Pre-merge soak` lights the same column as "Soaking" (hive's move; tooltip "admitted, soaking — Xm left", headline "Pre-merge soak — Xm left" from the payload's `soak_remaining_seconds`, aged from the lane's last sweep), so an admitted PR inside its soak window is no longer drawn as merging; the chevron's own tooltip names the exact stage. Covering pull requests nest under their issue. An issue with no PR yet draws the whole strip like any other row, lit at Open, Actionable or Claimed with the steps ahead faint. Once exactly one PR covers it, issue and PR are a single entry: the issue's `#N title` on the left, `↳ [author icon] #PR title` under the *PR in progress* header on the same title line (the ↳ marks a PR drawn on its issue's row; the PR link's tooltip says so — "linked PR, shown on its issue's row" — and, when two open issues list the same PR, every row but the lowest-numbered issue adds "also under #N"), and one continuous strip — Open, Actionable and Claimed passed, the PR's stage lighting the chevron from PR in progress through CI / Review, Merge gate and ✓; the chip, tooltip and `since` are the PR's. With two or more covering PRs the issue's own strip stops at Claimed — all passed, nothing on the issue line lit. Directly beneath, tighter than the gap between entries, sits one indented sub-row per open covering PR, furthest along first: `[author icon] #N title` at the *PR in progress* column, then that PR's own strip from there on — `PR in progress → CI / Review → Merge gate → ✓` — with its own lit chevron, chip, tooltip and `since`. So an issue with two PRs, one at 030, shows "PR in progress" lit on that sub-row, not on the issue. A PR linked to an open issue in the list has no top-level row of its own; a PR with no linked issue keeps a full-width row and shows the first steps as passed. A PR whose only linked issue is already closed — a superseded PR whose body still says `Fixes #N` after an earlier PR fixed it — keeps its own row with the closed issue in the issue slot: `#N title` as a link, muted and struck through, followed by a small `closed` chip, so the pairing reads at a glance; when the title cannot be resolved the slot says `#N (closed issue)` rather than a bare number (the title comes from the snapshot, or from one cached issue read the cycle makes once per issue — see [Linked issues](https://github.com/hivecommons/hive/blob/v5/src/docs/dashboard.md#pr-review-and-link-signals)). The *Pull requests* filter flattens the list — every PR as a top-level row; *Issues* and *All* show the nested view. Each row is named once on the left; a covering PR appears under the *PR in progress* header as `[author icon] #N title`. A pull request with no linked issue is named once: `[PR icon] [author icon] #N title` on the left over one continuous strip (first steps passed, its own step lit) and nothing in the *PR in progress* column, which is reserved for covering PRs — so a title never appears twice in a row. Pull requests are never issues here: when a number arrives through both the issues feed and the pulls feed the PR record wins and the issue copy is dropped on the server (pairing is keyed by repository, kind and number), and the *Issues* filter never lists a PR. The author icon is the one coloured legend: hive hexagon in blue, clanker robot in purple, human silhouette in teal (the `author/*` kind, inferred from the snapshot when the label is absent; the login is in the `#N` link's tooltip). The right-hand `since` column, in line with the chevron strip, is how long the item has been on its step (red when `stalled`). A plain *Open* step — no stage label yet and not on hold — also carries that age in its own words, "Open · since 3d" ("since 14h" under two days), taken from the same `stage_since` instant the column draws (falling back to the time the item was opened, which the chevron tooltip then marks "(opened)"); past `STAGE_TRACKER_OPEN_STALE_DAYS` (2) the chevron takes a subtle red tint. *Open · hold* never shows the age. An Open item carrying the `blocked` label is drawn the same way as a hold — "Open · blocked" with a ⛔ glyph in the hold's yellow — but keeps the age ("Open · blocked · since 7h"; a stale blocked item is exactly what should stand out, [#14194](https://github.com/hivecommons/hive/issues/14194)) and its chevron tooltip says why: "blocked on owner/repo#N" when the payload names the blocker (`blocked_on`), else "carries the blocked label"; when an item is both held and blocked, hold wins — it is a human instruction. A draft PR — the forge's draft bit, or prow's `do-not-merge/work-in-progress` label — reads "Draft · since 18h" in the Open cell with a ✎ glyph in the muted draft tint, never turns stale (the hive's review and merge lanes skip drafts, so the wait is the author's, not a stuck pipeline), and wears a muted `draft` chip whose tooltip says the lanes skip it until the author marks it ready; a hold or blocked label still wins the cell, with `draft` folded into the chip's `+N` ([#14572](https://github.com/hivecommons/hive/issues/14572)). At most one muted chip sits at the right end of the title line, directly over the strip's ✓ cap (on the last line when the title wraps; a hairline above each entry and a hover tint over the whole entry keep chip and `since` unambiguously with their own row — [#14640](https://github.com/hivecommons/hive/issues/14640)): the most important health modifier (`stalled`, `regressed`, `hold`, `needs-human`, `needs-decision` — shown as "needs human decision" —, `needs-triage` — shown as "triage needed" (amber) on an issue still carrying the `needs-triage` label before the claim (010, 017, 045; at 015 the lit chevron already says so); a `needs-triage` left on a claimed or later issue (020+) is stale, never shown as a chip or stage text, and reported instead as `stale_labels` in the payload with a muted *Stale label* line in the hover tooltip until the stage sweep removes it (hivecommons/hive#14586) —, `needs-rebase`, `blocked`, `draft` — set by the PR's draft bit or prow's `do-not-merge/work-in-progress` label, which the tracker reads as a draft rather than a hold) with a `+N` for the rest. Everything else — the reason sentence, who must act, nudges sent, the repository, all flags and the last stage hops — is in the hover tooltip on the item's `#N` link (on a nested sub-row, on its `#N` PR link). Hover text in a row lives only on the `#N` link, the covering-PR link, the title and the lit chevron (its stage, and whose move it is); passed and upcoming chevrons, the kind and author icons, chips and the `since` column have none. Title lines wrap rather than push the strip out of alignment: the issue title stays before the *PR in progress* column (a clear gap always separates it from the PR entry, which starts at the column edge) and the PR title before the chip; hovering a title shows the full text. Rows are sorted furthest-along first by default — a covered issue ranks by its furthest-along PR — then, within a column, attention-first: rows a person must act on now (any shown pill — `stalled`, `unauthorized`, `needs rebase`, `needs human`, … — or a human-gated stage: 045, 071, 072, 085, 090) come first, plain rows next, and rows nobody should act on — `hold` (incl. `do-not-merge/*`), `draft`, and a PR standing in for a closed issue — sink to the bottom of the column; within a tier the longest on the step comes first, then repo and number. Pills the server holds back until the merge gate (`deferred_modifiers`) never promote a row. The same tiering orders the PR sub-rows under an issue, and the server pre-sorts `GET /api/stage-tracker` the same way (stage, tier, wait, repo, number) so chat `/prs` and the hub read the same order. *Sort* also offers longest waiting (tier first, then wait), newest and number (no tiering); the arrow beside it flips the column / age / repo / number keys but never the tier — attention stays on top in both directions, as its tooltip says ("Attention-first within each column; arrow flips age order"). Every open issue and pull request of the watched repositories is listed — actionable, held, claimed, in progress, covered by a PR, filtered by a `needs-*` label, reporter-triage, draft and exempt-labelled alike; an item without a stage label or journal entry sits at *Open*, and one in a scanner bucket with no label yet is placed from the bucket (claimed → 020, in progress → 030, covered → 040). Filters work on the cached payload in the browser and persist across reloads (the page number does not): kind chips, *Repo*, a search box (`#number` / number / title substring, also matching a covering PR), *Since* (any, over 1 hour / 1 day / 1 week on the current step, or past the stage's configured dwell — stalled items always qualify), *Author* kind, *Needs attention* (anything with a drawn health modifier or on 071 / 072 / 085 / 090 — merge-gate pills held back before the gate do not count), and label chips built from every label present, each with its count: a **🙋 Needs human** facet first (attention-coloured; it matches any item carrying a label of the needs-human set — the server's `needs_human_labels`, the same list as the Projects *Needs human* preset: `stage/045-needs-decision`, `stage/085-needs-decision`, `needs-decision`, `needs-direction`, `needs-spec`, `needs-signal`, `needs-human` and `needs-human/*`, the hold labels, `stage/071-needs-author`, `sentinel-alert`, `needs-triage`; its count is the number of distinct items, not the sum of label counts), then every `stage/NNN-*` label in numerical order, then all other labels alphabetically (case-insensitive). When the row would overflow, the **+N more** button opens an in-app *All labels* dialog listing every label in the same order with counts, a type-to-filter box, the same toggles the chips use and a *Clear* button; a label selected there is promoted into the visible row. Chips of one family OR, families AND; *Needs human* ANDs with the label chips. *Clear filters* shows while anything is narrowed. The list is paged — 25 / 50 / 100 rows, default 50 — with Prev / Next and "showing X–Y of N (M filtered out)" above and below; the header count "N rows · M need attention" is for the filtered set, the **sidebar badge is the row count of the default view** (owner: the tracker counts rows, not items — a covering PR drawn on its issue's row is not a row of its own, and an issue with several PRs already has several), and both come from the same `stageTrackerCountsText()` call so they cannot disagree. When rows sit below the payload's open issues + PRs, the "showing" line's tooltip and the badge's footnote say why — "2 PRs shown on their issue's row" — rather than printing a second total.the chevron's own tooltip names the exact stage. Covering pull requests nest under their issue. An issue with no PR yet draws the whole strip like any other row, lit at Open, Actionable or Claimed with the steps ahead faint. Once exactly one PR covers it, issue and PR are a single entry: the issue's `#N title` on the left, `[author icon] #PR title` under the *PR in progress* header on the same title line, and one continuous strip — Open, Actionable and Claimed passed, the PR's stage lighting the chevron from PR in progress through CI / Review, Merge gate and ✓; the chip, tooltip and `since` are the PR's. With two or more covering PRs the issue's own strip stops at Claimed — all passed, nothing on the issue line lit. Directly beneath, tighter than the gap between entries, sits one indented sub-row per open covering PR, furthest along first: `[author icon] #N title` at the *PR in progress* column, then that PR's own strip from there on — `PR in progress → CI / Review → Merge gate → ✓` — with its own lit chevron, chip, tooltip and `since`. So an issue with two PRs, one at 030, shows "PR in progress" lit on that sub-row, not on the issue. A PR linked to an open issue in the list has no top-level row of its own; a PR with no linked issue keeps a full-width row and shows the first steps as passed. The *Pull requests* filter flattens the list — every PR as a top-level row; *Issues* and *All* show the nested view. Each row is named once on the left; a covering PR appears under the *PR in progress* header as `[author icon] #N title`. A pull request with no linked issue is named once: `[PR icon] [author icon] #N title` on the left over one continuous strip (first steps passed, its own step lit) and nothing in the *PR in progress* column, which is reserved for covering PRs — so a title never appears twice in a row. Pull requests are never issues here: when a number arrives through both the issues feed and the pulls feed the PR record wins and the issue copy is dropped on the server (pairing is keyed by repository, kind and number), and the *Issues* filter never lists a PR. The author icon is the one coloured legend: hive hexagon in blue, clanker robot in purple, human silhouette in teal (the `author/*` kind, inferred from the snapshot when the label is absent; the login is in the `#N` link's tooltip). The right-hand `since` column, in line with the chevron strip, is how long the item has been on its step (red when `stalled`). At most one muted chip sits at the right end of the title line, directly over the strip's ✓ cap (on the last line when the title wraps; a hairline above each entry and a hover tint over the whole entry keep chip and `since` unambiguously with their own row — [#14640](https://github.com/hivecommons/hive/issues/14640)): the most important health modifier (`stalled`, `regressed`, `hold`, `needs-human`, `needs-decision` — shown as "needs human decision" —, `needs-rebase`, `blocked`, `draft` — the PR's draft bit or prow's `do-not-merge/work-in-progress` label) with a `+N` for the rest. A `regressed` chip is red only when the row is parked on a person (a human-gate modifier or stage 045 / 071 / 072 / 085 / 090); on a hive-driven stage with nothing waiting on a human it draws as a muted dashed `stage ↓` pill, yields the lead slot to any other modifier, keeps the row out of **Needs attention**, and its tooltip ends "the hive is handling it — no action needed; clears on the next forward step". The marker is set by a genuine backward hop (a lapsed claim 020 → 010, a 030 ↔ 020 flicker and a 040 → 010 after a partially-addressing merge are not regressions) and clears on the first forward hop; see "The regressed marker" in `labels-and-control-signals.md`. Everything else — the reason sentence, who must act, nudges sent, the repository, all flags and the last stage hops — is in the hover tooltip on the item's `#N` link (on a nested sub-row, on its `#N` PR link). Hover text in a row lives only on the `#N` link, the covering-PR link, the title and the lit chevron (its stage, and whose move it is); passed and upcoming chevrons, the kind and author icons, chips and the `since` column have none. Title lines wrap rather than push the strip out of alignment: the issue title stays before the *PR in progress* column (a clear gap always separates it from the PR entry, which starts at the column edge) and the PR title before the chip; hovering a title shows the full text. Rows are sorted furthest-along first by default — a covered issue ranks by its furthest-along PR — then, within a column, attention-first: rows a person must act on now (any shown pill — `stalled`, `unauthorized`, `needs rebase`, `needs human`, … — or a human-gated stage: 045, 071, 072, 085, 090) come first, plain rows next, and rows nobody should act on — `hold` (incl. `do-not-merge/*`), `draft`, and a PR standing in for a closed issue — sink to the bottom of the column; within a tier the longest on the step comes first, then repo and number. Pills the server holds back until the merge gate (`deferred_modifiers`) never promote a row. The same tiering orders the PR sub-rows under an issue, and the server pre-sorts `GET /api/stage-tracker` the same way (stage, tier, wait, repo, number) so chat `/prs` and the hub read the same order. *Sort* also offers longest waiting (tier first, then wait), newest and number (no tiering); the arrow beside it flips the column / age / repo / number keys but never the tier — attention stays on top in both directions, as its tooltip says ("Attention-first within each column; arrow flips age order"). Every open issue and pull request of the watched repositories is listed — actionable, held, claimed, in progress, covered by a PR, filtered by a `needs-*` label, reporter-triage, draft and exempt-labelled alike; an item without a stage label or journal entry sits at *Open*, and one in a scanner bucket with no label yet is placed from the bucket (claimed → 020, in progress → 030, covered → 040). Filters work on the cached payload in the browser and persist across reloads (the page number does not): kind chips, *Repo*, a search box (`#number` / number / title substring, also matching a covering PR), *Since* (any, over 1 hour / 1 day / 1 week on the current step, or past the stage's configured dwell — stalled items always qualify), *Author* kind, *Needs attention* (anything with a drawn health modifier or on 071 / 072 / 085 / 090 — merge-gate pills held back before the gate do not count), and label chips built from every label present, each with its count: a **🙋 Needs human** facet first (attention-coloured; it matches any item carrying a label of the needs-human set — the server's `needs_human_labels`, the same list as the Projects *Needs human* preset: `stage/045-needs-decision`, `stage/085-needs-decision`, `needs-decision`, `needs-direction`, `needs-spec`, `needs-signal`, `needs-human` and `needs-human/*`, the hold labels, `stage/071-needs-author`, `sentinel-alert`, `needs-triage`; its count is the number of distinct items, not the sum of label counts), then every `stage/NNN-*` label in numerical order, then all other labels alphabetically (case-insensitive). When the row would overflow, the **+N more** button opens an in-app *All labels* dialog listing every label in the same order with counts, a type-to-filter box, the same toggles the chips use and a *Clear* button; a label selected there is promoted into the visible row. Chips of one family OR, families AND; *Needs human* ANDs with the label chips. *Clear filters* shows while anything is narrowed. The list is paged — 25 / 50 / 100 rows, default 50 — with Prev / Next and "showing X–Y of N (M filtered out)" above and below; the header reads "N items (I issues / P PRs) · M need attention" — N, I and P are the whole list, the same `openItems` set the Overview tile and its left-menu counter print, so N always equals the tile's **Total open issues + Total open PRs**; a "· X match filters" note follows while a filter is on. The sidebar counter beside **Hive activity monitor** is that same N — total open items, written in the same render pass as the header, so the two cannot drift apart; how many need attention is the warn-coloured header suffix and the counter's hover tooltip (counted over every item, filters or not). When covering pull requests are nested under their issue the "showing X–Y of R" line counts rows and adds "(P nested under their issues)" so R never reads as a different total. The small monospace strip beside the count — `engine enforce · N anomalies` — is the central stage engine ([ADR-0025](/docs/hive/adr/0025-central-stage-engine)): `engine` names the current `governor.labels.stage.engine.mode` (green `enforce`, neutral `shadow` = paused, nothing written; hover for what the mode does and the derivation count), `anomalies` counts stage moves the engine's admission table did not expect (red when non-zero). The same numbers are `/api/status.stageEngine`; the mode is changed under **Settings → Labels → Central stage engine mode** and the operator notes are in [operator-reference.md](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md#central-stage-engine-rollout). "updated Ns ago · cache" says how old the server-built payload is: the spoke rebuilds it at most every `dashboard.stage_tracker.cache_ttl_s` (default `governor.eval_interval_s`) from its existing snapshot and serves it with an `ETag`, so the browser's 30-second poll is a 304 and no request ever reaches GitHub (`/api/gh-rate-limits` lists the caller `dashboard:stage-tracker` with nothing charged). See [Stage labels](/docs/hive/labels-and-control-signals#stage-labels-stagennn--the-pipeline-state-machine).
+**The counter beside the section name.** The left-menu counter and the header lead (`37 rows (33 issues + 4 PRs on their own row) · 3 need attention`) count *rows* of the default view, not items: every open issue is a row; a pull request is its own row only when it is not drawn under a listed issue. The Overview counter (`33/13`) prints the same open set as issues/PRs, so the two differ by exactly the pull requests that share their issue's row (`9 PRs shown on their issue's row` in the tooltip). The header and tooltip also name the PRs sharing issue rows. Filters and the kind tabs narrow the list and the `showing X–Y of N` line, never the counter.
+
+Fed by `GET /api/stage-tracker`. Every issue and pull request follows the same pipeline, named once in the column header above the list: `Open → Actionable (010) → Claimed (020) → PR in progress (030/040) → CI / Review (050) → Merge gate (060/080/095/100) → merged`; the right-most column is headed by a history mark (clock + counter-clockwise arrow, hover "History — decisions and actions on this item"), not a ✓. Each entry is a title line over a thin chevron strip. Only the current step is coloured: it is filled amber and carries the step name in words; the final cap chevron wears the muted history mark until the PR merges, when it becomes the lit green ✓. Motion is reserved for what is running or needs a hand — the lit chevron falls into one of three buckets (`stageTrackerMotion()` in `index.html`): **static** (steady): Open, Open · on hold (any hold label wins over every other signal), Actionable (010), Claimed (020), PR in progress / Covered by PR (030/040) by a human author, Merged, and any unrecognised stage; **active** (gentle pulse): the hive is acting — 030/040 by a hive or clanker author, 050 review pending, 060 reviewed, 070 fixing, 074 adopted by hive, 080 merge gate, 095 pre-merge soak, 100 merge queue; **needs-human** (slower amber→red breathe): a person must move — 015 triage needed, 045/085 needs decision, 071 needs author, 072 returned to contributor, 090 ready for a human merge, or a `stalled` / `blocked` / `needs-human` / `needs-decision` / `needs-triage` modifier on any stage. Under `prefers-reduced-motion` nothing animates; a needs-human step keeps a steady red tint so it still stands out. Every other step is a low-contrast shape with a small grey glyph (circle, bolt, lock, branch, eye, shield, check) — passed steps a subtle neutral fill, steps ahead a faint outline. Side stages are not extra steps: `070 Fixing` (the author's move; the `needs-rebase` chip next to it says what is being fixed — the stage labeller always parks a needs-rebase PR at 070/071/072, never at CI / Review), `071 Needs author` (human-authored), `072 Returned to contributor` (clanker-authored) and `074 Adopted by hive` light *CI / Review* with their own name (an adopted PR stays on 074 through the hive's pushes going pending, green or red again, and moves on only when *Merge gate* takes it); `085 Needs human decision` (PR-only) and `090 Ready for human merge` light *Merge gate* with their own name (the 085 tooltip says what must be decided); an issue parked on a person wears `045 Needs human decision` and lights *Actionable* (the issue side) with that name — a pre-045 issue still wearing 085 is drawn the same way until the reconciler re-stages it; an actionable issue no maintainer has looked at yet wears `015 Triage needed` (carrying `needs-triage`, or held in the reporter-trust gate's `reporter_triage` bucket — never derived from who filed it) and lights *Actionable* as "Triage needed" (a human's move) — so the Actionable column reads "Actionable", "Triage needed" or "Needs human decision", and the Open column is untouched. An issue carrying a hold-class label (`hold`, `do-not-merge*`, `hive-pause/`) is never parked: it sits at *Open* as "Open · on hold" even beside `needs-human`, and the reconciler removes any 045 the hold caught it with ([#13012](https://github.com/hivecommons/hive/issues/13012)). The lit chevron ends with an ink-coloured icon of whose move it is — person (human), robot (clanker) or hexagon (hive): the claim holder on Claimed, the PR author on PR in progress and Returned to contributor, hive on CI / Review, Adopted by hive and Ready for auto merge, a human on Needs human decision and Ready for human merge. A row at `090 Ready for human merge` always says *why* a person and not the hive merges it (#14658): the lit word ends `· workflow files`, `· tests removed`, `· manual-merge policy`, `· branch protection`, `· merge method`, `· auto-merge off`, `· untrusted author`, `· unauthorized`, `· reviewer deferred` or `· policy` (payload `human_merge_reason`, a `prstage.HumanMergeReason*` code derived from the stage record's lane reason — no second store), the chevron tooltip leads with the one-sentence `human_merge_reason_text` ("Changes .github/workflows/** — the hive never merges workflow changes …; click Merge on GitHub"), the manual-merge word reads `Awaiting human merge · workflow files · approved by @login`, the search / needs-attention headline carries the same tail, and a PR nested under its issue mirrors `human_approver`, `manual_merge`, `human_merge_reason` and `human_merge_reason_text` onto its `linked_prs` entry so the sub-row says the same. `040 Covered by PR` shares the *PR in progress* column with 030; `060 Reviewed`, `080 Awaiting merge admission` and `100 Merge queue` all light *Merge gate* as "Ready for auto merge" (the chevron tooltip says "not yet admitted — " for 060/080 and "admitted, merging" for 100); `095 Pre-merge soak` lights the same column as "Soaking" (hive's move; tooltip "admitted, soaking — Xm left", headline "Pre-merge soak — Xm left" from the payload's `soak_remaining_seconds`, aged from the lane's last sweep), so an admitted PR inside its soak window is no longer drawn as merging; the chevron's own tooltip names the exact stage. Covering pull requests nest under their issue. An issue with no PR yet draws the whole strip like any other row, lit at Open, Actionable or Claimed with the steps ahead faint. Once exactly one PR covers it, issue and PR are a single entry: the issue's `#N title` on the left, `↳ [author icon] #PR title` under the *PR in progress* header on the same title line (the ↳ marks a PR drawn on its issue's row; the PR link's tooltip says so — "linked PR, shown on its issue's row" — and, when two open issues list the same PR, every row but the lowest-numbered issue adds "also under #N"), and one continuous strip — Open, Actionable and Claimed passed, the PR's stage lighting the chevron from PR in progress through CI / Review, Merge gate and ✓; the chip, tooltip and `since` are the PR's. With two or more covering PRs the issue's own strip stops at Claimed — all passed, nothing on the issue line lit. Directly beneath, tighter than the gap between entries, sits one indented sub-row per open covering PR, furthest along first: `[author icon] #N title` at the *PR in progress* column, then that PR's own strip from there on — `PR in progress → CI / Review → Merge gate → ✓` — with its own lit chevron, chip, tooltip and `since`. So an issue with two PRs, one at 030, shows "PR in progress" lit on that sub-row, not on the issue. A PR linked to an open issue in the list has no top-level row of its own; a PR with no linked issue keeps a full-width row and shows the first steps as passed. A PR whose only linked issue is already closed — a superseded PR whose body still says `Fixes #N` after an earlier PR fixed it — keeps its own row with the closed issue in the issue slot: `#N title` as a link, muted and struck through, followed by a small `closed` chip, so the pairing reads at a glance; when the title cannot be resolved the slot says `#N (closed issue)` rather than a bare number (the title comes from the snapshot, or from one cached issue read the cycle makes once per issue — see [Linked issues](https://github.com/hivecommons/hive/blob/v5/src/docs/dashboard.md#pr-review-and-link-signals)). The *Pull requests* filter flattens the list — every PR as a top-level row; *Issues* and *All* show the nested view. Each row is named once on the left; a covering PR appears under the *PR in progress* header as `[author icon] #N title`. A pull request with no linked issue is named once: `[PR icon] [author icon] #N title` on the left over one continuous strip (first steps passed, its own step lit) and nothing in the *PR in progress* column, which is reserved for covering PRs — so a title never appears twice in a row. Pull requests are never issues here: when a number arrives through both the issues feed and the pulls feed the PR record wins and the issue copy is dropped on the server (pairing is keyed by repository, kind and number), and the *Issues* filter never lists a PR. The author icon is the one coloured legend: hive hexagon in blue, clanker robot in purple, human silhouette in teal (the `author/*` kind, inferred from the snapshot when the label is absent; the login is in the `#N` link's tooltip). The right-hand `since` column, in line with the chevron strip, is how long the item has been on its step (red when `stalled`). A plain *Open* step — no stage label yet and not on hold — also carries that age in its own words, "Open · since 3d" ("since 14h" under two days), taken from the same `stage_since` instant the column draws (falling back to the time the item was opened, which the chevron tooltip then marks "(opened)"); past `STAGE_TRACKER_OPEN_STALE_DAYS` (2) the chevron takes a subtle red tint. *Open · hold* never shows the age. An Open item carrying the `blocked` label is drawn the same way as a hold — "Open · blocked" with a ⛔ glyph in the hold's yellow — but keeps the age ("Open · blocked · since 7h"; a stale blocked item is exactly what should stand out, [#14194](https://github.com/hivecommons/hive/issues/14194)) and its chevron tooltip says why: "blocked on owner/repo#N" when the payload names the blocker (`blocked_on`), else "carries the blocked label"; when an item is both held and blocked, hold wins — it is a human instruction. A draft PR — the forge's draft bit, or prow's `do-not-merge/work-in-progress` label — reads "Draft · since 18h" in the Open cell with a ✎ glyph in the muted draft tint, never turns stale (the hive's review and merge lanes skip drafts, so the wait is the author's, not a stuck pipeline), and wears a muted `draft` chip whose tooltip says the lanes skip it until the author marks it ready; a hold or blocked label still wins the cell, with `draft` folded into the chip's `+N` ([#14572](https://github.com/hivecommons/hive/issues/14572)). At most one muted chip sits at the right end of the title line, directly over the strip's ✓ cap (on the last line when the title wraps; a hairline above each entry and a hover tint over the whole entry keep chip and `since` unambiguously with their own row — [#14640](https://github.com/hivecommons/hive/issues/14640)): the most important health modifier (`stalled`, `regressed`, `hold`, `needs-human`, `needs-decision` — shown as "needs human decision" —, `needs-triage` — shown as "triage needed" (amber) on an issue still carrying the `needs-triage` label before the claim (010, 017, 045; at 015 the lit chevron already says so); a `needs-triage` left on a claimed or later issue (020+) is stale, never shown as a chip or stage text, and reported instead as `stale_labels` in the payload with a muted *Stale label* line in the hover tooltip until the stage sweep removes it (hivecommons/hive#14586) —, `needs-rebase`, `blocked`, `draft` — set by the PR's draft bit or prow's `do-not-merge/work-in-progress` label, which the tracker reads as a draft rather than a hold) with a `+N` for the rest. Everything else — the reason sentence, who must act, nudges sent, the repository, all flags and the last stage hops — is in the hover tooltip on the item's `#N` link (on a nested sub-row, on its `#N` PR link). Hover text in a row lives only on the `#N` link, the covering-PR link, the title and the lit chevron (its stage, and whose move it is); passed and upcoming chevrons, the kind and author icons, chips and the `since` column have none. Title lines wrap rather than push the strip out of alignment: the issue title stays before the *PR in progress* column (a clear gap always separates it from the PR entry, which starts at the column edge) and the PR title before the chip; hovering a title shows the full text. Rows are sorted furthest-along first by default — a covered issue ranks by its furthest-along PR — then, within a column, attention-first: rows a person must act on now (any shown pill — `stalled`, `unauthorized`, `needs rebase`, `needs human`, … — or a human-gated stage: 045, 071, 072, 085, 090) come first, plain rows next, and rows nobody should act on — `hold` (incl. `do-not-merge/*`), `draft`, and a PR standing in for a closed issue — sink to the bottom of the column; within a tier the longest on the step comes first, then repo and number. Pills the server holds back until the merge gate (`deferred_modifiers`) never promote a row. The same tiering orders the PR sub-rows under an issue, and the server pre-sorts `GET /api/stage-tracker` the same way (stage, tier, wait, repo, number) so chat `/prs` and the hub read the same order. *Sort* also offers longest waiting (tier first, then wait), newest and number (no tiering); the arrow beside it flips the column / age / repo / number keys but never the tier — attention stays on top in both directions, as its tooltip says ("Attention-first within each column; arrow flips age order"). Every open issue and pull request of the watched repositories is listed — actionable, held, claimed, in progress, covered by a PR, filtered by a `needs-*` label, reporter-triage, draft and exempt-labelled alike; an item without a stage label or journal entry sits at *Open*, and one in a scanner bucket with no label yet is placed from the bucket (claimed → 020, in progress → 030, covered → 040). Filters work on the cached payload in the browser and persist across reloads (the page number does not): kind chips, *Repo*, a search box (`#number` / number / title substring, also matching a covering PR), *Since* (any, over 1 hour / 1 day / 1 week on the current step, or past the stage's configured dwell — stalled items always qualify), *Author* kind, *Needs attention* (anything with a drawn health modifier or on 071 / 072 / 085 / 090 — merge-gate pills held back before the gate do not count), and label chips built from every label present, each with its count: a **🙋 Needs human** facet first (attention-coloured; it matches any item carrying a label of the needs-human set — the server's `needs_human_labels`, the same list as the Projects *Needs human* preset: `stage/045-needs-decision`, `stage/085-needs-decision`, `needs-decision`, `needs-direction`, `needs-spec`, `needs-signal`, `needs-human` and `needs-human/*`, the hold labels, `stage/071-needs-author`, `sentinel-alert`, `needs-triage`; its count is the number of distinct items, not the sum of label counts), then every `stage/NNN-*` label in numerical order, then all other labels alphabetically (case-insensitive). When the row would overflow, the **+N more** button opens an in-app *All labels* dialog listing every label in the same order with counts, a type-to-filter box, the same toggles the chips use and a *Clear* button; a label selected there is promoted into the visible row. Chips of one family OR, families AND; *Needs human* ANDs with the label chips. *Clear filters* shows while anything is narrowed. The list is paged — 25 / 50 / 100 rows, default 50 — with Prev / Next and "showing X–Y of N (M filtered out)" above and below; the header count "N rows · M need attention" is for the filtered set, the **sidebar badge is the row count of the default view** (owner: the tracker counts rows, not items — a covering PR drawn on its issue's row is not a row of its own, and an issue with several PRs already has several), and both come from the same `stageTrackerCountsText()` call so they cannot disagree. When rows sit below the payload's open issues + PRs, the "showing" line's tooltip and the badge's footnote say why — "2 PRs shown on their issue's row" — rather than printing a second total.the chevron's own tooltip names the exact stage. Covering pull requests nest under their issue. An issue with no PR yet draws the whole strip like any other row, lit at Open, Actionable or Claimed with the steps ahead faint. Once exactly one PR covers it, issue and PR are a single entry: the issue's `#N title` on the left, `[author icon] #PR title` under the *PR in progress* header on the same title line, and one continuous strip — Open, Actionable and Claimed passed, the PR's stage lighting the chevron from PR in progress through CI / Review, Merge gate and ✓; the chip, tooltip and `since` are the PR's. With two or more covering PRs the issue's own strip stops at Claimed — all passed, nothing on the issue line lit. Directly beneath, tighter than the gap between entries, sits one indented sub-row per open covering PR, furthest along first: `[author icon] #N title` at the *PR in progress* column, then that PR's own strip from there on — `PR in progress → CI / Review → Merge gate → ✓` — with its own lit chevron, chip, tooltip and `since`. So an issue with two PRs, one at 030, shows "PR in progress" lit on that sub-row, not on the issue. A PR linked to an open issue in the list has no top-level row of its own; a PR with no linked issue keeps a full-width row and shows the first steps as passed. The *Pull requests* filter flattens the list — every PR as a top-level row; *Issues* and *All* show the nested view. Each row is named once on the left; a covering PR appears under the *PR in progress* header as `[author icon] #N title`. A pull request with no linked issue is named once: `[PR icon] [author icon] #N title` on the left over one continuous strip (first steps passed, its own step lit) and nothing in the *PR in progress* column, which is reserved for covering PRs — so a title never appears twice in a row. Pull requests are never issues here: when a number arrives through both the issues feed and the pulls feed the PR record wins and the issue copy is dropped on the server (pairing is keyed by repository, kind and number), and the *Issues* filter never lists a PR. The author icon is the one coloured legend: hive hexagon in blue, clanker robot in purple, human silhouette in teal (the `author/*` kind, inferred from the snapshot when the label is absent; the login is in the `#N` link's tooltip). The right-hand `since` column, in line with the chevron strip, is how long the item has been on its step (red when `stalled`). At most one muted chip sits at the right end of the title line, directly over the strip's ✓ cap (on the last line when the title wraps; a hairline above each entry and a hover tint over the whole entry keep chip and `since` unambiguously with their own row — [#14640](https://github.com/hivecommons/hive/issues/14640)): the most important health modifier (`stalled`, `regressed`, `hold`, `needs-human`, `needs-decision` — shown as "needs human decision" —, `needs-rebase`, `blocked`, `draft` — the PR's draft bit or prow's `do-not-merge/work-in-progress` label) with a `+N` for the rest. A `regressed` chip is red only when the row is parked on a person (a human-gate modifier or stage 045 / 071 / 072 / 085 / 090); on a hive-driven stage with nothing waiting on a human it draws as a muted dashed `stage ↓` pill, yields the lead slot to any other modifier, keeps the row out of **Needs attention**, and its tooltip ends "the hive is handling it — no action needed; clears on the next forward step". The marker is set by a genuine backward hop (a lapsed claim 020 → 010, a 030 ↔ 020 flicker and a 040 → 010 after a partially-addressing merge are not regressions) and clears on the first forward hop; see "The regressed marker" in `labels-and-control-signals.md`. Everything else — the reason sentence, who must act, nudges sent, the repository, all flags and the last stage hops — is in the hover tooltip on the item's `#N` link (on a nested sub-row, on its `#N` PR link). Hover text in a row lives only on the `#N` link, the covering-PR link, the title and the lit chevron (its stage, and whose move it is); passed and upcoming chevrons, the kind and author icons, chips and the `since` column have none. Title lines wrap rather than push the strip out of alignment: the issue title stays before the *PR in progress* column (a clear gap always separates it from the PR entry, which starts at the column edge) and the PR title before the chip; hovering a title shows the full text. Rows are sorted furthest-along first by default — a covered issue ranks by its furthest-along PR — then, within a column, attention-first: rows a person must act on now (any shown pill — `stalled`, `unauthorized`, `needs rebase`, `needs human`, … — or a human-gated stage: 045, 071, 072, 085, 090) come first, plain rows next, and rows nobody should act on — `hold` (incl. `do-not-merge/*`), `draft`, and a PR standing in for a closed issue — sink to the bottom of the column; within a tier the longest on the step comes first, then repo and number. Pills the server holds back until the merge gate (`deferred_modifiers`) never promote a row. The same tiering orders the PR sub-rows under an issue, and the server pre-sorts `GET /api/stage-tracker` the same way (stage, tier, wait, repo, number) so chat `/prs` and the hub read the same order. *Sort* also offers longest waiting (tier first, then wait), newest and number (no tiering); the arrow beside it flips the column / age / repo / number keys but never the tier — attention stays on top in both directions, as its tooltip says ("Attention-first within each column; arrow flips age order"). Every open issue and pull request of the watched repositories is listed — actionable, held, claimed, in progress, covered by a PR, filtered by a `needs-*` label, reporter-triage, draft and exempt-labelled alike; an item without a stage label or journal entry sits at *Open*, and one in a scanner bucket with no label yet is placed from the bucket (claimed → 020, in progress → 030, covered → 040). Filters work on the cached payload in the browser and persist across reloads (the page number does not): kind chips, *Repo*, a search box (`#number` / number / title substring, also matching a covering PR), *Since* (any, over 1 hour / 1 day / 1 week on the current step, or past the stage's configured dwell — stalled items always qualify), *Author* kind, *Needs attention* (anything with a drawn health modifier or on 071 / 072 / 085 / 090 — merge-gate pills held back before the gate do not count), and label chips built from every label present, each with its count: a **🙋 Needs human** facet first (attention-coloured; it matches any item carrying a label of the needs-human set — the server's `needs_human_labels`, the same list as the Projects *Needs human* preset: `stage/045-needs-decision`, `stage/085-needs-decision`, `needs-decision`, `needs-direction`, `needs-spec`, `needs-signal`, `needs-human` and `needs-human/*`, the hold labels, `stage/071-needs-author`, `sentinel-alert`, `needs-triage`; its count is the number of distinct items, not the sum of label counts), then every `stage/NNN-*` label in numerical order, then all other labels alphabetically (case-insensitive). When the row would overflow, the **+N more** button opens an in-app *All labels* dialog listing every label in the same order with counts, a type-to-filter box, the same toggles the chips use and a *Clear* button; a label selected there is promoted into the visible row. Chips of one family OR, families AND; *Needs human* ANDs with the label chips. *Clear filters* shows while anything is narrowed. The list is paged — 25 / 50 / 100 rows, default 50 — with Prev / Next and "showing X–Y of N (M filtered out)" above and below; the header reads "N items (I issues / P PRs) · M need attention" — N, I and P are the whole list, the same `openItems` set the Overview tile and its left-menu counter print, so N always equals the tile's **Total open issues + Total open PRs**; a "· X match filters" note follows while a filter is on. The sidebar counter beside **Hive activity monitor** is that same N — total open items, written in the same render pass as the header, so the two cannot drift apart; how many need attention is the warn-coloured header suffix and the counter's hover tooltip (counted over every item, filters or not). When covering pull requests are nested under their issue the "showing X–Y of R" line counts rows and adds "(P nested under their issues)" so R never reads as a different total. The small monospace strip beside the count — `engine enforce · N anomalies` — is the central stage engine ([ADR-0025](/docs/hive/adr/0025-central-stage-engine)): `engine` names the current `governor.labels.stage.engine.mode` (green `enforce`, neutral `shadow` = paused, nothing written; hover for what the mode does and the derivation count), `anomalies` counts stage moves the engine's admission table did not expect (red when non-zero). The same numbers are `/api/status.stageEngine`; the mode is changed under **Settings → Labels → Central stage engine mode** and the operator notes are in [operator-reference.md](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md#central-stage-engine-rollout). "updated Ns ago · cache" says how old the server-built payload is: the spoke rebuilds it at most every `dashboard.stage_tracker.cache_ttl_s` (default `governor.eval_interval_s`) from its existing snapshot and serves it with an `ETag`, so the browser's 30-second poll is a 304 and no request ever reaches GitHub (`/api/gh-rate-limits` lists the caller `dashboard:stage-tracker` with nothing charged). See [Stage labels](/docs/hive/labels-and-control-signals#stage-labels-stagennn--the-pipeline-state-machine).
**Filter chips (Needs human / On hold / labels).** The label row under the toolbar starts with two synthetic chips, then one chip per label the listed items carry. **🙋 Needs human** means a person's decision or action is the *only* thing that advances the row: an issue at `015` triage-needed, `045` or `085` needs-decision, a PR at `090` ready-for-human-merge, a PR at `071`/`072` whose author is a human, or any row wearing a `needs human` / `needs human decision` / `unauthorized` pill. It is **not** `hold`, `stalled`, `blocked`, a handled regression, a `040` issue an open PR covers, or a PR the hive will merge itself — those are the hive's (or nobody's) to move. **⏸ On hold** is the separate chip for rows a maintainer paused on purpose with a hold label; a held row never counts as Needs human. The chip label, its count, the filter it toggles and the payload's `needs_human` / `on_hold` totals (and per-item flags) all come from one predicate (`stageTrackerNeedsHuman` in Go, `stageTrackerItemNeedsHuman` in the page), so what the chip counts is exactly what the filter shows ([#15008](https://github.com/hivecommons/hive/issues/15008)). Hover a chip for a one-line explanation. Label chips AND with the synthetic chips.
@@ -666,6 +669,8 @@ Fed by `GET /api/stage-tracker`. Every issue and pull request follows the same p
**Unauthorized pill.** A pull request the hive cannot move because its GitHub App lacks a permission for the next step — it wears `hive:unauthorized` ([#14589](https://github.com/hivecommons/hive/issues/14589)); first case: approving held `action_required` workflow runs needs `actions: write` — carries the `unauthorized` modifier in the tracker payload, drawn in the far-right pill column exactly like `stalled` / `hold` / `draft` but muted (dashed, `--text-muted`), never red: it is nothing wrong, just a step only a person may take. It counts under **Needs attention**. Before the merge gate only the held-workflow-runs step shows (see *Pills before the merge gate*). Its tooltip (payload `unauthorized`) names the missing permission and the human step; on the **Needs human decision** lane the lit chevron and the caption read **Awaiting human: approve workflow runs** (payload `unauthorized_step`, from the `unauthorized:` stage reason). The pill, label and payload fields clear themselves in the tick the step is no longer needed.
+**Merge train.** When several approved PRs to the same base touch the same files, the automerge lanes merge them one at a time ([#15117](https://github.com/hivecommons/hive/issues/15117)). The tracker renders every train contiguously, in merge order, with a thin left rail linking the rows and a header chip `⛓ merge train · 4 PRs · front #15074` (overlapping files listed beside it). Pill wording per state: the front reads "Merging · front of train"; siblings "In merge train · 3rd of 4"; once the front merged and the hive is rebasing a conflicting sibling, "Rebasing · attempt 1/2"; when the loop guard (`auto_merge.merge_train.max_rebase_attempts`) is spent, "Needs human decision · rebase failed twice" at stage/085 with the decision option "Rebase, then merge as maintainer". The tooltip lists the PRs ahead and the overlapping files. Waiting siblings are never red, never `needs human`; a `needs-rebase` label on one is reported under `stale_labels`, the train repairs it. Each item carries `merge_gate.state` (`queued-behind` / `rebasing` / `rebase-exhausted` / `front`, with an honest `reason` such as `behind #15074 in merge train (3rd of 4, overlapping files: stage_tracker.go, index.html)`) and `merge_train{id, state, front, position, size, attempt, max_attempts, overlap_files, ahead}`; the id is ` #`.
+
**Pills before the merge gate.** Owner's rule (2026-10-27): until a pull request is in the merge gate there is nothing a person can do about a `sentinel`, `needs human` or `unauthorized` notice — the PR is still in motion — so the tracker must not show them. The server splits every marker into two classes (`stageTrackerModifiers` in `stage_tracker.go`; each carries an explicit `ActionableNow` flag) and the row's payload carries the pills it draws in `modifiers` and the ones it holds back in `deferred_modifiers`. A deferred pill is simply absent — no chip, no breathing chevron, no **Needs attention** count — until the PR's stage reaches the merge-gate band (`stage/080` and later: 080, 085, 090, 095, 100, the same boundary adoption yields at), when it renders exactly as before and counts; the row tooltip alone says "Pending at merge gate: sentinel, needs-human". The merge-gate cell's own words ("Sentinel alert · needs human", "Awaiting human: …") are unchanged — they only appear at the gate anyway. Issues on their own stages (Open, 010, 020, 045) keep every pill: a `needs-human` there is a park the person owes now; an issue riding its covering PR's stage follows the PR. The GitHub labels are untouched — this is presentation and counting only.
| pill | class | when shown |
@@ -680,7 +685,9 @@ Fed by `GET /api/stage-tracker`. Every issue and pull request follows the same p
| `sentinel` (the trusted-author notice and the real `sentinel-alert` alike) | merge gate | PR at stage/080 or later; `sentinel_alert` itself stays truthful in the payload for the lane word and the Decide… preface |
| `needs human` (`needs-human` label) | merge gate | PR at stage/080 or later; always on an issue at its own stage |
| `needs human decision` (`needs-decision` labels on another stage) | merge gate | PR at stage/080 or later; always on an issue at its own stage |
-| ✓ decision badge (`last_operator_action`) | before the pills; green / amber (still at the same stage past the stuck window) / red (failed) / dotted … (hive deciding) | always, once a row action was submitted from the dashboard; tooltip tells what you chose, when, and what the hive did next |
+| history decision badge (`last_operator_action`) | before the pills; the history mark + action count, neutral / amber (still at the same stage past the stuck window) / red (failed) / dotted (hive deciding) | always, once a row action was submitted from the dashboard; tooltip tells what you chose, when, and what the hive did next |
+
+**Returned to a contributor's hive (072).** `stage/071-needs-author` is a person's PR: a human-authored pull request sent back by a maintainer's CHANGES_REQUESTED review or by the review swarm. The same verdict on a clanker-authored PR (`author/clanker`, or an author resolved to a contributor's hive agent) routes to `stage/072-returned-to-contributor`, whichever lane carried it: the actor is the owning contributor, the caption reads `Returned to @thatguywob's hive — @clubanderson requested changes`, and the normal contributor nudge goes to that hive ([hivecommons/hive#15159](https://github.com/hivecommons/hive/pull/15159): a human's out-of-cycle review on a clanker PR parked it at 071 under a maintainer's name). The 071/072 chevron names the routing — `Needs @sam`, `Returned to @thatguywob's hive` — and a sentinel finding on such a row is a ` · sentinel` suffix on that word (the chip folds into it, the tooltip carries the finding); `Sentinel alert · needs human` is only the chevron word when the maintainer's review is still outstanding (085). A trusted human's CHANGES_REQUESTED review on the current head *is* the human those parks (sentinel alert, `hold`, workflow-path) were waiting for: the PR routes to 071/072 and the sentinel pass does not re-assert 085 until a new head re-arms the alert. A return placed by hand counts too: when a trusted maintainer swaps `stage/085` for `stage/071`/`stage/072` on the PR, the reconciler reads the label event once per head, records it as that maintainer's change request against the head, and keeps the stage; the hive's own label events and untrusted actors are not honoured. **Give direction** on a clanker PR is accepted as well — the direction is posted to the contributor's hive as a change request and the row moves to 072; only a person's PR, which no hive can carry direction to, still answers `409 the hive did not open this pull request`.
**Adoption pill (072).** A nudged `author/clanker` pull request at `stage/072-returned-to-contributor` carries `adoption{author, due, blocked_by, since, nudges, author_online, author_registered, author_last_seen, author_maintainer, policy, action, text}` — the nudge count and first-nudge time from the stage record, the author's live hub presence (the same lookup the contributor-offline adoption itself consults), and, when the adoption engine judged adoption due and held it, why (`blocked_by: author-is-maintainer`). The row's caption and an amber pill say it in plain words instead of a bare "Returned to contributor": `waiting on @hanthor (maintainer, offline 26h) — adopt?` under `governor.labels.stage.adoption.maintainer_policy: ask`, `waiting on @zoe (offline 40m, 2 nudges)` while the default `adopt` policy's clock runs, `… — not adopted: maintainer_policy is never` under `never` ([hivecommons/hive#14524](https://github.com/hivecommons/hive/pull/14524): the PR sat 26h at 072 with its author's hive gone and nothing on screen saying why). When `action` is `decide:adopt` the row also carries `decision_needed` (question "@hanthor maintains this repository and has been offline for 26h; adopt the PR?", `blocker_reason: adoption:author-is-maintainer`, options Adopt then Hold/Release), so the lit 072 chevron and the `adopt?` pill open the Needs-human popover, and the row menu and the selection bar offer **Adopt (hive takes over)** — same action-id vocabulary as the other `decide:*` entries — which, after an in-app confirm, POSTs `{"decision":"adopt"}` to the item's decision endpoint: the hive posts an attributed note, sets `adoption/supersede` (the owner-delegate's request label) and the next sweep adopts the head on the existing adopt/supersede path; the toast reads `adoption-queued`. Write access required; the action is absent when nothing is the owner's to decide.
@@ -688,6 +695,12 @@ Fed by `GET /api/stage-tracker`. Every issue and pull request follows the same p
**Presence indicator on the waiting-on pill.** Every PR row that waits on a named person — `stage/071-needs-author`, `stage/072-returned-to-contributor` (before and after the nudge), `stage/017-possibly-existing` on a PR — carries `presence{login, state: online|offline|unknown, kind: clanker|human, since, last_seen, next: wait|adopt, adopt_at}`, read from the contributor hub's live presence (registered profile + relay heartbeat: the same lookup adoption consults, so the indicator and the adoption text never disagree) and projected through adoption's own clock ([hivecommons/hive#15051](https://github.com/hivecommons/hive/issues/15051)). The pill draws it just before the `@login`: a filled **dot** (`--cue-decision`, a soft breathe; none under `prefers-reduced-motion`) when the person's clanker — their connected contributor agent — is online, tooltip "@Danathar's clanker: online, last seen 2 min ago — hive is waiting for them"; a filled **square** (`--status-error`) when it is offline, "@hanthor's clanker: offline 26h — hive will adopt at 15:10 (in 38m)" (or "— hive is waiting for them" when `ask`/`never`/adoption-off holds the clock); a **hollow ring** when no clanker is registered here, "@zoe: no clanker registered — waiting for a human reply" with the pill reading `waiting on @zoe (human · no clanker)`. Shape, not colour alone, carries the state; the indicator has its own `aria-label` ("@hanthor: clanker offline — hive will adopt at …"). A row with presence but no adoption line (071, a 072 not yet nudged, a human's PR) gets a muted `waiting on @login` pill carrying the indicator; a nudged 072 row keeps its single amber adoption pill with the indicator inside it. The Needs-human popover shows the same line under the question when the waiting person is named, a per-PR sub-row under an issue mirrors it, and the row tooltip adds a `Presence:` line. It refreshes with the tracker like every other pill.
+**Rework pickup on the waiting-on pill.** The same 071 / 072 rows also carry `rework{state: requested|acknowledged|in_progress|pushed, actor, requested_at, since, evidence: queued|claim|activity|commit, mode, ahead, head}` — whether the person the row waits on has picked up the rework the hive (or a human reviewer) asked for ([hivecommons/hive#15280](https://github.com/hivecommons/hive/issues/15280): "waiting on @x" said who, never whether anything had happened since). It is derived from data the tracker already holds, no extra GitHub calls: `requested_at` is the first nudge the contributor engine recorded on the current head (`stage/072`) or, before any nudge, when the row entered the stage (`stage/071` after a human's changes-requested review); **pushed** when the PR's head is no longer the head the request named (`evidence: commit` — the stage engine routes the row back to `050` on its next sweep, the pill says `re-review pending` meanwhile); **in_progress** when the author's clanker relay — or, after a Reroute, the target's — has taken the `fix-pr` task from the contributor fix queue (`evidence: claim`, `mode` spawn/preempt/boundary, `since` the dispatch); **acknowledged** when the PR's `updated_at` moved after the request and after the hive's own nudge comment (`evidence: activity` — a reply, a review-thread answer, an edit; reactions do not bump it, so an 👀 alone is not seen); otherwise **requested**, with `evidence: queued` and `ahead` when the fix is enqueued on the clanker's hub queue but not yet dispatched. The pill appends the state after the login — `waiting on @x · no response`, `· queued`, `· acknowledged`, `· working`, `· re-review pending` — and its tooltip (and the row tooltip's `Rework:` line) says since when: "rework requested 1 hour ago (13:32 UTC) · no response from @x yet", "@x acknowledged · activity on the PR 20 min ago", "@x's clanker picked up the rework 5 min ago (spawn)", "@x pushed a new head 1 min ago · re-review pending". A state change at an unchanged stage writes the row's event line ("@x acknowledged the rework request", "@x's clanker picked up the rework", "@x pushed a new head — re-review pending"). The presence indicator stays; a nested per-PR row under an issue mirrors the field.
+
+**Push detection requires a nudge baseline.** The `pushed` state compares the current PR head with the stage record’s `NudgeHead`; entering `071` alone records a time, not a head. A `071` row that has never been nudged therefore cannot show `pushed` / `re-review pending` from a head change. If the push moves `updated_at` past the activity threshold, the pill may instead say `acknowledged` (`evidence: activity`); that signal does not distinguish a push from a comment or edit. The stage engine still routes the new head to `050` on its next sweep. This limitation affects the interim pickup signal, not re-review routing ([hivecommons/hive#15286](https://github.com/hivecommons/hive/issues/15286)).
+
+**Adopt now / Reroute (072, 071).** Every `author/clanker` pull request waiting on its author's hive — `stage/072-returned-to-contributor` or `stage/071-needs-author`, not yet adopted — also carries `takeover{author, author_online, adopt_now, adopt_now_reason, reroute, reroute_reason, clankers[], rerouted, forced_by, forced_at, fallback_at, grace_elapsed}`, and the ⋯ row menu (and the nested per-PR row under an issue) gains two operator actions beyond Hold / Re-evaluate / Mark needs-human / Open / Copy ([hivecommons/hive#15038](https://github.com/hivecommons/hive/pull/15038): "Returned to contributor · waiting on @Danathar" offered nothing to end the wait). **Adopt now — hive takes it over** (`decide:adopt-now`) carries the consequence line *closes the contributor's PR after hive opens its copy; their commits + sign-off are preserved* under the label, asks an in-app confirm (don't-ask-again supported, keyed `hive.tracker.confirm.decide:adopt-now`; the confirm adds "its hive is online right now" when it is) and POSTs `{"decision":"adopt","force":true}`: the hive posts an attributed `` note, sets `adoption/supersede`, marks the stage record forced and nudges the sweep, which runs the existing supersede adoption at once — presence, the grace windows and `maintainer_policy` are all bypassed (`hold` still stops it; the menu says *release the hold first*). The persistent result toast reads `Adopted → #N (hive's copy), contributor PR will close when #N merges` when the sweep lands inside the handler's wait, else `Adoption forced: hive opens its copy on the pass now running`; the audit entry is `repo_item_decision_adopt_now` with `action=adopt-now`. **Reroute to another clanker** (`decide:reroute`) opens a submenu of the currently-online registered contributor clankers from the contributor fleet (`@login · hive · online 12m ago · 1 in hand` / `idle`, via `clankers[]` — the same facts as `/api/contribute/fleet`) plus *any available (N online)*; in the Needs-human popover it is a required choice list. The pick (after a confirm keyed `hive.tracker.confirm.decide:reroute`) POSTs `{"decision":"reroute","to":"|any"}`: the hive enqueues the PR's follow-up work (the stage record's return reason — the failing check or the reviewer's requested changes — plus the operator's comment) on the target's contributor fix queue and dispatches it as a `fix-pr` `task_assign` interrupt, the same path the 072 nudge takes to the author's own relay; the task's instruction says whose PR it is and that the target either pushes the fix (if the branch allows) or opens a replacement PR carrying the author's commits and sign-off. The author is told by ``, the row shows a `rerouted → @X` pill with a presence dot (green while the target's relay is on the hub), and the toast reads `Rerouted → @X (dispatched now, preempt)` / `(queued; N ahead)`. If the target's relay has not taken the task within `governor.reroute.pickup_timeout` (default 30m) the hive falls back to Adopt now by itself: the queued item is withdrawn, the record marks the forced adoption, a `lapsed=true` note goes on the PR, a warning system alert and a `repo_item_reroute_fallback` audit entry are raised, and the sweep is nudged. With no clanker online the item is disabled with the reason *no clankers online — use Adopt now*. Both actions appear in the Needs-human popover's options when the blocker is a waiting-on-contributor condition — once the nudge grace has elapsed and the engine is not adopting on its own, the row carries `decision_needed` (`blocker_reason: adoption:`, options Adopt now, Reroute, Hold) — and **Let hive decide** picks Adopt now (`grace-elapsed-adopt-now`) when the grace has already run out. Write access required.
+
**Blocked pill.** An issue wearing `blocked` because the hive parked it on a pull request — its body carries `` (the review backlog's spin-off issues, `hive-open-issue comment --blocked-on-pr`) — names that PR: the payload carries `blocked_on_pr: {repo, number, url, state}`, the pill reads `blocked on #N` and links to the PR, its tooltip says `Blocked on PR owner/repo#N (open) — lifts when it merges; re-triage if closed unmerged`, the caption reads `Blocked on PR #N — lifts when it merges` instead of `No stage recorded yet`, and the row's title and chevron tooltip carry the blocker too ([#14952](https://github.com/hivecommons/hive/issues/14952): #14863 showed a bare grey pill and "blocked · since 7h" with nothing to say what it waited on). The state (`open`, `merged`, `closed`, or absent when nothing knows) comes only from what the hive already holds — the status snapshot's open PRs, the ForgeState cache, the recent-merge ring — never a GitHub call from the tracker build; a merged or closed-unmerged blocker says so (`merged; the blocked sweep lifts the label next` / `closed unmerged; the blocked sweep re-triages it next`). The server reads the marker from the issue body when it has one, else from the enumeration's `blocked on owner/repo#N` reason, so sidelined rows (which carry only the reason) name it too. A `blocked` issue with neither (a human-applied label) keeps the bare pill. This is distinct from `blocked_on`, the open GitHub "blocked by" dependencies of a sequenced sub-issue ([#14765](https://github.com/hivecommons/hive/issues/14765)); when a row has both, the chevron tooltip lists them together.
**New row (`000-new`, the pre-stage state).** Every open issue and pull request in `project.repos` is on the list from the moment GitHub reports it — there is no gap between "opened" and "the hive has an opinion about it" ([#15094](https://github.com/hivecommons/hive/issues/15094)). The `issues.opened` / `pull_request.opened` webhook re-read inserts the item into the status payload straight away (`insertFrontendRepoItem`; the governor side keeps the same row in its cached snapshot so a republish does not drop it), the per-item replay re-applies that insert after a governor republish, and the enumeration poll is the fallback for a missed delivery. Until something places the item — a stage record, a `stage/*` label, a hold, a blocked marker, a bucket other than *reporter triage* / *unclassified* — the row is anchored in the **Open** column with the lit chevron reading **New** (`ham-open-new`, blue ✦ rather than amber) and a caption that names who moves it next and when: `New · waiting for the stage-label pass (→ 010 actionable), then the scanner (next pass in 7m)` for an issue the reporter gate has already admitted, `New · waiting for a maintainer's triage (add `triage/accepted`); the hive's next pass in 7m` for a human contributor's issue the reporter-trust gate is still holding (the label names come from `issues.filter.reporter_trust.untrusted_require_labels`), and `New · waiting for the stage-label pass (→ 050 review pending)` for a pull request. The *since* time is the item's `created_at`; "next pass" is the governor's next kick (`governor.nextKickAt`, so it counts down with the Governor tile and reads *due now* once it has passed). The payload carries it all as `new{since, next_mover (maintainer|hive), next_mover_text, next_pass_at, next_pass_in_seconds, gates[], text}`; the chevron tooltip lists the gates ahead — **triage** (the reporter gate: done when admitted, next when a triage label is still needed), **actionable** (the stage-label pass marks it 010), **claimed** (the scanner claims it, 020) — each marked ✓ done, → next or · later, and the row's actor icon reads *human* while the next mover is a maintainer and *hive* otherwise. A row that already wears a hold, `blocked`, a draft marker or any stage record is never *New*; neither is an issue at `015` once the stage-label pass has written it.
@@ -698,13 +711,13 @@ Fed by `GET /api/stage-tracker`. Every issue and pull request follows the same p
**Row gutter.** Every issue and PR row (never a column or repo-group header) carries a left gutter with a select checkbox and a ⋯ actions button that is always present and always visible: dimmed at rest (`--ham-tool-rest-opacity`), at full emphasis on the row under the pointer or holding focus, on a selected row, and on every row while anything is selected or the context menu is open — the only animation is the hover fade (`--ham-tool-fade-ms`, ≤ 120 ms), and nothing about it depends on scrolling, row position or which rows the live refresh re-created.
-**Did I act on this? (the decision badge).** Owner (2026-10-28): "I need a way to see if I selected an action on an item so I can keep track of whether or not I addressed it. Hovering over the indicator should show me what action I requested." Every row action submitted from the dashboard — Approve, Give direction, Close as superseded, Close as won't-fix, Let hive decide, Hold / Release hold, Accept triage, Mark needs-human — is written to a server-side ledger (`/data/operator-actions.json`, `operator_actions.go`; newest 20 per item) as `{action, actor, at, note, target, outcome, detail, head_sha, stage}`, so the record survives reloads and shows in every browser. The tracker row carries the newest one as `last_operator_action` (+ `operator_action_count`) and `GET /api/repos/{owner}/{repo}/items/{number}` returns the full list. The row then wears a small **✓ badge** in the cap cell, before the state pills (`.ham-acted`): green once an action is recorded; **amber** when the item is still at the same stage past the stuck window (`HIVE_OPERATOR_ACTION_STUCK_AFTER`, default 30 min — "acted, but nothing happened"); red when the action itself failed; a dotted `…` while *Let hive decide* is still running. The in-flight spinner on the chevron is a different cue: that is the request still on the wire; the badge appears once the server has recorded it. Hover or focus (the badge is in the Tab order) opens an in-app tooltip — never a native `title` — written in plain words, no stage codes, in the order every cue on the row uses: **what it is waiting on → what you chose → what the hive did next**. E.g. `At Merge gate — waiting on CI.` / `You chose Approve · 15:55 (5 min ago) · done — released to the hive.` / `Then the hive moved it to Merged — a reviewer approved it · 1 min ago.` Once the item moves on, the badge stays and the first line reads `Addressed — now at `; while stuck it reads `Still at 42 min after you asked — hive says: `. Another operator's action is named (`@someone chose …`); your note and "N earlier actions on this item" follow. The **Decisions** select in the toolbar narrows the list: *Awaiting my decision* (parked on a person, no action recorded yet, or the last one failed), *I acted* (any badge), *Acted, still stuck* (amber badges only).
+**Did I act on this? (the decision badge).** Owner (2026-10-28): "I need a way to see if I selected an action on an item so I can keep track of whether or not I addressed it. Hovering over the indicator should show me what action I requested." Every row action submitted from the dashboard — Approve, Give direction, Close as superseded, Close as won't-fix, Let hive decide, Hold / Release hold, Accept triage, Mark needs-human — is written to a server-side ledger (`/data/operator-actions.json`, `operator_actions.go`; newest 20 per item) as `{action, actor, at, note, target, outcome, detail, head_sha, stage}`, so the record survives reloads and shows in every browser. The tracker row carries the newest one as `last_operator_action` (+ `operator_action_count`) and `GET /api/repos/{owner}/{repo}/items/{number}` returns the full list. The row then wears a small **history badge** in the cap cell, before the state pills (`.ham-acted`): the clock-with-counter-clockwise-arrow mark the right-most column header also carries (hover: "History — decisions and actions on this item"), with the count of recorded actions beside it — never a ✓, which read as "done/passed" (owner, 2026-10-28). A filled neutral pill once an action is recorded; **amber** when the item is still at the same stage past the stuck window (`HIVE_OPERATOR_ACTION_STUCK_AFTER`, default 30 min — "acted, but nothing happened"); red when the action itself failed; a dotted border while *Let hive decide* is still running. Below it, the strip's cap chevron wears the same history mark in muted outline until the PR merges, when it becomes the green ✓. The in-flight spinner on the chevron is a different cue: that is the request still on the wire; the badge appears once the server has recorded it. Hover or focus (the badge is in the Tab order) opens an in-app tooltip — never a native `title` — written in plain words, no stage codes, in the order every cue on the row uses: **what it is waiting on → what you chose → what the hive did next**. E.g. `At Merge gate — waiting on CI.` / `You chose Approve · 15:55 (5 min ago) · done — released to the hive.` / `Then the hive moved it to Merged — a reviewer approved it · 1 min ago.` Once the item moves on, the badge stays and the first line reads `Addressed — now at `; while stuck it reads `Still at 42 min after you asked — hive says: `. Another operator's action is named (`@someone chose …`); your note and "N earlier actions on this item" follow. The **Decisions** select in the toolbar narrows the list: *Awaiting my decision* (parked on a person, no action recorded yet, or the last one failed), *I acted* (any badge), *Acted, still stuck* (amber badges only).
-**Legend — the three cues a row may carry** (shared with the in-flight and needs-human work landing beside this): the *state pill* says where the item is and what it is waiting on (`hold`, `needs human`, `stage ↓` …); the *working* spin on the lit chevron says the hive is doing something in that stage right now (a still chevron = waiting on something, its tooltip says what); the *decision badge* (✓) says a person acted here and what followed. Plain words throughout — working / waiting on X / set back by Y / merged — nothing else blinks.
+**Legend — the three cues a row may carry** (shared with the in-flight and needs-human work landing beside this): the *state pill* says where the item is and what it is waiting on (`hold`, `needs human`, `stage ↓` …); the *working* spin on the lit chevron says the hive is doing something in that stage right now (a still chevron = waiting on something, its tooltip says what); the *decision badge* (the history mark with its count) says a person acted here and what followed. Plain words throughout — working / waiting on X / set back by Y / merged — nothing else blinks.
-**Row actions (right-click, ⋯, Shift+F10).** Right-clicking a row — or a nested PR sub-row — opens an in-app context menu of the actions that fit that item's current state; the ⋯ button in the row's left gutter (every nested PR sub-row has its own gutter with its own checkbox and ⋯) opens the same menu for pointer-free use, and so do **Shift+F10** and the context-menu key while the row or anything in it has focus (every row is a tab stop). The menu is positioned at the pointer, clamped to the viewport, uses `menu` / `menuitem` roles, moves focus with ↑ ↓ Home End, opens the *Decide…* group with → and closes it with ←, and closes on Esc, Tab, click-away, scroll or resize, returning focus to the row. Shift+right-click keeps the browser's own menu. Entries, each shown only when it applies: **Decide…** on a row waiting on a person — a PR wearing the *Needs human decision* pill at 085 on the Merge gate column (the primary case: e.g. two competing `dibs` PRs), an issue row whose one nested PR is parked there (the menu reads *Decide PR #458…* and acts on that PR, since the issue's own stage is only mirrored from it), or an issue parked at 045. **PR-scoped actions on nested PRs** ([#15009](https://github.com/hivecommons/hive/issues/15009): one issue, two PRs — [#15017](https://github.com/hivecommons/hive/pull/15017) *Ready for human merge · untrusted author* and [#15020](https://github.com/hivecommons/hive/pull/15020) *Needs human decision* — and no way to decide one without the other): every nested PR sub-row carries its own ⋯ menu, and its *Decide…* (Let hive decide, Approve, Close as superseded by…, Won't fix…, Give direction…), *Hold* / *Release hold* and the rest act on **that PR** — `runRowAction(action, row)` receives the PR's own row object (number, repo, head), never the parent issue's, and the needs-human popover opened from a sub-row's chevron does the same. A PR waiting for a person to merge it (*Ready for human merge*, a `human_merge_reason` such as an untrusted author) counts as parked for this purpose. When an issue has two or more open nested PRs the issue row's menu no longer offers a single *Decide…* that would silently pick one: it offers **Let hive decide — all N PRs** (hint *per PR*), which asks the server to decide each PR on its own (see [Let hive decide](#let-hive-decide)); the issue's own *Decide…* stays only when the issue itself is parked at 045. *Close as superseded by…* on a nested PR with exactly one open sibling under the same issue pre-fills that sibling's number. The first entry is always *Let hive decide* (see [Let hive decide](#let-hive-decide) below): the hive decides as the owner's delegate and records it the way a person's decision is recorded. For a PR: *Approve (/lgtm + /approve)* posts those two commands as comments attributed to you and records your approval against the PR's current head as the human answer the row was parked for — a `requires_human` / decision-requested reviewer verdict, the hive's own `needs-human` park (a supersession question, say), the `sentinel-alert` label and the evidence bundle's sentinel findings are all released by it (the response's `released` lists what was: `approval-recorded`, `needs-human`, `sentinel-label`, `sentinel-acknowledged`), the stage engine re-derives the stage on the spot and the tracker cache is dropped, so the row leaves *Needs human decision* for 080 *Awaiting merge admission* on the next read; the merge gate (and the auto-merge lanes, which honour the recorded approval on that head exactly as a trusted merger's GitHub Approve review) takes it from there; the dashboard never reviews as you and never merges. A new head carries no answer: pushing to the PR re-asks. The delegated *Let hive decide* path still posts the comments only — it recommends and never clears a sentinel alert — *Close as superseded by…* (a picker listing the other open PRs in the repo, or type `#458` / `owner/repo#458` / a URL; then an optional comment, then the sweep's labelled close), *Won't fix…* (a comment naming you, then a not-planned close) and *Give direction…* (your text is recorded as `/hive decision ` against the head, so the author agent pushes a new head; only on PRs the hive opened). For an issue: *Approve direction* (records the same `/hive approve` a maintainer would comment: the parking labels come off, `approved-direction` goes on, the stage engine logs the un-park and a reply comment names you; nothing is merged), *Send back to the hive…* (`/hive decision `), *Close as superseded by…* and *Close as won't-fix…*. A selection mixing parked issues and parked PRs has Decide… greyed; **Hold** or **Release hold** depending on the `hold` chip (the same `…/items/{number}/hold` toggle as the repository cards, and the same permission — owner, or a signed-in user with write access to the repo); **Re-evaluate now** (the hive-wide read-only rescan behind the Overview's refresh — a rescan already running is reported, not duplicated); **Accept triage** (only on an issue the chevron calls *Triage needed* — stage 015, or the `needs-triage` chip on another stage — never on a PR; the one-click form of a maintainer accepting triage on GitHub: adds the reporter-trust triage label, `triage/accepted` by default or the first of `issue_filter.reporter_trust.untrusted_require_labels`, removes the awaiting label `needs-triage` / `reporter_trust.awaiting_label` and a Hive-written `stage/015-triage-needed`; the row re-reads *Actionable* with the 👤 gone at once, the reporter-trust gate admits the issue on its next poll and the stage engine records 015 → 010; same permission as Hold, audited as `repo_item_accept_triage`, a no-op when already accepted); **Mark needs-human…** (hidden once the chip is on; merger or owner role, applies `needs-human` and a marker comment through the review pipeline's send-to-human endpoint); **Open on GitHub**; **Copy link**. Entries you may not use are greyed with the reason in their tooltip, never hidden. Confirmation and free-text input use the dashboard's own dialogs — no browser prompts. Every action is an existing audited dashboard endpoint (the decision endpoint is `POST /api/repos/{owner}/{repo}/items/{number}/decision` and triage acceptance is `POST /api/repos/{owner}/{repo}/items/{number}/accept-triage`, see [api-reference.md](https://github.com/hivecommons/hive/blob/v5/src/docs/api-reference.md)); none of them bypasses the hive's governance or merges anything.
+**Row actions (right-click, ⋯, Shift+F10).** Right-clicking a row — or a nested PR sub-row — opens an in-app context menu of the actions that fit that item's current state; the ⋯ button in the row's left gutter (every nested PR sub-row has its own gutter with its own checkbox and ⋯) opens the same menu for pointer-free use, and so do **Shift+F10** and the context-menu key while the row or anything in it has focus (every row is a tab stop). The menu is positioned at the pointer, clamped to the viewport, uses `menu` / `menuitem` roles, moves focus with ↑ ↓ Home End, opens the *Decide…* group with → and closes it with ←, and closes on Esc, Tab, click-away, scroll or resize, returning focus to the row. Shift+right-click keeps the browser's own menu. Entries, each shown only when it applies: **Decide…** on a row waiting on a person — a PR wearing the *Needs human decision* pill at 085 on the Merge gate column (the primary case: e.g. two competing `dibs` PRs), an issue row whose one nested PR is parked there (the menu reads *Decide PR #458…* and acts on that PR, since the issue's own stage is only mirrored from it), or an issue parked at 045. **PR-scoped actions on nested PRs** ([#15009](https://github.com/hivecommons/hive/issues/15009): one issue, two PRs — [#15017](https://github.com/hivecommons/hive/pull/15017) *Ready for human merge · untrusted author* and [#15020](https://github.com/hivecommons/hive/pull/15020) *Needs human decision* — and no way to decide one without the other): every nested PR sub-row carries its own ⋯ menu, and its *Decide…* (Let hive decide, Approve, Close as superseded by…, Won't fix…, Give direction…), *Hold* / *Release hold* and the rest act on **that PR** — `runRowAction(action, row)` receives the PR's own row object (number, repo, head), never the parent issue's, and the needs-human popover opened from a sub-row's chevron does the same. A PR waiting for a person to merge it (*Ready for human merge*, a `human_merge_reason` such as an untrusted author) counts as parked for this purpose. When an issue has two or more open nested PRs the issue row's menu no longer offers a single *Decide…* that would silently pick one: it offers **Let hive decide — all N PRs** (hint *per PR*), which asks the server to decide each PR on its own (see [Let hive decide](#let-hive-decide)); the issue's own *Decide…* stays only when the issue itself is parked at 045. *Close as superseded by…* on a nested PR with exactly one open sibling under the same issue pre-fills that sibling's number. The first entry is always *Let hive decide* (see [Let hive decide](#let-hive-decide) below): the hive decides as the owner's delegate and records it the way a person's decision is recorded. For a PR: *Approve (/lgtm + /approve)* posts those two commands as comments attributed to you and records your approval against the PR's current head as the human answer the row was parked for — a `requires_human` / decision-requested reviewer verdict, the hive's own `needs-human` park (a supersession question, say), the `sentinel-alert` label and the evidence bundle's sentinel findings are all released by it (the response's `released` lists what was: `approval-recorded`, `needs-human`, `sentinel-label`, `sentinel-acknowledged`), the stage engine re-derives the stage on the spot and the tracker cache is dropped — and the sentinel sweep honours the release for that head, re-arming only on a new push — so the row leaves *Needs human decision* for 080 *Awaiting merge admission* on the next read; the merge gate (and the auto-merge lanes, which honour the recorded approval on that head exactly as a trusted merger's GitHub Approve review) takes it from there; the dashboard never reviews as you and never merges. On a Prow repo where the hive bot is not an approver, Prow has already answered the hive's earlier commands with "restricted to collaborators" / "NOT APPROVED"; a later Approve then records the decision without posting the two comments again and says so (`commands_skipped: prow-rejects-bot-commands`, and the toast reads *Approval recorded; `/lgtm` + `/approve` not posted*). A new head carries no answer: pushing to the PR re-asks. The delegated *Let hive decide* path still posts the comments only — it recommends and never clears a sentinel alert — *Close as superseded by…* (a picker listing the other open PRs in the repo, or type `#458` / `owner/repo#458` / a URL; then an optional comment, then the sweep's labelled close), *Won't fix…* (a comment naming you, then a not-planned close) and *Give direction…* (your text is recorded as `/hive decision ` against the head, so the author agent pushes a new head; only on PRs the hive opened). For an issue: *Approve direction* (records the same `/hive approve` a maintainer would comment: the parking labels come off, `approved-direction` goes on, the stage engine logs the un-park and a reply comment names you; nothing is merged), *Send back to the hive…* (`/hive decision `), *Close as superseded by…* and *Close as won't-fix…*. A selection mixing parked issues and parked PRs has Decide… greyed; **Hold** or **Release hold** depending on the `hold` chip (the same `…/items/{number}/hold` toggle as the repository cards, and the same permission — owner, or a signed-in user with write access to the repo); **Re-evaluate now** (the hive-wide read-only rescan behind the Overview's refresh — a rescan already running is reported, not duplicated); **Accept triage** (only on an issue the chevron calls *Triage needed* — stage 015, or the `needs-triage` chip on another stage — never on a PR; the one-click form of a maintainer accepting triage on GitHub: adds the reporter-trust triage label, `triage/accepted` by default or the first of `issue_filter.reporter_trust.untrusted_require_labels`, removes the awaiting label `needs-triage` / `reporter_trust.awaiting_label` and a Hive-written `stage/015-triage-needed`; the row re-reads *Actionable* with the 👤 gone at once, the reporter-trust gate admits the issue on its next poll and the stage engine records 015 → 010; same permission as Hold, audited as `repo_item_accept_triage`, a no-op when already accepted); **Mark needs-human…** (hidden once the chip is on; merger or owner role, applies `needs-human` and a marker comment through the review pipeline's send-to-human endpoint); **Open on GitHub**; **Copy link**. Entries you may not use are greyed with the reason in their tooltip, never hidden. Confirmation and free-text input use the dashboard's own dialogs — no browser prompts. Every action is an existing audited dashboard endpoint (the decision endpoint is `POST /api/repos/{owner}/{repo}/items/{number}/decision` and triage acceptance is `POST /api/repos/{owner}/{repo}/items/{number}/accept-triage`, see [api-reference.md](https://github.com/hivecommons/hive/blob/v5/src/docs/api-reference.md)); none of them bypasses the hive's governance or merges anything.
-**What a row tells you without a click (`tracker-cues`).** The owner's framing (2026-10-28): "Hive is doing the right things mostly; we now need to keep the user informed of it — like a video game they can watch and see what happens easily." Every row therefore reads, in this order, *where it is → whether work is happening on it → what it waits on → what just happened*, with at most three cue types drawing the eye and nothing else blinking. **Where it is** is the lit chevron (the state pill). **Working** — the chevron's icon turns slowly and the pill pulses only while a hive agent is actually on the item right now (the status payload's `agents[].lastAction` for the current round, matched to the row or to the PR shown on its row); its tooltip then leads with *Working now: scout commented · 2m ago —* before the stage words. A still chevron means **waiting**; its tooltip already says where it stands and whose move it is (*Needs decision — … · Next move: human*). **Acting** — from the moment you choose a row action the affected row (and the nested PR sub-row when the action targets it) draws faint stripes over its lit chevron and an *Approving… / Hive is deciding… / Holding…* caption with a small spinner; it clears when the tracker reports the target in a new state, at once on a no-op or an error, and never later than three minutes. **Event line** — one short plain sentence under the strip about the latest thing that happened to the row, from the difference between two tracker payloads (the poll / SSE refetch the tracker already makes; no extra requests): *Hive: moved to CI / Review · 12s ago*, *Hive: fell back to Claimed — a reviewer requested changes*, *Hive: waiting on a person*, *Hive: hold released*, *You: approved*. It lives two minutes, dims for its last third, and a row carries only its latest line. **Set back** — a row wearing `regressed` colours the line's lead red. **Merged** — see *Merged (the chip, the ledger, the flight)* below. The CSS class names are shared so the sibling Activity Monitor PRs (needs-human options, decision trail) compose on them: `cue-working`, `cue-acting`, `cue-event`, `cue-setback`, `cue-merged`, `cue-decision` (badge, sibling PR), `cue-spin` (the one spin keyframe), `cue-flight` / `ham-merged-count` (the glyph and the Merged chip); the colour tokens `--cue-state`, `--cue-work`, `--cue-decision` on `.ham-row` are declared in the *Cue tokens* paragraph under [Let hive decide](#let-hive-decide) below. Legend, in the operator's words: **working** (icon turns), **waiting on X** (still; tooltip names X), **set back by Y** (red lead on the event line), **merged** (✓ glows, a bee rises to the Merged chip or off the top of the screen). Under `prefers-reduced-motion: reduce` every cue animation stops; the words stay.
+**What a row tells you without a click (`tracker-cues`).** The owner's framing (2026-10-28): "Hive is doing the right things mostly; we now need to keep the user informed of it — like a video game they can watch and see what happens easily." Every row therefore reads, in this order, *where it is → whether work is happening on it → what it waits on → what just happened*, with at most three cue types drawing the eye and nothing else blinking. **Where it is** is the lit chevron (the state pill). **Working** — the chevron's icon turns slowly and the pill pulses only while a hive agent is actually on the item right now (the status payload's `agents[].lastAction` for the current round, matched to the row or to the PR shown on its row); its tooltip then leads with *Working now: scout commented · 2m ago —* before the stage words. A still chevron means **waiting**; its tooltip already says where it stands and whose move it is (*Needs decision — … · Next move: human*). **Acting** — from the moment you choose a row action the affected row (and the nested PR sub-row when the action targets it) draws faint stripes over its lit chevron and an *Approving… / Hive is deciding… / Holding…* caption with a small spinner; it clears when the tracker reports the target in a new state, at once on a no-op or an error, and never later than three minutes. **Event line** — one short plain sentence under the strip about the latest thing that happened to the row, from the difference between two tracker payloads (the poll / SSE refetch the tracker already makes; no extra requests): *Hive: moved to CI / Review · 12s ago*, *Hive: fell back to Claimed — a reviewer requested changes* (a lower stage number reads as a fall only when the stage table's `falls_to` — the stage engine's regression rule — says so; triage accepted 015 → 010, a decision made 085 → 080 or the author resuming 072 → 070 read *moved to*), *Hive: waiting on a person*, *Hive: hold released*, *You: approved*. It lives two minutes, dims for its last third, and a row carries only its latest line. **Set back** — a row wearing `regressed` colours the line's lead red. **Merged** — see *Merged (the chip, the ledger, the flight)* below. The CSS class names are shared so the sibling Activity Monitor PRs (needs-human options, decision trail) compose on them: `cue-working`, `cue-acting`, `cue-event`, `cue-setback`, `cue-merged`, `cue-decision` (badge, sibling PR), `cue-spin` (the one spin keyframe), `cue-flight` / `ham-merged-count` (the glyph and the Merged chip); the colour tokens `--cue-state`, `--cue-work`, `--cue-decision` on `.ham-row` are declared in the *Cue tokens* paragraph under [Let hive decide](#let-hive-decide) below. Legend, in the operator's words: **working** (icon turns), **waiting on X** (still; tooltip names X), **set back by Y** (red lead on the event line), **merged** (✓ glows, a bee rises to the Merged chip or off the top of the screen). Under `prefers-reduced-motion: reduce` every cue animation stops; the words stay.
**Result toasts (`runRowAction`).** Every row action — *Let hive decide*, *Approve*, *Give direction*, *Close as superseded*, *Won't fix*, *Approve direction*, *Send back to the hive*, *Accept triage*, *Hold* / *Release hold*, *Re-evaluate now*, *Mark needs-human* — goes through one SPA helper, `runRowAction(action, row)`, which marks the row acting, sends the request, and renders a toast from the server's shared `result` object (additive on every endpoint's existing response: `{action, target: {repo, kind, number, url}, outcome, next, rule?, applied_at?}`, `action` one of `adopted | approved | recommended | closed | directed | refused | held | released | rescanned | triage-accepted | needs-human | noop-already-applied`). The toast reads, in order, *what happened → on what → what happens next*: a headline naming the clicked row and, when it differs, the target (*Hive decided on acme/console#14938 → PR #14947: adopted (contributor offline)*), the outcome sentence, the ⚠ sentinel line when the head carries one, the *next* line (*The hive rebases and merges it unattended.*), an explicit ✕ and an *Open PR #14947 ↗* link. It does **not** auto-dismiss: it stays until the tracker reports the target row in a new stage, caption or chip set (or gone), and up to five such toasts stack for pending decisions. A no-op says so plainly — *No change — hive already recorded this decision at 12:01Z; still blocked by needs-human* — and clears the acting state at once; an error toast says what failed and why (*Hold failed on acme/console#7 — write permission required*) and is sticky but not tied to a row. Older hives that answer without `result` still get a toast built from the legacy fields.
@@ -714,35 +727,43 @@ Fed by `GET /api/stage-tracker`. Every issue and pull request follows the same p
**One row per PR.** A pull request that closes several open issues (`Closes #A` and `Closes #B`) is drawn once, under the issue the server names in each linked entry's `drawn_under` (its `nested_under`, the lowest-numbered open issue it closes). The other issues keep their own strip and show a compact **↳ #N · under #M** chip beside the title (tooltip: *PR #N covers this issue too; its row is drawn under #M*) instead of a second identical sub-row, so hivecommons/hive#14921 appears once under #14910 and #14911 points at it. Payloads without `drawn_under` draw as before.
-**Merged (the chip, the ledger, the flight).** A thin bar at the top of the tracker — it stays pinned under the card header while the list scrolls, with the stage column header strip pinned just below it — carries a small hex glyph with **Merged N**: every pull request the hive tracked that reached merged, *all time*, with *N today · N this week* beneath. The number is the server's, not the tab's: the hive keeps a **merged ledger** at `/data/merged-ledger.json` (entries for 90 days, an all-time total that only grows, atomic writes, seeded on first boot from the audit log's `pr_merged` history with the PR throughput counter as the floor, then backfilled by the first closed sweep, which on such a boot asks GitHub for every PR closed in the last 30 days — merges by people and other bots the audit log never saw — instead of its usual one day; so the ledger counts every merged PR in a configured repository, and its all-time number is expected to run above the hive-observed PR throughput counter it was seeded from) fed by the audit log's `pr_merged` lines — the hive's own merges on every path and the poller's observations of merges by people or other bots — and by webhook / closed-sweep snapshots of a PR GitHub reports merged, which carry the title and the merger's login within seconds. `GET /api/stage-tracker` returns it as `merged{today, total_7d, total_30d, all_time, recent[≤20], tz, day_start}`; "today" rolls at midnight in the spoke's zone (`TZ`, else UTC) and the chip hydrates from it on the first render, so a reload, a new tab or a roll shows the same count (hivecommons/hive#15147 — the operator watched 4 become 0). Within a tab the number never goes down; a merge the tab just watched is added on top until the server's list carries it, and that list is also what settles a vanished row as *merged* rather than closed (alongside `recently_merged`), so a human's merge celebrates too. **Click the chip** for a popover of the recent merges — number (linked), title, *by hive* / *by @login*, lane, time — and the 30-day total; it is also where a merged row's trace lives once the row has left. When a PR on the list merges its row does **not** simply vanish on the next refresh: it stays pinned for about 2.5 s, its ✓ cell glows amber, the event line reads *Hive: merged #14947 · by hive*, and — in the operator's words — "an animation originates from the item's row where the check mark is and flies up to the top of the browser border; if the Merged pill is visible the animation stops there, if not we just see it go off the screen": a 🐝 (or **+1**, ⚙ → Merges → *Glyph*, `hive.tracker.celebrate_glyph`) rises out of the ✓ cell for ≈0.9 s with a slight arc, lands on the chip when the chip's box is inside the viewport (the count bumps with a soft scale-pop and the merge zap, when sound is on, plays as it lands) or exits through the top edge otherwise (the count still bumps). It works at any scroll position (`position: fixed`, `pointer-events: none`). At the same moment a **banner** appears at the bottom right — *✓ #15067 merged — fix: enable bulk delegation… (adopted, by hive, 17:42)* — and stays until dismissed or 30 s, stacked (newest at the bottom, at most five) when several merge together, with **Show row** (scrolls to the row and glows it again; once the row has left, says so and opens the list), **Open on GitHub** and **All merges**. Several merges in one refresh stagger 200 ms apart with at most three glyphs in flight (the rest bump the count without flying); a merged PR that is filtered out or on another page still counts and still gets its banner. Under reduced motion nothing flies: the glyph fades in place, the count bumps, the row glows, the banners appear without sliding. A PR drawn inline on its issue's row (the usual case — one covering PR, no sub-row) celebrates on that issue row: every row stamps the PR keys it draws (`data-ham-prs`) and the SPA remembers each PR's host row from the last payload, so the flight has somewhere to start from even when the merge also closes the issue. The ⚙ dialog's **Merges** section has **Celebrate merges** (default on, `hive.tracker.celebrate`); off, the row leaves as before and the zap plays at once. **Test celebration** beside it closes the dialog, picks the first row currently in the viewport (scrolling one into view if none is) and plays one flight from it with a *(test)* banner — pop and zap, the count itself unchanged — so the effect can be checked without waiting for a merge; under reduced motion it says so and only pops. The row effect is reusable in the SPA as `celebrateRow(rowEl, {kind: 'merged' | 'closed-by-merge'})`. Class names: `ham-sticky` (the pinned bar), `ham-merged-count` / `ham-merged-pop` (chip, popover), `cue-flight` / `cue-flight-fade` (the glyph), `merge-banners` / `merge-banner`.
+**Merged (the chip, the ledger, the flight).** A thin bar at the top of the tracker — it stays pinned under the card header while the list scrolls, with the stage column header strip pinned just below it — carries a small hex glyph with **Merged N**: every pull request the hive tracked that reached merged, *all time*, with *N today · N this week* beneath. The number is the server's, not the tab's: the hive keeps a **merged ledger** at `/data/merged-ledger.json` (entries for 90 days, an all-time total that only grows, atomic writes, seeded on first boot from the audit log's `pr_merged` history with the PR throughput counter as the floor, then backfilled by the first closed sweep, which on such a boot asks GitHub for every PR closed in the last 30 days — merges by people and other bots the audit log never saw — instead of its usual one day; so the ledger counts every merged PR in a configured repository, and its all-time number is expected to run above the hive-observed PR throughput counter it was seeded from) fed by the audit log's `pr_merged` lines — the hive's own merges on every path and the poller's observations of merges by people or other bots — and by webhook / closed-sweep snapshots of a PR GitHub reports merged, which carry the title and the merger's login within seconds. `GET /api/stage-tracker` returns it as `merged{today, total_7d, total_30d, all_time, recent[≤20], tz, day_start}`; "today" rolls at midnight in the spoke's zone (`TZ`, else UTC) and the chip hydrates from it on the first render, so a reload, a new tab or a roll shows the same count (hivecommons/hive#15147 — the operator watched 4 become 0). Within a tab the number never goes down; a merge the tab just watched is added on top until the server's list carries it, and that list is also what settles a vanished row as *merged* rather than closed (alongside `recently_merged`), so a human's merge celebrates too. **Click the chip** for a popover of the recent merges — number (linked), title, *by hive* / *by @login*, lane, time — and the 30-day total; it is also where a merged row's trace lives once the row has left. When a PR on the list merges its row does **not** simply vanish on the next refresh: it stays pinned for about 3.5 s, its ✓ cell glows amber, the event line reads *Hive: merged #14947 · by hive*, and — in the operator's words — "an animation originates from the item's row where the check mark is and flies up to the top of the browser border; if the Merged pill is visible the animation stops there, if not we just see it go off the screen": a 🐝 (or **+1**, ⚙ → Merges → *Glyph*, `hive.tracker.celebrate_glyph`) rises out of the ✓ cell for ≈2 s on an ease-out with a slight arc, lands on the chip when the chip's box is inside the viewport (the count rolls up like a mileage odometer — each digit that rose slides up through the digits it passes over ≈0.9 s — with a soft scale-pop, and the merge zap, when sound is on, plays as it lands) or exits through the top edge otherwise (the count still rolls). *N today · N this week* roll the same way; a number that falls (today → 0 at midnight), the first draw after a reload, and reduced motion just set the value, and the pill's width never changes mid-roll (the plain text underneath sets it). It works at any scroll position (`position: fixed`, `pointer-events: none`). At the same moment a **banner** appears at the bottom right — *✓ #15067 merged — fix: enable bulk delegation… (adopted, by hive, 17:42)* — and stays until dismissed or 30 s, stacked (newest at the bottom, at most five) when several merge together, with **Show row** (scrolls to the row and glows it again; once the row has left, says so and opens the list), **Open on GitHub** and **All merges**. Several merges in one refresh stagger 200 ms apart with at most three glyphs in flight (the rest bump the count without flying); a merged PR that is filtered out or on another page still counts and still gets its banner. Under reduced motion nothing flies: the glyph fades in place, the count bumps, the row glows, the banners appear without sliding. A PR drawn inline on its issue's row (the usual case — one covering PR, no sub-row) celebrates on that issue row: every row stamps the PR keys it draws (`data-ham-prs`) and the SPA remembers each PR's host row from the last payload, so the flight has somewhere to start from even when the merge also closes the issue. The ⚙ dialog's **Merges** section has **Celebrate merges** (default on, `hive.tracker.celebrate`); off, the row leaves as before and the zap plays at once. **Test celebration** beside it closes the dialog, picks the first row currently in the viewport (scrolling one into view if none is) and plays one flight from it with a *(test)* banner — pop and zap, the count itself unchanged — so the effect can be checked without waiting for a merge; under reduced motion it says so and only pops. The row effect is reusable in the SPA as `celebrateRow(rowEl, {kind: 'merged' | 'closed-by-merge'})`. Class names: `ham-sticky` (the pinned bar), `ham-merged-count` / `ham-merged-pop` (chip, popover), `cue-flight` / `cue-flight-fade` (the glyph), `merge-banners` / `merge-banner`.
**Multi-select.** The checkbox in a row's left gutter selects the row — and the checkbox in a nested PR sub-row's gutter selects that PR on its own, so one or more PRs under an issue can be picked without the issue or each other; **Shift+click** selects the range from the last clicked row (sub-rows are in the range order right after their issue), **Cmd/Ctrl+click** toggles, **Cmd/Ctrl+Shift+click** adds a range to what is selected; with a row focused, **Space** toggles it (Shift+Space extends). Cmd/Ctrl- or Shift-clicking the row itself, away from its links, selects it too. A selection bar appears above the list with the count, **Clear**, and the same actions for the whole selection — *Decide…* (opens the group as a menu, *Let hive decide* first — each selected row gets its own delegated-decision job, see below), **Let hive decide (N selected)** (one click for the whole selection: one delegated-decision job per selected item, each nested PR sub-row by its own number, with one summary toast — "3 sent · 2 decided · 1 no-op", plus "· 1 failed" when any failed — and the per-action confirm / *Don't ask again* of *Let hive decide*), *Hold*, *Release hold*, *Re-evaluate now*, *Mark needs-human…*, *Copy links* — each enabled only when it is valid for every selected row and otherwise greyed with the first reason ("Not applicable to acme/console#12", "Sign in with write access…"). Right-clicking a selected row acts on the whole selection. Bulk actions run one request per row, in list order, under a progress toast ("Hold 2/5 — acme/console#12…"); the result toast lists each failure by row and the successes stand, after which the list and the status snapshot refresh (a hive-wide rescan is sent once per batch). Selection is keyed on the row's item (repository, kind, number), not its node, so the live re-render keeps it; a row that leaves the list — merged, closed, filtered or paged out — leaves the selection.
**Needs-human decision popover.** Hovering — or, by keyboard, focusing — the *Needs human decision* chevron word on a row (an issue at 045, a PR at 085, an issue whose nested PR is parked there) or a lead *needs-human* / *triage needed* pill opens an in-app popover (no native tooltip on that element) that says, in this order: **what the row is waiting on** — the reviewer's own question when the hive has one (the text the hive's "Needs a decision" comment on the PR prints after *reviewer question:*, read from the same verdict record), else a plain-words sentence for the park reason, plus the blocker in plain words ("A reviewer requested changes on b0f5f80 — needs a new commit. Approving cannot override that."); **what you can do** — one button per applicable action, *Let hive decide* first, then Approve / Give direction / Close as superseded / Close as won't fix / Hold or Release hold (an issue at 015: Accept triage, Hold, Mark needs-human), each disabled with its reason when the row's state or your role rules it out; **who asked and when** ("Asked by a reviewer · 2 hours ago"); then the chevron's stage line. The buttons run the row context menu's code path, so the same result toast and in-flight chevron apply. The popover stays open while the pointer is over it or its trigger, pins on click or focus, and closes on Esc (focus returns to the trigger), click-away, scroll or resize. The text comes from the tracker payload's `decision_needed` (`question`, `asked_by`, `since`, `blocker`, `blocker_reason`, `options[] {action, label, enabled, reason_if_disabled}`), whose `action` ids are the context menu's own (`decide:delegate`, `decide:approve`, …).
+**Blocked pill — capability gaps** ([#15099](https://github.com/hivecommons/hive/issues/15099)). An issue parked because the hive *cannot* act — the scanner reported "the App token cannot push `.github/workflows/*` (no `workflows` permission); needs a human" — is a blocker, not a question, so its row does not read *Needs human decision*: the pill reads **Blocked · App lacks workflows permission** and the same popover says in plain words *Hive can't do this one: the GitHub App has no `workflows` permission, so it cannot push changes to .github/workflows/\*.* followed by the ways out: *Grant the App workflows permission* (opens the App settings page; the hive re-tries on its own once it sees the permission), *Re-check permissions* (probes the installation now and releases every item blocked on it), *Prepare the patch for a person to push* (disabled with its reason until a human-authorised push identity exists), *Mark partially addressed and close*, *Keep waiting*. *Let hive decide* stays in the list but disabled with the sentence that explains why — a direction decision cannot grant a permission — instead of being greyed silently; running it anyway returns *no-effect* with the same words and never applies `approved-direction`. The loop that motivated this (un-park → sweep re-parks with the same text → "I tried let hive decide and it still needs human") is guarded: when the same needs-human reason comes back after a delegated un-park, the row is parked with a blocker and *Let hive decide* is disabled on it until the reason changes. The blocker comes from the agent's `` marker or, failing that, from the prose of the needs-human report (`pkg/blocker`), and ⚙ → Security shows the fleet view: *GitHub App permissions: missing `workflows` · N items blocked* with the settings link (`/api/status.capabilities`).
+
**Hold pill with provenance.** When the hive can say who set a row's `hold` label and why, the pill does not read a bare *hold*: it reads **Held by hive · reporter trust (issue #15017 by @Karibhou, contributor)** when the hive's own reporter-trust gate ([#9665](https://github.com/hivecommons/hive/issues/9665)) set it, **Held by hive** when the hive set it for another reason, or **Held by @login** when a person added the label; a hold the hive cannot attribute keeps the plain *hold* word. The hover text repeats the pill, says since when, and for a hive-set reporter-trust hold explains that the hive set it itself and that *Release hold* lifts it. That same pill opens the needs-human decision popover: its question reads *The hive held this PR itself for reporter trust (issue #N by @X, contributor). Release the hold, or keep it?*, **Release hold** leads the buttons (enabled), *Let hive decide* waits until the hold is released, and `blocker_reason` is `hold:hive`. A person's hold names them in the blocker instead (*On hold (set by @login) — the hive does nothing until the hold is released.*). The data rides the tracker payload as `hold {by, source, reason, since, issue, reporter, association, pill}` on the item and on each nested `linked_pr`; `by` is `hive` or the login of the person whose label event set it, `source` is `reporter-trust` or `label`. It comes from the stage-label pass, which reads the hive's `` notice and the newest `hold` label event ([#15020](https://github.com/hivecommons/hive/pull/15020): a hive-authored PR sat 49 minutes as *hold +1 · Needs human decision* with no way to tell that the hive had held it two seconds after opening it). A hive-authored PR never keeps a hive-set reporter-trust hold: the same pass releases it, so the pill is normally seen only on contributor PRs.
-**Merge gate pills — legend** ([#15073](https://github.com/hivecommons/hive/issues/15073)). Four stages share the **Merge gate** column, and until this fix three of them (060, 080, 100) all read *Ready for auto merge* — a 060 PR whose CI was still running read as ready, and a spinning glyph that appeared on one such row and not its neighbour was the *working* cue (an agent's `lastAction` on that PR this round), unlabelled, so the operator read it as merge progress. Every PR at the gate now carries `merge_gate: {state, reason, sweep_running, last_sweep_at, next_sweep_at, soak_until, soak_remaining_seconds}` in the payload (mirrored on the issue's nested `linked_prs`), and the lit chevron reads one word per state, in pipeline order, with the live detail after a `·`:
+**Merge gate pills — legend** ([#15073](https://github.com/hivecommons/hive/issues/15073)). Three stages share the **Merge gate** column (080, 095, 100; the column starts at 080 — 060 *reviewed* is pre-admission, it can still regress to 070, so it lights **CI / Review** with the word *Review passed*), and until this fix three of them (060, 080, 100) all read *Ready for auto merge* — a 060 PR whose CI was still running read as ready, and a spinning glyph that appeared on one such row and not its neighbour was the *working* cue (an agent's `lastAction` on that PR this round), unlabelled, so the operator read it as merge progress. Every PR at the gate now carries `merge_gate: {state, reason, sweep_running, last_sweep_at, next_sweep_at, soak_until, soak_remaining_seconds}` in the payload (mirrored on the issue's nested `linked_prs`), and the lit chevron reads one word per state, in pipeline order, with the live detail after a `·`:
| stage | pill word | detail after `·` | glyph before the word (tooltip + `aria-label`) |
| --- | --- | --- | --- |
-| 060 reviewed | **Awaiting merge gate** | what holds it, from the lane's reason — *CI still running (build-and-test)*, *CI is red (lint)*, *branch has conflicts*, … | 🕓 clock — *Review approved, not yet admitted to auto merge — CI still running (build-and-test). hive re-checks the gate on its next sweep in 3m. A GitHub event on the PR (check run, review, push) triggers a pass sooner. Last checked 2m ago.* |
+| 060 reviewed — drawn in the **CI / Review** column, not the merge gate | **Review passed** | the lane's own recorded reason, in the operator's words — *CI in progress* (`N check(s) still running`, `check-pending`), *checks unreadable* (`checks could not be read`), *soak 30m remaining* (pre-admission soak); *waiting for merge admission* when the payload carries no reason | 🕓 clock — *Review passed, not yet admitted to auto merge — CI still running (build-and-test). hive re-checks the gate on its next sweep in 3m. A GitHub event on the PR (check run, review, push) triggers a pass sooner. Last checked 2m ago.* |
| 080 awaiting merge admission | **Ready for auto merge** | *next sweep in 42s* (counts down in place; *sweep due now* once past) | 🕓 clock — *Approved and green; waiting for hive's merge sweep — next sweep in 42s. …* |
| 095 pre-merge soak | **Soaking** | *7m left* (counts down; *soak window complete* once past) | 🕓 clock — *Admitted by the automerge lane; waiting for the soak window: 7m left. hive merges on its first sweep after that.* |
-| any of the above while a pass is evaluating this PR | the stage's word | *merge sweep running* | ⟳ spinning — *hive's merge sweep is running on this PR now.* |
-| 100 merge queue | **Merging** | — | ⟳ spinning — *Admitted and soaked; hive is merging it now (GitHub merge queue / merge window).* |
+| any of the above while a pass is evaluating this PR | the stage's word | *merge sweep running* | ⟳ (static) — *hive's merge sweep is running on this PR now.* |
+| 100 merge queue | **Merging** | — | ⟳ (static) — *Admitted and soaked; hive is merging it now (GitHub merge queue / merge window).* |
+| any stage while hive's merge call is in flight (`merge_gate.merging_since`, `merge_path`) | **Merging** | *merge call in flight*, then *merge landed* while the SPA holds the row | ⟳ (static) — *hive is merging it now (automerge sweep) — the merge call has been in flight since 3s ago.* |
+| any stage after GitHub refused hive's merge call (`merge_gate.state: merge-failed`, `merge_failed`, `merge_failed_at`) | **Merge failed** | the refusal's first line — *405 Pull Request is not mergeable* | 🕓 clock — *hive tried to merge this (maintainer merge from the dashboard) 9s ago and GitHub refused: …. The lane re-evaluates it on its next pass.* |
| 110 | **Merged** (the ✓ cap) | — | — |
-An **issue row** whose covering PR is at the gate reads the gate from that nested PR (`linked_prs[].merge_gate`), so the issue row and its PR sub-row always show the same word and detail; the issue's own lane reason — *open PR #15090 covers it (PR at 060-reviewed)* — is never a gate reason and never appears in the pill (it did briefly after [#15084](https://github.com/hivecommons/hive/pull/15084), as a two-line clipped pill — [#15101](https://github.com/hivecommons/hive/issues/15101)); it leads the glyph's tooltip instead, as *Open PR #15090 covers it · at 060-reviewed — Review approved, …*.
+**Merging is held for the toast's lifetime** ([#15245](https://github.com/hivecommons/hive/issues/15245)). The sweep evaluates and merges inside one pass, so a PR used to go from 060/090/095 to ✓ between two 30 s polls and *Merging* was never on screen. Every merge path (sweep, dashboard *Approve & merge*, relay, forge) now registers the attempt in `pkg/github` before the GitHub call and its outcome after; the tracker payload reads the registry on every build, lights the PR at 100 **Merging** with `merge_gate.merging_since` while the call is in flight (and while a just-merged PR is still listed), and says **Merge failed** with the reason when GitHub refused. The SPA keeps a hold per PR from the first moment it saw *Merging* — or, for a merge it never saw coming, from the poll that reported it merged — and for `STAGE_TRACKER_MERGING_HOLD_MS` (= `STAGE_TRACKER_MERGE_BANNER_MS`, 30 s) the row stays on the board at 100 *Merging · merge landed* before it advances to ✓ and the banner, glyph flight and chip bump fire. A refused merge drops the hold at once.
-The glyph is drawn for **every** row on the gate, so two rows in the same state always look the same; the ⬡ / 🤖 / 👤 icon *after* the word is whose move it is (`STAGE_TRACKER_ACTOR`), unchanged. The *working* spin (an agent on the item right now) stays a separate cue and now also carries its sentence as tooltip and `aria-label` (*Working now: reviewer reviewed it · 2m ago*). `next_sweep_at` comes from the automerge lanes' sweep clock (`automerge.SweepSnapshot()` — the next periodic floor pass, 5 min event-driven or the legacy 10 s tick); webhook-named passes run sooner, which the tooltip says. `last_sweep_at` is the stage record's `last_seen` (when a lane last evaluated the PR).
+An **issue row** whose covering PR is at the gate reads the gate from that nested PR (`linked_prs[].merge_gate`), so the issue row and its PR sub-row always show the same word and detail; the issue's own lane reason — *open PR #15090 covers it (PR at 060-reviewed)* — is never a gate reason and never appears in the pill (it did briefly after [#15084](https://github.com/hivecommons/hive/pull/15084), as a two-line clipped pill — [#15101](https://github.com/hivecommons/hive/issues/15101)); it leads the glyph's tooltip instead, as *Open PR #15090 covers it · its PR is review passed · waiting for merge admission — Review passed, …*.
+
+No glyph on a chevron or pill rotates (owner, 2026-10-28: "i dont like the status symbols rotating — remove them from all chevrons", [#15187](https://github.com/hivecommons/hive/issues/15187)): the ⟳ sweep glyph and the *working* glyph are static icons whose tooltips say what the hive is doing. The glyph is drawn for **every** row on the gate, so two rows in the same state always look the same; the ⬡ / 🤖 / 👤 icon *after* the word is whose move it is (`STAGE_TRACKER_ACTOR`), unchanged. The *working* spin (an agent on the item right now) stays a separate cue and now also carries its sentence as tooltip and `aria-label` (*Working now: reviewer reviewed it · 2m ago*). `next_sweep_at` comes from the automerge lanes' sweep clock (`automerge.SweepSnapshot()` — the next periodic floor pass, 5 min event-driven or the legacy 10 s tick); webhook-named passes run sooner, which the tooltip says. `last_sweep_at` is the stage record's `last_seen` (when a lane last evaluated the PR).
**Every ask has its action (the lead entries).** Owner, on hotshot#317 at *Ready for human merge · untrusted author* (2026-10-29): "this claims it needs human merge but there is no option to take that action" ([#15078](https://github.com/hivecommons/hive/issues/15078)). Whenever a row — an issue row whose one nested PR is parked, or a PR sub-row — is in a state that asks a person for something, the ⋯ menu **leads** with the matching action(s), taken straight from the payload's `decision_needed.options`, each with its consequence as the hint; the generic entries (*Decide…*, *Hold*, *Re-evaluate now*, *Mark needs-human…*, *Open on GitHub*, *Copy link*) follow under a separator, and the needs-human popover shows the same options with the consequence under each button (`ham-decision-consequences`). A Go table test (`TestStageTrackerNeedsHumanStatesCarryAnAction`) keeps every state `stageTrackerNeedsHuman` flags on a non-empty options list with at least one enabled action (or a disabled one whose `reason_if_disabled` is plain words), and renderer tests (`TestStageTrackerRowActionsLeadWithDecisionOptions`, `TestStageTrackerIssueRowWithNested090PRCarriesAllThree` — the issue row over its one nested 090 PR carries Approve & merge, Approve — let hive merge and Let hive decide) keep the menu leading with them. *Let hive decide* is on every needs-human state: where the hive cannot act it says so in plain words through the no-effect toast instead of silently recording a decision. State by state:
| State the pill shows | Lead actions in ⋯ and in the popover (consequence) |
|---|---|
| PR at 090 *Ready for human merge* (untrusted author, workflow files, manual-merge path, branch protection…) | **Approve & merge as maintainer** (merges #N into *base* now via hive's identity, attributed to you — a comment on the PR names you, the audit log records `repo_item_decision_merge`, `source: dashboard`) · **Approve — let hive merge** (records your approval as the owner decision — an APPROVE review and the automerge label; the hive's merge sweep merges it on its next pass once CI is green and the head has soaked) · Give direction (hive-authored PRs only) · Close as superseded · Close as won't fix · Hold. Both merge actions need the merger or owner role and confirm first (with *Don't ask again*); the toast reads *merged* / *queued for merge*; the badge records `merge-now` / `queue-merge`. **Let hive decide** is here too (third): on a 090 PR it never runs the policy — the hive answers at once through the no-effect toast, *I can't merge this: author @Karibhou is untrusted on hotshot — …*, with *Approve & merge as maintainer · Approve — let hive merge* as buttons and the 👥 Contributors fix in the next line. **Either Approve is the trust override for that head** ([#15136](https://github.com/hivecommons/hive/issues/15136); owner, 2026-10-29: "I approved these and then they went to adopted…"): the stage record gets `admitted_by: operator-approval:`, the contributor lane merges the head as-is after checks and soak, and the adoption engine refuses to adopt or supersede it while the admission stands (the row reads *not adopted: a maintainer approved this head; the hive merges it as-is*; the 090 tooltip and popover say *Approved by @you as the owner decision…* and the second Approve greys out). When the record's commit check found commits without a `Signed-off-by`, the popover's blocker line and both Approve confirms say so **before** acting (*2 of 3 commits have no Signed-off-by, so a required DCO check refuses the merge*) and a third lead action appears, **Re-author as hive PR** — the explicit, separate choice: the hive re-homes the commits on a signed-off hive branch and opens a superseding PR; nothing is merged as-is. It is greyed with the reason on a person's PR. Renderer test: `TestStageTrackerReauthorAndApprovalCaveats`. |
-| PR at 085 *Needs human decision* | Let hive decide · Approve · Give direction · Close as superseded · Close as won't fix · Hold (unchanged). |
+| PR at 085 *Needs human decision* | Let hive decide · Approve · Give direction · Close as superseded · Close as won't fix · Hold (unchanged). When the row also wears **needs rebase**, **Rebase, then merge as maintainer** sits right after Approve (below). |
+| PR at 085 or 090 that also wears **needs rebase** (behind or conflicting with its base) | **Rebase, then merge as maintainer** replaces *Approve & merge as maintainer* — a plain merge would be a GitHub 405 — with the consequence *hive rebases #N onto base, waits for CI, then merges it with your approval on record; you'll get a toast when it lands or if the rebase conflicts*. The click records your approval first (/lgtm + /approve in your name), arms a follow-through, and kicks the rebase (GitHub update-branch for the hive's own PR when it is merely behind; otherwise the fixer lane, which now owns a parked PR with an armed follow-through); the endpoint answers `202 rebase-queued` and the toast reads *rebase queued, merge to follow*. The row then reads **Rebasing → merge queued (approved by @you)** with the working cue, *Rebased → waiting for CI, then merge* once the head moves, and the normal merge-gate pills after that. On green the hive merges the rebased head with your approval attributed — only a head the hive itself produced counts as the rebase; a head someone else pushes after your click settles the action as *failed* (*the head moved after your approval*) with **Approve the new head & merge** · **Close as superseded** as the ways out, nothing merged. On a conflict (or CI red, or no rebase within 2 h) the action settles as *failed* with the conflicting files and two ways out — **Adopt into fresh branch** · **Close as superseded** — on the row's badge and in a note on the PR. The ledger's `last_operator_action.followed[]` carries each step (`approval-recorded` → `rebase-kicked` → `rebased` → `ci-green` → `merged`, or `failed`). ([#14822](https://github.com/hivecommons/hive/issues/14822)) |
+| PR (hive-authored or a trusted author's) that is **conflicting** with its base | No click needed: the fixer lane rebases it on the hive's behalf, and the gate pill reads **Rebasing · attempt n/2** (spinning) — each fixer push that GitHub still reports dirty is the next attempt. After two the hive stops: the pill reads **Rebase failed · 2/2 — needs a person** and the row's menu still offers *Adopt into fresh branch* / *Close as superseded*. A merely `behind` PR gets GitHub's update-branch from its merge lane even while it wears prow's stale needs-rebase label. **Approve** on such a PR records the approval and opens the rebase episode in your name — the toast reads *Approved — #N conflicts with base; hive will rebase it next pass (attempt 1/2)* with a **Rebase, then merge as maintainer** button, never "the merge gate takes it from here"; the *Approving…* cue clears the moment the decision records. ([#15103](https://github.com/hivecommons/hive/issues/15103)) |
| Issue at 045 *Needs decision* (direction) | Let hive decide · Give direction · Approve direction · Send back to the hive · Hold (unchanged). |
| Human-authored PR at 071 *Needs author* / 072 *Returned to contributor* | Let hive decide · Close as superseded · Close as won't fix · Hold — the hive never rewrites a person's PR, so the choice is wait or close. (A hive-authored PR there carries *Adopt now* when adoption applies.) |
| `needs-human` label set by a person or the hive (any stage) | **Resolve — lift needs-human** (removes the label; on a PR also leaves the hive's /lgtm + /approve, on an issue a `/hive approve`) · Let hive decide · Hold. |
@@ -753,7 +774,7 @@ The glyph is drawn for **every** row on the gate, so two rows in the same state
**"Untrusted author", in plain words.** The 090 pill's tooltip now says *why* and *what would make it trusted*, per reason code. For the common `contributor-lane:author-association` (hotshot#317): *Untrusted author: @login is not a collaborator of this repository on GitHub (no owner, member or collaborator association) and holds no trusted or merger tier in this hive's contributor registry, so the hive will not merge their PR unattended. To trust them: give them the trusted or merger tier under 👥 Contributors (`auto_merge.contributor_lane.require_tiers`), or add them as a collaborator of the repository on GitHub. Until then a maintainer merges — Approve & merge as maintainer, or Approve — let hive merge, from the row's ⋯ menu.* The other trust reasons (`untrusted-author-role` → merger role under ⚙ Security / `dashboard.authorized_users`; `author-permission-missing` → `auto_merge.trusted_authors.require_github_permission`; `fork-non-member`; the claim-tier / no-claim / unverified-claim family → the 👥 Contributors tier) each get their own sentence. The hive App's own PRs are never "untrusted" — the lane only names a human author; the same text is the popover's question.
-**Cue tokens.** The three cues of the legend above (state pill, working spin, decision badge) share one `tracker-cues` CSS block on `.ham-row`: `--cue-state` (what the row waits on — the pill and this popover), `--cue-work` (the hive is working right now — the chevron spin), `--cue-decision` (what people and the hive decided — the ✓ badge). Nothing else blinks; motion stays in the 150–300 ms range.
+**Cue tokens.** The three cues of the legend above (state pill, working spin, decision badge) share one `tracker-cues` CSS block on `.ham-row`: `--cue-state` (what the row waits on — the pill and this popover), `--cue-work` (the hive is working right now — the chevron spin), `--cue-decision` (what people and the hive decided — the history badge). Nothing else blinks; motion stays in the 150–300 ms range.
### Let hive decide
@@ -767,9 +788,30 @@ The glyph is drawn for **every** row on the gate, so two rows in the same state
**Audit marker.** Every comment the delegate writes ends with `` (and a visible "Decided by … as the owner's delegate under the owner-delegate policy (rule `…`)" line), so a delegated decision can always be told from a human one on GitHub, in the Decisions log, and in any later count.
-**Plain words, everywhere.** Every rule, verdict and reason the delegate can show is rendered from one glossary (`src/pkg/delegatedecision/decision_glossary.go`, mirrored into the page as `STAGE_TRACKER_DECISION_GLOSSARY`; see [Labels → Decision glossary](/docs/hive/labels-and-control-signals#decision-glossary)): the toast says "adopted (the author's hive hasn't been connected since the nudge)", never "(contributor-offline)"; the `/hive decision` comment carries a four-line block — **Hive decided** / **Why** / **What happens next** (with "expected within 30 min") / **If it doesn't happen** — above the attribution and the marker; the needs-decision popover's blocker line uses the same words.
+**Plain words, everywhere.** Every rule, verdict and reason the delegate can show is rendered from one glossary (`src/pkg/delegatedecision/decision_glossary.go`, mirrored into the page as `STAGE_TRACKER_DECISION_GLOSSARY`; see [Labels → Decision glossary](/docs/hive/labels-and-control-signals#decision-glossary)): the toast says "adopted (the author's clanker hasn't been connected since the nudge)", never "(contributor-offline)"; the `/hive decision` comment carries a four-line block — **Hive decided** / **Why** / **What happens next** (with "expected within 30 min") / **If it doesn't happen** — above the attribution and the marker; the needs-decision popover's blocker line uses the same words.
+
+**When the decision doesn't take effect.** A recorded decision is expected to cause something — the park lifted, a merge, a close, a fresh hive-authored PR, a new commit — within `decisions.delegate.effect_timeout` (default **30m**). Clicking *Let hive decide* again inside that window answers *No change — hive already recorded this decision at : ()* and *Expected by : *. Past the deadline the answer is `result.action: "no-effect"`: *Hive decided at , but it hasn't taken effect: * (the time is rendered as local relative time — *at 11:40 today*, *yesterday 17:51* — never raw RFC 3339) — the why read from the lane reason and the PR's state (a reviewer asked for changes; the hive's own decision comment was read as a change request, the [#14822](https://github.com/hivecommons/hive/pull/14822) case; security-sensitive files need a maintainer; conflicts; red CI; sign-off missing; a hold; the author is still online so adoption cannot start; adoption is switched off) — and the toast carries the concrete next actions as buttons (`result.options`, the popover's option shape): **Approve & merge as maintainer** (the in-tracker action, when the operator has merge rights and the tracker exposes it; otherwise a link to GitHub), **Re-run decision** (posts the same request with `"force": true`, skipping the dedup), **Close as superseded**, **Send back for a new commit**. At the same time the hive posts one follow-up comment on the item — *Hive decided … but it hasn't taken effect / What should have happened / Why it hasn't / What you can do* — stamped `` so it is posted once and never read as a decision. The dashboard's own `/hive decision` comments (the delegated marker) are never parsed as a reviewer's change request, whichever login they were posted under.
+
+**One post-decision state machine** (hive's own spoke, build dd412e4, 2026-10-28: three *Let hive decide* rows showed a green ✓, a *Hive is deciding…* spinner and the unchanged striped *Ready for human merge · untrusted author* pill at once — "is that celebration? if so hive is no longer deciding?"). A row the operator has sent to the delegate is in exactly one of these states, and the ✓ badge, the pill, the chevron and the row's progress text are projected from the same record (`decided` on the tracker item: `action`, `verdict`, `rule`, `actor`, `at`, `effect`, `stage`, `acting`, `next_at`, `effect_timeout_s`, `superseded_by`, and the pre-rendered `word` / `text` / `title`):
+
+| state | what the row shows |
+| --- | --- |
+| **deciding** (request posted, no record yet) | the pill reads *Hive deciding…* with the working spin inside it; no badge. |
+| **decided, effect pending** | the spinner goes; the ✓ **decision badge** appears at the right (tooltip *You chose Let hive decide · hive decided: adopt (supersede) · 17:51 UTC · hive chose adopt-supersede; next: adoption pass in 4m*). It is **not** a merge celebration — the celebration is the cue-merged glow, the token fly and the Merged counter, none of which run here. The pill names the recorded decision's effect, never the stale park: *Adopted · opening hive copy…*, *Approving…*, *Sending back…*, *Fixing · hive is addressing @hanthor's review*; the chevron moves out of 090/085 to where hive now is (080 *Awaiting merge gate* for adopt/approve, 070 *Fixing* for a direction). *Approving…* is only ever drawn for an `approve` verdict. |
+| **effect landed** | the pill follows the live state (merge gate, *Adopted → #15123*, merged). |
+| **effect timed out** (`decisions.delegate.effect_timeout`) | the no-effect state above, with its options. |
+
+The toast and the decision trail lead with the outcome in one line — *Adopted — hive will rebase & merge it next pass (~4m)*; *Adopted: hive opened #15123 with the same commits and will merge it; #15029 will be closed as superseded* — details second, nothing repeated.
+
+**Preview before acting.** *Let hive decide* no longer acts on the click. It first calls `POST …/decide-delegated/plan` , a dry run with no side effects that answers `{would, summary, consequences[], alternatives[]}`, and shows a confirm popover: *Hive would: re-author #15029 as a hive PR with the same commits and merge it; #15029 will be closed as superseded. Why: author @Karibhou is untrusted and their clanker is offline.* Buttons: **Let hive do that** · **Approve & merge as-is** (when the operator has merge rights — the in-tracker maintainer merge when the row exposes it, else an `approve` decision) · **Cancel**. Nothing is written until the operator picks, and the badge tooltip and the trail record which button was pressed (the request's `chosen`).
+
+**Adoption pairs.** When the delegate adopts in supersede mode and hive opens its copy, the tracker shows the pair explicitly rather than two unrelated PR sub-rows: the original is dimmed — *Adopted → #15123 · superseded (hive re-authored with sign-off)* — with no action chevron and no needs-human styling; the new PR carries the single active chevron and a chip *↳ adopted from #15029 · you approved 17:51*; the issue row's pill follows the new PR. Once the original is closed it drops out of the open-PR projection, so it renders as one dimmed line from the adoption record — *#15029 · closed · superseded by #15123* — with no chevron track; never a blank numbered row.
+
+**A decision wins over a stale label.** Recording any decision (approve, direction, adopt, delegate-resolved) removes `needs-human` and the 045/085 *needs-decision* stage labels in the same write (`liftDelegatedParkLabels`; `pr_decision_operator.go`), and the projection never places a row in 085 while a decision exists for the current head — the row sits where hive actually is, and the popover leads with *You chose Let hive decide · hive decided: fix then merge*, not with the stale ask.
+
+**One vocabulary.** Every operator-facing string — event lines, popovers, toasts, pills — uses the chevron's stage vocabulary through one shared mapping (`stage_tracker_vocabulary.go`; `STAGE_TRACKER_STAGE_PHRASE` in the page): *its PR is review passed · waiting for merge admission*, never *PR at 060 reviewed* or *moved to stage/085-needs-decision*. A guard test scans rendered event-line fixtures for raw stage codes and fails on any.
-**When the decision doesn't take effect.** A recorded decision is expected to cause something — the park lifted, a merge, a close, a fresh hive-authored PR, a new commit — within `decisions.delegate.effect_timeout` (default **30m**). Clicking *Let hive decide* again inside that window answers *No change — hive already recorded this decision at : ()* and *Expected by : *. Past the deadline the answer is `result.action: "no-effect"`: *Hive decided at , but it hasn't taken effect: * — the why read from the lane reason and the PR's state (a reviewer asked for changes; the hive's own decision comment was read as a change request, the [#14822](https://github.com/hivecommons/hive/pull/14822) case; security-sensitive files need a maintainer; conflicts; red CI; sign-off missing; a hold; the author is still online so adoption cannot start; adoption is switched off) — and the toast carries the concrete next actions as buttons (`result.options`, the popover's option shape): **Merge as maintainer (on GitHub)** (a link; the dashboard never merges), **Re-run decision** (posts the same request with `"force": true`, skipping the dedup), **Close as superseded**, **Send back for a new commit**. At the same time the hive posts one follow-up comment on the item — *Hive decided … but it hasn't taken effect / What should have happened / Why it hasn't / What you can do* — stamped `` so it is posted once and never read as a decision. The dashboard's own `/hive decision` comments (the delegated marker) are never parsed as a reviewer's change request, whichever login they were posted under.
+**Their clanker, not their hive.** A contributor's agent is their **clanker**; the glossary, toasts, adoption reasons and presence pills say *the author's clanker hasn't been connected since the nudge*, never "the author's hive" (a test asserts the glossary carries no such string).
**Setting.** `decisions.delegate.enabled` (default **true** — the same default as the other "let the hive do it" switches); `decisions.delegate.repos..enabled` (or the bare repository name) overrides per repository. `decisions.delegate.llm.enabled` (default **false**) adds the LLM step described below. `decisions.delegate.effect_timeout` (default **30m**, a Go duration) is how long a recorded decision may go without its expected effect before a repeat click reports it as stuck. When the delegate is off the menu entry stays visible but the endpoint answers 403 naming the setting. See [operator-reference.md](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md).
diff --git a/docs/content/hive/documentation-map.md b/docs/content/hive/documentation-map.md
index 2a9efc38..1c16d065 100644
--- a/docs/content/hive/documentation-map.md
+++ b/docs/content/hive/documentation-map.md
@@ -45,6 +45,7 @@ Documentation for the current Hive line (branch `v5`; the code and docs live und
- [Dashboard sections explained](/docs/hive/dashboard-sections) — plain-language operator help for every dashboard section: what it shows, how its numbers are worked out, what to do about them, when it appears, and which settings change it. The **?** mark on each section title opens its entry.
- [Dashboard settings explained](https://github.com/hivecommons/hive/blob/v5/src/docs/settings.md) — one entry per Settings tab: what it controls and which guide explains it. The **?** mark next to each setting opens its guide or this page.
- [Dashboard feedback](https://github.com/hivecommons/hive/blob/v5/src/docs/feedback.md) — reporting bugs and requesting features from the spoke dashboard, including diagnostics, screenshot handling, hub relay, user-auth, and fallback issue paths.
+- [Dashboard source layout](https://github.com/hivecommons/hive/blob/v5/src/docs/dashboard-source-layout.md) — `static/index.html` is generated from per-area CSS/JS/HTML sources in `static-src/` via `make dashboard-gen`; how to edit, add an area, and resolve merge conflicts.
- [Dashboard design system](https://github.com/hivecommons/hive/blob/v5/src/docs/dashboard-design-system.md) — shared token catalogue, component variants, migration rules, ratchet plan, and #8536 theme override contract for spoke, contributor, and hub dashboard surfaces.
- [Dashboard glossary and sidebar IA](/docs/hive/dashboard-glossary) — the operator-facing names used by the dashboard and the ADR-0018 sidebar information architecture.
- [The `auto-update` Compose profile](https://github.com/hivecommons/hive/blob/v5/src/docs/auto-update-profile.md) — what unattended Watchtower updates cost you, what the Docker socket proxy does and does **not** fix, and why Kubernetes should not use this profile at all.
@@ -104,7 +105,7 @@ Documentation for the current Hive line (branch `v5`; the code and docs live und
- [Hub disaster recovery](https://github.com/hivecommons/hive/blob/v5/docs/HUB_DISASTER_RECOVERY.md) — the hub-level runbook that goes beyond per-hive backup: hub backup and key escrow, spoke fleet recovery, Slack blast, and the full rebuild-from-zero procedure after a catastrophic loss.
- [Deployment helper scripts](https://github.com/hivecommons/hive/blob/v5/src/docs/deployment-scripts.md) — the all-in-one LXC setup, Proxmox LXC, and blue-green Compose helpers. All are Docker-only; the page states each script's runtime scope and where a Podman operator should go instead.
- [`bin/` pipeline script index](https://github.com/hivecommons/hive/blob/v5/bin/README.md) — map of the deterministic pipeline and operational scripts, grouped by function.
-- [Dashboard API reference](https://github.com/hivecommons/hive/blob/v5/src/docs/api-reference.md) — pragmatic route index for dashboard and hub endpoints.
+- [Dashboard API reference](https://github.com/hivecommons/hive/blob/v5/src/docs/api-reference.md) — pragmatic route index for dashboard and hub endpoints; one page per area under `api-reference/` (health, auth, configuration, agents, ACMM, cost, knowledge, contribute, nous, inception, beads, misc, hub SaaS, hub server).
- [Dashboard OpenAPI spec](https://github.com/hivecommons/hive/blob/v5/dashboard/openapi.json) — machine-readable REST API reference for integrations.
- [ioscan status](https://github.com/hivecommons/hive/blob/v5/src/docs/ioscan.md) — the untrusted-input scanner/canary feature (live and default-on in v4).
- [Deployment scripts](https://github.com/hivecommons/hive/blob/v5/src/deploy/README.md) — inventory of deployment helpers, including dashboard TTY panes and `hive-panes`.
diff --git a/docs/content/hive/hivectl.md b/docs/content/hive/hivectl.md
index e64986a7..0aca7ca3 100644
--- a/docs/content/hive/hivectl.md
+++ b/docs/content/hive/hivectl.md
@@ -295,6 +295,7 @@ Reporting only: nothing here reads into or alters merge eligibility. See
```bash
hivectl evidence benchmark --month 2026-10 --out-dir src/docs/benchmarks --readme README.md
hivectl evidence benchmark --root ./evidence --month 2026-10 --spend cost.json --hub hub.json --source "hive-oke snapshot"
+hivectl evidence benchmark --from-json src/docs/benchmarks/outcomes-2026-10.json --out-dir src/docs/benchmarks --readme README.md
```
`evidence benchmark` folds the schema v1 bundles under `--root` per PR and compares the pull requests Hive's review
@@ -306,12 +307,15 @@ interval; n < 10 is `not reported`. `--month YYYY-MM` bounds the merge time (def
observation time for maturation, `--hub` adds a saved `GET /api/hub/evidence` response as fleet context, `--source`
names the data in the methodology section. It writes `outcomes-YYYY-MM.{md,json}`,
`assets/outcomes-YYYY-MM-{bars,trend}.svg` and the plain-language README cards
-`assets/outcomes-YYYY-MM-card--{dark,light}.svg` under `--out-dir`, and `--readme FILE` rewrites that file's
+`assets/outcomes-YYYY-MM-card--{dark,light}.{svg,png}` under `--out-dir` (the PNG is drawn in-process with an
+embedded font and is what the README embeds), and `--readme FILE` rewrites that file's
`…` block from the same report. The README block carries only the cards
and flat sentences, no intervals: `--readme-min-base N` (default 200) is the smallest base a production-count card
may rest on, separate from the n ≥ 10 report threshold; `--stories FILE` supplies curated caught → fixed → merged
examples and `--red-team FILE` the prompt-injection evaluation counts (see [benchmarks/](https://github.com/hivecommons/hive/blob/v5/src/docs/benchmarks/README.md) for
-both formats). Local filesystem only; no hive session, no telemetry. `src/scripts/outcome-benchmark.sh` wraps it; see [benchmarks/](https://github.com/hivecommons/hive/blob/v5/src/docs/benchmarks/README.md).
+both formats). `--from-json FILE` skips the evidence tree and re-renders every output from a saved report, for
+redrawing committed assets after a layout change. Local filesystem only; no hive session, no telemetry.
+`src/scripts/outcome-benchmark.sh` wraps it (`--rerender` = `--from-json` on the newest committed report); see [benchmarks/](https://github.com/hivecommons/hive/blob/v5/src/docs/benchmarks/README.md).
### evidence — backfill bundles from GitHub history
diff --git a/docs/content/hive/labels-and-control-signals.md b/docs/content/hive/labels-and-control-signals.md
index 426c8eab..18712005 100644
--- a/docs/content/hive/labels-and-control-signals.md
+++ b/docs/content/hive/labels-and-control-signals.md
@@ -27,7 +27,7 @@ Hive-authored labels answer different questions. Read them as independent axes,
| **Agent** | `agent/` | Which agent works it and, for agent-filed issues, which lane filed it? | Routing + ownership + lane provenance | The suffix is always the lane name from `HIVE_AGENT`. The classifier routes on it, PR ownership compares against it, and `authorkind` treats it as this hive's author stamp. Agent-filed issues carry exactly one lane/provenance label: `agent/`. |
| **Origin** | `origin/` | Which non-lane hive component created this item? | Provenance only, never a gate | Reserved for hive filers that have no `agent/` equivalent: `origin/selfreport`, `origin/governor`, `origin/dashboard`, `origin/forge`, and `origin/convergence`. Agent lanes do not use `origin/`; the startup reconciler (`Client.MigrateOriginLabels`, `src/pkg/github/origin_labels.go`) removes redundant lane-origin and bare-lane labels while ensuring `agent/`. `origin/reviewer`, `origin/review`, and `from-review` are retired reviewer-lane spellings and map to `agent/reviewer`. |
| **Role** | `role/` | How far does the hive trust the author's login? | Control (auto-merge) | Was `author/` before #12581; the reconciler rewrites the old spelling. See [`role/*`](#role--would-the-hive-auto-merge-this). |
-| **Author** | `author/` | Who wrote it — a person, a contributor's hive, or this repo's hive? | Informational (gates the 072 nudge/adoption) | `author/human`, `author/clanker`, `author/hive`; names configurable under `governor.labels.author`. See [`author/*`](#author--who-wrote-this-pr). |
+| **Author** | `author/` | Who wrote it — a person, a contributor's clanker, or this repo's hive? | Informational (gates the 072 nudge/adoption) | `author/human`, `author/clanker`, `author/hive`; names configurable under `governor.labels.author`. See [`author/*`](#author--who-wrote-this-pr). |
| **Modifiers** | `blocked`, `hold`, `needs-*`, `hive-pause/` | What is stopping it right now? | Hold / suppress | See the reference table; these are the hard gates. |
### Origin sources
@@ -559,7 +559,7 @@ stage but written only on change:
**Renamed from `author/`** ([#12581](https://github.com/hivecommons/hive/issues/12581)).
The old prefix conflated trust with authorship: `author/trusted` sat on PRs a
-maintainer pushed by hand and on PRs a contributor's hive opened under that
+maintainer pushed by hand and on PRs a contributor's clanker opened under that
contributor's login. Trust is now `role/*`; `author/*` says who wrote it (next
section). Migration is automatic: every reconcile that writes a PR removes a
retired `author/trusted|contributor|newcomer|untrusted` and adds the `role/*`
@@ -636,7 +636,7 @@ revert-rate-by-author-kind drill-down (#12428) reads it.
| Label | Meaning | Example |
|---|---|---|
| `author/human` | a person pushed the PR themselves | a maintainer's hand-written PR |
-| `author/clanker` | a **contributor's own hive** (relay) authored the PR under that contributor's GitHub login — "clanker" is the project's word for a contributor's hive | mendezr's or Karibhou's relay-opened PRs: human login, `— hive:` trailer |
+| `author/clanker` | a **contributor's own agent** (relay) authored the PR under that contributor's GitHub login — "clanker" is the project's word for a contributor's agent (it is their clanker, not a hive) | mendezr's or Karibhou's relay-opened PRs: human login, `— hive:` trailer |
| `author/hive` | **this repository's own hive** — the App or one of its agent lanes — authored the PR | `app/hivecommons-hive` PRs, anything stamped `agent/` |
Detection rules, first match wins (`authorkind.Explain` returns the rule
@@ -680,7 +680,7 @@ the label, and with it the tracker word and whether the hive has anyone to
| Kind | Stage | Tracker word | Contributor nudge (`` + relay interrupt) | Offline adoption (074) |
|---|---|---|---|---|
| `author/human` | `stage/071-needs-author` | Needs author | no — plain wait on the person; they push or comment (a `role/trusted` human still gets one `` comment per head naming the reason, always on and not gated on `ContributorNudge`, [#13636](https://github.com/hivecommons/hive/issues/13636)) | no |
-| `author/clanker` | `stage/072-returned-to-contributor` | Returned to contributor | yes — the contributor's hive is poked to fix its own PR | yes — after the grace, the hive adopts |
+| `author/clanker` | `stage/072-returned-to-contributor` | Returned to contributor | yes — the contributor's clanker is poked to fix its own PR | yes — after the grace, the hive adopts |
| `author/hive` (and any self-authored PR) | `stage/070-fixing` | Fixing | n/a — the hive's own lanes rework the head | n/a |
The split also covers a head whose commits GitHub links to the hive App while
@@ -917,7 +917,7 @@ When a clanker-authored contributor PR goes red the hive does not just relabel i
owner decision 2026-10-28 on hivecommons/hive#14524, where a
maintainer's own `author/clanker` PR sat at 072 for 26h with its hive
gone while every sweep logged "author maintains the repository; not
- adopting"): a PR a contributor's hive opened is the hive's to stand in
+ adopting"): a PR a contributor's clanker opened is the hive's to stand in
for when that hive goes away, whoever owns the account. `ask` holds a
maintainer's PR for an owner's decision instead — the stage record
notes `author-is-maintainer`, the activity monitor row reads
@@ -943,6 +943,24 @@ When a clanker-authored contributor PR goes red the hive does not just relabel i
unregistered clock). A held adoption (`ask` / `never`, or
`adoption.enabled: false`) keeps `next: wait` with no `adopt_at`, so the
indicator never promises an adoption the pill text denies.
+ An operator can end the wait early from the activity monitor
+ row (hivecommons/hive#15038): **Adopt now** (decision API `adopt` with
+ `force: true`) posts ``, sets `adoption/supersede` and marks the stage record
+ forced, so the next sweep adopts at once in supersede mode whatever the
+ author's presence, the grace windows or `maintainer_policy` say (`hold`
+ still blocks it); the marker that follows reads `reason=operator-forced
+ by=@` and, when the author's hive is online, the adoption comment
+ says the maintainer took it over. **Reroute** (decision API `reroute`
+ with `to: |any`) hands the follow-up work to another online
+ clanker's fix queue and posts `` on the PR; if that relay has not picked the task up within
+ `governor.reroute.pickup_timeout` (default 30m) the hive posts
+ `` and falls back to
+ Adopt now on its own.
+ - `reason=operator-forced by=@` — an operator's Adopt now (or the
+ reroute pickup fallback acting for them); presence, grace and
+ `maintainer_policy` were not consulted;
- `reason=contributor-offline` — the author has a contributor profile
here and no relay heartbeat within `governor.labels.stage.presence_window`:
adopted `governor.labels.stage.adoption.after_offline` (default 15m)
@@ -1131,7 +1149,13 @@ When a clanker-authored contributor PR goes red the hive does not just relabel i
copy exists: the stage record is superseded (no row, no chevron, no
needs-human) and the copy is the one PR for the issue. A close GitHub
refuses (403/404) leaves the original open with the note, superseded
- all the same. `close_first: false` restores the keep-open behaviour
+ all the same. The note, label and close are unsolicited writes on a
+ contributor's PR, so the pass first asks the write-back gate as kind
+ `contributor_adopt` (default L6 + opt-in label; see the write-back
+ table in
+ [operator-reference.md](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md#what-hive-writes-to-github-and-when)):
+ refused, nothing is written and the original stays open on the
+ keep-open path below until the copy merges. `close_first: false` restores the keep-open behaviour
below: the note has no `close_first` field, the original moves to
`stage/074-adopted-by-hive` (its linked issue mirrors the stage) and
closes only when the copy merges. The hub drops the PR from the
@@ -1150,7 +1174,12 @@ When a clanker-authored contributor PR goes red the hive does not just relabel i
`adoption/supersede` request re-runs the takeover from the same head).
**Ping-pong guard:** one reopen per original per 24h — a second failed
copy inside the window leaves the original closed with a `held=true`
- note saying so. The orphan recovery below never runs for a close-first
+ note saying so. The reopen, its note and the label removal also go
+ through the `contributor_adopt` write-back gate: refused, nothing is
+ written and the record stays due its next poll, so a lifted repo pause
+ lands the reopen; a policy change after the close is warned in the log
+ and leaves the hive-closed original shut until a person reopens it.
+ The orphan recovery below never runs for a close-first
original (its copy being closed is a failure, not an orphan); the
`hive:adoption-copy-failed` note is a settled marker, so the marker scan
never re-adopts a reopened original on its own.
@@ -1440,8 +1469,10 @@ The plain-words comment the delegate posts has four lines — **Hive decided** (
| Token | Plain words | Detail |
|---|---|---|
+| `already-fixed` | the issue was already fixed by a merged PR; closed as superseded | The issue this PR fixes is already closed and a merged PR closed it (or a merged PR declares it supersedes this one), so nothing is left to deliver. The hive closes the PR as superseded with a note naming the merging PR; it never adopts it. |
+| `capability-gap` | the GitHub App lacks a permission; deciding cannot clear it | The blocker is a missing GitHub App permission (for example `workflows`). Approving a direction would only re-apply needs-human, so the park stays until the owner grants the permission or a person applies the change. |
| `ci-ratchet` | a CI hygiene gate, landed as a ratchet | A new CI hygiene gate lands as a ratchet: it records today's baseline and fails only on increases, so existing work is never blocked. |
-| `contributor-offline` | the author's hive hasn't been connected since the nudge | The PR's author has been offline since the hive nudged them, so the hive takes the work over instead of waiting. |
+| `contributor-offline` | the author hasn't been reachable since the nudge | The PR's author (or their clanker or hive) has been offline since the hive nudged them, so the hive takes the work over instead of waiting. The decision itself names which: the author's clanker, a connected hive, or the author. |
| `dedupe` | an older issue already tracks this | An older issue already tracks the same work; one thread keeps it moving. |
| `default-approve` | no special rule applies; the hive took its own recommendation | No owner rule singles this issue out, so the hive went with its own recommendation — the reversible choice that keeps work moving. |
| `default-release` | no special rule applies; a green branch is approved | No owner rule holds this PR back; its branch is green and a merge is reversible, so it is approved. |
@@ -1450,10 +1481,13 @@ The plain-words comment the delegate posts has four lines — **Hive decided** (
| `flaky-test` | a flaky test, quarantined with a retry | The failure is a flaky test: it is quarantined with a retry and a separate deflake task is filed, so it blocks nothing. |
| `hive-policy` | this changes the hive's own rules | This changes the hive's own settings, governance or security posture; the hive only recommends and the owner confirms. |
| `llm-delegate` | decided by the hive's model under the owner's rules | No mechanical rule applied, so the hive's model decided under the owner's standing rules; the same safety rails applied. |
+| `mergeable-as-is` | mergeable and green as it is; handed to the merge gate | The head is mergeable with green CI, so no adoption is needed: the hive approves it and the merge gate takes it as it is. |
| `owner-only`, `owner_only` | only the hive owner can do this | This needs a secret, token or organization permission that only the hive owner holds; the hive cannot do it for you. |
| `prefer-contributor` | a contributor's PR and a hive PR do the same work | A contributor's PR and a hive-authored PR cover the same work; the hive prefers the contributor's when it is mergeable and current. |
| `rail-hold` | the item is on hold | The item carries the hold label; the hive never acts on a held item until a person releases the hold. |
+| `rebase-then-merge` | only conflicts block it; a rebase then merge | Conflicts are the only blocker. The unattended rebase-then-merge path lands with hivecommons/hive#15104; until then the ladder records this rung and moves on. |
| `reporter-trust` | the hold on an unknown reporter is cleared because the fix is sound | The PR was held until the reporter's trust was known; its fix is green and mergeable, so the hold is cleared. |
+| `return-to-contributor` | the author's clanker or hive is connected; returned to them with the fix list | The author's clanker or hive is connected (or has picked the rework up), so the hive hands the PR back to them with the exact fix list instead of adopting it. |
| `review-backlog` | the PR closed without merging, so the issue stays open | The fixing PR closed unmerged; the issue stays open and is re-triaged rather than auto-closed. |
| `sentinel-trusted` | security-sensitive files changed, but the author is trusted | Security-sensitive files changed; because the author is trusted the hive approved, but a person must still remove the sentinel label before it merges. |
| `sentinel-untrusted` | security-sensitive files changed; a maintainer must approve | Security-sensitive files changed in a PR from an author without write access; a maintainer must review and approve it. |
@@ -1484,7 +1518,12 @@ The plain-words comment the delegate posts has four lines — **Hive decided** (
| Token | Plain words | Detail |
|---|---|---|
| `adoption-disabled` | adoption is switched off on this hive | Stage adoption is disabled on this hive (governor.labels.stage.adoption), so a request to take a PR over is never carried out. |
-| `author-online` | the author's hive is connected, so hive won't take the PR over | Adoption is for authors whose hive has gone offline; this author's hive is connected, so the hive leaves the PR with them. |
+| `adoption-executing` | the adoption engine is carrying it out | The decision is recorded and the adoption engine picks it up on its next stage pass (a few minutes): it pushes the commits to a hive branch and opens the hive's copy. Nothing to do yet. |
+| `adoption-stalled:contributor-lane:workflow-path`, `capability:workflows-permission`, `contributor-lane:workflow-path`, `workflow-path` | the PR edits .github/workflows; the hive's App lacks the `workflows` permission, so it cannot adopt it | The PR changes files under .github/workflows. The hive's GitHub App has no `workflows` permission, so the adoption fence refuses to publish those edits from the hive. Grant the App the `workflows` permission and re-run the decision, or have a person rebase and merge it on GitHub, or close it as superseded. |
+| `adoption-undone` | the hive's copy was closed without merging and the adoption was undone | The hive opened its copy, but the copy was closed without merging and a fresh one could not be opened, so the adoption was undone and the PR is back in its normal flow. Re-run the decision to adopt it again, or act on GitHub. |
+| `author-association`, `contributor-lane:author-association`, `human-merge`, `ready-for-human-merge`, `untrusted-author` | no merge lane vouches for this PR's author; a maintainer merges | The PR is ready, but its author is not a collaborator and holds no trusted or merger tier, so no merge lane vouches for them and the hive never merges it unattended. A maintainer merges it (Approve & merge as maintainer, or Approve — let hive merge), or trusts the author under 👥 Contributors. |
+| `author-online` | the author's clanker or hive is connected, so hive won't take the PR over | Adoption is for authors whose clanker or hive has gone offline; this author's is connected, so the hive leaves the PR with them. |
+| `blocker`, `capability`, `human-only` | the hive can't do this itself | The item is blocked on something the hive cannot do: a permission the GitHub App lacks, or a step only a person may take. No decision clears it; grant the permission (and re-check) or do the step by hand (hivecommons/hive#15099). |
| `behind` | the branch is behind the base branch | The branch is behind the base branch and must be updated before it can merge. |
| `changes-requested`, `changes_requested`, `contributor-lane:changes-requested`, `human-changes-requested`, `request_changes`, `reviewer-verdict:request_changes` | a reviewer asked for changes; a new commit is needed | A reviewer asked for changes on this branch. Approving cannot override that: only a new commit, or a maintainer merging by hand, moves it. |
| `checks-failing`, `ci-failing`, `ci-red`, `red` | CI is failing on this branch | Required checks are failing on this branch; nothing merges until they are green. |
@@ -1503,6 +1542,36 @@ The plain-words comment the delegate posts has four lines — **Hive decided** (
A `/hive decision …` comment the hive itself wrote as the owner's delegate (it carries the delegated-decision marker) is never read as a maintainer's command: not as a change request on the PR, not as an approval. [#14822](https://github.com/hivecommons/hive/pull/14822) fixes the parse of `/hive decision approve `; the marker guard covers every other decision text the delegate can post, whichever login it was posted under.
+## Blockers (`hive:blocker` marker)
+
+`needs-human` says *a person is needed*; it does not say *what for*. When the reason is a **capability gap** — the GitHub App cannot do the next step at all — a direction decision answers a question nobody asked, and "Let hive decide" would un-park the issue only for the next sweep to re-park it with the same text (hivecommons/hive#14976 → #15099). So an agent that parks an item because the hive *cannot* act records a structured blocker next to the label:
+
+```text
+
+```
+
+| Kind | Meaning | Clears when |
+|---|---|---|
+| `capability:workflows-permission` | the App token cannot push `.github/workflows/*` (no `workflows: write`) | the permission is granted and re-checked |
+| `capability:repo-write` | the App has no `contents: write` on the repo | the permission is granted and re-checked |
+| `capability:branch-protection` | a protection rule the App cannot satisfy or bypass | a person changes the rule |
+| `capability:secret` | a secret or token only the owner holds | a person sets it |
+| `human-only:cla` | a CLA / legal sign-off only a person can give | a person signs |
+| `human-only:re-asked` | the same needs-human reason came back after a delegated un-park (loop guard) | the reason text changes |
+
+Where it comes from:
+
+- **Agents** put the marker in the body of the needs-human report (the task-list sweep copies a merged PR's remainder into its progress comment and adds the marker when the prose names a capability). `bin/gh-wrapper.sh` adds the marker itself when a needs-human comment says "the App cannot … (no `workflows` permission)" and no marker is present, so a report written in plain prose still lands structured.
+- **The hive** reads it at every comment path — the `issue_comment` webhook, the task-list sweep, the stage pass — into `pkg/blocker`'s ledger (`/data/item-blockers.json`), inferring the kind from the prose when the marker is missing.
+
+What it changes:
+
+- The issue stays 045 with the reason `blocked: App lacks workflows permission`; the tracker pill reads **Blocked · App lacks workflows permission** and its popover says in plain words what the hive cannot do and lists the ways out (grant the permission, re-check, prepare the patch for a person where an identity exists, mark partially addressed and close, keep waiting).
+- **"Let hive decide" returns `no-effect`** (reason `blocker`) instead of applying `approved-direction`; it is shown disabled with that sentence, not greyed silently.
+- **Loop guard**: a delegated un-park records the reason it cleared. If the same reason is re-applied afterwards, the item is parked with a blocker (`human-only:re-asked` when no capability is named) and "Let hive decide" stays disabled on it until the reason changes.
+- **Label consistency**: `stage/045-needs-decision` and `approved-direction` cannot coexist. `needs-human` is always the newer signal next to `approved-direction` (the un-park removes `needs-human` when it applies the ack), so the stage pass keeps 045 and clears `approved-direction` — a person or sweep that re-parked after a delegated decision is never silently un-parked again.
+- `/api/status.capabilities` lists the missing permissions and the count of items blocked on them; ⚙ → Security shows the same line with a link to the App settings page. **Re-check permissions** on a row probes the installation and clears the blocker when the permission is there.
+
## Matching rules
- **GitHub generic holds** are case-insensitive substring matches against `hold`, `on-hold`, `hold/review`, plus configured non-`hive-pause/` extra holds. A label such as `threshold` contains `hold` and therefore holds (`src/pkg/github/client.go:3318-3361`).
@@ -1525,6 +1594,7 @@ A `/hive decision …` comment the hive itself wrote as the owner's delegate (it
- `runs.triage.enabled`, `runs.triage.spec_labels`, and `runs.triage.fix_labels` control run triage labels.
- `governor.question_autoclose.*` (`HIVE_QUESTION_AUTOCLOSE`, `HIVE_QUESTION_AUTOCLOSE_HOURS`) closes answered `question` issues; see [Question auto-close](#question-auto-close).
- `governor.claims.*`, `review.human_decision_label`, `hub.contribute_*`, `dashboard.issue_bands.*`, and `HIVE_ACTIONABLE_PRIORITY_LABELS` control the display/contributor/ranking labels above, including contributor label allow/deny filters and per-repo narrowing.
+- `writeback.unsolicited.*`, `writeback.per_repo`, and `writeback.kinds.` decide whether hive may put an unsolicited label or comment on a PR it did not open at all: each kind has an ACMM floor and, by default, requires the review opt-in label (`review.require_labels`, fallback `hive-review`) on the PR. Below the gate `stage/*`, `sentinel-alert`, `needs-human`, `has-open-pr` and similar labels are tracked internally and shown in the dashboard only. See [What hive writes to GitHub, and when](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md#what-hive-writes-to-github-and-when).
- `work_source.linear.hold_labels` and `work_source.jira.hold_labels` are non-GitHub label-equivalent gates with the different matching rules described above.
- Not labels: `project.paused_repos` pauses an entire repo; contributor queue holds can park `owner/repo#N` without changing GitHub labels.
diff --git a/docs/content/hive/security-model.md b/docs/content/hive/security-model.md
index 64634076..f7d3c065 100644
--- a/docs/content/hive/security-model.md
+++ b/docs/content/hive/security-model.md
@@ -256,6 +256,16 @@ secret exposure, CI gate weakening, test removal, security policy edits, and
remote code execution patterns. A match applies `sentinel-alert`, posts one
comment and writes an audit entry. The label blocks Hive approval and every
automerge lane until a maintainer removes it.
+Whether that label and comment reach GitHub is decided by the **write-back
+containment gate** (`pkg/writeback`): on a PR Hive did not open, sentinel
+writes need ACMM L5 plus the review opt-in label (L6 without it), the repo
+must not be in `issues-only`/`off` mode, and the PR must not predate Hive's
+write access to the repo (backfill guard). Below the gate the finding is
+recorded, audited as `sentinel-finding-suppressed`, and shown in the
+Security tab and `/api/sentinel` — the scan never stops, only the
+unsolicited write does. The same gate fronts hold-gate notices, stage
+labels, supersession and duplicate-fix notices and reporter-trust holds; a
+ratchet test fails on any new direct GitHub write outside the package.
**Trajectory review** runs every 600 seconds by default. A second model
compares each agent's assigned intent with the last 120 lines of its rendered
diff --git a/docs/content/hotshot/linux.md b/docs/content/hotshot/linux.md
index aa1ba43a..ea6c56c2 100644
--- a/docs/content/hotshot/linux.md
+++ b/docs/content/hotshot/linux.md
@@ -29,19 +29,22 @@ This is the same clipboard/typing contract as the macOS app.
## Install
-```bash
-git clone https://github.com/hivecommons/hotshot.git
-cd hotshot/linux
-./install.sh # installs to ~/.local/bin/hotshot-capture
-./install.sh --gnome-hotkey # also binds Ctrl+Shift+PrintScreen on GNOME
-./install.sh --prefix DIR # install somewhere other than ~/.local/bin
-```
-
-The installer reports which dependencies are missing for your session type.
-Uninstall with `./install.sh --uninstall`.
+First choose helpers for your desktop environment (DE: GNOME, KDE, sway, etc.)
+and display system. X11 and Wayland are two systems Linux uses to display
+windows. In a terminal, `echo "$XDG_SESSION_TYPE"` and
+`echo "$XDG_CURRENT_DESKTOP"` can help identify your session.
+The Wayland example below is for wlroots-based desktops such as sway and
+hyprland, not every Wayland desktop.
+
+Before installing, read [focus tracking](#parity-notes): X11, sway and hyprland
+can identify the target window's process ID (PID). Other Wayland compositors
+cannot query focus here and use the bracketed default, typing into whichever
+window receives focus. Start with `--no-type` if that is unsuitable.
### Dependencies
+Install Git for the clone route and the helpers for **one** matching session type.
+
| | X11 | Wayland |
|---|---|---|
| Capture | `maim` (or `scrot`) | `grim` + `slurp` |
@@ -59,6 +62,20 @@ sudo apt install maim xclip xdotool
sudo apt install grim slurp wl-clipboard wtype jq
```
+### Install the script
+
+```bash
+git clone https://github.com/hivecommons/hotshot.git
+cd hotshot/linux
+./install.sh # ordinary install: ~/.local/bin/hotshot-capture
+```
+
+Choose **one** installer variant. Instead of the last command, GNOME users
+can run `./install.sh --gnome-hotkey` to also bind Ctrl+Shift+PrintScreen.
+For a custom destination, use `./install.sh --prefix DIR` instead.
+The installer reports missing helpers for your session type.
+Uninstall from this checkout with `./install.sh --uninstall`.
+
## Hotkey binding
`install.sh` prints per-DE instructions. Examples:
@@ -86,9 +103,17 @@ hotshot-capture [--region|--full] [--no-type] [--dir DIR]
- `--no-type`: skip the typed injection (clipboard + file only)
- `--dir DIR`: save screenshots to DIR (a relative path or leading `~` is resolved to an absolute path)
+For a first capture, focus the terminal with your signed-in AI CLI, press the
+bound hotkey, and select a harmless region. Confirm a PNG in the configured
+folder and a path in the terminal input; press Enter when ready. With
+`--no-type`, expect only the file and clipboard image. If capture fails, check
+the installer's missing-helper report and the dependencies for your session;
+if only typing fails, read [focus tracking](#parity-notes) and [Logging](#logging).
+
## Parity notes
-- **Clipboard**: X11/Wayland clipboards have a single owner and `xclip` /
+- **Clipboard**: MIME identifies a clipboard data format, such as `image/png`.
+ X11/Wayland clipboards have a single owner and `xclip` /
`wl-copy` serve one MIME target per invocation, so by default the clipboard
carries the **image only** (paste with Ctrl-V in Claude Code or GUI apps);
the typed injection delivers the path. If [CopyQ](https://hluk.github.io/CopyQ/)
diff --git a/docs/content/hotshot/readme.md b/docs/content/hotshot/readme.md
index 837711d5..6b1307ca 100644
--- a/docs/content/hotshot/readme.md
+++ b/docs/content/hotshot/readme.md
@@ -16,7 +16,14 @@ If you're like me, you use screenshots constantly to debug your work — a broke
| **Linux** | Shell script (X11 & Wayland) | Hotkey you bind in your DE (e.g. Ctrl+Shift+Print) | `linux/install.sh` | [linux/README.md](/docs/hotshot/linux) |
| **Windows** | PowerShell script (+ optional AutoHotkey v2) | Global hotkey (default Ctrl+Alt+H) → Snipping overlay | `windows\install.ps1` | [windows/README.md](/docs/hotshot/windows) |
-All platforms honor the same contract:
+Choose your platform's install guide above. A **CLI** is the command-line
+coding assistant running in your terminal; install and sign in to it first.
+
+
+Advanced reference: cross-platform clipboard and process-detection contract
+
+All platforms honor the same contract (tty means terminal session; process-tree
+checks look for the CLI running inside that terminal):
| | macOS | Linux | Windows |
|---|---|---|---|
@@ -30,6 +37,8 @@ All platforms honor the same contract:
| Gemini CLI → `@` + escaped path | ✅ backslash-escaped | ✅ backslash-escaped | `@"path"` (always double-quoted, no escaping); refused if it contains `"` |
| Unknown CLI → bracketed default | ✅ | ✅ | ✅ |
+
+
The rest of this page documents the **macOS** app.
## What it looks like
@@ -39,14 +48,16 @@ The rest of this page documents the **macOS** app.
2. You switch to a browser and spot a bug
3. Take a screenshot the way you always do
4. hotshot detects the new file and injects the path into your terminal session
-5. Your AI assistant reads the image and starts helping
+5. Review the pasted path and press Return to submit it; **Auto-press Return is off by default**
**Clipboard mode (⌃⌘⇧3 / ⌃⌘⇧4):**
1. Take a screenshot to clipboard (Control-Command-Shift instead of Command-Shift)
-2. hotshot detects the new clipboard image and sends Ctrl-V to your terminal
-3. No files created — the image goes straight from clipboard to your AI assistant
+2. hotshot saves a PNG copy in your screenshot folder, then sends Ctrl-V to your terminal
+3. Whether your assistant receives image data or a path depends on the CLI and paste method; review the input and submit it when ready
-Clipboard mode is ideal for remote sessions (tmux, zellij, OpenShell) where there's no shared filesystem.
+Clipboard mode can help with remote sessions (tmux, zellij, OpenShell) when
+image clipboard data is shared. A local file path alone will not make the
+image available on a remote machine. The saved PNG remains on your Mac.
No dragging files around. No copy-pasting paths. No "here let me find where that screenshot went."
@@ -69,7 +80,7 @@ Releases are tagged with `v`-prefixed semantic versions; see the [latest release
```bash
git clone https://github.com/hivecommons/hotshot.git
cd hotshot
-git checkout v0.2.0
+git checkout v0.1.0
swift build -c release
sudo cp .build/release/hotshot /usr/local/bin/
```
@@ -103,6 +114,14 @@ To go back to an earlier build after a bad update, see [runbooks/release-rollbac
macOS will ask for Accessibility permission the first time — this lets hotshot type the path into your terminal. Grant it in System Settings > Privacy & Security. You only have to do this once.
+For a first check, start your coding assistant in a supported terminal and click
+that terminal once so hotshot knows its target. Capture a harmless test image.
+Expect a saved PNG and an image or path in the terminal input; review it before
+pressing Return. If nothing arrives, check the camera icon and Accessibility
+permission, then follow [injection failure recovery](https://github.com/hivecommons/hotshot/blob/main/runbooks/injection-failure.md).
+Accessibility allows hotshot to control keyboard input, so enable it only for a
+build you trust.
+
## How it works
hotshot remembers which terminal you last clicked on. It supports two injection modes:
diff --git a/docs/content/hotshot/roadmap.md b/docs/content/hotshot/roadmap.md
index 61687031..c9d461a6 100644
--- a/docs/content/hotshot/roadmap.md
+++ b/docs/content/hotshot/roadmap.md
@@ -11,7 +11,7 @@ the macOS app, Linux script, and Windows helper.
- macOS, Linux, and Windows screenshot-to-terminal flows with CLI-aware text
injection for Claude Code, GitHub Copilot CLI, aider, and opencode.
- Clipboard enrichment with image data, file URLs, and shell-escaped text paths.
-- Hermetic Linux and Windows tests plus extracted `HotshotCore` helpers for
+- Isolated Linux and Windows tests (with external tools replaced by test doubles) plus extracted `HotshotCore` helpers for
shared macOS decision logic.
- First tagged release (v0.1.0) and a Homebrew install path via the
`hivecommons/homebrew-hive` tap (`brew install hivecommons/hive/hotshot`).
@@ -21,9 +21,11 @@ the macOS app, Linux script, and Windows helper.
1. **Package distribution:** extend first-class install paths beyond Homebrew
on macOS, documenting follow-on candidates such as winget and distro
packages.
-2. **Cross-platform parity:** keep Linux Wayland/X11 and Windows behavior aligned
+2. **Cross-platform parity (consistent behavior):** keep Linux Wayland/X11
+ (Linux display systems) and Windows behavior aligned
with macOS for clipboard modes, typed injection, warning paths, and safe
- degradation when helper tools are missing.
+ fallback when helper tools are missing, such as keeping the saved image
+ available even when typed injection cannot complete.
3. **More terminal and agent targets:** expand detection and examples for
terminals and AI CLIs as the ecosystem changes, while keeping unknown targets
on safe, explicit fallback behavior.
diff --git a/docs/content/hotshot/windows.md b/docs/content/hotshot/windows.md
index da41929a..97a67e04 100644
--- a/docs/content/hotshot/windows.md
+++ b/docs/content/hotshot/windows.md
@@ -20,6 +20,9 @@ built-in Snipping Tool overlay, and hotshot:
in the foreground again — types the format that
CLI understands:
+
+Advanced reference: CLI process detection and Windows path formats
+
| CLI in focused terminal | Typed injection |
|---|---|
| Claude Code (`claude`) | `[C:\path\to\shot.png] ` (bracketed) |
@@ -31,9 +34,17 @@ Two or more of claude / copilot / aider / opencode / gemini in the same terminal
This is the same clipboard/typing contract as the macOS app.
+
+
## Install
-Everything uses built-in Windows components (PowerShell 5.1+, Snipping Tool).
+Capture uses built-in Windows components (PowerShell 5.1+, Snipping Tool on
+Windows 10 1809+). The clone route also requires Git. Have a supported coding
+assistant installed and signed in before your first capture.
+Run the following commands in **PowerShell**. `-ExecutionPolicy Bypass` applies
+to that PowerShell process only; it does not permanently change your policy.
+Review the downloaded scripts before allowing them to run, and respect any
+organization-enforced policy.
```powershell
git clone https://github.com/hivecommons/hotshot.git
@@ -49,9 +60,10 @@ are limited to Ctrl+Alt/Ctrl+Shift combos — pick another with
### Optional: AutoHotkey v2 for an instant hotkey
Start Menu shortcut hotkeys can take a second to launch. If you have
-[AutoHotkey v2](https://www.autohotkey.com) installed, run
-`%LOCALAPPDATA%\Hotshot\hotshot.ahk` (or put a shortcut to it in
-`shell:startup`) and press **Ctrl+Shift+PrintScreen** instead.
+[AutoHotkey v2](https://www.autohotkey.com) installed, press Win+R and enter
+`%LOCALAPPDATA%\Hotshot\hotshot.ahk`. To launch it at sign-in, enter
+`shell:startup` in Win+R and put a shortcut to that file in the folder that
+opens. Press **Ctrl+Shift+PrintScreen** instead.
## Usage
@@ -61,12 +73,21 @@ Start Menu shortcut hotkeys can take a second to launch. If you have
4. hotshot saves the PNG, loads the clipboard, refocuses your terminal, and
types the path in the right format. Press Enter.
-Manual invocation:
+Confirm the PNG exists in your Pictures `hotshot` folder and the terminal input
+contains the path before submitting. If it does not, confirm you focused the
+terminal before capture and check [Logging](#logging); a focus failure can leave
+the file and clipboard intact without typing anything.
+
+Manual invocation from the **installed folder**, not the checkout:
```powershell
-powershell -ExecutionPolicy Bypass -File hotshot-capture.ps1 [-NoType] [-Dir C:\shots]
+cd "$env:LOCALAPPDATA\Hotshot"
+powershell -ExecutionPolicy Bypass -File .\hotshot-capture.ps1
```
+Optionally append `-NoType` to save and copy without typing, or `-Dir C:\shots`
+to choose a destination folder.
+
## Parity notes
- **Capture UI**: uses the native `ms-screenclip:` Snipping Tool overlay
diff --git a/docs/content/pluk/readme.md b/docs/content/pluk/readme.md
index f3ce37f9..b55f0979 100644
--- a/docs/content/pluk/readme.md
+++ b/docs/content/pluk/readme.md
@@ -8,14 +8,19 @@
[](https://github.com/hivecommons/pluk/blob/main/LICENSE)
-Pluk classifies, subscribes to, and reacts to JSONL event streams from AI coding agent terminals.
+Pluk turns an AI coding agent's terminal output into events, such as “agent is
+idle” or “usage limit reached”. Other tools can read these events and decide
+what to do. Events are stored as **JSONL**: one JSON record per line.
## Install
Requirements:
- Node.js 22 or newer
-- tmux (`attach`, `send` and `watch --capture` drive tmux sessions)
+- tmux, a tool that keeps terminal sessions running (`attach`, `send` and
+ `watch --capture` drive those sessions)
+- For the live examples, an installed and authenticated coding-agent CLI
+ such as Claude Code; pluk does not install or sign in to it for you
```bash
npm install -g @hivecommons/pluk
@@ -71,9 +76,23 @@ helper claude idle ● 45s ago 892
goose-exp goose idle ○ 3m ago 156
```
+## Reference: commands, integration, and event formats
+
+After your first session, use the sections below as needed:
+[commands](#cli-commands), [flags](#attach-flags),
+[health diagnostics](#health-diagnostics-watch-subscribe),
+[library API](#programmatic-api), [event schema](#event-schema), and
+[custom patterns](#pattern-file-format).
+
+
+Optional: wire tmux and event capture manually
+
## Manual Setup
-If you prefer to wire things up yourself:
+**Advanced wiring; most users can stay with `attach`.** Data flows from the
+agent's tmux pane → `pipe-pane` (tmux's output pipe) → `pluk watch` → a JSONL
+log. `pluk subscribe` reads that log from another terminal.
+The run directory must be the same in both terminals.
```bash
# 1. Create a tmux session (don't start the CLI yet)
@@ -108,6 +127,8 @@ tmux attach -t my-agent
| `PLUK_LOG_KEEP_LINES` | `5000` | Keep up to this many trailing lines across rotation, within the `PLUK_LOG_MAX_BYTES` cap |
| `PLUK_RATIONGUARD_BIN` | *(unset)* | Rationguard command for `pluk attach --rationguard` when it is not on `PATH`; same as `--rationguard-bin` |
+
+
## CLI Commands
| Command | What it does |
@@ -157,6 +178,9 @@ Unknown, misspelled, or wrong-subcommand `--flags` exit 2 (`unknown flag --name
### Health diagnostics (`watch`, `subscribe`)
+
+Operator reference: counters and diagnostics intervals
+
`pluk watch` and `pluk subscribe` deliberately swallow capture-pane failures,
input errors and malformed log lines so the pipe-pane process never dies. To
tell a healthy quiet session from one that is repeatedly failing, add
@@ -178,8 +202,16 @@ period above 2147483.647 seconds (about 24.9 days) have it capped at that value
to stay within Node's timer limit; the CLI rejects out-of-range values as
described above.
+
+
## Programmatic API
+This example enables automatic messages and skips the launched CLI's permission
+prompts. Both are optional: omit `rebuttal: 'send'` and
+`dangerouslySkipPermissions: true` for a detection-only setup with normal
+permission checks. Only bypass prompts in an environment you have deliberately
+isolated and authorized.
+
```typescript
import { Classifier, getPatterns, subscribe, watch, discoverSessions, attach, send } from '@hivecommons/pluk';
diff --git a/docs/content/pluk/roadmap.md b/docs/content/pluk/roadmap.md
index 8deec5ba..d3642341 100644
--- a/docs/content/pluk/roadmap.md
+++ b/docs/content/pluk/roadmap.md
@@ -5,6 +5,8 @@
Pluk turns AI-agent terminal output into structured events that downstream
tools can consume. This roadmap describes the direction for the TypeScript
package and its relationship with the wider Hive Commons agent-tooling stack.
+For example, a `state_change` event can report that an agent went idle.
+Logs use JSONL: one JSON record per line.
## Recently shipped
@@ -18,20 +20,23 @@ package and its relationship with the wider Hive Commons agent-tooling stack.
## Near term
-1. **Event schema evolution:** keep the JSONL envelope additive within schema
+1. **Event schema evolution:** keep the JSONL envelope (fields around each event)
+ additive within schema
version 1 and define the process for a version bump, so tools such as
- rationguard can depend on it safely.
+ rationguard can depend on it safely: add fields without removing or changing
+ the meaning of existing ones within version 1.
2. **Broader CLI coverage:** add and test pattern packs for additional active
agent CLIs as they become adoption-relevant.
3. **Operational hardening:** continue replacing tmux/terminal edge cases with
- explicit diagnostics, non-zero exits for operator errors, and hermetic tests.
+ explicit diagnostics, non-zero exits for operator errors, and isolated tests
+ that do not depend on a real agent or external service.
## Implementation
Pluk is a single TypeScript package published to npm as `@hivecommons/pluk`;
there is no separate Go binary. Hive and other consumers shell out to the npm
CLI and read its JSONL logs, so event shapes and user-facing semantics are the
-compatibility surface.
+compatibility surface — the behavior other tools depend on.
## Non-goals for now
diff --git a/docs/content/promptargs/readme.md b/docs/content/promptargs/readme.md
index 6d4388e4..731eb55e 100644
--- a/docs/content/promptargs/readme.md
+++ b/docs/content/promptargs/readme.md
@@ -24,7 +24,8 @@ Review src/app.ts for security issues.
That's it. That's the whole idea.
-Works as a **CLI** and as a **skill** in Claude Code, Copilot, Codex, Gemini CLI, Goose, and Bob.
+Works as a **CLI** (a command you run in a terminal) and as a **skill**
+(instructions your coding agent loads) in Claude Code, Copilot, Codex, Gemini CLI, Goose, and Bob.
---
@@ -54,7 +55,15 @@ promptargs ships an [Agent Skill](https://agentskills.io/specification) at [`ski
gh skill install hivecommons/promptargs promptargs
```
-**Manually** (copy the `skills/promptargs/` directory):
+**Manually:** first obtain a checkout, then run the copy command for your agent
+from its top-level folder, where `skills/promptargs/` exists:
+
+```bash
+git clone https://github.com/hivecommons/promptargs.git
+cd promptargs
+```
+
+Choose the relevant destination below (Codex and Gemini share a destination):
```bash
# Copilot
@@ -102,6 +111,10 @@ curl -o ~/.bob/skills/promptargs.md \
## Quick Start (3 steps)
+Open a terminal in the project folder where you want `.prompts/` to live.
+These commands produce prompt text; they do not execute a code review or send
+the text to an agent. Inspect the result, then choose whether to pass it to one.
+
### Step 1: Make some templates
```bash
@@ -143,6 +156,12 @@ Focus on real bugs, not style nitpicks.
Variables with `=something` already have a default. Variables without one will ask you to fill them in.
+For later reference, see [template locations and file restrictions](#where-templates-live),
+[detected variables](#auto-magic-variables-), and
+[privacy and diagnostics](#privacy--diagnostics). None is needed to expand this
+first template, but review the privacy rules before using environment values
+or sharing diagnostic output.
+
---
## How Blanks Work
@@ -155,7 +174,13 @@ A blank with a default looks like this: `{{name=hello}}` (promptargs extension)
Padding inside the braces is fine too — `{{ name }}` and `{{ name=hello }}` mean the same thing, and so does `{{ name = hello }}` (padding around `=` is not part of the default).
-Names may use letters, digits, `_` and `-` (`{{file-name}}`). Dotted names such as `{{repo.owner}}` are rejected with an error — use `{{repo_owner}}`; the switch names `no-interactive`, `cross`, `json`, `status` and `help` are also rejected, as they are reserved for CLI switches. Likewise, the template file names `help`, `env`, `ui`, `init`, `list` and `show` are reserved for the subcommands of the same name; such a template is listed as unreachable by `promptargs list` (and `show` warns), because the subcommand always wins.
+Names may use letters, digits, `_` and `-` (`{{file-name}}`).
+For a first template, stick to plain blanks and comments.
+
+
+Advanced reference: reserved names and unsupported Mustache features
+
+Dotted names such as `{{repo.owner}}` are rejected with an error — use `{{repo_owner}}`; the switch names `no-interactive`, `cross`, `json`, `status` and `help` are also rejected, as they are reserved for CLI switches. Likewise, the template file names `help`, `env`, `ui`, `init`, `list` and `show` are reserved for the subcommands of the same name; such a template is listed as unreachable by `promptargs list` (and `show` warns), because the subcommand always wins.
A default may appear on any occurrence of a blank (`{{name}} … {{name=hello}}`); if two occurrences give different defaults, the first one wins.
@@ -163,6 +188,8 @@ That's the basics. Mustache comments (`{{! ignore me}}`) work as documented and
Mustache **sections** (`{{#flag}}...{{/flag}}`) and **partials** (`{{>name}}`) are parsed too, but aren't practically usable yet: promptargs only resolves plain `{{name}}` / `{{name=default}}` tags from `--flag=value`, auto-detected context, defaults, or interactive fill — a section's condition name is never filled this way (so its content is always dropped, or always kept for an inverted `{{^name}}` section), and partial content is never supplied, so `{{>name}}` always expands to nothing. Stick to plain blanks and comments; avoid sections and partials for now.
+
+
### Red means "you need to fill this in"
```bash
diff --git a/docs/content/rationguard/readme.md b/docs/content/rationguard/readme.md
index 56147e0f..5b9cecd6 100644
--- a/docs/content/rationguard/readme.md
+++ b/docs/content/rationguard/readme.md
@@ -8,9 +8,13 @@
[](https://github.com/hivecommons/rationguard/blob/main/LICENSE)
-Detect and rebut rationalization patterns in AI agent output.
+Flag phrases in AI agent output that may indicate work has stopped too soon.
-AI agents make excuses. "Standing by for instructions." "Too complex to fix." "I'll handle it next pass." These sound reasonable but mean the agent stopped working.
+Here, a **rationalization** is wording such as “Standing by for instructions”
+or “I'll handle it next pass” that may explain away unfinished work.
+A **rebuttal** is a suggested response asking the agent to check or continue.
+Matching a phrase is a heuristic signal, not proof the agent is wrong or idle;
+review the task and output before acting.
rationguard catches these patterns and provides rebuttals — in real-time via [pluk](https://www.npmjs.com/package/@hivecommons/pluk), injected into the agent's system prompt (prevention), or checked against agent output after each response (detection).
@@ -19,7 +23,8 @@ rationguard catches these patterns and provides rebuttals — in real-time via [
## Prerequisites
- Node.js 20 or newer (`engines.node` in `package.json`)
-- [pluk](https://www.npmjs.com/package/@hivecommons/pluk), installed alongside rationguard below
+- For the live stack, Node.js **22 or newer** and
+ [pluk](https://www.npmjs.com/package/@hivecommons/pluk), whose own minimum is 22
- `tmux`, for `rationguard attach` and for watching live sessions
- The AI CLI you want to supervise (`claude`, `copilot`, `gemini`, `goose` or `codex`), already installed and authenticated
@@ -28,32 +33,62 @@ rationguard catches these patterns and provides rebuttals — in real-time via [
## Install
```bash
-npm install -g @hivecommons/rationguard @hivecommons/pluk
+npm install -g @hivecommons/rationguard
```
---
-## Releases
+## Quick Start
-Releases ship from a version-bump PR: on merge to `main`, the Auto Release workflow creates the `v` tag and dispatches the publish workflow (a manual `v*` tag push also works; see [CONTRIBUTING.md](https://github.com/hivecommons/rationguard/blob/main/CONTRIBUTING.md#releases)). The publish workflow runs the test gate, then publishes `@hivecommons/rationguard` to npm with provenance via [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers) (GitHub Actions OIDC, `npm` environment) — no long-lived npm token is stored in the repository.
+### 1. Check text without controlling an agent
----
+```bash
+rationguard check "Standing by for further instructions. Nothing to do."
+```
-## Quick Start
+Expect a report containing matching phrases such as `standing by` and
+`nothing to do`, a category, and a suggested rebuttal. Exact scores and match
+counts depend on the installed pattern set. This sends nothing to an agent
+and does not bypass permission prompts. Review false positives before enabling
+automatic responses.
+
+### 2. Optionally watch a live session
-One command — creates a tmux session, starts the AI CLI, wires pluk event capture, opens a terminal window for you to interact, and runs rationguard in this terminal to detect rationalizations:
+Install the companion on Node.js 22+:
```bash
-rationguard attach my-agent --cli=claude --rebuttal=send --dangerous
+npm install -g @hivecommons/pluk
+```
+
+Create a tmux session, start the installed and signed-in AI CLI with normal
+permission checks, and log detections:
+
+```bash
+rationguard attach my-agent --cli=claude --rebuttal=log
```
What happens:
1. A tmux session named `my-agent` is created
-2. `claude --dangerously-skip-permissions` starts inside it (the `--dangerous` flag maps to the right permission-skip flag per CLI)
+2. `claude` starts inside it with its normal permission prompts
3. pluk captures all terminal output via `tmux pipe-pane`
-4. A **new terminal window opens** attached to the tmux session — this is where you interact with the agent
+4. On macOS with iTerm2 or Terminal.app, a new terminal window opens attached
+ to the session. Elsewhere, use the printed `tmux attach -t my-agent` hint
5. rationguard watches the pluk event stream in your original terminal, printing detections
-6. When a rationalization is detected, the rebuttal is **sent directly into the tmux session** — the agent receives it as its next message
+6. Matches and suggested rebuttals are logged; `--rebuttal=log` does not send them
+
+### 3. Choose interventions separately
+
+`--rebuttal=send` sends another message into the agent session. It is not
+required for detection. For an existing monitored session, opt in with
+`rationguard watch my-agent --rebuttal=send`.
+
+The separate optional `--dangerous` flag skips supported CLIs' permission
+prompts. It is not required for detection or automatic rebuttals; use it only
+in an environment you have deliberately isolated and authorized.
+
+## Live-control reference
+
+The examples below are alternatives, not a sequence to run on one session.
```bash
# Start goose with auto-approve
@@ -135,7 +170,8 @@ interval (default 60s) and once more when the watcher stops. It never
includes the session name, raw output, or excuse text — only fixed counter
categories — and nothing is sent anywhere.
-When rationguard detects an excuse, it prints the match and sends the rebuttal directly to the agent:
+With `--rebuttal=send`, a qualifying detection prints the match and sends the
+rebuttal directly to the agent. In log mode it only prints the suggestion:
```text
⚠ 100% False Completion — "tests are all passing"
@@ -154,6 +190,11 @@ After sending any rebuttal, rationguard enters a **60-second quiet period** —
## Three Modes
+The following is reference for integrations. For a first check, use
+[Quick Start](#quick-start). [Hooks](#as-a-claude-code-stop-hook) can continue
+an agent automatically; [learning-store rules](#auto-learning) explain which
+patterns are trusted for automatic sending. Review both before enabling them.
+
### Mode 1: Real-time detection via pluk (live)
Connect to a pluk event stream and check agent output as it happens.
@@ -393,12 +434,17 @@ The 15 default excuses in rationguard are the universal patterns that apply to a
---
+## Releases
+
+Releases ship from a version-bump PR: on merge to `main`, the Auto Release workflow creates the `v` tag and dispatches the publish workflow (a manual `v*` tag push also works; see [CONTRIBUTING.md](https://github.com/hivecommons/rationguard/blob/main/CONTRIBUTING.md#releases)). The publish workflow runs the test gate, then publishes `@hivecommons/rationguard` to npm with provenance via [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers) (GitHub Actions OIDC, `npm` environment) — no long-lived npm token is stored in the repository.
+
## Contributing & Community
rationguard is part of [Hive Commons](https://hivecommons.dev), a family of tools for working with AI agents.
- Want to contribute? See [CONTRIBUTING.md](https://github.com/hivecommons/rationguard/blob/main/CONTRIBUTING.md) for local setup, the test and coverage gates, and pull request guidelines.
- rationguard flagged text that is not an excuse, missed one that is, or you want to propose a new default excuse? Open an [excuse pattern report](https://github.com/hivecommons/rationguard/issues/new?template=excuse_pattern.yml).
+- Curious what 1.0 requires or which CLIs are first-class? See [ROADMAP.md](https://github.com/hivecommons/rationguard/blob/main/ROADMAP.md).
- Found a bug or have an idea? Open an [issue](https://github.com/hivecommons/rationguard/issues/new/choose).
- Reporting a security vulnerability? Follow [SECURITY.md](https://github.com/hivecommons/rationguard/blob/main/SECURITY.md) to report it privately, not through public issues.
- Questions? Join the [Hive Commons Discord](https://hivecommons.dev/discord).
diff --git a/docs/content/spektacular/getting-started.md b/docs/content/spektacular/getting-started.md
index 6e17c384..f0712d55 100644
--- a/docs/content/spektacular/getting-started.md
+++ b/docs/content/spektacular/getting-started.md
@@ -30,17 +30,38 @@ The tutorial works with every coding agent Spek supports. Pick yours at
the top of the page, and the agent-specific commands and screenshots will swap
to match.
+## Before you start
+
+This tutorial is for developers comfortable editing Go and working in a
+terminal. You need Git, the Go toolchain required by the example repository,
+Spektacular, and a supported coding agent with its account or model-provider
+credentials configured. A binary download of Spektacular does not include that
+agent. You should understand HTTP APIs and SQLite (an embedded database stored
+in a file); the walkthrough focuses on the workflow, not teaching Go or SQL.
+
+Work in a checkout you can change and review the agent's proposed commands.
+**Terminal commands** below run in your system shell. **Agent inputs** such as
+`/spek-new` are typed inside your coding agent, launched in that same checkout.
+Plain text or Markdown examples are document content, not shell commands.
+
+The route is [setup](#setting-up-your-project) → [specify](#the-specification)
+→ [add context](#context) → [review a plan](#the-plan) →
+[implement and verify](#implementation). Agent-integration and multi-store
+details are optional detours; finish each stage's check before continuing.
+
## What is Spektacular (Spek)
Spek is a CLI and a collection of skills that integrates with your coding
agent and drives Spec Driven Development end to end. It is agent-agnostic,
-shipping with adapters for Claude Code, Codex, Bob, and oh-my-pi (omp), and it has a pluggable
-data layer so you can store your speks and knowledge base in whatever way works
-for you.
+shipping with adapters for Claude Code, Codex, Bob, and oh-my-pi (omp).
+Its storage interface is pluggable, but only the file-store provider ships
+today; this tutorial uses local files.
There are three agent skills that do the real work:
-```bash
+These are **agent inputs**, not terminal commands:
+
+```text
/spek-new # scaffold a new specification
/spek-plan # turn a spek into a detailed implementation plan
/spek-implement # implement a spek, driving the agent through its plan
@@ -165,19 +186,21 @@ Run `spektacular init` from the root of the codebase you want Spek to
operate on, passing your chosen agent's identifier so Spek knows which
CLI to drive.
+For this tutorial, stay in `tutorial-spektacular-how-to`, the folder entered
+in Step 1. Run **only your chosen agent's terminal command** below; do not
+switch to a placeholder project directory.
+
**Claude Code**
```bash
-cd my-project
spektacular init claude
```
-
+
**Bob**
```bash
-cd my-project
spektacular init bob
```
@@ -186,18 +209,19 @@ spektacular init bob
**Codex**
```bash
-cd my-project
spektacular init codex
```
**Omp**
```bash
-cd my-project
spektacular init omp
omp
```
+
+Optional: omp integration files and shared-project upgrades
+
This installs the six skills in `.omp/skills/`, command wrappers and native
input-preserving handlers in `.omp/commands/`, and eight always-applied
standing-rule files in `.omp/rules/`, which carry the same short rules as
@@ -214,6 +238,8 @@ refreshes only the recorded agent, so another agent's copies fall behind.
After upgrading, run `spektacular init` once for each agent to refresh all
files and avoid duplicate current/stale skills in omp.
+
+
This command installs the skills for your agent and creates the
`.spektacular/` folder, which contains the default config and the
project-level knowledge base.
@@ -227,6 +253,10 @@ Let's now look a little deeper at the specification.
## The specification
+**Stage goal:** capture requirements and acceptance criteria for SQLite
+persistence, then review the saved spek before planning. The document examples
+below show what to enter during the agent's interview.
+
If you have worked with Product Requirement Documents (PRDs), you will be
familiar with the process of translating business requirements into a technical
specification that you can turn into code. The spek in Spek
@@ -262,10 +292,11 @@ feature.
[Watch this section on YouTube (from 1:48)](https://youtu.be/ZRVa9gml_Bg?t=108)
-Start a new instance of your agent and run the following to start the spek-new
-skill:
+Start your coding agent in the same `tutorial-spektacular-how-to` folder.
+Type the following **inside the agent**, not in your system shell, to start
+the spek-new skill:
-```bash
+```text
/spek-new
```
@@ -489,7 +520,7 @@ its own solution.
For that reason we will leave this section blank.
-```bash
+```text
No technical direction is required.
```
@@ -499,11 +530,11 @@ Next let's look at how we will measure the success of this feature.
[Watch this section on YouTube (from 6:47)](https://youtu.be/ZRVa9gml_Bg?t=407)
-The Success Metrics section is where you define how you will measure the success
-of the feature you are building. Define these so they are measurable, for
-example:
-* p99 latency of the read API is under 200ms
-* Read API should return the correct monster
+Success metrics describe measurable outcomes, not testing methods. For
+example, a production service might require 99% of read requests to complete
+within 200 ms under a specified load. “Test manually” describes a method, not
+an outcome. This tutorial uses its acceptance criteria to define the behavior
+to verify and does not add a separate performance target.
**Claude Code**
@@ -518,12 +549,11 @@ defined in the plan and created during the implementation phase. If the
feature's success cannot be measured with automated tests, the implementation
phase will generate a manual testing plan instead.
-For this tutorial we will keep it simple and test our migration manually. To do
-that, we can simply add the following:
-
-```markdown
-Will test manually
-```
+For this tutorial, leave **Success Metrics empty**. Review the tests the agent
+produces against the acceptance criteria. If any criterion cannot be checked
+automatically, review and run the resulting manual test plan. The historical
+screenshots may show different interview wording; they do not override this
+choice.
Finally we have the non-goals section.
@@ -601,6 +631,10 @@ let's look at something essential to generating a good plan: the context.
## Context
+**Stage goal:** add the context-cancellation and dependency-injection
+conventions before planning, then confirm both entries exist. The shared-store
+configuration at the end is optional for this single-repository exercise.
+
If we think back to the manual software development process, a product owner
creates a business requirement and hands it to a developer to implement. How the
developer translates that requirement into code draws on two sources of
@@ -702,12 +736,19 @@ injection, with the consumer relying on an interface rather than a concrete
implementation.
Let's add two new files to the knowledge base that capture these conventions.
+Run these **terminal commands** from the example repository's top-level folder,
+after initialization. They copy the tutorial's existing conventions from its
+`main` branch, not from a guessed project directory.
```bash
git show main:.spektacular/knowledge/conventions/context-cancellation.md > .spektacular/knowledge/conventions/context-cancellation.md
-git show main:.spektacular/knowledge/conventions/dependency-injection.md > .spektacular/knowledge/conventions/dependency-injection.m
+git show main:.spektacular/knowledge/conventions/dependency-injection.md > .spektacular/knowledge/conventions/dependency-injection.md
```
+You should now have `context-cancellation.md` and `dependency-injection.md`
+in `.spektacular/knowledge/conventions/`. Confirm the conventions are readable
+with `spektacular knowledge always-applied` before starting the plan.
+
These files contain the coding conventions and code snippets that the agent will
use when generating the plan.
@@ -762,6 +803,9 @@ supports multiple knowledge sources.
### Knowledge that spans repositories
+
+Optional detour: configure shared knowledge across repositories
+
The conventions we have just covered could realistically be team- or
language-based rather than specific to this repository. That kind of knowledge
belongs to the project rather than to any one repository, so you declare it in
@@ -799,10 +843,22 @@ came from. Narrow a request with `--tier` and `--filter` when you only want
particular stores. These results are passed back to the agent, which can use
them or request further clarification from the user.
+
+
Let's see all of this in action by generating the plan for our feature.
## The plan
+**Stage goal:** produce and review `plan.md`, `research.md`, and `context.md`
+for the spek you just approved. Inside the agent, type:
+
+```text
+/spek-plan
+```
+
+Select your SQLite migration spek when prompted. Do not implement until you
+have reviewed the proposed approach and how it verifies the acceptance criteria.
+
[Watch this section on YouTube (from 9:46)](https://youtu.be/ZRVa9gml_Bg?t=586)
The plan is the technical implementation blueprint of what the agent will
@@ -859,6 +915,13 @@ and generate the final code.
## Implementation
+**Stage goal:** implement the approved plan, inspect the changes, and verify
+each acceptance criterion. Inside the agent, type:
+
+```text
+/spek-implement
+```
+
[Watch this section on YouTube (from 18:24)](https://youtu.be/ZRVa9gml_Bg?t=1104)
In terms of user input, the implementation phase is the simplest of the three,
@@ -887,13 +950,15 @@ Once all the milestones are complete, the agent will give you a final summary of
**Claude Code**
-
+
The agent will attempt to create and run automated tests to verify the feature.
For anything it cannot cover automatically, it writes a manual test plan into the
-plans directory instead. In our case we defined no explicit success metrics, so
-the agent verified the acceptance criteria with automated tests and recorded that
-no manual testing was needed:
+plans directory instead. In the recorded example run, there were no separate
+success metrics and the agent reported that automated tests covered all
+acceptance criteria, so no additional manual test plan was needed. Your run
+may differ: inspect the actual tests, their results, and any remaining manual
+checks rather than treating this historical report as a guarantee:
```markdown
# Test Plan: 20260605141835-sqlite-migration
diff --git a/docs/content/spektacular/unknown-criteria.md b/docs/content/spektacular/unknown-criteria.md
index 5deabd66..6d358f24 100644
--- a/docs/content/spektacular/unknown-criteria.md
+++ b/docs/content/spektacular/unknown-criteria.md
@@ -6,6 +6,13 @@
_How you can use a spec-driven development flow when the criteria for a feature are unknown or incomplete._
+Read the [getting-started tutorial](https://spektacular.dev/tutorials/getting-started) first.
+This is a case study in investigating an unclear problem before writing
+requirements, not a setup guide. **SDD** means spec-driven development.
+**Spek** is the tool; a **spek** is its requirements document. `spec` commands
+operate that workflow, while the `spek-new` skill is the agent-side instruction
+set that guides you through it.
+
A question I often ask myself when I am working on a project is "Should I be
using spec driven development for this problem?". The answer is usually yes, but
sometimes I do not have a clear idea of what the criteria for the work are.
@@ -32,7 +39,7 @@ coding my way to another problem.
## A bug in Spek
Let's look at a concrete example. I was recently working on what I believed to be
-a bug in Spek when using the `new` mode to create specifications with
+a bug in Spek when starting a new specification with
the Bob agent. The way that Spek works is that internally the skills are
deliberately very simple: they only instruct the agent to do a single task.
Further instructions are provided to the agent once it has completed the task.
@@ -41,7 +48,12 @@ agent, it avoids a common problem in large, complex skills: the agent can skip a
step or go off on a tangent. The state machine approach keeps it focused on the
task at hand.
-An example of one of these skill prompts looks like this:
+An example of one of these skill prompts looks like this. The main idea is
+that a finite-state machine (FSM: a workflow with allowed next steps) checks
+transitions; an “event” is a request to move to one of those steps.
+
+
+Optional reference: the internal prompt from this investigation
```markdown
**You are writing a spec, not a plan.** A spec captures *what* is being built and the direction the user has *already decided* — requirements, acceptance criteria, constraints, and any high-level technical direction. It does **not** design the *how*, and it does **not** investigate the codebase: do not read source files, run code searches, or ground decisions in specific files, types, or functions. That grounding is the plan workflow's `discovery` step. Stay at spec altitude in every step that follows.
@@ -57,6 +69,8 @@ spektacular spec goto --data '{"step":"requirements"}'
```
+
+
When the agent calls `spektacular spec goto --data '{"step":"requirements"}'`,
the state machine validates the agent's current state and requested next step,
and if everything is valid, it returns the next prompt.
@@ -93,7 +107,9 @@ run: spec goto --data '{"step":"requirements"}'
Now, experience and seeing the issue firsthand makes me think that this is
probably what happened. The temptation is to just accept the fix and move on:
this code change probably does not require a spek and a plan, and in
-reality, I could change the code myself in about 30 seconds. But experience
+reality, I estimated I could change the code myself in about 30 seconds.
+That is an estimate about this particular fix, not a promised effort for
+similar problems. But experience
also tells me that this is not the end of the story: my gut tells me that
there is more than one error message like this in the codebase. So let's ask
the agent to look at the code and tell me if there are any other similar