Public: anyone can read this; the crew can change it.

Merging a pull refuses with team_review_required while a named team's reviewer has not approved #10096

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A pull now won't merge while a team named on the issue it closes still has to review it. `mergePull` refuses with `{ kind: "team_review_required", teams: ["Design"] }` while `teamsHolding` says any team holds the head. Every route says the same sentence: "Waiting on Design's review. Nothing was merged." That covers the web merge box, the API (409 with `{ error, message, teams }`), the MCP `merge`…

shipped production is serving it deploy a5a2512

opened this

A pull now won't merge while a team named on the issue it closes still has to review it. mergePull refuses with { kind: "team_review_required", teams: ["Design"] } while teamsHolding says any team holds the head. Every route says the same sentence: "Waiting on Design's review. Nothing was merged." That covers the web merge box, the API (409 with { error, message, teams }), the MCP merge tool and the studio.

  • Where the gate sits: core mergePull (src/core/pulls.ts) checks it right after review_required, against the live head read inside the merge. It gets the check as a new ReviewGate.teamsHolding(headOid) function rather than importing issue_teams.ts, because that file already imports from core/pulls.ts and the import would loop.
  • Break-glass: actions/pulls.ts passes that function in unless pastGate is set, so the owner's override skips the team gate as well as checks.
  • The sentence: it lives once, as teamHoldRefusal in src/core/teams_words.ts. mergeFailure (which the MCP tool uses) and src/http/api.ts both call it, so the API doesn't import a web page.
  • The test: test/team-merge-gate.test.ts covers the five cases from the issue: held while waiting, lands after approval, stale approval holds, a team with no reviewer here doesn't hold, break-glass passes. It also checks the API's answer. On main, 3 of its 6 tests fail.
  • Left out: the terminal's merge screen and the pull page's merge box still show the general review states before you press merge. They only show this sentence once the merge is refused.

Checks: bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts are clean, and test/mcp-size.test.ts passes. Running test/pull*.test.ts, test/issue-teams*.test.ts and test/teams-words.test.ts together gave 188 pass, 2 skip and 2 fail. Both failures were in test/pull-quiet.test.ts, which passed all 4 tests when run alone with this change (twice) and without it, so they look like timing under load rather than this change. No existing test was edited.

I couldn't call keep_plan because the tool isn't available in this session, so there is no plan posted on the issue.

🤖 Generated with Claude Code

Closes #10080

commented

A pull now won't merge while a team named on the issue it closes still has to review it. mergePull refuses with { kind: "team_review_required", teams: ["Design"] } while teamsHolding says any team holds the head. Every route says the same sentence: "Waiting on Design's review. Nothing was merged." That covers the web merge box, the API (409 with { error, message, teams }), the MCP merge tool and the studio.

  • Where the gate sits: core mergePull (src/core/pulls.ts) checks it right after review_required, against the live head read inside the merge. It gets the check as a new ReviewGate.teamsHolding(headOid) function rather than importing issue_teams.ts, because that file already imports from core/pulls.ts and the import would loop.
  • Break-glass: actions/pulls.ts passes that function in unless pastGate is set, so the owner's override skips the team gate as well as checks.
  • The sentence: it lives once, as teamHoldRefusal in src/core/teams_words.ts. mergeFailure (which the MCP tool uses) and src/http/api.ts both call it, so the API doesn't import a web page.
  • The test: test/team-merge-gate.test.ts covers the five cases from the issue: held while waiting, lands after approval, stale approval holds, a team with no reviewer here doesn't hold, break-glass passes. It also checks the API's answer. On main, 3 of its 6 tests fail.
  • Left out: the terminal's merge screen and the pull page's merge box still show the general review states before you press merge. They only show this sentence once the merge is refused.

Checks: bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts are clean, and test/mcp-size.test.ts passes. Running test/pull*.test.ts, test/issue-teams*.test.ts and test/teams-words.test.ts together gave 188 pass, 2 skip and 2 fail. Both failures were in test/pull-quiet.test.ts, which passed all 4 tests when run alone with this change (twice) and without it, so they look like timing under load rather than this change. No existing test was edited.

