Skip to content

User-defined completion labels: claim-eligibility guard for 'work complete' issues #4186

Description

@Trecek

User-defined completion labels: claim-eligibility guard for "work complete" issues

Problem

The claim boundary (claim_issue / claim_and_resolve_issue) enforces only two eligibility conditions: the issue is not closed, and the in-progress label is not held by a live session. There is no concept of "this work is already complete."

Staged issues remain open by design — they close only when the promotion PR to main merges via Closes #N. During the entire develop→main promotion window, a staged issue is open, carries no in-progress label, and is therefore fully re-claimable. Incident: issue #4180 (merged to develop via PR #4183, labeled staged at 2026-07-05T03:23Z) was re-claimed at 16:08Z by a user-supplied-URL remediation run, re-labeled in-progress (with staged retained — an unreachable state per the formal model), re-branched, and re-processed.

Because the closed-state guard structurally cannot cover the promotion window, completion must be expressible as a label — and different projects use different labels for "done" (staged, complete, shipped, released). This should be user-configurable, defaulting to the existing staged label.

Requirements

REQ-CFG-001: Users must be able to configure a set of completion labels (github.completion_labels) meaning "work complete — not claimable" via the standard AutoSkillit config hierarchy (defaults.yaml~/.autoskillit/config.yaml{project}/.autoskillit/config.yaml).

REQ-CFG-002: When completion_labels is unset (null), the effective completion set must default to the configured github.staged_label, automatically tracking renames of that label.

REQ-CFG-003: Setting completion_labels: [] must disable the guard entirely (mirroring the allowed_labels empty-list = unrestricted precedent).

REQ-CFG-004: Config validation must reject at load time a completion set that intersects the configured in_progress_label, queued_label, or fail_label (active/retryable states must not be markable as complete).

REQ-GUARD-001: Both claim_issue and claim_and_resolve_issue must reject, server-authoritatively, any claim attempt against an issue carrying a completion label, returning the established {"success": true, "claimed": false, "reason": ...} envelope.

REQ-GUARD-002: The completion check must take precedence over the in-progress liveness path when both label types are present, and existing behavior (closed-issue guard, in-progress liveness/ownership semantics, release_issue staging) must remain unchanged.

REQ-OVR-001: An explicit per-call override (allow_completed: bool = False) on both claim tools must permit a deliberate re-claim without weakening the default, mirroring the existing allow_reentry parameter.

Design sketch

Config surface (GitHubConfig, config/_config_dataclasses.py:205-209; config/defaults.yaml github block)

Where users set it: the standard AutoSkillit config hierarchy (config/settings.py:4-6) — shipped default in src/autoskillit/config/defaults.yaml, overridable in user config ~/.autoskillit/config.yaml, overridable again in project config {project}/.autoskillit/config.yaml. Users add it under the existing github: section alongside staged_label / in_progress_label:

# .autoskillit/config.yaml (project) or ~/.autoskillit/config.yaml (user)
github:
  completion_labels: null   # null → [staged_label]; [] → guard disabled; or custom list, e.g. ["shipped", "released"]
completion_labels: list[str] | None = None

@property
def effective_completion_labels(self) -> frozenset[str]:
    if self.completion_labels is None:
        return frozenset({self.staged_label})
    return frozenset(self.completion_labels)
  • None default keys off staged_label, so a user who renames staged to complete gets the right guard with zero extra config.
  • Custom labels need not be IssueLabelState members — the guard is a set-membership check, not a state-machine lookup, so arbitrary user labels work.

Enforcement point (server/tools/_claim_helpers.py:_try_claim_with_liveness)

Both tools already call this shared helper with current_labels in hand (tools_issue_labels.py:107, tools_issue_composite.py:137). Add completion_labels: frozenset[str] and allow_completed: bool parameters and a first branch before the in-progress presence check:

hits = sorted(set(current_labels) & completion_labels)
if hits and not allow_completed:
    return ClaimDecision(
        claimed=False,
        reason=(
            f"Issue #{issue_number} has completion label '{hits[0]}' — work is already "
            f"complete (merged, awaiting promotion). Pass allow_completed=true to deliberately re-claim."
        ),
    )

Override (mirrors allow_reentry, _claim_helpers.py:82)

  • allow_completed: bool = False parameter on claim_issue and claim_and_resolve_issue.
  • No session-scoped or config kill-switch needed: completion_labels: [] already provides global opt-out; a per-call flag covers deliberate re-runs. (Follows the reset_dispatch(force=True) / claim_issue(allow_reentry=True) precedent.)

Config validation (load-time)

  • Error if the effective completion set intersects {in_progress_label, queued_label, fail_label} — those are active/retryable states; marking them "complete" would deadlock claiming.
  • Overlap with staged_label is expected (it is the default).
  • No allowed_labels requirement: completion labels are blockers the claim tools read, never labels they apply.

Defense in depth (secondary; can be follow-up scope)

  • triage-issues/SKILL.md:74-75 — jq candidate filter currently excludes only hardcoded "in-progress". Extend to exclude completion labels via config templating (precedent: {{github.in_progress_label}} in build-execution-map/SKILL.md:95; a list would render as a JSON array for a jq any(...) test).
  • hooks/guards/fleet_claim_guard.py:62 — hook hardcodes "in-progress" and is stdlib-only/fail-open. Extending it to completion labels requires plumbing label config through the hook config overlay. Optional: the server guard is authoritative and catches dispatched claims at claim time regardless.

Test plan

  • Both claim tools reject an issue whose mock labels include staged under default config (claimed=false, reason mentions completion) — the exact missing regression from the Per-run post_run_diagnostics enablement: demote to config-as-default (overridable + lockable) and complete the authority-feedback consumption contract #4180 incident.
  • Custom completion_labels: ["shipped"] honored; staged alone no longer blocks in that config.
  • staged_label: "complete" rename with completion_labels: nullcomplete blocks (default tracks rename).
  • allow_completed=true bypasses; completion_labels: [] disables.
  • Precedence: labels [staged, in-progress] → completion reason wins over liveness path.
  • Config validation: overlap with in-progress/queued/fail raises at load.
  • Existing closed-issue and liveness tests unchanged/green.

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions