From 74dc1ed196652ff31506c0cce7fd0869ac449a85 Mon Sep 17 00:00:00 2001 From: clubanderson <407614+clubanderson@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:18:11 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=96=20docs:=20resync=20the=20Hive=20do?= =?UTF-8?q?cs=20mirror=20from=20hive@v5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/content/crux/readme.md | 4 + docs/content/hive/agent-configuration.md | 23 +- docs/content/hive/contributor-relay.md | 3 + docs/content/hive/dashboard-sections.md | 115 ++++++- docs/content/hive/env-vars.md | 6 +- .../hive/labels-and-control-signals.md | 303 +++++++++++++++--- docs/content/hive/release-channels.md | 22 +- docs/content/hive/running-at-level-6.md | 5 +- docs/content/hive/troubleshooting.md | 22 +- docs/content/hive/work-sources.md | 4 + docs/content/hotshot/readme.md | 2 +- docs/content/pluk/readme.md | 2 + docs/content/promptargs/readme.md | 14 +- 13 files changed, 460 insertions(+), 65 deletions(-) diff --git a/docs/content/crux/readme.md b/docs/content/crux/readme.md index f5535c6..d0b6fd5 100644 --- a/docs/content/crux/readme.md +++ b/docs/content/crux/readme.md @@ -145,6 +145,10 @@ refuses to start without a token. Set `CRUX_TOKEN` instead of passing [`crux serve`](https://github.com/hivecommons/hive/blob/v5/src/docs/crux-serve.md) page; the Go client lives in `github.com/hivecommons/crux/api`. +When a running service misbehaves (`/v1/health` fails, an agent is stuck, +a release has to be withdrawn), start from the +[runbooks index](https://github.com/hivecommons/crux/blob/main/runbooks/README.md). + ## How it fits with Hive Hive is a system that runs a whole team of agents against a set of diff --git a/docs/content/hive/agent-configuration.md b/docs/content/hive/agent-configuration.md index 4d85f49..c883c99 100644 --- a/docs/content/hive/agent-configuration.md +++ b/docs/content/hive/agent-configuration.md @@ -289,6 +289,10 @@ spec omits `launch_cmd`, Hive still builds the backend command normally. # kick_failed_repeatedly self-report is filed # only if kicks still fail after that restart max_turn_duration: 0s # optional visibility-only Working ceiling; 0 disables + session_recycle: # optional override of the fleet agents.session_recycle + max_rss_mb: 1536 # policy: recycle this agent's CLI session (a normal + max_age: 6h # restart) once idle at the prompt and over the RSS + min_idle: 2m # or age budget; enabled: false turns it off restart_strategy: immediate # how to bring a dead session back beads_dir: /data/beads/scanner # work-record (bead) storage; default per-agent replicas: 3 # materialize scanner, scanner-2, scanner-3 (max 5) @@ -655,6 +659,7 @@ backends: Names compare case-insensitively; an empty `backend:` (the hive default) is always allowed. The list is enforced where placements are decided: - **ACMM pack apply** never places an agent on a disallowed backend. A pack agent whose pack backend is disallowed gets the first allowed `allow` entry instead (or the hive default when `allow` is empty), without the pack's model, so the replacement backend uses its own default model. A pack-owned agent already on a now-disallowed backend is moved the same way on the next apply. Backends an operator explicitly chose (operator-owned) are left alone. +- **ACMM pack agents follow the hive's inference gateway** ([#14678](https://github.com/hivecommons/hive/issues/14678)). When every backend you picked yourself (operator-owned — and any inference backend or gateway name with no `backend_owner`, e.g. one written straight into `hive.yaml`, since a pack never writes one) names the same inference backend or configured gateway, and more than half of the other enabled agents already run on it, a pack agent whose pack backend is a subscription CLI is placed on that gateway instead, with no `model:` so the gateway's default model applies. This applies to agents a level creates and, on every apply (each restart), to existing agents whose backend is still pack-owned — so after an upgrade the L2 `supervisor`/`brainstorm` may move from `copilot` onto your LiteLLM gateway alongside the new `reviewer`. An operator-owned `backend:` or a legacy `backend:` without an owner is never moved, a hive with an operator-picked CLI agent or with only a minority of agents on a gateway is left as is, and a pack that itself names a gateway or whose backend the `backends:` list replaced is not redirected. - **`PUT /api/config/agent/{name}/models`** rejects a placement onto a disallowed backend with `400` and a message naming it. ### Changing backend and model together: use the atomic endpoint @@ -779,7 +784,7 @@ You don't have to design a roster. Hive ships six **ACMM packs** (`level-1.yaml` | L5 | Semi-Autonomous (Semi-Automated) | issues **and** PRs; PRs are gated by literal `hold`; humans batch-approve | | L6 | Autonomous | auto-merge on green CI; no level hold, except cross-repo outreach PRs (target repo not managed by this hive) remain held | -Applying a level **reconciles the whole roster**, not just the diff: missing agents are created (as overlay files in `/data/agent-configs/`), existing agents are merged — pack values fill blanks, but your explicit `backend:`, `model:`, and `enabled: false` always win — and the level's `kick_template`, `mode` and `on_demand` are updated so the agent's *policy* matches the level (an `on_demand` you toggled yourself in the agent's settings dialog is operator-owned and left alone; leaving on-demand starts the agent, entering it stops it). A failed agent doesn't abort the rest; the level is only recorded as cleanly applied when every agent reconciled. +Applying a level **reconciles the whole roster**, not just the diff: missing agents are created (as overlay files in `/data/agent-configs/`), existing agents are merged — pack values fill blanks, but your explicit `backend:`, `model:`, and `enabled: false` always win — and the level's `kick_template`, `mode` and `on_demand` are updated so the agent's *policy* matches the level (an `on_demand` you toggled yourself in the agent's settings dialog is operator-owned and left alone; leaving on-demand starts the agent, entering it stops it). One exception to "pack values fill blanks": a `backend:` the pack itself wrote may be moved onto the hive's inference gateway, as described under [Disabling a backend spoke-wide](#disabling-a-backend-spoke-wide) ([#14678](https://github.com/hivecommons/hive/issues/14678)). A failed agent doesn't abort the rest; the level is only recorded as cleanly applied when every agent reconciled. The L5 roster is the canonical worked example — twelve agents, nine on the governor timer, two opt-in agents paused in every governor mode, plus one on demand: @@ -935,7 +940,7 @@ An empty allowlist denies every repo even with `allow_github_prompt: true` — t A fetched file is capped at 512 KiB (`maxDefinitionBytes`, `defsrc.go:43`); an oversized file is truncated (and likely then fails to parse) rather than consuming unbounded memory. -The per-agent `prompt_source` (a kick template fetched from GitHub by `pkg/promptsrc`) has a tighter, token-oriented cap and a stricter failure mode ([#12130](https://github.com/hivecommons/hive/issues/12130)): 64 KiB by default (`promptsrc.DefaultMaxPromptBytes`, >2× the largest shipped template; `HIVE_PROMPT_SOURCE_MAX_BYTES` overrides), and a body over it is **rejected whole, never truncated**. The kick falls back to the last successfully fetched copy, then to the normal `kick_template` chain, with a logged warning naming the repo, path, size and cap; the dashboard's save/bake flow fails with the same message instead of writing a cut-off policy. See [operator-reference.md](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md#notable-fields). +The per-agent `prompt_source` (a kick template fetched from GitHub by `pkg/promptsrc`) has a tighter, token-oriented cap and a stricter failure mode ([#12130](https://github.com/hivecommons/hive/issues/12130)): 96 KiB by default (`promptsrc.DefaultMaxPromptBytes`, >2× the largest shipped template; `HIVE_PROMPT_SOURCE_MAX_BYTES` overrides), and a body over it is **rejected whole, never truncated**. The kick falls back to the last successfully fetched copy, then to the normal `kick_template` chain, with a logged warning naming the repo, path, size and cap; the dashboard's save/bake flow fails with the same message instead of writing a cut-off policy. See [operator-reference.md](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md#notable-fields). ### Validating a source before saving it @@ -1028,6 +1033,7 @@ Semantics: - An issue must carry **at least one** required label to be eligible. Matching is case-insensitive and **exact** (a prefix like `approved-for-agents-maybe` does not satisfy `approved-for-agents` — prefix matching would over-admit through an approval gate). - **Exempt wins on conflict**: an issue carrying both an exempt label and a required label stays excluded. There is deliberately no separate `exclude_labels` field — the exempt list *is* the exclusion mechanism, applied first. - PRs and the Hold list are unaffected: open PRs are in-flight work, and held issues still appear under On Hold. +- PRs have their own, separate allow-list for **reviews**: `review.require_labels` (Settings → Features → Review Gate → "Only review labelled PRs") keeps the hive from reviewing a PR until it carries an approval label; see [review-swarm.md](https://github.com/hivecommons/hive/blob/v5/src/docs/review-swarm.md#only-review-labelled-prs-reviewrequire_labels). Use a distinct label from the issue gate if the two approvals mean different things to your maintainers. Both polarities are enforced at **enumeration** — the point where GitHub issues become the hive's actionable set — not in the prompt. A filtered issue never enters the queue, never appears in a kick, never triggers plan-from-label, and cannot be re-selected by a confused (or prompt-injected) agent re-listing the repo. Kick prompts additionally state the active require policy so agents know the list is intentionally short. Both lists are edited on the Labels tab; an active require gate is also noted read-only under **Projects**, and hub-managed hives can receive `issue_filter` with their project config over the heartbeat. @@ -1053,6 +1059,19 @@ github: - **Merge.** A PR whose rationale (its closing or referencing links, or the request's declared issue list) traces to an untrusted reporter's issue without any `untrusted_require_labels` acceptance label receives `hold` at **every** ACMM level, including L6, together with a marked notice naming the issue reporter (not the PR author). Any one unaccepted untrusted citation holds. A configured acceptance label supplies maintainer sign-off, so accepted issues do not trigger a new reporter-trust PR hold. A human removes the label; Hive never auto-releases a reporter-trust hold — the level-hold release path recognises the notice and leaves the label, and holdguard re-holds on a new head SHA. `github.reporter_trust_hold` follows `reporter_trust.enabled` unless set explicitly; `project.repo_policies[].reporter_trust_hold` overrides per repo; `HIVE_REPORTER_TRUST_HOLD` locks it from the environment, exactly like the #5117 knobs. - **Where you see it.** Both halves are edited in the dashboard: **Settings → Labels → Reporter trust** (the switch, the association checkboxes, extra logins, triage labels) and **Settings → Repos** (the hold, with the same all-repos default / per-repo override / inherit rows as #5117). An active gate is stated in the read-only note under **Projects**, and each repo card counts issues **needs triage** separately from generic filter refusals. The issue itself carries `needs-triage` while waiting when Hive had to add it. Holds carry the reason in the PR notice and in the `agent_pr_created` audit entry (`reporter_trust_held`, `reporter_login`, `reporter_association`). +### Tracking and archive issues + +Some issues are never meant to be worked or closed: a standing report archive that a workflow appends to every week, a long-lived checklist, a "Keep this issue open" status thread. Mark them with **any one** of the label `hive/tracking`, the body marker ``, or the literal sentence "Keep this issue open" in the body, and the hive treats the issue as a tracking issue. + +Issues managed by [GitHub Agentic Workflows](https://github.com/github/gh-aw) (`[aw] Detection Runs`, the weekly research archives, …) are detected **automatically**, with no hive label needed: any `` body marker (`gh-aw-detection-runs`, `gh-aw-expires: …`), gh-aw's boilerplate sentences ("automatically managed by GitHub Agentic Workflows", "Do not close this issue manually", "Do not assign to an agent", case-insensitive), or the `agentic-workflows` label on an `[aw]`-prefixed title all count (`skip:tracking:gh-aw-`). Either way the hive treats the issue as a tracking issue: + +- **Not advertised.** The good-first-issue picker scores it `skip:tracking:` and never adds `good first issue` / `help wanted`; a tracking issue that already carries those labels has them withdrawn on the next sweep. +- **Not actionable.** Enumeration and the webhook patch sideline it into the `filtered` bucket with reason `tracking` (counted as `tracking` in the repo breakdown), so it never enters the queue, a kick, or the stage engine (it derives to *outside the pipeline*). +- **Never auto-closed.** Every Hive close path goes through the central `CloseIssue`, which refuses a tracking issue; close-on-merge records `tracking_issue` as its skip reason, and the already-done, stale, and advisory sweeps inherit the same refusal. Only an owner closing the item from the dashboard overrides it. +- **Closing keywords are nudged.** A hive-authored PR whose body says `Closes`/`Fixes #` has the reference downgraded to `Refs` with an inline note; a PR created through the hive gets one marked reviewer comment explaining that the issue is an archive and the keyword would be ignored. GitHub itself still honours a `Closes #N` written by a human on a PR the hive does not own — the owning workflow is expected to recreate its tracking issue in that case. + +The `hive/tracking` label also satisfies the tracker-issue check used by `/contribute`, so a tracking issue is never handed to an external contributor either. + **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. ## When to add what diff --git a/docs/content/hive/contributor-relay.md b/docs/content/hive/contributor-relay.md index 6721047..310ba33 100644 --- a/docs/content/hive/contributor-relay.md +++ b/docs/content/hive/contributor-relay.md @@ -1117,6 +1117,7 @@ The agent can now say it explicitly, the same way it says `no_work_needed`: - **Relay: the label.** When the hub's `auth_ok` `permissions` include `issues:write` (every tier from `newcomer` up) and the task is a GitHub issue, the relay runs `gh issue edit --add-label blocked` with the task credential *before* dropping it at task exit — the same ordering as the #7879 PR comment. If the repository does not define the label, the relay creates it once and retries once; any further failure costs a log line, never the completion. Without `issues:write` (an `advisor`-tier credential, an older hub that sends no `permissions`), the cooldown above is the whole hold. **Lifting the label stays human**: the relay never removes it, so the admission gate holds the issue past the cooldown until someone who knows the dependency has cleared says so. - **Prompt: the comment.** The assignment prompt now asks the agent, before printing either `no_work_needed` or `blocked`, to leave one comment on the issue naming exactly what covers or blocks it — the open or merged PR, the commit, or the external dependency — signed with the same `— hive: …` attribution line as a PR body. That comment is what puts the finding on GitHub rather than only in this hub's ledger: it creates the PR→issue cross-reference nobody had made (fsdk-containers#299 was correctly `no_work_needed` for open PR #289 with no text ref, no sidebar link and an empty cross-reference timeline, so the merge could not close it and the next cycle had to re-verify), and it tells a human why the `blocked` label appeared. The prompt also fixes a boundary: the agent must never edit someone else's PR body to add `Fixes #N` — if that PR's merge should close the issue, it says so in a comment on the PR, noting that only a maintainer editing the body makes the merge close it. - **Not blocked: the contributor's own tooling.** A `blocked` whose reason describes the *contributor's* environment failing — the push to the fork rejected, a token without the `workflow` scope, a credential or permission error, the `gh`/`git` wrappers, a stale fork — is not blocked on anything outside the repository ([#11775](https://github.com/hivecommons/hive/issues/11775), [#11776](https://github.com/hivecommons/hive/issues/11776): two issues sat `blocked` fleet-wide with no PR after exactly that). The relay demotes it (`isToolingFailureBlockedReason`): no label, and the wire carries the `verdict_reason` but no verdict, so the hub books an idle completion on the short cooldown and re-offers the issue. The hub applies the same classifier to what it receives (`reclassifyToolingBlockedVerdict`, for relays older than this), and when it demotes one it removes the `blocked` label the relay applied and leaves a single `` comment saying why. The prompt also tells the agent outright that its own environment failing is never a `blocked` issue. +- **Not complete: the agent's environment is broken.** A verdict under any token except `wrong_base` whose reason says the agent's own runtime cannot run commands — "environment failure", "Bash tool broken", "every command fails", `apply-seccomp`, `/proc/self/setgroups` (`ENVIRONMENT_FAILURE_REASON_PATTERNS`) — is classified `blocked` with `environmentFailure` set, labels nothing, and is reported as `task_failed` with `failure_kind: environment` instead of `task_complete` ([#14832](https://github.com/hivecommons/hive/issues/14832): on Ubuntu's AppArmor user-namespace restriction every sandboxed command failed and the agent's `HIVE_VERDICT: complete — environment failure: …` was booked as a finished task). A PR this task opened still overrides the verdict. ### A maintainer-decision verdict is labelled until a human answers @@ -1262,6 +1263,8 @@ 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). + **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. This does not loosen who may claim what. The restored record is one the *hub itself* wrote, and a resume is matched against it exactly as before; a lease whose window has already passed is dropped at startup rather than restored. diff --git a/docs/content/hive/dashboard-sections.md b/docs/content/hive/dashboard-sections.md index b5abbcc..2652a64 100644 --- a/docs/content/hive/dashboard-sections.md +++ b/docs/content/hive/dashboard-sections.md @@ -433,6 +433,8 @@ Review Pipeline shows pull requests by stage first; Review Queue lists the same Each open PR in the stage the review pipeline derived for it (GET /api/review/pipeline), with reviewers, P0–P3 findings, the fix-loop counter and the next action. +When **Settings → Features → Review Gate → Only review labelled PRs** (`review.require_labels`) is on, PRs without one of the approval labels stay in the *unreviewed* stage until a maintainer labels them: the planner never dispatches them and the relay refuses reviews on them. + **What it tells you.** Each column is one stage of the review loop: unreviewed, reviewing, changes requested, fixing, waiting for a human, approved, merged and abandoned. Each card shows the pull request, who has reviewed it, how many findings were raised at each severity and what should happen next. **How the numbers are worked out.** Hive reads the last review results it recorded for each pull request and places the card in a stage from those results alone. The collapsible Pipeline tile header shows how many pull requests are on the board. Nothing is fetched from GitHub when you open it. @@ -628,6 +630,10 @@ More about the line: **What it is good for.** Use it to pause a worker, restart a stuck one, or change its settings. Fix any card that says it needs a login. Read the **Now:** lines to see what each worker is on without opening its log. +**The log buttons.** `📄 full log` opens the worker's whole log and `📋 log text` copies it; `🕘 past kicks` lists the logs of earlier rounds. For a normal worker this is the terminal it is typing in. For a worker that runs each round in a sandbox or a Kubernetes Job (its card says so), there is no terminal: the buttons show the output Hive captured from the sandbox when the last round ended, and `past kicks` holds the earlier rounds. Before such a worker's first round has ended the button says there is no archived run yet. + +**Sandboxed worker's last run.** A sandboxed worker's card also carries a **last run** line: when it ran, how long it took, whether it succeeded, and the pull request it opened. If the worker's own report said the run failed, or the sandbox exited with an error, Hive keeps any commits the run made off GitHub and the line reads `no PR opened: ` — for example `no PR opened: agent report status=failed: RPM install returned 401`. The same line appears when a configured publish preflight (`publish.verify`, for example a whole-suite `pytest --collect-only`) failed after the run: `no PR opened: verify failed (1/2): uv run pytest --collect-only -q (exit 2)`, with the command's output tail in the tooltip. The full reason is in the line's tooltip and in the full log's outcome block, where each verify command has its own `---- verify: … ----` section. + **Example.** A card says "needs login". You click **Login** and sign in again. Another card says `Now: working — no issue or pull request yet · started 50 min ago`. That worker has changed nothing through the hive for 50 minutes, so you open its log to see what it is doing. **When it appears.** Always. @@ -642,35 +648,118 @@ 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/060) → Merge gate (080/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. 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`) 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 health modifier or on 071 / 072 / 085 / 090), 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`) 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 health modifier or on 071 / 072 / 085 / 090), 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). +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). + +**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. **Repo chip.** Each row carries a small monospace chip before the number naming the repository it belongs to (`dibs #456`, `hive #13524`); the chip is tinted per repository from a fixed palette so one repo always looks the same, the configured primary repo is shown muted, and the full `owner/repo` is in the tooltip. Linked PR sub-rows carry the same chip. Clicking a chip sets the **Repo** filter to that repository (click it again, or pick "All repos", to clear). By default the chip appears only when the list spans more than one repository; the ⚙ settings modal's **Repositories** section offers **Show repo chip: always / only when multiple repos / never** (stored under `hive.tracker.repochip`). **Group.** The **Group** control next to **Sort** switches between **None** (one flat list, the default) and **Repo**. Grouped by repo, rows sit under a sticky header per repository showing the `owner/repo`, its row count, how many of those rows need attention, and a mini distribution of rows across the pipeline columns; the primary repo's group comes first, then groups by row count. The chosen **Sort** still orders rows within each group. A header's chevron collapses that group to its header alone; collapsed repos are remembered across reloads (`hive.tracker.group.collapsed`), as is the Group choice itself (`hive.tracker.group`), which is also available in the settings modal. Group headers are keyed by repo so a refresh keeps them in place while rows move, and a header leaves only after the last row in its group has played its exit animation. -**Sounds.** The 🔇/🔊 chip at the right end of the toolbar turns on two optional, browser-made sounds: a "bug zapper" that plays when a pull request already on the list merges while you have the page open (never on first load, never for issues, once per pull request, at most two zaps per refresh, silent while the tab is hidden), and a short bee-like buzz when an issue or pull request first appears on the list (never on first load, once per item, at most two buzzes per refresh, same hidden-tab rule). Both are off by default and remembered per browser; switching them on plays each sound once so you know what to listen for. They fire for every repo the hive governs — there is no primary-repo scoping in the detection. +**Sounds.** The 🔇/🔊 chip at the right end of the toolbar turns on two optional, browser-made sounds: a "bug zapper" that plays when a pull request already on the list merges while you have the page open (never on first load, never for issues, once per pull request, at most two zaps per refresh, silent while the tab is hidden), and a short bee-like buzz when an issue or pull request first appears on the list (never on first load, once per item, at most two buzzes per refresh, same hidden-tab rule). Both are off by default and remembered per browser; switching them on plays each sound once so you know what to listen for. They fire for every repo the hive governs — there is no primary-repo scoping in the detection. Browsers only let a page make sound after you have clicked or pressed a key on it, and that permission does not survive a reload — including the dashboard's own automatic reload when a new hive version is published. If sound is on but the browser is still holding the audio engine back, the 🔊 chip turns amber (hover it for the reason) and, once per page load, a toast offers an **Enable sounds** button; pressing it, or any click or key press on the page, releases the engine and the chip returns to normal. A zap or buzz that fell in that window is skipped (the toast says so) rather than queued to burst out later. -**Sound settings (⚙ on the card header).** The gear on the Hive activity monitor card opens an in-app settings modal with a **Play sounds** master toggle (the same preference as the toolbar chip), a **Volume** slider (0–100, applied to the built-in sounds and to your own files alike, remembered per browser), and one row per event — **Open sound** (a new issue or pull request appears) and **Merge sound** (a pull request merges). Each row shows what currently plays ("Built-in (bee buzz)" / "Built-in (zapper)" or your file's name), a **▶ Play** preview (the button reads *Playing…* while the sound runs; if the browser refuses to start audio, a toast says so rather than failing silently), a **Choose WAV…** picker that accepts `.wav` or any other audio your browser can decode (up to 4 MB; larger files are refused with a toast), and **Reset to built-in**. Uploaded files are kept in this browser only (IndexedDB) and decoded with the Web Audio API; if a stored file cannot be decoded the built-in sound plays instead and the row says so. **▶ Play both** auditions the merge sound followed by the open sound. Previews start the browser's audio engine inside the click itself and wait for it to come up (about a second at most) before playing, so they work in Safari and on a page that has never made a sound; an automatic zap or buzz on an engine the browser still holds back waits only briefly and otherwise skips rather than queueing up to burst out on your next click, and the next click or key press re-arms it. +**Sound settings (⚙ on the card header).** The gear on the Hive activity monitor card opens an in-app settings modal with a **Play sounds** master toggle (the same preference as the toolbar chip), a **Volume** slider (0–100, applied to the built-in sounds and to your own files alike, remembered per browser), and one row per event — **Open sound** (a new issue or pull request appears) and **Merge sound** (a pull request merges). Each row shows what currently plays ("Built-in (bee buzz)" / "Built-in (zapper)" or your file's name), a **▶ Play** preview (the button reads *Playing…* while the sound runs; if the browser refuses to start audio, a toast says so rather than failing silently), a **Choose WAV…** picker that accepts `.wav` or any other audio your browser can decode (up to 4 MB; larger files are refused with a toast), and **Reset to built-in**. Uploaded files are kept in this browser only (IndexedDB) and decoded with the Web Audio API; if a stored file cannot be decoded the built-in sound plays instead and the row says so. **▶ Play both** auditions the merge sound followed by the open sound. Previews start the browser's audio engine and schedule their sound inside the click itself (Safari only unlocks audio for work done synchronously in a user gesture) and otherwise wait for the engine to report it is running (about a second at most), so they work in Safari and on a page that has never made a sound; an automatic zap or buzz on an engine the browser still holds back waits only briefly and otherwise skips rather than queueing up to burst out on your next click, and the next click or key press re-arms it. **Movement animation (⚙ on the card header).** When a refresh moves an item's lit chevron to a different column — forward (advance) or backward (regress) — the row plays a short, subtle three-beat sequence so the eye can see where it went: first the chevron it is *leaving* flashes the active highlight twice (two ~140 ms pulses with a short gap), then the chosen effect travels from that chevron to the new one, and finally the new chevron lands lit — the whole sequence takes about a second. Pick the travel effect in the same settings modal, under **Movement animation** below the sound settings: **None**, **A Sliding puck** (a highlight glides from the old chevron to the new one and settles), **B Ripple** (each chevron between old and new lights in turn), **C Ghost crossfade** (the old highlight fades out while the new one fades in with a slight slide), **D Comet** (a streak with a bright head travels the strip and lands on the destination), **E Destination bump** (the destination chevron scales briefly with a fading direction arrow), or **F Colour wash** (default — a tinted sweep flows across the intermediate chevrons). Each option has a *Preview* button that plays the full flash → move → land sequence on a sample row inside the dialog. The choice is remembered per browser (`localStorage` key `hiveStageTrackerMotion`). Items that stay put never move; a row that leaves the list (merged, closed, or filtered away) collapses and fades out over `--ham-move-exit-ms` and a row that arrives fades in over `--ham-move-enter-ms`; nothing animates on first load, and at most twelve rows animate per refresh (a filter or page change that turns over more rows than that redraws at once). The list is reconciled row by row on every refresh rather than rebuilt, so a move keeps playing while the hive keeps publishing. When the operating system asks for reduced motion (`prefers-reduced-motion: reduce`), every style falls back to a single soft flash followed by a plain crossfade and departing rows only fade, regardless of the setting; **None** disables the flash, the effect and the row fades entirely. Durations, the flash timing (`--ham-move-flash-ms`, `--ham-move-flash-gap-ms`, `--ham-move-flash-count`) and tints come from the `--ham-move-*` tokens in `tokens.css`. -**Sentinel alert.** A pull request the suspicious-activity sentinel flagged — it wears the `sentinel-alert` label (`sentinel.label`), or the review evidence bundle for its current head records sentinel findings — carries `sentinel_alert: true` in the tracker payload (with `sentinel_findings` and a short `sentinel_reason` when the bundle has them); a webhook label change flips it live. In the **Needs human decision** lane the lit chevron's own words read **Sentinel alert · needs human** (danger-tinted, the same wording in the cell's `title` and `aria-label`, and in the compact caption), so a maintainer reads the security implication before the stage mechanics; in every other lane the row shows a compact danger-tinted `sentinel` chip ahead of `needs rebase` / `regressed` / `stalled`. The tooltip reads "Sentinel flagged security-relevant changes at ``: ``" (a generic description stands in when the bundle has no reason). An issue carried by one flagged PR shows the lane text too, and a nested PR sub-row shows its own. The *Decide…* dialogs for a flagged PR open with a ⚠ line per flagged PR naming the sentinel reason, and *Approve* is styled as a dangerous action. Lane text and chip update in place on refresh (keyed reconcile, no flash). +**Sentinel alert.** A pull request the suspicious-activity sentinel flagged — it wears the `sentinel-alert` label (`sentinel.label`), or the review evidence bundle for its current head records sentinel findings — carries `sentinel_alert: true` in the tracker payload (with `sentinel_findings` and a short `sentinel_reason` when the bundle has them); a webhook label change flips it live. In the **Needs human decision** lane the lit chevron's own words read **Sentinel alert · needs human** (danger-tinted, the same wording in the cell's `title` and `aria-label`, and in the compact caption), so a maintainer reads the security implication before the stage mechanics; in every other lane at or past the merge gate (080, 090, 095, 100) the row shows a compact danger-tinted `sentinel` chip ahead of `needs rebase` / `regressed` / `stalled`; before the gate the chip is held back with the other merge-gate pills (see *Pills before the merge gate*) while `sentinel_alert` stays truthful in the payload. The tooltip reads "Sentinel flagged security-relevant changes at ``: ``" (a generic description stands in when the bundle has no reason). An issue carried by one flagged PR shows the lane text too, and a nested PR sub-row shows its own. The *Decide…* dialogs for a flagged PR open with a ⚠ line per flagged PR naming the sentinel reason, and *Approve* is styled as a dangerous action. A person's *Approve* through that dialog **is** the human review the alert asked for: it removes the `sentinel-alert` label when present and records a `sentinel_acknowledged` human action keyed to the head in the review evidence bundle, so a bundle-only alert (findings but no label — hivecommons/crux#30) clears too; the payload then reads `sentinel_alert: false` with `sentinel_reason` suffixed `— acknowledged by @login`, the findings stay on record, and a new head starts over. The delegated *Let hive decide* path never clears it. Hovering the lit chevron of a parked row names the parking stage itself (`Needs human decision`), not the 080 column it rides on; `Next move` follows the real stage (human at 085, hive at 080). Lane text and chip update in place on refresh (keyed reconcile, no flash). -**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**. 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. -**Closed issue, open PR.** A pull request whose `Fixes #N` issue has already closed (auto-closed by a bot, or closed by hand) while the PR is still open is live work, not a stale line: the row is anchored on the PR ([#14655](https://github.com/hivecommons/hive/issues/14655)). The PR's own icon, number and title lead the row at full width, its stage drives the strip and its timestamps sort it — closing the issue is not a regression of the PR — and the closed issue shrinks to a muted `closes #N · closed` chip linking to it. An issue closed as *not planned* adds an `issue closed (not planned)` chip so the reader can tell the PR may be moot. At 090 such a row still counts under *Needs attention*: the human has a merge to click. When the PR closes or merges too, the row leaves the list as before (a merged PR shows in *Recently merged* for its retention window). +**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. + +**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 | +| --- | --- | --- | +| `hold` | actionable now | always (a person placed it) | +| `draft` | actionable now | always (the author's move) | +| `stalled` | actionable now | always | +| `regressed` | actionable now | always; muted `stage ↓` unless the row is parked on a person (see above) | +| `needs-rebase`, `blocked`, `triage needed`, `possible dupe` | actionable now | always | +| `unauthorized` — `unauthorized:approve-workflow-runs` | actionable now | always — the carve-out: held workflow runs mean CI cannot even start, and waiting for the gate would waste the whole CI / review cycle (rationguard#397 at 072) | +| `unauthorized` — any other step (merge, branch update, no step named) | merge gate | PR at stage/080 or later | +| `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 | + +**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. + +**Adopted → #N (074 / 085).** An adopted pull request names the hive's superseding copy in its detour word (`adopted_by`). When that copy was closed without merging (hivecommons/hive#15052) the row does not fall back to *Needs human decision*: `adopted_by_state: "closed"` renders "Adopted → #N (closed) · recovering" while the hive opens a fresh copy from the same head, and the caption says so in plain words; once the new copy is on record the word reads "Adopted → #M". If two copies are closed within an hour the hive stops (`adopted_by_state: "closed-twice"`, park reason `adoption-child-closed-twice`): the row reads "Adopted → #N (closed twice) · needs a person" and the popover question asks you to reopen the last copy, give direction, or close the PR. With close-first supersession (the default, hivecommons/hive#15135) the original is closed in the adopting pass and has **no row** while its copy is open — the copy is the one active PR for the issue, and the original's record is superseded (no chevron, no *Needs human decision*). A copy that then fails (closed unmerged, or red at two heads) reopens the original: `adopted_by_state: "copy-failed"` renders "Adopted → #N (copy failed) · reopened, needs a person" at 085 with the caption "The hive's copy #N failed; this PR was reopened and needs a person". +**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. + +**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. + +**Hive's own unblock is live.** When the blocked sweep or the review-backlog block reconciler lifts `blocked` itself (the tracked PR merged or closed; the `` note), it takes the same path as an inbound label webhook: the tracker cache is dropped and the issue is queued for the targeted re-read (`ItemPatches` delivery `self:edit`), which patches the status row and the governor's cached snapshot at once. A reconcile enumeration that was already listing when the unblock happened used to overwrite that patch with its pre-unblock copy, and the rows reverted to `blocked` until the next pass (~an hour on the hive's hive); the governor now replays re-reads newer than the listing's start onto the pass's result before publishing it, so a row never moves backwards behind a read it has already made. + +**Closed issue, open PR.** A pull request whose `Fixes #N` issue has already closed (auto-closed by a bot, or closed by hand) while the PR is still open is live work, not a stale line: the row is anchored on the PR ([#14655](https://github.com/hivecommons/hive/issues/14655)). The PR's own icon, number and title lead the row at full width, its stage drives the strip and its timestamps sort it — closing the issue is not a regression of the PR — and the closed issue shrinks to a muted `closes #N · closed` chip linking to it. An issue closed as *not planned* adds an `issue closed (not planned)` chip so the reader can tell the PR may be moot. At 090 such a row still counts under *Needs attention*: the human has a merge to click. When the PR closes or merges too, the row leaves the list as before (a merged PR shows in *Recently merged* for its retention window). **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. -**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 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 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. 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 — the merge gate takes it from there exactly as if you had typed them; the dashboard reviews, labels and merges nothing — *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. +**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). + +**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. + +**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. + +**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. + +**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. -**Multi-select.** The checkbox in a row's left gutter selects the row; **Shift+click** selects the range from the last clicked row, **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), *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. +**Confirmations and "Don't ask again".** Each confirm dialog a row action raises — Approve (including a sentinel-flagged head: *⚠ Sentinel flagged security-relevant changes at 40a959b …*), Let hive decide, Close as superseded / won't-fix, Mark needs-human and the rest — carries a **Don't ask again for this action** box. Ticking it and confirming stores `hive.tracker.confirm.=skip` in this browser's `localStorage` (one key per action id, e.g. `hive.tracker.confirm.decide:delegate`); later clicks on that action run at once. Nothing the dialog would have said is lost: the result toast carries the same ⚠ sentinel line (it is per head, so skipping the confirm never skips the warning) and the one-line item summary. Actions that need your text (*Give direction…*, *Send back…*, the superseder picker) always ask. The ⚙ settings dialog's **Confirmations** section lists which actions are skipped and has one **Reset confirmations** button that turns them all back on. + +**Who is on it (actor icons).** The small icon after the lit pill's words names whose move the row is, and it comes from the payload — `stage_actor: {kind, login, since, why}`, derived server-side for every item and every nested PR — never from a guess on a login string. `kind` is one of **hive** (⬡ honeycomb: a hive lane or agent acts next; `login` is the agent when one is on it right now), **clanker** (robot head: a registered contributor agent must push), **human** (the person glyph the *Needs human decision* pill uses: the author, approver or a maintainer must act; `login` names them when known), **bot** (cog: a non-hive automation account such as `github-actions[bot]` or renovate authored the PR; the hive does not fix bot PRs, it waits for the bot's next run), or **none** (no icon: nobody is working on it — the pill's tooltip then reads *no one is working on this right now — *). `why` is a short sentence the tooltip appends after *Next move: @*. A `070` Fixing pill is only shown as **Fixing** when someone can fix it; a bot-authored or unowned red PR reads its state in plain words instead — **Conflicting · bot PR**, **Red · nobody on it** — with the matching icon, so a row such as hivecommons/docs#500 (a nightly resync PR that conflicts with its base) no longer looks like active hive work. Issues riding one covering PR take that PR's actor; nested PR sub-rows carry their own. + +**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`. + +**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`, …). + +**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 `·`: + +| 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.* | +| 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).* | +| 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, …*. + +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). | +| 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. | +| `hive:unauthorized` | **Re-evaluate now** (re-reads GitHub; the hive resumes and clears the label once you did the step on GitHub) · Let hive decide · Hold · Mark needs-human. | +| 015 *Triage needed* | Accept triage · Let hive decide · Hold · Mark needs-human. | +| Hold set by an operator | **Release hold** leads; the state's own options stay listed but disabled with *Release the hold first*. | +| Any other stage the hive leaves to a person | Re-evaluate now · Let hive decide · Hold · Mark needs-human, with the question naming the stage. | + +**"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. ### Let hive decide **What it is.** *Let hive decide* is the first entry under *Decide…* on a row parked at *Needs human decision* (an issue at 045, a PR at 085, or an issue whose nested PR is parked there), and under *Decide…* in the selection bar for a bulk run. It asks the hive to make the decision itself, as the owner's delegate, under the owner's standing operating rules — and to record it exactly as a human decision is recorded: a `/hive decision ` comment on the item (plus the queue approval on a PR head when the decision is to release the PR), the parking labels lifted, `triage/accepted` added where it applies, and the stage engine's un-park logged with the requesting operator named. The item leaves 085/045 and proceeds without the owner. The action is audited as `repo_item_decision_delegated` (or `repo_item_decision_delegated_failed`) and the **Decisions log** shows those rows with a *delegated* badge so delegated and human decisions can be told apart and counted. -**How it runs.** Clicking it posts `POST /api/repos/{owner}/{repo}/items/{number}/decide-delegated` (body `{"type": "issue"|"pr", "reason", "caption", "sentinel_reason"}` — the row's park words, so the decider reads why the hive stopped) which enqueues one delegated-decision job per item (a second click while a job is running answers 409 with the running job). The page shows *Hive is deciding acme/console#12…*, polls the same URL (`GET`) until the job reports `done` or `failed`, then the result toast reads what was decided — "Hive decided: acme/console#12 approve (small-low-risk)" — and the row refreshes. A job gathers the item (title, body, labels, author, state), the PR's mergeable state and changed files, whether the author is a hive account, the author's repository permission (trusted = write or above), the sentinel label, and the row's park reason, then runs the policy and executes the resulting actions through the same GitHub helpers the manual *Decide…* entries use. Permission is the one *Hold* uses (owner, or a signed-in user with write access to the repository); the requesting user is named in the recorded decision as the person who delegated it. +**How it runs.** Clicking it posts `POST /api/repos/{owner}/{repo}/items/{number}/decide-delegated` (body `{"type": "issue"|"pr", "reason", "caption", "sentinel_reason"}` — the row's park words, so the decider reads why the hive stopped) which enqueues one delegated-decision job per item (a second click while a job is running answers 409 with the running job). **An issue with two or more open nested PRs is never decided by silently picking one** ([#15009](https://github.com/hivecommons/hive/issues/15009)): *Let hive decide — all N PRs* on the issue row sends `"type": "issue"` with `"prs": [15017, 15020]` (the open nested PR numbers; without the hint the server reads them from the tracker's `linked_prs`), and the server runs one delegated decision per PR — each under the PR's own job key, so `GET …/items/{pr}/decide-delegated` reads it and a PR already being decided is reported, not decided twice — and answers with `items` (one entry per PR: `number`, `type`, `status`, `decision`, `notes`, `error`, `result`) plus a summary `result` (`action: "decided-each"`, outcome "Decided each of the 2 nested pull requests: #15017 approved (small low risk); #15020 no change (already decided).", next "2 sent · 1 decided · 1 no-op. Each PR's own toast says what happens next."). The issue row's toast shows that summary and each PR gets its own result toast on its own sub-row; the job fails only when every PR failed. One nested PR (or none) keeps the issue's own decision as before. Before the request an in-app dialog explains the delegate once (what it records, its rails) with a *Don't ask again for this action* box (see *Confirmations* above; reset from ⚙ → Confirmations). The row shows *Hive is deciding…* over its lit chevron, the page polls the same URL (`GET`) until the job reports `done` or `failed`, and the job's `result` (`rule`, `action`, `target`, `outcome`, `next`) becomes a persistent result toast that says what hive decided — *Hive decided on acme/console#14938 → PR #14947: adopted (contributor offline) — Adopted acme/console#14947 in supersede mode as #14977. The hive rebases and merges it unattended.* — and leaves only when the target row changes state. Clicking again after the same rule already decided the item writes nothing and answers `action: noop-already-applied` — *No change — hive already recorded this decision at