Managing the Issue Tracker

This guide describes how Spec Kitty structures its GitHub issue tracker so the work graph stays honest, machine-navigable, and free of the drift that hand-maintained checklists cause. It is aimed at maintainers and agents doing tracker hygiene — parenting issues, wiring dependencies, and pruning stale rollups.

The rules exist because a misleading work graph is expensive: a real defect parented under a release rollup is hidden when that rollup is closed, and a dependency captured only as prose is invisible to every tool that reads the tracker.

Functional epics versus meta-trackers

The tracker has two distinct kinds of parent issue. Telling them apart is the first decision in any hygiene pass.

  • A functional epic is a real domain or slice of work. It owns native GitHub sub-issue children, carries the epic label, and has a body that describes the work (see Epic bodies).
  • A meta-tracker is a convenience rollup — a release go/no-go list, a stabilization checklist, or a dashboard view. It carries the meta-tracker label, its body is a checklist that references issues owned elsewhere, and it has no native children of its own.

The distinguishing test is native children, not the title. An issue titled Umbrella: … that owns native sub-issues is a functional epic; an issue that holds only a body checklist of items homed under other epics is a meta-tracker. Judge by the sub-issue graph, never by the name.

Parent functional issues under functional epics only. Never parent a real work item under a meta-tracker.

Express the parent/child relationship as a native GitHub sub-issue link, not as a ## Children checklist in the parent's body. Body checklists duplicate the relationship and silently drift out of sync.

Create a link (REST — sub_issue_id is the child's numeric database id, not its issue number):

CID=$(gh api repos/OWNER/REPO/issues/<CHILD> --jq .id)
gh api --method POST repos/OWNER/REPO/issues/<PARENT>/sub_issues -F sub_issue_id="$CID"

Or via GraphQL (node ids, not numbers):

PID=$(gh api repos/OWNER/REPO/issues/<PARENT> --jq .node_id)
CID=$(gh api repos/OWNER/REPO/issues/<CHILD> --jq .node_id)
gh api graphql -f query='mutation($p:ID!,$c:ID!){addSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number}}}' -f p="$PID" -f c="$CID"

List a parent's children with gh api repos/OWNER/REPO/issues/<PARENT>/sub_issues.

Single-parent constraint. GitHub allows an issue only one parent; a second link attempt returns HTTP 422. To reparent, remove the old link first, then add the new one:

gh api graphql -f query='mutation($p:ID!,$c:ID!){removeSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number}}}' -f p="$OLD_PARENT_NODE" -f c="$CHILD_NODE"
gh api graphql -f query='mutation($p:ID!,$c:ID!){addSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number}}}' -f p="$NEW_PARENT_NODE" -f c="$CHILD_NODE"

Execution order is blocked_by, not prose

Encode sequencing as native blocks / blocked_by dependencies between children, not as a #A → #B → #C sequence in the epic body. An epic body should carry no ordering prose that a tool cannot read.

# REST — issue_id is the BLOCKING issue's numeric database id
gh api --method POST repos/OWNER/REPO/issues/<BLOCKED>/dependencies/blocked_by -F issue_id=<BLOCKER_DB_ID>

# GraphQL — issueId is the BLOCKED issue's node id
gh api graphql -f query='mutation($b:ID!,$k:ID!){addBlockedBy(input:{issueId:$b,blockingIssueId:$k}){issue{number}}}' -f b="$BLOCKED_NODE" -f k="$BLOCKER_NODE"

For a strangler sequence of children, wire each step blocked_by its predecessor so the ready-to-start item is always unambiguous.

Epic bodies describe work, not children

An epic body is a functional description, not a child enumeration. Every epic body must convey:

  • Why — the problem or motivation.
  • For whom — which users or roles benefit.
  • Intended effect — the outcome once the epic is done.

Preserve substantive rationale; drop mechanical checklists (the sub-issue graph already records the children).

Meta-tracker lifecycle

  • Label a meta-tracker meta-tracker, never epic.
  • Its body checklist is by design. Do not audit a meta-tracker for "no in-body children" and do not native-link its checklist entries — the real issues live under their own epics.
  • Close a meta-tracker once it holds zero native sub-issues. Its purpose is spent when the work it references is tracked under proper epics. Close it with an audit comment, and explicitly note any residual free-text checklist items so nothing silently vanishes.

Filing children from an epic

When you decompose an epic into issues:

  • Match the epic's own stated breakdown — file exactly the slices the body names, in the order it names them. Do not invent scope the epic did not specify; flag anything too vague to file cleanly instead of guessing.
  • Ground each slice in the real code first. If a named slice already shipped, record that with evidence rather than filing a phantom "to-do" ticket that misrepresents completed work as pending.

Triaging issues: type, severity, and release-blocking bugs

Every open issue carries two orthogonal classifications a triage pass must get right: what kind of thing it is (native type) and how urgent it is (priority). They are set independently and mean different things. A third, independent axis — which code domain it lives in — is carried by the domain:* labels; assign it from Domain labels rather than guessing a surface, since that section keys each label to the enforced Modularity SSOT.

Native type is the kind carrier

Set the GitHub native issue type — Task, Bug, or Feature (the tokens are case-sensitive: gh issue edit <N> --type Bug). Feature here is GitHub's native issue-type name, not Spec Kitty terminology; the domain object is always a Mission. The type is the sole kind carrier; the old bug label was retired repo-wide and must not be reintroduced. A capability request wearing a bug title is a Feature (retype it, don't leave it as a Bug). The enhancement label and the whole type:* label family are retired for the same reason; the issue templates set the native type directly.

