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 codecatalystCLI. - GCP has no native work tracker — GCP-hosted teams pick one of the
above (or
other); there is nogcpvalue 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
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.bin/worklog sync— the dispatcher handles scope, hash-skip, create-vs-update, echo suppression, conflict detection, andworklog ingestof pulled changes.- 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-onlysync 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 ingestwith 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'scapabilities.markershould 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 withworklog 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 isworklog sync --dry-runreporting 0 creates. Nothing irreversible happens before the first real push.