From 9bcce9673d2196b0515ede12831f20afdc77d7d1 Mon Sep 17 00:00:00 2001 From: hivecommons-hive <280983584+hivecommons-hive@users.noreply.github.com> Date: Sun, 1 Nov 2026 06:28:03 +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 | 3 +- .../adr/0026-sandbox-runtime-selection.md | 214 ++++++++++++++++++ docs/content/hive/adr/readme.md | 2 +- .../dashboard-sections/activity-monitor.md | 70 +++++- docs/content/hive/documentation-map.md | 1 + .../stage-labels.md | 8 +- docs/content/hive/landscape.md | 2 +- docs/content/hotshot/roadmap.md | 5 +- docs/content/pluk/readme.md | 1 + docs/content/promptargs/readme.md | 8 + 10 files changed, 297 insertions(+), 17 deletions(-) create mode 100644 docs/content/hive/adr/0026-sandbox-runtime-selection.md diff --git a/docs/content/crux/readme.md b/docs/content/crux/readme.md index 944072af..bd7ccc27 100644 --- a/docs/content/crux/readme.md +++ b/docs/content/crux/readme.md @@ -436,7 +436,8 @@ hivecommons/hive, pushed by Hive's [`crux-sync`](https://github.com/hivecommons/hive/blob/v5/.github/workflows/crux-sync.yml) workflow on every change. `.hive-sync` names the exact Hive commit this tree came from, and a `Sync guard` check rejects pull requests here that -touch Go files. Only the scaffolding is maintained in this repository: +touch Go files, `go.mod`, `go.sum`, `.hive-sync`, `Makefile` or +`docs/MODULE.md`. Only the scaffolding is maintained in this repository: `.github/`, `.goreleaser.yaml`, `Dockerfile`, this README and `LICENSE`. The module's own README is synced to [`docs/MODULE.md`](https://github.com/hivecommons/crux/blob/main/docs/MODULE.md). diff --git a/docs/content/hive/adr/0026-sandbox-runtime-selection.md b/docs/content/hive/adr/0026-sandbox-runtime-selection.md new file mode 100644 index 00000000..b6918505 --- /dev/null +++ b/docs/content/hive/adr/0026-sandbox-runtime-selection.md @@ -0,0 +1,214 @@ +> **Synced from Hive.** This page is pulled from [hivecommons/hive@v5](https://github.com/hivecommons/hive/blob/v5/src/docs/adr/0026-sandbox-runtime-selection.md) during the docs build. Edit the canonical source in the Hive repository. + +# ADR-0026: Sandbox runtime selection + +Status: Proposed + +## Context + +By default Hive agents share one container, separated by Unix user. The opt-in +sandbox ([sandbox-isolation.md](https://github.com/hivecommons/hive/blob/v5/src/docs/sandbox-isolation.md), #2804) runs a kick in a +rootless Podman container or, with `sandbox.runtime: job`, a Kubernetes Job +(#6311). Both are ordinary containers: the boundary is the runtime's default +seccomp profile and namespaces, and network confinement in `restricted` mode is +whatever policy the operator supplies. + +The landscape refresh (#13806) named two external runtimes built for that gap. +#14092 asked for a spike comparing them against the sandbox contract. This +record covers that comparison as a paper evaluation, read from each project's +own documentation on 2026-10-25: + +- **NVIDIA OpenShell** ([repo](https://github.com/NVIDIA/OpenShell), v0.1.5, + 2026-10-20). +- **OpenSandbox** ([repo](https://github.com/opensandbox-group/OpenSandbox), + 1.1.1, 2026-10-09; 1.2.0-rc.2 on 2026-10-21). It is a Linux Foundation project + under AAIF, contributed by Alibaba. + +**Not done in this record:** the prototype run on a hive spoke (#14092 step 2). +Startup latency, image and boot size, and proxy and attribution behaviour are +**not measured** here. [Prototype protocol](#prototype-protocol) lists what to +measure. This record stays `Proposed` until those numbers exist. + +### The contract a runtime must satisfy + +These are the requirements from `sandbox-isolation.md`. The seam is the +`sandbox.Launcher` interface (`src/crux/sandbox/sandbox.go`): +`Run(ctx, LaunchSpec) (Result, error)`. Podman and `crux/kubejob` already +implement it. A new runtime is one more `sandbox.runtime` value behind that +interface. + +| # | Requirement | Where hive enforces it today | +|---|---|---| +| C1 | Task in: a prepared workspace and `.hive/kick-prompt.txt`, a command, an env allowlist | `LaunchSpec` | +| C2 | The workspace is credential-free and no GitHub token enters | `SanitizedEnv`, credential-name filter | +| C3 | Pushes go through the push broker, outside the sandbox | `crux/pushbroker` reads commits back from the workspace | +| C4 | The proxy still mediates GitHub | ADR-0002 iptables redirect in the hive pod; agent identified by socket UID | +| C5 | Transcript and exit code come back; a timeout kills the run | `Result`, `.hive/sandbox-transcript.log` | +| C6 | Hosted spokes (OKE) need no privileged pods | namespace-scoped RBAC from the hub provisioner | + +## Evaluation + +### NVIDIA OpenShell + +**Model.** A gateway is the control plane. A compute driver (Docker, Podman, +Kubernetes or MicroVM) creates two parts per sandbox: + +- a **workload**, which holds the agent and has no network of its own; +- a trusted **supervisor**, which carries every TCP connection and DNS lookup + the workload makes. + +Inside the workload, the agent runs as one non-root user with no Linux +capabilities. Landlock confines files. Seccomp user notification hands each +`connect` and DNS query to the supervisor, which checks it against policy and +opens the real connection. Policy covers files, processes, network endpoints +and HTTP methods. A prover flags proposed changes that would widen +credentialed reach. + +| # | Fit | Notes | +|---|---|---| +| C1 | Yes | `sandbox create` from an OCI image, `sandbox upload` for the workspace, `sandbox exec`, which returns the command's exit code. The Go SDK is `github.com/NVIDIA/OpenShell/sdk/go`. Workspace transport changes: hive's shared-PVC `subPath` model becomes an upload/download, or a mount set in the sandbox template. | +| C2 | Yes, stronger | OpenShell's own premise is that the workload never holds credentials. Hive attaches no GitHub provider, so there is nothing to inject. | +| C3 | Yes | Unchanged. The broker reads commits from the returned workspace. | +| C4 | Changes shape | The supervisor terminates TLS and opens the upstream connection. In the Kubernetes driver the supervisor is **its own pod**, so the hive pod's iptables redirect never sees the traffic, and ADR-0002's socket-UID identification has no meaning across pods. See [Proxy and attribution](#proxy-and-attribution). | +| C5 | Yes | Exec exit code, gateway logs and OCSF JSON export. If the supervisor connection drops, the workload freezes, so the run fails closed. | +| C6 | Yes, with cluster prerequisites | No privileged pod. The workload has no added capabilities, and the gateway meets the Restricted Pod Security Standard. The cluster needs three things: (a) the [Agent Sandbox](https://github.com/kubernetes-sigs/agent-sandbox) controller and CRDs, a cluster-scoped install; (b) a CNI that **enforces** ingress and egress `NetworkPolicy`, because without one the workload can bypass the supervisor; (c) node kernels with Landlock ABI 3 (Linux ≥ 6.2) and the seccomp user-notification operations it probes for. A node that lacks them fails closed at sandbox start. | + +**Licensing and cadence.** Apache-2.0, DCO, public RFC board. There have been +six stable `0.1.x` releases since 0.1.0 on 2026-09-25: stable builds are +promoted from nightly pre-releases that pass qualification. It is pre-1.0, so +expect API movement. Anonymous telemetry is **on by default**. Hive must set +`OPENSHELL_TELEMETRY_ENABLED=false`, or `server.telemetryEnabled=false` in +Helm. + +### OpenSandbox + +**Model.** A lifecycle server (Python, `opensandbox-server`) creates sandboxes +on Docker or Kubernetes, the latter through an operator. Each sandbox runs +`execd` for commands and files. Isolation is chosen per server profile: + +- `""`: plain runc; +- `gvisor`; +- `kata`: a RuntimeClass; +- `firecracker`: Kata-FC, or the "Fast Sandbox" Fastlet path. + +Network policy is an **egress sidecar** in the sandbox's network namespace. It +proxies DNS, sets nftables allow sets and can run an optional transparent +mitmproxy, which the Credential Vault uses. + +| # | Fit | Notes | +|---|---|---| +| C1 | Yes | Lifecycle API with `commands.run` and `files.write_files`. Volumes, including Kubernetes PVCs, can be set at creation. The Go SDK's import path is still `github.com/alibaba/OpenSandbox/sdks/sandbox/go`. Hive would run and upgrade one more service, the Python lifecycle server. | +| C2 | Yes | The Credential Vault keeps real secrets in the sidecar. Hive would use none. | +| C3 | Yes | Unchanged. | +| C4 | Changes shape | The sidecar owns transparent interception in the pod and rejects a second interceptor in the same namespace. It can chain to an upstream proxy over HTTP/1 CONNECT, but only with its own transparent MITM enabled. Hive's proxy would then see a re-originated TLS session from the sidecar, not the agent. | +| C5 | Yes | Sandbox state, exec results and OpenTelemetry metrics. | +| C6 | **Only at the weakest isolation level** | On Kubernetes the egress sidecar holds the pod's `NET_ADMIN`. That is the same capability class the hive pod already needs, and it is not privileged. But with runc that buys FQDN egress policy and nothing more. The boundary is the same container boundary `sandbox.runtime: job` already has. gVisor is **rejected** with egress policy, because its netstack cannot take the nat redirect. Kata needs a cluster RuntimeClass and KVM on the nodes. Fast Sandbox's Fastlet container is **privileged with `/dev/kvm`** and needs bare metal or nested virtualization, plus XFS reflink storage. Its headline numbers (64 ms P50 create) come from that path. | + +**Licensing and cadence.** Apache-2.0, Linux Foundation, release images signed +with Cosign. A new line ships every two weeks. Only the latest line is +supported (no LTS): a line reaches end-of-life when the next one ships, apart +from a 72-hour emergency-CVE window. The project is post-1.0, but that pace is +an upgrade obligation on every spoke. + +### Proxy and attribution + +Both runtimes move the agent's outbound TCP to a component outside the hive +pod's network namespace. ADR-0002's two mechanisms rest on the hive pod: the +iptables redirect, and agent identification by socket owner UID. Neither +reaches a sandbox on these runtimes. + +For a sandboxed kick this matters less than it first appears. C2 already keeps +every GitHub token out of the sandbox, so nothing inside can authenticate a +GitHub write. The proxy's job for a sandboxed kick reduces to "unauthenticated +reads only". OpenShell can express that directly: a network policy that allows +`GET` on `api.github.com` and git fetch on `github.com`, and denies the rest. The +prover can check it in CI. The push broker's own traffic leaves from the hive +pod and stays under ADR-0002. + +Attribution moves from UID to sandbox: one OpenShell sandbox per kick, named +for the agent and kick. OpenShell's logs carry the sandbox name and the calling +executable. Hive's existing `audit: sandbox agent kicked` line already keys on +the agent. + +## Decision + +**Proposed:** adopt **OpenShell** as a third `sandbox.runtime` value +(`openshell`), behind the existing `sandbox.Launcher` interface. Use the +Kubernetes driver on hosted spokes and the Podman driver on standalone hosts. +The existing `podman` and `job` runtimes stay as they are, and the default +isolation mode does not change (out of scope for #14092). + +Reasons: + +1. **It meets C6 without privileged pods and still raises the boundary.** The + workload has no capabilities, and Landlock, seccomp mediation and a + network-less workload are confirmed before the agent starts. OpenSandbox + meets C6 only with runc, where it adds egress policy but no stronger + boundary than the current Job runtime. Its stronger modes need KVM, and + Fast Sandbox needs privileged pods. +2. **No new long-lived service per spoke beyond the gateway**, and the gateway + speaks gRPC with a Go SDK. OpenSandbox adds a Python lifecycle server plus a + per-pod sidecar image. +3. **Default-deny egress is the runtime's contract**, not a policy the operator + has to supply. That closes the "`restricted` depends on an operator-provided + network policy" gap listed in `sandbox-isolation.md`. + +**Do not adopt OpenSandbox now.** Revisit it for self-hosted spokes on +KVM-capable nodes, where Kata gives a VM boundary without privileged pods, if +the OpenShell prototype fails its gates. + +### Preconditions the implementation epic must verify first + +These are blocking. If any one fails, the recommendation falls back to "no +change". + +- The hosted OKE node pool's kernel passes OpenShell's Landlock ABI 3 and + seccomp qualification. Oracle UEK7 is 5.15 and fails the Landlock ABI 3 + floor; a 6.2+ kernel such as UEK8 is required. Check `uname -r` on the pool, + then run a real `sandbox create`. +- The hosted cluster's CNI **enforces** egress `NetworkPolicy`. Flannel does + not; Calico or Cilium do. Kubernetes accepts unenforced policies silently, so + test with a denied connection, not by reading the manifest. +- Someone with cluster-admin can install the Agent Sandbox controller and CRDs + once per cluster. Per-spoke isolation then maps to OpenShell workspaces in + `operator` namespace mode, one pre-provisioned namespace per spoke, so a spoke + still needs only namespace-scoped RBAC. + +### Prototype protocol + +Run one real kick for the same agent and task on hive's own spoke, under +`job`, `openshell` (Kubernetes driver), and OpenSandbox (runc + egress sidecar) +for comparison. Record these numbers and attach them to this ADR before it +moves to `Accepted`: + +| Measure | How | +|---|---| +| Cold start to agent's first command | Kick timestamp until the first transcript line, median of 5 | +| Warm start | Same, with images already on the node | +| Image and boot size | Pulled bytes for sandbox, supervisor and sidecar images | +| GitHub write refused | A `POST` to `api.github.com` from inside the sandbox fails with no token present | +| GitHub read allowed | `gh api repos/` read and `git fetch` succeed | +| Direct egress refused | A raw TCP connect to an arbitrary IP fails | +| Attribution | The refused write appears in runtime logs with the sandbox and kick name | +| Broker path intact | The kick's commits arrive as a PR through `pushbroker`, unchanged | + +## Consequences + +- Hive gains a sandbox boundary that is checked before the agent runs, without + privileged pods. The cost is three cluster prerequisites: the controller, an + enforcing CNI, and a 6.2+ kernel. Those turn into hub provisioning and + preflight work. +- For sandboxed kicks under `openshell`, ADR-0002's iptables and UID mechanism + is replaced by OpenShell network policy. ADR-0002 still governs the hive pod + and tmux agents. A change to the OpenShell policy file for hive is a policy + change and goes through the same maintainer-approval gate as proxy rules. +- OpenShell is pre-1.0 and ships often. Pin the gateway, supervisor and sandbox + images by digest, upgrade through Renovate, and run its prover in CI against + hive's policy. +- Telemetry must be disabled explicitly. A preflight check should refuse a + gateway that reports it enabled. +- If the prototype fails a gate, this record is rewritten to decide "no + change" with the measured reason, and the Podman and Job runtimes remain the + only ones. + +Refs #2804, #6311, #13806, #14092. diff --git a/docs/content/hive/adr/readme.md b/docs/content/hive/adr/readme.md index a20b147a..721683fd 100644 --- a/docs/content/hive/adr/readme.md +++ b/docs/content/hive/adr/readme.md @@ -81,4 +81,4 @@ sequencing; ADR-0019/0025 give their current action/mode contract first. - [ADR-0023: Hive may merge PRs whose author could merge them](/docs/hive/adr/0023-trusted-author-automerge) - [ADR-0024: Provider credential lifecycle](/docs/hive/adr/0024-provider-credential-lifecycle) — **Proposed**, not a credential-pool or refresh-service setup guide. - [ADR-0025: One stage engine derives and writes every `stage/*` label](/docs/hive/adr/0025-central-stage-engine) -- [ADR-0026: Sandbox runtime selection](https://github.com/hivecommons/hive/blob/v5/src/docs/adr/0026-sandbox-runtime-selection.md) (Proposed) +- [ADR-0026: Sandbox runtime selection](/docs/hive/adr/0026-sandbox-runtime-selection) (Proposed) diff --git a/docs/content/hive/dashboard-sections/activity-monitor.md b/docs/content/hive/dashboard-sections/activity-monitor.md index 18e2c813..060b94c9 100644 --- a/docs/content/hive/dashboard-sections/activity-monitor.md +++ b/docs/content/hive/dashboard-sections/activity-monitor.md @@ -8,10 +8,48 @@ Part of [Dashboard sections explained](/docs/hive/dashboard-sections); see that A progress strip per open issue and pull request showing which step of the pipeline it is on and who must act next. +Human requirements have visible **Needs human: …** text inside the corresponding +row, with the actual PR number, requester and next mover when known. This is +separate from its pipeline stage: a covered issue can still read **PR in +progress** while a specific linked PR asks a person to act. Multiple-PR trees +identify the asking PR rather than placing its siblings on hold; a PR drawn +under another issue names that host. A missing reason explicitly says **Human +decision required; reason unavailable** and links to the item. + +The person glyph is reserved for an exact-current requirement. Claim ownership +and ordinary author work instead have explicit **Claimed by** / **Next move** +context; **Let hive decide** can be an optional delegation action, not a status +indicator. These lines do not change stages, sorting, reference-versus-closing +links, preparation train styling, native queues, or live notes' tuck/reopen +controls. Cleared decisions and mismatching PR heads lose the requirement. +An operator's recorded approval is not a review verdict: current-head review +changes requested remain the visible blocker and keep the author/rework stage. +The tracker records the approval separately rather than claiming that the merge +queue will take the PR. Approval on an earlier head is historical, not permission +or current review proof for a new head. +When the PR's new head satisfies a recorded old-head author-push request, the +backend reconciles push/rework observations before building the row: its pill, +actor and caption return to pending CI/re-review instead of still asking for a +push. An independently evaluated author action on the new head remains visible. +If the head is known but its push time is not, no push age is invented. +When the current-head snapshot observes real checks running again without any +failing checks, an old CI-only push request becomes **checks pending · no new push +requested**. This is not a push or merge-readiness proof; independent review, +conflict and sign-off requirements still stand. +Adoption ownership policy is not a PR rejection: the chip distinguishes +**author-owned**, **maintainer-owned** and **contributor-managed** from an actual +**adoption blocked** capability. No adoption observation means no adoption chip. +Operator admission never promises that outstanding review/CI findings are clear. +Human availability is unknown: agent heartbeats do not prove that a person is +online or offline. Observed push/comment and waiting timestamps remain action +history, not presence evidence or an implied relinquishment of ownership. +Explicit adoption actions/provenance remain visible; a returning human does not +silently revoke an adoption. + ### First use: find the next action 1. Find an item by repository, number or title. Every open issue and PR in the watched repositories is represented, including held, draft, filtered, exempt and reporter-triage work. Being visible does not make it actionable. -2. Read the lit step and its words, then the reason on the item's `#N` link or lit chevron. The word says where the work is; the actor icon says whose move it is. +2. Read the lit step and the visible human/actor context beneath it. The word says where the work is; **Needs human** names the actual requirement, while **Next move** and **Claimed by** distinguish ordinary actor/claim ownership. Links and chevrons retain further detail. 3. Use **🙋 Needs human** for work waiting on a person's action. Use **⏸ On hold** separately for work a maintainer deliberately paused. Neither filter grants permission to act. 4. Open **⋯** (or right-click / Shift+F10) for the actions allowed in that state. Read the confirmation and the result toast; a queued decision is not proof the PR merged. @@ -44,6 +82,10 @@ At 090 the row explains why: `· workflow files`, `· tests removed`, `· manual ### Pairing, rows and counts +A verified merge from the canonical merge ledger is terminal whether Hive, a maintainer or another bot performed it. The last known root or linked PR model reaches **Merged** during the existing short terminal/pin window, even without an earlier queue observation. The comment names the actual recorded merger; an unknown actor stays unknown. This does not invent an earlier queue/admission state or close a merely related issue. A stale open refresh cannot undo a verified merge, and repeated delivery or later actor enrichment does not replay the merge or restart its window. Terminal visibility does not re-enable closed-PR actions. + +A failing external PR without a recorded Hive stage actor remains **CI failing · author-owned** when its PR author is known, **CI failing · owner recorded** for an observed claimant, or **CI failing · owner unknown** otherwise. Its work activity is unknown; missing internal worker evidence is not evidence that nobody is working. Inline and nested rows use the PR author or claimant, not the related issue's filer or holder. Explicit unclaimed Hive/bot routing and real CI failures remain unchanged. + - With no covering PR, an issue has a full strip lit at Open, Actionable or Claimed. - With exactly one covering PR, issue and PR share one continuous strip; early issue steps are passed and the PR supplies the current stage, chip, tooltip and age. - With several covering PRs, the issue's own strip stops at Claimed, with no current step lit. Indented PR sub-rows begin under *PR in progress*, each with its own stage and age, furthest along first. @@ -61,6 +103,8 @@ The header reports the filtered row count and how many need attention; paginatio ### How you'll know something is waiting on you +A verified trusted-author security notice without the live blocking sentinel label is informational, not the reason a human must act. Its tooltip says **Informational security notice (not a merge gate)**. An independent reviewer decision retains its quoted question with reviewer provenance. A login mentioned in that question is not an assignment or exclusive authority; neither that suggestion nor the PR author's identity supplies a decision owner. A real blocking sentinel label still names its security gate, even when a reviewer question also exists. + The hive often parks work on a person: a decision asked at **045** / **085**, a PR at **090** ready for a human merge, an approval the hive's GitHub App cannot give (`needs-human`, `unauthorized`), or a contributor who has gone offline past their grace so the operator is the fallback. Those waits used to look like any other row ("PR in progress"); nothing added them up and nothing said so. Three surfaces now read one server block, `/api/stage-tracker` → `waiting_on_human` (`count`, `items[{repo, number, type, kind, since, who}]`), so a human wait is never silent: - **Sidebar.** A second, orange badge on **Hive activity monitor** carries the waiting-on-you count (the grey badge stays the row count). Its tooltip lists the five longest waits with what each needs and how long it has waited. Hidden at zero. @@ -278,13 +322,13 @@ A copy that then fails (closed unmerged, or red at two heads) reopens the origin #### 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 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 contributor agent is registered here, "@zoe: no clanker registered — waiting for a human reply" with the pill reading `waiting on @zoe (human · no clanker)`. +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}` ([hivecommons/hive#15051](https://github.com/hivecommons/hive/issues/15051)). For a contributor agent, presence comes from the contributor hub's profile/relay heartbeat and uses the same adoption clock: a filled **dot** (`--cue-decision`, a soft breathe; none under `prefers-reduced-motion`) for an online clanker, a filled **square** (`--status-error`) for an offline clanker, and a **hollow ring** when that clanker is not registered here. These are agent states, never human availability. A human author carries only the person glyph and `unknown`: "Waiting on @zoe (human) — human availability is unknown; waiting for a human reply". No agent heartbeat, elapsed response time or GitHub activity becomes a human online/offline indicator or an implied ownership transfer. 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. -**An offline clanker's PR that has not been returned.** A clanker-authored PR the engine has not classified yet (the chevron reads `PR in progress · @odttlnt's clanker`) carries the same `presence` for its author, with `next: wait` and no `adopt_at` — the adoption clock runs only at 072. When the clanker is offline (or not registered here) the chevron's holder gains a muted suffix with the state square: `PR in progress · @odttlnt's clanker · ■ offline 3h`, and the caption states the next automatic step: "@odttlnt's clanker is offline 3h; a green PR is reviewed and merged without its author — hive adopts it only once it is returned (red CI, conflicts or review asks) and nothing is pushed for 15m" (`governor.labels.stage.adoption.after_offline`). A green PR does not need its author online: review and merge admission run on the contributor lane regardless; adoption is the exit for a PR that is red, conflicting or asked to change while its clanker is away, never for a green one, and never for a human's PR. +**An offline clanker's PR that has not been returned.** A clanker-authored PR the engine has not classified yet (the chevron reads `PR in progress · @odttlnt's clanker`) carries the same agent `presence`, with `next: wait` and no `adopt_at` — the automatic clock runs only at 072. When the clanker is offline (or not registered here) the chevron's holder gains a muted suffix with the state square: `PR in progress · @odttlnt's clanker · ■ offline 3h`, and the caption states the next automatic step: "@odttlnt's clanker is offline 3h; a green PR is reviewed and merged without its author — hive adopts it only once it is returned (red CI, conflicts or review asks) and nothing is pushed for 15m" (`governor.labels.stage.adoption.after_offline`). This automatic clanker projection does not infer a human's availability or an adoption request. Explicit adoption actions and provenance are separate; a returning human's activity does not silently undo an adoption. A contributor PR whose `stage/*` labels the write-back gate kept off GitHub (no opt-in label, see [What hive writes to GitHub, and when](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md#what-hive-writes-to-github-and-when)) is tracked from the engine's record alone: the record carries `labels_suppressed` and the tracker trusts its stage (085 reads *Needs human decision*, not *PR in progress*) even though the item's labels say nothing. Until this fix the tracker read the missing park label as a lifted park and dropped the record, so a dozen fork PRs parked at 085 for a human's decision sat under "PR in progress · @x's clanker" for hours while their clankers were offline. @@ -358,7 +402,7 @@ Every issue and PR row (never a column or repo-group header) carries a left gutt #### Did I act on this? (the decision badge) -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. +Every row action submitted from the dashboard — Approve, Give direction, Close as superseded, Close as won't-fix, Close issue, Let hive decide, Hold / Release hold, Accept triage, Mark needs-human — is written to a server-side ledger (`/data/operator-actions.json`, `operator_actions.go`; newest 20 per item) as `{action, actor, at, note, target, outcome, detail, head_sha, stage}`, so the record survives reloads and shows in every browser. The tracker row carries the newest one as `last_operator_action` (+ `operator_action_count`) and `GET /api/repos/{owner}/{repo}/items/{number}` returns the full list. The row then wears a small **history badge** in the cap cell, before the state pills (`.ham-acted`): the clock-with-counter-clockwise-arrow mark the right-most column header also carries (hover: "History — decisions and actions on this item"), with the count of recorded actions beside it — never a ✓, which read as "done/passed" (owner, 2026-10-28). @@ -376,7 +420,7 @@ Plain words throughout — working / waiting on X / set back by Y / merged — n #### The row's lead action, inline -A row whose next move is yours does not hide its answer behind three dots. Beside the chips on the right of the header (and on a nested PR sub-row's own header), the row renders its **lead action** as a real `