PR Control /v1.1.0 /MIT /Claude Code plugin github ↗
Claude Code plugin

"Open" is not a status. Which one do I merge first?

One page for every PR you have in flight — joining GitHub, the review channel, the tickets they close, and the local Claude Code sessions that wrote them. Then it works out the parallel order they have to land in.

/plugin marketplace add Abhay-Sastha-S/pr-control
then /plugin install pr-control → /pr-control
PR Control your PRs PipelineTimelineMerge order
Mergeable in parallel now
Merge Copilot
Ask about your merge order…Ask
Illustration of the interface — your real tree is built from your own PRs
Why it exists

"Open" collapses four different situations into one word.

State is invisibleWaiting for a first review, waiting on requested changes, approved and waiting for a human to press merge, and blocked behind someone else's deploy are different problems — and only one is your move. GitHub shows all four as a green "Open".
The merge order lives where no API can see itA sentence in a Slack thread — "merge the backend one first or the page 404s" — two PRs closing one ticket, a pair of stacked branches. Get the order wrong and you lose the afternoon to rebasing.
A PR nobody was told about isn't in reviewIt's just open. That gap is invisible on GitHub, where the PR looks perfectly healthy.
The reasoning is in a session you can't nameWhy you made a call four days ago is in a Claude Code transcript identified by a UUID. Each PR links the sessions that touched it, with a ready-to-paste claude --resume.

Every one of those answers already exists — just in four tools that don't join to each other. This does the join, and draws the order out of it.

What you get

One self-contained page, three ways to look at the same PRs.

View 01

Pipeline

Every PR in a column for its real state — first review, changes requested, approved-and-waiting-on-a-human, merged to dev — instead of one green "Open".

View 02

Timeline replay

Scrub the last weeks and watch each PR open, collect its review ask, and land — so you can see where the time actually went.

View 03

Merge order

The dependency tree. Independent chains sit in separate tracks that run in parallel; within a track, PRs in the same wave merge at once. Tick one and the wave it unlocks lights up.

Copilot

Merge Copilot

Reads the whole graph and answers in plain words — what can I merge now, the critical path, who's waiting on me. It jumps the tree to a named PR and drafts a review ping. It never sends Slack or merges — that stays your call.

What it computes

The edge set — a must merge before b — in precedence order.

SourceMeaning
Channel-stated chainsSomeone said so in the review channel. Highest authority, hand-maintained in config because no API exposes it.
Branch stacksb's base branch is a's head. Structural, so it's inferred.
Shared open ticketsTwo open PRs closing the same ticket ship together.
Same-repo orderingOlder open PRs in a repo where you have one merge first, or you rebase.

Ties break on a configurable service rank — shared libraries before services, services before user-facing apps — and the result is laid out as parallel tracks with the fan-out drawn as branching wires. Other people's PRs are pulled in only when they share an edge with one of yours, so the page stays about your work while still showing who you're waiting on.

The copilot

A grounded assistant that reads the tree — it doesn't guess, and it doesn't act for you.

The panel on the merge-order view reads the built graph — every dependency, who authored what, what's blocked on whom — and answers from your own data. Ask it what can I merge now and it names the set that's unblocked and can go in parallel; ask what's blocking web#99 and it walks the edges. Naming a PR jumps the tree to it and pulses it; mark it merged re-flows the graph so the wave it unlocks lights up.

It runs entirely client-side over the data already in the page, so there's no backend and no key to leak. And in keeping with the rule below, it drafts and deep-links — it never sends Slack or merges anything.

Privacy

Nothing about your organisation is in the repo.

One config file, never committed

Your org, GitHub login, Slack workspace and channel, teammates, Linear workspace, and dependency chains all live in one config file. The release check fails the build if a config file, a Slack export, or a built page is ever staged — so the repo can't start leaking by accident.

cp config.example.json ~/.claude/pr-control/config.json
$EDITOR ~/.claude/pr-control/config.json

The page template holds no organisation strings either — it labels itself at runtime from a meta block the build injects.

What it reads

Quite a lot — but all of it locally, and only for the repos you list.

  • GitHub, through the gh CLI you're already signed in to — PR metadata only.
  • Slack, read-only, through an MCP server — one channel, searched for review requests. The page never gets Slack write tools; replying — including the copilot's drafts — is copy-to-clipboard plus a deep link into the thread. Nothing is ever sent for you.
  • Linear, read-only — issue titles and statuses.
  • ~/.claude/projects, read-only — your local Claude Code transcripts, scanned for PR URLs. Content never leaves your machine; only the session id, directory, and existing summary line are used.

Everything runs locally. The only thing that goes anywhere is the finished page, and only when you publish it.

The pipeline

Five local reads, one join, one page — with a smoke test that runs it.

gh pr list ─────────────► prs.jsonl ──┐
slack search ──► slack_mentions.json ─┤
linear issues ─► linear_issues.json ──┼──► merge.py ──► prdata.json
extract_sessions.py ──► sessions.json ┤                      │
gh pr list (bodies) ──► tickets.jsonl ┘                      ▼
                                              build_page.py ──► pr-control.html
                                                                     │
                                            scripts/smoke-page.mjs ──┴──► publish

That last step earns its place. The page is one large inline script, and a syntax check only proves it parses — a const used before its declaration parses perfectly and then renders a blank page. The smoke test executes the built page against a DOM shim and fails on load-time errors. It caught exactly that bug while this was being packaged.

Install

Add the marketplace, install, and run the skill.

/plugin marketplace add Abhay-Sastha-S/pr-control
/plugin install pr-control
/pr-control              # rebuild the tracker from live data and republish

Requires the gh CLI signed in, and — for the Slack and Linear columns — those MCP servers connected in Claude. GitHub and local-session data refresh even without them.

Credits

What it's built on.

  • Claude Code, by Anthropic — the sessions column reads its local transcript format, and the skill drives the refresh.
  • GitHub CLI — every PR fact comes from it.
  • Slack and Linear — read through their MCP servers; this only reads from them.
  • No runtime dependencies — Python standard library and one inline HTML page. Nothing bundled or vendored.
  • Written with Claude Code, co-author on the commits that built this.

Claude, Claude Code, and Anthropic are trademarks of Anthropic; GitHub, Slack, and Linear are trademarks of their respective owners. This project is unofficial and not affiliated with, endorsed by, or supported by any of them.