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.
110 lines
4.4 KiB
Rust
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;
|
|
}
|
|
}
|
|
}
|