ci(#1555): prose-ify legacy tracker tags; add lint:allow escape hatch

Clean the legacy backlog so the tracker-tag lint can become a required
gate (mara's warn-during-cleanup -> full-tree-enforcement path). Rewrite
the ~33 real `closes/see #NNN` provenance refs in doc-comments to prose
across hive-forge, hive-c0re, hive-ag3nt, hive-matrix-mcp, hive-sh4re,
and add a `lint:allow` line marker to check-issue-refs.sh for genuine
non-tracker `#<digits>` (a hash-digit heading-detection test input).
Tree is now lint-clean; tracker-tag lint ready to promote to required.
This commit is contained in:
atlas 2026-06-10 01:40:57 +02:00 committed by mara
commit ab1b07acce
23 changed files with 55 additions and 47 deletions

View file

@ -46,7 +46,7 @@ pub async fn run(socket: PathBuf) {
let token_path = format!("{state_dir}/forge-token");
// Retry reading the token to handle races where hive-priv provisions the
// token after the harness starts, or where a parent-container chown briefly
// makes the file unreadable (see #1304 / #1309). We wait up to
// makes the file unreadable. We wait up to
// TOKEN_RETRY_MAX * TOKEN_RETRY_SECS before giving up.
let token = {
let mut attempts = 0u32;
@ -205,14 +205,14 @@ fn escape_md_headings(body: &str) -> String {
/// Strict `CommonMark` ATX-heading detector: 1-6 leading `#`s followed
/// by either a space, tab, or end-of-line. Anything tighter (`#tag`,
/// `#123`) is a non-heading line that the renderer will not promote.
/// `#9`) is a non-heading line that the renderer will not promote.
fn is_atx_heading(line: &str) -> bool {
let hashes = line.bytes().take_while(|&b| b == b'#').count();
if !(1..=6).contains(&hashes) {
return false;
}
// Bare `#` / `##` / ... on its own line, or proper ATX with a
// space/tab after the run of `#`s; anything else (`#tag` / `#123`)
// space/tab after the run of `#`s; anything else (`#tag` / `#9`)
// is not a heading.
matches!(line.as_bytes().get(hashes), None | Some(b' ' | b'\t'))
}
@ -843,11 +843,11 @@ mod tests {
#[test]
fn escape_md_headings_skips_non_atx_hash_lines() {
// ATX requires a space after the `#`s. Lines like `#tag`,
// `#123`, `#!/bin/bash` are NOT headings — escaping them
// would just add cosmetic noise where the renderer
// wouldn't promote the line in the first place.
let body = "#tag\n#123\n#!/bin/bash\n####### too many hashes\nbody";
// ATX requires a space after the `#`s. Lines like `#tag`, a
// hash-then-digits run, or `#!/bin/bash` are NOT headings —
// escaping them would just add cosmetic noise where the
// renderer wouldn't promote the line in the first place.
let body = "#tag\n#123\n#!/bin/bash\n####### too many hashes\nbody"; // lint:allow: hash-digit heading test input, not a tracker tag
let escaped = escape_md_headings(body);
// All four leading `#` lines pass through untouched: too few
// (still need space), seven `#`s (over the cap), shebang

View file

@ -841,7 +841,7 @@ pub(crate) fn handle_send(
let resolved = crate::topology::resolve_recipient(agent, to);
// Validate that the resolved recipient is a known local agent or the
// special "operator" recipient. Without this check a typo in `to`
// silently queues a message nobody will ever read (issue #1165).
// silently queues a message nobody will ever read.
//
// Cross-hive messaging (`name@hive` qualified names) is not routed
// through the broker — use the Matrix MCP tools for that instead.

View file

@ -347,7 +347,7 @@ impl Broker {
}
/// Unacknowledged messages addressed to `recipient`, newest-first.
/// Backs the dashboard's operator inbox (#1469): the operator never
/// Backs the dashboard's operator inbox: the operator never
/// `recv`s over an agent socket, so messages to `"operator"` sit in
/// the broker with `acked_at IS NULL` until the operator hits "mark
/// all read" (which calls [`Broker::mark_all_read`]). This read
@ -1352,7 +1352,7 @@ mod tests {
assert_eq!(broker.ack_turn("b").unwrap(), 5);
}
/// The #1462 fix: a transient `ping` fired while no `recv` is parked
/// Transient-wake regression guard: a `ping` fired while no `recv` is parked
/// must NOT be lost — it's buffered and drained by the next collect.
#[test]
fn transient_ping_buffered_when_no_receiver_parked() {

View file

@ -1221,8 +1221,8 @@ pub(crate) fn emit_meta_inputs_snapshot(coord: &Coordinator) {
});
}
/// Unread operator-directed messages for the dashboard's Y3R C4LL inbox
/// (#1469). Returns messages addressed to `"operator"` that haven't been
/// Unread operator-directed messages for the dashboard's Y3R C4LL inbox.
/// Returns messages addressed to `"operator"` that haven't been
/// acked yet (the operator clears them via the existing
/// `POST /api/agent/operator/mark-all-read`). Newest-first; path-shaped
/// tokens are validated so the client renders file links like the

View file

@ -1145,7 +1145,7 @@ async fn set_nspawn_flags(
// Make /shared writable by every agent. Containers share host uids (no
// PrivateUsers), but each agent is a distinct unix user, so a root-owned
// 0755 dir leaves them unable to write — the documented "read/write for
// all agents" contract was broken (#1374). A setgid group would need a
// all agents" contract was broken. A setgid group would need a
// pinned GID declared in every container plus all agent users joined to
// it (cross-container coordination + a rebuild cascade); instead we use
// the /tmp model — sticky world-writable (1777). The sticky bit lets any

View file

@ -1224,7 +1224,7 @@ mod tests {
kind: QueueKind::Rebuild,
agent: "agent-a".to_owned(),
source: QueueSource::Approval,
reason: "approval #42 apply commit".to_owned(),
reason: "approval 42 apply commit".to_owned(),
parent_id: None,
inputs: Vec::new(),
approval_id: Some(42),

View file

@ -1,5 +1,5 @@
//! Body-input resolution shared by every verb that posts a body.
//! Matches the bash `resolve_body` helper (#382): exactly one source
//! Matches the bash `resolve_body` helper: exactly one source
//! between `--body`, `--body-file`, and piped stdin. Passing both
//! `--body` and `--body-file` is a clear error.

View file

@ -32,7 +32,7 @@ pub struct Client {
pub default_repo: String,
/// Global `--json` flag — verbs that have a human-readable
/// default path branch on `client.json_mode()` to pick the
/// JSON output shape instead. Closes #421.
/// JSON output shape instead.
json_mode: bool,
}
@ -131,7 +131,7 @@ impl Client {
/// params (`?limit=N&state=open&...`) are preserved. Pages drain
/// while the response carries a `Link: rel="next"` header, up to
/// `max_pages` (the runaway-loop safety cap). Returns the merged
/// array. Used by `lint` for repo-wide queries (closes #505).
/// array. Used by `lint` for repo-wide queries.
pub fn get_json_all(&self, path: &str, max_pages: u32) -> Result<Vec<Value>> {
let sep = if path.contains('?') { '&' } else { '?' };
let mut merged = Vec::new();
@ -271,8 +271,8 @@ fn read_token() -> Result<String> {
}
/// Surface non-2xx HTTP responses as anyhow errors with the response
/// body included (matches `curl --fail-with-body`). Closes #353's
/// "silent failures with no clue what went wrong" case.
/// body included (matches `curl --fail-with-body`) — turns
/// silent failures into errors with a clear message.
fn check_status(resp: Response, op: &str) -> Result<Response> {
let status = resp.status();
if status.is_success() {

View file

@ -8,7 +8,7 @@
//! Single binary with verb subcommands. Replaces the prior bash
//! script (`hive-forge-tools.nix`) so that agents and operators get
//! the same error handling, exit codes, and JSON shapes regardless
//! of how the bash mood was that day (closes #280).
//! of how the bash mood was that day.
#![warn(missing_docs)]
// Clap-derived `Args` structs are intentionally consumed by their
@ -26,7 +26,7 @@ use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "hive-forge",
about = "Forgejo CLI wrapper for hyperhive (closes #280)",
about = "Forgejo CLI wrapper for hyperhive",
disable_help_subcommand = true
)]
struct Cli {
@ -36,7 +36,7 @@ struct Cli {
#[arg(short = 'r', long, global = true)]
repo: Option<String>,
/// Emit JSON output instead of the verb's default human-readable
/// shape, for verbs that support both (closes #421). Verbs whose
/// shape, for verbs that support both. Verbs whose
/// only output is already JSON (`issue`, `pr`, etc.) ignore this
/// flag — they always print JSON regardless.
#[arg(long, global = true)]
@ -100,7 +100,7 @@ enum Verb {
Subscription(verbs::subscription::Args),
/// List timeline events on an issue or PR (closes, label adds,
/// assignments, commit refs, pushes, etc.) — the audit trail
/// `view` + `comments` don't surface (closes #783).
/// `view` + `comments` don't surface.
Timeline(verbs::timeline::Args),
/// Upload a file as an attachment to an issue.
AttachIssue(verbs::attach::IssueArgs),

View file

@ -1,7 +1,7 @@
//! `assign <number> <user> [--remove]` — add or remove a user from an
//! issue/PR's assignee list. Forgejo has no dedicated POST endpoint —
//! we read the current list, mutate, and PATCH the issue back (closes
//! #353's "no such endpoint" trap; matches the bash helper's logic).
//! we read the current list, mutate, and PATCH the issue back (Forgejo
//! has no such endpoint; matches the bash helper's logic).
use anyhow::Result;
use clap::Args as ClapArgs;

View file

@ -1,6 +1,6 @@
//! `comments <number> [--limit N | --tail N]` — list comments on an
//! issue or PR. Closes the curl-fallback gap (#418); `--tail`
//! closes the third of the four #694 gaps (paging-for-long-threads
//! issue or PR. Replaces the curl fallback; `--tail`
//! handles the paging-for-long-threads
//! awkwardness).
//!
//! - `--limit N` (default 50, Forgejo's cap) returns the first N
@ -14,7 +14,7 @@
//! on this long thread?" without scrolling through the whole
//! history.
//!
//! Use the global `--json` flag for JSON output (#421).
//! Use the global `--json` flag for JSON output.
use anyhow::Result;
use clap::Args as ClapArgs;

View file

@ -5,7 +5,7 @@
//! `package-lock.json`, …) is collapsed to a single
//! `[<path>: contents changed (+N -M, --full for content)]`
//! line so a `flake.lock` rev bump doesn't drown the human-
//! authored changes in 5 000 lines of lock churn (#222). The
//! authored changes in 5 000 lines of lock churn. The
//! per-file git headers (`diff --git`, `index`, `---`, `+++`,
//! and any rename / mode metadata) are suppressed alongside the
//! hunks since the placeholder already carries the file path and

View file

@ -1,5 +1,5 @@
//! `lint <subcommand>` — issue/PR/branch lint queries for triage
//! workflows (closes #505). Replaces ad-hoc curl + jq filtering with
//! workflows. Replaces ad-hoc curl + jq filtering with
//! typed commands that always emit JSON via the global `--json`
//! (default is a compact one-line-per-item human shape).
//!
@ -97,7 +97,7 @@ struct NoReviewerArgs {
/// Reviewer login to look for (matches `@<reviewer>` in PR body or
/// any comment). Required — defaulting to a specific name would
/// bake one deployment's reviewer convention into the binary
/// (mara's nit on #507).
/// (flagged in review).
#[arg(long)]
reviewer: String,
}
@ -177,7 +177,7 @@ fn run_no_reviewer(client: &Client, args: NoReviewerArgs) -> Result<()> {
continue;
}
// Paginate so PRs with >50 comments don't yield false positives
// (argus nit on #507). Same 1000-comment ceiling as elsewhere.
// (flagged in review). Same 1000-comment ceiling as elsewhere.
let comments = client.get_json_all(
&format!("/repos/{repo}/issues/{number}/comments?limit={PAGE_LIMIT}"),
MAX_PAGES,

View file

@ -5,7 +5,7 @@
//!
//! Mirrors Forgejo's `GET /repos/{owner}/{repo}/issues` query-string
//! filters one-for-one so the mental model carries over. Closes the
//! second of the four #694 gaps (read-side; no boundary concerns —
//! read-side curl-fallback gap (no boundary concerns —
//! every agent + the operator queries the issue tracker constantly).
use std::fmt::Write as _;

View file

@ -8,7 +8,7 @@
//! hint block is filtered out of git's stderr (we print the canonical
//! URL ourselves once the API returns). Other git stderr passes
//! through. Default behaviour is unchanged: no push unless asked.
//! Closes the auto-push half of #222 per operator decision (opt-in
//! Adds the auto-push path per operator decision (opt-in
//! flag).
//!
//! With `--agit` the PR is opened via Forgejo's `AGit` flow instead of

View file

@ -1,7 +1,7 @@
//! `timeline <number> [--limit N]` — list timeline events on an
//! issue or PR. Closes #783 (last piece of the #694 epic: agents kept
//! issue or PR. Fills the gap where agents kept
//! falling back to curl for "who closed this?" / "when was this
//! labelled?" archaeology). Composes naturally with `view <n>` /
//! labelled?" archaeology. Composes naturally with `view <n>` /
//! `comments <n>` — separate verb keeps the existing shapes stable.
//!
//! Forgejo's `/issues/{n}/timeline` endpoint returns BOTH the actual
@ -12,7 +12,7 @@
//!
//! `--tail N` is a follow-up (the timeline endpoint doesn't expose a
//! total-count field so we can't use the count-then-page trick that
//! `comments --tail` lands in #770; future shape probably mirrors
//! `comments --tail` uses; future shape probably mirrors
//! `comments --tail` once Forgejo grows a `count` query or we accept
//! the trailing-slice cost).
@ -211,7 +211,7 @@ mod tests {
//! Tests call `format_event` directly so any new event-type arm
//! added in `print_event`'s dispatch is automatically covered by
//! the rendering path (no parallel test-side dispatch to keep in
//! sync). Argus on PR #798 🟡: "extract a `format_event(ev) ->
//! sync). A review flagged: "extract a `format_event(ev) ->
//! String` helper and test that function directly instead of
//! duplicating the logic" — addressed.
use super::*;

View file

@ -4,7 +4,7 @@
//! the payload to claude; `Error { message }` becomes the tool-call
//! error message claude sees).
//!
//! Tool surface mirrors damocles-daemon's v0 set per mara on #548:
//! Tool surface mirrors damocles-daemon's v0 set per the operator's call:
//! `send_message`, `send_dm`, `send_reaction`, `send_reply`, `mark_read`,
//! `list_rooms`, `list_room_members`, `read_room`. Plus a `ping` for the
//! MCP bridge's liveness probe.

View file

@ -5,7 +5,7 @@
//! The wire protocol between the two binaries lives in [`protocol`].
//! Path helpers (token file, daemon socket) live in [`paths`].
//!
//! Phase 3 of #548. Architecture rationale + tool surface mirror the
//! Phase 3 of the matrix-MCP work. Architecture rationale + tool surface mirror the
//! existing `damocles-daemon` (see issue thread for details).
pub mod client;

View file

@ -18,7 +18,7 @@ pub const DEFAULT_HOMESERVER: &str = "http://localhost:8008";
/// Lives under systemd's `RuntimeDirectory=hive-matrix` (a tmpfs path
/// that disappears on container restart — fine, because the daemon
/// recreates the socket on its own boot) so the agent unix user
/// (post-#658) can bind a socket inside it without root in `/run`.
/// can bind a socket inside it without root in `/run`.
pub const DEFAULT_DAEMON_SOCKET: &str = "/run/hive-matrix/socket";
/// Resolve the matrix access-token file path. Override via

View file

@ -1,7 +1,7 @@
//! Matrix event handlers: incoming room messages fire a hyperhive
//! wake signal so the agent's harness drives a new claude turn.
//!
//! Per mara on #548: wake body is a SHORT TEASER, not the full message
//! Per the operator's call: wake body is a SHORT TEASER, not the full message
//! (msg stays unread server-side; agent fetches via `read_room`). The
//! `wake::format_wake_body` truncates to ~100 chars.
//!

View file

@ -7,7 +7,7 @@
//! The agent harness's `agent_server` parses it and treats it as a
//! `Wake` from the matrix subsystem.
//!
//! Per mara's call on #548 phase 3: the body is a SHORT TEASER, not
//! Per the operator's call (phase 3): the body is a SHORT TEASER, not
//! the full message — the agent then reads the unmarked event via
//! the `read_room` MCP tool. Truncation to ~100 chars keeps the wake
//! prompt focused (`forge_notify` embeds longer excerpts because the

View file

@ -77,7 +77,7 @@ pub fn core_avatar_png() -> PathBuf {
/// `$HIVE_ASSETS_DIR/branding/agent-configs.png` — secondary org
/// mark for the `agent-configs/` mirror org. Rendered from
/// `agent-configs.svg` at asset-build time (was rendered in
/// `hive-c0re/build.rs` before #555).
/// `hive-c0re/build.rs`).
#[must_use]
pub fn config_org_avatar_png() -> PathBuf {
dir().join("branding/agent-configs.png")

View file

@ -19,16 +19,24 @@
# 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.
#
# 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.
set -eu
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.
# and an empty file list. Lines carrying the `lint:allow` marker are
# dropped (legitimate non-tracker `#<digits>`; see the header).
hits="$(
git ls-files -z '*.rs' '*.nix' '*.js' '*.ts' '*.css' '*.html' \
| xargs -0 -r grep -nE "$pattern" /dev/null 2>/dev/null || true
| xargs -0 -r grep -nE "$pattern" /dev/null 2>/dev/null \
| grep -v 'lint:allow' || true
)"
if [ -n "$hits" ]; then