Agent triage surface: deliberate workflow actions write through the tracker seam¶
The maintainer's issue workflow runs on agent skills (triage, to-issues, to-prd): an agent in an interactive instance reads an issue, applies triage roles, files new issues broken out of plans, and closes what won't be fixed. Per D10 labctl is a session's only tracker surface — but until now it could only view, list, comment, and open a PR/CR, so every one of those workflows dead-ended inside the instance, on both tracker bindings.
This ADR extends the agent surface end-to-end — labctl → agent API → Tracker seam → both bindings — with the full triage set: issue create (labels attached at creation), label add/remove on an issue, label list, idempotent label create ("ensure"), and issue close. And it revises the ADR-0009 sentence "lab never writes labels or issue state to a forge" into the invariant it always meant:
- What stays dead is the ENGINE writing tracker state implicitly. Claim-is-the-branch is untouched (reference ADR-0013): no label ever encodes engine state, the
in-progresslabel class stays deleted, and nothing in the scheduler/reaper/stop path writes to a tracker. - A deliberate agent workflow action flows through the tracker seam to whichever backend the repo's tracker binding names — exactly as agent comments and PR/CR creation already did. The revision widens only the agent surface; operator mutations remain builtin-only (a forge-bound repo's operator UI still answers "manage issues on the forge").
The mechanics, pinned:
- Seam:
TrackergrowsCreateIssue,AddIssueLabels,RemoveIssueLabels,Labels, andEnsureLabel;CloseIssue(already in the seam) is exposed to agents. Callers speak label names only. Applying or attaching a name the repo does not define isErrUnknownLabel(agent API: 400 naming it) — never an implicit create, so a typo becomes a loud error instead of a permanent garbage label. The metrics decorator forwards the new ops into the existing(binding, op, result)vocabulary and, per the ADR-0013 rule, keeps forwarding capability interfaces. - Forgejo binding: name-to-ID resolution happens inside the client (Forgejo's labels-are-IDs quirk stays behind the seam), and strict resolution doubles as the loud-failure guarantee — Forgejo silently discards unknown label entries, the same quirk that already forces the ready queue's client-side verification.
EnsureLabelis list-first with a re-list on the forge's duplicate answer, so it is idempotent whatever the forge does with duplicate names. ADR-0009's pagination and label-verification rules carry over unchanged. - Builtin binding: label ops ride the existing
labels/issue_labelstables; issue numbers come from the repo row's counter inside the insert transaction, as before. Created issues carry run attribution like comments (migration 0002 addsissues.author_kind/run_id— the one schema change, mirroringissue_comments), rescoped through the sameRunScoperseam. - Agent API / labctl: run-token-authenticated, repo-scoped routes; one token kind and one identical surface for interactive instances and AFK runs — the run token's repo scope stays the only security boundary.
issue listoutput gains created-at and labels so the triage buckets are computable from one call. Close has no comment sugar: skills post the explanation, then close. Errors keep the canonical envelope and pinned exit codes. - Incogni: the body sanitizer widens from PR/CR bodies to every agent-authored body — issue create and comment create included — so the defense-in-depth story has no unsanitized write path. A skill's disclaimer line is body content, not an attribution trailer, and passes through; running the triage workflow against an incogni repo is an operator-level contradiction (noted in ops.md), not something lab enforces.
- Seeding: the generated
CLAUDE.local.mddocuments the full command set. Triage labels are still never created at repo add —EnsureLabelis agent-invoked, so forge label sets are only ever mutated on an invited, deliberate action.
Status¶
Accepted. Revises the operator/engine-write sentence of ADR-0009 (which gains a status pointer here); everything else in ADR-0009 stands. The GitHub REST binding (fast-follow, #1) must implement the new seam ops when it lands. The "operator mutations remain builtin-only" sentence is itself revised by ADR-0046: operator title/body edits now flow through Tracker.EditIssue on every binding — everything else (create, state, labels, comments) stays builtin-only.
Considered options¶
- Keep the tracker read-only for agents and drive triage via the operator API. Rejected: the operator API is cookie+CSRF-authenticated and unscoped per run; the run token's repo scope is the security boundary the whole agent surface is built on, and skills run inside sessions where only
LAB_URL/LAB_TOKENexist. - Auto-create labels on apply. Rejected: a typo'd
--labels ready-for-agnetwould mint a permanent garbage label on the tracker; strict resolution plus an explicit idempotent ensure gives skills self-healing setup without silent writes. - Close-with-comment flag. Rejected: two calls (comment, then close) match the existing convention and keep one wire shape per op.
- Seeding the five triage labels at repo add on forges. Still rejected (ADR-0009): lab does not mutate forge label sets uninvited; the ensure op is the invited path and skills run it unconditionally.
issue edit/issue reopen. Deferred: no skill consumes them; add when a consumer exists.