skills/linear-audit/SKILL.md
Forensic retro across a whole Linear board to find WHERE work went wrong — especially the "collaborator asked for X, we delivered, it wasn't right" loop. Reconstructs every ticket's lifecycle from issue history (status transitions + actor), labels, comments, and attachments; cross-references the linear-listen activity log (agent dispatches per ticket) and linked GitHub PRs; isolates every Ready-to-Test → bounced-back rework instance; classifies each by root cause (context-not-loaded / bad-question / built-before-AC / no-AC-for-verification / spec-misread / premature-done / env); and writes a trends report to .linear-audit/. Files SYSTEM gaps against ronan-skills and PROJECT gaps against the product repo; the weekly scheduled run also opens Linear review issues tagged to Ronan. Use when Ronan says "audit the linear board", "linear retro", "why do tickets keep bouncing", "where did we go wrong with Eoin's tickets", "/ro:linear-audit".
npx skillsauth add RonanCodes/ronan-skills linear-auditInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The diagnostic counterpart to /ro:linear-run. Where linear-run moves tickets forward, linear-audit looks backward across the whole board and answers one question: where did we deliver something the collaborator asked for, and get it wrong? That loop — a ticket reaches Ready to Test, the collaborator bounces it back ("this isn't what I asked for"), we rebuild — is the signal that matters. This skill counts those bounces, reconstructs why each happened, and turns "it keeps happening" into "here are the four causes, ranked, with the fix for each."
Run it from the product repo (the one with .ro-linear.json, first use case nutmeg). It is read-only against Linear by default: it reads the board, writes a local report, and files gap issues. It never moves a ticket.
The collaborator is whoever .ro-linear.json names (econnolly321 / Eoin for nutmeg — the same person Ronan refers to as "Owen"). Everything below is config-driven; no name is hardcoded.
# Audit everything since the last audit checkpoint tag (default window — see US-0)
/ro:linear-audit
# Specific window
/ro:linear-audit --since 30d
/ro:linear-audit --since 2026-06-01
# Weekly scheduled run: report + GH gap issues + Linear review issues tagged to Ronan + ping
/ro:linear-audit --scheduled
# Report only — no issues filed anywhere
/ro:linear-audit --no-issues --no-linear-issues
/ro:repo-mode: a work repo NEVER touches Linear or GitHub — refuse loudly. Personal repos only..ro-linear.json (teamKey, collaborator, owner, reworkLabel, reviewLabel, states.*). --team / --collaborator override.--since accepts an ISO date or a relative span (14d, 30d). Default: since the last audit checkpoint — git tag -l 'linear-audit/*' | sort | tail -1, using that tag's commit date (every audit ends by tagging main, US-6b). No tag yet → fall back to the newest report date in .linear-audit/, then to 14d. The audit reads tickets touched (any history/comment event) inside the window, regardless of current state — a ticket sitting in Done can still have bounced twice last week.LINEAR_API_KEY from ~/.claude/.env for the raw GraphQL calls below (the MCP get_issue / list_comments are fine for content, but issue history — the state transitions this audit is built on — needs GraphQL).The audit triangulates the Linear board, the listener's own logs, and the GitHub execution layer. Don't skip a source: the board says what state the ticket reached, the listener log says how the agent handled it, and the PRs say what actually shipped.
Source A — Linear board (GraphQL). For every issue touched in the window, pull the full lifecycle. History is the load-bearing part: it carries the state transitions and their actor.
query($teamId: String!, $after: DateTimeOrDuration) {
issues(filter: {team: {id: {eq: $teamId}}, updatedAt: {gt: $after}}, first: 100) {
nodes {
identifier title description url createdAt updatedAt
state { name type }
labels { nodes { name } }
creator { name }
assignee { name }
attachments { nodes { title url } }
comments(first: 100) {
nodes { body createdAt updatedAt user { name } }
}
history(first: 100) {
nodes { createdAt fromState { name } toState { name } actor { name } }
}
}
}
}
Attribution (clean since the RoBot service account). The agent now acts as its own Linear user (RoBot,
robotUserId), soactoron every comment, label, AND state transition distinguishes agent from human exactly — no timestamp-correlation guesswork. Attribute each event byactor.id: RoBot = agent, the collaborator / owner = human. (Older history from before the service-account switch on 2026-06-17 still acts as Ronan; for transitions in that window, fall back to correlating against the listener activity log in Source B. Note the cutover date in the report's attribution line.)
Pull all images: every attachment URL above, plus inline images in the description and comments via mcp__linear-server__extract_images — the audit needs to see whether the collaborator supplied a screenshot/mockup that the build ignored.
Source B — the listener's forensics. Read ~/.claude/linear-listen-activity.log (time ⇥ NUT-id ⇥ who ⇥ reason ⇥ logpath) and the per-run logs under ~/.claude/linear-runs/<ticket>-<ts>.log. This is where the comment-storm signal lives: multiple dispatches for the same ticket close together in time means the agent acted on comments piecemeal (the bug fixed by coalesce/supersede in /ro:linear-listen). Count dispatches per ticket and flag any ticket with ≥2 dispatches inside a 10-minute span as a candidate context-not-loaded case.
Source C — GitHub PRs. gh pr list --state merged --search "merged:>=<since>" --json number,title,body,mergedAt and match each to its ticket via the mirrored issue's Closes #N. Lets the audit say "the bounce came after PR #156 shipped" and read the close-with-summary comment for what the agent thought it built.
For every ticket, build an ordered timeline from the merged history + comments + dispatches:
NUT-42 "filter by venue" (creator: Eoin)
09:02 created (Backlog)
09:03 comment Eoin: "also needs to remember the last filter" ← 2nd comment, +60s
09:05 → In Progress (actor Ronan, +2s after agent dispatch 09:05:01) [AGENT]
09:18 comment RoBot: AC checklist (3 items)
09:41 → Ready to Test [AGENT] PR #156
11:20 comment Eoin: "the venue filter works but it forgets on refresh"
11:20 → Backlog (actor Eoin) ← BOUNCE
The thing to spot: a → <interview state> transition followed later by a → <backlog/todo> transition, or a problem-comment from the collaborator while in the interview state. That pair is a rework instance.
A rework instance is any of:
states.interview, e.g. "Ready to Test") later followed by a transition back to a backlog/unstarted state./ro:linear-run § "Detecting rework").reworkLabel (🔁 Rework) applied at any point./ro:linear-run § "Done audit").Record each with: ticket id, how many times it bounced (a 2× bounce is a critical pattern), the collaborator's exact bounce comment, what shipped (PR), and the timeline slice around the bounce.
For each rework instance, assign exactly one primary cause. This is where "it keeps happening" becomes actionable. Use the bounce comment + the timeline + the three sources to decide:
| Cause | How you recognise it | The fix it points to |
|---|---|---|
| context-not-loaded | The collaborator left multiple comments (or a comment + an image) and the build only reflects some of them; OR Source B shows ≥2 dispatches close together (comment-storm); OR an image on the ticket was never addressed. | The full-context receipt gate (/ro:linear-run § "Load full ticket context") + coalesce/supersede (/ro:linear-listen). If a bounce predates those, it's confirmation they were needed; if it postdates them, the gate failed — escalate. |
| unasked / bad question | The bounce reveals a decision the grill never surfaced, or the grill asked the wrong thing. | Improve the grill question bank for this ticket shape (/ro:linear-grill). File the specific missing question. |
| built-before-AC-agreed | No AC-agreement comment from the collaborator before the → In Progress transition. | Enforce the AC-agreement gate (/ro:linear-run Step 2). |
| no-AC-for-verification | The interview hand-off comment had no AC checklist and/or no screenshots for the collaborator to verify against — they bounced because they couldn't tell what to check, or checked the wrong thing. | Enforce the hand-off template (/ro:linear-update) — see US-5. |
| spec-misread | AC was agreed and images were present, but the build diverged from the described behaviour anyway. | Implementer discipline; add a guard (e.g. a Playwright test encoding the AC) so the next build can't regress it. |
| premature-done | The agent moved the ticket to Done without the collaborator's tested-and-happy confirmation. | The Done audit already self-heals this (/ro:linear-run); trend it to confirm the audit is holding. |
| env / deploy / flake | The bounce was a runtime/deploy issue, not a spec miss (worked locally, broke on the deployed app). | /ro:diagnose + observability, not a grill/spec change. |
When the signal is ambiguous, use a sub-agent classifier:
You are classifying why a Linear ticket bounced back from review.
Ticket: <id> — <title>
What the collaborator originally asked (description + all comments, oldest→newest): <...>
Images on the ticket: <list / none>
What the agent shipped (PR title + close-summary): <...>
The bounce comment (verbatim): <...>
Dispatch timeline for this ticket (from the listener log): <...>
Pick ONE primary cause from: context-not-loaded, unasked-question, built-before-AC,
no-AC-for-verification, spec-misread, premature-done, env-deploy-flake.
Output JSON only:
{"cause":"<one>","evidence":"<one sentence quoting the deciding signal>","fix":"<one sentence>"}
Two board-wide checks the audit always runs, because they are the cheapest wins and the user called them out directly:
Write the markdown artefact (mirrors the .nightshift/retros/ factory shape). The report is committed to the product repo — it's the durable trail of how the collaboration is trending, and its merge commit is the checkpoint the next audit's window starts from. On first run, remove .linear-audit/ from the repo's .gitignore if an older setup put it there (it was a local artefact before 2026-07-03).
<repo>/.linear-audit/<YYYY-MM-DD>-board-audit.md
Open the report with a one-line checkpoint note ("this report is committed and tagged linear-audit/<date>; the next audit's window starts at that tag") and, where a previous report exists, a trend table (previous audit's headline metrics vs this one) — the trend is the whole point of a standing audit.
Sections:
# Linear board audit — <board> — <YYYY-MM-DD> (window: <since> → now)
## Headline
<N> tickets touched · <R> rework instances on <T> tickets · <D> double-bounces (critical) · AC-on-handoff <x/y> · screenshots-on-handoff <x/y> · comment-storms <n>
## Rework causes (ranked)
| Cause | Count | Tickets | The fix |
|---|---|---|---|
| context-not-loaded | 4 | NUT-42, NUT-51, … | full-context gate + coalesce/supersede |
| no-AC-for-verification | 3 | … | enforce hand-off template |
| unasked-question | 2 | … | add Q to grill bank: "…" |
...
## "Not done as asked" — per ticket
- **NUT-42** (2× bounce) — asked: remember last filter. Shipped: filter without persistence. Cause: context-not-loaded — 2nd comment ("remember the last filter") arrived 60s after the first and a second dispatch fired 4m later; the build reflected only the first. Fix: coalesce (now shipped) would have read both. [PR #156]
...
## Set-up-to-succeed checks
- AC checklist present on 7/11 hand-offs. Missing on: NUT-49, NUT-53, NUT-57, NUT-60.
- Screenshot present on 4/11 hand-offs. …
- Comment-storms (≥2 dispatches/10min): NUT-42, NUT-51.
## Action items
- [ ] **[SYSTEM][ronan-skills][high]** … (proof: NUT-42)
- [ ] **[PROJECT][nutmeg][med]** … (proof: NUT-49)
## Attribution note
<n> transitions could not be cleanly attributed agent-vs-human (see US-9 service-account item).
Then print a one-screen terminal summary: the headline line + the ranked causes table + the top 3 action items. That's what Ronan reads first.
The report only works as a checkpoint if it lands on main with a tag the next run can find. After writing it:
audit/<date>-board-audit), respecting the multi-agent worktree rule (ro worktree acquire — never edit a possibly-busy primary checkout) and the commit-timestamp rule./ro:ship policy (local gate, gh pr merge --squash --admin).git fetch origin main && sha=$(git rev-parse origin/main)
git tag -a "linear-audit/$(date +%F)" -m "Linear board audit checkpoint $(date +%F) (window <since> -> now)" "$sha"
git push origin "linear-audit/$(date +%F)"
The tag is the machine-readable checkpoint (US-0 reads it to resolve the window); the committed report is the human-readable one. Skip neither. A second audit on the same day suffixes the tag (linear-audit/<date>.2).
Same routing as /ro:night-shift-retro § US-5. For each action item (skip under --no-issues):
gh issue create -R RonanCodes/ronan-skills --label from-audit --label "source-repo:<repo>".--label ready-for-human --label from-audit.Body carries: the cause, the proof tickets (with Linear URLs), the bounce comment, and the recommended fix. Dedupe against existing open from-audit issues by title so a weekly run doesn't refile the same gap.
--scheduled)The audit is most useful as a standing weekly habit, not a thing Ronan remembers to run. Two additions over the on-demand run:
1. Create Linear review issues tagged to Ronan. For each high-severity trend (cap at the top 5; never spam the board), open a Linear issue on the team so Ronan reviews it where the work lives — he asked for this so he can sanity-check the audit's conclusions:
[Audit] <trend> (e.g. "[Audit] 4 bounces last week traced to context-not-loaded").NUT-xx links), the recommended fix, and a one-line "close this if you disagree" so a wrong call is cheap to dismiss.LINEAR_API_KEY), so the listener ignores it automatically (RoBot-created) — no label needed.list_users); assignment is what routes it to his queue.--no-linear-issues. Dedupe against open [Audit] issues by title so the weekly run updates rather than duplicates.2. Notify. Fire Pushover + Telegram with the headline line, deep-linking the report:
url="file://$repo/.linear-audit/$(date +%F)-board-audit.md"
bash "${CLAUDE_PLUGIN_ROOT:-$HOME/Dev/ronan-skills}/skills/pushover/scripts/notify.sh" \
"linear audit: <R> bounces on <T> tickets, top cause <cause> (<n>×)" \
--title "Linear audit" --url "$url"
bash "${CLAUDE_PLUGIN_ROOT:-$HOME/Dev/ronan-skills}/skills/telegram/scripts/notify.sh" \
"linear audit: <R> bounces on <T> tickets, top cause <cause> (<n>×) — $url" \
--title "Linear audit"
Scheduling (launchd, not /schedule or CronCreate). This run needs the local repo, local ~/.claude/.env, and gh auth, and must fire whether or not a Claude REPL is open — so a cloud routine (/schedule) and the session-scoped CronCreate are both wrong. Use a launchd agent (same mechanism as /ro:linear-listen's daemon), which fires on the next wake if the laptop was asleep and survives reboots. The nutmeg agent is installed at ~/Library/LaunchAgents/com.ronan.linear-audit.plist:
<key>Label</key><string>com.ronan.linear-audit</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string><string>-lc</string>
<string>cd "$HOME/Dev/<your-product>" && exec ~/.local/bin/claude -p "/ro:linear-audit --scheduled" --dangerously-skip-permissions</string>
</array>
<key>StartCalendarInterval</key><dict><key>Weekday</key><integer>5</integer><key>Hour</key><integer>17</integer><key>Minute</key><integer>7</integer></dict>
<key>StandardOutPath</key><string>~/.claude/linear-audit.log</string>
launchctl load ~/Library/LaunchAgents/com.ronan.linear-audit.plist to enable, launchctl unload … to pause. Friday 17:07 (off the :00 mark). For a second board, copy the plist with a new Label + cwd. The cwd is the product repo because the skill reads .ro-linear.json from there.
Attribution is no longer a blind spot. The agent acts as its own Linear user (RoBot, robotUserId) as of 2026-06-17, so every agent comment, label, and state transition carries a distinct actor — the audit attributes each event exactly by actor.id, and /ro:linear-listen's loop-safety is a clean user-id check (NUT-77). The only residue is history from before the cutover (agent acted as Ronan then); for transitions in that window, fall back to the Source-B timestamp correlation and say so in the report.
If a future audit ever finds the agent writing as Ronan again (a transition or comment by the owner id that lines up with a dispatch), that means a skill used the OAuth mcp__linear-server__* tools instead of the RoBot LINEAR_API_KEY — file that as a SYSTEM gap against ronan-skills (the offending skill must switch to the key), because it both breaks attribution and risks a listener loop.
| Condition | Message |
|---|---|
| .ro-linear.json missing | "No Linear board config. Run /ro:linear-run setup first (it writes .ro-linear.json)." |
| LINEAR_API_KEY unset | "LINEAR_API_KEY not in ~/.claude/.env — needed for issue-history GraphQL. Add it and re-run." |
| No tickets touched in window | "No board activity in the last <since>. Widen with --since 60d." |
| ~/.claude/linear-listen-activity.log absent | Proceed without Source B; note in the report that comment-storm detection was skipped (listener not running / never used). |
| Repo mode work | Refuse: "This board flow is personal-repo only." |
/ro:linear-run's job, not this one.from-audit GH issues and [Audit] Linear issues by title./ro:linear-run — the forward-motion front door; § "Load full ticket context" and § "Detecting rework" are the contracts this audit checks against./ro:linear-listen — the webhook receiver; its coalesce/supersede logic is the fix for context-not-loaded bounces, and its activity log is Source B./ro:linear-grill — where unasked-question fixes land./ro:linear-update — where no-AC-for-verification fixes land (the hand-off template)./ro:night-shift-retro — the sibling retro for GitHub swarm runs; same SYSTEM/PROJECT routing./ro:linear-run) and the coalesce/supersede fix (/ro:linear-listen) — this skill measures whether those fixes actually drop the bounce rate. Weekly --scheduled mode files Linear review issues tagged to Ronan; US-9 captures the bot-service-account idea Ronan raised in the same conversation.testing
--- name: linear-pipeline description: The Fable orchestrator for a single dispatched Linear ticket. Holds almost no context itself; it receives `--issue <ID> --detached`, decides the stage sequence, and fans out a sub-agent per stage, passing forward only each stage's artifact (never re-derived, never inlined into its own context). Step zero, before any planning or stage routing, is a boundary triage against `canon/security-boundary.md` (#199): a match tags Ronan Connolly and stops the run, no
development
--- name: in-your-face description: Capture a chat-only answer into a durable artifact (markdown + HTML, PDF when cheap) and launch it automatically so the user cannot miss it. Use when user says "in your face", "don't let me lose this", "save that answer", "make that durable", or right after answering a substantive side question (a recipe, comparison, how-to, or generated prompt) that would otherwise die with the context. category: workflow argument-hint: [--no-open] [--vault <short>] [hint of
tools
One-shot headless OpenAI Codex CLI calls for background/admin AI tasks — summaries, classification, extraction, admin glue. The default engine for anything that runs AI constantly in the background (daemon-driven, per-event), because it bills the flat ChatGPT subscription instead of Claude usage or per-token API spend, and it keeps working while Claude is rate-limited. NEVER for coding — coding stays Claude. Use when a skill or daemon needs a cheap always-on AI call, when the user says "use codex", "ask codex", "codex as backup", or when building a background summarizer/classifier into a listener or loop. Reads auth from ~/.codex/auth.json (ChatGPT account, no API key).
research
Turn a warranty rejection, repair quote, or RMA email into a cited decision brief — legal read (NL/EU consumer law), is the part user-serviceable, live part and new-unit prices, repair-vs-DIY-vs-new economics, before-you-send-it checklist, deadlines. Use when the user pastes or screenshots a repair quote, warranty rejection, "not covered" email, onderzoekskosten fee, or asks "should I repair or replace this".