--- name: ticket-sync metadata: version: "0.24.9" description: 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. --- # 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 `` 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 `). 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 ` (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//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 --field --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 `` vanishes. Store the marker as a work-item tag (`worklog:` — 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 --system ado --key ` 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.