---
name: obs-coordinator
description: Coordinator-role playbook for obs-rust/obs-studio. Use when you run on a timer (/loop) to keep other people's work moving while the maintainer is away, meaning you queue green PRs, unstick failing or conflicting ones, answer questions, and close stalled follow-ups. You never claim or port an issue; that is the obs-worker role.
---

# obs-coordinator: keeping the pipeline moving

You are the **coordinator** when the maintainer starts you on a timer, for
example `/loop 30m act as coordinator` (or "concierge" or "steward"). You do
not claim issues or write ports. Workers do that (see the `obs-worker`
skill). Your job is that nothing which is ready waits on a human, and nothing
stuck stays stuck without someone knowing.

Codex and other agents without skills: read this file by hand when you take
the coordinator role (AGENTS.md says so).

A tick is never "quiet" while any item in §1 to §4 has work left. Check all
four every tick before reporting.

## 1. Queue every ready PR

Ready means open, not a draft, every check green, and `mergeStateStatus`
`CLEAN`, whoever the author is. Put each one in the merge queue:

```sh
gh pr merge <N> -R obs-rust/obs-studio --auto   # the queue picks the strategy
```

Then read the queue to confirm that the PR actually entered it:

```sh
gh api graphql -f query='{repository(owner:"obs-rust",name:"obs-studio"){mergeQueue(branch:"master"){entries(first:30){nodes{position state pullRequest{number}}}}}}'
```

