diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-11 17:05:03 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-11 17:05:03 -0500 |
| commit | da93ffd91dea133963ffceaff24d41bc76b8ff93 (patch) | |
| tree | 6aac57d5eb712463a852c74e75150331be2298b1 /.claude | |
| parent | 61e37f55c044ff7bbd41cb142ce9dfe232934216 (diff) | |
| download | rulesets-da93ffd91dea133963ffceaff24d41bc76b8ff93.tar.gz rulesets-da93ffd91dea133963ffceaff24d41bc76b8ff93.zip | |
feat(commands): /update-skills syncs forks with upstream via 3-way merge
Upstream releases fixes worth pulling into the forks (arch-decide, playwright-js, playwright-py) without losing our local modifications. Each fork now has a manifest at upstreams/<name>/ plus a committed baseline snapshot that is the 3-way merge base. scripts/update-skills.py classifies each file's drift and merges to stdout. The command owns per-file confirmation, per-hunk conflict prompts, and every target write.
I centralized manifests under upstreams/ instead of per-skill dotfile dirs because arch-decide is now two flat files in commands/ and can't carry one. A "files" map in its manifest handles the upstream rename of SKILL.md to arch-decide.md.
I seeded baselines from today's upstream HEADs, so pre-existing local modifications classify as local-only from here on. git merge-file signals hard errors as exit 255, which subprocess reports as positive. The guard treats anything 128 and up as an error so a binary-file failure isn't misread as a conflict.
Diffstat (limited to '.claude')
| -rw-r--r-- | .claude/commands/update-skills.md | 92 |
1 files changed, 92 insertions, 0 deletions
diff --git a/.claude/commands/update-skills.md b/.claude/commands/update-skills.md new file mode 100644 index 0000000..fbc740a --- /dev/null +++ b/.claude/commands/update-skills.md @@ -0,0 +1,92 @@ +--- +description: Sync forked skills and commands with their upstreams via 3-way merge against a committed baseline. Discovers forks from upstreams/*/manifest.json in the rulesets repo, runs scripts/update-skills.py check per fork (clones upstream to a cache, classifies every file as unchanged / upstream-changed / local-only / both-changed / upstream-new / local-new / upstream-deleted), then walks the user through applying upstream changes — per-file confirmation first, per-hunk keep-local / take-upstream / both / skip prompts when a 3-way merge conflicts. Updates last_synced_commit and refreshes the baseline on completion. Use to pull upstream fixes into arch-decide, playwright-js, playwright-py, or any future fork without losing local modifications. Do NOT use to create a new fork (write the manifest + run bootstrap by hand), to sync .ai/ templates (startup's rsync owns that), or outside the rulesets repo. +disable-model-invocation: true +--- + +# /update-skills — Sync forks with their upstreams + +Pull upstream changes into forked skills/commands deliberately: classify, confirm per file, resolve conflicts per hunk, never lose a local modification silently. + +## Usage + +``` +/update-skills [FORK ...] [--dry-run] +``` + +- `FORK` — one or more fork names (`arch-decide`, `playwright-js`, `playwright-py`). Default: all forks registered under `upstreams/`. +- `--dry-run` — run the classification and report what a sync would do; write nothing. + +## Layout + +Each fork is registered at `upstreams/<name>/`: + +- `manifest.json` — upstream `url`, `ref`, `subpath`, repo-relative `target`, optional `files` map (upstream-path → target-path, for forks whose files were renamed, like arch-decide's `SKILL.md` → `arch-decide.md`), `license`, `last_synced_commit`. +- `baseline/` — committed snapshot of the upstream at the last sync, mirroring the *target* layout. This is the 3-way merge base; without it every both-changed file degrades to side-by-side review. + +The helper is `scripts/update-skills.py`. It never writes a fork's target files — it classifies (`check`), merges to stdout (`merge-file`), and maintains the manifest + baseline (`bootstrap`, `mark-synced`). All target writes happen in this command's flow, with the user's per-file confirmation. + +## Flow + +### 1. Discover and check + +``` +scripts/update-skills.py list +scripts/update-skills.py check <fork> --json +``` + +Run `check` for each requested fork. Network is required; if a clone fails, report the fork as unreachable and continue with the rest (degrade, don't abort the batch). + +Present a per-fork classification table. If everything is `unchanged` (or only `local-only`/`local-new`, which need no sync), say so and skip to the next fork. `--dry-run` stops here. + +### 2. Apply upstream changes, per file + +Process only the statuses that carry upstream movement, walking each file with a confirmation (yes / no / show-diff) before touching it: + +- `upstream-changed` — local matches baseline, upstream moved. Safe fast path: copy the upstream version from the cache checkout (`/tmp/update-skills/<fork>/<subpath>/...`) over the target file. +- `upstream-new` — new file upstream. Offer to add it at the target path. +- `both-changed` — run the 3-way merge: + + ``` + scripts/update-skills.py merge-file <fork> <target-relative-path> + ``` + + Exit 0: clean merge — show the result, write it to the target on confirmation. + Exit 1: conflict — the output carries `<<<<<<< local` / `=======` / `>>>>>>> upstream` markers. Walk each conflicting hunk with the user, showing the local and upstream sides (and the baseline text when it clarifies): + + 1. keep local + 2. take upstream + 3. both (local then upstream) + 4. skip — leave the markers for manual resolution later + + Assemble the resolved hunks and write the final file. A skipped hunk leaves its markers in place; flag every skipped file in the summary so it isn't forgotten. + +- `upstream-deleted` — upstream removed a file the baseline had. Ask: delete locally, or keep (it becomes `local-new` at the next baseline refresh). Never delete without asking. + +Statuses needing no action: `unchanged`, `local-only` (our modification, preserved), `local-new` (our addition, preserved), `local-deleted` (we removed it deliberately), `no-baseline` (see below). + +### 3. Mark synced + +When every file in a fork is resolved (no conflict markers left anywhere): + +``` +scripts/update-skills.py mark-synced <fork> +``` + +This refreshes `baseline/` to the checked upstream commit and stamps `last_synced_commit`. Skip this step if any file still carries markers — the baseline must only advance past fully-absorbed upstream states. + +### 4. Summarize and commit + +Per fork: upstream commit synced to, files taken / merged / conflicted / skipped, anything left for manual follow-up. Then commit the result (targets + `upstreams/` baselines + manifests together) through the normal review-and-publish flow — one commit per fork when changes are substantial, one batch commit when they're small. + +## Fallback: missing baseline + +`no-baseline` statuses mean the fork was never bootstrapped (or its baseline was deleted). No 3-way merge is possible. Offer two paths: + +1. Side-by-side review: show local vs upstream per file and let the user pick, then `bootstrap` to seed the baseline going forward. +2. Seed first: `scripts/update-skills.py bootstrap <fork>` snapshots the *current* upstream as the baseline. Local differences from it become `local-only` — correct from then on, but upstream changes made before the seed are invisible to future merges. + +## Notes + +- Baselines were first seeded 2026-06-11 from the then-current upstream HEADs, so each fork's pre-existing local modifications are tracked as `local-only` from that point. +- Persistent `local-new` files are normal (e.g. playwright-js's `package-lock.json` and repo-root-copied `LICENSE`); they never block a sync. +- Upstream renames surface as `upstream-deleted` + `upstream-new`. Handle as a pair: take the new file, then decide the old one's deletion. For mapped forks (arch-decide), update the manifest's `files` map instead. |
