---
name: apt:triage
description: "Move a task through the 5-state triage machine (needs-triage → needs-info → ready-for-agent | ready-for-human → wontfix). Mirrors to local triage.json by default; backends pluggable."
apt-skill-version: {{APT_VERSION}}
stage: plan
intent: investigate
when_to_use: "An inbound task / bug report / feature request needs triage — is it ready for an agent, ready for a human, blocked on more info, or won't-fix? Always available; only the BACKEND it mirrors to is configurable."
user_invocable: true
internal: false
spawns_agent: false
agent_name: null
task_context: require-existing
default_track: STANDARD
default_execution_mode: step
execution_modes:
  - auto
  - step
allowed-tools: "Read, Write, Bash, Grep, Glob"
argument-hint: "apt:triage [task-id]"
gates: []
config_keys:
  - task_tracking.backend
  - task_tracking.publish_to_tracker
  - task_tracking.tracker_url
  - task_tracking.tracker_label_vocabulary
---

# Triage — 5-State Task Machine

This skill moves a task through a 5-state machine. The state machine
runs identically regardless of backend (local-only / github-issues /
app-inbox); only the mirror destination changes.

**Aperant adoption attribution.** State machine + AGENT-BRIEF.md +
OUT-OF-SCOPE.md sub-files ported from 
`triage` skill. Aperant-specific extensions:

- Backend abstraction (configured at `apt:setup` Batch 8 — task_tracking.backend)
- Local-only default (zero GitHub auth on first run — ID-01: Framework
  full-featured locally; backends are pluggable)
- Extensible metadata field `extra: Record<string, unknown>` for v2 OSS
  contributor primitives (trust tier, repro status, security flag,
  response templates) — non-breaking schema extension when v2 adds them
- Fast Path Guarantee: QUICK-routed tasks NEVER enter triage (ID-05)

## When to Use

- An inbound task / bug / feature request needs status assignment
- Periodic review of `needs-triage` tasks
- Re-triage when new info arrives on a `needs-info` task

## When NOT to Use

- QUICK-routed tasks (Fast Path Guarantee — they go straight to
  `/apt:quick` with no triage)
- Tasks already in a terminal state (`wontfix` or `ready-for-agent`
  with an active agent run)

## 5-state machine

```
                                  ┌──→ ready-for-agent  (terminal)
                                  │
  needs-triage ──⇄── needs-info ──┼──→ ready-for-human  (terminal)
       │                          │
       │                          └──→ wontfix           (terminal)
       │
       ├──→ ready-for-agent  (direct)
       ├──→ ready-for-human  (direct)
       └──→ wontfix          (direct)
```

All 8 valid transitions: see prose list below and
`apt-triage-state-machine.test.ts` for the exhaustive matrix.

States:

- **needs-triage** — inbound default. No state assigned yet.
- **needs-info** — triage agent identified missing info; blocked on
  the reporter.
- **ready-for-agent** — task is concrete enough for `/apt:plan` +
  `/apt:execute`. Has acceptance criteria, files identified.
- **ready-for-human** — task requires human judgment (design taste,
  product decision, security review). NOT for an agent to ship.
- **wontfix** — closed without implementation. Reason recorded.

Valid transitions (8):

1. `needs-triage` → `needs-info` (need more info)
2. `needs-triage` → `ready-for-agent`
3. `needs-triage` → `ready-for-human`
4. `needs-triage` → `wontfix`
5. `needs-info` → `ready-for-agent` (info supplied)
6. `needs-info` → `ready-for-human`
7. `needs-info` → `wontfix`
8. `needs-info` → `needs-triage` (re-triage — reset)

All other transitions are invalid and rejected.

Categories (orthogonal to state): `bug` | `enhancement`.

## v1 schema (with extensible metadata for v2)

```jsonc
{
  "task_id": "string",
  "state": "needs-triage | needs-info | ready-for-agent | ready-for-human | wontfix",
  "category": "bug | enhancement",
  "transitions": [
    { "from": "needs-triage", "to": "needs-info", "at": "ISO-8601", "by": "user|agent", "reason": "string" }
  ],
  "extra": { /* v2 OSS contributor primitives go here — non-breaking schema extension */ }
}
```

The `extra` field is the v2-extension slot. v2 will add:

- `contributor_trust_tier` (number)
- `repro_status` (string: not-reproduced | reproduced | needs-info)
- `security_sensitive` (boolean)
- `response_template_id` (string)

v1 ignores these fields if present; v2 reads them. No schema migration
needed.

## Backend configuration

`.aperant/config.json.task_tracking.backend` selects the mirror:

| Backend | behavior |
|---|---|
| `local-only` (default) | State mirrors to `.aperant/tasks/{task-id}/triage.json` only. Zero network dependency. |
| `github-issues` | Mirrors triage state to a real GitHub issue via the `gh` CLI. The first mirror CREATES an issue (`triage: <task-id>`, labelled `<category>` + `<vocab>:<state>`); subsequent transitions UPDATE the same issue (label swap), and `wontfix` CLOSES it as "not planned". The issue pointer (`github.{issue_number, issue_url, repo}`) persists on `triage.json` so transitions stay idempotent — no duplicates. Honors `task_tracking.tracker_label_vocabulary` (default → `triage:<state>`). Requires `gh` installed + authenticated + a resolvable git remote; otherwise returns a structured error (`reason: gh-unavailable | gh-unauthenticated | no-remote | gh-command-failed`) and exits non-zero — never a crash/stacktrace. Label ops are best-effort: an unknown label degrades to a `warnings[]` entry while the open/closed state change still lands. |
| `app-inbox` | Still a stub: returns `{status: 'not-implemented', backend: 'app-inbox', v2_issue: '#TBD'}`. Belongs to a future Convex-backed task (post-app-launch). |

Skill body reads `task_tracking.backend` at the start of every
invocation. For `app-inbox` (still unimplemented), the skill fails fast
with the `not-implemented` envelope — it does NOT silently fall back to
`local-only`. The `github-issues` backend is fully implemented (see
above); its preflight/`gh` failures surface as structured errors, again
without falling back to `local-only`.

## Fast Path Guarantee — QUICK never enters triage (AC9, ID-05)

When the router classifies a task as QUICK, the task NEVER enters this
state machine. `/apt:quick` skips triage by design — it's the one-shot
path for trivial fixes, and triage ceremony would violate the Fast
Path Guarantee. apt-triage-quick-exempt.test.ts asserts this constraint
holds.

## Sub-files (MIT-attributed)

- `AGENT-BRIEF.md` — what an agent does with each state
- `OUT-OF-SCOPE.md` — what triage explicitly is NOT (decision-locking,
  feature design, multi-task prioritization)

## Skill body process

1. Parse `task-id` from `$ARGUMENTS`.
2. Read `.aperant/config.json.task_tracking.backend`.
3. Read the current `triage.json` state (default `needs-triage`).
4. Apply the requested transition via the `apt-tools triage` CLI
   (sub-command on the dispatch map).
5. Mirror to the backend if configured (local-only = write
   `.aperant/tasks/{id}/triage.json`).
6. Output the new state envelope.

State transitions surface the autonomy-aware degradation table
(`apt-discuss/appendices/grill-discipline.md`) when the transition
needs the user's judgment (e.g., moving to `wontfix` without
escalation).
