Source: adversarial review of PR #73 (the tbd_ship tier gate), 2026-10-07.
#63/#73 gates tbd_ship only. internal/tools/pr_merge.go:67 merges any PR it is handed, with no CI, protection or risk-tier check, so an agent that wants to merge a risk/medium change can still do it by calling pr_merge directly. Local-dev rule 5's reviewed lane therefore stays a convention for that path.
Decide (Mathias): (a) gate pr_merge on the same tier + green CI (needs an issue or a label on the PR, since a PR number alone carries no classification), (b) remove pr_merge from the agent-facing tool set and keep merging a human act, or (c) accept and state the limit in assessor-loop §5 (done by local-dev#27's text). Also open: branch protection on main (local-dev#27 option 2), which would hold for every tool.
Source: adversarial review of PR #73 (the tbd_ship tier gate), 2026-10-07.
#63/#73 gates `tbd_ship` only. `internal/tools/pr_merge.go:67` merges any PR it is handed, with no CI, protection or risk-tier check, so an agent that wants to merge a `risk/medium` change can still do it by calling `pr_merge` directly. Local-dev rule 5's reviewed lane therefore stays a convention for that path.
Decide (Mathias): (a) gate `pr_merge` on the same tier + green CI (needs an `issue` or a label on the PR, since a PR number alone carries no classification), (b) remove `pr_merge` from the agent-facing tool set and keep merging a human act, or (c) accept and state the limit in assessor-loop §5 (done by local-dev#27's text). Also open: branch protection on `main` (local-dev#27 option 2), which would hold for every tool.
Related: gitea-mcp#63, PR #73, local-dev#27.
---
_Created via git-mcp on behalf of @mathias_
Done and live 2026-10-08 (PR #76, squash d1e501c, tag v0.18.0). Mathias decided: gate pr_merge.
What:pr_merge runs the same gate as tbd_ship (shared evaluateMergeGate): CI fully green on the PR's head commit, base not review-protected, exactly one risk/low label on the issue named in issue; otherwise merged:false with reason, ci_status, risk. Not-open and draft PRs are refused. An unreadable PR or protection probe is an error; a merge conflict still is. Both tools now bind the merge to the evaluated head commit (head_commit_id, present in Gitea 1.27.1's MergePullRequestOption) so a push after the check cannot slip in.
Adversarial review, 2 rounds: round 1 found a real bypass (an empty head sha unscoped the CI query and dropped the merge binding, also affecting the already-live tbd_ship); fixed with evaluateMergeGate + a pollRuns guard + tests; round 2 PASS. Two caller-wiring mutants survive as equivalent (documented). Whitespace-only sha is not rejected (not a bypass).
Verified live through the production MCP:pr_merge descriptor now has issue and ci_timeout_seconds and mentions risk/low. On a throwaway PR (#78, green CI, closed, branch deleted, nothing on main): issue=63 (risk/medium) → merged:false, ci_status:success, risk:medium; no issue → merged:false, risk:none; again with squash → refused; the PR stayed open and unmerged.
Not live-tested: the positive case (a risk/low issue merging) and Gitea's server-side refusal of a moved head.
Remaining, tracked: UI merges and file_write_branch commits to main are outside the gate; the platform-level control is branch protection on main (decision filed in local-dev). merge_message_title was never a Gitea field → #77.
Behaviour change:pr_merge without issue, or on a PR without green CI (or in a repo with no CI), merges nothing; merge those in the Gitea UI.
Done and live 2026-10-08 (PR #76, squash d1e501c, tag v0.18.0). Mathias decided: gate `pr_merge`.
- **What:** `pr_merge` runs the same gate as `tbd_ship` (shared `evaluateMergeGate`): CI fully green on the PR's head commit, base not review-protected, exactly one `risk/low` label on the issue named in `issue`; otherwise `merged:false` with `reason`, `ci_status`, `risk`. Not-open and draft PRs are refused. An unreadable PR or protection probe is an error; a merge conflict still is. Both tools now bind the merge to the evaluated head commit (`head_commit_id`, present in Gitea 1.27.1's `MergePullRequestOption`) so a push after the check cannot slip in.
- **Adversarial review, 2 rounds:** round 1 found a real bypass (an empty head sha unscoped the CI query and dropped the merge binding, also affecting the already-live `tbd_ship`); fixed with `evaluateMergeGate` + a `pollRuns` guard + tests; round 2 PASS. Two caller-wiring mutants survive as equivalent (documented). Whitespace-only sha is not rejected (not a bypass).
- **Deployed:** infra pinned `localhost:5000/gitea-mcp:d1e501c`; rollout 1/1, 0 restarts; rollback target `d3faffc`.
- **Verified live through the production MCP:** `pr_merge` descriptor now has `issue` and `ci_timeout_seconds` and mentions risk/low. On a throwaway PR (#78, green CI, closed, branch deleted, nothing on main): `issue=63` (`risk/medium`) → `merged:false, ci_status:success, risk:medium`; no issue → `merged:false, risk:none`; again with squash → refused; the PR stayed open and unmerged.
- **Not live-tested:** the positive case (a `risk/low` issue merging) and Gitea's server-side refusal of a moved head.
- **Remaining, tracked:** UI merges and `file_write_branch` commits to `main` are outside the gate; the platform-level control is branch protection on `main` (decision filed in local-dev). `merge_message_title` was never a Gitea field → #77.
- **Behaviour change:** `pr_merge` without `issue`, or on a PR without green CI (or in a repo with no CI), merges nothing; merge those in the Gitea UI.
---
_Created via git-mcp on behalf of @mathias_
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Source: adversarial review of PR #73 (the tbd_ship tier gate), 2026-10-07.
#63/#73 gates
tbd_shiponly.internal/tools/pr_merge.go:67merges any PR it is handed, with no CI, protection or risk-tier check, so an agent that wants to merge arisk/mediumchange can still do it by callingpr_mergedirectly. Local-dev rule 5's reviewed lane therefore stays a convention for that path.Decide (Mathias): (a) gate
pr_mergeon the same tier + green CI (needs anissueor a label on the PR, since a PR number alone carries no classification), (b) removepr_mergefrom the agent-facing tool set and keep merging a human act, or (c) accept and state the limit in assessor-loop §5 (done by local-dev#27's text). Also open: branch protection onmain(local-dev#27 option 2), which would hold for every tool.Related: gitea-mcp#63, PR #73, local-dev#27.
Created via git-mcp on behalf of @mathias
Done and live 2026-10-08 (PR #76, squash
d1e501c, tag v0.18.0). Mathias decided: gatepr_merge.pr_mergeruns the same gate astbd_ship(sharedevaluateMergeGate): CI fully green on the PR's head commit, base not review-protected, exactly onerisk/lowlabel on the issue named inissue; otherwisemerged:falsewithreason,ci_status,risk. Not-open and draft PRs are refused. An unreadable PR or protection probe is an error; a merge conflict still is. Both tools now bind the merge to the evaluated head commit (head_commit_id, present in Gitea 1.27.1'sMergePullRequestOption) so a push after the check cannot slip in.tbd_ship); fixed withevaluateMergeGate+ apollRunsguard + tests; round 2 PASS. Two caller-wiring mutants survive as equivalent (documented). Whitespace-only sha is not rejected (not a bypass).localhost:5000/gitea-mcp:d1e501c; rollout 1/1, 0 restarts; rollback targetd3faffc.pr_mergedescriptor now hasissueandci_timeout_secondsand mentions risk/low. On a throwaway PR (#78, green CI, closed, branch deleted, nothing on main):issue=63(risk/medium) →merged:false, ci_status:success, risk:medium; no issue →merged:false, risk:none; again with squash → refused; the PR stayed open and unmerged.risk/lowissue merging) and Gitea's server-side refusal of a moved head.file_write_branchcommits tomainare outside the gate; the platform-level control is branch protection onmain(decision filed in local-dev).merge_message_titlewas never a Gitea field → #77.pr_mergewithoutissue, or on a PR without green CI (or in a repo with no CI), merges nothing; merge those in the Gitea UI.Created via git-mcp on behalf of @mathias