You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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"]
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)
ifhitsandnotallow_completed:
returnClaimDecision(
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."
),
)
Response envelope unchanged: tools already serialize ClaimDecision.reason as {"success": true, "claimed": false, "reason": ...} — same shape as the closed-issue guard. Recipes route through their existing claimed != true branch (register_clone_failure) with zero recipe changes.
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.
Config validation: overlap with in-progress/queued/fail raises at load.
Existing closed-issue and liveness tests unchanged/green.
Out of scope
Wiring validate_label_transition / LABEL_TRANSITIONS enforcement at the claim boundary (complementary, enum-only — cannot cover custom labels; candidate for a separate /rectify-driven ticket).
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 thein-progresslabel 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
mainmerges viaCloses #N. During the entire develop→main promotion window, a staged issue is open, carries noin-progresslabel, and is therefore fully re-claimable. Incident: issue #4180 (merged to develop via PR #4183, labeledstagedat 2026-07-05T03:23Z) was re-claimed at 16:08Z by a user-supplied-URL remediation run, re-labeledin-progress(withstagedretained — 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_labelsis unset (null), the effective completion set must default to the configuredgithub.staged_label, automatically tracking renames of that label.REQ-CFG-003: Setting
completion_labels: []must disable the guard entirely (mirroring theallowed_labelsempty-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, orfail_label(active/retryable states must not be markable as complete).REQ-GUARD-001: Both
claim_issueandclaim_and_resolve_issuemust 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_issuestaging) 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 existingallow_reentryparameter.Design sketch
Config surface (
GitHubConfig,config/_config_dataclasses.py:205-209;config/defaults.yamlgithub block)Where users set it: the standard AutoSkillit config hierarchy (
config/settings.py:4-6) — shipped default insrc/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 existinggithub:section alongsidestaged_label/in_progress_label:Nonedefault keys offstaged_label, so a user who renames staged tocompletegets the right guard with zero extra config.IssueLabelStatemembers — 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_labelsin hand (tools_issue_labels.py:107,tools_issue_composite.py:137). Addcompletion_labels: frozenset[str]andallow_completed: boolparameters and a first branch before the in-progress presence check:staged+in-progress, as Per-run post_run_diagnostics enablement: demote to config-as-default (overridable + lockable) and complete the authority-feedback consumption contract #4180 now has) reports "complete" rather than "in progress elsewhere."ClaimDecision.reasonas{"success": true, "claimed": false, "reason": ...}— same shape as the closed-issue guard. Recipes route through their existingclaimed != truebranch (register_clone_failure) with zero recipe changes.Override (mirrors
allow_reentry,_claim_helpers.py:82)allow_completed: bool = Falseparameter onclaim_issueandclaim_and_resolve_issue.completion_labels: []already provides global opt-out; a per-call flag covers deliberate re-runs. (Follows thereset_dispatch(force=True)/claim_issue(allow_reentry=True)precedent.)Config validation (load-time)
{in_progress_label, queued_label, fail_label}— those are active/retryable states; marking them "complete" would deadlock claiming.staged_labelis expected (it is the default).allowed_labelsrequirement: 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}}inbuild-execution-map/SKILL.md:95; a list would render as a JSON array for a jqany(...)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
stagedunder 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.completion_labels: ["shipped"]honored;stagedalone no longer blocks in that config.staged_label: "complete"rename withcompletion_labels: null→completeblocks (default tracks rename).allow_completed=truebypasses;completion_labels: []disables.[staged, in-progress]→ completion reason wins over liveness path.Out of scope
validate_label_transition/LABEL_TRANSITIONSenforcement at the claim boundary (complementary, enum-only — cannot cover custom labels; candidate for a separate/rectify-driven ticket).recipe:implementationbut run through remediation).staged+in-progressstate and the orphaned branch.