I couldn't call keep_plan because the tool isn't available in this session, so there is no plan posted on the issue.

🤖 Generated with Claude Code

commented

I didn't change or push anything this round: the preview deploy for #10096 didn't fail, and there was nothing to fix. The log you sent ends in ready, serving and done. The only odd line is "no tests declared, none run". That's expected: forge.json lists no app tests, because tests run in the separate check. The preview at f3d4caf answers /healthz and / with 200 in under 2 s. Every page forge.json lists under looks answers 200, or 303 sign-in redirects, the same as production.

What the pull does: a pull now won't merge while a team named on the issue it closes still has to review it. mergePull refuses with { kind: "team_review_required", teams: ["Design"] } while teamsHolding says any team holds the head. Every route says the same sentence: "Waiting on Design's review. Nothing was merged." That covers the web merge box, the API (409 with { error, message, teams }), the MCP merge tool and the studio.

  • Where the gate sits: core mergePull (src/core/pulls.ts) checks it right after review_required, against the live head read inside the merge. It gets the check as a new ReviewGate.teamsHolding(headOid) function rather than importing issue_teams.ts, because that file already imports from core/pulls.ts and the import would loop.
  • Break-glass: actions/pulls.ts passes that function in unless pastGate is set, so the owner's override skips the team gate as well as checks.
  • The sentence: it lives once, as teamHoldRefusal in src/core/teams_words.ts. mergeFailure (which the MCP tool uses) and src/http/api.ts both call it, so the API doesn't import a web page.
  • The test: test/team-merge-gate.test.ts covers the five cases from the issue: held while waiting, lands after approval, stale approval holds, a team with no reviewer here doesn't hold, break-glass passes. It also checks the API's answer. On main, 3 of its 6 tests fail.
  • Left out: the terminal's merge screen and the pull page's merge box still show the general review states before you press merge. They only show this sentence once the merge is refused.

Checks: bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts are clean, and test/mcp-size.test.ts passes. Running test/pull*.test.ts, test/issue-teams*.test.ts and test/teams-words.test.ts together gave 188 pass, 2 skip and 2 fail. Both failures were in test/pull-quiet.test.ts, which passed all 4 tests when run alone with this change (twice) and without it, so they look like timing under load rather than this change. No existing test was edited.

I couldn't call keep_plan because the tool isn't available in this session, so there is no plan posted on the issue.

🤖 Generated with Claude Code

commented

A pull now won't merge while a team named on the issue it closes still has to review it. mergePull refuses with { kind: "team_review_required", teams: ["Design"] } while teamsHolding says any team holds the head. Every route says the same sentence: "Waiting on Design's review. Nothing was merged." That covers the web merge box, the API (409 with { error, message, teams }), the MCP merge tool and the studio.

  • Where the gate sits: core mergePull (src/core/pulls.ts) checks it right after review_required, against the live head read inside the merge. It gets the check as a new ReviewGate.teamsHolding(headOid) function rather than importing issue_teams.ts, because that file already imports from core/pulls.ts and the import would loop.
  • Break-glass: actions/pulls.ts passes that function in unless pastGate is set, so the owner's override skips the team gate as well as checks.
  • The sentence: it lives once, as teamHoldRefusal in src/core/teams_words.ts. mergeFailure (which the MCP tool uses) and src/http/api.ts both call it, so the API doesn't import a web page.
  • The test: test/team-merge-gate.test.ts covers the five cases from the issue: held while waiting, lands after approval, stale approval holds, a team with no reviewer here doesn't hold, break-glass passes. It also checks the API's answer. On main, 3 of its 6 tests fail.
  • Left out: the terminal's merge screen and the pull page's merge box still show the general review states before you press merge. They only show this sentence once the merge is refused.

Try it on the preview: teams start empty, so set one up first.

  1. As preview-admin, make a team whose member reviews on preview-admin/sample.
  2. Name that team on an issue.
  3. Open a pull whose body says Closes #<issue>.
  4. POST /api/v1/repos/preview-admin/sample/pulls/<n>/merge answers 409 with the sentence.
  5. Approve the head as that reviewer, and the merge lands.

