Design rationale, decisions made, and the rubber-duck critique log.
Read alongside README.md (user-facing) and AMBIGUITIES.md (open decisions).
Jean-Marc reviews many PRs at scale across github.com, GitHub Enterprise
(Microsoft's GHE), and Azure DevOps. The dual-model-review skill (Opus 4.7 +
GPT-5.5) is mature (N=6, asymmetry pattern stable as of PRs #4133/#51/#53/#4248).
The harness is not.
pr-inbox is that harness. It does not perform the review — it
aggregates the inbox, mirrors per-PR state across sessions, and bootstraps
a Copilot session with full context. The CLI is the UI; SQLite is the truth.
Jean-Marc lives in terminals and Copilot sessions. A daemon-with-dashboard introduces operational overhead; a CLI that piggybacks on the existing Copilot session model is native. SQLite is appropriate for a personal operator tool — enough structure for state, sync status, and future telemetry, no service infrastructure to run.
- Auditability + type safety + idiomatic for a Microsoft.Identity-team author.
- Could grow into a shipped tool; .NET ecosystem fits.
- PowerShell 5.1 has known encoding traps (see
current.md2026-05-13 lesson onaz repos pr update --description+ em-dash mojibake) that would bite us across the whole tool surface. - Python is fastest to iterate, but the tradeoff cost of two ecosystems on Jean-Marc's machine + cross-platform packaging is real.
- Three platforms with different shapes (Azure DevOps has no global reviewer inbox; GitHub has stable node IDs; ADO has different thread semantics).
- Capability flags prevent ADO's limitations from leaking everywhere as
ad-hoc conditionals. Example:
listqueriesSupportsGlobalReviewerInboxand falls back to per-project enumeration when it'sfalse. - v0.1 is read-only by construction:
IPrReadSourcehas no write methods. A futureIPrReviewPublisheris a separate type the v0.1 binary cannot accidentally call.
pr-inbox stores no tokens. Reads them on demand from:
gh auth token --hostname <host>(GitHub.com + GHE; one identity per host)Azure.Identity.AzureCliCredentialfor ADO (resource id499b84ac-1321-427f-aa17-267ca6975798)
Eliminates the entire class of "PAT leakage in config" risk, removes any need
for Windows Credential Manager wrapping, and aligns with the tools Jean-Marc
already uses to authenticate. The cost is a hard dependency on gh and az
being installed and authenticated, validated by config doctor.
The rubber-duck pass flagged this as a top blocker. A "current-row" mirror with
last_seen_author_commit_sha and my_open_thread_ids JSON answers "what was
the latest thing I remembered?" but not "what changed since my last
review?" — exactly the question v0.2 follow-up needs.
Solution:
| Table | Cardinality | Purpose |
|---|---|---|
pull_requests |
1 per PR | Fast triage lookup, current-row truth |
pr_snapshots |
N per PR | Platform state at each successful sync |
observed_threads |
N per PR | Per-thread observation, computes "unread" as derived query |
review_runs |
N per PR | Immutable record of each review invocation |
posted_reviews |
(v0.2+) | Reviews I posted, linked back to a review_runs row |
sync_runs |
N per source/identity | Per-attempt status, partial-failure visibility |
Snapshots dedupe: only insert if any tracked field changed. Otherwise just
bump pull_requests.last_synced_at.
pr-inbox review <id> could overwrite brief.md each time. The rubber-duck
critique correctly flagged this as a state continuity hole. Three problems:
- Which brief is canonical a week later?
- Re-running review against new head SHA destroys the prior context.
- A later Copilot session can't tell if it's reviewing the same HEAD or stale.
Solution: %APPDATA%\PrInbox\reviews\<pr_dir>\<UTC-ts>_<head_sha[:12]>\.
Re-running review always appends a new immutable run, registered in
the review_runs table. Future --launch flag, future latest symlink,
future clean verb all live above an immutable substrate.
If sync only pulls currently-assigned PRs, follow-up work is silently
dropped (assignment changes, threads remain open, Copilot keeps commenting).
The registry keeps a PR until explicitly archived:
tracking_reason |
When |
|---|---|
assigned |
Currently a reviewer on this PR |
previously_assigned |
Was a reviewer; still active threads or recent commits |
manually_added |
User added via pr-inbox add (v0.2+) |
archived |
User-archived; ignored by default in list |
Status (open/closed/merged/inaccessible) is orthogonal.
last_briefed_head_sha == current_head_sha is insufficient. If the author
force-pushed, the prior reviewed SHA may no longer be reachable from current
HEAD — materially different from "new commits added." The brief and the
list table need to call this out.
Solution: every snapshot stores ordered_commit_shas (newest-first JSON).
Diffing snapshots tells us:
+3 commits— fast-forwardforce-pushed—last_briefed_head_shanot reachable from current HEADbase changed—base_shachanged since last snapshot
IPrReadSource providers must support reachability checks.
Repos rename. Projects move. PR numbers are repo-scoped. The pretty string
gh.com:owner/repo#N survives a rename only because GitHub continues to
serve at the new owner/repo via redirect — fine for HTTP, not fine as
a primary key.
Solution:
pull_requests.pr_identity(display): primary key for commands/joins within v0.1pull_requests.stable_identity(platform ID-based): durable key for rename migrations and cross-session continuity
Both are stored from day 1. If a rename ever happens, a migration updates
pr_identity to the new display string while stable_identity stays put.
Top blockers raised:
- Registry state too "current-row" oriented. ✅ Adopted — append-only snapshot/event tables added.
- Copilot handoff has a state continuity hole. ✅ Adopted — immutable
review-run directories +
review_runstable. - Stable PR identity underspecified for ADO. ✅ Adopted —
stable_identitycolumn carries platform IDs (project GUID + repo GUID + PR number for ADO). - Secret storage must be designed now. ✅ Reframed by Jean-Marc — delegate
to
ghandaz; no secret storage inpr-inboxat all. IPrSourcecannot just be "fetch assigned PRs". ✅ Adopted — capability- oriented interface withSourceCapabilitiesrecord; read/write separation enforced at type level.
Sharp questions:
- "Assigned to me" vs "needs my attention" →
tracking_reasonenum. last_reviewed_shaambiguity → split intolast_briefed_head_sha/last_review_run_head_sha/last_posted_review_head_sha.- Force-push detection → ordered commit SHAs + reachability check.
- "Copilot unread" → derived query over
observed_threads, not a stored scalar. - v0.1 mutation gates →
IPrReadSourceonly;IPrReviewPublisherdoesn't exist as a type yet.
Smaller risks flagged:
- Sync deletion semantics → never hard-delete; mark status.
- Partial sync → per-source
sync_runswith status enum;listsurfaces stale sources. - JSON arrays bad for querying → relational tables for anything counted/diffed/linked.
- Schema migrations from v0.1 with backup-before-migrate.
- Brief files may leak private data → kept under
%APPDATA%, never in repo, gitignored. - Git from day 1, even for personal tools.
Things explicitly fine (not re-litigated):
- Print-and-paste Copilot handoff for v0.1.
- SQLite as backing store.
- ADO per-project enumeration (no global reviewer inbox API).
- Stable + display identity pairing.
See AMBIGUITIES.md.