Skip to content

Commit 4ccf0ef

Browse files
authored
review: mark a blocking verdict on the PR, not only in firstmate's chat (#46)
* review: mark a blocking verdict on the PR, not only in firstmate's chat PR #44 merged carrying seven confirmed defects while its crew was still fixing them. The review had found all seven and firstmate had said not to merge, but that verdict existed only in the firstmate session's chat, so whoever clicked merge never saw it. AGENTS.md section 6 step 3 now requires the verdict to land on the PR when one is already open: draft a firstmate-authored PR with `gh pr ready --undo`, which blocks the merge button mechanically instead of merely advising, plus a comment carrying the reason; never draft a bot's PR, where a comment and a `do-not-merge` label are used instead. docs/pr-block-signal.md owns the commands, the per-repo label setup, and the verification record - including the check that drafting does NOT suppress this repo's PR CI, which is what makes the draft path safe for the fix loop. * docs(pr-block-signal): replace inferred draft-CI reasoning with live evidence The event reference does not document whether workflows run on draft PRs, so the claim now rests on a live draft PR in a public repo whose workflow has the same bare `pull_request:` trigger shape as ours (cli/cli#14013, 15 completed check runs while isDraft). Also records that GitHub's current stage-change docs carry no plan or visibility restriction on drafts at all, which makes gh's "If supported by your plan" caveat conservative rather than a live constraint. * docs(pr-block-signal): record the bot-versus-firstmate discriminator check Verified on live PRs of both kinds that .author.is_bot separates the two mechanisms, and stated the two commands that close the one gap left in the record - the live draft conversion - on any open firstmate PR.
1 parent bb344c4 commit 4ccf0ef

4 files changed

Lines changed: 213 additions & 0 deletions

File tree

‎AGENTS.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ A ship crewmate pushes its branch and reports `review-ready:` (mode `PR`) or `do
243243
1. Read the diff with `bin/fm-review-diff.sh <id>` (summary first, then `--full` or `--files <path>`) - never a raw `git diff`, which can be stale.
244244
2. Review it against the project's direction (section 5, gate 4). Mechanical quality is the hooks' and CI's job; you are looking for drift, wrong-shaped solutions, and scope creep.
245245
3. Reply with `bin/fm-send.sh <id>`: findings (the crew fixes them in place and re-signals) or approval (the crew opens the PR and reports `done: PR <url>`).
246+
If a PR is already open and your review finds a merge-stopping defect, mark it unmergeable **on the PR too**, because a verdict that lives only in this chat never reaches whoever clicks merge.
247+
Draft a firstmate-authored PR with `gh pr ready --undo`, which blocks the merge button mechanically rather than advising, and comment the verdict so the reason travels with the PR; `gh pr ready` restores it once the fix lands.
248+
Never draft a bot's PR (Dependabot and similar): comment plus a `do-not-merge` label instead, which is also the fallback when `--undo` is refused.
249+
`docs/pr-block-signal.md` owns the commands, the label setup, and the verification record.
246250
4. Run `bin/fm-pr-check.sh <id> <PR url>` to arm the CI poll, then tear the crew down (below).
247251
5. Tell the user: the PR's full `https://...` URL, a one-paragraph summary, and your direction verdict. If the change drifts, say so plainly.
248252
6. On the user's "merge it", run `bin/fm-pr-merge.sh <id> <full GitHub PR URL>`, never `gh pr merge` directly. For `local-only`, run `bin/fm-merge-local.sh <id>` after approval. Never merge a red PR.

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ Firstmate's skills live in two separate places with different audiences:
185185
- [docs/architecture.md](docs/architecture.md) - how the crew, supervision, worktrees, secondmates, and project modes work.
186186
- [docs/configuration.md](docs/configuration.md) - environment variables, `FM_HOME`, runtime backend selection, optional X mode, the files you set, and harness support.
187187
- [docs/wedge-alarm.md](docs/wedge-alarm.md) - configure the active alert for a wedged away-mode escalation delivery.
188+
- [docs/pr-block-signal.md](docs/pr-block-signal.md) - how a blocking review verdict is marked on an open PR so it reaches whoever clicks merge, with the incident and verification record behind it.
188189
- [docs/tmux-backend.md](docs/tmux-backend.md) - setup guide for the tmux reference backend: prerequisites, attaching, and watching crew windows.
189190
- [docs/treehouse-backend.md](docs/treehouse-backend.md) - verified treehouse worktree-pool behavior: why repo-level `treehouse.toml` hooks are ignored, what a returned slot keeps, and why APFS clone savings are invisible to `du`.
190191
- [docs/herdr-backend.md](docs/herdr-backend.md) - setup guide for the experimental herdr backend, plus its verification notes and known gaps.

‎docs/pr-block-signal.md‎

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Blocking a merge from the PR itself
2+
3+
`AGENTS.md` section 6, step 3 owns the rule: when firstmate's review of an already-open PR finds a merge-stopping defect, the verdict goes on the PR, not only into the chat.
4+
This doc owns the mechanics, the per-repo setup, and the verification record behind them.
5+
6+
## The incident this exists for
7+
8+
On 2026-07-30, PR #44 in this repo (`webjema/firstmate`) merged carrying seven confirmed defects while its crew was still fixing them.
9+
An independent review had found all seven and firstmate had told the user not to merge.
10+
The verdict existed only in that firstmate session's chat, so the person clicking merge acted without it.
11+
One of the seven bricks a worktree pool slot on every warm, on Linux as well as macOS.
12+
13+
The generalizable failure is not "the review was late" - the review was on time and correct.
14+
It is that a review verdict living only in chat does not reach the person who merges, including a collaborator who never sees that session at all.
15+
Firstmate's normal review sits before the PR exists, which keeps this rare, but once a PR is open the chat is no longer where the merge decision happens.
16+
17+
An advisory mark - a title prefix, or a comment on its own - is what already failed: it informs a reader who looks, and the merge button stays live for one who does not.
18+
Prefer the mechanism that blocks.
19+
20+
## firstmate-authored PRs
21+
22+
Convert the PR to draft, which disables merging outright - GitHub's own docs put it as "No one can merge the pull request until you mark the pull request as ready for review again" - and comment the verdict so the reason travels with the PR:
23+
24+
```sh
25+
gh pr ready --undo <pr-url> # blocks the merge button
26+
gh pr comment <pr-url> --body-file verdict.md # carries the reason
27+
```
28+
29+
Restore it with `gh pr ready <pr-url>` once the fix lands and the re-review is clean.
30+
31+
`--undo` is plan-gated (`gh pr ready --help`: "If supported by your plan").
32+
It fails loudly - a GitHub API error and a non-zero exit - never silently, so treat a non-zero exit as the signal to fall back to the bot-authored path below on the same PR.
33+
34+
Two things hold while a PR sits drafted:
35+
36+
- PR CI keeps running on every push, so the fix loop keeps its gate (see the verification record).
37+
- `bin/fm-pr-merge.sh` fails closed, because GitHub refuses to merge a draft PR at the API. That is defense in depth, not a bug to route around: mark the PR ready first, then merge on the user's word.
38+
39+
## Bot-authored PRs
40+
41+
Never draft a bot's PR - Dependabot and similar bots act on their own PR's state, and drafting can interfere with that handling.
42+
Use a comment plus a blocking label:
43+
44+
```sh
45+
gh pr comment <pr-url> --body-file verdict.md
46+
gh pr edit <pr-url> --add-label do-not-merge
47+
```
48+
49+
The label is advisory by itself; make it bite by requiring its absence in the repo's branch protection or merge-queue rules where that is configured.
50+
Remove it with `gh pr edit <pr-url> --remove-label do-not-merge`.
51+
52+
`do-not-merge` is not a stock GitHub label and does not exist in most repos.
53+
Create it once per repo, on first use:
54+
55+
```sh
56+
gh label create do-not-merge --color B60205 --description "Blocking review defect - do not merge" --force
57+
```
58+
59+
`--force` updates an existing label instead of failing, so the command is safe to re-run.
60+
61+
Authorship and current draft state both come out of the `gh-axi pr view <pr>` firstmate already reads (`author:` and `draft:` lines); `gh pr view <pr-url> --json author --jq .author.is_bot` is the exact read when the distinction is not obvious from the login.
62+
Every mark above is a mutation, so it runs on plain `gh`, per section 6's read/mutate split.
63+
64+
## Verification record
65+
66+
Run 2026-07-30 in `webjema/firstmate`, `gh version 2.96.0 (2026-07-02)`, authenticated as `ignovak` with `push` on the repo.
67+
68+
**1. Is `gh pr ready --undo` supported here, and how does it fail when it is not?**
69+
70+
```console
71+
$ gh repo view webjema/firstmate --json visibility,isPrivate
72+
{"isPrivate":false,"visibility":"PUBLIC"}
73+
```
74+
75+
GitHub's current documentation for changing a PR's stage states no plan or visibility restriction on draft pull requests at all, so `gh`'s "If supported by your plan" caveat is at best conservative.
76+
This repo is public in any case - the visibility that has always carried draft support - and the underlying mutation is present in the API:
77+
78+
```console
79+
$ gh api graphql -f query='{ __type(name:"Mutation"){ fields{ name } } }' --jq '.data.__type.fields[].name' | grep -i draft
80+
convertPullRequestToDraft
81+
...
82+
```
83+
84+
The failure mode is loud, not silent:
85+
86+
```console
87+
$ gh pr ready --undo 99999 --repo webjema/firstmate
88+
GraphQL: Could not resolve to a PullRequest with the number of 99999. (repository.pullRequest)
89+
$ echo $?
90+
1
91+
```
92+
93+
Not proven directly: a live draft conversion on this repo.
94+
Doing that needs an open PR, and a crewmate may not open one before firstmate approves its branch.
95+
The instruction is therefore written to key its fallback on the non-zero exit rather than on an assumption that the plan allows drafts, which makes it correct on either kind of plan.
96+
97+
**2. Does drafting suppress this repo's PR CI?**
98+
99+
No - which is what makes the draft path usable, and this was the check with the power to kill it.
100+
101+
```console
102+
$ grep -n -A 5 "^on:" .github/workflows/ci.yml
103+
9:on:
104+
10- push:
105+
11- branches: [main]
106+
12- pull_request:
107+
13- branches: [main]
108+
$ grep -n "draft\|if:" .github/workflows/ci.yml
109+
$ echo $?
110+
1
111+
```
112+
113+
The `pull_request` trigger carries no `types:` filter, so it uses the default activity types `opened`, `synchronize`, and `reopened`, and the workflow has no draft guard at all - the `if: github.event.pull_request.draft == false` idiom repos use to opt out of draft CI is absent.
114+
115+
GitHub's own event reference does not spell out draft behavior, so this was checked against a live draft PR in a public repo whose workflow has the same trigger shape as ours:
116+
117+
```console
118+
$ gh api repos/cli/cli/contents/.github/workflows/go.yml -H "Accept: application/vnd.github.raw" | head -6
119+
name: Unit and Integration Tests
120+
on:
121+
push:
122+
branches:
123+
- trunk
124+
pull_request:
125+
$ gh pr view 14013 -R cli/cli --json isDraft,statusCheckRollup --jq '{isDraft, total:(.statusCheckRollup|length)}'
126+
{"isDraft":true,"total":15}
127+
```
128+
129+
That PR is a draft and carries 15 completed check runs, `build (ubuntu-latest)` among them, from a bare `pull_request:` trigger.
130+
So a draft PR does run this shape of workflow, and firstmate's Lint shell scripts, Behavior tests, and Repo invariants all keep running on every fix push while the PR is drafted.
131+
132+
One nuance worth knowing: `converted_to_draft` and `ready_for_review` are not default activity types, so neither drafting the PR nor marking it ready spawns a run by itself.
133+
CI runs on the fix push, which is exactly when the gate matters.
134+
135+
**3. Does a `do-not-merge` label exist?**
136+
137+
No.
138+
139+
```console
140+
$ gh label list --repo webjema/firstmate
141+
bug, documentation, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix
142+
```
143+
144+
Only the nine stock labels.
145+
The label must be created per repo on first use, with the `gh label create ... --force` command above.
146+
147+
**4. Does the bot-versus-firstmate discriminator actually separate the two paths?**
148+
149+
Yes, on live PRs of both kinds:
150+
151+
```console
152+
$ gh pr view 44 --repo webjema/firstmate --json author --jq '.author | .login, .is_bot'
153+
ignovak
154+
false
155+
$ gh pr view 177523 --repo home-assistant/core --json author --jq '.author | .login, .is_bot'
156+
app/dependabot
157+
true
158+
```
159+
160+
`.author.is_bot` is the field to branch on; a bot's `login` also carries the `app/` prefix.
161+
162+
The one step not exercised live is the draft conversion itself, for the reason given under verification 1.
163+
Any open firstmate PR closes that gap in two commands: `gh pr ready --undo <pr-url>` and then `gh pr ready <pr-url>`, checking that the merge button goes away and comes back.
164+
If that ever comes back refused, record it here and switch the firstmate path to the label mechanism too.

‎tests/fm-pr-block-signal.test.sh‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env bash
2+
# Behavior tests for the contract that a blocking review verdict is marked on an
3+
# open PR, not only in firstmate's chat with the user.
4+
set -u
5+
6+
# shellcheck source=tests/lib.sh disable=SC1091
7+
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
8+
9+
test_agents_review_step_marks_the_pr() {
10+
local agents="$ROOT/AGENTS.md"
11+
12+
assert_grep 'gh pr ready --undo' "$agents" "AGENTS.md review flow does not block the merge button on a firstmate-authored PR"
13+
assert_grep 'do-not-merge' "$agents" "AGENTS.md review flow does not give bot PRs the label path"
14+
assert_grep 'Never draft a bot' "$agents" "AGENTS.md review flow does not forbid drafting a bot's PR"
15+
assert_grep 'docs/pr-block-signal.md' "$agents" "AGENTS.md review flow does not point at the mechanics doc"
16+
pass "AGENTS.md review flow marks a merge-stopping verdict on the PR itself"
17+
}
18+
19+
test_block_signal_doc_owns_the_mechanics() {
20+
local doc="$ROOT/docs/pr-block-signal.md"
21+
22+
assert_present "$doc" "docs/pr-block-signal.md is missing"
23+
assert_grep 'gh pr ready <pr-url>' "$doc" "doc does not say how to clear a draft mark"
24+
assert_grep 'gh label create do-not-merge' "$doc" "doc does not say how to create the blocking label"
25+
assert_grep 'non-zero exit' "$doc" "doc does not key the fallback on --undo failing loudly"
26+
assert_grep 'draft guard' "$doc" "doc does not record whether drafting suppresses PR CI"
27+
pass "docs/pr-block-signal.md owns the commands, the label setup, and the CI evidence"
28+
}
29+
30+
test_project_ci_has_no_draft_guard() {
31+
local wf="$ROOT/.github/workflows/ci.yml"
32+
33+
# The draft path only works because drafting does not stop this repo's PR CI.
34+
# A `types:` filter or a draft conditional added to ci.yml would take the fix
35+
# loop's gate away and invalidate the instruction in AGENTS.md.
36+
assert_no_grep 'pull_request.draft' "$wf" "ci.yml gained a draft conditional - drafting a PR would now suppress its CI"
37+
assert_no_grep 'ready_for_review' "$wf" "ci.yml gained a ready_for_review types filter - draft pushes would no longer run CI"
38+
assert_grep 'pull_request:' "$wf" "ci.yml no longer runs on pull_request at all"
39+
pass "PR CI still runs on draft PRs, so the fix loop keeps its gate"
40+
}
41+
42+
test_agents_review_step_marks_the_pr
43+
test_block_signal_doc_owns_the_mechanics
44+
test_project_ci_has_no_draft_guard

0 commit comments

Comments
 (0)