@v4/tickets

The project's Jira tickets as typed code, their gates, and the one-way tooling that pushes and maintains them on the ELBWEB board.

2026-11-07

Overview

@v4/tickets is the project's tickets authored as typed TypeScript — one defineTicket file per ticket — with the gates that check them and the scripts that push them ONE-WAY to Jira and keep the board's design, criteria and links in step.

This repo runs in Client mode. The package and its rules came from the template, which also has a Platform mode for its own product board; the tooling decides which mode it is in from one field, templateCommit in .claude/manifest.json, which this repo carries (src/repo-mode.ts). In Client mode:

  • the ELBWEB Jira board is authoritative the moment a ticket lands on it — nothing syncs back;
  • the content model is read from the Contentful space, not from @v4/model;
  • design is the client's own Figma files.

The rules are not restated here. Read them before changing any ticket or any ticket tool:

File What it settles
.claude/rules/tickets/platform-and-client.md The two modes, the setup table, the order a board comes up in, the status lock, refined-for-client / clientSpec
.claude/rules/tickets/tickets-as-code.md The spine, the hard rules, every push-jira mode and what each may write on a live board
.claude/rules/tickets/acceptance-criteria.md How a criterion and a CMS row are worded (QA- and editor-facing, no developer vocabulary), owner vs reference tickets
.claude/reference/tickets/tickets-plan.md The full contract

The skills drive the tooling: /ticket-setup first (it ends with tickets-doctor), then /ticket-new, /ticket-review, /ticket-push, /ticket-design, /ticket-board, /ticket-status, /ticket-update, /ticket-update-epic and /ticket-work. /ticket-resolve is in .claude/skills but is Platform-only and does not apply here. Each skill is listed in the skills catalogue.

The workflow on ELBWEB

  1. Set up and check — /ticket-setup writes the board (jira.defaultProject) and the ELB Figma files, then tickets-doctor checks every setting is set and working and reports the board's state.
  2. Refresh what tickets are checked against — build-cms-model --from=contentful (the live space and environment, never defaulted), then match-figma <file-key> --write once per file in figma.files, which repoints each ticket's design link at ELB's own frame.
  3. Bring tickets onto the board — an empty or partial board is pushed one epic at a time: push-jira --epic=<slug> --create (a dry run listing only the tickets not yet on the board), then the same with --apply --verify. A board someone cloned by hand is taken over with adopt-board --apply instead, joined by the v4-slug-<slug> labels; push-jira refuses to create on a board it did not populate. Then register-epics --apply, and commit tickets.config.json and tickets.pushed.json.
  4. Keep the design in step — push-jira --design-only rebuilds only the DESIGN panel, and sync-screenshots --apply attaches a screenshot only where ELB's Figma marks the design Complete, keeping design.status current in the file and on the board. --links-only turns tool-written issue links into smart links.
  5. Refine a ticket into ELB's specification — /ticket-update (one ticket) or /ticket-update-epic (one epic) proposes criteria, CMS, TECH and Claude panels from Jira, the code, ELB's Figma and the space; on approval write-panels --key=<KEY> --proposal=<file> writes them and labels the issue refined-for-client. --criteria-only is the bulk criteria flow and prints every line it would remove and add before --apply. mark-refined adds the label on its own after a plain resync.
  6. Hand over — ready-for-dev --key=<KEY> --apply moves a finished ticket to READY FOR DEVELOPMENT. From then on it is locked: no tool writes to a ticket outside Backlog.
  7. Build — /ticket-work reads the ticket from Jira (it stops rather than fall back to the .ticket.ts), works on a branch named for the key and opens the pull request.

When /sync-upstream brings in a new feature from the template, the same steps 2–3 put its tickets on the board: push-jira --epic=<slug> --create creates only what is not already there and not recorded as deleted.

What is in the package