Checks: bun scripts/gate.ts passes against the merge base with main, now that a commit carries the Try: line the check asked for. bun x tsc --noEmit and bun scripts/design-lint.ts are clean, and test/mcp-size.test.ts passes. Running test/pull*.test.ts, test/issue-teams*.test.ts and test/teams-words.test.ts together gave 188 pass, 2 skip and 2 fail. Both failures were in test/pull-quiet.test.ts, which passed all 4 tests when run alone with this change (twice) and without it, so they look like timing under load rather than this change. No existing test was edited.

I couldn't call keep_plan because the tool isn't available in this session, so there is no plan posted on the issue.

🤖 Generated with Claude Code

Changes requested — before the latest push

The gate itself is sound: it sits in core mergePull, reads the live head, and break-glass skips it. One thing to fix first: the terminal's merge shows garbage when a team holds the pull. In src/tui/screens/pull.ts:775 any non-guarded object error goes through errorText(String(why)), so the new { kind: 'team_review_required' } error renders as '[object Object]'. Add a branch for it that uses teamHoldRefusal, as the guarded branch uses guardedRefusal, and add a test that would fail without it. The terminal is a door like the page, so this is a change request. Smaller: the new test file covers mergeFailure and the API but not the terminal; box 2 also says 'MCP merge' and 'the API error map', and I could not confirm the map entry beyond the inline 409 branch in api.ts. Separately, the merge box on the page and in the terminal still show 'ready' before merge while a team holds, so the person learns only on refusal. Surface teamsHolding there, or open an issue if you leave it.

commented

A merge on a pull now stops with 409 team_review_required while a team named on the pull's issue has a reviewer on this repository who has not approved the pull's head commit. Every way into the merge gives the same sentence: "Waiting on Design's review. Nothing was merged." The check runs inside mergePull against the head as it is at merge time, and the owner's break-glass still skips it.

Review fixes in this push:

  • Terminal: the pull screen's merge (src/tui/screens/pull.ts) now goes through a new mergeRefusal. A team hold gives teamHoldRefusal, a guarded path gives guardedRefusal, and any other structured git error gives its kind as words instead of [object Object].
  • Studio and fleet: their merge actions (src/web/studio/actions.ts, src/fleet/verbs.ts) also printed only the bare code before; they now say the same sentence.
  • MCP: the merge tool (src/http/mcp/tools/merge.ts) words its refusal with the page's mergeFailure, so it already said the sentence. There is no separate API error map: the 409 is the inline branch in src/http/api.ts. The earlier description wrongly named a map.
  • Test: test/pull-tui-teams.test.ts gains a screen test. It opens a pull that the Design team holds, presses m then enter, and expects the sentence, no [object Object], and the pull still unmerged. Without the fix it fails on [object Object].

Not done here: before you press merge, the merge box on the page and in the terminal still says "ready", with "Design: awaited" on the line below it. Changing that state touches both doors' merge-box logic, so it is in #10136.

Checks: the type check, scripts/gate.ts and the design lint pass. scripts/fast.ts found the diff too broad to name tests, so I ran the 18 test files covering MCP merge, fleet, studio actions, the terminal pull screens and the team merge gate: 91 pass, 0 fail.

To try it on the preview: as preview-admin on preview-admin/sample:

  1. Make a team with a reviewer and name it on an issue.
  2. Open a pull whose body says Closes #<issue>.
  3. Merge it from the page, the terminal (m) or POST …/pulls/<n>/merge. Each answers "Waiting on <Team>'s review. Nothing was merged."
  4. Approve as that reviewer and the merge lands.

🤖 Generated with Claude Code

Approved

Earlier asks met: the terminal merge toast now words a structured refusal via mergeRefusal (no [object Object], tested in pull-tui-teams), and studio and fleet say the same sentence. The gate sits in core mergePull on the live head, break-glass skips it, and API/MCP/web/TUI share teamHoldRefusal. Non-blocking: the pre-merge merge box still says ready while a team holds, tracked in #10136.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs