---
name: skill-triage
type: skill
description: Triage and project-manage Linear tickets in the operator's delivery projects on behalf of the Claude Code PM role. Use this whenever running the triage routine, or when a ticket carries the agent:R3-LAY label, or a comment @mentions the claude-code label — set status, priority, and links, decide whether the work needs execution, and hand off to Forge or to the operator. Apply this rather than triaging ad hoc, so routing, labels, and the Pattern A handoff stay consistent. Trigger it even when the request just says "triage", "tidy the board", or "what needs doing" in a Linear context.
---

# triage

The PM / triage role of the Claude Code loop. Runs as a polling Cloud Routine — there is no Linear agent app, so triggering is by label, not agent session. Follows convention-linear throughout.

## Trigger

Poll for issues carrying `agent:R3-LAY` across delivery projects only — never the Pipeline team (Network / Roles / Advisory / Pitches). A comment @mentioning the `claude-code` label is a human flag to look; the issue label is the routing signal.

**Scope the poll to open states only.** `agent:R3-LAY` is a triage marker that is *evicted* when a ticket is routed to a real executor — but tickets that are never routed (epics the operator closes by hand, `FRICTION:`/`OPS:` notes, human-decision tickets) keep it through to close, so the marker accumulates on Done / Canceled / Duplicate work indefinitely. An unscoped `label:R3-LAY` poll therefore re-reads every closed ticket's full body every run — overflowing the tool's token limit and drowning live candidates in closed-ticket noise (worked case: 66 issues / ~99.8k characters returned, 41 of them closed, 2026-06-30, APP-401). Filter to **open states** (exclude Done / Canceled / Duplicate — e.g. iterate the active states, or drop the completed/canceled state types), since a closed ticket is never a triage candidate regardless of its label. This is the label-scoped-poll companion to the whole-board overflow guidance in `convention-linear` *Reading Linear safely*.

**Skip genuinely-not-ready Backlog leaves — the active-siblings / phased-placeholder scope.** Open-state scoping drops closed tickets, and the executor-hygiene relabel (below) drops human-owned Refinement work, but a third class still re-surfaces every poll with **no terminating action**: `agent:R3-LAY` Backlog leaves that are legitimately R3-LAY, open, and genuinely not yet refinable. Two shapes:

  - **Phased-placeholder leaves** — a Backlog leaf whose body is an explicit placeholder ("detail after Phase X surfaces the real …"), correctly parked awaiting an *earlier phase's* output, so not yet refinable.
  - **Un-greenlit-epic children** — R3-LAY Backlog leaves under an epic with **no active siblings** (zero Todo / In Progress / Done anywhere under it) that the operator has not greenlit; refining or routing them would hand an un-greenlit backlog to the delivery loop.

  Apply the **active-siblings test** already used in the F0-RGE dormant-project carve-out (*Resting state*, the invariant): a Backlog leaf in a project or epic with work already in flight is a live triage candidate; a leaf in a dormant / un-greenlit project or epic, or one whose body explicitly defers its spec to a later phase, is **not** — **skip it in the poll** until its gating phase completes or the epic is greenlit. Do not mutate it (it is already correctly `R3-LAY` in Backlog — unlike the human-owned and dormant-`F0-RGE` cases there is no hygiene relabel to make). **Surface the parked epic once** to the operator with the recommendation (greenlight, or leave parked) rather than re-scanning its children every run. This keeps the poll bounded to genuinely-actionable candidates so a single new ticket is not drowned among dozens of correctly-parked leaves (worked case: the only actionable ticket on the whole board sat among ~25 re-surfacing parked leaves — 2026-07-07, APP-452; companion to the closed-ticket overflow APP-401 and the human-owned re-surfacing APP-304 / APP-391, and distinct from the dormant-`F0-RGE` carve-out APP-435, which relabels rather than scopes).

  **Exclusion — a decision-gated leaf in an *active/greenlit* epic is not skipped; it goes to placement 4.** The skip scope above is for leaves that are *not yet refinable* because an earlier phase or a greenlight is missing. It must **not** be applied to a leaf that is refinable-but-decision-gated — one whose only outstanding need is an operator decision (an ownership call, a rescope, a scope-vs-fold) — sitting under an epic that **is** greenlit and in flight (has Todo / In Progress / Done siblings). Such a leaf belongs in **placement 4 — Refinement + `human` + assigned to the operator** with the decision named, on his plate, **never skipped-as-parked and never left in Backlog under `agent:R3-LAY`**. The distinguishing test is the **same active-siblings test, applied to the *epic***: active/greenlit epic + leaf-needs-a-decision → placement 4; dormant / un-greenlit epic → skip and surface the epic once. Conflating the two drops decision-gated leaves into a dead zone — not routed, not skipped-cleanly, just sitting under a standing recommendation that never reaches the operator (worked case: five Phase-1 firmup leaves — A1-98/101/102 the app-side ownership decision, A1-100 the stateless rescope, A1-104 the scope-vs-fold — sat in Backlog under `agent:R3-LAY` across two triage passes while their epic A1-110 was greenlit and in flight, "assigned to relay but nothing happened", corrected to Refinement + `human` + assigned, 2026-07-07, APP-459).

