# Ticket Sync

> Push work items to the team's configured ticketing system (GitHub Issues, Jira, Azure DevOps, GitLab, Linear...). Use when asked to sync tickets, after closing out a plan, or when items should be visible in the tracker. Push and pull.

- Skill: `spillwavesolutions/ticket-sync` (Agent Skill)
- Install (CLI): `npx skillmds@latest add spillwavesolutions/ticket-sync`
- Raw SKILL.md: https://api.skillmd.com/api/skills/spillwavesolutions/ticket-sync/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: spillwavesolutions (https://skillmd.com/u/spillwavesolutions)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/spillwavesolutions/ticket-sync

---


# Ticket sync

Sync runs through a typed adapter contract: the dispatcher
(`bin/sync_dispatch.py`) owns every invariant; a per-system adapter is a
dumb translator. This skill's job is to run it and read the report — not
to re-implement the rules. Contract:
`docs/plans/2026-07-18-typed-adapter-contract.md`.

## 1. Read the config

Read the `ticketing:` block in `.work/config.yml`. `system` names the
tracker: `github`, `gitlab`, `jira`, `ado`, `linear`, `codecatalyst`,
`other`, or `none`. If it is `none` or absent, say so and stop — there is
nothing to push to.

The enum is advisory, not a capability list. `bin/worklog` only ever
branches on `none`; every other value is a name this skill resolves to
whatever tooling exists (CLI, MCP server, REST). Named values carry known
caveats; `other` is any tracker not listed — set it, note the real system
in `options:`, and the skill researches the tooling at runtime.

- **linear** — no git remote to detect; reach it via the Linear MCP server
  or CLI. Markers go in the issue description (markdown survives).
- **codecatalyst** — AWS's ADO equivalent (issues per project). Closed to
  NEW customers since 2025-11-07; existing spaces keep working. Reach via
  `aws codecatalyst` CLI.
- **GCP** has no native work tracker — GCP-hosted teams pick one of the
  above (or `other`); there is no `gcp` value to configure.

## 2. Scope

Open items plus externally-linked items whose canonical hash differs from
the last pushed one; closed and in-sync items are inert (spec §10.5 — the
dispatcher computes this, see the contract doc).

## 3. Flow

1. `bin/worklog adapter check` — activates and validates the adapter for
   the configured system against the contract. Missing adapter → the
   dispatcher runs local-only; that is a mode, not an error.
2. `bin/worklog sync` — the dispatcher handles scope, hash-skip,
   create-vs-update, echo suppression, conflict detection, and
   `worklog ingest` of pulled changes.
3. Read the drift report and surface anything that needs a human
   (conflicts, unsupported fields, degraded type mappings, auth failures,
   unmarked remotes, closed-on-remote items).

## 3b. Do not file or edit tracker tickets by hand

The log is the source of truth. On a worklog-managed tracker:

- **Never** `gh issue create`, `gh issue edit`, or the equivalent on
  GitLab/Jira/ADO. A ticket filed that way has no ULID marker, so fold
  and the roadmap cannot see it, and the next `--push-only` sync will
  not absorb it.
- **Never** edit a ticket body on the tracker to "narrow the issue".
  Push-only overwrites the GitHub body from the log. The durable edit is
  `worklog update --body`, then sync.
- Child worktrees that were told "do not use WikiTicket, that is the
  parent checkout" must **not** file tracker issues instead. Either the
  parent runs `worklog add` (and syncs), or the child files nothing.

When a ticket already exists remotely with no marker:

    bin/worklog adopt --system github --key N

That creates the log item, links it, and stamps `<!-- worklog:{ulid} -->`
so the next push updates. `--dry-run` first.

When sync reports `closed on remote, still open in the log`, it has
already closed the log item (or, on `--dry-run`, named `worklog close
<id>`). Do not push the open state back — that rewrites a closed ticket.

## 4. Invariants — dispatcher-enforced, context only

Each of these is code in `bin/sync_dispatch.py` now, not procedure for you
to follow — see `docs/plans/2026-07-18-typed-adapter-contract.md`:

- **Canonical hash / skip-unchanged** (spec §10.3): an item is pushed only
  when its canonical hash differs from `last_pushed_hash` — the dispatcher
  enforces this.
- **Idempotency** (spec §10.5): the item ULID travels as a marker in the
  ticket body and a retried push finds rather than duplicates — the
  dispatcher enforces this.
- **Pull mechanics** (spec §10.1–10.3, §10.6): remote edits enter the log
  via `worklog ingest` with deterministic event IDs, echo-suppressed and
  conflict-checked — the dispatcher enforces this.

## 4b. Rich ticket bodies

When creating or updating a ticket, compose its body with
`bin/worklog ticket-body <ulid>` (issue-description skill): summary,
epic/plan context with the frozen plan's wiki page, milestone, and
traceability edges, rendered from the log + graph. Never hand-edit richness
into the remote ticket only — enrich the source (`worklog update --body`,
`link-pr`, sidecar `relates_to`) and let sync carry it out.

## 5. Closing

When a scoped item's status is `done` or `cancelled`, the sync closes the
remote ticket (adapter `close` verb) with a short comment naming the
resolution. After that push the item is inert: hashes match, so it is
never rescanned.

## 6. Per-system tooling

Lives in the adapters, not here. Authoring rules and shipped
implementations: `adapters/README.md`, `adapters/<system>/adapter`.

## 7. Report

Finish with counts: pushed / updated / closed / skipped, plus conflicts
and anything deferred or needing human action.

Conflicts surface in `worklog list` (stderr), `worklog show`, and the
status report's Needs-attention section. Resolve with:

    bin/worklog resolve <item> --field <f> --take local|remote

## Azure DevOps: field-tested caveats (no adapter ships yet — hints for building/driving one)

- **Marker must be a TAG, not an HTML comment.** ADO silently strips HTML
  comments from a work item's Description, so `<!-- worklog:<ulid> -->`
  vanishes. Store the marker as a work-item tag (`worklog:<ulid>` — ADO
  preserves colons and case exactly) and search by tag. An ADO adapter's
  `capabilities.marker` should be `{"style": "tag", "template": "worklog:{ulid}"}`.
- **Updates merge, never overwrite.** When updating an existing work item:
  merge tags, and write title/state only when they actually changed. Existing
  Description/content in Azure Boards is never replaced — teams adopt worklog
  onto boards full of real content.
- **Migrating existing tickets (any system):** create-vs-update is decided
  purely by whether the local item carries `external`. Pre-seed it with
  `worklog link <ulid> --system ado --key <AB#id>` for every imported item so
  the first sync treats all of them as updates — never duplicates. Pilot one
  epic first; the acceptance gate is `worklog sync --dry-run` reporting
  **0 creates**. Nothing irreversible happens before the first real push.

