Process PRs

SkillDev tools

Process PRs

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Process PRs skill

What this skill tells your AI

The instructions your AI receives, as published by markmhendrickson/neotoma in .claude/skills/process_prs/SKILL.md and read by ahel’s review.

Use this skill to work the open PR queue end-to-end: assess merge readiness, run security gates, selectively request Claude Opus review on substantial PRs, merge eligible PRs, and hand off release PRs to /release.

When to Use

When you want to triage and advance open PRs on the repository — checking CI status, running security checks, merging what is ready, and surfacing what is blocked.

Trigger with /process_prs, or naturally ("work the PR queue", "triage open PRs").

Workflow

Step 1: Discover Open PRs

gh pr list --state open --json number,title,headRefName,baseRefName,isDraft,reviewDecision,statusCheckRollup,labels,mergeable,author

Exclude draft PRs from automated merge actions. Include them in the summary report.

Step 1a: Partition the queue and fan out the audit to subagents

The per-PR audit (Steps 1b, 1c, 2, 3-discovery) is read-only gh/git work with no cross-PR dependencies. It is the right shape for parallel subagent delegation: each subagent audits a disjoint batch, returns a structured verdict, and the main thread synthesizes and takes all merge/comment/review actions itself. This keeps mutation single-threaded (no two agents racing to merge) while collapsing the wall-clock cost of a large queue.

