plugins/dev/skills/gherkin-ticket/SKILL.md
Shape every ticket around a scannable use-case before it's filed. **ALWAYS use when** the user says 'file a ticket', 'create a ticket', 'file tickets for', 'open an issue', 'add a ticket', 'log a bug', or whenever drafting, titling, or rewriting a ticket. Turns vague, implementation-first tickets into an outcome title (`<actor> should <outcome> so that <benefit>`) plus tiered Gherkin (Given/When/Then) acceptance criteria — even for backend bugs and chores.
npx skillsauth add coalesce-labs/catalyst gherkin-ticketInstall 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.
Every ticket must open with a use case a stranger can understand: who gets what outcome, under what condition, and why. Most tickets fail this — they dive straight into implementation ("Wire HRW ownership into dispatchTriage") and the reader has to reverse-engineer the point. This skill fixes that at authoring time.
This skill owns ticket format (title voice + body structure). It does not own the Linear CLI
mechanics — once a draft is ready, hand off to the /catalyst-dev:linear skill to actually create
or update the issue. CLI syntax lives in /catalyst-dev:linearis.
Auto-invoked whenever a ticket is being born or rewritten — you do not need to say "gherkin":
/catalyst-pm:create-tickets)If the user is mid-creation in another skill (linear, create-tickets, phase-triage), apply
these rules to the title and body before the issue is written.
The title must let a reader who didn't write the ticket understand, in one glance, what it is and what the expected outcome of doing it is — enough to judge whether and when it should be done.
A useful starting shape (not a template to force) is:
<Actor> should <outcome> [when <condition>] [so that <benefit>].
where the actor is whoever gets value — not always a human: Operators, Developers,
The scheduler, The reaper, A phase worker, The daemon, The dashboard.
But bias hard toward concise and scannable. Do not force the formula.
so that… the
moment the benefit is obvious; drop when… unless the trigger is the point. A 90-char title that
reads cleanly beats a 150-char one that recites every clause. Vary the phrasing — identical
"X should Y so that Z" scaffolding on every ticket becomes noise the reader tunes out.reap-complete, bootReplay, schedulerTick, layoutId, --label-mode). The title is for
someone deciding whether to care, not implementing. If you can't state the outcome without
naming an internal mechanism, you don't yet understand the outcome — go figure out what the work
is for, then write that. The mechanism belongs in the body.[API] / [Frontend] prefixes.The hardest case is deep-internals work (a reaper fix, a scheduler reorder). The temptation is to title it by its mechanism because the outcome feels invisible. It isn't — every change exists to make something better for someone (the operator, the daemon, the next developer). Name that. "The reaper should record an already-gone session as reaped so the daemon stops re-checking it every boot" beats "Reap echo on already-gone bg session" even though both describe the same patch.
| ❌ Implementation-first (cryptic) | ✅ Outcome-first (scannable) |
|---|---|
| Wire HRW ownership + claim into monitor dispatchTriage | The orchestrator should claim a ticket before triaging it so that two workers never grab the same job |
| Fix CTL-874 preflight label scope | catalyst-monitor preflight should pass in workspaces that have no team-level labels |
| Add stale-worker detection to dispatcher | The dispatcher should skip workers that have gone silent when assigning new work |
| Dashboard needs-attention banner | Humans should see an indication on the dashboard when something needs their attention so that they can react to it |
| Refactor: extract dispatchAndVerify | Developers should change dispatch-and-verify logic in one place so that the three sweeps can't silently diverge |
Notice the actor varies (orchestrator, monitor, dispatcher, humans, developers) and the so that
is present only when it adds information.
Match the ceremony to the work. Forcing a full Given/When/Then block onto a pure chore produces
vacuous Gherkin (Then the code is cleaner) — a known anti-pattern. Use the tier that fits:
Always wrap scenarios in a ```gherkin fenced code block. This renders as a monospace block in
Linear (and picks up Gherkin syntax coloring wherever the viewer supports it), keeping the
Given/When/Then visually distinct from the prose around it. The Tier-C Context/Motivation/Outcome
prose stays as normal text; only the scenario blocks are fenced.
A one-line intent (the title already carries it; restate only if the body needs framing) plus one
or more Scenario: blocks:
Scenario: <one specific behavior, stated as a complete sentence>
Given <minimum starting state>
And <additional precondition>
When <the single action or event>
Then <observable outcome>
And <additional observable outcome>
Rules (borrowed from gherkin-lint heuristics — these are hard rules):
When per scenario. A second When means a second behavior → second scenario.Then asserts an observable outcome (user-visible or caller-visible), not an internal
artifact — unless it's an explicitly technical/backend scenario where the state change IS the
observable outcome (then say so).POST /api/v2/users, no field
IDs. Mechanics belong in the implementation, not the acceptance criterion.Given = minimum context to trigger the behavior, not a full user journey.Same as Tier A, with a discipline: Then states the CORRECT behavior (the scenario must go green
when fixed), and a # CURRENTLY: comment marks what's broken today.
Scenario: Preflight passes when the workspace has no team-level labels
Given the workspace defines labels only at the workspace scope
And no labels are defined at the team scope
When catalyst-monitor preflight runs
Then the preflight check exits 0
And no "label not found" error is emitted
# CURRENTLY: preflight queries --team labels, returns "label not found", and the team starves
A full scenario would be hollow. Lead with Context / Motivation / Outcome instead:
Context: dispatchAndVerify() is duplicated verbatim in sweep-triage, sweep-implement, sweep-verify.
Motivation: when one copy's timeout changes, the other two silently stay wrong (the CTL-826 class).
Outcome: a single shared dispatchAndVerify() is extracted; all three sweeps import it.
Add invariant scenarios only if there is behavior that must survive the change and can be
written as a testable postcondition (e.g. "the dispatcher still assigns to the least-loaded worker").
Do not invent a scenario whose Then is "the code is cleaner".
Title: Humans should be able to see which workers need assistance so that they can respond in time
Scenario: A blocked worker surfaces in the assistance list
Given a phase worker has been waiting on a needs-human decision for 5 minutes
When an operator opens the dashboard
Then that worker appears in the "needs assistance" list
And the list shows the ticket, the phase, and how long it has been waiting
Scenario: A worker drops off the list once unblocked
Given a worker is shown in the "needs assistance" list
When the operator resolves its needs-human decision
Then the worker disappears from the list without a page reload
Title: Integrating systems should have failed webhook deliveries retried so that a transient downstream error doesn't lose data
Scenario: A transient 503 triggers a backoff retry
Given a webhook event is queued for delivery
And the endpoint returns HTTP 503 on the first attempt
When the retry scheduler evaluates the event
Then a second attempt is scheduled with a 30-second backoff
And the event is NOT yet marked as delivered
Scenario: An event is abandoned after five consecutive failures
Given a webhook event has failed delivery four consecutive times
When the fifth attempt also fails
Then the event is marked "abandoned"
And it is moved to the dead-letter queue
Title: The daemon should not declare a live phase worker dead on its first commit
Scenario: A committing worker is left alone
Given a --bg phase-implement worker has just made its first commit
And its signal mtime is older than the stale-bg threshold
When the reclaim sweep evaluates the worker
Then the worker is left running
And no implement-complete event is emitted on its behalf
# CURRENTLY: reclaim fires implement-complete on the first commit, so verify runs on a partial branch
Title: Developers should change dispatch-and-verify logic in one place so that the three sweeps can't silently diverge
Context: dispatchAndVerify() is copy-pasted across three scheduler sweeps.
Motivation: a timeout tweak in one copy leaves the other two wrong with no error.
Outcome: extract a shared dispatchAndVerify(); all three sweeps import it; behavior unchanged.
Scenario: Dispatch still waits for completion before returning # invariant
Given a task is eligible for dispatch
When dispatchAndVerify is called
Then the task is dispatched to a worker
And the call does not return until a completion signal arrives or the timeout elapses
/catalyst-dev:linear to create the issue. Add component label,
estimate, priority there (see feedback_linear_ticket_hygiene).## Technical notes section below the
Gherkin so it stays but doesn't lead.Score a ticket against the checklist and report gaps. Used to triage a backlog before a rewrite pass.
Title:
so that present only when it adds information (not hollow filler).[Component] prefix — component is a label.Body:
# CURRENTLY, C chore prose).When.Then is observable (or explicitly technical); no leaked DB/queue internals on user features.Then describes the CORRECT behavior; # CURRENTLY: documents the break.Then the code is cleaner).Dependencies:
If you know that other work must finish before this ticket can start, record it as a first-class
Linear blocked_by link at authoring time — you know the prerequisites better than any later
pass will. Catalyst does NOT infer dependencies from prose (CTL-838): writing "depends on
CTL-123" or "see CTL-456" in the description does nothing — it is not scraped into a blocker, and it
should not be (a mention is not a dependency).
# After the ticket exists, link each genuine prerequisite (see /catalyst-dev:linearis for syntax):
linearis issues update <NEW-TICKET> --blocked-by <PREREQ-TICKET>
Rules of thumb:
CTL ticket on an OTL/ADV ticket): the
execution-core daemon only works its own team, so a cross-team blocker just deadlocks. Coordinate
cross-team work out-of-band./catalyst-dev:phase-triage does a semantic second pass over the
backlog and can add genuine ones it finds. But a false blocker you add stalls real work, so
when in doubt, leave it out.Catalyst ships these defaults, but other teams write tickets differently. Before drafting, check for a project override and layer it on top of (it wins over) the defaults here:
# Optional project-specific ticket style — actor vocabulary, extra sections, stricter tiers
OVERRIDE=".catalyst/ticket-style.md"
[[ -f "$OVERRIDE" ]] && echo "Applying project ticket-style overrides from $OVERRIDE"
If .catalyst/ticket-style.md exists, read it and honor its actor list, required sections, and any
stricter rules (e.g. "every ticket including chores must carry one literal Given/When/Then"). Absent
that file, use the defaults above. This is how the same shipped skill serves teams with different
conventions.
/catalyst-dev:linear — does the actual Linear create/update. This skill produces the title +
body it consumes./catalyst-pm:create-tickets — when exploding a PRD into many tickets, apply these title/body
rules to each. (Its legacy [Component] Action: title format is superseded by outcome titles +
component labels.)/catalyst-dev:phase-triage — triage reads tickets; a ticket authored to this standard makes
triage's classify/estimate step far more reliable./catalyst-dev:linearis — CLI syntax reference. Never hardcode linearis commands here.testing
Phase-agent that fixes a failing verify verdict so the pipeline self-heals instead of stalling to needs-human (CTL-653). Reads `${ORCH_DIR}/workers/<ticket>/verify.json`, fixes the `findings[]` (every severity:"high" plus the regression_risk drivers) directly via Edit/Write, commits the remediation, and emits `phase.remediate.complete.<ticket>`. The scheduler's router then re-dispatches `verify` to re-check (the verify⇄remediate cycle, cap 3). Dispatched as a `claude --bg` job by `phase-agent-dispatch`, which invokes it via slash command — hence `user-invocable: true`.
tools
--- name: phase-triage description: Phase agent that triages a Linear ticket — expands acronyms, classifies (feature/bug/docs/refactor/chore), identifies genuine blockers (a semantic second-pass over the backlog — NOT a prose scrape; CTL-838), estimates scope, writes triage.json, and posts a triage analysis comment to Linear. Triage completion is signaled by that comment plus the local triage.json — there is no `triaged` label. Emits phase.triage.complete.<TICKET> on success and phase.triage.fai
tools
Phase agent for the research step of the 9-phase orchestrator pipeline (CTL-450). Wraps /catalyst-dev:research-codebase and produces thoughts/shared/research/<date>-<ticket>.md, then emits phase.research.complete.<ticket>. Reads triage.json from the worker dir as its prior-phase artifact. Spawned via plugins/dev/scripts/phase-agent-dispatch, which invokes it via slash command — hence `user-invocable: true`.
development
Phase-agent wrapper that opens the pull request after implementation completes (CTL-449 Initiative 1 Phase 3). Delegates to `/catalyst-dev:create-pr` (which already auto-runs `describe-pr` and transitions Linear to `inReview`), then writes the PR number + URL into the phase signal file so the downstream `phase-monitor-merge` agent can read it without re-querying GitHub. Dispatched as a `claude --bg` job by `phase-agent-dispatch`, which invokes it via slash command — hence `user-invocable: true`.