Priority, and what makes a P0

Priority is a label (priority:P0 … priority:P3). priority:P0 on a Bug means a release-blocking defect — and, per ADR 2026-07-17-1, that has teeth: an open P0 bug is expected to red mainline, mainline CI is the authoritative release gate, and red CI means no release.

Because a P0 bug halts the release train, calibrate it deliberately. A bug is P0 if it trips any of:

  • Data/state corruption or loss — overwrites recorded results, poisons/drops events, wrong-authority or split-brain committed writes.
  • Silent wrong result / false success — a command reports success but skips or loses work (especially damning under the honesty ADR).
  • Core workflow broken with no workaround — cannot implement / review / merge / accept / commit a mission; a deadlock or hang with no escape hatch.
  • Blocks release directly, or a security exposure (credentials/secrets).

The escalation test — irreversibility × silence × false-success. The four triggers above share one shape: a defect is P0 when it can destroy or lie without telling the operator. Weigh three aggravating axes — is the damage irreversible (data/state loss, wedged mission with no escape), does it happen silently (no error surfaced), and does the tool claim success anyway (exit 0, "done", a fabricated record). A fault that scores on these axes is P0 even when it feels borderline; a fault that is bounded, loud, and recoverable is not.

Downgrade below P0 when a clear workaround exists, the fault is edge-case-only, cosmetic/UX, confined to a dev-assist command, or the exposure is bounded and sophistication-gated (a narrow race window, an attack needing privileged local position). Do not inflate P0 — crying wolf devalues the red-mainline signal. But do not under-call a genuine corruption, hang, or false-success: when a real destructive/silent/false-success defect is borderline, escalate. Red main is the safety net, not the failure (ADR 2026-07-17-1) — an honestly red mainline on a genuine P0 is the system working, not breaking. Escalation to P0 is an operator decision.

The operator-betrayal class is the archetypal P0. The most damning defects this project ships are the ones where Spec Kitty fights its own operator — the tool reports success while skipping, losing, or fabricating work: a --fix that exits 0 repairing nothing, a merge that prints "cleaned up" while every delete was refused, a close that commits a fabricated completion record, an init that silently deletes user-authored missions, a finalize that wedges the mission with no escape. These are almost always surfaced by running Spec Kitty on Spec Kitty (label them catfooding), and under the honesty ADR they are release-blocking by default: a tool the operator cannot trust to tell the truth about its own actions is not shippable. Treat operator-betrayal defects as P0 unless a concrete argument downgrades them.

