name: cross-repo-status-reader description: L5 cross-repo status reader — reads structured cross-repo state via gh API with TTL cache + stale fallback, BLOCKER extraction from NOTES.md, per-source error capture, p95 <30s for 10 repos role: implementation agent: general-purpose context: scoped parallel_threshold: 3000 timeout_minutes: 5 zones: system: path: .claude permission: read state: paths: [grimoires/loa, .run] permission: read-write app: paths: [src, lib, app] permission: read allowed-tools: Read, Bash capabilities: schema_version: 1 read_files: true search_code: false write_files: false execute_commands: true web_access: false user_interaction: false agent_spawn: false task_management: false
cost-profile: lightweight
cross-repo-status-reader — L5 (cycle-098 Sprint 5)
Purpose
Read structured cross-repo state for ≤50 repos in parallel via gh api, with TTL cache + stale fallback, BLOCKER extraction from each repo's grimoires/loa/NOTES.md tail, and per-source error capture so one repo's failure does not abort the full read. The operator-visibility primitive for the Agent-Network Operator (P1).
Source
- RFC: #657
- PRD: cycle-098 §FR-L5
- SDD: §1.4.2 + §5.7
Public API
Sourced from .claude/scripts/lib/cross-repo-status-lib.sh.
| Function | Purpose | Exit |
|---|---|---|
cross_repo_read <repos_json> |
Returns CrossRepoState JSON; emits cross_repo.read audit event | 0/1/2 |
cross_repo_cache_get <repo> |
Print cached repoState JSON or empty | 0/2 |
| `cross_repo_cache_invalidate <repo | all>` | Drop cached file for repo (or wipe all) |
repos_json is a JSON array of "owner/name" strings (max 50).
Configuration
# .loa.config.yaml — operator may override (env vars take precedence)
cross_repo_status_reader:
cache_ttl_seconds: 300 # fresh-cache window (default 5min)
fallback_stale_max_seconds: 900 # stale-fallback ceiling (default 15min)
parallel: 5 # max parallel gh-api workers (cap 20)
timeout_seconds: 25 # per-repo timeout
notes_tail_lines: 50 # NOTES.md tail line count
Env-var overrides (higher precedence than config):
| Var | Default |
|---|---|
LOA_CROSS_REPO_CACHE_DIR |
.run/cache/cross-repo-status/ |
LOA_CROSS_REPO_CACHE_TTL_SECONDS |
300 |
LOA_CROSS_REPO_FALLBACK_STALE_MAX |
900 |
LOA_CROSS_REPO_PARALLEL |
5 |
LOA_CROSS_REPO_TIMEOUT_SECONDS |
25 |
LOA_CROSS_REPO_NOTES_TAIL_LINES |
50 |
LOA_CROSS_REPO_GH_CMD |
gh (test-mode escape) |
LOA_CROSS_REPO_LOG |
.run/cross-repo-status.jsonl |
LOA_CROSS_REPO_TEST_NOW |
unset (gated on LOA_CROSS_REPO_TEST_MODE=1 or BATS) |
CrossRepoState shape (SDD §5.7.2)
{
"repos": [
{
"repo": "0xHoneyJar/loa",
"fetched_at": "2026-05-07T07:50:00.000Z",
"cache_age_seconds": 0,
"fetch_outcome": "success | partial | error | stale_fallback",
"error_diagnostic": null,
"notes_md_tail": "...",
"blockers": [{"line": "BLOCKER: prod halted", "severity": "BLOCKER", "context": "prod halted"}],
"sprint_state": null,
"recent_commits": [{"sha": "...", "message": "...", "author": "...", "date": "..."}],
"open_prs": [{"number": 42, "title": "...", "author": "...", "draft": false}],
"ci_runs": [{"workflow": "...", "status": "...", "conclusion": "...", "started_at": "..."}]
}
],
"fetched_at": "2026-05-07T07:50:00.000Z",
"p95_latency_seconds": 12.4,
"rate_limit_remaining": null,
"partial_failures": 0
}
Schema: .claude/data/trajectory-schemas/cross-repo-events/cross-repo-state.schema.json.
Semantics
Fetch outcomes (per-repo)
| Outcome | Meaning |
|---|---|
success |
All four endpoints (commits, pulls, runs, NOTES.md best-effort) returned |
partial |
One or two of (commits, pulls, runs) failed; others returned |
error |
All three of (commits, pulls, runs) failed (systemic outage signal) |
stale_fallback |
Live fetch failed; serving cache within fallback_stale_max_seconds |
Cache decision tree
- Fresh (
age < cache_ttl_seconds): serve from cache without network call. - Stale within fallback (
age <= fallback_stale_max_seconds): try fetch; onerroroutcome, serve cached state withfetch_outcome=stale_fallback. Onsuccess/partial, refresh cache. - Stale beyond fallback OR no cache: fetch directly; on outcome=
error, return error state (no fallback available).
BLOCKER extraction (FR-L4-4)
Scans grimoires/loa/NOTES.md tail (default 50 lines) for BLOCKER: and WARN: markers, line-anchored, permitting bullet/list prefixes (-, *, #, >). Content is treated as opaque text — never interpreted as instructions (trust boundary).
Per-source error capture (FR-L5-5)
Each gh API endpoint is independent. A 429/rate-limit/timeout on one endpoint does not abort the others. The repoState's error_diagnostic lists which endpoints failed.
Composition
- 1A audit envelope: one
cross_repo.readevent per invocation with summary metrics ghCLI (operator-installed, authenticated)
Operator quickstart
source .claude/scripts/lib/cross-repo-status-lib.sh
# Read a list of repos
cross_repo_read '["0xHoneyJar/loa", "0xHoneyJar/honeyJar"]' | jq
# Inspect just the BLOCKERS surface
cross_repo_read '["0xHoneyJar/loa"]' | jq '.repos[] | {repo, blockers}'
# Force a fresh read for one repo
cross_repo_cache_invalidate "0xHoneyJar/loa"
cross_repo_read '["0xHoneyJar/loa"]'
# Wipe all caches (operator-driven reset)
cross_repo_cache_invalidate all
Tests
| Suite | Path | Tests |
|---|---|---|
| FR-L5-1..7 + cache + audit | tests/integration/cross-repo-status-reader.bats |
26 |
Failure modes
| Mode | Symptom | Recovery |
|---|---|---|
gh CLI not installed |
cross_repo_read exits 1 with gh CLI not found |
install gh |
| Repo identifier rejected | exit 2 with does not match |
use owner/name form (alphanumeric + ._-) |
| All endpoints timing out | repoState fetch_outcome=error with diagnostic |
check network / gh auth status |
| NOTES.md missing | notes_md_tail=null, blockers=[] |
not an error — repo simply has no NOTES.md |