The run also revisits tickets in **Blocked** carrying any `agent:*` label (not only R3-LAY), to catch blockers that have since cleared — see *Blocked-state sweep* below. **In Progress** and **In Review** are never touched by the run.

## Behaviour

Read the issue and its comments, then:

- Set state, priority, and links. Apply missing `type:` / `work:` labels per convention-linear. Never move an unrefined ticket out of Backlog; never leave a refined, actionable one stuck in it.
- Decide whether the ticket needs execution (code):
  - **Needs execution** — set `agent:F0-RGE` (single-select evicts `agent:R3-LAY`), move to Todo, **clear the assignee**, and comment what's needed with the acceptance criteria embedded in the body (Pattern A), so the exec leg is self-contained. Only ever route a **leaf ticket** to Forge — never an epic (`type:epic` is an outcome closed by the operator when its children are done; see convention-linear *Structure*).
  - **Needs a human decision** — leave a clear comment and assign to the operator. The comment must @mention him (`@aledpritchard`) and lead with the specific action or decision needed, phrased so he can reply or act directly. Where it hands him something to *do* — a command, a setup step, a manual verify — write it as numbered, novice-friendly steps, one action per line, commands spelled out literally in a code span, a decision kept separate from a thing to type (`convention-ticket` *Writing an instruction the operator has to action*). Do not guess.
  - **Not actionable** — route to the right state (needs info, blocked, canceled) with a one-line reason.

## Refinement: placement, structure, assignee

When refining a Backlog ticket, structure it before polishing its content:

- **Instantiate from the type template (every Backlog ticket).** Set the ticket up from its `type:` template (`convention-ticket` *Per-type ticket formats*): confirm the section skeleton for that type, **paste the standing `## Definition of done`** for the type, and confirm the per-ticket **`## Acceptance criteria`** are present and testable. Don't promote a ticket to Ready without **both** sections in place — AC written and DoD pasted. The author writes AC; never leave it to Forge.
- **Placement check (every Backlog ticket).** Before refining content, ask where the ticket belongs: does it sit under an existing epic or feature, and does it belong to an existing milestone? Set the parent and milestone during refinement. A parent existing is not enough — test that the parent's **home matches the ticket's domain**: a `domain:*` ticket (or one whose subject clearly belongs to one venture) parented under an epic in a different team/project, or under a canon epic whose siblings carry no domain, is mis-homed. Flag it for re-homing in the refinement comment rather than refining it into the wrong place. If no home exists and the work implies one, say so in the refinement comment rather than inventing structure. (Continuous detection of mis-homed children and scope-bled epics across the board is `skill-coordinate` Pass 3; triage applies the same test reactively, one ticket at a time, at refinement.)
- **Harvest linked context.** Read the ticket's linked tickets — blocked-by, blocks, related, and any reference in its body or comments. Where a linked ticket is **Done**, pull its resolved context into this ticket's body (the decision, design, icon, node-id, acceptance detail) so the ticket is self-contained for whoever executes it. A Done ticket's context is a refinement input, not something left stranded on the closed ticket. (Removing satisfied blocker relations stays with `skill-coordinate`; triage only reads and harvests.) **Where the ticket's body or a routing instruction is predicated on a linked ticket's state** ("re-harvest after X lands", "X is still Blocked"), re-fetch that linked ticket's *current* status before acting on the instruction rather than trusting the body's assertion — a hard-coded state goes stale the moment the linked ticket moves (worked case: a re-harvest instruction against a ticket that had already shipped Done, 2026-07-02, APP-422). This is the read-side mirror of the `convention-ticket` authoring rule against hard-coding a linked ticket's state in a body, and the same discipline as judging a blocker by its own status below.
- **Milestone assignment.** Every refined leaf ticket gets a milestone where the project has them (app.fitness M1–M6, os.Claude M1–M3).
- **Restructure oversized tickets (propose-first).** A Backlog ticket too big for one PR is not refined as-is. Propose an epic + sub-task breakdown in a comment for the operator's approval, and wait — do not create sub-issues in bulk until he approves (the A1-3 pattern, made standard).
- **Resting state — the four placements.** A refined leaf rests in exactly one of four states, chosen by the nature of its outstanding gap. This is the canon binding of the placement model in `convention-ticket`:
  1. **Clean — nothing outstanding.** → **Todo** + executor (`agent:F0-RGE`), assignee cleared. Relay auto-promotes; there is no separate manual Todo gate.
  2. **Sequencing dependency — spec complete, only ordering outstanding.** → **Todo** + executor + blocked-by relation. The builder self-skips while the blocker stands and picks it up when it clears.
  3. **Harvest dependency — spec determined by the blocker's output** (the design→build case). → **Refinement under `agent:R3-LAY`** (the triage marker, never an agent executor). Relay re-checks it each pass and harvests the blocker's output into the leaf's body when the source completes, then promotes it (see *Harvest sweep* below).
  4. **Needs operator input.** → **Refinement under `human`**, assigned to the operator with an @mention leading the specific decision needed. This is where a **decision-gated leaf in an active/greenlit epic** belongs — see the *Skip genuinely-not-ready Backlog leaves* exclusion above: a leaf whose only gap is an operator decision, under an epic already in flight, is refined to here, not skipped and not left in Backlog.
  - **Invariant: an agent executor never rests in Refinement — with a dormant-project carve-out.** Refinement holds only `agent:R3-LAY` (a harvest leaf awaiting its source) or `human` (a leaf awaiting the operator). A leaf carrying `agent:F0-RGE` parked in Refinement is a **blind spot** — the triage poll scans `R3-LAY`, the delivery loop scans Todo, so neither picks it up and it stalls silently (worked case: A1-49/50/51/53, four refined F0-RGE build leaves parked in Refinement for two weeks, invisible to both, 2026-06-30, APP-400). But two different states share this shape, and only one is the blind spot the invariant corrects:
    1. **Stranded ready leaf in an active flow** — a refined, genuinely-ready leaf in a project that is **in flight** (has Todo / In Progress / Done siblings), left in Refinement by oversight. This *is* the blind spot: correct it to placement 1 or 2 (Todo, or Todo + blocked-by) — the mirror of `R3-LAY` lingering in Todo.
    2. **Pre-labelled leaf in a dormant, un-greenlit project** — a leaf whose `agent:F0-RGE` was set at *creation* (the pre-assign anti-pattern in `convention-linear` *Backlog default*, which skips the `R3-LAY` triage gate), sitting in a project with **no active siblings** (zero Todo / In Progress / Done) that the operator has not greenlit. This is **not** ready to build, and correcting it to Todo would hand an entire un-greenlit backlog to the delivery loop — a large, consequential mutation nobody requested. Do **not** auto-promote it. The correcting move is to take it off the delivery-loop's Todo path — relabel it off `F0-RGE` to `agent:R3-LAY` (routing, not content, like the executor-hygiene relabel) so it no longer reads as build-ready — but where this affects a whole dormant backlog (dozens of leaves), **surface the project to the operator** with the recommendation (greenlight, or relabel off `F0-RGE` until greenlit) rather than bulk-relabelling unprompted, since a large mutation across an un-greenlit project is consequential. (Worked case: ~25 Luna MVP leaves plus later-milestone app.fitness and cOS.Build video-iteration-2 leaves, all pre-labelled `F0-RGE` in Refinement with no active siblings, where a literal invariant would have moved ~25 tickets to Todo — 2026-07-04, APP-435.)
    The test that separates the two is **active siblings**: a leaf in a project with work already in flight is a stranded ready leaf (correct to Todo); a leaf in a project with none is a dormant pre-label (relabel off `F0-RGE`, don't promote, surface the project). `agent:R3-LAY` is itself a legitimate resting label in Backlog / Refinement / In Review, not triage-only.
- **Assignee discipline.** Assign the operator at refinement **only** when the ticket rests under `human` (placement 4) with a genuine question or decision for him; placements 1–3 park **unassigned**. When routing a Todo ticket to Forge, clear the assignee. Any comment that assigns the operator must @mention him (`@aledpritchard`) and lead with the specific action or decision needed, and where it hands him steps to run, format them as novice-friendly numbered actions (see `convention-ticket` *Writing an instruction the operator has to action*).

## Refinement comment sweep

Refinement is the operator's lane and is **read-only** to the pm leg for ticket *content* — never re-refine, or rewrite the title or body of, a Refinement ticket — with two narrow exceptions:

1. **Operator comment since the last pm/agent comment.** For tickets where the operator has commented since the last pm/agent comment, process his comments as trusted instructions (steps below).
2. **Executor-hygiene relabel.** A clearly human-owned Refinement ticket still carrying `agent:R3-LAY` — a pre-convention leftover, parked under a standing agent comment for the operator (e.g. a real-device verify, a credential setup, a recruit-testers task) — may have its `agent`-group value set to **`human`** and be assigned to the operator. This changes *routing, not content*, so it is not a re-refinement: it drops the ticket from the `R3-LAY` triage poll, matching convention-linear's rule that refined human-owned work carries `human` precisely so it leaves the poll. Setting `human` here is the documented hygiene action, not a consequential call — no hold and no new operator comment is required. Leave the ticket's title, body, and standing comment untouched, and skip the comment-idempotency re-read on a ticket once it has been migrated (it no longer matches the poll). This is the sanctioned migration path for tickets parked in Refinement under the old label before the `human`-value convention existed.

For exception (1), process his comments as trusted instructions:

- Apply his answers to the ticket — **title and body**. Where the answer resolves a placeholder carried in the title (e.g. `(purpose + icon)`), rename the title to reflect it. Don't echo back a question whose answer is already on the ticket.
- Reply confirming what changed.
- Promote to Todo when he says it is ready (routing to Forge where execution is needed, per the rule above).
- Clear the assignee once the ask is resolved.

Never re-refine a Refinement ticket's content without a new operator comment; the executor-hygiene relabel in exception (2) is the only routing change permitted without one. A **harvest** of a completed blocker's output into a parked harvest-dependent leaf (below) is likewise a permitted content change without a new operator comment — it applies the source ticket's resolved output, not a fresh refinement of the leaf.

## Harvest sweep

Placement 3 (harvest dependency) parks a leaf in Refinement under `agent:R3-LAY` because its spec does not yet exist — it is determined by a blocker's output. Each run, re-check these parked harvest leaves and complete them when their source resolves.

**Run the harvest and blocked sweeps before comment-idempotency silencing** (see the guardrail). A blocker completing since the last comment is a material change, so the sweep must re-evaluate the leaf before any "nothing changed, stay silent" test runs — otherwise a standing Relay comment silences the very run that should harvest and promote (worked case: A1-108 / A1-109 left parked though both blockers — A1-106 Mux env, A1-107 the placement decision — had gone Done minutes earlier, 2026-07-01, APP-408). The harvest is self-terminating — it routes the leaf to Todo + F0-RGE and out of the poll — so there is no re-fire risk in running it first.

For each Refinement leaf under `agent:R3-LAY` carrying a harvest dependency:

- **Is the source blocker done? Read the blocker's own status, never the relation.** The connector does not expose Linear's derived active/resolved flag: a `blockedBy` array keeps listing a blocker after it completes and auto-resolves (the activity feed's "no longer blocked by …" is derived, not a change to the relation record), and the relation payload carries only `{id, title}` — no `resolved` / `active` field (verified across A1-108, A1-109, COS-236, A1-15, 2026-07-01, APP-408). So for each blocked-by id, fetch that blocker's status and treat **Done / Canceled / Duplicate** as satisfied. Do **not** wait for the relation to disappear — it never does on its own; the record persists until explicitly deleted — and do **not** read block-satisfied off the relation payload, where no flag exists. This costs one `get_issue` per blocker until the connector carries the flag; the connector-side ask is named in `convention-linear` *Reading Linear safely*. If the blocker is not done, leave the leaf parked — a blocker merely reaching a done state is the *cue* to harvest, not the completion itself.
- **Harvest the output into the leaf's body.** Lift the blocker's resolved output into the leaf so it becomes a self-contained Pattern A brief the builder can run headless. For a design→build dependency this means the Figma node-id, the reference asset/SVG, the states, and the interaction/motion spec are written into the build leaf's `## Acceptance criteria` and body — not a bare "matches the locked design" with nothing to build against. (Worked example: a build leaf carrying a "Locked design (harvested from …)" block with the node-id, SVG, and hover spec is buildable; one carrying only an icon *name* is not — unless the icon renders from the CMS, see the DoD note below.)
- **Then route to Todo + executor** (placement 1 or 2), assignee cleared. Harvest-then-promote is a single triage action.

**Design definition-of-done — a producing leaf isn't done until its output exists to harvest, *unless the build renders it from the CMS*.** A design (or other producing) leaf marked done on an artefact *name* alone — no node-id, no asset/SVG, no states, no interaction spec, no resolved purpose/destination — has nothing harvestable, so its dependent build children cannot proceed regardless of state, and the "done" is misleading. When refining or reviewing a producing leaf, require its DoD to name node-id + asset + states + interaction spec + resolved purpose before it can be closed. Two failure modes this guards against: (1) the design goes done but nobody lifts its spec into the build child (harvest never triggered — this sweep is the trigger); (2) the design is closed with no real output (nothing to harvest).

**But distinguish a bespoke-spec build leaf from a CMS-rendered one before bouncing on "only a name".** The DoD above applies to build leaves carrying a *bespoke* design — custom motion, exact states, a hand-drawn asset — where the name without the spec is genuinely un-buildable. It does **not** apply to a build leaf that renders an icon purely from the CMS (`item.icon`) with standard interaction and an established sibling build pattern: there the icon *name* plus the sibling pattern is a complete brief, and no hardcoded node-id/SVG exists to harvest by design (the schema decision made icons CMS SVG uploads). Bouncing such a leaf as "un-harvestable" is a false negative — it forced repeated Todo→Refinement bounces and operator intervention on three CMS-rendered nav-icon builds whose only bespoke sibling carried the motion spec (A1-50 / A1-51 / A1-53 vs the m-wave A1-49, 2026-07-01, APP-406). So before bouncing a build leaf for a name-only design source, ask whether the build needs a bespoke spec at all: if it renders from the CMS with standard interaction and a sibling pattern to follow, it is buildable — route it to Todo, do not bounce it.

## Blocked-state sweep

Blocked is otherwise a silent gap: a blocker that has since cleared (PR merged, dependency Done, relation cancelled) leaves the dependent stranded until the operator notices by hand. Each run sweeps it.

Scope: tickets in **Blocked** carrying **any** `agent:*` label (not only R3-LAY). In Progress and In Review are left untouched.

For each such ticket, read its blocked-by relations and judge whether each blocker is resolved (Done / merged / Canceled / Duplicate). As in the harvest sweep, judge resolution by the **blocker's own status**, not by the relation's presence or absence — the connector keeps a satisfied block in the `blockedBy` array with no resolved flag, so a live block and an auto-resolved one look identical until you fetch the blocker (APP-408):

- **Stale-relation cleanup (autonomous).** Where a blocker was **Canceled** or marked **Duplicate**, actively remove that blocked-by relation and note the removal in a comment. This is the one autonomous mutation the sweep makes.
- **All blockers cleared → surface, never auto-route.** Do not silently move the ticket onward — auto-routing a cleared ticket straight to Forge + Todo risks an unblock→route→re-block loop. Instead post a comment @mentioning the operator (`@aledpritchard`) with the recommended next move, assign him, and set priority **Urgent**. Only after he agrees does the *next* Relay run execute the move.
- **Still genuinely blocked → leave in place.** Optionally note any stale or invalid blocker relation, but do not move it.

**Informal-blocker check.** The bullets above judge resolution by **formal blocked-by relations**. But a ticket is just as often parked in Blocked by an **informal blocker** — a precondition stated in a comment that no relation tracks (e.g. an `[exec]` note "blocked — canon content not attached", or "waiting on the operator's decision"). These are the silent ones: nothing in the relation graph changes when they clear, so the sweep must read the thread. For each Blocked ticket, alongside its relations:

- **Identify the blocking comment.** Take the most recent agent / exec / Relay comment that explains why the ticket sits in Blocked, and extract its stated precondition(s): a missing attachment or file, body content that must be present before execution, or a decision/answer awaited from the operator.
- **Test against current state, not the state when the comment was written.** Are the named attachments now present? Has the description been edited to include the missing content? Has the operator commented an answer since? A change to `updatedAt` or the attachment count *after* the blocking comment is the cue to re-read.
- **All preconditions satisfied → surface, never auto-route** — same handling as a cleared formal blocker: comment @mentioning the operator (`@aledpritchard`) with the recommended next move, assign him, set priority **Urgent**, hold in Blocked until he confirms.
- **Still outstanding → leave in place**, optionally noting in one line exactly what is still missing so the thread stays current.

**Unblock policy — what to recommend when all blockers have cleared:**

- *Ready to action* (complete brief, just needs execution) → recommend **Todo** with concrete instructions. If it is repo/code work, the *subsequent* routing pass sets `agent:F0-RGE` — never auto-set in the same run.
- *Clearing it needs separate work* → recommend a new unblocking ticket in **Backlog** at High/Urgent, linked as the blocker.
- *Brief now incomplete* → ask the specific question in-thread, refine on the operator's reply, then promote to Todo.

**Freshness check before recommending a move.** When all blockers have cleared (formal or informal) and you are about to recommend **Todo**, sanity-check that the content the ticket *carries* is still current before recommending the move. Scan the ticket's related tickets and recent comments for any note that an attached or referenced artefact has since been superseded. If it has, recommend re-attaching the latest version or narrowing the ticket's scope rather than routing stale content onward. (Worked case: a publish ticket whose two formal blockers had both cleared, but a related ticket recorded that one of its attached canon files had since been advanced in a later session — surfacing that wrinkle rather than auto-routing a stale publish was the correct call, and this check makes it mandatory.)

## Guardrails

- **Act on safe, unambiguous moves; hold only the consequential.** Triage's default is to *act*, not to propose-and-wait. A refined leaf is moved to its **correct resting state** per the four placements above — **Todo + executor** when clean, **Todo + executor + blocked-by** for a sequencing dependency (the builder skips it while blocked and auto-picks it up when the blocker clears), **Refinement under `R3-LAY`** for a harvest dependency, or **Refinement under `human`** where a genuine operator decision remains (including a decision-gated leaf in an active/greenlit epic — never left in Backlog) — rather than held in Backlog under a comment awaiting a separate confirm. Note the invariant: an agent executor (`F0-RGE`) never rests in Refinement — a human gap parks under `human`, a harvest wait under `R3-LAY`. **The invariant's "correct it" action applies only to a stranded ready leaf in an active flow; a pre-labelled `F0-RGE` leaf in a dormant, un-greenlit project (no active siblings) is not corrected to Todo but relabelled off `F0-RGE` and surfaced to the operator — see the invariant's dormant-project carve-out.** This is what convention-ticket / convention-linear require: a refined leaf ready-except-for-a-sequencing-dependency belongs in **Todo-with-blocker**, not parked. It applies on **all** delivery teams, not only os.Claude; os.Claude is simply the same default, with repo-native and publish work going straight to Todo. Reserve **hold-and-ask** strictly for **consequential or ambiguous** calls — those still go to the operator in-thread, never a guess (the guardrail below). (Supersedes the earlier blanket "suggest-mode first". This reverses the act-first rejection recorded against APP-283 / APP-292 (Canceled), on a fresh, direct operator instruction — 2026-06-30, when A1-108 / A1-109 sat in Backlog under `R3-LAY` while fully refined and only dependency-blocked, and the operator flagged the hold as slowing refinement. The consequential/ambiguous escalation below remains the unattended-safety rail, so dropping blanket suggest-mode does not remove it.)
- **Comment idempotency.** When triage *does* hold-and-ask — a consequential or ambiguous call, or a recommendation surfaced to the operator — a standing recommendation is posted once, not re-posted every poll. Before commenting a routing, decision, or surface recommendation, check whether the latest pm/agent comment on the ticket already carries the **same standing recommendation** and nothing has changed since it — no new operator comment, no state, label, assignee, or attachment change. **A blocked-by or related source reaching a done state counts as a material change** for this test, not only changes on the leaf itself: a harvest source going Done since the last comment must re-trigger the sweep, never be silenced as "nothing changed" (APP-408). This is why the harvest and blocked sweeps run before idempotency silencing. If nothing material has changed, stay silent: the standing recommendation holds. Re-comment only when the recommendation itself changes, the ticket changes materially, or the operator has replied. This prevents near-identical comments accumulating on tickets correctly held awaiting the operator (worked case: APP-324, A1-78, each carrying four duplicate Relay routing comments, 2026-06-29). Distinct from APP-304's fix, which relabelled *human-owned* tickets to `human`; these are agent-owned and Forge-ready, so the gap is comment idempotency, not executor labelling.
- **Human-owned Refinement tickets get migrated, not re-read every poll.** A clearly human-owned ticket parked in Refinement under `agent:R3-LAY` matches the poll on every run, and the read-only rule otherwise offers no terminating action — so it re-surfaces unboundedly, costing comment-reads and obscuring genuinely new `R3-LAY` work. The executor-hygiene relabel in the *Refinement comment sweep* is the terminating action: set the `agent`-group value to `human`, assign the operator, and it drops from the poll. Apply it the first time such a ticket is seen rather than re-evaluating it each run (worked case: ~5 pre-convention human-owned Refinement tickets — APP-176, APP-123, APP-130, COS-235, COS-167 — re-surfacing every triage poll, 2026-06-30).
- Never executes code, opens branches, or merges.
- Consequential or ambiguous calls go to the operator, never a guess.
- **Canon changes: spot, capture, and stop — never author the wording here.** When triage converges on a canon change (to `convention-linear`, `convention-ticket`, a skill, etc.), file a `FRICTION:` note to the Pulse queue and stop. Do **not** author the proposed canon wording — not even as a "draft for review" — here: authoring and packaging is the Pulse drain's job (`skill-ops-retro` / `task-pulse-retro`), which assesses the change against current canon and consolidates it. A triage session reaching the drain step is doing the drain's work in the wrong place (worked case: 2026-06-30, APP-403). The completion pull to write it out is exactly what this guardrail resists: the note captures the observation; the drain authors the artefact.
- **"Never a ticket" means never a delivery/Forge ticket — the `PUL-5E` capture note is correct.** Filing a `FRICTION:` capture note to the Pulse queue is the *right* action, not a forbidden one. The forbidden routes are a canon edit routed to Forge as a delivery ticket, and an in-place `SKILL.md` edit. Do not let "canon changes never go through a ticket" discourage the legitimate capture note.

**Blocker discipline.** Blocked-by relations are for genuine dependencies only: shared files that would conflict, artefacts that must exist first (assets, builds, merged tooling), or decisions that gate scope. Strategic sequencing — platform order, milestone order, "do this before that" — is carried by project milestones and the operator's promotion to Todo. Never encode sequencing preferences as blocks. Do not add a blocked-by relation merely because tickets are thematically sequential or because one would logically follow the other.

**Repo-wide baseline tickets.** A ticket that touches many files across a repo (format pass, lint sweep, large-scale refactor) is a genuine dependency for any parallel ticket editing the same files. When such a baseline ticket is in flight (Todo → In Review), identify any simultaneously-actionable tickets that edit overlapping files and add blocked-by relations to them so they are not worked concurrently. Sequencing via blocks here prevents merge conflicts at the source, complementing CI and the merge queue.

## Setup

- Claude Code Desktop -> Schedule -> New remote task. Cadence: a few times a day (mind the ~1h floor).
- Connector: Linear only.
- Routine prompt: "Run the triage skill."

On finish, run skill-ops-retro capture on this run's friction: file a FRICTION note to the Pulse queue for the drain pass to assess.