Priority on a non-bug means something else: a priority:P0 Feature/Task is the priority of planned work, not a release-blocking defect — it does not imply a red mainline.

A P0 bug should carry a failing reproduction test

Per the ADR, when you file or accept a P0 bug, land an issue-pinned red-first reproduction test so CI honestly reflects the blocker instead of only asserting it. Mark it @pytest.mark.p0_repro(issue=N), reproduce the defect through the pre-existing entry point, and say in the docstring that it is an intentional red-first P0 reproduction. It must fail because of the product defect, not because of a test error: a false red corrupts the signal it exists to give.

p0_repro tests run only in the nightly p0-repro lane. They never run in a PR's CI, so PRs land on green while the nightly, and therefore the release, stays blocked until the fix lands. The fix PR removes the marker. Do not use regression (per-PR guards for bugs that are already fixed) or quarantine (built never to run) for an open P0. Expensive QA and manual testing wait for green, and maintainers prioritize red recovery. See the red-main policy.

What "good first issue" means here

good first issue is not a difficulty or seniority signal — this project's contributor base is senior by default. It marks a good entry point into the codebase: a self-contained issue that teaches a subsystem while carrying low customer and blast-radius impact, so someone new to the repo can land it without first absorbing deep cross-cutting context.

Reach for it when an issue is well-scoped, has a clear deliverable, and touches a peripheral or tooling surface rather than a load-bearing one. One guardrail: if a good first issue could still alter a blocking or shared gate (or otherwise carry real blast radius despite being a good entry point), require maintainer concurrence on the chosen approach before implementation — normal PR review then covers the rest.

Running a triage pass over a batch of issues

A maintainer pass over a batch of open issues (by default those created in the last 7 days) is run by the issue-triage-pass procedure, which the issue-triage pack skill starts. The orchestrator fixes the work list and the code baseline, dispatches triage subagents that load the planner-priti profile, re-checks every issue before each write, and puts the flagged items to the operator. The rules above are what each subagent applies. Three points are easy to miss:

  • Never set priority:P0 in a pass. Name the criterion the issue meets and flag it; escalation is the operator's decision.
  • Leave status:* and taken-by-human alone, with one exception. They are leases and queue state, set by a human or by contributor tooling (see Issue workflow-state labels). The exception is status:triage: once an issue's triage decisions are applied, remove it, and add no other status label in its place (triage:* sub-labels stay). A flagged issue keeps status:triage until the operator's ruling is applied. Report any other status label that looks stale.
  • Check liveness against the canonical remote. Judge "still exists" or "already shipped" against main of spec-kitty/spec-kitty, fetched now, not a fork's origin/main, which can lag far behind.

Milestones and release cycles

Milestones carry release scope; labels carry kind, priority, and domain. The active cycle is 4.0.0, currently at the release-candidate stage on main. The 4.0.0 milestone roadmap is the authority for what each milestone means. In short:

  • 4.0.0 release scope — the GA gate: issues that must close before a non-rc 4.0.0 ships. Put an issue here only if it truly blocks GA.
  • CLI 4.x stable — stable 4.x work after the 4.0.0 launch. Post-4.0.0 deferrals go here.
  • 4.x Work — the former 4.0.0 milestone; now hosted-collaboration and product work beyond the stabilization release. Being on it does not make an issue a GA blocker.

The 3.x milestones are closed: 3.2.7 was the last 3.x release, and the 3.3.x milestone was retired on 2026-08-23 without shipping. An issue whose text says "deferred to 3.3.x" needs re-triage onto one of the milestones above; do not reopen or reuse the 3.x milestones.

Label taxonomy

