sync-work-items
Invocation: User- and model-invocable
Argument hint: [--push-only|--pull-only] [--preview] [--all] [filter-flags…]
Allowed tools: Bash(${CLAUDE_PLUGIN_ROOT}/scripts/config-*), Bash(${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/*)
Source: skills/work/sync-work-items/SKILL.md
Sync Work Items
Section titled “Sync Work Items”Active integration: !${CLAUDE_PLUGIN_ROOT}/scripts/config-read-work.sh integration
Default project code: !${CLAUDE_PLUGIN_ROOT}/scripts/config-read-work.sh default_project_code
Work items directory: !${CLAUDE_PLUGIN_ROOT}/scripts/config-read-path.sh work
/sync-work-items reconciles the local work items under the work directory with
the remote tracker named by work.integration. It is on-demand (never
background), operates against exactly one integration per invocation, and
writes can affect remote state — which a local VCS revert cannot recover —
so a --preview mode is provided to inspect the plan before any side effect.
The safety-critical orchestration lives in tested scripts, not this prose:
work-item-sync-decide.sh owns the (mode × state) decision table,
work-item-sync-classify.sh owns change detection, and work-item-sync-apply.sh
owns the per-item commit sequence (side-effect first, baseline last). This skill
parses arguments, renders decisions, and runs the prompts/gates around them.
Step 0: Config gate and prerequisites
Section titled “Step 0: Config gate and prerequisites”Config gate. The Active integration read above gates the whole skill.
config-read-work.sh integration exits 0 with a blank line when nothing is
configured, so branch on the string. If it is empty, print a clear,
actionable error and stop — do not guess a tracker:
/sync-work-items needs an active remote tracker, but `work.integration` is notconfigured.
What: the `work.integration` setting selects which remote your work items sync with. It is currently unset. Why: sync reads and writes that tracker's API; with no tracker there is nothing to reconcile against. Fix: set `work.integration` to one of `jira`, `linear`, `trello`, or `github-issues` via /accelerator:configure, then re-run.<sys> for every script below is this configured value — never re-derived.
trello and github-issues are not built yet; the bridges report
“not available” (exit 72) for them, which you surface as a clear message.
Prerequisites. Before any remote call, confirm jq (with -S support),
sha256sum or shasum, and the repo’s VCS binary (git or jj) are present.
If one is missing, name it and how to obtain it (same what/why/how shape as the
config gate) rather than failing later with a raw command not found or a
silently divergent hash.
Step 1: Parse mode and filters
Section titled “Step 1: Parse mode and filters”Resolve the directional mode through the decision script so the mutually-exclusive rule is enforced in one tested place:
${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/work-item-sync-decide.sh mode \ [--push-only] [--pull-only]It prints bidirectional (the default when neither flag is given), push-only,
or pull-only, and errors (exit 2) if both directional flags are supplied —
surface that error and stop. Other arguments:
--preview— report the full set of intended changes (push, pull, conflict, push-unsynced, untracked-pull) without any local write or remote API write, and without touchinglast-sync.json. Combinable with any directional flag.--all— for the untracked-remote pull (Step 4 / Phase 8), drop only the implicitwork.default_project_codescope; user filters still apply.- remaining flags are tracker
search-*filter flags, forwarded verbatim.
Omitting the directional flags means bidirectional. Example:
/sync-work-items --push-only --preview previews only the local→remote pushes.
Capture the run-start epoch now (before reading any item) — it becomes the
baseline’s global timestamp on clean completion, so a file edited during the
run is re-hashed (not wrongly short-circuited) on the next run.
Step 2: Build the remote pre-filter map
Section titled “Step 2: Build the remote pre-filter map”Collect the non-empty external_ids of the local work items (synced items) and
fetch their remote state in one bulk call:
${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/work-item-fetch-remote.sh \ --integration <sys> search --keys <comma-separated external_ids>It returns { "found": {<key>:{updated}}, "absent": [...], "indeterminate": [...] }
— the adapter picked the per-tracker strategy, so you never branch on tracker. If
the bridge exits non-zero (remote unreachable / timed out), treat every key
as indeterminate: nothing is written, and each affected item is reported as
needs-retry. Resolve the baseline path and global timestamp once:
BASE=$(${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/work-item-sync-baseline.sh path)TS=$(jq -r '.timestamp // 0' "$BASE" 2>/dev/null || echo 0)Step 3: Reconcile synced items
Section titled “Step 3: Reconcile synced items”For each local item with a non-empty external_id, emitting
processing item k of N progress as you go:
-
Classify with the shared engine. Derive
--remote-statusfrom the pre-filter map (found→presentwith--remote-updated;absent→absent;indeterminateor degraded→indeterminate). For afoundkey whoseupdateddiffers from the baseline entry’sremote_updated_at, fetch the body (work-item-fetch-remote.sh … show --external-id <key>), project + canonicalise it withwork-item-project-remote.sh --integration <sys> body, write it to a temp file, and pass--remote-body-file:STATE=$(${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/work-item-sync-classify.sh \--file <path> --external-id <key> \--baseline "$(work-item-sync-baseline.sh get <id>)" --timestamp "$TS" \--remote-status <present|absent|indeterminate> \[--remote-updated <iso>] [--remote-body-file <tmp>]) -
For a
remotely-modifieditem, test the local file’s cleanliness before any overwrite (the recovery model is VCS revert, which cannot recover uncommitted working-copy changes):if work-item-file-dirty.sh <path>; then DIRTY=1; else DIRTY=0; fi -
Decide the action deterministically:
ACTION=$(${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/work-item-sync-decide.sh \decide --mode <mode> --state "$STATE" --dirty "$DIRTY")push→work-item-sync-apply.sh push(the update bridge, not create: the issue already exists). Extract the item’s title and body, write the body to a temp file, and pass--integration \<sys> --external-id \<key> --id \<id> --file \<path> --title \<t> --body-file \<tmp>.pull→ reconstruct the local file (keep the local frontmatter —id,external_id, and the other authored fields — replacing title/body from the projected remote), write it to a temp file, project the remote body, and runwork-item-sync-apply.sh pull --id \<id> --file \<path> --new-content-file \<tmp> --remote-updated \<iso> --remote-body-file \<proj>. Emit<id>: local replaced from remoteso the overwrite is visible and revertable.skip-dirty→ skip the pull, warn and list theid(a dirty local file is never silently overwritten). Reported underneeds-retry.skip-conflict→ report theidunderconflicts-skippedand write neither side. This is the directional-mode outcome (--push-only/--pull-only): resolving a conflict needs a write the mode forbids, so it is reported and skipped with no prompt.prompt→ bidirectional conflict resolution (see “Conflict resolution” below). The dirty-pull route (remotely-modified+ dirty in bidirectional) also returnspromptand is resolved the same way.noop→ nothing to do (synced, or a forbidden-write cell, orindeterminate/remote-absent). Reportindeterminateitems underneeds-retryandremote-absentitems underremote-absent(never push to a non-existent issue).
-
Aggregate pull-overwrite gate. If the number of local files a run will overwrite from remote exceeds the shared threshold (25, the same constant the untracked-pull gate uses), pin and evaluate before any pull write:
Use the
AskUserQuestiontool with two options (stating the count N):- Yes, proceed — overwrite the N local files from remote
- No, abort — abort with zero writes and a non-zero exit
It fails safe: if not running interactively, abort with zero writes.
-
Terminal push handling. A 71/terminal code from the update bridge is never auto-retried (a resent request could apply twice on a response-uncertain failure): report the item as needs-manual-reconciliation and leave its baseline entry unset (the apply helper already does this), so the next run re-classifies authoritatively.
work-item-sync-apply.sh performs each item’s side-effect, then sets that id’s
baseline entry last (per-item resumability). Re-running after a mid-run
interruption is idempotent: reconciled items match their baseline and are skipped.
Conflict resolution (bidirectional only)
Section titled “Conflict resolution (bidirectional only)”When decide returns prompt, resolve the conflict interactively. First render a
section-grouped diff so a large item stays reviewable — local is the -
baseline side, remote is the + side (the recommended/default-accept side):
${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/work-item-section-diff.sh \ <local-file> <remote-reconstructed-file>Then prompt with a typed token (not a y/n keystroke — a reflexive Enter
must never discard local edits, and this avoids colliding with the [y/N]
polarity used by the batch-push and untracked-pull gates). Pin the exact string:
Conflict on <id> (<external_id>). Recommended: keep remote.Type 'remote' to OVERWRITE your local edits with the remote version,'local' to push your local version to the remote, or'skip' to leave both unchanged and resolve it later. [remote/local/skip]No default — Enter (or an unrecognised entry) re-asks once, then skips.Read the raw input and map it through the tested entry point (never re-derive the mapping in prose):
${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/work-item-sync-decide.sh \ resolve-conflict-token "<raw input>"It returns one action:
accept-remote→ resolve as a pull: overwrite the local file from the remote viawork-item-sync-apply.sh pull(Phase 6 ordering, incl. the post-writeremote_hash).push-local→ resolve as a push: push the local version viawork-item-sync-apply.sh push, and emit an override-log line to the summary naming the item, e.g.OVERRIDE <id> (<external_id>): pushed local→remote.skip→ report underconflicts-skipped, write nothing.
There is deliberately no Enter default: ‘Recommended: keep remote’ steers the
choice but still requires typing the word, so a reflexive Enter (empty input) or
any unrecognised token re-asks once, then resolves to skip — never to a
destructive write.
Step 4: Unsynced push offer and untracked pull
Section titled “Step 4: Unsynced push offer and untracked pull”Unsynced push offer
Section titled “Unsynced push offer”For each local item with no external_id (never pushed), offer a push using
one pinned grammar (per-item [y/N] with the fast-path keys surfaced in the
string so they are discoverable):
Push <id> "<title>" to <tracker>? [y/N] (a = push all remaining, d = decline all remaining)-
a/dtouch only un-decided items and never resurrect declines. -
Accepted → push via the create bridge:
${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/work-item-create-remote.sh \--integration <sys> --title <t> --kind <kind> --body-file <tmp>Substitute the returned key into the item’s
external_idline in memory, then write the whole item (frontmatter incl.external_id+ body) in a singleatomic_write, so the file never exists half-linked.work-item-push-decide.shgoverns retry/terminal handling exactly as/create-work-itemdoes (a 71/terminal is never auto-retried; the returned key, if any, is preserved with loud guidance). -
Declined → untouched.
-
Under
--preview: report the intended pushes via the create bridge’s--dry-run; make no write.
Untracked remote pull
Section titled “Untracked remote pull”Fetch remote issues via the read bridge, forwarding the user’s filter flags verbatim:
${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/work-item-fetch-remote.sh \ --integration <sys> search [filter-flags…]- Default scope is
work.default_project_code— for jira this is the search flow’s own default project, so plainsearchis already scoped; for linear the team is catalogue-fixed (single-team), so there is no project scope. --allforwards the tracker’s--all-projectsprimitive (jira only), dropping only the project clause; any user filters (e.g.--label) still apply.- Compute the untracked set: remote issues whose key is not already held
by any local item’s
external_id(a held key is already tracked — never create a duplicate).
Blast-radius gate. When the untracked set exceeds the shared threshold (25 — the same constant the pull-overwrite gate in Step 3 uses), pin, and evaluate before any creation write:
Use the AskUserQuestion tool with two options (stating the count N):
- Yes, proceed — create the N untracked issues locally
- No, abort — abort with zero creations and a non-zero exit
It fails safe: if not running interactively, abort with zero creations. This
stops a mis-scoped --all or an automation-flooded project from flooding the
work directory and exhausting IDs.
Allocate the whole batch up front — never per item in a loop (which would hand every pulled item the same number until each file lands):
${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/work-item-next-number.sh --count NFor each issue, build the full frontmatter (incl. external_id = remote key and
the allocated id) and body in memory, write it in a single atomic_write,
then record its baseline entry (work-item-sync-baseline.sh set \<id> \<remote_updated_at> \<remote_hash> \<local_hash>, with remote_hash from
work-item-project-remote.sh … body | work-item-normalise.sh --stdin over the
issue’s show body, and local_hash from the just-written file). Re-validate
each allocated id is still free immediately before its write and abort the
batch on an unexpected collision rather than overwriting (single-writer
assumption). The pull is idempotent across re-runs: a created item now carries an
external_id, so it is no longer untracked. Under --preview: report the
untracked set, allocate nothing, create nothing.
Step 5: Persist and summarise
Section titled “Step 5: Persist and summarise”On clean completion (and never under --preview), advance the global
baseline timestamp with the run-start epoch from Step 1:
${CLAUDE_PLUGIN_ROOT}/skills/work/scripts/work-item-sync-apply.sh finalise \ --timestamp <run-start-epoch>Under --preview, run the same classification and decisions but route every
push through the update bridge’s real dry-run (--dry-run, which forwards
--print-payload) and report every pull instead of writing it. No baseline
mutation occurs under --preview — neither per-item set nor finalise.
Print a summary grouped by action, listing the affected ids (not bare counts)
so the user can see exactly which items changed without re-running:
pushed: <ids>pulled: <ids>pushed-unsynced: <ids> (new external_id written back)pulled-untracked: <ids> (remote key → new local id)conflicts-skipped: <ids>overrides: OVERRIDE <id> (<external_id>): pushed local→remoteneeds-retry: <ids>remote-absent: <ids>unsynced (not pushed): <ids> (declined)
