Repository navigation
Description
Design-architect pass 29 (2026-10-31), audit at origin/main 558cc1d2. Full design doc: files/designs/code-github-backup-mirror-contract.md in the keep-the-four-repos-healthy goal workspace. NO user/review — every origin carries explicit acceptance with verified probe evidence, and the judgment calls below are the architect's recorded decisions.
Problem
infra/github-backup/ (umbrella #3215, landed today in PR #3214) mirrors Drew's whole GitHub estate — 818 bare mirrors across 17 owners — with a webhook freshness layer, a daily fetch, and a weekly reconcile it calls its correctness floor. In its first day it produced eight defect filings, and every one is a place where the system's claims about itself are not checked against the thing claimed: valid_mirror tests bareness only, so a zero-ref bare skeleton is recorded already-present and reported mirrored in the DR source of truth (#3238), while a non-bare partial destination wedges the "resumable" clone driver permanently (#3233); a fetch timeout raises out of the daily run unrecorded and unwritten (#3250), while the drainer acks webhook events for fetches that failed or mirrors that are missing, deleting the freshness signal (#3219); the chronic-failure alarm counts failure days since the last success instead of the docstring's "3 distinct days in a row" and latches (#3257), yet can never fire at all for a missing mirror (#3270); every generated-state file is written by truncation and read with a bare json.load, so one torn write wedges fetch, drainer and manifest until manual repair (#3240); and a null repositoryOwner is read as "owner has no repos", silently shrinking plan.json and MANIFEST.json with exit 0 (#3229). The sibling precedent is design #1854 ("a backup that can prove itself") for the laptop file-backup jobs — this is that contract's estate-mirror form, in this system's vocabulary; #1854's system is different and untouched.
The rules (recorded decisions)
- Validity is resolvability. One predicate in
ghb_common.pyfor every consumer (clone skip, fetchmissing, manifeststatus, reconcilemissing): bare and HEAD resolves (rev-parse --verify -q HEAD— the callmanifest.head_shaalready makes). A planned repo is never legitimately empty —build_planexcludes zero-commit repos under the live config (skip_zero_commit_repos: true) — so no emptiness exception exists inside the plan; that settles github-backup(valid_mirror): a bare skeleton with zero refs passes as a verified mirror — clone_all records it ok and MANIFEST.json reports it 'mirrored' #3238's open either/or without an emptiness signal. - Destinations have four states, each with a disposition. Absent → clone; valid → skip (never deleted/re-cloned); skeleton (bare, unresolvable HEAD — zero refs, zero objects by construction) → remove and clone (deletion provably loses nothing); debris (exists, not bare — may hold transferred objects) → move aside to
<dest>.partial-<timestamp>and clone. Disposition recorded inclone-status.jsonl. - Every attempt ends in a recorded outcome.
fetch_reponever raises for a per-repo outcome — a timeout becomes a recordedfailnaming the timeout (clone_all's existing precedent, generalised). One recorder path turns an outcome (ok/fail/missing) into state +fetch.log+ summary, used by both the daily loop and the drainer. - An event is acked only when its fetch landed. Drainer acks a planned repo's event only on recorded
ok; onfail/missingthe event stays in KV for the next drain and thedrain_last::stamp advances only onok. Unplanned repos keep today's ack + note behaviour (reconcile owns creation). - The alarm is a streak of unavailable days. One per-repo day record —
fail, timeout-failandmissingall append today; success clears. Chronic = today completes 3 consecutive calendar days (the docstring pinned over github-backup(fetch): fail_days counts every failure day since the last success, not 3 days in a row — intermittent failures weeks apart trip the chronic alarm and latch exit 1 #3257's window alternative; github-backup(fetch): a missing mirror can never trip the chronic alarm — 'missing' returns before any fail_days record, so the daily run exits 0 forever #3270's immediate-vs-3-day choice settled to the same 3-day scale). Legacyfail_dayslists are read as this record as-is: non-consecutive legacy days never form a streak, so latched false alarms clear on the first post-upgrade run unless a true streak exists. - State is written atomically, loaded tolerantly, only through
ghb_common. Temp file in the same directory + fsync +os.replace; last-good copy kept; a torn load falls back with a loud named warning, never a startup traceback. The twostate.jsonwriters (daily fetch, 5-minute drainer) read-modify-write under a sharedflocklockfile — github-backup(state): plan.json, state.json and MANIFEST.json are written non-atomically and loaded with bare json.load — one torn write wedges the daily fetch, the drainer and manifest regeneration #3240's lost-update window is closed, not documented. - A failed owner keeps its plan. Null
repositoryOwner/repositoriesin a successful GraphQL response is an owner-level error viainventory()'s existingerrorschannel (present-but-empty still inventories as zero). Reconcile retains the failed owner's previously planned entries inplan.json, marks them retained in the report, and still exits non-zero — github-backup(inventory): a null repositoryOwner is silently read as 'this owner has no repos' — reconcile drops the owner's mirrors from plan.json and MANIFEST.json and exits 0 #3229's report-only alternative is pinned away: there is no drop to make unmistakable.
Phases (adopt the origins — no parallels)
- Phase 1 — Validity and destinations: github-backup(valid_mirror): a bare skeleton with zero refs passes as a verified mirror — clone_all records it ok and MANIFEST.json reports it 'mirrored' #3238 + github-backup(clone_all): a clone killed mid-flight leaves a partial destination every rerun rejects — the 'resumable' driver never resumes that repo #3233. Lands first: every other phase's
missingdepends on what validity means. - Phase 2 — Recorded outcomes and the alarm: github-backup(fetch): a git fetch that hits the 3600s timeout raises TimeoutExpired out of fetch_mirrors.main() — every later repo is skipped, no failure is recorded, and state.json/MANIFEST.json are never written #3250 + github-backup(drain): webhook events are acked even when the fetch failed or the mirror is missing — freshness signal silently dropped #3219 + github-backup(fetch): a missing mirror can never trip the chronic alarm — 'missing' returns before any fail_days record, so the daily run exits 0 forever #3270 + github-backup(fetch): fail_days counts every failure day since the last success, not 3 days in a row — intermittent failures weeks apart trip the chronic alarm and latch exit 1 #3257. Independent of Phase 1 in code; its
missingsemantics inherit Phase 1's predicate whenever that has landed. - Phase 3 — Durable state and a plan that cannot shrink silently: github-backup(state): plan.json, state.json and MANIFEST.json are written non-atomically and loaded with bare json.load — one torn write wedges the daily fetch, the drainer and manifest regeneration #3240 + github-backup(inventory): a null repositoryOwner is silently read as 'this owner has no repos' — reconcile drops the owner's mirrors from plan.json and MANIFEST.json and exits 0 #3229 + a new closing audit issue filed with this design (gated on Phases 1–2,
status/blocked): helper adoption across all five scripts, the closing inventory of remaining barejson.load/open("w")on generated state, the contract stated in README + RUNBOOK, evidence on github-backup: estate mirror system (laptop mirrors + dedicated App + Worker webhooks + weekly sweep) #3215.
Alternatives rejected (in the doc)
Fixing the eight independently (the defects are the divergences — that is how the family arose); the window rule for the alarm (the docstring is the operator contract systemd is judged against; the streak is also the less latch-prone reading); deleting debris outright (its objects may be the only copy mid-estate-clone; quarantine is cheap); fetching into the skeleton in place (re-derives clone --mirror's remote/refspec setup in a second place; removal is lossless by the state's definition); a separate watchdog for missing mirrors (the fetch exit code is this system's stated alarm channel — #1854's watchdog belongs to a system without one); report-only plan shrinkage (MANIFEST is regenerated from the plan, so a shrunken plan falsifies the DR record for mirrors that exist).
Risks (in the doc)
Phase 1 recounts skeletons as missing on the first post-landing reconcile/fetch while #3215's initial 818-mirror clone is still in flight — the correction working, but triage should announce the one-time recount on #3215. Ack retention grows the Worker KV queue for persistently failing repos (bounded by coalescing + the rule-5 alarm). Plan retention keeps a vanished owner's repos planned while it errors — loudly (weekly report marks them, exit stays 1), resolved via config exclusions/gaps where owner rulings already live.
Open questions (in the doc, never as issues)
A third MANIFEST status for the skeleton state vs folding into missing; the 3600s fetch timeout against the drainer's 5-minute cadence (decide on Phase 2's timeout records); state.json schema versioning; Worker-side dead-lettering; FAMILIES.md / reconcile-report atomicity (Phase 3's audit dispositions them explicitly).
Acceptance criteria
- Phase 1 lands: github-backup(valid_mirror): a bare skeleton with zero refs passes as a verified mirror — clone_all records it ok and MANIFEST.json reports it 'mirrored' #3238 and github-backup(clone_all): a clone killed mid-flight leaves a partial destination every rerun rejects — the 'resumable' driver never resumes that repo #3233 close with their filed acceptance met, under rules 1–2 as recorded here.
- Phase 2 lands: github-backup(fetch): a git fetch that hits the 3600s timeout raises TimeoutExpired out of fetch_mirrors.main() — every later repo is skipped, no failure is recorded, and state.json/MANIFEST.json are never written #3250, github-backup(drain): webhook events are acked even when the fetch failed or the mirror is missing — freshness signal silently dropped #3219, github-backup(fetch): a missing mirror can never trip the chronic alarm — 'missing' returns before any fail_days record, so the daily run exits 0 forever #3270 and github-backup(fetch): fail_days counts every failure day since the last success, not 3 days in a row — intermittent failures weeks apart trip the chronic alarm and latch exit 1 #3257 close with their filed acceptance met, under rules 3–5 as recorded here.
- Phase 3 lands: github-backup(state): plan.json, state.json and MANIFEST.json are written non-atomically and loaded with bare json.load — one torn write wedges the daily fetch, the drainer and manifest regeneration #3240 and github-backup(inventory): a null repositoryOwner is silently read as 'this owner has no repos' — reconcile drops the owner's mirrors from plan.json and MANIFEST.json and exits 0 #3229 close with their filed acceptance met, under rules 6–7 as recorded here, and the closing audit issue completes its inventory + contract statement.
- No surface of the system (clone-status, fetch summary/exit code, drain summary, MANIFEST.json, reconcile report) can report coverage, success or health that the underlying mirrors, records or inventory do not support.
Activity
2 remaining items
dezren39 commentedon Nov 1, 2026
Agent note (program tending, 2026-11-01): child progress — sub-issues #3229 (github-backup inventory: null repositoryOwner silently misread) CLOSED 2026-11-01T01:59:58Z and #3219 (github-backup drain: webhook events acked on failed fetch) CLOSED 2026-11-01T02:00:05Z (parent link REST-verified this run). This epic's last comment (2026-11-01T01:41Z) predates both closures, so the landings are recorded here. Progress note only — other children remain open; not a completion proposal.
dezren39 commentedon Nov 2, 2026
Program tending — sub-issue progress (2026-11-02)
Closed since this thread's last activity: #3257 (closed 2026-11-02T11:10Z), #3233 (closed 2026-11-02T14:04Z), #3250 (closed 2026-11-02T14:24Z).
Recorded for the parent rollup; this is a progress note, not a completion proposal — the parent stays open on its remaining children.
developing-today commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03
dezren39 commentedon Nov 3, 2026
agent-note:v1
state: picked_up
task: task 8225 (triage, repo-health-code-pilot, lane code-l, direct execution lane)
worker: repo-worker-code
issue: #3282
Triaging design #3282 at current main: verifying each phase/origin issue's fresh state, recording blockers and ordering on the issue, and routing the design's remaining work. No hard-stop labels present (no held / user/owned / do-not-merge / do-not-auto-edit).
dezren39 commentedon Nov 3, 2026
agent-note:v1
state: triaged
task: task 8225 (triage, repo-health-code-pilot, lane code-l)
worker: repo-worker-code
issue: #3282
Triage findings — design #3282 at current main (fresh check 2026-11-03)
Verdict: still live, not closable. 5 of the 8 adopted origin issues are closed with merged fixes; the remaining 3 are already in the queue system with named blockers. No new fix is scoped from this triage — the design's phases and ordering stand as filed.
Phase status (verified via fresh
ghreads this run)packet_defect(the earlier triage note routed the fix as task 5685 for the same issue/scope, so the canonical queue row could not be established safely; resume after task-ID/dedupe reconciliation — see the 2026-11-02 note on github-backup(valid_mirror): a bare skeleton with zero refs passes as a verified mirror — clone_all records it ok and MANIFEST.json reports it 'mirrored' #3238).infra/github-backup/bin/fetch_mirrors.py); in the system as task 6813 (small_fix, queued).Ordering / blockers (as recorded)
Acceptance boxes on this design stay unchecked until each phase's origins close with their filed acceptance met, per the criteria above. Umbrella #3215 remains open and in progress independently.