status:pr-open was applied by gitea_create_pr and never removed again. Every
terminal path finished without touching it, so a repository audit found 40
closed issues still advertising an open PR.
Add terminal_pr_label_cleanup.py as the single authoritative rule and route
every sanctioned terminal path through it, so the paths cannot drift:
- merge (gitea_merge_pr)
- close without merge (gitea_edit_pr)
- supersession/abandonment (gitea_reconcile_superseded_by_merged_pr)
- already-landed reconciliation (gitea_reconcile_already_landed_pr)
- controller closure (gitea_close_issue)
- retry/recovery (new gitea_cleanup_terminal_pr_labels)
The rule removes only status:pr-open, preserves every other label, allows an
empty resulting set, is a no-op when the label is absent (so retries are
safe), and confirms the outcome by read-after-write rather than assumption.
Controller closure runs the cleanup before the state change and fails closed
if it cannot be completed and verified; closing first would bake in the stale
label with no later step to catch it. Post-merge cleanup never blocks the
merge, which already happened, and reports failures with a safe next action.
Also:
- gitea_assess_terminal_label_hygiene: read-only terminal validation that
reports residual status:pr-open, exempting issues with a genuinely open PR.
- _put_issue_label_names now accepts Gitea's empty response body when the
requested set is empty, so clearing the last label works.
- test_audit's close_issue fixture keys on the request instead of call order,
since closing now also reads labels for the cleanup and its read-back.
Docs: label-taxonomy terminal-transition section, runbook pointer, and the
review-merge / reconcile-landed final-report terminal-label requirements.
Suite: 4045 passed, 11 failed, 6 skipped. The same 11 failures reproduce on
clean master df31674 (4010 passed, 11 failed) and are pre-existing.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
8.4 KiB
Label Taxonomy
This document defines the canonical issue labels used by MCP workflows.
Every issue should carry:
- one
type:*label - one
status:*label
Discussion-only issues must carry type:discussion.
Issue Type Labels
| Label | Use |
|---|---|
type:bug |
Bug or defect |
type:feature |
Feature or enhancement |
type:process |
Process or policy work |
type:workflow |
Workflow automation or guidance |
type:guardrail |
Safety gate or guardrail |
type:docs |
Documentation work |
type:test |
Tests or test infrastructure |
type:discussion |
Discussion-only issue |
type:umbrella |
Umbrella or tracker issue |
type:cleanup |
Cleanup or hygiene work |
Workflow Status Labels
Only one status:* label should be active on an issue at a time. When an issue
moves forward, tooling must remove the old status:* label and apply the new
one.
| Label | Use |
|---|---|
status:triage |
Issue needs triage |
status:ready |
Issue is ready for work |
status:claimed |
Issue is claimed |
status:in-progress |
Issue is being worked on |
status:blocked |
Issue is blocked |
status:needs-review |
Issue work needs review |
status:pr-open |
A linked PR is open |
status:changes-requested |
Reviewer requested changes on the linked PR |
status:approved |
Linked PR is approved |
status:merged |
Linked PR is merged |
status:reconcile |
Issue needs reconciliation |
status:done |
Issue workflow is complete |
status:duplicate |
Issue is a duplicate |
status:wontfix |
Issue will not be fixed |
Role Ownership Labels (#603)
A single role:* label shows which workflow role currently owns the item. It is
advisory visibility only — the control-plane lease (#601) is the source of truth
for mutation authority. Only one role:* label is active at a time; tooling
replaces it on handoff via transition_role_labels.
| Label | Use |
|---|---|
role:author |
Author currently owns the item |
role:reviewer |
Reviewer currently owns the item |
role:merger |
Merger currently owns the item |
Hazard Labels (#603)
Hazard labels are orthogonal warning flags. Unlike status:* and role:*,
more than one hazard may be active at once, and a hazard never substitutes
for a live lease / PR-state check. Add/remove with add_hazard_label /
clear_hazard_label.
| Label | Use |
|---|---|
hazard:stale-lease |
A stale or expired lease references this item |
hazard:workflow-contaminated |
Session/workflow state is contaminated; do not mutate |
hazard:conflicted |
Linked PR has merge conflicts |
hazard:root-mutation |
Work was mutated in the project root checkout |
hazard:manual-state |
Session or lease state was edited manually |
hazard:terminal-blocker |
A terminal review/merge lock blocks progress (#332/#602) |
Any item that carries status:blocked or any hazard:* flag must also have a
blocking-reason / next-action comment (requires_blocking_reason).
state:* → canonical mapping (#603 migration)
Issue #603 proposed a parallel state:* vocabulary. To avoid a conflicting
second lifecycle prefix, those requested states are folded into the existing
canonical labels rather than introduced as state:*. state:* is not a
supported prefix; use the canonical label on the right.
Requested state:* |
Canonical label |
|---|---|
state:needs-triage |
status:triage |
state:claimed |
status:claimed |
state:authoring |
status:in-progress |
state:needs-review |
status:needs-review |
state:reviewing |
status:needs-review |
state:changes-requested |
status:changes-requested |
state:approved |
status:approved |
state:merge-ready |
status:approved |
state:merged |
status:merged |
state:blocked |
status:blocked |
state:terminal-blocker |
hazard:terminal-blocker |
state:abandoned |
status:wontfix |
The transition helpers accept these names as synonyms (e.g.
canonical_status_label("authoring") → status:in-progress), so callers may use
the #603 wording while a single canonical status stays active.
Allocator Cross-Check (#603)
Labels are advisory queue hints. The work allocator (#600/#613) uses labels as
one signal but cross-checks live leases and PR state and never trusts labels
alone. Discussion issues (type:discussion) are excluded from implementation
queues (is_implementation_candidate) unless a controller explicitly selects
them.
Transition Rules
Suggested lifecycle:
- New issue created:
status:triageorstatus:ready - Issue selected by an author:
status:claimed - Author starts work:
status:in-progress - Work is blocked:
status:blocked - PR opened:
status:pr-open - PR approved:
status:approved - PR merged but issue still needs closure/reconciliation:
status:reconcile - Issue fully complete:
status:done - Duplicate issue:
status:duplicate - Won't-fix issue:
status:wontfix
The helper module issue_workflow_labels.py is the source of truth for the
canonical label specs and status transition replacement behavior.
Terminal PR transitions retire status:pr-open (#780)
status:pr-open states that a linked PR is currently open. The moment that
stops being true the label must go, whatever ended the PR:
| Terminal reason | Raised by |
|---|---|
merged |
gitea_merge_pr |
closed_without_merge |
gitea_edit_pr closing the PR |
superseded |
gitea_reconcile_superseded_by_merged_pr |
already_landed |
gitea_reconcile_already_landed_pr |
controller_closure |
gitea_close_issue |
abandoned |
abandonment handling |
retry_recovery |
gitea_cleanup_terminal_pr_labels after a partial failure |
All of these route through one rule in terminal_pr_label_cleanup.py, so the
paths cannot drift apart. The rule guarantees:
- only
status:pr-openis removed — every other label is preserved verbatim; - an empty resulting label set is valid (it was the issue's only label);
- an issue that no longer carries the label is a no-op, so retries are safe;
- the result is confirmed by a read-after-write re-read, not assumed.
Controller closure runs the cleanup before changing issue state and fails
closed if it cannot be completed and verified — closing first would bake in the
stale label with no later step to catch it. Post-merge cleanup never blocks the
merge: the transition already happened, so failures are reported with a
safe_next_action instead.
Use gitea_assess_terminal_label_hygiene as terminal validation before
declaring a transition or cleanup batch complete. It enumerates issues plus the
live open PRs and reports any issue still carrying status:pr-open without an
open PR to justify it. Issues with a genuinely open PR are exempt, not
residual.
Recovery from a partial failure is gitea_cleanup_terminal_pr_labels with
terminal_reason='retry_recovery'.
Discussion Issues
Discussion issues must be labeled type:discussion.
A discussion issue should not be treated as implementation-ready unless it also has a clear implementation status and next action.
If a discussion produces implementation work, either:
- convert the discussion issue into an implementation issue by changing labels and adding acceptance criteria, or
- create child implementation issues and leave the discussion issue as
type:discussion.
Tooling
manage_labels.py --create-labelscreates the canonicaltype:*andstatus:*labels.gitea_create_issuerecommendstype:*andstatus:*labels when missing and can apply supplied label names.gitea_mark_issue(..., action="start")replaces oldstatus:*labels withstatus:in-progress.gitea_create_prfails closed before PR creation ifstatus:pr-opencannot be applied to the locked issue, then applies it after the PR is created.gitea_set_issue_labelsaccepts an explicitworktree_pathso author sessions can satisfy the branches-only mutation guard while changing labels.gitea_cleanup_terminal_pr_labelsretiresstatus:pr-openafter a terminal PR transition; it is idempotent, so it is also the retry/recovery path.gitea_assess_terminal_label_hygieneis the read-only terminal validation for residualstatus:pr-open.
Existing Non-Workflow Labels
Existing non-workflow labels such as mcp, workflow, labels, tracker,
jenkins, glitchtip, documentation, and testing remain valid topical
labels. They do not replace the required type:* and status:* labels.