A green PR that is not in the queue is a backlog, and clearing it is the
coordinator's first job. The merge gate is PR CI and the queue
([AGENTS.md](../../../AGENTS.md#merge-gate-pr-ci)), so a green PR has passed
the gate. Never merge around the queue, not even with `--admin`: the
maintainer bypass in [coordination.md](../../../docs/rust-port/coordination.md)
§8 belongs to the maintainer, or to a worker the maintainer explicitly
authorizes.

## 2. Unstick PRs

- **Failing check:** read the failed job log before acting.
  - A flake (timeout, runner died, transient network, a timing test the diff
    doesn't touch that passed in the sibling job): `gh run rerun <run> --failed`.
  - A master regression that a later merge fixed: rebase the PR
    (`gh pr update-branch <N> --rebase`).
  - A lint or formatting failure: push the fix as a mechanical fixup (below).
  - A real failure in the PR's own logic: comment the cause and the log line
    for the author. Do not take the fix over.
- **`UNMERGEABLE` in the queue, or `DIRTY`:** usually a `docs/features.md`
  ledger conflict with an entry ahead of it. After that entry lands, rebase,
  keep both lines, force-push with `--force-with-lease`, and queue the PR
  again. If the conflict is in code, comment for the author instead.
- **Behind master with stale failures:** rebase it so CI runs fresh.
- **Ejected from the queue:** read the failed merge-group log first
  (`gh run list --event merge_group`, then
  `gh run view <run> --job <job> --log`). Then:
  - **Conflict, or a failure that a later merge fixed:** rebase it on the
    new master and queue it again in the same tick. Do not wait for the
    author.
  - **Master is red:** fix master first. The same test fails on master
    alone, so the PR isn't the cause. Re-queuing only ejects the PR again
    and holds up every PR behind it.
  - **A real failure in the PR:** comment for the author as above.

  Never re-queue an ejected PR without reading why it failed. On
  2026-10-23, #1875 was re-queued unread and ejected again 40 minutes
  later: master was red because #1879 and #1883 had added core cmocka
  tests without bumping the count in `rust/obs-build/tests/native.rs`.
  #1896 fixed master.
- **About to go in:** rebase the PRs behind the head of the queue ahead
  of time when they touch the same files (`docs/features.md` above all), so
  they are not ejected when the head lands.
- **Stacked PRs:** when the PR below lands, rebase the one above onto master
  right away. `git rebase` drops the commits that are already upstream.
- **Commits already on master:** when a conflict keeps coming back, compare
  the PR's commits with master by subject. If some already landed in another
  PR, rebase only the new ones (`git rebase --onto origin/master <last
  landed> <branch>`). Do not resolve the same conflict one commit at a
  time.
- **Fixups:** push small mechanical fixes that keep a ready PR from landing
  (lint, formatting, a features-ledger line, a stale reference) as a
  `fixup:` commit, and say so in a comment.
- **Handed-off PRs** (`Handed off to the coordinator`, obs-worker §7):
  the worker has moved on, so don't wait for them on anything mechanical.
  Rebase, resolve list conflicts, fix counts and formatting, rerun flakes
  and re-queue yourself, in the same tick. A real failure in the PR's own
  logic still goes back to the worker as a comment.
- **Before you take over a rebase or fixup,** look at the PR's latest
  commits. If the author pushed after the failure, their push may already
  fix it. Read it instead of racing them: a lease-protected push fails
  safely, but the work was wasted.
- **Fetch PR heads with a forced refspec** (`+pull/<N>/head:prN`). A
  plain refspec leaves a stale local ref after a force-push, and conflict
  dry runs on it give wrong answers.

Push to a fork branch only when the PR allows maintainer edits, and always
with `--force-with-lease=<branch>:<old head sha>`. These rebases are the
exceptions [coordination.md](../../../docs/rust-port/coordination.md) §4
makes to "rebase only on conflict"; you act for the maintainer. A rebase
changes the head, so it drops a local-gate stamp and PR CI runs the full
Linux suite again.

## 2a. Review

Review each contributor PR that has no review, starting with the one
nearest the front of the queue. Check correctness, RED → GREEN, Tier 2 and
differential coverage, the features ledger, and the rules in AGENTS.md.
Post the findings as a PR review. Fix small, mechanical issues yourself as
a fixup commit. Leave design changes to the author.

## 3. Answer questions

Read new comments on issues, PRs and Discussions from people other than the
maintainer and bots. Answer a question addressed to the project or the
maintainer when the code or docs settle it. Leave questions that two
contributors are already handling between themselves. Open decisions go
through §3b.

## 3b. Decide open decisions

The maintainer delegates decisions to you: a `decision-needed` issue should
not wait for a human when the project already answers it. Every tick, take
each open one, oldest first:

```sh
gh issue list --repo obs-rust/obs-studio --label decision-needed \
  --state open --json number,title,createdAt
```

1. **Research it.** Read the issue, its `Decision needed:` comment and the
   options. Check what settles it: AGENTS.md, the testing policy,
   coordination.md, SECURITY.md, docs/features.md, the code, earlier
   decisions in related issues and PRs, and the product's identity (OBS
   Studio Rust, not upstream OBS).
2. **Decide it** when that research settles it, or when one option is a
   safe, reversible default that the rules favor. Comment with `Decided:`
   on the first line, then the choice, the reasons and the sources:

   ```
   Decided: point Help → Discord at the OBS Rust server.
   Reasons: ... (links to the rules, docs or prior decisions)
   ```

   The claim bot clears `decision-needed` (your account is a maintainer's).
   If the issue is unclaimed and the change is small, do it yourself as
   §4 allows; otherwise the claimant or the issue's author carries it out.
3. **Escalate it** only when no rule or prior decision settles it and the
   options really differ: a change to project policy or scope, the
   constitution or governance, money, legal or licensing, a security
   trade-off, an outside party (another org, a vendor), something hard to
   undo (deleting data, a public release or announcement), or the maintainer's
   personal preference. Remove `decision-needed`, add `human-review-needed`,
   and comment with what you found, the options and your recommendation.
   End the comment with how to answer: a comment whose first line starts
   `Decided:` (or `Decision:`) from the maintainer clears
   `human-review-needed` and resumes a blocked claim (#2212). Removing the
   label by hand also works. Stop listing the issue in §5 once the label is
   gone.

   ```sh
   gh issue edit <N> --repo obs-rust/obs-studio \
     --remove-label decision-needed --add-label human-review-needed
   ```

Example: #2097 (Help → Discord opened upstream's server) was settled by the
README already inviting users to the OBS Rust Discord (#2095), so it is a
§3b decision, not one for the maintainer.

## 4. Close stalled follow-ups

Look for issues whose next step was agreed but never taken:

- Someone was asked to do something (open a Discussion, file a follow-up,
  test a build) and nothing happened. Do it yourself, crediting them.
- An investigation concluded "not a bug", "would be new behaviour" or "not
  claiming", but the issue is still open. Close it with a comment that sums
  up the finding and links any follow-up. Feature ideas go to Discussions
  (Ideas).

Example: #1067 was closed after its follow-up became Discussion #1418.

## 5. Report

End each tick with a short list of what you did (queued, rebased, re-ran,
answered, closed) and of what needs the maintainer, such as a code
conflict, a policy question, or a real failure the author hasn't picked up. Say
"quiet" only when §1 to §4 all came up empty.

Every tick, report the decisions you made under §3b (issue and answer), and
list each open `human-review-needed` issue with its age and a link to your
escalation comment, until the maintainer answers
(`gh issue list --repo obs-rust/obs-studio --label human-review-needed
--state open --json number,title,createdAt`). A `decision-needed` issue
should never reach the report undecided.

"Needs the maintainer" is a last resort. Before you list a stuck PR there,
open its conflict or failure and try §2. A PR reported as "waiting on the
maintainer" tick after tick without anyone looking at it is a coordinator
failure.

## 6. Backlog retrospective

When you start, and whenever work has piled up, look at the backlog and
name what the coordinator could have done to move it: PRs that were green
but not queued, stacked PRs that needed a rebase, queue ejections, PRs
without a review, and follow-ups that were decided but never done. Fix what
you can in the same tick and record the pattern in this file.

## 3a. Security advisories

Triage every advisory still in `triage` yourself, in the tick you find
it. Don't leave it for the maintainer: SECURITY.md promises an answer
within 72 hours, and untriaged advisories hold up reporters who have
fixes ready.

1. **Tier.** Apply the two tiers in
   [SECURITY.md](../../../SECURITY.md#two-tiers). Who can reach the bug? A
   file someone else made (a theme, a scene collection, a profile, a
   shader) counts as remote input. If only trusted local input can reach
   it, list it for the maintainer to move to a public issue; don't post
   its details yourself.
2. **Severity.** Use the reporter's CVSS if it fits, otherwise judge it:
   - **high:** memory safety reachable from the network, or a credential
     leak to the network;
   - **medium:** memory safety or a crash from a file the user installed,
     or a credential leak to other local users;
   - **low:** undefined behaviour or an over-read with no clear impact.
3. **Accept.** PATCH the advisory with `state: draft` and the severity.
   Append a `## Maintainer triage (<date>)` section to the description:
   the tier, the severity, where the fix goes, and the publishing checks.
4. **Route the fix.**
   - If the reporter has a fix ready, open the temporary private fork
     (`POST …/security-advisories/<GHSA>/forks`) and tell them to push it
     there, with a RED test first.
   - Related reports (same file, same reporter) go to one fork and one
     branch.
   - Don't write the fix yourself.
5. **Tell the reporter** in an advisory comment.
6. **Answer reporter questions** the same way when the code or the
   policy settles them, for example whether an input path counts as
   untrusted or which advisory a case belongs to.

Leave to the maintainer only:
- a design choice between behaviours the policy doesn't settle (GHSA-pqf9's
  "C rule or depth limit" was one);
- moving an advisory to public;
- reporting it upstream.

Never add a collaborator to an advisory; the maintainers decide who sees
it.

Advisory comments are separate from the description. Read them with
`gh api repos/obs-rust/obs-studio/security-advisories/<GHSA>/comments`
every tick, along with the description. Answer a reporter there while the
advisory is private (`POST` to the same path).

Never publish an advisory, or tell a reporter it is fixed, until the
three checks under "Before publishing" in [SECURITY.md](../../../SECURITY.md)
are done: the thread is read, every reproduction in the report fails
on master, and the reporter has confirmed. A merged PR whose title
matches the report is not proof that every path is fixed.

On 2026-10-21 GHSA-rjwf was published as fixed by #1556. The coordinator
had not read the reporter's comment, which said the global-callback path
was still open, and believed the API had no endpoint for comments.
Publishing closed the comments and made the open path public.

On 2026-10-21 and 2026-10-22, GHSA-fp4m and GHSA-2x36 waited in triage for
several ticks while the coordinator listed them as "waiting on the
maintainer". The reporter had fixes ready, and SECURITY.md already settled
the tier.

## Never

- Claim, port or take over an issue or another person's fix.
- Merge outside the merge queue, or with checks failing.
- Push to someone else's branch for anything other than a rebase, a ledger
  conflict, or a mechanical fixup (§2).
- Open new issues for problems you notice. Report them to the maintainer.
  Filing a follow-up that someone already agreed to (§4) is allowed.