Path Contents
src/<domain>/*.ticket.ts One ticket per file: the slug is the filename, the domain is the directory. Today basketball 89, core 72, content 60, tooling 14, video 8
src/schema.ts The ticket schema, the area prefixes and labels, COMPONENT_LESS_TYPES (link, linkList, imageLink, image)
src/define.ts defineTicket, which tightens the domain, epic and skill names to real values at compile time
src/__generated__/ vocab.ts (from domains.json and .claude/manifest.json), cms-types.json (from build-cms-model), figma-frames.json (from sync-figma-frames). Never hand-edited
domains.json Ticket-domain ownership, mirroring packages/model/domains.json; platformOnly.scripts (not in this repo) and cloneOwned.paths (this project's, never offered by /sync-upstream)
tickets.config.json The board, the Figma files, the epic registry, statuses and the deferred / absorbed / removed maps — committed
tickets.pushed.json The record of what is on the board — committed
scripts/*.mjs The command-line tools, run with tsx

The package's only runtime dependency is zod, and its one export (., src/index.ts) is: defineTicket, TicketError, DefineTicketInput; ticketSchema, TICKET_TYPES, TICKET_PRIORITIES, TICKET_EPICS, TICKET_EPIC_NAMES, LEGACY_TICKET_EPICS, TICKET_AREAS, AREA_PREFIX, AREA_LABEL, displayTitle, ticketLabels and the types Ticket, TicketInput, TicketTarget, TicketEpic, TicketArea; findVoiceViolations, BANNED_AC_VOCABULARY, VoiceViolation; and TICKET_DOMAINS, SKILL_NAMES with their types.

This project adds one area of its own: basketball, titled Basketball - and labelled V4_Basketball. The basketball domain is this project's own — its epics were created by hand on ELBWEB and are joined to the files by the v4-slug-<slug> label, the only join key that survives between boards.

The rest of src/ is the machinery behind the rules: the Jira document renderer and differ (adf.ts, adf-diff.ts), the single-panel splices that make targeted writes safe (design-splice.ts, cms-table-splice.ts, links-splice.ts), the edited-in-Jira guard (fingerprint.ts), the status lock (lock.ts), the client markers (refined.ts), the declared board scope (board-scope.ts), the "an update never creates" policy (create-policy.ts), --only validation (only.ts), and attachment handling that only ever deletes a <slug>.png the tools wrote (attachments.ts).

Configuration

Committed — tickets.config.json:

  • jira.site https://stadion.atlassian.net, jira.defaultProject ELBWEB. The board key is config, not environment.
  • figma.files — ELB's Figma files, one per domain: core HIjezWNeRQSBGkxx8VqmjA, content 9iz5GeR9syO68cmPr1ppE1, video gR9NZ422d1RuQbdgtQ8VA9, basketball 8Jl1miPEIZslMda8jZ8w0C. push-jira refuses a design link to any file not listed here.
  • jira.projects.ELBWEB — epics (27, bound by key), statuses (readyForDev READY FOR DEVELOPMENT; editable ["Backlog"]), deferred (video: no ELB design yet), absorbed (19 slugs folded into others), removed (12 deleted outright, each with a reason).

tickets.pushed.json records each pushed slug's key, description fingerprint, epic and screenshot media (245 tickets today), plus the epics and the issues deleted from the board. Commit both files after every push or adopt — the record is what makes the next run create-only.

outstanding.json (the Platform board's orange outstanding panel) does not exist here, by design: ELB's bugs live on ELBWEB.

Credentials — the root .env, read by the one reader src/env.ts (a process variable wins; in the file the LAST non-empty value wins; inline # comments are stripped):

Variable Used for
JIRA_EMAIL Jira; falls back to git config user.email
JIRA_API_TOKEN Jira
FIGMA_PAT match-figma, sync-screenshots, sync-figma-frames, push-jira, tickets-doctor
CONTENTFUL_SPACE_ID, CONTENTFUL_MODEL_ENV build-cms-model --from=contentful — never defaulted, the environment included
CONTENTFUL_CMA_TOKEN build-cms-model
JIRA_PROJECT_KEY Overrides the board for ONE run. Never export it
JIRA_BASE_URL Overrides the site

Scripts

All run as pnpm --filter @v4/tickets <script>. Write scripts are dry runs unless given --apply.

Script What it does
lint-tickets Schema, ownership, targets, blocker and composes references, duplicate slugs, the story's actor, developer-only rules, no board key in board text
lint-ac-voice Fails criteria that use developer vocabulary
lint-cms-fields Checks every CMS section against the built content model: real fields, exact editor display names, owner completeness
verify-ticket Plays a ticket back against the code, the content model and Figma (--only=a,b). Read the playback, not the exit code
build-cms-model Builds cms-types.json; --from=model or --from=contentful, defaulting by mode (Contentful here)
sync-figma-frames Caches the files' feature frames in figma-frames.json (--check in drift)
gen-vocab Regenerates vocab.ts
drift The local pre-push check: gen-vocab + a clean src/__generated__, then every lint, check-script-refs, and the two --checks. Not run by CI
render-board An HTML preview of the board for sign-off
push-jira The one-way push. Modes: --create (the only mode that creates, here), --resync, --design-only, --criteria-only, --links-only; scoped by --epic / --only; --verify, --force, --bind-found
match-figma Repoints every ticket's Figma link at a client file by its figmaFrame (<file-key> --write, once per file)
sync-screenshots Attaches a screenshot only where the client's Figma marks the design Complete, and keeps design.status in step
adopt-board Takes over a board copied by hand in Jira, by its v4-slug-<slug> labels
write-panels Writes approved panels to one ticket (--key, --proposal=<file>) and labels it refined-for-client
mark-refined Adds the refined-for-client label, on the board and in the file
register-epics Copies recorded epic keys into the config registry
ready-for-dev Moves ONE finished ticket to the readyForDev status
list-epic Lists one epic's tickets as Jira has them
tickets-doctor Read-only: every setting set and working, and the board's state
check-script-refs Every pnpm --filter @v4/tickets <script> named under .claude/ must exist
test, lint, check-types Vitest, ESLint, tsc

scripts/build-storybook-map.mjs has no package.json entry: it reads the deployed Storybook's index into scripts/storybook-story-map.json. The template's Platform-mode generators and board tools are not in this repo.

The rules that bite most often

One line each; the rule files above have the detail.

  • A ticket outside Backlog is locked. Every write path reads the live status and writes only in editable. There is no override, --force included.
  • A resync skips a ticket edited in Jira (the fingerprint guard), and here also skips one with no fingerprint (an adopted ticket). It also skips anything labelled refined-for-client, unless named with --only and --force.
  • Bulk writes on a live board are limited to --design-only and --links-only; --criteria-only prints the lines it would remove and add before --apply.
  • Only push-jira --create makes tickets or epics. Deferred, absorbed and removed tickets are declared in the config, not remembered.
  • No Jira key in any text that reaches the board. Name the other ticket in words and link it through composes.
  • Title and labels are derived (displayTitle, ticketLabels); never write them by hand.

Separate from this package: .github/workflows/pr-ticket.yml requires a pull request to name its Jira ticket, and .github/workflows/deploy-watch.yml listens to Flightcontrol's deployment statuses and, on a staging deploy, moves the merged pull requests' tickets on (staging-tickets).