Skip to content

[US-234] feat: PR state flow (gate≠review) + pair review as required check - #390

Open
rucka wants to merge 6 commits into
mainfrom
feature/US-234-pr-state-flow-required-check
Open

[US-234] feat: PR state flow (gate≠review) + pair review as required check#390
rucka wants to merge 6 commits into
mainfrom
feature/US-234-pr-state-flow-required-check

Conversation

@rucka

@rucka rucka commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

What Changed

Introduces the PR state flow that separates mechanical gates (lint/type/build/tests) from judgment review (the pair-review verdict), and synthesizes both — plus tier and, at 🔴, explicit human approval — into a single PR macrostate: to-be-reviewedready-to-merge / not-approved. Registers pair-review (agent-published verdict) and pair-explicit-approval (deterministic host job) as required status checks so merge is blocked until gates are green AND the review is approved AND, at 🔴, a human has explicitly approved.

Why This Change

Today gate-passing and review-approval are not tied together as a single enforced merge condition — a green CI run says nothing about whether a human/agent judgment review happened, and there was no host-level mechanism preventing merge before that review lands. This closes that gap: the state model + required checks make "reviewed" a fact the code host enforces, not a label anyone could leave stale.

Story Context

User Story: As a maintainer, I want the PR's mergeability to reflect both passing gates and an actual review verdict (with explicit human approval at high risk), so a PR can't be merged just because CI is green.

Acceptance Criteria:

  • Gate ≠ review: mechanical checks and judgment review are modeled as separate, non-substitutable conditions.
  • Three PR states (to-be-reviewed, ready-to-merge, not-approved) synthesized from gates × review verdict × tier × explicit approval.
  • The pair review runs automatically at PR creation and is registered as a REQUIRED code-host check (merge blocked until approved).
  • At 🔴 tier, an explicit human approval (non-bot, non-author, on the current head commit) is required in addition to the review verdict.
  • Fail-safe direction is always "blocked" (pending/unknown ⇒ not merge-enabling; unknown tier ⇒ 🔴).

Changes Made

