tbd_ship: gate auto-merge on the risk tier, and refuse when unclassified #63

Closed
opened 2026-08-29 12:13:21 +00:00 by mathias · 1 comment
Owner

Follows the rule-5 change in mathias/local-dev (f30dbf6): the PR trigger is now risk, not concurrency. risk/low commits straight to main; risk/medium+ opens a PR.

tbd_ship already implements the hard half — a fail-closed CI gate, described in its own code as "the load-bearing safety of tbd_ship... it must NEVER return merge=true" without every workflow green. What it lacks is the risk tier as an input, so it currently treats all work identically.

What to add

Read the issue's risk/ label and refuse to auto-merge anything above risk/low:

Tier tbd_ship behaviour
risk/low + all CI green auto-merge (today's behaviour)
risk/medium, risk/high refuse — leave the PR open for a human, and say why
no risk/ label refuse — unclassified is not low

That last row matters most. An unlabelled issue must not inherit the permissive path: the whole taxonomy rests on classification being deliberate, and defaulting absent-to-low would make forgetting a label the way to bypass review.

Why this is the right home

The alternative is a new merge tool. tbd_ship's gate is already the audited, fail-closed piece and already has tbd_ship_internal_test.go asserting the gate "must NEVER return merge=true" under failure. Adding a predicate to a tested gate beats building a second gate that needs its own proof.

Test it the way the existing gate is tested

Per the repo's own standard, the new predicate needs a case proving it refuses when it should — not just that it merges when it should:

  • risk/medium + all-green CI → no merge, reason reported
  • no risk/ label + all-green CI → no merge
  • risk/low + all-green → merges (unchanged)
  • risk/low + any non-green → no merge (unchanged, must not regress)

Context worth carrying

The estate has merged everything on green until now, which diverges from what assessor-loop's transfer story §4.1 tells a client we do. §5 of that document concedes the reviewed lane is undemonstrated. This change is what makes it demonstrated — the estate becomes its own evidence rather than an exception to its own model.

Related

  • brain fail-closed-ci-gate-auto-merge-pattern — the pattern this extends
  • brain gitea-pr-merge-autoclose-and-review-comment-footguns — merging auto-closes gated issues; worth checking whether tbd_ship should refuse to merge a PR whose linked issue carries a hold
  • mathias/local-dev .context/AGENT.md rule 5 — the rule this enforces
Follows the rule-5 change in `mathias/local-dev` (`f30dbf6`): **the PR trigger is now risk, not concurrency.** `risk/low` commits straight to main; `risk/medium`+ opens a PR. `tbd_ship` already implements the hard half — a fail-closed CI gate, described in its own code as *"the load-bearing safety of tbd_ship... it must NEVER return merge=true"* without every workflow green. What it lacks is the **risk tier as an input**, so it currently treats all work identically. ## What to add Read the issue's `risk/` label and refuse to auto-merge anything above `risk/low`: | Tier | `tbd_ship` behaviour | |---|---| | `risk/low` + all CI green | auto-merge (today's behaviour) | | `risk/medium`, `risk/high` | **refuse** — leave the PR open for a human, and say why | | no `risk/` label | **refuse** — unclassified is not low | That last row matters most. An unlabelled issue must not inherit the permissive path: the whole taxonomy rests on classification being deliberate, and defaulting absent-to-low would make forgetting a label the way to bypass review. ## Why this is the right home The alternative is a new merge tool. `tbd_ship`'s gate is already the audited, fail-closed piece and already has `tbd_ship_internal_test.go` asserting the gate *"must NEVER return merge=true"* under failure. Adding a predicate to a tested gate beats building a second gate that needs its own proof. ## Test it the way the existing gate is tested Per the repo's own standard, the new predicate needs a case proving it **refuses** when it should — not just that it merges when it should: - `risk/medium` + all-green CI → **no merge**, reason reported - no `risk/` label + all-green CI → **no merge** - `risk/low` + all-green → merges (unchanged) - `risk/low` + any non-green → no merge (unchanged, must not regress) ## Context worth carrying The estate has merged everything on green until now, which diverges from what `assessor-loop`'s transfer story §4.1 tells a client we do. §5 of that document concedes the reviewed lane is **undemonstrated**. This change is what makes it demonstrated — the estate becomes its own evidence rather than an exception to its own model. ## Related - brain `fail-closed-ci-gate-auto-merge-pattern` — the pattern this extends - brain `gitea-pr-merge-autoclose-and-review-comment-footguns` — merging auto-closes gated issues; worth checking whether `tbd_ship` should refuse to merge a PR whose linked issue carries a hold - `mathias/local-dev` `.context/AGENT.md` rule 5 — the rule this enforces
mathias added the risk/mediumsize/stype/feat labels 2026-08-29 12:13:49 +00:00
Author
Owner

Done and live 2026-10-08 (PR #73, squash d3faffc, tag v0.17.0). Mathias authorized the merge and deploy.

  • Reviewed: adversarial pass by execution (PASS, no bypass; 14 reviewer mutants, 12 killed, survivors addressed); task check green; CI green on the PR head (run 9693).
  • Deployed: CD pinned localhost:5000/gitea-mcp:d3faffc in infra; rollout 1/1 ready, 0 restarts. Rollback target was dbf78a3.
  • Verified live through the production MCP: tools/list for tbd_ship went from no issue input / no risk/low in the description (before) to both present (after). Live-fire on a throwaway PR (closed, branch deleted, nothing on main): issue #63 (risk/medium) with CI green and polled to completion → merged:false, ci_status:success, risk:medium, reason "above risk/low a human reviews the change"; no issue named → merged:false, risk:none, "unclassified is not low". Also exercised the real GetIssue response shape.
  • Not live-tested: the positive case (a risk/low issue merging on green) — it would merge a throwaway file onto main; covered by the unit and end-to-end tests.
  • Limits, tracked: the caller names the issue (trusted); pr_merge is a separate ungated merge path → #74.
  • Behaviour change: a tbd_ship call without issue now opens the PR and stops.

Created via git-mcp on behalf of @mathias

Done and live 2026-10-08 (PR #73, squash d3faffc, tag v0.17.0). Mathias authorized the merge and deploy. - **Reviewed:** adversarial pass by execution (PASS, no bypass; 14 reviewer mutants, 12 killed, survivors addressed); `task check` green; CI green on the PR head (run 9693). - **Deployed:** CD pinned `localhost:5000/gitea-mcp:d3faffc` in infra; rollout 1/1 ready, 0 restarts. Rollback target was `dbf78a3`. - **Verified live through the production MCP:** `tools/list` for `tbd_ship` went from no `issue` input / no `risk/low` in the description (before) to both present (after). Live-fire on a throwaway PR (closed, branch deleted, nothing on main): issue #63 (`risk/medium`) with CI green and polled to completion → `merged:false`, `ci_status:success`, `risk:medium`, reason "above risk/low a human reviews the change"; no issue named → `merged:false`, `risk:none`, "unclassified is not low". Also exercised the real `GetIssue` response shape. - **Not live-tested:** the positive case (a `risk/low` issue merging on green) — it would merge a throwaway file onto main; covered by the unit and end-to-end tests. - **Limits, tracked:** the caller names the issue (trusted); `pr_merge` is a separate ungated merge path → #74. - **Behaviour change:** a `tbd_ship` call without `issue` now opens the PR and stops. --- _Created via git-mcp on behalf of @mathias_
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/gitea-mcp#63