Commit graph hyperhive/hive-forge
Author SHA1 Message Date
atlas
529e9416c1 hive-forge: say which flat forms were removed and which are staying
`docs/tools/forge.md` said the flat forms "still work as hidden
back-compat aliases" and named `pr-create` and `pr-status` as its
examples. Both refuse now: nine flat verbs parse only far enough to
print the namespaced form that replaced them.

The doc had no way to be read correctly, because the seven that do still
work are hidden too and `--help` shows neither group. Naming the split
here is the only place the difference is visible.

At the enum the kind-agnostic seven now read as staying rather than as
"still work", and `credential-helper` is marked as belonging to neither
group: it is hidden because git invokes it, so a sweep reading
`hide = true` as "deprecated" would remove checkout auth.

Refs #3974.
2026-09-03 02:16:55 +02:00
damocles
9065898e08 hive-forge: carry the HTTP status structurally instead of flattening it to a string 2026-09-03 01:34:02 +02:00
atlas
cdac6091eb hive-forge: name the CI job ci-log actually served
`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.
2026-09-02 21:57:43 +02:00
atlas
0824caaa2b hive-forge: two more pure renames say what replaced them
Second round for the flat-alias deprecation. `pr-commits` and
`pr-assign-reviewer` are back-compat aliases with an exact scoped
equivalent, and they were missed by all three buckets of the original
accounting -- which was wrong in both terms: `reopen`/`dependency`/
`reaction` appear only in the doc-comment prose listing the scoped
surface and are not Verb variants at all, so the real count is 17
(7 + 2 + 7 + 1), not 18.

The test for "pure rename" is that an exact scoped form exists AND the
verb is PR-only, so a caller holding just a number can be sent somewhere
unambiguous. Verified rather than assumed: `pr commits` and
`pr assign-reviewer` exit 0, while `issue commits` and
`issue assign-reviewer` exit 2 -- the same code a deliberately bogus
verb returns, which is what makes those 2s mean "no such verb".

That is also why the seven kind-agnostic aliases are untouched here:
each exists as both `issue <verb>` and `pr <verb>`, so refusing them
would remove a capability rather than rename one.

Both run functions are still called from pr_cmd.rs, so nothing becomes
dead code. The enum's block comment enumerates the pure renames by name
and is extended to match -- left alone it would be a stale claim naming
seven of nine.
2026-09-02 20:35:22 +02:00
damocles
d27cf6ce3e hive-forge: add markdown-docs generator and CI freshness check 2026-09-02 19:38:34 +02:00
atlas
74b48c5afa hive-forge: test the two invariants dependency.rs documents but never checked
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.
2026-09-02 19:01:31 +02:00
atlas
e0c2f1aeaa hive-forge: die by SIGPIPE like every other CLI in a pipeline
Rust's runtime sets SIGPIPE to SIG_IGN at startup, so writing to a pipe
whose reader has gone away returns EPIPE and println! panics. `hive-forge
<verb> | head` printed a panic and exited 101 where cat, grep and every
other pipeline member exit quietly.

101 is not cosmetic: to a caller running `set -o pipefail` it is a real
failure, so a shell script that pipes our output stops on a condition
that is not an error.

Restore SIG_DFL first thing in main, before any output. Measured against
the same command: piped to head, 141 (killed by the signal) with empty
stderr; unpiped, 0; the pre-change binary, 101 with a panic.

Closes #3972
2026-09-02 18:40:33 +02:00
atlas
f293454df9 hive-forge: the renamed flat verbs now say what replaced them
Seven hidden aliases are pure renames — `pr status` does exactly what
`pr-status` did. They now refuse and name the replacement instead of
running, which is the deprecation step before removing them outright.

Deliberately not all of the hidden verbs. The other ten (`view`,
`comment`, `comments`, `close`, `reopen`, `assign`, `labels`, `timeline`,
`dependency`, `reaction`) exist as BOTH `issue <verb>` and `pr <verb>`,
so a caller holding only a number cannot be told which to run. Refusing
those removes a capability rather than renaming one, and there is no
message that would help; they keep working pending a decision on what
replaces them.

Measured: each of the seven exits 1 naming its replacement; `view` still
exits 0; `pr status` still returns its merge-readiness verdict (0 on a
ready PR, 1 on one with CI pending).

Refs #3974
2026-09-02 18:39:04 +02:00
atlas
8a48f958b8 hive-forge: ci-runs --branch takes the refs the listing prints
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
2026-09-02 17:16:56 +02:00
damocles
fe7bf81d4a hive-forge: defer repo resolution to the accessor, not construction 2026-09-02 17:08:01 +02:00
atlas
39b95c2ede treefmt: apply prettier
Pure `nix fmt` output from the commit before this one — no hand edits.
203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs.

Reproduce with `nix develop -c nix fmt` on the parent commit; the result
should be byte-identical to this tree.

None of the 13 `.prettierignore` entries appears here — verified by
intersecting the changed-file list against the ignore file, with a
control proving the intersection finds a match when one exists.
2026-09-02 15:25:07 +02:00
atlas
b0507a0936 hive-forge: move the lint test module to the end of the file
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.
2026-09-02 11:22:42 +02:00
atlas
a78e783c3b hive-forge: 10 behaviour tests for lint's pure helpers, which had none
WIP commit so the mutation run below has a clean base to restore to.
2026-09-02 11:17:14 +02:00
damocles
16f82f3ef6 hive-forge: remove pr-blocked-by-issue hint now that the direction works 2026-09-02 10:22:20 +02:00
damocles
56c0602c2f hive-forge: add ci-runs listing verb, fix ci-log's ambiguous no-log message 2026-09-02 08:36:44 +02:00
iris
07b62612b0 docs: restructure into topic subdirectories, collapse duplicated index
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.
2026-09-02 01:55:37 +02:00
atlas
06e8a9a09e Fix nix references in prose that no longer resolve
Nine of the 38 .nix tokens mentioned anywhere in *.rs did not name
anything that exists. Twelve mentions, five distinct targets:
hive-c0re.nix, hive-gateway.nix and hive-forge.nix are all directories
now; nix/modules/ is not a directory we have; hive-forge-tools.nix was
a bash script the binary replaced and is gone.

Where the reference is load-bearing it is corrected rather than
deleted, because the reference is the point: a comment saying a
constant must match a nix literal is only useful if you can open the
file it names. Where the module member was unambiguous the path now
names it exactly.

paths.rs's STATE_ROOT marker was the worst of them: it claimed the
value came from services.hyperhive.c0re.statePath, in hive-c0re.nix.
Neither exists. The option is not declared anywhere and the file is a
directory, so a "must match" contract pointed at two things that
cannot be opened. /var/lib/hyperhive is hardcoded on both sides, which
is what the comment now says.

hive-forge-tools.nix keeps no replacement: naming a file that was
deliberately deleted helps nobody, and "replaces a prior bash script"
is complete without it.

Measured before and after with the same command: 9 unresolved of 38
before, 4 of 35 after. The remaining four are an example path in a doc
comment, an upstream nixpkgs path, and two from one synthetic test
fixture.
2026-08-30 14:30:27 +02:00
damocles
f5a4e380c9 hive-forge: precise wording on stale-branches rebase+ff ancestry claim 2026-08-28 21:44:19 +02:00
damocles
b8eb598329 hive-forge: fix stale-branches comment/docs — hyperhive rebase+ff merges, not squash 2026-08-28 21:44:19 +02:00
damocles
fa73cd1ddb hive-forge: flag stale-branches merge outcome as unknown when the PR fetch is truncated 2026-08-28 21:44:19 +02:00
damocles
6e05863f02 hive-forge: lint stale-branches reports each branch's merge outcome 2026-08-28 21:44:19 +02:00
atlas
7630b0993c hive-forge: hoist a PR row's merge state in list --json
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.
2026-08-26 23:39:05 +02:00
damocles
676f7715fd hive-forge: stop embedding the forge token in clone URLs 2026-08-26 00:20:58 +02:00
damocles
62ee57123d hive-forge: document ci-rerun --pr's event-type gap on PR status 2026-08-24 15:05:25 +02:00
damocles
ad7a0572cd hive-forge: explain the pr-blocked-by-issue dependency failure instead of a raw 500 2026-08-24 12:09:08 +02:00
damocles
bd4906ab17 hive-forge: unit-test blocking_open_count 2026-08-19 15:17:28 +02:00
damocles
a3f14f5126 hive-forge: surface issues an issue/pr blocks in list/issue/pr 2026-08-19 15:03:15 +02:00
atlas
74dfd366b4 hive-forge: pin forgejo-api to 0.11.1 so it shares the workspace reqwest
forgejo-api 0.11.0 links reqwest 0.12 while the workspace is on 0.13, and
cargo unifies features per (crate, VERSION) — not per crate. So forgejo-api's
internal client was a *second* reqwest, built with neither the workspace's TLS
features nor the system trust store, and every https call it made to a gateway
name failed `invalid peer certificate: UnknownIssuer` while a client built in
this workspace succeeded against the same host.

Pinning to 0.11.1 puts one reqwest in the tree, which is what makes the
existing "feature unification gives forgejo-api our TLS backend" comment true
rather than aspirational — the claim is a fact about the lockfile, so the pin
is load-bearing and says so.

The reqwest feature list moves with it: `rustls-native-certs` and
`webpki-roots` were 0.13.1-era names that no longer exist, and `rustls` now
carries the platform verifier (OS trust store, honours SSL_CERT_FILE). Naming
a feature that is gone is a hard resolution error rather than a silent no-op,
so the list had to change in the same commit as the pin.

Refs #3391
2026-08-19 12:30:50 +02:00
damocles
3c4ff447e3 hive-forge: issue-create refuses an empty body 2026-08-19 01:43:10 +02:00
damocles
8b83eca0c1 fix(#3435): hive-forge issue/pr show fail on forgejo's null-for-empty reactions
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).
2026-08-18 15:26:07 +02:00
damocles
fb9cc5635a hive-forge reaction: add --list-allowed to surface the instance's configured shortcodes 2026-08-18 13:53:27 +02:00
damocles
96a27cef3c docs: cover the reaction verb and comments --show-reactions 2026-08-18 13:53:27 +02:00
damocles
2c45a9960f hive-forge: show + post/remove emoji reactions on issues, PRs, and comments 2026-08-18 13:53:27 +02:00
damocles
a796c24037 hive-forge list: show dep-progress count for items with open dependencies 2026-08-18 12:18:57 +02:00
damocles
4f8b78a4d4 hive-forge dependency: don't drop the original errors if the verification read-back also fails 2026-08-16 18:51:46 +02:00
damocles
eefd971bd8 hive-forge dependency: surface a converged-despite-error case instead of swallowing it 2026-08-16 18:48:39 +02:00
damocles
7668454bae hive-forge: verify dependency add/remove by read-back instead of trusting HTTP status 2026-08-16 18:47:01 +02:00
damocles
bad5285f2e fix(hive-forge): populate owner/repo in dependency add/remove body 2026-08-16 14:57:17 +02:00
damocles
b7a7628845 hive-forge: point to per-line review comments instead of inlining them, fix pr reviews line numbers 2026-08-15 12:34:23 +02:00
damocles
6a95aceedb hive-forge: add dependency verb for issue/pr blocking relationships 2026-08-15 09:44:28 +02:00
damocles
b4e56b964f pr-merge: don't tell the caller to just un-draft a PR that isn't theirs 2026-08-13 15:21:12 +02:00
damocles
dd5ccb5ce7 hive-forge: don't assert 'conflicts' from a bare mergeable=false 2026-08-13 15:21:12 +02:00
damocles
6f3ac755e0 hive-forge: surface comment/issue attachments in comments/view/comment-show/issue 2026-08-11 20:31:36 +02:00
atlas
0e64f46221 fix(#3129): treat a blank head sha as unknown too, not as a real value
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.
2026-08-09 21:52:09 +02:00
atlas
178eb13993 fix(#3129): age out a review against the PR head, not forgejo's stale bit
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.
2026-08-09 21:47:57 +02:00
atlas
6e64489050 fix(forge): suggest the nearest name when a filter value misses
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.
2026-08-05 22:13:45 +02:00
atlas
0aa9a854bc fix(forge): validate list's label + milestone filters, and paginate both
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.
2026-08-05 22:06:12 +02:00
atlas
8e690c0694 feat(forge): search + milestone filters, and a page trailer that can't lie
`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.
2026-08-05 18:45:17 +02:00
damocles
4cf647aa70 hive-forge: add --since cursor paging to comments and timeline 2026-08-03 20:50:53 +02:00
damocles
f6629f0c23 hive-forge: cap timeline --limit so truncation detection can't go blind at the page-size boundary 2026-08-03 20:50:53 +02:00