From 1bdfd31b196e0e2867a3bc7deb7fe64f50dc891b Mon Sep 17 00:00:00 2001 From: hivecommons-hive <280983584+hivecommons-hive@users.noreply.github.com> Date: Thu, 5 Nov 2026 06:29:16 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=93=96=20docs:=20resync=20the=20docs/?= =?UTF-8?q?content=20mirrors=20from=20upstream?= 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 | 48 ++- docs/content/dibs/readme.md | 1 + docs/content/hive/acmm-policy-matrix.md | 21 + docs/content/hive/crux-lanes.md | 399 ++++++++++++++++++ docs/content/hive/crux-serve.md | 12 +- .../dashboard-sections/activity-monitor.md | 32 +- .../dashboard-sections/workflow-and-agents.md | 4 +- docs/content/hive/documentation-map.md | 4 +- docs/content/hive/env-vars.md | 2 +- .../content/hive/integrations/clanker-flue.md | 2 +- docs/content/hive/integrations/cline.md | 230 ++++++++++ docs/content/hive/integrations/forges.md | 93 ++++ .../integrations/work-source-providers.md | 2 +- .../glossary-and-operations.md | 2 +- .../stage-labels.md | 20 +- docs/content/hive/landscape.md | 2 +- docs/content/hive/running-at-level-6.md | 2 +- docs/content/hive/work-sources.md | 2 +- docs/content/pluk/readme.md | 23 +- scripts/sync-hive-docs.files.json | 6 + 20 files changed, 855 insertions(+), 52 deletions(-) create mode 100644 docs/content/hive/crux-lanes.md create mode 100644 docs/content/hive/integrations/cline.md create mode 100644 docs/content/hive/integrations/forges.md diff --git a/docs/content/crux/readme.md b/docs/content/crux/readme.md index 836c65fb..8358a344 100644 --- a/docs/content/crux/readme.md +++ b/docs/content/crux/readme.md @@ -49,7 +49,7 @@ brew install hivecommons/hive/crux go install github.com/hivecommons/crux/cmd/crux@latest # Container image (amd64 and arm64; tmux is already inside) -docker run --rm ghcr.io/hivecommons/crux:0.2.0 --help +docker run --rm ghcr.io/hivecommons/crux:0.3.0 --help ``` Or download a release archive from the @@ -62,13 +62,13 @@ flags below are required: cosign refuses to run without them, and a wildcard would accept a signature from anyone. To check: ```sh -# from the v0.2.0 release page, download these three files first +# from the v0.3.0 release page, download these three files first cosign verify-blob checksums.txt \ --bundle checksums.txt.sigstore.json \ --certificate-identity-regexp '^https://github\.com/hivecommons/crux/\.github/workflows/release\.yml@' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com sha256sum -c checksums.txt --ignore-missing -tar xzf crux_0.2.0_linux_amd64.tar.gz +tar xzf crux_0.3.0_linux_amd64.tar.gz ./crux --version ``` @@ -79,15 +79,15 @@ the selected archive against its signed checksum before extracting it: ```sh # Apple Silicon example; use darwin_amd64 in both places on Intel -grep 'crux_0.2.0_darwin_arm64.tar.gz$' checksums.txt | shasum -a 256 -c - -tar xzf crux_0.2.0_darwin_arm64.tar.gz +grep 'crux_0.3.0_darwin_arm64.tar.gz$' checksums.txt | shasum -a 256 -c - +tar xzf crux_0.3.0_darwin_arm64.tar.gz ./crux --version ``` The container image is signed the same way: ```sh -cosign verify ghcr.io/hivecommons/crux:0.2.0 \ +cosign verify ghcr.io/hivecommons/crux:0.3.0 \ --certificate-identity-regexp '^https://github\.com/hivecommons/crux/\.github/workflows/release\.yml@' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com ``` @@ -193,18 +193,20 @@ lists the controls, which ones belong to Crux, and what is still missing. | `crux logs ` | Show recent output. `-n N`, `--buffer`, `--full`, `--json`; `-f` follows when using `--server`. | | `crux kick ` | Send work to an agent. `-` reads the message from stdin. | | `crux stop ` | Stop an agent. | -| `crux serve` | Serve the runtime over HTTP+JSON. `--socket`, `--listen`, `--token`, `--hive-callback`. | +| `crux serve` | Serve the runtime over HTTP+JSON. `--socket`, `--listen`, `--token`, `--hive-callback`, `--lanes-config`. | | `crux agy-turn` | Internal. The runtime types this into a pane itself; do not run it by hand. | +| `crux cline-turn` | Internal. One bounded Cline CLI turn run by the runtime for `backend: cline`; do not run it by hand. | | `crux --version` | Print the version. | Global flags: `--workdir`, `--defs` (same as `--config`), `--socket`, `--local`, `--server`, `--token`. Environment variables that stand in for them: `CRUX_DEFS`, `CRUX_SERVER`, `CRUX_TOKEN`. (`CRUX_SOCKET` is not used for -the global `--socket`; it sets the listening socket for `crux serve --socket`.) +the global `--socket`; it sets the listening socket for `crux serve --socket`. +`CRUX_LANES_CONFIG` is the default for `crux serve --lanes-config`.) ## Status -Current release: v0.2.0. This is early software. +Current release: v0.3.0. This is early software. What works today: the `crux` CLI (`run`, `ps`, `logs`, `kick`, `stop`), `crux serve` with its HTTP+JSON API and Go client, remote mode with @@ -361,6 +363,26 @@ other route. [`api/client`](https://github.com/hivecommons/crux/blob/main/api/cl `client.Runtime` is a full `crux.Runtime` over the API, and is what `runtime.New` returns when `CRUX_MODE=sidecar`. +#### Opt-in lane routes + +The `/v1/lanes` routes are only present when `crux serve` is started with +`--lanes-config` (or `$CRUX_LANES_CONFIG` set). They live in a separate +project-authenticated auth domain; neither the unix socket nor the legacy +admin token grants lane access — each request must carry a project +credential in the Bearer token. The full contract, including delivery group +semantics, receipt fencing, and replay, is at +https://github.com/hivecommons/hive/blob/v5/src/docs/crux-lanes.md. + +| Route | Verb | What it does | +|---|---|---| +| `/v1/lanes` | GET | Registry of available lanes. | +| `/v1/lanes/{lane}/publish` | POST | Publish a message to a lane. | +| `/v1/lanes/{lane}/events` | GET | Replay messages from a lane. | +| `/v1/lanes/{lane}/groups/{group}/claim` | POST | Claim delivery of a group. | +| `/v1/lanes/{lane}/groups/{group}` | GET | State of a delivery group. | +| `/v1/lanes/{lane}/groups/{group}/recover` | POST | Recover a group. | +| `/v1/lanes/{lane}/groups/{group}/{ack\|nack\|renew}` | POST | Ack, nack, or renew a receipt. | + ### 3. The hive callback, what crux asks the host for This layer runs the other way. Crux never holds a long-lived GitHub key, a @@ -396,9 +418,9 @@ a refusal. ### Packages shipped with it - `agent`: the manager. tmux sessions, launch, restarts, kicks. It knows - nine engines by name in `agent/manager_launch.go`: claude, copilot, - gemini, agy, pi, goose, bob, codex and omp. `config.CLIBackends` lists - fifteen; the other six (aider, opencode, kilo, muse, openhands, + ten engines by name in `agent/manager_launch.go`: claude, copilot, + gemini, agy, pi, goose, bob, codex, omp and cline. `config.CLIBackends` lists + sixteen; the other six (aider, opencode, kilo, muse, openhands, omnigent) launch as a bare binary. - `proxy`: the GitHub and inference proxy. Policy enforcement at the tool boundary: every agent write to GitHub and every model call goes through @@ -419,6 +441,8 @@ a refusal. - `logscrub`: a log handler that masks tokens and other secrets. - `pushbroker`: the trusted step that pushes a sandboxed agent's work with credentials the sandbox never sees. +- `lanes`: opt-in JetStream durable event/work lanes with project ACLs, + replay, claimable delivery groups and fenced receipts. - `gitidentity`: the git identity each agent commits as. - `agentmode`: the ladder of GitHub permission tiers, one per trust level. - `agentparse`: parsers for the tables and lists agents print. diff --git a/docs/content/dibs/readme.md b/docs/content/dibs/readme.md index 80b4e443..98c25019 100644 --- a/docs/content/dibs/readme.md +++ b/docs/content/dibs/readme.md @@ -119,6 +119,7 @@ deployments also work. | `DIBS_ADMINS` | (unset) | Comma-separated, case-insensitive GitHub logins granted admin access (idea-moderation endpoints) | | `DIBS_CLANKER_MARKERS` | `hive: agent=,hivecommons-hive[bot]` | Comma-separated, case-insensitive markers matched against PR branch/labels/body/author to classify agent activity | | `DIBS_INTAKE_MAX_MB` | `25` | Max upload size (MB) accepted for idea-intake file uploads | +| `DIBS_INTAKE_EXTRACT_WORKER` | _(on)_ | Set to `off` to extract uploaded documents in-process instead of in a memory-limited, time-limited child worker (not recommended: crafted documents can then exhaust the server's memory) | | `DIBS_STT_URL` | (unset) | Speech-to-text gateway URL; when set, enables voice/audio idea submission | | `DIBS_STT_KEY` | (unset) | API key for the STT gateway | | `DIBS_STT_MODEL` | (unset) | Model name routed by the STT gateway | diff --git a/docs/content/hive/acmm-policy-matrix.md b/docs/content/hive/acmm-policy-matrix.md index 2892e235..78b1ed59 100644 --- a/docs/content/hive/acmm-policy-matrix.md +++ b/docs/content/hive/acmm-policy-matrix.md @@ -18,6 +18,27 @@ issues. The **KB** is the knowledge base of reusable facts; an **RFC** is a request for comments on a proposed design. For a first deployment's trust choices, start with [Securing your hive](/docs/hive/securing-your-hive). +## Repository E2E detection + +The codebase evaluator's `acmm:prereq-e2e` prerequisite accepts the existing +Playwright/Cypress configuration and E2E directory conventions, as well as +GitHub Actions workflow files named `test*-install*.yml`, `test*-boot*.yml`, +or `*e2e*.yml` (also `.yaml`). These workflow alternatives cover projects +whose end-to-end boundary is installing and booting a system rather than +driving a web browser. + +Workflow patterns use Go `path.Match` semantics on the file basename, only +inside the literal `.github/workflows` directory. They do not recurse, match +directories named like workflow files, or accept an unrelated CI workflow +just because it exists. Detection remains a file-existence heuristic, not +proof that a workflow passes or that its contents implement an E2E test; +maintainers must keep these paths backed by real coverage rather than add +empty directories or dummy workflows to satisfy the criterion. + +An evaluator fix takes effect only after a release containing it is deployed. +After deployment, refresh the ACMM evaluation (subject to its refresh debounce) +before closing an existing false-negative gap issue. + ## Policy Modes The four policy tiers below describe the main autonomy ladder. The roster also diff --git a/docs/content/hive/crux-lanes.md b/docs/content/hive/crux-lanes.md new file mode 100644 index 00000000..64b8ffc9 --- /dev/null +++ b/docs/content/hive/crux-lanes.md @@ -0,0 +1,399 @@ +> **Synced from Hive.** This page is pulled from [hivecommons/hive@v5](https://github.com/hivecommons/hive/blob/v5/src/docs/crux-lanes.md) during the docs build. Edit the canonical source in the Hive repository. + +# Crux durable lanes + +Crux owns the lane contract. Projects are peer publishers/subscribers; Hive is +one optional participant, not the broker authority. No repository, issue, pull +request, forge label or forge credential is required. Lane IDs, project IDs, +message IDs, group IDs and schema IDs are plain `^[a-z][a-z0-9_-]*$` identifiers, +at most 64 characters. These lanes are unrelated to trajectory safety lanes. + +## Enable in the existing Crux service + +Use `crux serve --lanes-config ./lanes.json`, or set `CRUX_LANES_CONFIG`. +This adds routes to the **existing** HTTP/JSON Unix-socket/loopback-TCP server; +it does not start another daemon. Without this option, existing behavior is +unchanged. `/v1/events` remains the lossy, in-memory legacy observer SSE stream; +it is **not** the durable lane log. + +Broker initialization failures distinguish typed `state conflict` and +`broker unavailable` categories; other failures remain explicit initialization +errors. These CLI diagnostics never print raw backend errors, broker addresses +or credentials. + +Example administrator-owned `lanes.json` (no secrets): + +```json +{ + "url": "tls://broker.example.net:4222", + "credentials_env": "CRUX_NATS_CREDENTIALS_FILE", + "namespace": "commons", + "principals": [ + {"project": "atlas", "token_env": "ATLAS_LANE_TOKEN"}, + {"project": "beacon", "token_env": "BEACON_LANE_TOKEN"} + ], + "lanes": [{ + "id": "measurements", + "owner": "atlas", + "publishers": ["atlas", "beacon"], + "subscribers": ["atlas", "beacon"], + "groups": {"summarizers": ["beacon"]}, + "recoverers": {"summarizers": ["beacon"]} + }] +} +``` + +Generate distinct project bearer tokens of 32–4,096 bytes in a secret +manager; inject them through the named environment variables. +`CRUX_NATS_CREDENTIALS_FILE` contains the path to a protected NATS credentials +file, not its contents. The existing `CRUX_TOKEN` remains an **agent-control** +credential. It must differ from every project credential. + +The configured project token determines the principal, never an envelope, +`X-Project` header, URI or forge identity. Project tokens cannot call existing +agent-control RPCs; the agent-control token and local socket identity do not +grant lane access. Membership is explicit per capability: ownership alone does +not grant publish, event subscription or work claims. Group membership grants +work consumption, not event subscription. No credential is returned in registry +responses. Provenance is optional, untrusted absolute URI metadata; inline URI +credentials are rejected and it must never contain secrets. + +## Public HTTP and Go API + +All lane routes require a configured project bearer token: + +| Route | Capability / result | +|---|---| +| `GET /v1/lanes` | Registry filtered to the principal's memberships | +| `POST /v1/lanes/{lane}/publish` | Publisher; `LanePublished` | +| `GET /v1/lanes/{lane}/events?cursor=0&limit=32` | Event subscriber; `LaneReplay` | +| `POST /v1/lanes/{lane}/groups/{group}/claim` | Group member; `LaneClaim`, or 204 when empty | +| `GET /v1/lanes/{lane}/groups/{group}` | Group member; revision, reusable slot allocation, retained bounds, missing identities, own receipts/retries and recovery history | +| `POST .../groups/{group}/recover` | Explicitly configured group recoverer; revision-fenced expired-gap acceptance | +| `POST .../groups/{group}/renew` | Current lease holder; renewed expiry | +| `POST .../groups/{group}/ack` | Current lease holder; 204 | +| `POST .../groups/{group}/nack` | Current lease holder; 204 and immediate eligibility for redelivery | + +Publish request: + +```json +{"id":"reading-one","kind":"event","schema":"temperature-v1","payload":"eyJjZWxzaXVzIjoyMX0="} +``` + +`payload` is a bounded byte array encoded as base64 by JSON; here it contains +`{"celsius":21}`. Use `kind:"work"` for claimable work. `provenance` is optional. +The durable envelope adds authenticated `project`, `lane` and `created_at`. +`id` is the idempotency key within `(lane, project)`. A different kind, schema, +payload or provenance with the same retained key fails with `lane_conflict`. + +Ack/nack/renew requests contain only the returned sequence and receipt: + +```json +{"sequence":3,"receipt":"<64 hexadecimal characters returned by claim>"} +``` + +`crux/api.Client` exposes `LaneRegistry`, `PublishLane`, `ReplayLane`, +`ClaimLane`, `RenewLane`, `AckLane`, `NackLane`, `InspectLaneGroup` and +`RecoverLaneGroup`. It uses the existing Crux +address parsing, transport and typed `StatusError`. `ClaimLane` returns +`nil, nil` for 204, not a zero-valued work message. + +Forge-free peer example: + +```go +atlas, err := api.NewClient(address, os.Getenv("ATLAS_LANE_TOKEN")) +if err != nil { return err } +beacon, err := api.NewClient(address, os.Getenv("BEACON_LANE_TOKEN")) +if err != nil { return err } +_, err = atlas.PublishLane(ctx, "measurements", api.LanePublish{ + ID: "summarize-one", Kind: api.LaneWork, + Schema: "summary-v1", Payload: []byte(`{"values":[21,22]}`), +}) +if err != nil { return err } +work, err := beacon.ClaimLane(ctx, "measurements", "summarizers") +if err != nil { return err } +if work == nil { return nil } +// Perform a registered, authorized operation; make its side effects idempotent. +return beacon.AckLane(ctx, "measurements", "summarizers", api.LaneReceipt{ + Sequence: work.Sequence, Receipt: work.Receipt, +}) +``` + +## Durability, cursors and concurrency + +Every accepted publish has a JetStream acknowledgement. Immutable per-lane +file-backed streams retain both events and work; each subscriber stores its +own event cursor. Replay is a bounded pull page, never a dropping channel. +The cursor is the last **examined** stream sequence, including work records +skipped by event replay. Pages examine at most 512 records and return at most +128 events. Save the returned cursor only after processing the events. +Cursor zero explicitly starts at the earliest retained record; a future cursor +or a nonzero cursor with an expired gap fails with `lane_cursor`. Missing +retained messages during a scan also fail rather than silently skipping. + +Work groups have independent persistent cursor/lease ledgers in JetStream KV. +KV compare-and-set is the ownership linearization point, not process memory. +Competing clients in a group claim a work record once per live lease; other +groups receive their own delivery. A lost response or service crash leaves a +lease that expires and redelivers. A random receipt fences each assignment and +binds lane, group, authenticated holder, sequence and ownership generation. +Stale or cross-project/group ack, nack and renew fail, including after +reassignment. No broker ACK subject is exposed to a participant. + +Stream publication and KV ownership are not claimed to be one transaction. +Publication first reserves a key/payload hash in KV, then publishes with a +generation-specific JetStream message ID and a **subject expected-sequence CAS**. After a crash, +retry verifies the persisted subject before completing the publish. This +prevents concurrent mismatched payloads and repeated retained publications, +including outside the broker's finite duplicate window. A reservation has a +random persisted generation; a resumed request reuses it. The broker ACK +sequence (including **normal** and **duplicate** ACKs) is read back and checked +against the exact envelope, subject and generation, and the active KV +reservation is rechecked before success. A message-ID duplicate is never +trusted as proof of this request's payload: NATS deduplicates before subject +CAS. Generation-specific IDs prevent an expired request from inheriting the +ACK of a different payload published under a reused key. + +Reservation and stream writes are separate broker operations, not an atomic +cross-resource transaction. A request fenced by expiry/replacement can fail +even if its own message reached durable storage; unavailable or expired +metadata also fails explicitly. Retry/reconcile rather than treating any +error as proof that nothing was stored. Successful responses always resolve +to matching persisted content. Reservations are +bounded by retention; IDs can be reused once both reservation and log record +have expired. **There is no infinite idempotence guarantee.** + +Acknowledgements are idempotent for the configured acknowledgement-retention +period. Completed receipts occupy bounded ledger slots during that period; +once pruned, an old acknowledgement fails as stale. Unacked work retries until +successful ack or log retention expiry; no automatic dead-letter discard is +performed. Expired pending work fails explicitly until authorized gap recovery. +Receipts are bearer secrets scoped to their project/group: do not log them. + +### Explicit same-namespace group recovery + +An operator configures `recoverers` per group; each recoverer must also be a +group member. Group membership, lane ownership, an agent-control token, Hive +identity and a local socket do **not** implicitly grant this capability. + +Read `InspectLaneGroup`, then submit: + +```json +{ + "id": "accept-expired-gap-one", + "revision": 42, + "cursor_through": 19, + "expired": [{"sequence": 12, "receipt": ""}], + "missing": [13] +} +``` + +The revision must match the entire group state. Concurrent claims, renewal, +ACK or another recovery cause a CAS conflict; refresh the inspection and use +a new request ID if changing the request. `cursor_through` must be exactly the +expired prefix boundary (`first_retained - 1`) when there is a cursor gap, or +the current cursor when there is no gap. It cannot advance across retained +unclaimed work. Each explicitly retired delivery must be missing from retention, +lease-expired and unacknowledged. `expired` additionally requires the caller's +exact original receipt. `missing` lists inspected sequences without needing +another project's secret receipt: the separately configured recovery capability +authorizes expiration of absent owners' deliveries in this group. Neither form +can retire live, retained, completed or foreign-group work. At most 16 combined +deliveries can be accepted per request; inspect and submit successive batches +with refreshed revisions. Duplicate sequences are invalid. + +The result durably records the accepted cursor gap, observed `missing_through` +prefix and expired delivery retirements without marking them acknowledged. +Valid pending leases and ACK history remain intact. The result's `expirations` +records the original project, sequence and attempt of every retired delivery; +its `project` identifies the authenticated recoverer. This is an explicit +expiration audit, **not** a successful ACK. Retired entries release their +ledger slots immediately, including when the owner has disconnected forever. +Recovery requires neither an unrelated unread gap nor an own-project receipt. +Original receipts can no longer ACK, NACK or renew these deliveries. +The missing-prefix checkpoint does not silently suppress unlisted pending +deliveries; those require their own explicit, fenced acceptance. + +The exact request ID/body and result are persisted together in the same group +CAS update. Retrying a lost response, including after service/broker restart, +returns that result without reapplying recovery. A changed body or another +principal using the ID conflicts. Detailed history and recovery idempotence are +bounded by `ack_retention`, at most 64 recovery records per group; overflow is +explicit backpressure. The accepted missing-prefix checkpoint and cursor are +persistent fixed-size state. No new namespace or daemon is required. + +Delivery is **at least once**, not exactly-once side effects or execution. +An operation may finish and crash before ack, or continue after losing its +lease; applications must honor cancellation and use downstream idempotence or +fencing for effects. Renew before expiry. No global FIFO or ordering of work +completion is promised. Stream sequence orders accepted records per lane; +eligible retry selection rotates by a persistent sequence cursor, while a +separate persisted turn alternates retries and fresh work when both exist. +Rotation wraps among ready deliveries, so an always-ready low-sequence poison +item cannot starve already-claimed higher-sequence work after response loss. +When retries exist, the next retry turn is committed before attempting fresh +value growth: even a failed shared-bucket admission cannot pin the group +forever to unsuccessful fresh attempts. +NACKs schedule durable +exponential retry delay capped by `max_retry_delay`; duplicate NACKs do not move +the deadline. Polling slower than the retry delay cannot let one rejected item +starve fresh work. Released/expired retries do not consume live-lease slots, +but remain in the bounded ledger and are never silently ACKed or discarded. + +## Bounds and failure behavior + +Duration fields accept either a string parsed by Go `time.ParseDuration` +(for example `"30s"` or `"5m"`) or an integer number of nanoseconds; omitted or +zero values select defaults. Out-of-range settings fail validation with an +error naming the field and its allowed range, for example +`lanes: max_pending must be at least 1, got -1`. Negative/invalid explicit values fail initialization. +Short retention remains supported even when an operation timeout is longer: +expiry produces explicit generation/cursor errors, not a false duplicate ACK. +For normal deployments choose retention comfortably above polling, retry and +operation-deadline requirements. + +| Setting | Default / accepted bounds | +|---|---| +| `retention` | 7 days; 1 second–30 days | +| `dedup` | 2 minutes; 100 milliseconds–retention | +| `lease` | 1 minute; 1 second–1 hour | +| `ack_retention` | 1 hour; lease–retention | +| `retry_delay` | 1 second; 100 milliseconds–`max_retry_delay` | +| `max_retry_delay` | 1 minute; retry delay–1 hour | +| `timeout` | 5 seconds; positive–1 minute | +| `max_payload` | 64 KiB; positive–1 MiB | +| `max_messages` | 10,000 per lane; 1–1,000,000 | +| `max_bytes` | 64 MiB per lane; payload size–1 GiB | +| `max_kv_bytes` | 64 MiB per bucket; positive–1 GiB | +| `max_pending` | 128 live leases per group; positive and at most ledger size | +| `max_ledger` | 1,024 per group; pending size–10,000, additionally limited by worst-case reservations within 512 KiB | +| `replicas` | 1; 1–5 subject to broker topology | + +Registry configuration permits at most 64 lanes, 64 principals, 64 groups per +lane and 64 members per capability/group. The serialized registry is bounded +to 512 KiB and its input file to 1 MiB. Provenance is at most 2,048 bytes. +Streams use `DiscardNew`: exceeding capacity rejects new publications; it +does not evict accepted records before age retention. All request operations +have deadlines. Reconnect buffering is disabled: disconnection returns an +error, never a success-shaped queued publication. Reconnection restores +access to persisted state; retry with the same idempotency key. + +HTTP errors distinguish invalid requests (400), missing credentials (401), +denied capabilities (403), state/cursor/stale-receipt conflicts (409), group +capacity (429), cancellation (408), unavailable broker (503), and deadlines (504). +The existing JSON `code` values have Go constants in `crux/api`: +`CodeLaneInvalid`, `CodeLaneForbidden`, `CodeLaneConflict`, `CodeLaneCursor`, +`CodeLaneStale`, `CodeLaneCapacity`, `CodeLaneCanceled`, `CodeLaneUnavailable` +and `CodeLaneTimeout`. Missing credentials still use `CodeUnauthorized`. +Broker-side capacity/storage failures are unavailable errors, not accepted +publications. A successful duplicate is returned only after verifying a +durably persisted matching record. + +Claim admission reserves the serialized maximum of all fields of every +delivery (including maximum-width attempts, IDs and UTC nanosecond timestamps) +plus all 64 possible recovery responses of 16 deliveries each, gap metadata +and request hashes. Space padding allocates this reservation in the actual +namespace KV value. ACK, NACK and renew do not grow its reserved size; recovery +uses already-reserved history and removes expired delivery entries. Near the +512 KiB byte limit, new claims fail before consuming completion headroom. +The effective delivery count can therefore be below `max_ledger`, even with +short actual IDs. History is pruned only after its documented retention window. +An exhausted recovery-history window applies finite backpressure until expiry. + +Each persisted group has a durable, nonshrinking slot high-water mark. +An initial empty poll reserves **at least one reusable delivery slot**, not +just cursor/history bytes; its later first claim therefore needs no value +growth. Pruning completed records and retiring missing foreign deliveries +free entries **within** those slots without reducing their physical reservation. +Previously allocated slots remain reusable after service/broker restart and +response loss. Inspection reports `slots` (zero for an uninitialized group). +Slot allocation cannot exceed `max_ledger` or the 512 KiB worst-case bound. +Additional slots require successful broker admission before a new lease is +accepted; failed expansion does not remove existing reusable slots. + +The same physical reservation prevents newly admitted groups from consuming +existing groups' value-growth headroom in the shared bucket. Padding also +compensates byte-for-byte for the pinned driver's decimal CAS-header growth. +Bucket exhaustion +rejects admission explicitly. Broker metadata, account limits, replication, +network and disk availability can still fail any operation; reservations are +not a guarantee of infinite broker capacity or availability. The internal +state-format generation 3 rejects incompatible older prototype writers. + +Stream names use a bounded digest of the namespace/lane tuple, not ambiguous +underscore concatenation. Subject, KV and group-key boundaries use validated +identifiers and disjoint fixed delimiters/suffixes. + +The registry and delivery bounds form an immutable namespace generation. +Startup rejects incompatible existing streams, KV buckets or registry; it +does not rewrite another service's permissions. Membership/admin mutations +are **not supported over the project API** (405/404); explicit group recovery +is a separate configured capability, and ownership is not a +remote administrator grant. Operators provision reviewed generations through +configuration, coordinating migration/replay before retiring the old one. + +## Hive adapter + +Set `HIVE_LANES_ADDRESS`, `HIVE_LANES_TOKEN`, `HIVE_LANES_LANE` and +`HIVE_LANES_GROUP` to opt Hive into an ordinary Crux project membership. +`HIVE_LANES_SNAPSHOT_PROJECTS` is an optional comma-separated explicit publisher +allowlist for `hive-governor-snapshot-request-v1` work. Without it no snapshot +handler is registered. Requests must contain `{}`; they can only read governor +state and publish a correlated `hive-governor-snapshot-v1` event, not invoke +tools, prompts, forge writes or merges. At-least-once retries may emit multiple +correlated snapshots if state changes between attempts. + +Committed governor mode changes are additionally published with schema +`hive-governor-mode-change-v1`; existing local hooks remain attached. A broker +failure logs that durable delivery of the transition was **not confirmed**; this optional +adapter does not pretend the governor state commit and remote publish are an +atomic outbox. The public `pkg/laneparticipant` adapter accepts only explicit +schema-and-publisher handler registrations and bounds handlers by their lease. +Unhandled/unauthorized/failed work is nacked, not executed or acknowledged. +Hive's existing trust and forge-write policies remain unchanged; any future +write handler must independently apply them rather than treating lane +membership as write authority. + +## Deployment and verification + +No NATS deployment is installed by this feature. Provision JetStream with +durable storage and backups yourself; use TLS with certificate validation, +NATS account authentication and subject/API ACLs, plus a TLS proxy for remote +Crux HTTP access (the existing listener allows only loopback plain HTTP). +Only Crux service credentials may access this namespace's streams/KV/API. +Participants use Crux HTTP credentials, not direct broker credentials; giving +them direct KV access would bypass fencing and registry policy. +Protect credentials and private Unix sockets with OS ownership/modes. +Configure NATS `max_payload` above the base64-expanded envelope size and +512 KiB ledger ceiling, and provision sufficient account stream/byte limits. +Use synchronized service clocks for lease/expiry decisions; KV CAS remains +the assignment fence. Multi-replica deployments require matching configuration +and supported JetStream topology; single-replica defaults are not an HA claim. + +The named CI integration step tests real JetStream lanes, API and serving in +the standalone Crux module and the Hive participant through the root module. +CI's existing two-module affected-package test matrix also discovers the new +`crux/lanes` and `pkg/laneparticipant` packages, with the normal 90% coverage +gate, and builds both the Hive host and standalone Crux module. Tests start +real bounded embedded JetStream servers in isolated project-relative fixtures +and stop/remove them. They exercise forge-free two-project HTTP exchange, +fan-out, duplicate/mismatched publish, independent/competing groups, +ack/renew/nack, expiry and stale fencing, persisted restart and pending-publish +recovery, broker outage/reconnect, retention/cursors, bounds, cancellation, +auth isolation and the Hive adapter. Independent child Crux processes exercise +delayed publication across reservation expiry and durable retry/fresh fairness. +Recovery regressions include lost responses/restart, concurrent claim/ACK CAS, +live lease and ACK-history preservation, absent foreign-owner expiration with +sole-slot/no-gap recovery, accepted missing prefixes and API +capability isolation. Byte-bound regressions admit a state within one delivery +of 512 KiB, exercise ACK/NACK/renew and retry, recover retained-history gaps, +and resume later work without pruning unexpired ACK history. Maximum-shape +and full-namespace tests cover history, timestamps and CAS-header headroom. +Reusable-slot tests poll five empty groups at a 1 MiB bucket cap before any +publication, then claim through each group; they also exercise pruning and +foreign retirement at that same replacement-growth boundary, with durable +high-water reuse, broker restart and lost-response fencing. +Existing API/SSE tests remain in their normal CI selection. For AI agents, +repository policy prohibits local Go test/build/vet; their executable verdict +comes from CI. Human contributors should follow the +[CONTRIBUTING local development guide](https://github.com/hivecommons/hive/blob/v5/CONTRIBUTING.md#local-development). diff --git a/docs/content/hive/crux-serve.md b/docs/content/hive/crux-serve.md index 1ce26d73..aa0bf8f6 100644 --- a/docs/content/hive/crux-serve.md +++ b/docs/content/hive/crux-serve.md @@ -9,7 +9,7 @@ Stage 3 process boundary of the Crux design record [#13536](https://github.com/hivecommons/hive/issues/13536), stage 2/3 tracker [#14138](https://github.com/hivecommons/hive/issues/14138)): a harness other than hive drives agents through it, and the `crux` CLI has a remote mode that -talks to it. Hive's optional [durable lane participant](https://github.com/hivecommons/hive/blob/v5/src/docs/crux-lanes.md) uses +talks to it. Hive's optional [durable lane participant](/docs/hive/crux-lanes) uses separate project-authenticated lane RPCs on this server; that does not change its agent-control hosting mode or grant lane tokens agent-control authority. @@ -35,7 +35,7 @@ runtime and every agent must arrive as a definition on `POST /v1/agents`. | `--socket PATH`, `CRUX_SOCKET` | unix socket to serve on. Default `$XDG_RUNTIME_DIR/crux/crux.sock`, else `/crux-/crux.sock`. The directory is created `0700`, the socket `0600`, and it is never reachable at that path with wider permissions (it is bound under a temporary name, chmod'ed, then renamed). A socket somebody already answers on is refused; a stale one is replaced. | | `--listen HOST:PORT` | also serve on TCP. **Refused without a token.** | | `--token T`, `CRUX_TOKEN` | bearer token every request must carry. When set it gates the socket too. | -| `--lanes-config PATH`, `CRUX_LANES_CONFIG` | opt into [durable event/work lanes](https://github.com/hivecommons/hive/blob/v5/src/docs/crux-lanes.md) on this same server, backed by NATS JetStream. Lane project credentials are separate from agent-control credentials. | +| `--lanes-config PATH`, `CRUX_LANES_CONFIG` | opt into [durable event/work lanes](/docs/hive/crux-lanes) on this same server, backed by NATS JetStream. Lane project credentials are separate from agent-control credentials. | | `--hive-callback ADDR`, `CRUX_HIVE_SOCKET` (+ `CRUX_HIVE_TOKEN`) | dial hive's callback endpoint (`crux/api/callback`) for the capabilities hive owns — scoped GitHub tokens, PR creation, Linear credentials, text/canary scanners, conditions — and feed the runtime's seams from it instead of leaving them nil. Health reports it under `hive_callback`; an unreachable endpoint raises the `hive_callback_unavailable` condition. (#14246) When the runtime is a manager it also polls hive's host policy from the same endpoint; see [Host policy from hive](#host-policy-from-hive-14138-stage-4). | `serve` does not use the per-work-dir control socket that a foreground @@ -62,8 +62,8 @@ runtime and every agent must arrive as a definition on `POST /v1/agents`. All routes are under `/v1`. Bodies and responses are JSON; agent-control errors are `{"error": "...", "code": "unauthorized|bad_request|not_found|conflict|runtime_error"}`. Opt-in lane routes require a separate project bearer credential and capability -ACLs; see the [lane routes and request types](https://github.com/hivecommons/hive/blob/v5/src/docs/crux-lanes.md#public-http-and-go-api) -and [lane error codes and failure behavior](https://github.com/hivecommons/hive/blob/v5/src/docs/crux-lanes.md#bounds-and-failure-behavior). +ACLs; see the [lane routes and request types](/docs/hive/crux-lanes#public-http-and-go-api) +and [lane error codes and failure behavior](/docs/hive/crux-lanes#bounds-and-failure-behavior). | Route | Body | Runtime method | Returns | |---|---|---|---| @@ -76,7 +76,7 @@ and [lane error codes and failure behavior](https://github.com/hivecommons/hive/ | `POST /v1/agents/{name}/kick` | `{"message":…}` | `Kicker.SendKick` | `{"agent","process"}` | | `POST /v1/agents/{name}/stop` | | `Lifecycle.Stop` | `{"agent","process"}` | | `GET /v1/events` | | watcher + `Kicker.SetKickObserver` + the `crux.Host` observers | `text/event-stream` of `Event` records | -| `GET /v1/lanes` and lane/group subroutes | Publish/recovery/receipt JSON; [request shapes](https://github.com/hivecommons/hive/blob/v5/src/docs/crux-lanes.md#public-http-and-go-api) | `lanes.Broker` | Opt-in only; lane-project credential and capability ACLs; JSON or `204`; [full routes and responses](https://github.com/hivecommons/hive/blob/v5/src/docs/crux-lanes.md#public-http-and-go-api) | +| `GET /v1/lanes` and lane/group subroutes | Publish/recovery/receipt JSON; [request shapes](/docs/hive/crux-lanes#public-http-and-go-api) | `lanes.Broker` | Opt-in only; lane-project credential and capability ACLs; JSON or `204`; [full routes and responses](/docs/hive/crux-lanes#public-http-and-go-api) | ### Runtime families (#14138 stage 3) @@ -301,7 +301,7 @@ trust whoever can reach the socket or holds the token, and hive still constructs `*agent.Manager` in-process. Those are the rest of Stage 3 in [design/crux.md](https://github.com/hivecommons/hive/blob/v5/src/docs/design/crux.md#after-the-cut). -The opt-in [durable lanes](https://github.com/hivecommons/hive/blob/v5/src/docs/crux-lanes.md) are a separate project-authenticated +The opt-in [durable lanes](/docs/hive/crux-lanes) are a separate project-authenticated surface with explicit capability ACLs, configured per-lane retention/storage bounds and per-group lease/ledger limits. Hive uses them as an ordinary lane client. This does not provide general multi-tenant isolation, change Hive's diff --git a/docs/content/hive/dashboard-sections/activity-monitor.md b/docs/content/hive/dashboard-sections/activity-monitor.md index 908f53b9..485ab831 100644 --- a/docs/content/hive/dashboard-sections/activity-monitor.md +++ b/docs/content/hive/dashboard-sections/activity-monitor.md @@ -84,7 +84,7 @@ At 090 the row explains why: `· workflow files`, `· tests removed`, `· manual 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. +A failing external PR without a recorded Hive stage actor remains **Fix owed · author-owned** when its PR author is known, **Fix owed · owner recorded** for an observed claimant, or **Fix owed · owner unknown** otherwise (the red verdict itself is the CI chevron's word, never this lane's). 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. @@ -262,7 +262,7 @@ nested PR spacing. An unrelated row or child ends the rail rather than acquiring misleading membership. A missing, closed or stale front is never synthesized as a verified owner; recorded/unverified metadata remains explicit. -When several approved PRs to the same base touch the same files, the automerge lanes merge them one at a time ([#15117](https://github.com/hivecommons/hive/issues/15117)). The tracker renders every train contiguously, in merge order, with a thin left rail linking the rows and a header chip `🚆 merge train · 4 PRs · 1st: #15074` — the glyph is an inline-SVG railcar (14px, currentColor, hover *Merge queue*), the same icon the **Merge queue** column header and its chevrons wear; overlapping files are listed beside it. Pill wording per state is the PR's place in the queue (the owner's words — `front` and `queued-behind` appear nowhere a person reads): the head reads "Merge queued · 1st of 4 · ~13m" (its own soak left plus the next pass); the others "Merge queued · 2nd of 4 · ~4m", "Merge queued · 3rd of 4", … (`1st 2nd 3rd 4th … 11th 12th 13th 21st`, always with the queue length, countdown to its turn); when the page draws more than one train each queue word ends with its train's tag — "Merge queued · 1st of 2 · train main.go" (the first overlapping file's basename, else the head `#N`) — and the hover leads *Train of 2 PRs overlapping main.go; merges after #15567.*, so two "1st"s on one page tell apart (one train → no tag); the header and every member row of a train share one rail accent (`.ham-train-k0…k3`, cycling in page order); once the front merged and the hive is rebasing a conflicting sibling, "Rebasing · attempt 1/2"; when the loop guard (`auto_merge.merge_train.max_rebase_attempts`) is spent, "Needs human decision · rebase failed twice" at stage/085 with the decision option "Rebase, then merge as maintainer". +When several approved PRs to the same base touch the same files, the automerge lanes merge them one at a time ([#15117](https://github.com/hivecommons/hive/issues/15117)). The tracker renders every train contiguously, in merge order, with a thin left rail linking the rows and a header chip `🚆 merge train · 4 PRs · 1st: #15074` — the glyph is an inline-SVG railcar (14px, currentColor, hover *Merge queue*), the same icon the **Merge queue** column header and its chevrons wear; overlapping files are listed beside it. Pill wording per state is the PR's place in the queue (the owner's words — `front` and `queued-behind` appear nowhere a person reads): the head reads "Queued for merge · 1st of 4 · ~13m" (its own soak left plus the next pass); the others "Queued for merge · 2nd of 4 · ~4m", "Queued for merge · 3rd of 4", … (`1st 2nd 3rd 4th … 11th 12th 13th 21st`, always with the queue length, countdown to its turn); when the page draws more than one train each queue word ends with its train's tag — "Queued for merge · 1st of 2 · train main.go" (the first overlapping file's basename, else the head `#N`) — and the hover leads *Train of 2 PRs overlapping main.go; merges after #15567.*, so two "1st"s on one page tell apart (one train → no tag); the header and every member row of a train share one rail accent (`.ham-train-k0…k3`, cycling in page order); once the front merged and the hive is rebasing a conflicting sibling, "Rebasing · attempt 1/2"; when the loop guard (`auto_merge.merge_train.max_rebase_attempts`) is spent, "Needs human decision · rebase failed twice" at stage/085 with the decision option "Rebase, then merge as maintainer". The tooltip lists the PRs ahead and the overlapping files. Waiting siblings are never red, never `needs human`; a `needs-rebase` label on one is reported under `stale_labels`, the train repairs it. Each item carries `merge_gate.state` (`queued-behind` / `rebasing` / `rebase-exhausted` / `front`, with an honest `reason` such as `behind #15074 in merge train (3rd of 4, overlapping files: stage_tracker.go, index.html)`) and `merge_train{id, state, front, position, size, attempt, max_attempts, overlap_files, ahead, members}`; the id is `#` and `members` is the whole roster in merge order. @@ -274,7 +274,7 @@ The tooltip lists the PRs ahead and the overlapping files. Waiting siblings are **The train starts at 080.** The merge train is a merge-queue concept: only PRs at 080 and beyond — green, approved, soaking or merging — ride one, and the header counts only them. A PR the planner has ordered while it is still at CI / Review (050 / 060) rides no train (`stageTrackerBelowMergeQueue`): hive's own page once led `merge train · 2 PRs · 1st: #15637 — overlapping: sidebar.html, index.html` over #15637 and #15633 while both chevrons read *CI / Review*. Such a row carries `overlap: {behind, with, files, note}` instead of `merge_train` — no rail, no ordinal, no header — and shows one muted badge **overlaps #15637** whose hover is the note, *Not in the merge queue yet: overlaps #15637 (sidebar.html, index.html) — will queue behind it* (the planner's own front reads *overlaps #15633 (…) — merges ahead of it once both are ready*). The members that do ride close ranks around it, and a train left with one 080 member is no train. -**Queue position, countdown, stall** ([#15395](https://github.com/hivecommons/hive/issues/15395)). The queued chip is its place in the queue and nothing else — **Merge queued · 2nd of 6 · ~4m** — so it reads the same whether or not a merge sweep is evaluating it: the sweep is the spinning ⬡ glyph and a *Checking now…* line in the hover, never a change of words (one label per chevron), and no state slug (`queued-behind`, `front`) ever reaches the page. The hover is the place sentence — *2nd of 6 in the merge queue · behind #15253 (soaking, 12m left) · ETA ~25m (2 ahead · head soaking 12m · next pass in 40s). Hive merges one at a time, in order.*; the head's reads *1st of 6 in the merge queue · merges next · ETA ~13m.* — or, when the head is on a person or on CI, the blocker instead of a clock: *… behind #15253 (waiting for a maintainer (untrusted author)) · no estimate until #15253 moves.* Every train state — the 1st included — lights the **Merge queue** column, whatever numeric stage the lane holds the head at (the train's facts are mirrored onto a head still at 050/060; hive's own #15400 once lit CI / Review beside its 2nd-place sibling) — a head under repair is no member (below) and lights CI / Review. The server adds to each member's `merge_train`: `length`, `head_number`, `head_state` (`needs-human` / `ci` / `soaking` / `ready` / `merging` / `fixing` / `unknown`), `head_reason` (plain words), `head_since`, `eta_s`, `eta_at`, `eta_basis`, `late`, `stalled`, `stalled_s`. +**Queue position, countdown, stall** ([#15395](https://github.com/hivecommons/hive/issues/15395)). The queued chip is its place in the queue and nothing else — **Queued for merge · 2nd of 6 · ~4m** — so it reads the same whether or not a merge sweep is evaluating it: the sweep is the spinning ⬡ glyph and a *Checking now…* line in the hover, never a change of words (one label per chevron), and no state slug (`queued-behind`, `front`) ever reaches the page. The hover is the place sentence — *2nd of 6 in the merge queue · behind #15253 (soaking, 12m left) · ETA ~25m (2 ahead · head soaking 12m · next pass in 40s). Hive merges one at a time, in order.*; the head's reads *1st of 6 in the merge queue · merges next · ETA ~13m.* — or, when the head is on a person or on CI, the blocker instead of a clock: *… behind #15253 (waiting for a maintainer (untrusted author)) · no estimate until #15253 moves.* Every train state — the 1st included — lights the **Merge queue** column, whatever numeric stage the lane holds the head at (the train's facts are mirrored onto a head still at 050/060; hive's own #15400 once lit CI / Review beside its 2nd-place sibling) — a head under repair is no member (below) and lights CI / Review. The server adds to each member's `merge_train`: `length`, `head_number`, `head_state` (`needs-human` / `ci` / `soaking` / `ready` / `merging` / `fixing` / `unknown`), `head_reason` (plain words), `head_since`, `eta_s`, `eta_at`, `eta_basis`, `late`, `stalled`, `stalled_s`. **One PR per issue** ([#15395](https://github.com/hivecommons/hive/issues/15395)). Two open PRs declaring the same `Fixes #N` never both ride the queue (hive's own #15402 and #15407, both for #15396, once sat side by side as "waiting its turn"). Before the trains are grouped the sweep keeps one PR per closing issue — a green head (not conflicting, not blocked on a red required check) beats a red one; among equals the earlier-opened wins — and holds the other out of every train with the stage reason `merge-train:duplicate-of#:/0:0/0:`. The held row's gate reads `duplicate-of:` (`merge_gate.duplicate_of`, `merge_gate.duplicate_issue`), its chip **Duplicate of #15402 · needs a pick** with no clock, it carries no `merge_train`, and it wears `needs-decision` so both PRs sit under their issue with the hold visible. *Let hive decide* on either row takes the existing supersede path (rule `one-pr-per-issue`): on the held PR it closes it as superseded by the kept one with a credit comment, through the same write gate as every delegate close; on the kept PR it releases it and names the duplicate the queue holds (or, if the kept PR has since gone red, only recommends the fix — the queue re-picks on its next pass, so an earlier red PR yields to a later green one). @@ -331,7 +331,7 @@ An adopted pull request names the hive's superseding copy in its detour word (`a 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 superseded original keeps a muted, chevron-less sub-row under the issue while its superseder is open and listed (hivecommons/hive#15511 showed hive's #15574 *Merge queued* and, beneath it, the still-open #15526 as a greyed line whose note "· Adopted → #15574 · superseded (hive re-authored with sign-off)" wrapped to two lines). The note now reads **"superseded · hive adopted → #M"**, or **"superseded · closed · hive adopted → #M"** once the original is closed, and never wraps — the PR title ellipsizes first; the hover carries the long form ("Hive re-authored this PR as #M with the same commits and sign-off. This PR closes as superseded when #M merges." / "…This PR is closed as superseded."). The tracker sets `superseded_by: M` on the original (omitted for an in-place adoption, cleared when the copy is closed / closed-twice / copy-failed so the original counts again) and `adopted_from.state` / `lineage.from_state` on the copy (`"open"` or `"closed"`). A closed original is listed in this muted form only while #M is open and listed; it leaves the tracker once #M merges or closes. Agent-authored originals close in the adopting pass by policy (`governor.labels.stage.adoption.close_superseded`, default `on_adopt`); a person's PR closes then only with the write-back opt-in label, else when the copy merges — see [hive-merge.md](https://github.com/hivecommons/hive/blob/v5/src/docs/hive-merge.md). +A superseded original keeps a muted, chevron-less sub-row under the issue while its superseder is open and listed (hivecommons/hive#15511 showed hive's #15574 *Queued for merge* and, beneath it, the still-open #15526 as a greyed line whose note "· Adopted → #15574 · superseded (hive re-authored with sign-off)" wrapped to two lines). The note now reads **"superseded · hive adopted → #M"**, or **"superseded · closed · hive adopted → #M"** once the original is closed, and never wraps — the PR title ellipsizes first; the hover carries the long form ("Hive re-authored this PR as #M with the same commits and sign-off. This PR closes as superseded when #M merges." / "…This PR is closed as superseded."). The tracker sets `superseded_by: M` on the original (omitted for an in-place adoption, cleared when the copy is closed / closed-twice / copy-failed so the original counts again) and `adopted_from.state` / `lineage.from_state` on the copy (`"open"` or `"closed"`). A closed original is listed in this muted form only while #M is open and listed; it leaves the tracker once #M merges or closes. Agent-authored originals close in the adopting pass by policy (`governor.labels.stage.adoption.close_superseded`, default `on_adopt`); a person's PR closes then only with the write-back opt-in label, else when the copy merges — see [hive-merge.md](https://github.com/hivecommons/hive/blob/v5/src/docs/hive-merge.md). 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". @@ -433,6 +433,16 @@ The *state pill* says where the item is and what it is waiting on (`hold`, `need Plain words throughout — working / waiting on X / set back by Y / merged — nothing else blinks. +#### Chevron activation — the canonical rules + +An *active* chevron is filled amber and carries words; an inactive one is its icon alone (CI / Review: the ring, green when the last run on this head passed, red when it failed, neutral when not yet run), with the counts in its tooltip. On every strip: + +1. **1 ≤ active ≤ 2.** At least one chevron is active — the column where the item is or where it is stuck. +2. **CI and Review resolve first.** A track is active only while it genuinely runs on the current head (a check-run in progress or queued, a ring not yet full; a reviewer running or queued). If either runs, the PR is waiting on it: the stage chevron (PR in progress, Merge queue …) renders settled-current — filled but still, icon only, its words in the tooltip — and nothing else is active. +3. **CI + Review is the only pair.** Two chevrons are active together only when both tracks run at once; every other combination is exactly one active chevron. The stage chevron is the ≥ 1 fallback: active only when no track is in motion. +4. **The PR in progress chevron speaks the PR's lifecycle** — draft, pushed, rebasing, *Unassigned · next: scanner*, *Waiting for @author · fix owed* — never a CI or review verdict. *CI failing*, *CI 4/11 · 1 failing* and *changes requested* are the CI and Review chevrons' words; the PR in progress tooltip keeps the red reason as detail. +5. **The CI and Review tooltips state what the track waits on — never "activity unknown".** With no verdict and no reviewer in flight the spoke derives the Review statement from what it already knows, in this order: the PR is merged or closed (*Review · n/a (closed)*), it is a draft (*Review · draft, not requested*), CI on the head has not finished (*Review · waiting for CI*), CI is done and nobody started (*Review · not started · hive reviewer next*, or *· needs reviewer* when the repo has no hive reviewer lane). A settled verdict explains itself — *Changes requested by the hive reviewer: 1 perspective posted findings … · 12m ago · quality ✓ · security ✗: token logged* — and stays known as its observation ages. The one no-data wording, *Review · no data for head f269349 yet · refreshing* (CI: *CI · no checks observed for head … yet · refreshing*), appears only when the spoke has fetched nothing for the head, and names the next step. + #### 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 `