Closes gitea-mcp#63. Enforces local-dev rule 5's second half (risk/medium+ is reviewed by a human), which until now was a convention (local-dev#27).
Behaviour:tbd_ship takes an optional issue. It squash-merges only when CI is fully green, the base is not review-protected AND that issue has exactly one risk/ label, risk/low. No issue / no label / several labels / unknown value / risk/medium / risk/high / unreadable issue → PR opened, merged:false, reason, and the risk tier in the result. The zero tier is not low.
Tests: gate table (every non-low tier with green CI), classifyRisk table (case, prefix containment, duplicates), end-to-end through the fake Gitea (medium/high/none/ambiguous/unknown/case, issue lookup 500/404/403, risk/low not overriding red CI or a protected base, tier reported). Nine mutants (gate ignores risk, no-issue=low, unreadable=low, prefix→contains, ambiguous=low, unknown=low, medium=low, …) each turn tests red. task check exit 0.
Behaviour change for callers: a tbd_ship call without issue now opens the PR and stops (it used to merge on green). That is the point; callers that want merge-on-green pass the risk/low issue.
Stated limit: the gate enforces the tier of the issue the caller names; it cannot prove that issue is the change's own. The link is the caller's claim.
Not done here / for Mathias: merging this PR triggers gitea-mcp's CD to production on koala. Gate code (rule 8): an adversarial review by execution is requested before merge; this PR is deliberately left open for it.
Closes gitea-mcp#63. Enforces local-dev rule 5's second half (risk/medium+ is reviewed by a human), which until now was a convention (local-dev#27).
**Behaviour:** `tbd_ship` takes an optional `issue`. It squash-merges only when CI is fully green, the base is not review-protected AND that issue has exactly one `risk/` label, `risk/low`. No issue / no label / several labels / unknown value / `risk/medium` / `risk/high` / unreadable issue → PR opened, `merged:false`, `reason`, and the `risk` tier in the result. The zero tier is not low.
**Tests:** gate table (every non-low tier with green CI), `classifyRisk` table (case, prefix containment, duplicates), end-to-end through the fake Gitea (medium/high/none/ambiguous/unknown/case, issue lookup 500/404/403, risk/low not overriding red CI or a protected base, tier reported). Nine mutants (gate ignores risk, no-issue=low, unreadable=low, prefix→contains, ambiguous=low, unknown=low, medium=low, …) each turn tests red. `task check` exit 0.
**Behaviour change for callers:** a `tbd_ship` call without `issue` now opens the PR and stops (it used to merge on green). That is the point; callers that want merge-on-green pass the `risk/low` issue.
**Stated limit:** the gate enforces the tier of the issue the caller names; it cannot prove that issue is the change's own. The link is the caller's claim.
**Not done here / for Mathias:** merging this PR triggers gitea-mcp's CD to production on koala. Gate code (rule 8): an adversarial review by execution is requested before merge; this PR is deliberately left open for it.
---
_Created via git-mcp on behalf of @mathias_
tbd_ship takes an optional `issue`; the PR is squash-merged only when CI is fully green, the base is not
review-protected AND the issue carries exactly one risk/ label, risk/low. No issue, no label, several labels,
an unknown value, risk/medium, risk/high or an unreadable issue all fail closed to PR-only with a reason, and the
result reports the tier. The zero tier is not low. Tests: gate table incl. every non-low tier, classifyRisk table,
end-to-end cases (green+medium, lookup 500/404/403, label case, containment), nine mutants flip-tested red.
Limit: the gate enforces the tier of the issue the caller names; it cannot prove it is the right issue.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZkhi49hfeSqBp5aZJNXsM
Adversarial review verdict (gate code, local-dev rule 8): PASS at c7c283d (+ nit commit). Agent reviewer, not four-eyes; review only, not merged.
By execution: label tricks (trailing space/newline/NUL, Cyrillic and zero-width lookalikes, risk/low/extra, risk/ low, case, fullwidth slash), duplicate/multiple labels in any order, issue as duplicate/string/float/overflow/negative/null, alias shadowing (number/index cannot reach issue), issue lookup 404/403/500/malformed JSON, resume path, merge-conflict path, and the only MergePullRequest call site in tbd_ship sits behind evaluateShipGate. No bypass. 14 reviewer mutants, 12 killed; the 2 survivors were an equivalent dead nil-guard and a test gap (value case/whitespace) now pinned by new tests (mutant re-run: 4 failing). go test ./... -race -count=1 passes; task check exit 0.
Stated limits (not bypasses): the caller names the issue (an unrelated, closed, or its own PR number that carries risk/low is taken on trust; documented); pr_merge (internal/tools/pr_merge.go) is a separate merge path with no tier gate; fakes, not real Gitea, so the real issue shape for a PR number is unverified.
Not merged on purpose: merging triggers gitea-mcp's CD to production. Behaviour change for callers: tbd_ship without issue now opens the PR and stops.
**Adversarial review verdict (gate code, local-dev rule 8): PASS** at c7c283d (+ nit commit). Agent reviewer, not four-eyes; review only, not merged.
By execution: label tricks (trailing space/newline/NUL, Cyrillic and zero-width lookalikes, `risk/low/extra`, `risk/ low`, case, fullwidth slash), duplicate/multiple labels in any order, `issue` as duplicate/string/float/overflow/negative/null, alias shadowing (`number`/`index` cannot reach `issue`), issue lookup 404/403/500/malformed JSON, resume path, merge-conflict path, and the only `MergePullRequest` call site in tbd_ship sits behind `evaluateShipGate`. No bypass. 14 reviewer mutants, 12 killed; the 2 survivors were an equivalent dead nil-guard and a test gap (value case/whitespace) now pinned by new tests (mutant re-run: 4 failing). `go test ./... -race -count=1` passes; `task check` exit 0.
Stated limits (not bypasses): the caller names the issue (an unrelated, closed, or its own PR number that carries `risk/low` is taken on trust; documented); `pr_merge` (internal/tools/pr_merge.go) is a separate merge path with no tier gate; fakes, not real Gitea, so the real issue shape for a PR number is unverified.
**Not merged on purpose:** merging triggers gitea-mcp's CD to production. Behaviour change for callers: `tbd_ship` without `issue` now opens the PR and stops.
---
_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.
Closes gitea-mcp#63. Enforces local-dev rule 5's second half (risk/medium+ is reviewed by a human), which until now was a convention (local-dev#27).
Behaviour:
tbd_shiptakes an optionalissue. It squash-merges only when CI is fully green, the base is not review-protected AND that issue has exactly onerisk/label,risk/low. No issue / no label / several labels / unknown value /risk/medium/risk/high/ unreadable issue → PR opened,merged:false,reason, and therisktier in the result. The zero tier is not low.Tests: gate table (every non-low tier with green CI),
classifyRisktable (case, prefix containment, duplicates), end-to-end through the fake Gitea (medium/high/none/ambiguous/unknown/case, issue lookup 500/404/403, risk/low not overriding red CI or a protected base, tier reported). Nine mutants (gate ignores risk, no-issue=low, unreadable=low, prefix→contains, ambiguous=low, unknown=low, medium=low, …) each turn tests red.task checkexit 0.Behaviour change for callers: a
tbd_shipcall withoutissuenow opens the PR and stops (it used to merge on green). That is the point; callers that want merge-on-green pass therisk/lowissue.Stated limit: the gate enforces the tier of the issue the caller names; it cannot prove that issue is the change's own. The link is the caller's claim.
Not done here / for Mathias: merging this PR triggers gitea-mcp's CD to production on koala. Gate code (rule 8): an adversarial review by execution is requested before merge; this PR is deliberately left open for it.
Created via git-mcp on behalf of @mathias
Adversarial review verdict (gate code, local-dev rule 8): PASS at
c7c283d(+ nit commit). Agent reviewer, not four-eyes; review only, not merged.By execution: label tricks (trailing space/newline/NUL, Cyrillic and zero-width lookalikes,
risk/low/extra,risk/ low, case, fullwidth slash), duplicate/multiple labels in any order,issueas duplicate/string/float/overflow/negative/null, alias shadowing (number/indexcannot reachissue), issue lookup 404/403/500/malformed JSON, resume path, merge-conflict path, and the onlyMergePullRequestcall site in tbd_ship sits behindevaluateShipGate. No bypass. 14 reviewer mutants, 12 killed; the 2 survivors were an equivalent dead nil-guard and a test gap (value case/whitespace) now pinned by new tests (mutant re-run: 4 failing).go test ./... -race -count=1passes;task checkexit 0.Stated limits (not bypasses): the caller names the issue (an unrelated, closed, or its own PR number that carries
risk/lowis taken on trust; documented);pr_merge(internal/tools/pr_merge.go) is a separate merge path with no tier gate; fakes, not real Gitea, so the real issue shape for a PR number is unverified.Not merged on purpose: merging triggers gitea-mcp's CD to production. Behaviour change for callers:
tbd_shipwithoutissuenow opens the PR and stops.Created via git-mcp on behalf of @mathias