Two orthogonal axes carry the load and are documented above: the native type (Bug / Feature / Task — the sole kind carrier) and priority (priority:P0…priority:P3). Labels never encode kind. The bug and enhancement labels and the whole type:* label family (type:bug / type:feature / type:chore / type:fix / type:finding / type:flake / type:thesis) duplicated the native issue type as a parallel, drift-prone kind carrier and are retired: the native GitHub type is the single canonical authority for kind. These labels were deleted from the repository on 2026-10-19; if one reappears, strip it and set the native type. The former type:decision label is now triage:needs-decision (see Triage-state labels). Beyond type and priority, labels fall into the families below. Apply as many as genuinely apply; a catfooding reliability Bug at priority:P2 is a normal, well-triaged issue.

Classification labels — what kind of work it is

These describe the nature of the work independently of the subsystem. They are orthogonal to native type (a catfooding issue is still a Bug, Feature, or Task).

  • catfooding — Doctrine Catfooding: dogfooding Spec Kitty's own quality/doctrine practices. Applied to defects and frictions surfaced by running Spec Kitty on Spec Kitty — a live mission session that exercises the specify → plan → tasks → implement → review → merge loop and reports where the tool fought its own operator. This is the dominant classifier for the post-convergence dogfooding backlog; expect clusters of catfooding workflow/git/usability issues that circle a single root cause.
  • tidy-up — Campsite / declutter / degodding cleanup surfaced during other work (the boy-scout residue a mission couldn't fold in-line). The former cleanup label was merged into it.
  • tech-debt — Accumulated lint, type-checking, static-analysis, and code-quality debt.
  • design-spike — Foundational design work.
  • research — Research / experiment validation work.

Domain labels — which code domain the work routes to

domain:* labels route an issue to the code domain it lives in. The domain set is keyed to the Modularity SSOT — the enforced pair CI actually defends, not this prose:

  • Module inventory — pyproject.toml [tool.hatch.build.targets.wheel].packages, guarded by tests/architectural/test_pyproject_shape.py.
  • Import direction — the landscape fixture in tests/architectural/conftest.py + tests/architectural/test_layer_rules.py, whose enforced chain is kernel <- charter <- {glossary, runtime, mission_runtime} <- specify_cli.

Every prose module map — this section, docs/architecture/00_landscape/README.md, docs/architecture/04_implementation_mapping — is a derived view; on any conflict the enforced pair wins. Because the top specify_cli module is large, a few domain:* labels route to its high-traffic sub-packages rather than to the whole application layer.

Selection rule. Pick the domain:* whose code the fix will actually edit; apply every domain that genuinely applies (a change spanning modules carries one label per surface it really touches). A domain:* answers which module, independently of native type, priority, and the cross-cutting concern labels below.

Label Code domain (SSOT module / sub-package)
domain:kernel src/kernel/ — zero-dependency foundation primitives (root layer)
domain:charter src/charter/ — governance authority; includes the former doctrine system now absorbed at src/charter/offering/
domain:glossary src/glossary/ — terminology / semantic-integrity pipeline + DRG glossary bridge
domain:runtime src/runtime/next/_internal_runtime/ — canonical mission control loop
domain:mission-runtime src/mission_runtime/ — artifact-placement seam (PlacementSeam, resolver port, identity, lifecycle_phase)
domain:cli src/specify_cli/cli/ — control-plane / CLI command surface
domain:invocation src/specify_cli/invocation/ — dispatch & profile-invocation routing (Op lifecycle)
domain:decisions src/specify_cli/decisions/ + cli/commands/decision.py — Decision Moment flow, models, emit
domain:status src/specify_cli/status/ + lanes/ + coordination/ + workspace/ + state/ — status event-log & lane state machine; state/ is the project-scoped state-surface contract and doctor state-root checks
domain:consolidation src/specify_cli/consolidation/ — lane consolidation, retention, preflight (formerly domain:merge)
domain:skills src/specify_cli/skills/ — command/skill rendering, install, and the command-skills manifest
domain:upgrade src/specify_cli/upgrade/ (incl. upgrade/migrations/) + cli/commands/upgrade.py — spec-kitty upgrade: version detection, migration registry/runner, redeploying generated commands and skills
domain:review src/specify_cli/review/ + cli/commands/review/ — WP review cycle, verdicts, arbiter, the pre-review transition gate (gate_registry, pre_review_gate), and the spec-kitty review mission-review gates
domain:acceptance src/specify_cli/acceptance/ — the accept gate: acceptance matrix, verify, summary_core path conventions
domain:orchestrator src/specify_cli/orchestrator_api/ — the orchestrator-api external machine contract (JSON envelope + subcommands), documented in docs/api/orchestrator-api.md
domain:contracts src/specify_cli/contracts/ + docs/contracts/ — payload / handoff-packet contract registry
domain:tracker src/specify_cli/tracker/ — tracker provider integrations & connectors (spec_kitty_tracker consumer)
domain:docs docs/ + scripts/docs/ + src/specify_cli/docs/ + cli/commands/docs.py — documentation content, docs build/freshness tooling, and the docs query retrieval index
domain:ci GitHub Actions workflows (.github/workflows/) + scripts/ci/ — pipeline, gate selection, coverage aggregation
domain:onboarding first-run setup spanning modules (init, charter, remote, first mission)
domain:git src/specify_cli/git/ + src/kernel/git/ — git operations, worktree / topology helpers
domain:agent-profiles src/charter/offering/agent_profiles/ + profile loading in specify_cli
domain:schema-versioning src/specify_cli/schemas/ — event / payload schema versioning and spec-kitty-events consumption
domain:status-api services/mission-status-api/ — the Mission Status Read/Write API service, its contract and its consumers (successor of the removed dashboard, #5530; formerly domain:dashboard)

domain:ci, domain:docs, domain:onboarding and domain:status-api name a workflow surface or service outside the Python Modularity SSOT rather than a single package. They are still domain labels because they answer where the work lives for routing — unlike the concern labels below, which name a quality/experience axis that can attach to any domain.

Legacy labels — history only

legacy:sync (the local sync transport, deleted in The Convergence, ADR 2026-09-06-1-convergence-retirement-and-client-repo-inversion) and legacy:hosted (the hosted Team Kitty clients saas_client/, zeitgeist_client/ and auth/, removed in #6164; formerly domain:hosted) mark closed issues for history. Never apply them; re-home any live issue to the domain:* label whose code the fix edits. The earlier renames of the non-namespaced labels (dashboard, saas, agent-profiles, schema-versioning, git) are complete. A legacy:* label exists only for a retired code area; retired process labels are deleted outright.

Cross-cutting concern labels — what quality/experience axis, not a code domain

These name a concern that spans modules; they are not domain:* labels and do not route to one package. Apply them alongside a domain:* label.

  • reliability — Runtime reliability, resiliency, observability, incident prevention.
  • usability — Ergonomics of any operator-facing surface: messages, flags, output, defaults.
  • workflow — The mission loop itself (specify → plan → tasks → implement → review → consolidate) behaves wrongly or awkwardly.
  • windows — Windows-specific issues (a platform axis, not a module).

Triage-state labels — transient hygiene state, not classification

These record where an issue sits in triage. Remove them once resolved; they are working state, not a permanent property.

  • triage:maybe-duplicate — Suspected duplicate pending confirmation (as opposed to the confirmed duplicate).
  • triage:needs-revision — Scope/spec needs rework before action.
  • triage:stale — Reproduce and close if no longer valid.
  • triage:repro-needed — Reported against behavior that may already be fixed; needs live re-reproduction before implementing.
  • triage:needs-investigation — Root cause is not yet adjudicated. The defect is real (often a confirmed red), but which layer owns it is unknown — e.g. a red test that could be a stale fixture/seed or a genuine product bug. Blocks final homing (parent/priority) until an investigation or debugger pass resolves the root-cause axis; remove it once the cause is known.
  • triage:needs-decision — A human (operator or maintainer) must make a call before anyone implements; the body states the options. Formerly type:decision.

Apply at most one triage:* label per triage cycle.

Provenance labels — who surfaced it

These record where an issue came from. Provenance is orthogonal to type, priority and domain.

  • from:ci — Filed by CI automation (scripts/ci/nightly_escalation.py). Code writes and searches for it; never remove it by hand.
  • from:qa — Found by a human QA tester.
  • from:dev — Found during development.
  • from:squad — An adversarial-squad finding (the adversarial-squad skill).

PR-workflow labels — for pull requests, not issues

These live on pull requests, not issues.

  • ready-for-squad — the implementer has run the required tests and self-review; the PR is finished and independently testable. Do not apply it to a still-draft scope or to work whose tests have not run. (While the SkyKitty agent fleet was active this label also handed the PR to the fleet; the fleet is switched off.)
  • pr:p0-repro — the PR lands the red-first reproduction test for a confirmed open P0 Bug. Reference the issue with Refs #<n>, not Closes, and mark the test @pytest.mark.p0_repro(issue=<n>) (see A P0 bug should carry a failing reproduction test).
  • pr:needs-refresh — PR branch drifted from main; rebase/refresh before review or merge.
  • pr:needs-revision — PR has unresolved review findings that must be addressed before it can merge.
  • pr:kept-for-reference — Kept open for reference/history; not intended to merge as-is.
  • pr:deferred — Held under a declared freeze; the label-gated CI jobs (ci-windows.yml, check-spec-kitty-events-alignment.yml, release-readiness.yml) are skipped until it is removed.
  • pr:skip-ci — Intentionally skip CI for this PR (docs / manual review only).

Lifecycle & community labels

  • deferred — Work paused pending an activation trigger.
  • feedback-welcome — Community feedback & brainstorming welcome; comment ideas/objections, no code needed.
  • good first issue — a good entry point (see What "good first issue" means here); not a difficulty signal.
  • release — Release engineering: tagging, publishing, release-pipeline integrity. Release and launch scope lives in milestones, not labels.
  • duplicate — A confirmed duplicate; pair it with the native "Close as duplicate". invalid, wontfix, question and help wanted are retired: use the native close reason "not planned", or a triage:* label.
  • Docs work is routed by domain:docs (the former documentation label was merged into it); doctrine prose is routed by domain:charter.

Grouping labels (documented above)

epic and meta-tracker are covered in Functional epics versus meta-trackers — they are not free-floating classification labels and carry structural obligations (native children vs. a body checklist).

Issue workflow-state labels

The issue-side status:* labels and taken-by-human are human workflow state:

Label Meaning
status:triage New, untriaged work
status:ready Triaged, de-duplicated, and resolvable without a pending decision; ready to pick up
status:claimed Someone is actively working the issue. A lease: valid only with a live claim comment naming who is working it. Remove it when you stop
status:blocked Blocked, with a Depends on: #<n> line naming the gating issue
taken-by-human A human contributor owns the issue or its linked PR. Pair it with status:claimed when you begin

Contributor tooling may also set these labels; treat a label it set like one a person set.

Label-driven fleet workflow

Status: the SkyKitty fleet is switched off (the CI Fleet Verdict workflow is disabled; last run 2026-09-26). The issue-side status:* labels and taken-by-human stay in use as human workflow state (see Issue workflow-state labels). The PR-lane labels (squad:*, needs:implementer, ci:green / ci:red) were deleted on 2026-10-19 and would be recreated if the fleet is reactivated. The rest of this section is historical: it records how the fleet consumed the labels while active.

The SkyKitty agent fleet and its sibling programme repos run on a label-driven queue: GitHub labels are the only admission control. Agents and humans move work by changing labels, never by out-of-band assignment. This section documents the queue that consumes the labels catalogued above. Source of truth: the planning repo's agents/dispatcher.md, agents/groom.md, agents/merge.md, agents/review.md, and PROGRAM.md.

The fleet scripts named below (bin/unblock.py, bin/take-next-ticket.sh, and bin/dispatch.sh) live in the private EXPERIMENTAL-spec-kitty-planning repository, not in this Spec Kitty checkout. They describe fleet behavior; contributors do not need to run them to manage an issue.

The status:* lifecycle (issues)

This is the fleet's admission lifecycle, distinct from the transient triage:* hygiene sub-states above (which flag why an issue sits in triage, not where it is in the queue).

Label Meaning Who sets it
status:triage New/untriaged work. The groom agent's queue. Filing agents (from:review, from:squad, from:ci, …) always file into triage
status:ready Valid, de-duplicated, resolvable without a reserved decision. The fleet's admission queue. The groom agent promotes triage → ready. Review-agent findings are promoted by the controller, never by the review agent itself
status:claimed An implementer is actively working this issue. Lease — valid only with a live claim comment naming the harness/model or dispatcher VM. The dispatcher sets and reaps fleet claims. A human-driven contributor must add both status:claimed and taken-by-human when beginning work, then comment claimed by <harness>/<model> on <machine>. Never add a bare claim when nobody is working the issue: the dispatcher treats the label as occupied capacity
status:blocked Genuinely blocked, with a Depends on: #<n> line naming the concrete gating issue. bin/unblock.py flips it back to status:ready automatically once all dependencies close. Groom/controller
taken-by-human A human contributor (not a fleet VM) owns the linked PR. The only label that keeps the fleet's PR-routed fix and recovery lanes off human-driven work: those lanes select on the PR's own labels and never see the linked issue's status:claimed, so this — not status:claimed — is what holds a fleet implementer VM off your PR (PROGRAM.md §13). (Initial admission is separate: the dispatcher already skips issues that carry status:claimed.) Pair it with status:claimed on the issue when you begin. The human contributor; bin/take-next-ticket.sh sets it automatically for fleet-assisted human claims

Issues with no status:* label and an open linked PR (a real closes # relationship) are mid-negotiation, not unowned — the groom skips them deliberately.

Admission: from label to VM

  1. The dispatcher (bin/dispatch.sh, deterministic, not an LLM) sweeps continuously. Each sweep lists open issues with status:ready and no status:claimed, ordered by priority P0..P3, then repo order (PROGRAM.md §2), then issue number.
  2. It skips issues labelled status:blocked, epic, or triage:needs-decision (formerly type:decision; a reactivated dispatcher must match the new name) — a decision issue is never autonomous implementation work, even at status:ready; it waits on a resolving comment (docs/DECISION-ISSUES.md).
  3. For each issue up to free implementer capacity, the dispatcher claims it: adds status:claimed, comments claimed by dispatcher → sk-impl-<repo>-<n>, and boots a one-shot implementer VM.
  4. Reaping: when an implementer VM exits with no linked PR, the dispatcher posts the last 40 log lines as [dispatcher] implementer exited without a PR, removes status:claimed, and destroys the VM — the issue returns to the status:ready queue and is re-claimed on a later sweep. A VM older than 6 hours with no PR is treated the same.

PR-lane labels (review and landing; historical, labels deleted)

PRs moved through their own label set, driven by the same dispatcher (see also the ready-for-squad handshake under PR-workflow labels):

Label Meaning
ready-for-squad PR is ready for adversarial review; dispatcher boots a squad VM (one iteration)
squad:running Squad VM active (lease; stale-claim recovery requires confirming the VM is actually dead, never elapsed time alone)
squad:passed / squad:majors Squad verdict. Two squad:majors rounds is the cap — the merge agent then decides merge-with-findings or sends it back; never a third squad
squad:exhausted Two verdicts already posted (or the merge agent ruled out a further pass); merge proceeds on [ci] + controller evidence
ci:green / ci:red Best-effort label mirror of the latest exact-head [ci] comment. The comment is authoritative; the label is never a gate on its own
needs:implementer Any fix/rebase request that expects a new push. Mandatory on every such request — the dispatcher dispatches fix-round VMs when this label or squad:majors admits the PR to the fix lane; a request comment carrying neither is never seen

See also