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]>
208 lines
8.4 KiB
Markdown
208 lines
8.4 KiB
Markdown
# 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:
|
|
|
|
1. New issue created: `status:triage` or `status:ready`
|
|
2. Issue selected by an author: `status:claimed`
|
|
3. Author starts work: `status:in-progress`
|
|
4. Work is blocked: `status:blocked`
|
|
5. PR opened: `status:pr-open`
|
|
6. PR approved: `status:approved`
|
|
7. PR merged but issue still needs closure/reconciliation: `status:reconcile`
|
|
8. Issue fully complete: `status:done`
|
|
9. Duplicate issue: `status:duplicate`
|
|
10. 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-open` is 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:
|
|
|
|
1. convert the discussion issue into an implementation issue by changing labels
|
|
and adding acceptance criteria, or
|
|
2. create child implementation issues and leave the discussion issue as
|
|
`type:discussion`.
|
|
|
|
## Tooling
|
|
|
|
- `manage_labels.py --create-labels` creates the canonical `type:*` and
|
|
`status:*` labels.
|
|
- `gitea_create_issue` recommends `type:*` and `status:*` labels when missing
|
|
and can apply supplied label names.
|
|
- `gitea_mark_issue(..., action="start")` replaces old `status:*` labels with
|
|
`status:in-progress`.
|
|
- `gitea_create_pr` fails closed before PR creation if `status:pr-open` cannot
|
|
be applied to the locked issue, then applies it after the PR is created.
|
|
- `gitea_set_issue_labels` accepts an explicit `worktree_path` so author
|
|
sessions can satisfy the branches-only mutation guard while changing labels.
|
|
- `gitea_cleanup_terminal_pr_labels` retires `status:pr-open` after a terminal
|
|
PR transition; it is idempotent, so it is also the retry/recovery path.
|
|
- `gitea_assess_terminal_label_hygiene` is the read-only terminal validation
|
|
for residual `status: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.
|