Implementation Details

  • T1 — PR state model + declarative synthesis evaluator: pr-states.md (new) is the single home of the flow — 3 states, gate≠review table, synthesis truth table, per-tier requirements read from quality-model §4 (no criteria duplicated, D18). pr-state.sh (new) is the provider-agnostic evaluator: resolve_pr_state, merge_allowed, explicit_approval_required, review_check_conclusion — signals + tags only, fail-safe on unknown/non-pass input. tech-debt counts as an approving verdict (matches review Step 5.3's TECH-DEBT→APPROVE mapping).
  • T2 — Review subagent at PR creation: publish-pr SKILL.md gains Phase 5 — registers pair-review pending on the head commit (merge blocked from t0), labels pr-state:to-be-reviewed, and dispatches /review $pr=<n> to an anonymous clean-context subagent (D23). Degraded path never runs the review inline in the authoring session.
  • T3 — Required-check + branch-protection setup: setup-gates SKILL.md gains Step 4.5 wiring pair-review + pair-explicit-approval as required checks (both pipeline modes, never tier-scoped away), plus stale-approval dismissal and a DEGRADED — enforcement advisory report with manual steps when the host/token lacks admin scope. github-implementation.md documents the host mechanics: check-run publication, the explicit-approval job (reads risk:* only, requires non-bot/non-author approval on the current head), and branch-protection payload.
  • T4 — Tier requirement enforcement + gate-first cap: process/review SKILL.md Step 2.1 gains a gate-first cap (a red gate can never yield APPROVED/TECH-DEBT, so synthesis never reaches ready-to-merge); new Step 5.4 publishes the pair-review conclusion and resolves tier requirements (reviewers/SLA/checklist/explicit approval) from quality-model §4, then swaps the pr-state:* label. merge-and-cascade.md gets a new Step 6.0 that re-synthesizes and HALTs rather than bypassing a required check.
  • T5 — Tests + docs + distribution: pr-state-flow.test.ts (new, 30 tests, written RED-first per the conformance ADL) covers every artifact above; pr-state-flow.sh smoke scenario (new, 55 assertions) executes the state machine end-to-end (tier × fail-safe × merge_allowed × conclusion mapping × D18 greps); docs site page concepts/pr-state-flow.mdx + catalog rows regenerated via generateCatalogRows(). Root .claude/skills + .pair/knowledge mirrors regenerated with the real copy pipeline (pair-cli update --offline) — not hand-edited — so the tech-debt: extend mirror-equality guard to per-skill SKILL.md pairs (all 36 skills) #352 per-skill byte-equality guard (PR [US-352] chore: extend mirror-equality guard to per-skill SKILL.md pairs #377) passes.

Review round 1 fixes (commit 94251636)

  • pair-review is published as a commit status, not a check run. POST /repos/{owner}/{repo}/check-runs is writable only by a GitHub App installation token — verified 403 on this repo with the token the skills actually hold — so the previously documented recipe could never publish, which would have made every PR permanently blocked once protection was applied. The recipe now uses POST /repos/{owner}/{repo}/statuses/{sha} (verified usable: 422 for a bogus SHA, not 403), which branch protection accepts as a required context identically. Token prerequisite (repo:status / statuses: write) and a "publication refused ⇒ advisory, reported" degradation are documented in github-implementation.md, publish-pr and review.
  • The 🔴 approval query works. gh pr view --json reviews has no author.is_bot field, so the old filter returned 0 for every review (verified on a real APPROVED review) and no 🔴 PR could ever pass. Replaced with the REST reviews endpoint carrying both fields: select(.state=="APPROVED" and .commit_id==env.HEAD_SHA and .user.type=="User" and .user.login!=env.PR_AUTHOR); the smoke audit token moved from is_bot to the new expression.
  • A 🔴 merge requires a second human account — the author cannot approve their own PR, so on a single-maintainer repo (this one) no 🔴 PR can satisfy pair-explicit-approval. The impossible "a solo maintainer satisfies it deliberately" claim is corrected in pr-states.md, pr-state-flow.mdx, ADR-018 and way-of-working.md; an alternative solo-approval token is explicitly deferred to its own story rather than half-specified.
  • pr-state:* labels are provisioned (gh label create × 3 in the guide) with a non-blocking degradation in both skills — they never auto-create.
  • Ordering constraint recorded (provision labels + add the job → observe both contexts reporting → only then PUT protection, enforce_admins last) in the guide, setup-gates Step 4.5 and this repo's way-of-working.md, which also states that the workflow is not yet added here. This repo's job is deliberately not added: with a single maintainer it would report red on every 🔴 PR forever (see the finding above).
  • Branch-protection payload: required_approving_review_count: 0 stated explicitly (the tier job is the approval authority; an unstated ≥1 default would contradict 🟢 self-merge) and strict: false with the rationale (per-head pair-review would have to be re-earned on every base update). Pending-verdict guard added to the publication snippet; least-privilege permissions: block added to the workflow template.
  • Doc coherence / cross-links: quality-model §4 and tier-aware-pipeline.md now carry the red-gate refinement clause; canonical-states.md back-links pr-states.md; the guidelines-catalog PM row links the PR State Flow page; how-to-11's decision table names the merge_allowed precondition; the dataset way-of-working.md line got a Status: [applied | not yet applied] placeholder; 🟡's "reviewer approval" is explicitly mapped to the pair review; the post-force-push re-run is stated as triggered, not automatic.

Files Changed

  • Added: pr-states.md, pr-state.sh, adr-018-pr-state-flow-required-checks.md, github-implementation.md, pr-state-flow.test.ts, pr-state-flow.sh (smoke), pr-state-flow.mdx (docs) — plus the mirrored copies under .pair/knowledge/**, .claude/skills/**, .pair/llms.txt (regenerated, never hand-authored).
  • Modified: publish-pr/SKILL.md (0.4.1→0.5.0), setup-gates/SKILL.md (0.5.0→0.6.0), process/review/SKILL.md (0.7.0→0.8.0), merge-and-cascade.md, quality-model.md, way-of-working.md, skills-catalog.mdx, tag-driven-gates.mdx, smoke-tests/run-all.sh (registers new scenario).

Testing

Test Coverage

  • Unit/Conformance Tests: 50 tests in pr-state-flow.test.ts (30 original + 20 added in review round 1, all written RED-first), part of knowledge-hub's passing suite.
  • Smoke Tests: pr-state-flow.sh exercises the state machine per tier + fail-safe + merge_allowed + conclusion mapping + doc/skill wiring; registered in run-all.sh.
  • Executed host assertions (round 1, read-only): the smoke scenario now calls the host, so the recipe cannot silently rot — (1) the commit-statuses surface is reachable with the ordinary agent token, (2) the REST reviews payload carries commit_id + user.type, (3) gh pr view --json reviews exposes no bot flag (the trap the guide warns about). They WARN-skip offline/unauthenticated, never fail. Manually executed against the live host for this round: POST …/check-runs403 "You must authenticate via a GitHub App"; POST …/statuses/<bogus-sha>422 "No commit found" (i.e. permitted); documented is_bot filter on a real APPROVED review ⇒ 0, corrected filter ⇒ 1. Only the branch-protection PUT needs admin scope and remains the human step.
  • End-to-End Tests: Website pnpm e2e — 39/39 passing (unaffected, run as part of the full 🔴-tier gate set).
  • Manual Testing: N/A — this is process/skill logic, verified via the conformance suite + smoke scenarios above.

Test Results

Test Suite: ✅ Passing (knowledge-hub, incl. 50 pr-state-flow tests + #352 mirror-equality guard)
Smoke scenarios: ✅ pr-state-flow.sh (new), tier-aware-gate.sh, links.sh — all PASS
Website E2E: ✅ 39/39
Coverage: maintains/improves baseline (coverage guardrail unaffected)
Linting: ✅ Clean
Quality Gate (pnpm quality-gate): ✅ PASS — ts:check + test + lint (10 packages), hygiene, docs:staleness, skills:conformance (40 skills), dup:check

Testing Strategy

  • Happy Path: green/yellow/red tier PRs synthesize the correct state as gates and review verdict land, per the truth table in pr-states.md.
  • Edge Cases: unknown/malformed risk:* tag (fail-safe → 🔴), missing review check (pending, not merge-enabling), tech-debt verdict (still approving), stale approval on a new head commit (dismissed).
  • Error Handling: any non-pass gate signal is treated as not merge-enabling (fail-safe direction is always "blocked"); host/token without admin scope degrades to an advisory report with manual steps, never a silent no-op.

Quality Assurance

Code Quality Checklist

  • Code follows established style guides and conventions
  • Functions and classes have appropriate documentation
  • Error handling implemented for edge cases (fail-safe defaults throughout pr-state.sh)
  • Security best practices followed (explicit-approval job requires non-bot/non-author — an agent cannot self-grant the 🔴 human gate)
  • Performance considerations addressed — n/a (declarative/skill-layer change, no runtime hot path)
  • No debugging code or console logs left behind

Deployment Information

Environment Impact

This PR changes process/skill documentation, a shell evaluator asset, and conformance/smoke tests only — no deployable application code. Nothing to release; the two required checks (pair-review, pair-explicit-approval) still need to be applied to this repo's branch protection, which requires admin scope on the code host and is called out below as a deliberate human step (see way-of-working.md's new line and the setup-gates DEGRADED report).

Deployment Notes

  • Feature Flags: none.
  • Dependencies: none added.
  • Manual step required: applying branch protection (making pair-review + pair-explicit-approval required checks) needs code-host admin scope — out of a code change's reach. Until applied, enforcement here is advisory (documented degraded mode in setup-gates' Step 4.5 report and the way-of-working.md status line).

Documentation

Documentation Updates

  • Technical Documentation: pr-states.md (flow spec), github-implementation.md (host mechanics), ADR-018 (design + rejected alternatives), docs site concepts/pr-state-flow.mdx + cross-link from tag-driven-gates.mdx.

Knowledge Sharing

  • Technical Decisions: ADR-018 — required code-host checks are the merge authority, pr-state:* labels are a view only (a forged/stale label cannot enable a merge). Two checks, not one: pair-review (verdict) + pair-explicit-approval (deterministic host job) so the reviewing agent can never self-grant the 🔴 human approval. Rejected alternatives: labels-as-policy (no enforcement, R5.7 violated) and a single combined check (collapses two authorities, hides which condition is unmet).

Reviewer Guide

Review Focus Areas

  1. State model correctness: pr-states.md's truth table and pr-state.sh's fail-safe defaults — especially that unknown/pending inputs never resolve to ready-to-merge.
  2. D18 compliance: the evaluator and docs read tier/thresholds only from tier-resolve.sh / quality-model §4 — no restated criteria.
  3. Gate-first cap in review SKILL.md Step 2.1: a red gate must cap the verdict below APPROVED/TECH-DEBT even if the reviewing agent would otherwise approve.
  4. ADR-018's two-check design: confirm the human-approval job genuinely cannot be satisfied by the reviewing agent itself (non-bot/non-author check).
  5. Mirror regeneration: root .claude/skills/** and .pair/knowledge/** mirrors were regenerated via pair-cli update --offline (never hand-edited) so the tech-debt: extend mirror-equality guard to per-skill SKILL.md pairs (all 36 skills) #352 per-skill byte-equality guard (PR [US-352] chore: extend mirror-equality guard to per-skill SKILL.md pairs #377) passes — worth a spot diff if in doubt.

Testing the Changes

pnpm --filter @pair/knowledge-hub test -- pr-state-flow
bash scripts/smoke-tests/scenarios/pr-state-flow.sh
pnpm quality-gate

Key Test Scenarios

  1. Green-tier PR with a passing pair-review check → synthesizes ready-to-merge without needing explicit approval.
  2. Red-tier PR with pair-review APPROVED but no explicit human approval yet → stays to-be-reviewed, merge_allowed is false.
  3. Any tier with a failing/pending gate → not-approved/to-be-reviewed regardless of review verdict (gate-first cap).

Dependencies & Related Work

Related PRs

Follow-up Work

  • Human step: apply branch protection on this repo (admin scope) to move pair-review + pair-explicit-approval from advisory to actually enforced — in the documented order (labels + workflow → observe the contexts → protect, enforce_admins last), tracked via the way-of-working.md status line until done.
  • Own story (design change, deliberately not in this PR): a solo-maintainer approval token (human-applied approved:human label with actor verification, or an /approve comment command) so a single-account repo can satisfy the 🔴 explicit-approval rule without a second human account.

Type emphasis: Feature — user impact (unenforceable review → host-enforced merge gate) + technical decisions (ADR-018's two-check split) are the focus.

@rucka rucka added the risk:red Classification: high risk tier label Jul 28, 2026
@github-actions
github-actions Bot temporarily deployed to Website Preview July 28, 2026 20:11 Inactive
@rucka

rucka commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict

risk:red · cost:greenCHANGES-REQUESTED — model, evaluator and docs are solid, but the two host commands the enforcement actually runs on are both broken as written (check-run publication 403s with normal gh auth; the human-approval query always returns 0), so applying the documented protection would make every PR — and every 🔴 PR in particular — permanently unmergeable.

PR: #390 · Author: rucka · Reviewer: independent review agent · Date: 2026-07-28 · Story: US-234 · Type: feature

Classification matrix — per dimension
Dimension Tier Source Note
Service/domain criticality yellow KB default (no Criticality Table) unchanged from refinement
Change/diff risk red diff footprint branch-protection/required-check provisioning recipe + PR state machine + 3 SKILL.md contracts
Business impact red subdomain class merge/review enforcement is a core pair capability
Security relevance yellow /pair-capability-assess-security (raise-only, D17) authorization surface added; controls fail closed — no introduced vulnerability, but the 🔴 control is non-functional (Critical 2)
Coupling balance green /pair-capability-assess-coupling ($scope: diff) one new KB guideline + one shared shell asset consumed by 3 skills; strength=shared-asset, distance=same repo/team, volatility=low — balanced

Tier = max(assessed) = risk:red (confirms the refinement-time tier, no drift). Cost = highest detected signal = green.

Assessments

Security — Input validation

Verdict: green — the only external inputs (PR labels, PR author, head SHA) reach the shell through env: rather than ${{ }} interpolation inside run:, so the template is not script-injectable; resolve_tier fail-safes a malformed risk:* label to red.

Details
  • github-implementation.md:520-533PR_LABELS, PR_AUTHOR, HEAD_SHA are passed via env:; no expression interpolation inside the run: block. This is the correct GitHub Actions hardening pattern and worth keeping.
  • pr-state.sh rejects every unrecognized gate/review/tier value toward "blocked".

Security — Output handling

Verdict: green — no rendering surface touched; the only outputs are check-run summaries and ::error:: annotations.

Security — Authentication

Verdict: yellow — the recipe assumes the ambient gh token can create check runs, which is only true for a GitHub App / Actions GITHUB_TOKEN with checks: write; the prerequisite is undocumented and there is no degradation for a refused publication (Critical 1).

Security — Authorization

Verdict: yellow — the two-check split (agent verdict vs deterministic human-approval job) is the right authorization design and the reviewing agent genuinely cannot self-grant the 🔴 gate; but as shipped the 🔴 control can never be satisfied at all (Critical 2, Major 3), i.e. it fails closed and is unusable rather than unsafe.

Security — Introduced vulnerabilities

Verdict: green — 0 introduced, 0 pre-existing.

Details

No pull_request_target, no secret echo, no ${{ }} in run:, no privilege widening. Every failure mode found during review resolves to "merge blocked", never "merge allowed".

Cost

Verdict: cost:green — no new paid integration, no new dependency; two extra CI checks per PR (one trivial shell job) on the existing runner budget.

Architecture (Coupling)

Verdict: green — the synthesis lives once in pr-state.sh and is read by publish-pr / review / a host job; per-tier thresholds are read from quality-model §4 rather than copied (D18 respected, grep-verified by the smoke scenario).

Details

Findings by severity

Critical (must fix before merge)

  • packages/knowledge-hub/dataset/.pair/knowledge/guidelines/collaboration/project-management-tool/github-implementation.md:486 (and :494) — The documented pair-review publication command (gh api repos/$OWNER/$REPO/check-runs -X POST …) cannot be executed by the skills that are told to run it. Verified against this repo: POST /repos/foomakers/pair/check-runs403 {"message":"You must authenticate via a GitHub App."}. publish-pr Phase 5 step 3 (.skills/capability/publish-pr/SKILL.md:102) and review Step 5.4 step 3 (.skills/process/review/SKILL.md:251) both run agent-side with ordinary user auth, so pair-review is never published. Once the documented branch protection is applied, the required context never reports → every PR is permanently blocked, and "enforce_admins": true in the same payload (:555) removes the admin escape hatch. AC1 ("its check is registered") and AC5 are therefore unachievable as documented, and the failure mode is not in the degraded-mode section (which only covers "no permission to write branch protection"). Fix: publish via the commit-statuses API instead — gh api "repos/$OWNER/$REPO/statuses/$HEAD_SHA" -X POST -f state=pending|success|failure -f context=pair-review (verified usable with the same token: returns 422 for a bogus SHA, not 403), which branch protection accepts as a required context identically; or keep check-runs and document that publication must happen inside a workflow with permissions: checks: write plus the relay that gets the verdict there. Either way add the token prerequisite and a "publication refused ⇒ advisory, reported" degradation.
  • …/github-implementation.md:536-538 — The pair-explicit-approval job's approval query is always 0. gh pr view --json reviews exposes author.login only — there is no author.is_bot field — so select(… .author.is_bot==false …) filters out every review. Verified on a real APPROVED review (cli/cli#13981): the documented filter returns 0, the same filter without the is_bot clause returns 1. Impact: at 🔴 (and at untagged ⇒ 🔴) the job fails even when a genuine human approval exists on the current head, so AC4's "explicit approval satisfies the requirement" branch can never pass and 🔴 PRs are unmergeable. The bug is additionally pinned by a test: scripts/smoke-tests/scenarios/pr-state-flow.sh:168 audits the guide for the literal is_bot. Fix: use the REST reviews endpoint, which does carry both fields — gh api "repos/$OWNER/$REPO/pulls/$PR/reviews" --jq '[.[] | select(.state=="APPROVED" and .commit_id==env.HEAD_SHA and .user.type=="User" and .user.login!=env.PR_AUTHOR)] | length' — and update the smoke-test audit token accordingly.

Major (should fix before merge)

  • …/project-management-tool/pr-states.md:79 — The self-authored/solo-maintainer edge case is stated as satisfiable but is not: GitHub rejects an approving review from the PR author, and the job requires a non-author human, so on a single-maintainer repo (this one — every PR here is authored by the same human) no 🔴 PR can ever reach ready-to-merge. The row currently claims "at 🔴 the explicit human approval requirement is what a solo maintainer satisfies deliberately", which is impossible via a review approval; ADR-018's consequences repeat the assumption. Fix (in this PR): correct the claim in pr-states.md, the docs page (pr-state-flow.mdx:38) and ADR-018 — state plainly that 🔴 merges require a second human account. Designing an alternative solo-approval token (e.g. a human-applied approved:human label whose actor is verified, or an /approve comment command) is a design change large enough for its own story; do not silently leave the impossible claim in the KB either way.
  • .skills/capability/publish-pr/SKILL.md:103 + .skills/process/review/SKILL.md:253 — Nothing provisions the pr-state:* label family. Both skills apply/require exactly one pr-state:to-be-reviewed|ready-to-merge|not-approved label, but no artifact in this PR creates those labels (contrast guidelines/collaboration/issue-management/github-issues.md:33-37, which documents gh label create for every label family the KB uses); gh label list on this repo returns only risk:green|yellow|red. So on first run the label call fails (gh pr edit --add-label → "not found"), and unlike classify (which declares "PM tool lacks label-API access ⇒ reported, non-blocking") neither skill has a degradation for it — while review Step 5.4's Verify hard-requires the label to be present. Fix: add the three gh label create pr-state:… --color … lines to the new github-implementation.md section, and a non-blocking degradation line ("label absent / no label API ⇒ report; the checks remain the authority") to both skills.
  • PR-wide (verification strategy) — The story's DoD ("Merge-block verified on the real code host for: red gate, missing review check, 🔴 without explicit approval") and its stated acceptance-testing approach (one PR per tier on a sandbox repo) were not executed; verification covers doc-content invariants (30 conformance tests) plus the pure shell evaluator (55 smoke assertions) only. That is exactly the gap that let both Critical findings through — each was reproducible with one command against the live host. Fix: exercise the two host interactions against a real PR/sandbox before merge (publishing the status/check and querying reviews need no admin scope; only the branch-protection PUT does) and record the result in the PR.

Minor (consider)

  • …/github-implementation.md:493-497 — The verdict-publication snippet passes review_check_conclusion's output straight into -f status='completed' -f conclusion="$CONCLUSION", but pending is not a valid check-run conclusion (GitHub → 422). The prose immediately below says not to publish a completed check for it; the copy-pasteable code doesn't implement it. Add the guard: [ "$CONCLUSION" = pending ] && { echo "no decision yet — leaving the queued run in place"; exit 0; }.
  • …/github-implementation.md:551-554 — The branch-protection payload enables required_pull_request_reviews with only dismiss_stale_reviews: true, leaving required_approving_review_count to an unstated API default. If that resolves to ≥1, every PR (including 🟢) requires a human approving review, which contradicts quality-model §4's 🟢 "self-merge once gate checks are green" row and makes the tier-scoped pair-explicit-approval job redundant. "strict": true is also introduced without discussion: it forces branch-up-to-date, and since updating the branch changes the head, the per-head pair-review check must be re-earned (a fresh agent review) on every base update — churn/livelock risk on a busy main. State the review count explicitly and document (or drop) strict.
  • .pair/adoption/tech/way-of-working.md:41 — Dogfooding/ordering gap: the pair-explicit-approval job exists only as a doc template; no .github/workflows/* job is added here, even though writing the workflow needs no admin scope (only the protection PUT does — cf. the coverage guardrail, which this repo does dogfood). If a human applies the documented protection before the job exists, the required context never reports and all merges stop. Either add the job now or record the ordering constraint ("add the job, confirm it reports on a PR, then apply protection") next to the "not yet applied" status line.
  • guidelines/quality-assurance/quality-model.md:74 / guidelines/infrastructure/cicd-strategy/tier-aware-pipeline.md:204 vs pr-states.md:29 — Doc coherence: the two pre-existing statements say review "starts only once gates are green", while the new model (and review Step 2.1) has the review run and report findings at red gates with a capped verdict. Reconcile in one place — e.g. a clause in §4 pointing at pr-states.md's refinement — so the two readings can't be cited against each other.
  • apps/website/content/docs/reference/guidelines-catalog.mdx:73, guidelines/…/canonical-states.md, how-to/11-how-to-code-review.md:102 — Cross-link asymmetry: pr-states.md declares itself the pull-request companion to canonical-states.md but gets no back-link; the guidelines-catalog "Project Management Tools" row links canonical-states and DoR/DoD but not the new PR State Flow page; how-to-11 still maps APPROVED → squash merge with no mention of the merge_allowed precondition or the required checks. Three one-line additions.
  • packages/knowledge-hub/dataset/.pair/adoption/tech/way-of-working.md:18 — The dataset (template) line asserts the two checks "are required status checks on the protected branch" as fact for every new project, with no status qualifier — the root copy correctly says "not yet applied". A freshly bootstrapped project ships an untrue statement until setup-gates runs. Give the template a status placeholder (Status: [applied | not yet applied]), mirroring the root wording.
  • …/github-implementation.md:516-520 — The workflow template declares no permissions: block; least privilege would pin pull-requests: read (plus checks: write if the same workflow ever publishes pair-review). This repo's own workflows declare permissions where they matter.

Questions

  • pr-states.md:40 (synthesis table, 🟡 row) — At 🟡 the table makes green gates + an AI APPROVED verdict sufficient for ready-to-merge with no human in the loop, while quality-model §4's 🟡 row reads "1 reviewer / reviewer approval". Is the pair review intended to be 🟡's reviewer approval (in which case 🟡 differs from 🟢 only in gate breadth, and that should be stated in "Per-tier requirements are read, never restated")? Or does 🟡 need its own approval condition in the truth table? Right now the model and the quality model can be read two ways.
  • pr-states.md:76 (force-push edge case) — The story's edge case says the review "re-runs on the new head", which reads automatic; as documented the re-run is manual (re-invoke publish-pr/review) because nothing host-side re-registers pair-review as pending on synchronize. The direction is fail-safe (merge stays blocked), but is a synchronize-triggered re-registration intended, or should the edge-case row say the re-run is human/skill-triggered?
Positive feedback
  • ADR-018 is a genuinely good ADR: three options with concrete, falsifiable cons; the "two checks, not one" rationale (an agent must not be able to assert the human approval) is the strongest part of the design and is carried consistently into the skills.
  • pr-state.sh is small, pure and fail-safe in every branch — unknown gate ⇒ blocked, unknown tier ⇒ red, merge_allowed passes only ready-to-merge, and every rejection prints its reason to stderr. Independently re-ran the smoke scenario: 55/55 assertions pass. Re-ran the new conformance file: 30/30 pass (and 419 non-mirror knowledge-hub tests pass against this branch).
  • D18 discipline holds: the evaluator and both docs read the risk:* tag through tier-resolve.sh and defer every threshold to quality-model §4; the smoke scenario greps the code blocks for criteria tokens, so the invariant is machine-enforced rather than asserted.
  • Mirrors are real regenerations, not hand edits: spot-diffed all three changed SKILL.md pairs plus merge-and-cascade.md and the .pair/knowledge copies — the only deltas are the pipeline's skill-name prefixing and link normalization.
  • Injection-safe workflow template (env: instead of ${{ }} inside run:) — the right pattern, kept even in a doc snippet.
Functionality & requirements (AC coverage)
AC Status Evidence
AC1 — review dispatched + check registered at PR creation ⚠️ partial publish-pr Phase 5 documents pending-check + clean-context subagent dispatch; the check registration itself cannot execute (Critical 1)
AC2 — red gate ⇒ no merge-enabling verdict review Step 2.1 gate-first cap; pr-state.sh gate-first branch; smoke assertions for fail/pending/unknown gates
AC3 — synthesis ⇒ ready-to-merge / not-approved truth table + resolve_pr_state; 🟢/🟡 and changes-requested paths asserted
AC4 — 🔴 blocked without explicit human approval ⚠️ partial blocking half correct; the satisfying half never passes (Critical 2) and is impossible for a solo maintainer (Major 3)
AC5 — required check blocks merge on the host ⚠️ partial model, setup-gates Step 4.5 and protection payload documented; unverified on a real host and unpublishable as written (Critical 1, Major 5)
  • Error handling: fail-safe direction is consistently "blocked" — no path found where a broken signal yields a merge.
Testing & quality gates
  • Conformance: 30 new tests, all passing (independently re-run against this branch); test count matches the PR description.
  • Smoke: new pr-state-flow.sh, 55 assertions, all passing; registered in both the CI list and local discovery; file mode 755 (correct).
  • Gap: all verification is content/regex + pure-shell. Nothing exercises the two host commands, which is why both Critical findings are invisible to the suite — and one test (pr-state-flow.sh:168) actively pins the broken is_bot predicate. Add at least one executed check against the live host (see Major 5).
  • Gates: pnpm quality-gate reported PASS by the author; not re-run in full here (no install in the read-only review worktree), but the knowledge-hub tests that do not need workspace deps pass (419/419) and the mirror-equality guard's inputs were verified by hand.
Adoption compliance
  • Degradation level 1 (verify-adoption + assess-stack available).
  • No new dependency; tech-stack.md untouched — correct.
  • ADR present for the new decision (ADR-018) and referenced from way-of-working.md — the HALT-on-missing-ADR condition does not fire.
  • Conventions respected: conformance-test-per-artifact ADL, gate-tooling ADL (shell verified by smoke, not vitest), asset file mode 644 for sourced helpers, pr-state.sh alongside tier-resolve.sh/coverage-gate.sh.
  • Distribution: new files land under directory-mirrored registries in apps/pair-cli/config.json, so no CLI change is required — the DoD's CLI item is legitimately N/A.
Tech debt
Documentation
  • New KB guideline, ADR, host implementation section, docs-site concept page + cross-link from tag-driven-gates, llms.txt entry, PM-tool README entry, skills-catalog rows regenerated (the review skill's row correctly unchanged — the generator uses the first sentence only). All relative links in the new artifacts resolve; all four docs-site link targets exist.
  • Missing: the three cross-links in Minor Advanced Features Enablement #5 above, and the label-provisioning recipe (Major 4).
Performance & deployment
  • No runtime hot path; two extra CI checks per PR (one trivial job).
  • Deployment: nothing to release. The remaining human step (branch-protection PUT) is correctly disclosed in the PR and recorded in way-of-working.md as "not yet applied" — but see Minor Adoption Management & Guideline Linking #3 for the ordering trap (protection before the job/publication path exists blocks all merges) and Critical 1 for why the publication path must be fixed before anyone applies it.

@github-actions
github-actions Bot temporarily deployed to Website Preview July 28, 2026 20:43 Inactive
@rucka

This comment has been minimized.

@rucka

This comment has been minimized.

@rucka

This comment has been minimized.

@github-actions
github-actions Bot temporarily deployed to Website Preview July 30, 2026 07:41 Inactive
@github-actions
github-actions Bot temporarily deployed to Website Preview July 30, 2026 08:24 Inactive
@rucka

rucka commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

Remediation — final (verified at head 6ba4f82a)

All 14 findings (4 Major + 10 Minor) are closed at the current head. Verdict requested: APPROVED.

Why the review above still shows them open: it was posted 2026-07-30 08:57 CEST, while the round-2 and round-3 fixes landed at 09:35 and 10:23. It re-reviewed the round-1 head from scratch — an artefact of the orchestration being stopped and resumed, which re-posted a first review instead of continuing the cycle. Each finding was re-checked against the tree rather than assumed closed.

Major

  • The approval job could be tampered with by the PR it authorizes. The recipe now uses pull_request_target (base version of the workflow) and pins actions/checkout to github.event.pull_request.base.sha, so the sourced explicit_approval_required() / pr-state.sh are base versions. The doc states the bypass was reproduced (a risk:red PR shipping a neutered predicate published success on its own head) and distinguishes this from workflows whose tampering only narrows a test matrix — here it would remove an authorization control. The pin is also justified for the pull_request_review trigger, where the workflow file comes from the default branch but GITHUB_REF is refs/pull/<n>/merge.
  • Nested subagent dispatch made the primary path unreachable. /publish-pr Phase 5 no longer nests: running inside a subagent it emits Review: review-dispatch-required — /review $pr=<n> and returns, and the caller (/implement Step 3.3, top-level session) spawns the anonymous review subagent. The pair-review check is already pending before the return, so the merge stays blocked across the handoff — nothing is lost.
  • The dispatch prompt had no "never merge" constraint. /review now has an explicit non-interactive (dispatched) contract: a $dispatched run does not ask and does not self-answer Step 1.4 or Step 5.5, the Step 5.5 outcome is "the human merges", and Phase 6 is unreachable by contract — "even on APPROVED with merge_allowed true". The contract holds even if the dispatch instruction omits it, so the guarantee does not depend on prompt wording.
  • DoD — merge-block never verified on a real host. Now executed end-to-end on a disposable repository (§ Verified on a throwaway repository, 2026-07-30): labels → workflow → contexts observed → protection PUT with enforce_admins: true → one PR per tier, with the results tabulated per scenario. It covers precisely the two parts the finding called most failure-prone: a required status context does block (🔴, no pair-review published at all → blocked), and the approval-time re-run re-reports on the same head SHA, where protection reads it.

Minor

  • $REPO ambiguity resolved — one form throughout (repos/$REPO), no mixed repos/$OWNER/$REPO.
  • enforce_admins — the copy-paste payload ships false, with the bullet explaining it is deliberate and flipped to true only in ordering step 4, after one PR has merged through the rule.
  • Unbounded ?per_page=100 review read — gone.
  • pr-states.md — the self-contradicting "does not copy those thresholds" sentence is gone.
  • extended checklist — now defined in the KB (quality-model.md, verify-done, review), so Step 5.4's instruction resolves.
  • pr-state-flow.sh:91 — the byte-identical assertion is replaced by a real transition case (same PR, gates green and review approved throughout, tier raised).
  • pr-state-flow.sh:229 — the hard-coded third-party PR fallback (cli/cli#13981) is gone.
  • Questions on head-SHA association and reviewer-account independence — both answered in the doc (measured, and the same-account degradation to --comment documented).

Verification

599/599 knowledge-hub · PR State Flow (gate != review) smoke suite passed · CI secret-scan/preview/build all SUCCESS on 6ba4f82a.

One thing for the merge gate

This PR makes pair-review and pair-explicit-approval required contexts. Once it merges, every subsequent PR — including the other five in this batch — is evaluated under the new regime. The recipe is deliberately ordered (protection applied after both contexts have reported on a real PR, enforce_admins flipped last) precisely so that applying it cannot leave every PR permanently unmergeable. Worth merging this one last in the batch, or knowingly accepting that the others land under the new rules.

@rucka rucka self-assigned this Jul 30, 2026
rucka added a commit that referenced this pull request Jul 30, 2026
…te no longer depends on the caller

Two defects observed driving the #277/#230/#281/#236/#234/#279 batch.

1. A batch that drove NOTHING reported success. `args` was normalised with
   JSON.parse in a try/catch whose catch set `_args = undefined`, so
   `STORIES = _args?.stories ?? []` silently became empty: a run invoked with
   `args: "#234 #236 …"` exited in ~30ms with zero agents and returned
   `{batch: [], note: 'PRs are ready-for-merge or escalated…'}` — the shape of a
   COMPLETED batch, indistinguishable from a real run whose stories all failed.
   Now validated loudly: a non-JSON string, absent args, an object without
   `stories`, or a story missing id/title/branch throws with a message naming the
   required shape and why title/branch cannot be derived (no gh/fs in the sandbox).
   An EXPLICITLY empty list stays a legal no-op — a computed 'nothing to do' is
   not a mistake. A bare array and a JSON string are both still accepted, and a
   leading '#' on an id is normalised so no shell path carries it.

2. The duplicate-first-review guard depended on the caller's bookkeeping. The
   #373 continuation probe sat inside `if (resuming)`, and `resuming` is
   `Number.isInteger(story.prNumber)` — so it only ran when the caller passed
   `prNumber`. A `Workflow({resumeFromRunId})` resume replays implement/PR from
   cache with the SAME args, so `story.prNumber` is absent, the probe never ran,
   `firstReviewPosted` stayed false, and round-0 posted ANOTHER first review on a
   PR that already had one — three times on story #234 (PR #390) across three
   pause/resume cycles, which is why that story ended the batch least-progressed
   with 4 Major still open while the others were at zero.
   The gate is now the PR's existence, a fact the script knows. One sonnet/low
   probe per story per run is far cheaper than one duplicated opus/xhigh review,
   and on a fresh story both signals return false so the fresh path is unchanged.

Also: the args contract is now in meta.whenToUse, so a caller reads it BEFORE
invoking — the skill wrapper interpolates received arguments verbatim into
`Workflow({args})`, which is how the wrong shape was reachable by construction.

Tests 46 -> 55. One pre-existing assertion was INVERTED on purpose: '#373 fresh
story: no continuation probe runs' pinned the cost saving that caused defect 2.
Its real intent (fresh path unchanged) is preserved and now asserted on the
OUTCOME — the first review still posts — plus a new test proving a garbage probe
return cannot silence it (fail-open).

Adoption: way-of-working.md now records the assignment rule — every story,
tech-debt, epic and PR that is worked on carries an assignee. Same class of
tracking hole as the two defects above: the board is read filtered by assignee,
so an unassigned item is invisible there even when open, PR'd and green. The
follow-ups filed *during* a batch are the ones most often left orphaned, and a
PR with an author but no assignee does not show up either.

Closes #401

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rucka added a commit that referenced this pull request Jul 31, 2026
…doption (T-6)

Surgical edits only — three files carry live hunks from parked PRs #389/#390,
so each change is a single-line replacement with no reflow or reordering.

- way-of-working.md: `Coverage baseline commit-back: disabled` appended to the
  existing Coverage guardrail bullet, with the credential requirement and why
  it stays off here until #234's protection lands and the secret exists
- tier-aware-pipeline.md (both corpora): the nested opt-in, the push-not-PR
  trigger, the bot-PR landing, monotonicity, termination and the
  degrade-to-warning contract
- coverage-baseline.md: what writes to this file and when, and that a human
  remains the only writer while the flag is disabled

Two dead markdown links removed: both pointed at
adr-018-pr-state-flow-required-checks.md, which exists only in parked PR #390.
The local gate does not catch these — the broken-.md-link check is an e2e test,
outside the local gate set — so they would have surfaced in CI or, worse, only
once #390 merged.

Refs #372

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rucka added a commit that referenced this pull request Jul 31, 2026
…r capability yet (M4)

The shipped, adopter-facing KB documented the flag, the trigger, the bot-PR
landing and the credential as if an adopter could enable it. They cannot: the
logic lives in packages/knowledge-hub/src/tools/ (pair-internal — the shipped
assets are coverage-gate.sh and tier-resolve.sh), and /pair-capability-setup-gates
never asks about the nested flag, never records it and never emits the step. An
adopter setting `Coverage baseline commit-back: enabled` got a SILENT NO-OP —
the worst shape for an opt-in: config says on, docs say what it does, nothing
happens, nothing complains.

Both the guideline and the config example now state the pair-internal scope, in
both corpora, and point at #409 for the adopter path.

Deliberately NOT fixed here: the stale "automated commit-back is story #372"
note in setup-gates/SKILL.md:199. It is one line, but that file is modified by
BOTH parked PRs #389 and #390 — a three-way contention in a PR whose
disjointness is a stated business rule. It belongs to #409, which has to edit
that skill anyway to add the flag to the interview.

Refs #372 · #409

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rucka added a commit that referenced this pull request Jul 31, 2026
… as a bot PR, opt-in and off by default (#405)

* [US-372] docs: ADL — ratchet lands as a bot PR, never a base push

Decide AC5 (PR vs push) and the credential model against the POST-#234
regime, not today's unprotected main:
- a pull_request run NEVER writes (uniform for forks; no head-SHA
  mutation, so a human approval is never invalidated)
- the post-merge push proposes the raise as a bot PR from
  chore/coverage-baseline-ratchet — no branch-protection bypass is
  requested, which is the point of #234
- credential: repo-scoped COVERAGE_RATCHET_TOKEN (contents:write +
  pull-requests:write, no admin, no bypass list); GITHUB_TOKEN rejected
  because a PR it opens triggers no run and can never satisfy the
  required contexts
- loop termination: marker predicate PLUS a floor(measured)-1pp fixpoint;
  [skip ci] rejected (it would skip the checks the ratchet PR must pass)
- rejected options recorded with the reason each fails

- Task: T-1 — Decide and record PR-vs-push behaviour + credential model

Refs: #372

* [US-372] feat: monotonic coverage-baseline ratchet + opt-in CI wiring

Gate logic in a tested module (ADL 2026-07-13); ci.yml stays a thin
entrypoint that only forwards the measured numbers and exits 0.

- ratchet writer: raises `^baseline.<type>=` values IN PLACE, never
  rewrites the surrounding markdown; only a value strictly above the
  committed one is a raise (equal/drop/unknown-type/malformed all hold)
- concurrency: the config is re-read immediately before writing and every
  proposal re-checked, so a higher value from a concurrent run is dropped,
  never clobbered
- skip predicate: flag-disabled (default) / not-base-push (every PR run)
  / automated-commit (marker or ratchet branch in the head commit)
- git plan is data, so its non-negotiables are asserted by tests: one
  push, to the ratchet ref only, no `git add -A`, no branch switch,
  always restored to the checked-out SHA
- push uses --force-with-lease AND first maps+fetches a remote-tracking
  ref for the ratchet branch: without it git rejects every later
  non-fast-forward push as `stale info` (verified), so the ratchet would
  have worked exactly once and then warned forever
- refused write => named ::warning:: and exit 0; the guardrail's verdict
  is untouched (step runs after it)
- token bound to the base-branch push event in ci.yml, so a PR run never
  has the credential in its environment
- smoke scenario DEMONSTRATES loop termination as a real CLI sequence
  (marked commit, merge commit, and the fixpoint that needs no marker)

- Tasks: T-2 ratchet writer, T-3 flag + CI wiring, T-4 loop termination,
  T-5 degradation on refused write

Refs: #372

* [US-372] docs: document the ratchet flag, token and default in KB + adoption (T-6)

Surgical edits only — three files carry live hunks from parked PRs #389/#390,
so each change is a single-line replacement with no reflow or reordering.

- way-of-working.md: `Coverage baseline commit-back: disabled` appended to the
  existing Coverage guardrail bullet, with the credential requirement and why
  it stays off here until #234's protection lands and the secret exists
- tier-aware-pipeline.md (both corpora): the nested opt-in, the push-not-PR
  trigger, the bot-PR landing, monotonicity, termination and the
  degrade-to-warning contract
- coverage-baseline.md: what writes to this file and when, and that a human
  remains the only writer while the flag is disabled

Two dead markdown links removed: both pointed at
adr-018-pr-state-flow-required-checks.md, which exists only in parked PR #390.
The local gate does not catch these — the broken-.md-link check is an e2e test,
outside the local gate set — so they would have surfaced in CI or, worse, only
once #390 merged.

Refs #372

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [US-372] fix: review round 1 — credential layering, lease-safe write, guarded I/O, anchored flags

Code-side findings from PR #405's review. Work started by an implementer the
watchdog killed; recovered from the worktree, verified, completed and refactored
rather than redone.

M1 credential layering — `extraHeader` is MULTI-valued and `actions/checkout`
already persists the read-only GITHUB_TOKEN into the same key, so ADDING ours
made git send two Authorization headers and left the winner to the server. The
likely loser was ours: a refusal that AC6 turns into a warning on every push —
a feature that never works and never says so. `gitAuthConfig` +
`EXTRAHEADER_CONFIG_KEY` now override the single key instead of layering.

M2 unguarded happy path — every file/exec call could throw and exit non-zero,
contradicting AC6's own rule that persistence never changes the gate's verdict.
`main` now funnels everything except an argument-parsing bug (a workflow-authoring
error, which must be loud) into the same warning + exit 0.

M3 concurrent clobber — the story's exceptional scenario was unimplemented.
`applyRaises` re-checks every proposal against BOTH the text on disk and the
config as committed on the open ratchet branch, so a higher value written by a
concurrent run is never clobbered on either ref.

Minor — the flag parse was an unanchored whole-document regex, first match wins,
and the KB sentence this PR ships contains the literal flag string: an adopter
quoting that sentence would have turned the flag ON. Now anchored to a list
bullet. `readGuardrailFlag` makes the "nested under the guardrail" framing
enforced rather than documentation-only. The loop guard no longer fires on a
commit message that merely mentions the ratchet branch. `git config --add
remote.fetch` no longer mutates the checkout's persisted config, so the plan's
"leaves no trace" is now true; `restore` is `--mixed` + a checkout of the one
edited path, not `--hard`, so it cannot revert unrelated tracked work before the
E2E step. `--margin` and the measured values are validated instead of silently
becoming NaN (which would have made the ratchet never raise, quietly).

`ratchetGitPlan` split via `ratchetWriteCommands` — it had grown to 64 lines
against a 50-line ceiling. Caught by the pre-push gate, not by the test run:
vitest was green while lint was red, the same trap a PR fell into earlier today.

80 tests (was 70). `pnpm quality-gate` green.

Refs #372

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [US-372] docs: state that the ratchet is pair-internal, not an adopter capability yet (M4)

The shipped, adopter-facing KB documented the flag, the trigger, the bot-PR
landing and the credential as if an adopter could enable it. They cannot: the
logic lives in packages/knowledge-hub/src/tools/ (pair-internal — the shipped
assets are coverage-gate.sh and tier-resolve.sh), and /pair-capability-setup-gates
never asks about the nested flag, never records it and never emits the step. An
adopter setting `Coverage baseline commit-back: enabled` got a SILENT NO-OP —
the worst shape for an opt-in: config says on, docs say what it does, nothing
happens, nothing complains.

Both the guideline and the config example now state the pair-internal scope, in
both corpora, and point at #409 for the adopter path.

Deliberately NOT fixed here: the stale "automated commit-back is story #372"
note in setup-gates/SKILL.md:199. It is one line, but that file is modified by
BOTH parked PRs #389 and #390 — a three-way contention in a PR whose
disjointness is a stated business rule. It belongs to #409, which has to edit
that skill anyway to add the flag to the interview.

Refs #372 · #409

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [US-372] fix: validate coverage values before writing them to GITHUB_ENV

`$GITHUB_ENV` is the classic Actions environment-injection sink: a value
carrying a newline defines arbitrary further variables for every later step in
the job. The two measured percentages were written unvalidated.

These come from a coverage report, so a stray value is a tooling bug rather
than an attack — but the guard is one case statement and removes the sink
outright. A non-numeric value is dropped with a `::warning::`, so the ratchet
sees no measurement and proposes nothing: the conservative outcome, and it
still cannot block the gate (AC6).

Verified: a value of `20\nEVIL=1` is rejected and only the legitimate `85.04`
reaches the file; `sh -n` clean on the snippet.

Refs #372

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@rucka
rucka force-pushed the feature/US-234-pr-state-flow-required-check branch from 6ba4f82 to f3455bf Compare July 31, 2026 18:16
@github-actions
github-actions Bot temporarily deployed to Website Preview July 31, 2026 18:17 Inactive
@github-actions
github-actions Bot temporarily deployed to Website Preview July 31, 2026 18:19 Inactive
rucka and others added 3 commits August 1, 2026 10:13
…check

- pr-states.md: 3 PR states (to-be-reviewed → ready-to-merge / not-approved), synthesis table (gates × verdict × tier × explicit approval), enforcement layers, edge cases, degraded mode
- assets/pr-state.sh: provider-agnostic evaluator (resolve_pr_state, merge_allowed, explicit_approval_required, review_check_conclusion) — tags/signals only, no criteria (D18), fail-safe red
- publish-pr: registers required pair-review check pending at creation + labels pr-state:to-be-reviewed + dispatches review to a clean-context subagent (AC1)
- review: gate-first cap on the verdict (AC2), Step 5.4 publishes the check + synthesizes the state + reads per-tier reqs from quality-model §4 (AC3/AC4), Phase 6 merge_allowed precondition
- setup-gates: Step 4.5 wires pair-review + pair-explicit-approval required checks + stale-approval dismissal, DEGRADED report when no host API (AC5)
- github-implementation: check-run publication, explicit-approval job (human, non-author, current head), branch-protection payload, manual degraded steps (R2.12)
- ADR-018: required checks are the merge authority, pr-state labels are a view
- tests: conformance pr-state-flow.test.ts (RED→GREEN) + smoke scenario executing the state machine per tier
- docs: concepts/pr-state-flow page + catalog rows regenerated; root .pair/.claude mirrors via pair update

Refs: #234
…ST reviews) + doc coherence

Critical
- pair-review published via the commit-statuses API, not check-runs: POST /check-runs is
  GitHub-App-only (verified 403 on this repo) while the skills run with an ordinary token;
  statuses accepts it and branch protection treats the context identically. Token prereq
  (repo:status) + "publication refused ⇒ advisory, reported" degradation added.
- pair-explicit-approval approval query rewritten on the REST reviews endpoint
  (commit_id + user.type=="User"): gh pr view --json reviews has no author.is_bot, so the
  old filter always returned 0 and no red PR could ever pass. Smoke audit token updated.

Major
- solo maintainer: a red merge needs a SECOND human account (author cannot self-approve) —
  corrected in pr-states.md, the docs page, ADR-018 and way-of-working; alternative
  solo-approval token explicitly deferred to its own story.
- pr-state:* labels provisioned (gh label create) + non-blocking degradation in publish-pr
  and review.
- executed host assertions added to the smoke scenario (read-only, skip offline): statuses
  API reachable with the agent token, reviews payload carries commit_id + user.type, and
  gh pr view exposes no bot flag.

Minor / questions
- pending guard in the publication snippet; required_approving_review_count: 0 explicit and
  strict: false justified (per-head review churn); least-privilege permissions block in the
  workflow template; ordering constraint (labels+job → observe → protect, enforce_admins
  last) in the guide, setup-gates and way-of-working; quality-model §4 + tier-aware-pipeline
  reconciled with the red-gate refinement; cross-links (canonical-states back-link,
  guidelines-catalog row, how-to-11 merge precondition); dataset way-of-working status
  placeholder; 🟡 reviewer approval mapped to the pair review; force-push re-run stated as
  triggered, not automatic.

Tests: 50 conformance (20 new, RED→GREEN) + smoke scenario incl. 3 executed host assertions.

Refs: #234
…host) + executable review dispatch

Round 2: 13 findings, all resolved, none escalated. 16 new conformance assertions RED->GREEN.

- approval job is an authorization control now: pull_request_target + checkout pinned to
  base.sha + persist-credentials:false. Reproduced the bypass first (PR-supplied job published
  pair-explicit-approval=success at risk:red), then verified the pin kills it.
- verdict pinned to the PR head SHA as a commit status (GITHUB_SHA is base/merge-ref, measured),
  job renamed explicit-approval-gate (one producer per context), concurrency + fresh label read
  (observed label-event race).
- dispatch: publish-pr returns review-dispatch-required (no nested subagent); implement Step 3.3
  dispatches; prompt bounded to phases 1-5, never merge; /review gains $dispatched + a
  non-interactive contract for its two human prompts.
- DoD: merge-block executed end-to-end on a throwaway repo (pending/failing/absent check, green
  merge, red block, tier raise, approval-time re-run, tamper, protected push) — evidence table in
  github-implementation.md + ADR-018.
- recipe fixes: one $REPO form, enforce_admins:false in the payload, --paginate on reviews.
- pr-states.md stops restating quality-model thresholds; quality-model defines checklist depth
  (standard vs extended); ADR-018 records the reviewer-identity option as deferred.
- smoke: real tier transition, approval filter asserted against a committed fixture, live host
  probes warn-only (no third-party PR dependency).
rucka and others added 3 commits August 1, 2026 10:13
…ing-first, both merge paths

- Pin the authorization context: branch protection uses required_status_checks.checks
  with app_id (GitHub Actions) instead of the unpinned legacy contexts array; state the
  companion settings (read-only default GITHUB_TOKEN, CODEOWNERS on workflows) and the
  residual (App/check-run is the only unforgeable form, #398).
- Stop overclaiming: pair-review is documented as an ANTI-ACCIDENT control (forgeable by
  any push-access principal, by construction), not authorization — guide, pr-states,
  ADR-018, docs page.
- Fail closed on interruption: the approval job posts state=pending on the head SHA as
  its FIRST step (property 5), so a cancelled/aborted evaluation cannot leave a stale
  lower-tier success standing.
- Both merge paths carry the precondition: /implement Phase 4 Step 4.1 re-synthesizes and
  HALTs on merge_allowed (was "at least one approval"); pr-states actor table names it.
- One projection, no transliteration: the 🔴 approval predicate ships as
  human_approval_jq_filter in pr-state.sh; the workflow and the smoke fixture both read it.
- Cite #398 wherever the solo-maintainer token is deferred; dataset way-of-working mirrors
  the full ordering (approval-time re-run) and links github-implementation.md § Ordering;
  publish-pr no longer calls /implement a "future" caller (#256 closed); the host evidence
  table states it is a point-in-time observation, re-verified by re-running the ordering.
- Tests: +19 conformance (80 total), +24 smoke assertions (108) — RED first.

Refs: #234
…, review 0.9.0)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r call)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rucka
rucka force-pushed the feature/US-234-pr-state-flow-required-check branch from 329a4c4 to f913890 Compare August 1, 2026 08:16
@github-actions
github-actions Bot temporarily deployed to Website Preview August 1, 2026 08:16 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:red Classification: high risk tier

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant