---
name: yarnt
description: Work the Yarnt ticket queue for the current repo. Read `.yarnt` for the project name, pull every ticket in that project assigned to you, do them or dispatch them to subagents, commit the results, then re-check and halt when nothing is left assigned to you. Use whenever there is Yarnt work to pick up.
---

# Yarnt

You take the tickets Yarnt has assigned to you and either dispatch them to subagents or do them
yourself, then verify and commit the work. Decide and move forward on your own; involve a human
only for what you fundamentally cannot do (secrets, third-party account setup).

**Scope.** This skill covers the queue: which tickets are yours, what they say, who does them, and
when they are done. It does not cover **publishing** — pushing to origin, releasing, deploying.
Those differ per repo and per machine, so a skill that guessed at them would guess wrong. If your
harness ships work, it wraps this skill and does that part around it. On its own, this skill leaves
finished work committed.

**Work your own queue until it is empty, then stop.** Every ticket query is scoped
`assignee=me` — you only ever work tickets assigned to *you*, never the owner's and never
unassigned ones. Pull that queue, work everything in it, then re-check once for tickets assigned to
you that arrived while you were working. When nothing new is assigned to you, report and halt — no
backoff, no waiting, no scheduled wakeup. If work appears after that, the owner re-invokes `/yarnt`.

This skill is global — it runs in whatever repo you're invoked from. **Your first job is to figure
out which Yarnt project this directory is, then work only that project's tickets.**

## Identity & scope
- **You are** an integration in this Yarnt workspace. `whoami` returns your own id/name/kind, plus
  the person who granted you access. Use that id anywhere assignees or user ids are needed — do not
  guess which `list_users` row is you.
- **Production** MCP is `mcp__yarnt` (NOT `mcp__yarnt-dev`). Tools (20): `list_projects`, `list_tickets`,
  `get_ticket`, `create_ticket` (pass `wish: true` to file it in Wishes), `update_ticket`
  (edit `title`/`body`/`assignee`/`project` — `assignee`/`project` null to clear), `start_ticket`,
  `finish_ticket`, `drop_ticket`, `unstart_ticket`, `add_comment`, `list_comments`, `list_history`,
  `list_attachments`, `get_attachment`, `add_attachment`, `drop_attachment`, `start_upload`,
  `finish_upload`, `list_users`, `whoami`. Assignees must be user IDs (from `list_users`), not emails.
- **One workspace, many projects.** Each repo is a *project* inside that one workspace. Your identity
  never changes; what changes per repo is the **project** you scope to.
- The human owner's user ID is available from `list_users` — use it for reassignment (not email).

## Match this directory to a project (do this first, every time)
The repo names its own project in a **`.yarnt` file at its root**, exactly like it names its remote:

```
project: Yarnt
```

1. Read `.yarnt`. If it names a project, adopt it and skip the rest — no discovery, no guessing.
2. **No `.yarnt`** → take the working directory's basename (e.g. `/path/to/yarnt` → `yarnt`) and
   `list_projects`, looking for a case-insensitive match.
3. On a clean match, adopt it and say so. On no match or an ambiguous one, **ask the owner** which
   Yarnt project this directory maps to, offering the `list_projects` names.
4. Either way, **write `.yarnt` and commit it** so no future run has to re-derive the mapping.
   Committing it is the point: a memory lives outside the repo, does not survive a move to another
   machine, and is not reviewable. The file also gives a human somewhere to override a bad guess,
   and lets an external coordinator build a project-to-directory map by globbing `*/.yarnt` across
   its checkouts.
5. If `.yarnt` names a project that no longer appears in `list_projects`, re-confirm with the owner
   before proceeding, then correct the file.

The file may carry other `key: value` lines belonging to whatever harness runs you. Read the
`project:` line and leave the rest alone.

**Scope every ticket query to the matched project.** Pull with
`list_tickets assignee=me project=<name>`, and pass `project=<name>` when you `create_ticket` so
escalations land in the right project.

## Pull your queue, and re-check until it's empty
Call `list_tickets assignee=me project=<name>`. **Always `assignee=me`** — tickets assigned to the
owner, or to nobody, are not yours to pick up, no matter how relevant they look. That set is your
current batch.
- Empty on the very first call → say so and stop. Nothing else.
- Non-empty → work **all** of it before you re-check. Don't cherry-pick the easy ones.

When the batch is done (finished or escalated), **call `list_tickets assignee=me` again**. Work can
be assigned to you mid-run — the owner assigns one, another agent does, or your own work spawns a
follow-up.
- New tickets assigned to you → they become the next batch. Work them, then re-check again.
- Nothing new assigned to you → go to **Halt**.

Anything you escalate leaves your queue by construction: you reassign it to the owner, so the next
`assignee=me` check will not return it. Never pull it back to keep yourself busy.

**Keep a set of ticket IDs you have already attempted this run**, and treat a re-check as empty when
every ticket it returns is already in that set. Escalation should keep blocked work out of your
queue on its own; the set is the backstop for when it doesn't, so a ticket you cannot make progress
on can never loop you forever.

Re-check **immediately** after each batch — never sleep, back off, or schedule a wakeup between
checks. This ends by your queue being empty, not by a timer.

## Read the description, not just the title
`list_tickets` surfaces only the title line. **Every ticket has a `body` (description) you must
read before designing anything.** Fetch it with `get_ticket id=<id>` — it returns the full body,
project, assignee, labels, and blocked status. Treat that body as the spec. Tickets may also have
**comments** that add context or feedback — fetch them with `list_comments ticketId=<id>` (oldest first)
and read them as part of your full ticket context.

**Attachments are part of the spec too.** A screenshot is how a person reports a visual bug, and
they will assume you looked at it. Call `list_attachments ticketId=<id>`, and `get_attachment`
anything that could matter — images come back as images you can actually see, logs and diffs as
their text. A build log or a screenshot of what you produced belongs there too, not pasted into a
comment. **To hand back a file, use `start_upload` and `finish_upload`, not `add_attachment`.**
`start_upload` gives you a URL your shell PUTs the file to, and `finish_upload` records what landed
— so the bytes never pass through you. **Run the upload command exactly as `start_upload` prints
it**, including its `-H "Content-Type: …"`: `curl --upload-file` declares octet-stream on its own.
`add_attachment` takes base64 inline and is only for something short you are writing out yourself:
you cannot emit a real file's base64 without a slip, and one slipped character corrupts it.
`drop_attachment id=<id>` removes one you attached by mistake, so a wrong file does not sit next to
its replacement forever.

## Ticket lifecycle
- `start_ticket` when you begin it (dispatch to a subagent, or start it yourself).
- `finish_ticket` when the work is **tested, committed, and merged into the repo's main line**, and
  never before you comment the commit log (see *Verify, then finish*). Finishing is about the work
  being done and correct, not about it being released — a ticket does not wait on a deploy that this
  skill does not perform.
- **A genuinely blocked ticket is escalated, never left sitting.** If you cannot make progress —
  it conflicts with in-flight work you can't isolate, or it needs something only a human can supply
  — hand it back:
  1. `add_comment` saying exactly what you tried and what you are blocked on. Write it for the
     person who picks it up; `unstart_ticket` clears the `blocked` flag and reason, so **the comment
     is the only durable record**.
  2. `unstart_ticket` — the ticket leaves `doing` for the top of Next, freeing the WIP slot.
  3. `update_ticket assignee=<owner id>` to put it on the human's plate.

  Leaving it started and assigned to you is the wrong outcome twice over: it burns a WIP slot, and
  because it stays in your queue any scheduled coordinator re-dispatches an agent onto the same dead
  ticket on every cycle, forever. Report every escalation at the end. Never silently drop one.

## Dispatch pattern
1. Read the body (above). Research the subsystem yourself or with an `Explore`/research subagent.
2. Do it yourself when it's small enough that briefing costs more than doing it; dispatch when the
   work is sizeable, parallelizable, or needs its own context window.
3. Pick the model by complexity: **Sonnet** for well-templated / mechanical work following a clear
   plan; **Opus** for design-sensitive core UX, non-obvious debugging, or anything that seats a
   convention. Set the model explicitly (subagents don't inherit yours).
4. Give a **detailed brief**: exact files/patterns to mirror, the acceptance criteria from the body,
   shell discipline (one command per Bash call; no `&&`/`;`/`cd`/`git -C`/heredocs/`$(...)`; plain
   git; create files with Write), commit-as-you-go by explicit pathspec, and **no PRs**.
5. Isolate with `isolation: "worktree"` whenever the main tree is dirty from parallel agents or when
   multiple subagents edit files concurrently — the subagent gets a clean checkout and can't collide.
6. **Split by conflict surface.** If a ticket has an isolable backend and a UI half that would
   collide with in-flight work, ship the backend first (independent) and defer the UI until the tree
   settles — under the same ticket, which stays in `doing` until both land.
7. Batch related tickets to one subagent when they should move together.

## Coordinating with uncontrolled parallel agents
Other agents (not yours) may be editing the main working tree concurrently.
- Never merge while another process is writing to it (ref/index race, `git add -A` sweeps).
- If the owner has a done-signal convention for this repo (e.g. touching a marker file), watch for it
  (a one-shot background `until [ -f ... ]` script), act, then delete the marker.
- Always commit by explicit pathspec and verify `git diff --cached --stat` is 100% yours.

## Verify, then finish
1. Confirm a clean tree; `git merge --no-edit <branch>` (clean auto-merge expected when files don't
   overlap).
2. Typecheck and test with this repo's own targets (e.g. `make check`; the relevant suite for what
   changed). Run the tests the change actually touches — a ticket is not done because a subagent
   said so.
3. **`add_comment` with the commit log, every time.** List each commit that made the fix, one line
   apiece, as `<short sha> <subject>`. This is what tells the reader where the work is; the ticket
   is the only durable record, and a finished ticket with no commits named makes the reader search
   the history by hand. Add a sentence above the list when something is not obvious from the
   subjects — a decision you made, a part you left out. No sentence is fine. **No list is not.**
   A ticket that needed no commit (a duplicate, a question you answered) says that instead.
4. `finish_ticket`, then clean up any worktree: `git worktree remove --force <path>` +
   `git branch -D <branch>`.

## Decisions & escalation
- **Bias to decide.** Resolve product/design questions yourself from the repo's own docs
  (`PRODUCT.md` / `project/*.md` if present) and memory. Bubble up only genuine judgment calls about
  the owner's priorities.
- **Bubble to a human** only for what you fundamentally cannot do: secrets/env vars, third-party
  account setup.
- **Channels: a ticket assigned to the owner is the only one that reliably arrives.** `create_ticket`
  (with `project=<name>`), then `update_ticket assignee=<owner id>` (from `list_users`). For something
  that belongs to an existing ticket, `add_comment` and reassign that ticket instead.
- **Search the queue before you file — the duplicate is on someone else's plate.** Each run starts
  with no memory of the last one, so a blocker that outlives a run (a missing credential, a broken
  deploy) gets re-filed on every cycle unless you look first. Before every `create_ticket`, call
  `list_tickets project=<name>` **with no assignee filter** (`list=queue`, and `wishlist` if you
  would file a wish) and read the titles for the same problem. Your own `assignee=me` queries never
  show it, because you reassigned it to the owner the moment you filed it.
  - Same problem already open → `add_comment` on it with what this run saw (the commits now
    unshipped, the exact command and error if they differ) and file **nothing**.
  - Genuinely new → file it, and title it so the *problem* is legible from the queue alone, not the
    run that hit it. A title the next run can recognise as its own problem is what makes this check
    work; three titles for one missing token is what happens when it doesn't.

  A second ticket never makes the owner act sooner. It splits the history across copies and leaves
  the reader to work out which one is live.
- **Fold duplicates you find, even ones you filed.** Comment the merged detail on the oldest copy —
  it is the one nearest the top of the owner's queue and it carries the history — then `drop_ticket`
  each newer copy with a comment naming the survivor's id. Leaving them is how one blocker grows a
  ticket per cycle.
- You may also prompt in chat *when a human is demonstrably there* — but assume one is not. A
  scheduled coordinator runs this unattended, so anything raised only in chat or only in your final
  report is said to nobody. **If the owner has to do something, it is a ticket assigned to them, every
  time.** Say it in the report as well, never instead.

## Halt
Halt when an `assignee=me` re-check returns **no ticket you have not already attempted this run**.

Report, across every batch you worked, per ticket: what landed, what tests ran and their result, and
anything you left undone and why — call out escalated tickets explicitly, since they are now waiting
on the owner. Say plainly that the work is committed and not published, so whatever runs you knows
there is something to ship.

Then end the turn — no `ScheduleWakeup`, no self-rescheduling, no idle polling for work that has not
arrived yet.