Partition first — do not audit every PR at full depth. From the Step 1 JSON, bucket PRs before spending audit budget:

  • mergeable: CONFLICTING → blocked-needs-rebase. These cannot merge regardless of any other signal. Do not deep-audit; list them in the report's rebase bucket. (Verify the bucket isn't masking a base-branch issue, but a conflicting PR is never merge-eligible this run.)
  • Stacked PRs (baseRefName is not main/dev but another open PR's head) → blocked on parent. Note the parent; do not deep-audit until the parent lands.
  • mergeable: MERGEABLE → the real audit candidates. These get the full Step 1b/1c/2/3 fan-out.

When to fan out vs. inline: if the MERGEABLE candidate set is > 6 PRs, fan out. At or below that, audit inline on the main thread (subagent spin-up overhead isn't worth it). Scale the subagent count so each handles 3–5 PRs: ceil(candidates / 4) subagents, capped at 8.

Dispatch (single message, multiple Agent calls so they run concurrently):

  • subagent_type: "Explore" for the audit batches — it is read-only by construction (cannot merge, comment, or push), which is exactly the guarantee you want for a discovery agent. Give each one a disjoint list of PR numbers and the full per-PR audit checklist (CI status + mechanical-vs-substantive failure classification, base-branch/stacked detection, security-adjacency with triggering files, diff size + contract-surface flag, Step 1c review-blocker verdict with quoted blocking findings, linked-issue detection including the auto-close gap, and reviewDecision/mergeable). Require a terminal one-line verdict per PR: MERGE-ELIGIBLE | BLOCKED(<reason>) | STACKED(base=<branch>) | REVIEW-BLOCKED | SECURITY-GATE-NEEDED.
  • For a deep correctness read of a specific high-risk PR (security-adjacent + large contract diff), additionally spawn the code-reviewer subagent on that single PR. Use this sparingly — it is for the PRs where a holistic human-grade review matters, not routine triage.

Synthesis is mandatory and adversarial — never act on a subagent verdict alone. Subagents read excerpts, not whole files, and will miss things. Before any merge, the main thread MUST independently re-confirm the load-bearing facts for that specific PR:

  • CI freshness — re-run gh pr checks <n> and confirm the green run is against the current head SHA, not a months-old run. A subagent reporting "CI passed" may be quoting a stale run; main moving (or the baseline gate's openapi-drift check) can invalidate it. This is the single most common synthesis correction.
  • Actual diff surface — re-run gh pr diff <n> --name-only. Subagents have under-counted security-adjacency and contract-surface by trusting a PR's self-description or a stale rebase view. Verify src/actions.ts, openapi.yaml, and the security-adjacency file set directly.
  • mergeStateStatusCLEAN is required to merge; UNKNOWN means GitHub is still computing mergeability (wait/re-poll), BEHIND/DIRTY means rebase needed.

Treat the subagent report as a prioritized worklist with evidence, not a decision. The main thread owns every gh pr merge, gh pr comment, gh issue comment/close, and @claude review call.

Step 1b: Gate Substantial PRs for Opus Review

Before classifying for merge, check whether each PR needs (or needs another) automated Opus review. The claude_pr_review.yml workflow runs on synchronize events but deliberately skips claude/* branches to avoid runaway agent loops. This step compensates by explicitly posting @claude review on PRs that are worth a holistic look.

Skip review-requesting for:

  • Draft PRs
  • Release PRs (dev→main with version bump)
  • PRs whose head branch starts with claude/ — the workflow already runs on opened for these

For newly opened PRs (state was just opened, no review comment yet):

gh pr diff <number> --stat

Parse the stat output to get: files changed, insertions, deletions. Also check whether the diff touches both src/ and openapi.yaml simultaneously:

gh pr diff <number> --name-only

Post @claude review if any of the following are true:

  • Files changed > 5
  • Total lines changed (insertions + deletions) > 150
  • Diff touches both src/ and openapi.yaml (contract surface change)
  • PR is security-adjacent (per the criteria in Step 2)
gh pr comment <number> --body "@claude review"

For PRs with new commits since last review was requested:

Find the most recent @claude review comment:

gh pr view <number> --json comments --jq '[.comments[] | select(.body | contains("@claude review"))] | last | .createdAt // empty'

If a prior review comment exists, count commits that landed after that timestamp:

gh api repos/{owner}/{repo}/pulls/<number>/commits --jq '[.[] | select(.commit.committer.date > "<last_review_iso_timestamp>")] | length'

Post another @claude review if new commits since last review ≥ 3.

Never post @claude review more than once per batch run on the same PR. If you already posted in this run, do not post again.

Step 1c: Audit Existing Review Findings (Merge Blocker)

Reviews posted asynchronously by the Opus review job can land after the author pushed the final commit. The agent that merges later must not assume "review happened, so review was addressed." This step blocks merge on unaddressed substantive findings.

For each PR, fetch the most recent review comment from github-actions[bot]:

gh api repos/{owner}/{repo}/issues/<number>/comments \
  --jq '[.[] | select(.user.login == "github-actions[bot]" and (.body | test("PR Review|Recommended|Blocker|Blocking|MUST|Findings"; "i")) and (.body | length > 300))] | last | {created_at, body}'

Detect and skip placeholder reviews that never completed:

  • Body contains only the line I'll analyze this and get back to you. (no substantive content)
  • usage.output_tokens was 0 (the action timed out or errored)

If the most recent review is a placeholder, treat the PR as "review requested but not delivered" and re-post @claude review (subject to the once-per-batch cap in Step 1b).

For substantive reviews, classify findings by scanning the body for:

  • Blocking: lines containing Blocker, Blocking, MUST, must fix, Required:, request changes, Verdict: request changes
  • Substantive non-blocking: numbered findings (**1.**, ### 1., 1. **) that are not in a "Resolved" or "Verified" section
  • Informational: lines in an ## Approve, Verdict: approve, Resolved, or Non-blocking section

If the review has any Blocking items, the PR is review-blocked until either:

  1. A commit lands after the review timestamp whose commit message explicitly references the finding (by quoting the reviewer's text, the finding number, or a referenced filename and line number), or
  2. The author posts a comment containing the literal phrase Waiver: <reason> on a per-finding basis acknowledging the deferral, or
  3. A fresh @claude review is requested and the new review's verdict is approve / approve in substance.

Detecting "housekeeping commits" that do not count as addressing findings:

A commit is housekeeping (and does not clear a Blocking finding) if its message matches any of:

  • ^chore: regenerate test catalog
  • ^chore: regenerate openapi types
  • ^chore: apply prettier formatting
  • ^chore: fix prettier formatting
  • ^Merge branch 'main' / ^Merge pull request
  • ^chore: rebase onto main

Counting these as "post-review work" is the exact pattern that caused PRs #152, #222, #223, #224, #178, #233, #232, #151 to merge with unaddressed findings on 2026-05-18.

Use this check to compute pending_review_blockers for each PR:

# Count substantive (non-housekeeping) commits after the latest substantive review
last_review_iso=$(gh api repos/{owner}/{repo}/issues/<number>/comments \
  --jq '[.[] | select(.user.login == "github-actions[bot]" and (.body | length > 300) and (.body | test("PR Review|Recommended|Blocker|MUST|Findings"; "i")))] | last | .created_at // empty')

if [ -n "$last_review_iso" ]; then
  gh api repos/{owner}/{repo}/pulls/<number>/commits \
    --jq "[.[] | select(.commit.committer.date > \"$last_review_iso\") | select(.commit.message | test(\"^chore: (regenerate|apply|fix) prettier|^chore: regenerate test catalog|^chore: regenerate openapi types|^Merge branch|^Merge pull request|^chore: rebase\"; \"\") | not) | .sha[:8]]"
fi

If the review had Blocking items and zero substantive (non-housekeeping) commits landed after it, the PR is review-blocked. Post a summary comment listing the unaddressed findings and request fixes; do not merge.

Step 1d: Check Agentic Behavior Eval Coverage (Merge Blocker)

A PR that changes agentic behavior — the instructions agents receive, the tools they can call, or the flows they execute — must demonstrate eval coverage before it can be merged.

Detect agentic behavior changes. A PR is agentic-behavior-adjacent if its diff touches any of:

  • docs/developer/mcp/instructions.md
  • src/tool_definitions.ts
  • src/server.ts
  • .cursor/skills/*/SKILL.md
  • .claude/skills/*/SKILL.md
  • Any file whose path contains agent_instructions
  • Any file under src/services/ whose name contains agent or instruction

Detection command:

gh pr diff <number> --name-only | grep -E \
  'docs/developer/mcp/instructions\.md|src/tool_definitions\.ts|src/server\.ts|\.cursor/skills/.*/SKILL\.md|\.claude/skills/.*/SKILL\.md|agent_instructions'

If the command produces any output, the PR is agentic-behavior-adjacent.

Check for eval evidence. A PR satisfies the eval requirement if at least one of the following is true:

  1. PR body references evals. The PR description body (not just title) contains at least one of: eval, agentic eval, agentic_eval, scenario, tier 1, tier 2.

    gh pr view <number> --json body --jq '.body' | grep -iE 'eval|agentic_eval|scenario|tier 1|tier 2'
    
  2. Linked issue references evals. Any issue closed by the PR (from gh pr view <number> --json closingIssuesReferences) has a body or comment referencing eval coverage.

  3. New or modified eval fixture. The diff includes a file under tests/fixtures/agentic_eval/.

    gh pr diff <number> --name-only | grep 'tests/fixtures/agentic_eval/'
    
  4. Human reviewer confirmed evals pass. A human reviewer (non-bot) posted a comment containing eval coverage confirmed, evals pass, or evals verified (case-insensitive).

    gh pr view <number> --json comments --jq '.comments[] | select(.author.login != "github-actions[bot]") | select(.body | test("eval coverage confirmed|evals pass|evals verified"; "i")) | .body'
    

If no eval evidence is found, the PR is eval-blocked. Post a comment on the PR:

This PR modifies agentic behavior (agent instructions, MCP tool definitions, skill files, or related paths) and does not yet have eval coverage.

**Before this PR can be merged, please provide at least one of:**

1. Update the PR description to reference passing evals (mention `eval`, `agentic eval`, `scenario`, or `tier 1`/`tier 2`).
2. Add or update an eval fixture in `tests/fixtures/agentic_eval/` that covers the changed behavior.
3. Post a comment confirming evals pass: `eval coverage confirmed` or `evals pass`.

See [docs/developer/eval_requirements.md](../docs/developer/eval_requirements.md) for how to write an eval fixture and what counts as eval evidence.

If eval evidence is found, continue to Step 2. Note this in the per-PR summary as "Eval coverage: ✓".

Note: a commit message referencing evals (e.g. test: add eval fixture for …) is not by itself eval evidence unless the commit also adds/modifies a fixture file under tests/fixtures/agentic_eval/.

Step 1e: Docs-Coverage Gate

For each PR that adds or modifies user-facing surfaces (CLI commands, flags, API endpoints, MCP tools, config knobs), verify that documentation was updated in the same PR.

Surfaces that require documentation updates:

  • New or modified CLI commands/flags → docs/developer/cli_reference.md and docs/developer/cli_agent_instructions.md
  • New or modified MCP tools → docs/developer/mcp/tool_descriptions.yaml and docs/developer/mcp/instructions.md
  • New or modified API endpoints or request/response fields → openapi.yaml
  • New config options or environment variables → docs/developer/cli_reference.md Runtime overrides table
  • New behavioral rules affecting agents → mirrored in both MCP and CLI instruction files

Check for missing docs coverage:

gh pr diff <number> --name-only

If the diff contains files under src/cli/ (new CLI surface), src/services/ (new tool behavior), or openapi.yaml — cross-check that the PR also modifies the corresponding doc files listed above.

If a documented surface was added or modified but the corresponding doc file was not changed:

  • Flag the PR as docs-blocked
  • Post a comment naming the missing doc update(s)
  • Do not merge until the doc gap is filled or the author posts Waiver: <reason> explaining why no doc update is needed

A Waiver: comment on the PR is sufficient to clear a docs-blocked status.

Step 1f: Test-Coverage Gate

For each PR that adds or modifies user-facing surfaces, verify that tests covering the new behavior were added or updated in the same PR.

Surfaces that require test coverage per change_guardrails_rules.md rules 19–21:

  • New CLI commands → integration test in tests/cli/ exercising the command end-to-end
  • New destructive or data-mutating operations (migrations, repair commands, bulk rewrites) → round-trip integration test covering identity, dry-run non-mutation, idempotency, and NULL preservation
  • New external-file-shape parsers (harness transcripts, SQLite imports, third-party configs) → fixture-based test exercising the actual parser path (not just detectSource)
  • New HTTP runtime-config knobs (timeouts, headers, keep-alive) → runtime behavior test asserting observable output, not just source constants
  • New store/ingestion paths → test asserting the POST body fields sent to the store API

Check for missing test coverage:

gh pr diff <number> --name-only

If the diff adds files in src/cli/ (new CLI command), src/services/ (new service behavior), or touches migration scripts — cross-check that the PR also adds or modifies test files in tests/ or co-located *.test.ts files.

For transcript/file-import surfaces specifically: verify that tests assert POST body structure (field names and values), not just the count of POST calls. A test that only counts calls will not catch a broken parser that sends {} instead of { file_path, observation_source }.

If a testable surface was added but no test covers it:

  • Flag the PR as test-blocked
  • Post a comment naming the missing test coverage
  • Do not merge until tests are added or the author posts Test-waiver: <reason> explaining why coverage is not feasible

A Test-waiver: comment (distinct from Waiver: used for docs and review findings) is sufficient to clear a test-blocked status.

Step 2: Classify Each PR

For each open PR, determine:

  1. Is it a release PR? Criteria: base branch is main, head branch is dev (or configured integration branch), and package.json version bump is present in the diff.

    • If yes: do not merge. Report it and instruct the user to run /release to handle it. Do not inline release steps.
  2. Is it security-adjacent? A PR is security-adjacent if its diff touches any of:

    • src/actions.ts, src/services/local_auth.ts, src/middleware/, any route handler
    • Auth, middleware, proxy trust, forwarded-for handling, session, credential, or token logic
    • protected_routes_manifest.json, scripts/security/, docs/security/
    • Any file matching *auth*, *token*, *session*, *credential*, *permission*
  3. What is the CI status? Read statusCheckRollup from the PR JSON. Classify as: passed, failed, pending, or missing.

  4. Is it mergeable? A PR is merge-eligible when:

    • Not a draft
    • Not a release PR
    • CI status is passed (all checks green)
    • reviewDecision is APPROVED or review is not required by branch protection
    • mergeable is MERGEABLE
    • Security gates pass (see Step 3)
    • No unaddressed review blockers from Step 1c. A PR with Blocking findings from the most recent substantive github-actions[bot] review and no substantive (non-housekeeping) commits or Waiver: comments since is review-blocked and must not be merged.
    • Eval coverage present for agentic-behavior-adjacent PRs (Step 1d). A PR that touches agent instructions, MCP tool definitions, skill files, or related agentic flows is eval-blocked until eval evidence is provided.
    • No docs-coverage gaps from Step 1e. A PR that adds a user-facing surface without updating the corresponding doc files is docs-blocked until the gap is filled or a Waiver: comment explains the omission.
    • No test-coverage gaps from Step 1f. A PR that adds a testable surface without adding tests is test-blocked until tests are added or a Test-waiver: comment explains why coverage is not feasible.
    • Closure comments drafted for every linked issue (Step 4). A merge that closes issues without a closure narrative loses the resolution trail. The closure comment must be posted before gh pr merge.

Step 3: Run Security Gates

For security-adjacent PRs — mandatory before merge:

npm run security:lint

Errors are blocking. Warnings are noted but do not block. If errors are found, report them on the PR and do not merge until resolved.

npm run security:manifest:check
npm run test:security:auth-matrix

Both must pass. If security:manifest:check fails due to an out-of-date manifest (not a code error), run:

npm run security:manifest:write

and confirm the manifest change is staged in the PR branch before re-running the check.

For all other PRs — advisory:

Run npm run security:lint and report results. Do not block merge on warnings, but surface any errors for the PR author.

Do not run security:classify-diff, security:ai-review, or the deployed probes (deployed_probes.sh) — those are release-scoped gates that run in /release Step 3.5 and Step 5.

Step 4: Post issue-closure comments on linked issues (before merge)

For each PR that passes all merge-eligibility criteria, before running gh pr merge, walk every issue the PR claims to close and post a closure-narrative comment on it. This memorializes the work and forces a final verification that every original ask was addressed.

The auto-close behavior of GitHub (Closes #N in PR body) only posts a generic merge-link comment. That is not sufficient: it does not verify that each bullet of the issue body was addressed, does not reconcile review-thread follow-ups, and does not call out explicit deferrals.

Detect the linked issues:

# Issues GitHub auto-tracks (must be in body via "Closes/Fixes/Resolves #N")
gh pr view <number> --json closingIssuesReferences --jq '[.closingIssuesReferences[].number]'

# Also scan the PR body and commit messages for bare references that GitHub did
# not parse (e.g. "fix bugs #168 #170 #171 #172" — only #168 auto-closes).
gh pr view <number> --json body,commits --jq '.body, (.commits[].messageHeadline), (.commits[].messageBody // "")' \
  | grep -oE '(closes|close|fixes|fix|resolves|resolve)\s+#[0-9]+' -i | sort -u

# Any issue listed here that is NOT in closingIssuesReferences will not be
# auto-closed at merge time — flag it for manual `gh issue close` after merge.

Closure comment shape (uniform across PRs):

Resolved by PR #<pr> (merged <yyyy-mm-dd>). Shipping in **v<version>**.

## What the issue asked for

<bullets quoted from the issue's Expected / Proposed fix / Acceptance / Problem section>

## How it was addressed

<concrete description of the fix: bullets from the PR's Summary section,
ideally referencing file:line or commit SHA for each ask. If the PR closes
multiple issues, include only the bullet that pertains to *this* issue.>

<If review-thread follow-ups for this PR landed in a separate cleanup PR,
note that explicitly: "Review-thread follow-ups for PR #X were addressed in
cleanup batch PR #Y.">

## Verification

<bullets from the PR's Verification / Test plan section, with checkboxes
ticked since the PR has merged.>

## Deferred / out of scope (if applicable)

<bullets describing any original-issue ask or review-comment ask that was
NOT addressed by this PR, with a link to the follow-up issue or a written
reason for the deferral.>

Operationally:

  1. For each linked issue, read the issue body (gh issue view <iss>) and the PR body (gh pr view <pr>).

  2. Build the comment per the shape above. Quote the issue's original asks verbatim where possible.

  3. Verify that every original ask appears in the "How it was addressed" or "Deferred" section. If any ask is unaccounted for, do not merge — push back on the PR author or file a follow-up issue.

  4. Post the comment:

    gh issue comment <iss> --body-file <draft.md>
    
  5. If the issue is in the "auto-close gap" list from the detection step (closure verb in body/commit but not in closingIssuesReferences), close it manually after merge:

    gh issue close <iss> --reason completed
    

Skip the closure comment for:

  • Issues that are purely process/governance (e.g. "we should be more rigorous about commit-message scopes") — leave open with a comment explaining why.
  • Trivial single-line fixes where the PR title + auto-close comment are obviously sufficient. The judgment call: would a reader landing on this issue six months from now want a narrative, or is the linked PR self-evidently dispositive? Default to writing the comment — narratives age better than diffs.

Step 4c: Merge Eligible PRs

For each PR that passes all merge-eligibility criteria AND has closure comments posted on every linked issue:

gh pr merge <number> --merge --delete-branch

Use --merge (no-ff merge) to preserve PR history. Use --squash only if the PR branch has a single logical commit and the repo convention allows it. Do not rebase-merge.

After merging, confirm the merge succeeded and manually close any auto-close-gap issues identified in Step 4:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
32
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
process-prs
Source
github.com/markmhendrickson/neotoma