Mara's decision on #4179: the default window for comments/timeline (no
flags at all) is now the newest 10, matching an explicit --tail 10 --
agent can still opt into the old oldest-first window via an explicit
--limit. Same restructure in both verbs: limit u64 -> Option<u64>, only
the explicit-limit branch takes the head path now, the tail-or-default
branch covers both --tail N and the no-flags case.
timeline.rs also gets the real fix: TimelineListHeaders.x_total_count is
a genuine field forgejo-api already returns on every timeline call --
verified against the vendored crate source. The old 'no total exists'
premise in the module doc was false, same shape as #3200. Replaced the
over-fetch-by-one boolean 'more' with an exact count via a cheap
page_size=1 fetch_total, mirroring comments.rs's fetch_tail pagination
math for --tail. JSON output reshaped to {events, more_before,
more_after, since_more, since_limit_clamped}, matching comments.rs --
a breaking interface change, intentional.
Also fixed a real pre-existing bug in comments.rs's truncation_note:
the more_before message suggested retrying with --tail, which is
nonsensical since more_before only ever fires from a tail-shaped
window. Now correctly suggests --limit. Added a regression test.
fixes#4179
A run triggered by a pull_request-event workflow carries a #<n> pseudo-ref
in prettyref rather than a real branch name. Dispatching a workflow with
that as the ref 500s. resolve_run now recognizes the #<n> form and falls
back to the same branch_for_pr lookup --pr already uses, so ci-rerun --run
works on PR-triggered runs the same way ci-rerun --pr does.
diagnosis + discriminating control table (real branch vs #<n> pseudo-ref)
from atlas.
fixes#4201
7 bail! messages in pr_merge.rs were prefixed pr-merge: -- the flat-alias
verb name that main.rs's renamed-verb list now refuses to run, not the
current pr merge kind-namespaced form. Someone who read the prefix and
tried hive-forge pr-merge got a second, unrelated refusal for a verb that
doesn't exist anymore.
#4157 swept the renamed verbs out of docs, prompts and the deprecation
list; it didn't cover runtime error prefixes, which is where these
survived (same shape as the two pr_status.rs had, fixed in #4182/#4184).
One of the seven also told the reader to check pr status --pr {number};
now pr status {number}, matching #4184's new positional form.
fixes#4183
Every other PR-scoped verb takes the number as a positional — `pr show
42`, `pr comments 42`, `pr assign-reviewer 42 argus`. `pr status` alone
required `--pr 42`, so whichever form you learn first is wrong for the
other, and clap's error for the mistake suggests `-- --pr`, which would
pass the literal string on as the next positional.
The number is positional here too now. `--pr` stays, because it was the
only spelling this verb had; `--sha` keeps its flag because the two are
alternatives rather than one required argument.
Two error strings in this file also named `pr-status`, a form that has
refused to run since the subcommand rename. One of them is the message
you get for passing neither argument — i.e. exactly when you are already
unsure what the verb is called.
The four new cases cover both halves: that clap accepts each spelling,
and that something reads it. `target_pr` is named rather than inlined
for exactly that reason — dropping the positional from the selection
passed every parse-only case while leaving `pr status 42` reporting
"pass a PR number". The conflict case earns its keep the same way: clap
accepting an argument and clap ignoring it are indistinguishable from a
passing parse, so `42 --sha abc` and `42 --pr 42` both have to be
rejected.
Closes#4182.
docs/tools/forge.md already listed the nine renamed verbs as removed, then
used them ~30 more times in pasteable blocks. Sweeps every occurrence a
reader would type, including three runtime messages that told the user to
run a verb the same binary rejects.
The renamed-verb list itself keeps the old names; it is what documents them.
Refs #4155
hive-forge and hivectl doc-comment strings for pr-create --label, diff
--full, list --label, ci-runs --branch, and several hivectl subcommand
options read as if things happened to themselves. Name the actor (hivectl,
hive-forge, or the CLI) instead, and regenerate the three docs/tools/*-cli.md
files from the rebuilt binaries.
swarmctl-cli.md needs no source change: its only passive hit is
clap-markdown's own generated boilerplate, out of scope here.
Fixes the "obvious ones first" slice of #4042 (mara: do the obvious
ones first) -- 81 hits across write-good.So, write-good.ThereIs, and
write-good.Weasel, all in docs/. Each is a genuine sentence rewrite
(lead with the real subject instead of "There is/are", drop a
sentence-initial "So ", replace a vague intensifier), not a blind
regex substitution -- read every hit in its real file context before
touching it.
3 of the 81 hits were in CI-generated CLI docs (docs/tools/{hivectl,
swarmctl,forge}-cli.md) -- fixed at the clap #[arg(...)]/doc-comment
source in hivectl/src/cli.rs, swarmctl/src/main.rs, and
hive-forge/src/verbs/repo_add_collaborator.rs, then regenerated via
each crate's `markdown-docs` subcommand so CI's freshness check stays
green.
Verified: fresh vale re-run shows 0 remaining So/ThereIs/Weasel hits
and no new hits introduced (983->982, exactly the one incidental fix
this pass also picked up at docs/scheduler/observability.md:48).
cargo fmt --check and clippy clean on the three touched crates.
Remaining write-good backlog (Passive: 726, TooWordy: 207) is
judgment-heavy and left for a follow-up slice of #4042, not bulk-
rewritten here.
Same bug as the swarmctl/hivectl fix, a third instance argus's review
didn't name but nix/checks.nix's hive-forge-docs freshness check (same
pattern as hivectl-docs/swarmctl-docs) caught in CI: the earlier
Contractions/Foreign/Auto batches edited docs/tools/forge-cli.md
directly instead of the clap #[arg(...)]/doc-comment strings in
hive-forge/src/main.rs and hive-forge/src/verbs/*.rs.
Applied the same 13 wording changes to source that the earlier commits
already made to the generated .md, matched 1:1 against
'git diff origin/main HEAD -- docs/tools/forge-cli.md' rather than
guessed. Several source doc comments feed two rendered sections each
(e.g. reaction.rs's one Add-variant doc renders under both
'issue reaction add' and 'pr reaction add', since both subcommands
share the same enum) -- one source fix, two generated-doc fixes.
Regenerated from the now-fixed source and confirmed byte-identical to
what's already committed (diff exit 0) -- source and generated output
are back in sync, same as the swarmctl/hivectl fix.
cargo clippy -p hive-forge --all-targets -- -D warnings and
scripts/check-doc-refs.sh both clean.
`ci-log --run <n> --job <idx>` accepted any index and exited 0. Past the
run's job count it printed job 0's log, with nothing marking the
substitution. Measured against run 3363 (4 jobs): indices 0-3 gave three
distinct md5s, while 99 and 12345 both returned output byte-identical to
job 0.
Both web log routes clamp an out-of-range index to job 0 and answer 200.
I had claimed nothing in the response distinguishes them -- that was
measured on the body only, and the body was the wrong place to look. The
`Content-Disposition` header names the job the server actually served:
--job 3 -> filename="ci-doc-pointer lint-8363.log"
--job 99 -> filename="ci-nix flake check-8360.log" (job 0's)
So the fix is to report what came back rather than to pre-validate the
index. Establishing the real job count needs two extra API calls on every
indexed read -- `ActionRun` carries no job count, and the only job
endpoints are the per-run listing and a per-job log keyed by internal job
id, not by index. The header costs nothing: it is already in the response
being read.
The provenance line goes to stderr, so it cannot corrupt a piped log;
under `--json` it is a `served` field instead. An out-of-range `--job` is
therefore no longer an error -- it is a read whose true subject is named.
`get_bytes_named` is a sibling of `get_bytes_raw` rather than a signature
change, leaving the attachment and artifact download paths untouched.
Rebased onto main after the two flat-rename PRs landed; the only conflict
was the test module's import list, resolved by keeping both sides. While
reading the surrounding context this commit's own defect surfaced:
`check_status`'s doc comment had been left glued to the head of
`disposition_filename`'s, so one function carried two unrelated
descriptions and the other carried none. No gate can see that -- it is
well-formed rustdoc either way.
dep_meta's comment records a measured 404: leaving owner/repo as None
made every same-repo dependency fail, because forgejo resolves the
dependency's repo from the request BODY rather than the URL. Nothing
stopped a later tidy-up from simplifying them back to None.
index() promises "error instead of wrapping". A wrap would not fail
loudly -- it would hand the forge a negative index it looks up as some
other issue.
The runs-list query's `ref` filter matches only a fully-qualified ref,
while the listing prints `prettyref` (`main`, `#3967`). So no value a
caller can read off the output was accepted by the filter, and the
rejection rendered as "no matching runs" — indistinguishable from a
branch that has never been built.
Qualify the value before it reaches the query: `refs/...` passes
through, `#N`/`N` becomes the pull head ref, anything else becomes a
branch. A slash cannot mark an already-qualified ref, since branch
names contain them.
Not shared with ci-rerun: its `ref` is a workflow_dispatch body field
taking a bare branch name, so the same normalisation would break it.
Closes#3970
clippy::items_after_test_module (pedantic, denied) — a #[cfg(test)] mod
has to be the last item in its file. Pure move: same line count, same
line multiset, no content change.
`cargo test` was green while this was broken; only clippy saw it.
Per mara's go-ahead on hyperhive#3902 ("getting started is good, but
terminal rendering does not go in there i think"):
Moved 21 top-level docs/*.md files into 7 new topic subdirectories
(existing web-ui/, turn-loop/, swarm/, tools/, crates/ untouched):
getting-started/ setup.md
agent-lifecycle/ agent-hierarchy.md, approvals.md, persistence.md
trust-boundary/ boundary.md, security.md
integrations/ forge.md, matrix.md, github.md, knowledge.md
networking/ gateway.md, network.md, snapshot-store.md
scheduler/ jobq.md, coordinator.md, ci.md, observability.md
process/ conventions.md, gotchas.md, pr-review-gate.md
web-ui/ terminal-rendering.md (moved into the EXISTING dir,
per mara's correction to the original getting-started
guess -- it's UI implementation detail, not onboarding)
The physical layout now matches docs/README.md's own topical headers,
which already amounted to this taxonomy -- see the scoping comment on
the issue for the two findings that motivated this (a genuine
duplication between CLAUDE.md's old "Reading paths" list and
docs/README.md's grouped one, since drifted out of sync with each
other; and the flat layout not matching the grouping we already had).
Fixed every cross-reference this moved across the whole repo (~120
files: docs/ internal links at every depth, Rust doc comments, nix
module option docs, crate READMEs) -- verified two ways: a grep sweep
confirming zero remaining references to any old path, and a script
that resolves every markdown link in docs/**/*.md + CLAUDE.md +
README.md against the filesystem and reports anything that doesn't
exist (zero broken links).
Collapsed CLAUDE.md's "Reading paths" section (the duplicate) down to
a pointer at docs/README.md, now the single index. Rewrote
docs/README.md itself to use the new subdirectory paths and added the
one doc it was missing that CLAUDE.md's old copy had (pr-review-gate.md).
Classified all 22 docs/*.md files first via a haiku subagent (mara's
suggestion) on two axes -- proposed grouping and operator-vs-
implementation focus -- before finalizing the taxonomy; spot-checked
the report and found internal inconsistencies (its classification
table disagreed with its own summary section for a few files), so this
taxonomy is my original proposal + the one correction mara gave
directly, not a blind application of the subagent's table. The
operator-focus data it gathered is still useful for a follow-up
content pass (docs skewing 'mixed' rather than pure operator-facing),
not addressed in this PR -- structure only.
nix fmt clean, both pre-push lints clean.
Forgejo's issue-list endpoint answers 'was this merged?' only inside the
nested pull_request object, while state says closed for a merged PR and
for one closed without merging alike. So the obvious top-level query is
null or ambiguous for every row, and with a // default it renders as a
confident 'nothing merged' that cannot ever be right -- a wrong answer
shaped exactly like a clean one.
Copy merged and merged_at up to the top level of each PR row so the
obvious query is the correct one. Additive: the nested object is left
untouched so an existing consumer keeps working, and issue rows have no
pull_request and pass through unchanged.
The head branch is deliberately not hoisted: this endpoint does not carry
it at all -- the row's ref is an empty string, not the branch -- so there
is nothing to lift. pr show has head_branch.
Forgejo returns a bare `null` body (not `[]`) for a reactions list when
nothing has reacted yet. issue_reactions/comment_reactions deserialized
straight into forgejo-api's typed Vec<Reaction>, which has no null
tolerance, so issue show / pr show / view failed on every item with zero
reactions - i.e. nearly everything.
Generalize pr_status's existing null_as_empty (same quirk, hit earlier
on combined-status statuses) into a shared helper in verbs/mod.rs, and
fetch reactions via the raw JSON path (Client::get_api_json) with a
NullableVec<T> wrapper instead of the typed client's bare Vec<Reaction>.
Also names the failing request in errors going forward, since
get_api_json's error context includes the URL - closes the gap the
issue itself flagged (the old error said what didn't parse but not
what was fetched).
argus's review note: the emptiness guard was one-sided. A reviewed sha
of "" was already treated as unknown, but a head of "" was not — so a
forge returning an empty string rather than omitting the field would
make every review compare unequal and mark the whole PR stale. That is
the wrong-direction failure this function exists to prevent, and the
asymmetry contradicted its own doc comment.
Both sides now check emptiness, for the same reason: a blank string is
a value the forge sent, not a sha it has.
Forgejo's per-review `stale` flag is eventually consistent: seconds
after a push it still reports the pre-push answer, so a verdict against
the previous head reads as current in exactly the window where the CLI
gets run right after pushing.
latest_reviews now fetches the PR head once and ORs a direct comparison
of the review's own commit_id into the flag. Fixing it at construction
rather than at the call sites means superseded() is unchanged and all
three consumers are corrected together: assign-reviewer's refusal,
pr_merge's changes-requested gate, and — the one that matters most —
pr_status's readiness verdict, which could otherwise report a dead
approval as valid.
Unknowns fail toward keeping the verdict: a missing head or commit_id
degrades to today's behaviour instead of voiding every review on the PR.
The third bullet of the issue's ask, dropped in the first pass and caught
in review: a filter value that does not resolve is a near-miss far more
often than an invention, and an error that only lists all 18 available
names makes the reader do the diff by eye -- on the one occasion they
already know they mistyped something.
Thresholded rather than always suggesting the minimum-distance
candidate: a wrong suggestion is worse than none, because it invites a
second failed attempt at a name that was never there. The bound scales
with the needle (a third of its length, capped at 3), so a short name
does not match half the repo and a long one still tolerates a typo or
two, and an unrelated word falls back to the full list.
Tie-break is on length then alphabetical, so the suggestion does not
depend on the order the forge happened to return its labels in.
A filter value the forge cannot resolve is DISCARDED, not rejected, so a
typo does not narrow the result set -- it returns the unfiltered one.
That does not waste a query, it inverts the answer: "is anything open in
this milestone" comes back as every open issue and reads as yes, and a
duplicate check gets a list that never narrowed.
`list` now resolves both before querying. Labels reuse the write side's
resolver; the ids are discarded because this endpoint filters by name, so
resolution here is a spell-check rather than a lookup -- reusing it keeps
the message identical to the one the write side has always produced.
Milestones accept a title or an id and are checked against the ALL-state
set: filtering on a closed milestone is a normal query, and validating
against open-only would reject exactly the retrospective ones.
Both fetchers paginate. `repo_labels` asked for one page of 100 and
treated it as the population -- the inverse of the trailer bug, same
root: a valid label past the cut fails to resolve, and the error then
prints an "available labels" list that is itself truncated, so the
message argues for the typo.
`--assignee` / `--author` stay unvalidated on purpose: someone who has
left still legitimately appears on old issues, so a login that is not a
current member is not necessarily a typo.
Also drops the docs paragraph claiming unknown labels are silently
dropped on the write side; that has not been true since the resolver
landed.
`list` already built its query with `q: None, milestones: None` — both
fields were on the request it was sending. So full-text search over
title and body is a flag, not a new verb, and a text match is only
useful composed with the other filters anyway.
The trailer was the real defect. It fired on `count == limit`, but the
forge clamps page size to its own `api.MAX_RESPONSE_ITEMS`: ask for 400,
get a full 50, and `50 != 400` kept it silent — suppressing the warning
in precisely the case where the truncation is invisible. It now reports
the real total from `X-Total-Count`, which the response header struct
already parsed and the call site discarded. The requested limit is not
clamped client-side: that ceiling is the remote's configuration, not
ours.