hyperhive/hive-forge-notify/src/bin/hive-github-notify/main.rs
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

110 lines
4.4 KiB
Rust

//! `hive-github-notify` binary — long-running per-agent **github.com**
//! notification poller. Same delivery path as its Forgejo sibling: unread
//! list, per-thread summary, todo upsert on the harness's in-agent socket,
//! mark read on the source.
//!
//! Takes no arguments. `HYPERHIVE_STATE_DIR` holds the PAT
//! (`github-token`, provisioned from the dashboard credentials tab — see
//! `docs/integrations/github.md`) and `HIVE_AGENT_SOCKET` is the harness's todo socket.
//! Having a PAT *is* the opt-in: with no token the poller logs why and
//! exits 0, so deploying this unit to an agent that never gets one costs a
//! settled process rather than a restart loop.
//!
//! ⚠️ Reading the notification stream needs the **`notifications` scope**
//! on the PAT — a token minted for `gh` + `git push` usually carries `repo`
//! only, which is enough to push and open PRs but not to read (or mark
//! read) notifications. A PAT without it is not fatal: the poller logs the
//! refusal and stays quiet, so the symptom is silence rather than an error.
mod source;
use std::collections::HashMap;
use std::time::Duration;
use hive_forge_notify::notify::{
POLL_INTERVAL_SECS, TOKEN_RETRY_MAX, TOKEN_RETRY_SECS, poll_once, resolve_own_login,
};
use hive_forge_notify::{HTTP_TIMEOUT_SECS, agent_socket, init_tracing};
use source::GithubSource;
use tracing::{debug, info, warn};
#[tokio::main]
async fn main() {
init_tracing();
let socket = agent_socket();
info!(socket = %socket.display(), "hive-github-notify starting");
// Returns only when no PAT ever arrives; otherwise loops forever.
github_loop(hive_forge_notify::state_dir(), socket).await;
}
/// Waits for the PAT to appear: the token is written out of band from the
/// dashboard and takes effect without a rebuild, so an agent that gains a
/// PAT mid-session starts getting notifications on the next tick rather
/// than after a restart. Gives up — returning, so the process exits 0
/// rather than restart-looping — when no PAT ever arrives, which is the
/// common case for an agent that has the unit but no account.
async fn github_loop(state_dir: String, socket: std::path::PathBuf) {
let token_path = format!("{state_dir}/github-token");
let mut attempts = 0u32;
let token = loop {
match tokio::fs::read_to_string(&token_path).await {
Ok(t) if !t.trim().is_empty() => break t.trim().to_owned(),
_ => debug!("forge_notify: no github token at {token_path} yet"),
}
attempts += 1;
if attempts >= TOKEN_RETRY_MAX {
debug!(
"forge_notify: no github token after {TOKEN_RETRY_MAX} retries — github disabled"
);
return;
}
tokio::time::sleep(Duration::from_secs(TOKEN_RETRY_SECS)).await;
};
let client = match reqwest::Client::builder()
.timeout(Duration::from_secs(HTTP_TIMEOUT_SECS))
.build()
{
Ok(c) => c,
Err(e) => {
warn!("forge_notify: failed to build github HTTP client: {e}");
return;
}
};
let source = GithubSource::new(&token);
let mut own_login = resolve_own_login(&client, &source).await;
let mut delivered: HashMap<String, String> = HashMap::new();
// GitHub tells callers how often it is willing to be polled
// (`X-Poll-Interval`, 60s in practice) and rate-limits those who
// ignore it. Start at our own cadence and re-arm to whatever the
// server asks for — never faster than it wants, never slower than we
// need.
let mut cadence = POLL_INTERVAL_SECS;
let mut interval = tokio::time::interval(Duration::from_secs(cadence));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval.tick().await;
info!("forge_notify: github polling started");
loop {
interval.tick().await;
if own_login.is_empty() {
own_login = resolve_own_login(&client, &source).await;
}
let hint = poll_once(&source, &client, &socket, &mut delivered, &own_login).await;
if let Some(secs) = hint.filter(|s| *s > cadence) {
debug!(
secs,
"forge_notify: github asked for a slower poll — re-arming"
);
cadence = secs;
interval = tokio::time::interval(Duration::from_secs(cadence));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval.tick().await;
}
}
}