check-issue-refs: blanket-ban tracker tags in markdown too, no exceptions

This commit is contained in:
damocles 2026-09-09 21:02:48 +02:00
commit e1e913015d
24 changed files with 81 additions and 89 deletions

View file

@ -92,7 +92,8 @@ declarations.
- **What does a specific Rust crate do, on its own terms?**
[`crates/`](crates/README.md) — every workspace crate's own
`README.md`, one level up from source (#3051); the crate
`README.md`, one level up from source
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3051`); the crate
itself is still the source of truth, this is just a walkable mirror.
## Process & conventions

View file

@ -4,8 +4,8 @@ Every agent has a place in an operator-editable parent/child tree, used
to scope which agents can manage which others. This doc covers how
hive-c0re stores and edits the tree today, the rules that are meant to run on top
of it once enforcement is finished, and where the manager still gets
special-cased in the meantime. Tracking issue: #361
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/361`).
special-cased in the meantime. Tracking issue:
`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/361`.
## Where the tree lives

View file

@ -69,7 +69,8 @@ request.
and no approval. (it's currently mounted read-write, which is a
defect tracked separately, not an authoring path.)
Branch protection (push/merge allowlist = `core`, approvals allowlist
= operator team; see "Forge mirror" and #1787) makes the agent a
= operator team; see "Forge mirror" and
`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/1787`) makes the agent a
write collaborator that **can't merge its own config PR**.
2. hive-c0re's `/webhook/config-pr` endpoint receives the Forgejo
`pull_request` event (opened / synchronized / reopened) and queues a
@ -476,7 +477,8 @@ repo — so it can push a branch and open a config PR — but not a member
of any other agent's, so it can't reach another agent's config through
the forge. Branch protection keeps `main` push/merge `core`-only with
operator-team approval, so an agent can't fast-forward its own config or
self-merge its PR (see the End-to-end flow + #1787). hive-c0re passes the tokenised push
self-merge its PR (see the End-to-end flow +
`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/1787`). hive-c0re passes the tokenised push
URL inline to `git push`, never writing it into
`applied/<n>/.git/config`; that repo is RO-bind-mounted into the root
agent, and a stored token would leak core's admin credential to an

View file

@ -437,7 +437,7 @@ would only be transient.
On a btrfs host, `lifecycle::ensure_agent_state_subvolume` creates a brand-new agent's state root as a
**btrfs subvolume** instead of a plain directory (progressive
enhancement — see the #1762 lane). This is a no-op fallback on
enhancement — see `$HIVE_FORGE_URL/hyperhive/hyperhive/issues/1762`). This is a no-op fallback on
non-btrfs hosts and for any agent whose root already exists, so
nothing is automigrated: existing agents keep their plain dirs
until an explicit opt-in upgrade.

View file

@ -3,7 +3,8 @@
One page per Rust workspace crate — its own `README.md`, unchanged, served
here for browsing alongside the rest of the docs site. **The crate's own
`README.md` is the source of truth; nothing here is hand-maintained.**
`nix/packages/reference-docs.nix` (#3051) generates every page at
`nix/packages/reference-docs.nix`
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3051`) generates every page at
build time straight from the crate's real `README.md`, so it can never
drift out of sync the way a hand-copied mirror would — edit the crate's
own README to change what shows up here.

View file

@ -95,7 +95,8 @@ store's container reads it, writes the `swarm-controller` policy, enables the
cert auth method, and creates the `swarm-controller` role that attaches the two.
⚠️ **This has only run once, and it didn't go all the way through.** The
first real provision against a live store returned 403 (#4124), so
first real provision against a live store returned 403
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/4124`), so
treat the block above as derived-then-partly-tested rather than proven: the
grants still come from `-output-policy`, not from a swarm that came up on them.
What that 403 does _not_ tell you is whether the login or the write was
@ -128,7 +129,8 @@ can't hand you, being what opens it. ⚠️ Not the gateway's HTTPS certificates
store that took its identity from an authority it will itself distribute could
never come up before that authority.
Making even the `init` unnecessary is tracked in issue #3768.
Making even the `init` unnecessary is tracked in
`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3768`.
### 4 · Swarm SSO (only when `deploy.authelia`)

View file

@ -215,7 +215,7 @@ pipeline steps in order:
`CommonMark` ATX heading (1-6 leading `#`s followed by a space,
tab, or end-of-line), prepend `\` so the embedded body doesn't
blow into a top-level h1/h2 inside the wrapper message when the
dashboard renders it. Lines like `#tag`, `#123`, `#!/bin/bash`
dashboard renders it. Lines like `#tag`, `#123`, `#!/bin/bash` <!-- lint:allow: syntax examples, not real tags -->
**aren't** headings — no escape, no cosmetic noise. Indented
"headings" inside lists / nested quotes keep their leading
whitespace.

View file

@ -64,7 +64,8 @@ hive-c0re owns the host admin socket.
is the wake-event-injection surface. Recipient is implicit — the
agent the socket belongs to — and `from` is caller-chosen so the
wake prompt can label the source verbatim (`"matrix: new message in
#general"`, `"forge: PR #42 opened"`, etc.). Typical caller: an
#general"`, `"forge: PR #42 opened"`, etc.). <!-- lint:allow: example strings, not real tags -->
Typical caller: an
in-container background task (the matrix daemon, a scraper, the
forge-notify webhook subscriber) that needs to signal "external work
has arrived" without going through the broker as a peer agent.

View file

@ -412,7 +412,7 @@ connects to the compositor at `127.0.0.1:<vnc_port>`.
Wayland clients couldn't find the compositor — libwayland falls
back to a headless display or errors out, the app "works" on a
second invisible display, and the VNC session shows a blank weston
desktop (#540 double-screen).
desktop (`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/540`, double-screen).
- **VNC bind address**: Weston's `vnc-backend` has no CLI
bind-address flag (unlike the `rdp-backend`'s `--address`), so the
listener binds `0.0.0.0`. The harness relay only connects via

View file

@ -112,7 +112,7 @@ summary change signals the harness turn loop directly (in-process, no broker
round-trip), so the harness drives a turn for the agent to handle it via
`get_loose_ends`, then clears the todo with `cancel_loose_end(kind: "todo", id: N)` (dials the
in-container socket directly — no bash task involved, so clearing doesn't
spawn another todo; see #2639). Same mechanism the matrix daemon uses for
spawn another todo; see `$HIVE_FORGE_URL/hyperhive/hyperhive/issues/2639`). Same mechanism the matrix daemon uses for
unread rooms. An inline `wait_seconds` / `status` observation that already
delivered the result instead clears the keyed todo, so no redundant
loose-end follows.

View file

@ -66,12 +66,12 @@ hive-forge comment-edit 18042 --body "..." # edit a comment
hive-forge assign 42 damocles
hive-forge close 42
hive-forge labels 42 add feature
hive-forge issue dependency 42 # list #42's dependencies (issues it's blocked by)
hive-forge issue dependency 42 add 40 41 # #42 is now blocked by #40 and #41
hive-forge issue dependency 42 remove 40 # drop the #40 dependency link
hive-forge issue dependency 42 # list issue 42's dependencies (issues it's blocked by)
hive-forge issue dependency 42 add 40 41 # issue 42 is now blocked by 40 and 41
hive-forge issue dependency 42 remove 40 # drop the dependency on 40
hive-forge pr dependency 55 add 42 # same verb, PR-scoped (Forgejo shares the issue/PR index)
hive-forge issue reaction 42 # list #42's reactions
hive-forge issue reaction 42 add heart # react to #42 with :heart:
hive-forge issue reaction 42 # list issue 42's reactions
hive-forge issue reaction 42 add heart # react to issue 42 with :heart:
hive-forge issue reaction 42 --comment 18042 add +1 # react to a specific comment instead
hive-forge issue reaction 42 remove heart # remove your own :heart: reaction
hive-forge issue reaction 1 --list-allowed # this instance's actual configured shortcodes
@ -202,7 +202,7 @@ verdict alone.
```
hive-forge pr-status --pr 42
# PR #42: feat(...): ...
# PR #42: feat(...): ... # lint:allow: sample CLI output, not a real tag
# state: open (mergeable: yes)
# CI: e39a87ea3949: ✅ success (1 context(s))
# ✅ CI / nix flake check (pull_request): success — Successful in 1m50s

View file

@ -63,7 +63,7 @@ What still holds: the grant is **read-only** (a hive can't write an agent's
credential, so it can't hand itself an agent's identity), and it's scoped to
the agent-credential prefix — nothing else in the store is reachable with it.
**#4137** tracks narrowing it, with the two candidate directions: scope
**`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/4137`** tracks narrowing it, with the two candidate directions: scope
the grant per hive (and pay for the re-emission), or give each agent container
its own store identity so credentials never pass through a hive at all.

View file

@ -133,8 +133,9 @@ the reactive path only fires once claude-code has already refused a whole
turn as too long. Neither can see context growth happening _inside_ a
single long turn's own tool-calling before it returns — a turn whose tool
calls alone blow past the window hits the reactive path with no preceding
checkpoint, losing whatever wasn't already persisted (#2707, root-caused in
#3727).
checkpoint, losing whatever wasn't already persisted
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/2707`, root-caused in
`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3727`).
**Safety net for that gap: claude-code's own native autocompact is on**,
via the managed settings (`"autoCompactEnabled": true`,

View file

@ -222,7 +222,7 @@ operator-driven `hivectl choom` session — which passes neither `--tools`
nor `--allowedTools` — gets claude's built-in synchronous `Bash` tool
(inline, human-approved). That sidesteps the async `mcp__bash__run`
completion wake landing in the wrong session (the harness inbox) for a
choom-started task (#2356); `choom` is an operator (root) action, so
choom-started task (`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/2356`); `choom` is an operator (root) action, so
built-in shell there stays within the existing trust boundary. The bash MCP server
(`run` / `status` / `kill`) uses `allowedTools = ["*"]` so all
`mcp__bash__*` tools are always available regardless of tool groups.

View file

@ -85,7 +85,7 @@ structurally rather than for one specific trigger. Two columns:
`agent_links()` in hive-agent is the single source of truth for
both. No separate overflow (`⋯`) menu exists any more — it used
to hold exactly this dashboard link plus a rebuild-container action
(mara, #3704: "remove rebuild button, move link to
(mara, `$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3704`: "remove rebuild button, move link to
dashboards into links menu") — rebuild had no real discoverability
need of its own (the dashboard's own R3BU1LD button already covers
it) so it's gone outright, and the dashboard link moved here,

View file

@ -67,7 +67,7 @@ default palette). Default hexes shown are Catppuccin Mocha.
| `--blue` | `base0D` | `#89b4fa` | links, interactive accent (distinct from cyan) |
| `--purple` | `base0E` | `#cba6f7` | accent — active tabs, links, highlights |
| `--pink` | `base0F` | `#f5c2e7` | thinking events |
| `--crust` | `mix(base00 58%, #000)` | `#11111b` | terminal / code block background (below `--bg`) |
| `--crust` | `mix(base00 58%, #000000)` | `#11111b` | terminal / code block background (below `--bg`) |
| `--muted` | `mix(base05 55.5%, base00)` | `#7f849c` | secondary / dimmed text |
| `--subtext0` | `mix(base05 77.7%, base00)` | `#a6adc8` | toolbar/status text; dimmer than `--fg`, lighter than `--muted` |

View file

@ -146,7 +146,7 @@ part-way through a spawn is byte-identical on disk to a tombstone, and
the pane lists both. The pane carries a standing warning to that effect, and
the row badge says `offline` rather than `destroyed` — the absence of a
container is all hive-c0re can actually prove. Fixing it properly
needs a recorded destroy: **#3020**, deferred to the swarm-controller /
needs a recorded destroy: **`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3020`**, deferred to the swarm-controller /
snapshot-storage rework where the problem changes shape.
*Stale permission entries*: agents with explicit capability or
@ -236,7 +236,7 @@ than reconstructing chrome hive-c0re no longer sends. Settled entries
render their **full step tree**, not just a bare summary — the wire
doesn't filter `Done` nodes out.
**State filter (#2606).** A row of per-state checkboxes above
**State filter (`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/2606`).** A row of per-state checkboxes above
the tree — one per lifecycle state, matching the row glyphs — lets the
operator narrow which root groups render; unchecking a state re-fetches
`GET /api/jobq/graph?states=<checked, comma-joined>` rather than
@ -811,7 +811,7 @@ below).
### Status badges
**Which single badge (#3139):** `ContainerView.failed`
**Which single badge (`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3139`):** `ContainerView.failed`
(systemd `ActiveState=failed` — the unit exhausted its bounded
restarts and gave up on its own) draws a red `✖ gave up` badge;
otherwise a plain muted `■ not running` — a container an operator

View file

@ -244,7 +244,8 @@ render, `restoreOpenDetails()` re-applies after. Long-content
drill-ins (file previews, diffs, journald logs) now open in the
**side panel** (see below) rather than expanding inline. The
dashboard's own answered-questions history list was the only
`<details data-restore-key>` consumer and is gone (#3721)
`<details data-restore-key>` consumer and is gone
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3721`)
`snapshotOpenDetails()` / `restoreOpenDetails()` currently have
nothing to restore, kept in place for the next collapsible section
that needs it rather than ripped out along with their one caller.

View file

@ -28,6 +28,6 @@ additions are layered on top of the default agent dist via the
output is reproducible without network access at build time.
- **esbuild**: vanilla-JS bundler, no framework runtime overhead.
Each workspace's `build.mjs` is ~30 lines.
- **Single-PR migration**: see issue #273 for the design proposal and
- **Single-PR migration**: see `$HIVE_FORGE_URL/hyperhive/hyperhive/issues/273` for the design proposal and
the four-commit shape (npm scaffold → nix derivations → container
plumbing → Rust cutover).

View file

@ -55,7 +55,8 @@ sets a per-call cwd, so with `services.hyperhive.c0re.buildSlots > 1`,
two concurrent calls (any mix of `create`/`update`) could share that one
`.tmp`: one's `readlink(".tmp")` resolving to the _other's_ build
output, handing an agent's container the wrong agent's closure — the
"agent container gets closure of other agent" bug (#3312).
"agent container gets closure of other agent" bug
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3312`).
Building the toplevel here and passing the resolved store path via
`--system-path` for _every_ call means `buildFlake()` never runs at all,

View file

@ -1,72 +1,52 @@
#!/bin/sh
# CI lint, two passes over tracked files. Emits a CI error annotation per
# hit, exits 1 if either pass finds something. Its own required CI job
# CI lint: flags tracker tags (a hash followed by an issue number) anywhere
# in tracked text, source or docs. The hive convention is prose, not
# tracker tags — in code because tags rot (they point at moving targets and
# leak tracker coupling into the source tree); in markdown because the
# forge's public mirror carries no issue/PR data at all, so *any* `#N`
# form — bare, qualified `owner/repo#N`, or an ambiguous glued `owner#N` —
# is equally dead weight for a public reader. No exemption for markdown:
# that used to exist ("prose docs may cite the tracker with a bare `#N`")
# and got dropped once that read as still allowing exactly the kind of
# reference the public-mirror problem rules out.
#
# Emits a CI error annotation per hit and exits 1 if any tag is found, 0
# otherwise. It runs as its own CI job and IS a required check on the forge
# (branch protection) — a hit blocks merge.
#
# 1. Source comments (`.rs .nix .js .ts .tsx .css .html`): flags ANY tracker
# tag (hash + issue number) outright — the hive convention is prose, not
# tracker tags, in code (/knowledge/hive-rules.md); tags rot. Markdown is
# exempt here (prose docs may cite the tracker with a bare `#N`). Pattern:
# a hash, 2-5 digits, then non-alphanumeric-or-EOL — skips letter-bearing
# hex colours and digit-runs-then-letter (`#24h`). Residual: a pure-
# numeric short hex trips it — write the six-digit form to dodge.
# Scope: every tracked `*.rs *.nix *.js *.ts *.tsx *.css *.html *.md`. The
# pattern matches a hash, 2-5 digits, then a non-alphanumeric char or
# end-of-line. A real tracker tag is never glued to a letter, so the
# trailing class skips both letter-bearing / 6-8-digit hex colours (the
# digit run breaks or overruns) and digit-runs followed by a letter — e.g.
# hash-route fragments like #24h. Residual: a pure-numeric short hex (e.g.
# three identical digits) trips it — write the six-digit form to dodge.
#
# 2. Markdown (`.md`): narrower — only an AMBIGUOUS cross-repo-shaped ref, a
# word glued directly to `#N` with no `/` (`hyperhive#1234`). A bare
# `#1234` or a qualified `owner/repo#1234` stay legal; only the glued,
# unqualified `owner#N` — indistinguishable from a real cross-repo
# citation without checking whether `owner` is a live org — gets caught
# (the forge's public mirror carries no issue/PR data, so this is dead
# weight for a public reader regardless of which repo it meant). The
# glued prefix must start lowercase (real org/repo slugs are, see
# `hive_types::Ident`) — rules out `PR#4124`/`Hyperhive#1234`. Residual:
# an ordinary lowercase word glued to a number (`line#123`) still
# matches — no live-allowlist option in a dependency-free script; rare in
# practice, same escape hatch as pass 1.
#
# Escape hatch (both passes): a line with the marker `lint:allow` is exempt.
# Reserve it for a genuine non-tag hit and keep a short reason next to it —
# not for a real tracker tag or ambiguous ref, rewrite those instead.
# Escape hatch: a line containing the marker `lint:allow` is exempt.
# Reserve it for genuine `#<digits>` that aren't tracker tags — e.g. a
# `#123` markdown-heading example or hash-prefixed test-input data — and
# keep a short reason next to the marker. Don't use it to keep a real
# tracker tag; rewrite those to prose (or a full issue URL) instead.
set -eu
src_pattern='#[0-9]{2,5}([^0-9a-zA-Z]|$)'
md_pattern='(^|[^0-9A-Za-z._/-])[a-z][a-z0-9-]*#[0-9]{2,5}([^0-9a-zA-Z]|$)'
pattern='#[0-9]{2,5}([^0-9a-zA-Z]|$)'
# `/dev/null` forces grep to always print a filename prefix, even when
# xargs hands it a single file. `-r`/`-0` keep it robust to odd paths and
# an empty file list. Lines carrying the `lint:allow` marker are dropped
# (legitimate non-tracker hit; see the header).
src_hits="$(
git ls-files -z '*.rs' '*.nix' '*.js' '*.ts' '*.tsx' '*.css' '*.html' \
| xargs -0 -r grep -nE "$src_pattern" /dev/null 2>/dev/null \
| grep -v 'lint:allow' || true
)"
md_hits="$(
git ls-files -z '*.md' \
| xargs -0 -r grep -nE "$md_pattern" /dev/null 2>/dev/null \
# (legitimate non-tracker `#<digits>`; see the header).
hits="$(
git ls-files -z '*.rs' '*.nix' '*.js' '*.ts' '*.tsx' '*.css' '*.html' '*.md' \
| xargs -0 -r grep -nE "$pattern" /dev/null 2>/dev/null \
| grep -v 'lint:allow' || true
)"
count=0
if [ -n "$src_hits" ]; then
echo "$src_hits" | while IFS=: read -r file lineno _; do
printf '::error file=%s,line=%s::tracker tag in source — write prose, not a hash-number tag (see /knowledge/hive-rules.md)\n' "$file" "$lineno"
if [ -n "$hits" ]; then
echo "$hits" | while IFS=: read -r file lineno _; do
printf '::error file=%s,line=%s::tracker tag — write prose or a full issue URL, not a hash-number tag (see /knowledge/hive-rules.md)\n' "$file" "$lineno"
done
src_count="$(printf '%s\n' "$src_hits" | wc -l | tr -d ' ')"
count=$((count + src_count))
fi
if [ -n "$md_hits" ]; then
echo "$md_hits" | while IFS=: read -r file lineno _; do
printf '::error file=%s,line=%s::ambiguous cross-repo reference — a bare `#N` or a fully qualified `owner/repo#N` are fine, but this looks like an unqualified `owner#N`; spell out the repo or clarify in prose\n' "$file" "$lineno"
done
md_count="$(printf '%s\n' "$md_hits" | wc -l | tr -d ' ')"
count=$((count + md_count))
fi
if [ "$count" -gt 0 ]; then
printf 'check-issue-refs: %s hit(s) found\n' "$count" >&2
count="$(printf '%s\n' "$hits" | wc -l | tr -d ' ')"
printf 'check-issue-refs: %s tracker tag(s) found\n' "$count" >&2
exit 1
fi
exit 0

View file

@ -43,7 +43,8 @@ It used to be otherwise, and the seam was real: each side had its own
`users.json` treated as authoritative, rendering the _same_ physical
`users.yml`. A writer whose own JSON was missing could not tell "nothing
here yet" from "someone else's users", so it refused to write at all —
which is exactly what a hive with existing users hit (#3422).
which is exactly what a hive with existing users hit
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3422`).
What still has to hold, since two processes share the file:

View file

@ -16,7 +16,7 @@ That is the whole intent of the first slice. The point is to make the _unit_
real — service user, runtime and state directories, socket, nginx
reachability — so the swarm-level surfaces that follow have somewhere to land.
Inventing those surfaces before they are agreed would bake in a shape nobody
chose. See #3066 and the `hyperhive.swarm` consolidation epic.
chose. See `$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3066` and the `hyperhive.swarm` consolidation epic.
## Why a unix socket, not a port

View file

@ -32,7 +32,8 @@ There used to be: a private `users.json` here, canonical, with `users.yml`
rendered from it, while `swarm-authelia-bridge` kept its own pair against
the _same_ physical file. Two canonical stores for one file is a seam, and
it bit — a writer whose own JSON was missing could not tell "nothing here
yet" from "someone else's users", and refused to write (#3422).
yet" from "someone else's users", and refused to write
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/3422`).
The argument for the split was that it let this crate work without a YAML
parser. It didn't: the JSON was read back on every run, so the round-trip