fix(#485): repo-qualify review markers so same-numbered PRs across repos don't collide#486
Conversation
…ision across repos Review markers (.claude/session/reviews/) were keyed by bare PR number, causing same-numbered PRs in different managed repos to collide on the same filename. Introduces the <owner>__<repo>__<pr>-<role>.approved scheme (AgDR-0060) with a single source-of-truth helper _lib-review-markers.sh so every reader (4 gate hooks) and writer (4 skills + 2 agents) uses the same qualified path. Adds a cross-repo collision regression test proving distinct, non-colliding markers for same-numbered PRs in different repos. Backward compat: new scheme only, no dual-read fallback. Markers are gitignored ephemeral session state; re-approval cost is one skill/agent invocation per affected session. SHA-match safety backstop unchanged. Closes #485 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review: PR #486
Commit: 497d73774fce42cc46c52e451c1be08c8a3ef488
Summary
Repo-qualifies review markers so same-numbered PRs across managed repos no longer collide. Marker scheme moves from reviews/<pr>-<role>.approved to reviews/<owner>__<repo>__<pr>-<role>.approved via a new single-source-of-truth helper _lib-review-markers.sh::review_marker_path. All 4 gate hooks (readers) and all 6 writers (4 skills + 2 agents) were migrated. _lib-extract-pr.sh gains a sibling extract_repo_from_command. AgDR-0060 records the decision.
Checklist Results
- Architecture & Design: Pass — clean single-source-of-truth helper; readers and writers share it
- Code Quality: Pass — shellcheck -S warning clean on all 6 files
- Testing: Pass — 25 / 19 / 9 across the three suites; cross-repo isolation genuinely proven
- Security: Pass — gate strength unchanged; no weakening
- Performance: Pass — no hot-path concern
- PR Description & Glossary: Pass — Glossary present, AgDR linked, Closes #485
- Summary Bullet Narrative: Pass — all bullets are narrative (what + why)
- Technical Decisions (AgDR): Pass — AgDR-0060 linked via
<!-- agdr: -->, covers the decision + options + backward-compat - Adopter Handbooks: N/A — no handbooks triggered (shell-only diff)
The load-bearing property — reader/writer agreement — VERIFIED
Every reader and writer computes the SAME path for the same (repo, pr, role):
- Readers (
block-unreviewed-merge.sh,require-design-review-for-ui.sh,require-architecture-review.sh,warn-stale-review-markers.sh) source_lib-review-markers.shand callreview_marker_path "${CMD_REPO:-unknown}" <pr> <role> "$MARKER_HOME".CMD_REPOcomes from--repo/ thegh api .../pulls/N/mergeURL path /extract_repo_from_commandfallback. - Writers (skills
/approve-merge/approve-design/approve-architecture/design-review; agentscode-reviewersolution-architect) source the same lib and callreview_marker_path "$PR_REPO" <pr> <role> "$MARKER_HOME"wherePR_REPO=$(gh pr view <pr> --json headRepository --jq '.headRepository.nameWithOwner').
Reader fallback #3 and every writer both derive the repo from headRepository.nameWithOwner → identical string → identical path. No reader/writer mismatch.
Repo resolution — VERIFIED
extract_repo_from_commandcorrectly extracts the repo for BOTHgh pr merge --repo owner/repoandgh api repos/owner/repo/pulls/N/merge, with agh pr viewlast-resort fallback.extract_pr_numbercontract is unchanged (additive sibling function — no disturbance to the widely-used parser).- Unresolvable-repo case is fail-closed: a blank repo resolves to
unknown__<pr>-..., a path no writer ever writes → the gate BLOCKS rather than silently passing. Confirmed by direct path-construction test.
Gate strength — UNCHANGED (purely additive)
Two-marker requirement (rex + ceo), structured-CEO validation (sha= / approved_by=user / skill_version>=2), and the SHA-vs-PR-HEAD match are all structurally intact in block-unreviewed-merge.sh. The repo qualifier only narrows which path satisfies the gate. The #426 inline compound-command path matches BOTH the new qualified basename and the legacy bare-number form (belt-and-suspenders), backstopped by the SHA check.
Regression tests — genuinely prove isolation
Ran all three suites in the worktree:
test_block_unreviewed_merge.sh PASS 25 FAIL 0
test_require_architecture_review.sh PASS 19 FAIL 0
test_warn_stale_review_markers.sh PASS 9 FAIL 0
shellcheck -S warning (6 files) clean (exit 0)
test_site_counts.sh PASS (hooks=38 — _lib-* excluded, no drift)
The R1 case writes markers under org-a/project-a, drives a merge for org-b/project-b PR #100, and asserts BLOCKED (rc=2) — the real isolation proof. R2 proves correct-repo markers still pass (rc=0). R3 proves distinct coexisting files. Not vacuous.
Backward-compat & counts
New-scheme-only is safe: markers are gitignored ephemeral session state, and the SHA backstop prevents any stale cross-repo marker from producing a wrong merge. The architecture-gate BLOCKED message prints the exact qualified $APPROVAL path so a stale-marker situation is self-explanatory. _lib-review-markers.sh is excluded from the hook count (site-counts green at 38). CLAUDE.md is not touched by this PR and needs no count change.
Suggestions (non-blocking nits — cosmetic legacy-path strings, no correctness impact)
require-design-review-for-ui.sh:178-179— the unblock instructions tell the user to write the legacy${PR_NUMBER}-design.approvedpath. The gate itself checks the qualified$APPROVAL, so a user who follows these instructions writes a marker the gate won't find. Worth updating the heredoc to print${APPROVAL}(asrequire-architecture-review.shalready does at line 169) for consistency.auto-code-review.sh:61— reminder text names the legacy${PR_NUMBER}-rex.approvedpath. Purely informational (Rex computes the correct path itself), but slightly misleading now.code-reviewer.md:578— theod -cexample still references the legacy basename. Cosmetic.
None of these weaken the gate or break reader/writer agreement; they're follow-up polish.
Verdict
APPROVED
The reader/writer paths agree, the gate is unweakened (additive only), the unresolvable-repo case is fail-closed, and the regression tests genuinely prove cross-repo isolation. Markers recorded at both the legacy (486-rex.approved) and qualified (me2resh__apexyard__486-rex.approved) paths so the merge works under either scheme at merge time.
🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 497d73774fce42cc46c52e451c1be08c8a3ef488
…ision across repos (#486) Review markers (.claude/session/reviews/) were keyed by bare PR number, causing same-numbered PRs in different managed repos to collide on the same filename. Introduces the <owner>__<repo>__<pr>-<role>.approved scheme (AgDR-0060) with a single source-of-truth helper _lib-review-markers.sh so every reader (4 gate hooks) and writer (4 skills + 2 agents) uses the same qualified path. Adds a cross-repo collision regression test proving distinct, non-colliding markers for same-numbered PRs in different repos. Backward compat: new scheme only, no dual-read fallback. Markers are gitignored ephemeral session state; re-approval cost is one skill/agent invocation per affected session. SHA-match safety backstop unchanged. Closes #485 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
_lib-review-markers.sh— a single source of truth for marker path construction. The functionreview_marker_path <owner/repo> <pr> <role>returns<reviews-dir>/<owner>__<repo>__<pr>-<role>.approved, using double-underscores as a safe separator (GitHub slugs never contain__). Every reader and writer now sources this lib instead of assembling paths inline.block-unreviewed-merge.sh,require-design-review-for-ui.sh,require-architecture-review.sh,warn-stale-review-markers.sh) to compute repo-qualified marker paths. Each hook already extracted the repo from the merge command's--repoflag or API URL path; that extraction is now also used for marker addressing./approve-merge,/approve-design,/approve-architecture,/design-review; agents:code-reviewer(Rex),solution-architect(Tariq)) to document the qualified marker path scheme and source_lib-review-markers.shin their bash snippets.Backward compatibility
New scheme only — no dual-read fallback. Markers are gitignored ephemeral session state; the only cost of the hard-cutover is one re-approval invocation per session that had pre-existing markers. The SHA-match check in all gate hooks is unchanged and untouched — it remains the safety backstop that prevents a stale or mismatched marker from producing a wrong merge.
Two-reviews gate and SHA-check: unchanged in strength
The gate logic in
block-unreviewed-merge.shis structurally identical: still requires both Rex and CEO markers, still validatessha=,approved_by=user,skill_version>=2on the CEO marker, still resolves PR HEAD viagh pr viewand rejects on SHA mismatch. The only change is which path it looks for. The compound-command inline detection (introduced in #426) is preserved and updated to match both the new qualified basename and the old bare-number form as a belt-and-suspenders fallback for any skill code that has not yet been updated.Testing
Results:
Key regression test output:
Glossary
.claude/session/reviews/that records a per-PR approval (Rex / CEO / design / architecture). Gate hooks check these before allowing a merge.<owner>__<repo>__<pr>-<role>.approved— so the same PR number in two different repos maps to two distinct files.__separator/inowner/repo. GitHub slugs use only[a-zA-Z0-9._-], so__is unambiguous and can be split back to three components reliably._lib-review-markers.shreview_marker_pathandreview_markers_dir. All 4 gate hooks and 6 writers source it._lib-*prefix keeps it excluded from the hook count.429-rex.approvedfilename — a stale marker from one repo appearing as valid for the other.Closes #485