aboutsummaryrefslogtreecommitdiff
path: root/.claude
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-11 17:05:03 -0500
committerCraig Jennings <c@cjennings.net>2026-06-11 17:05:03 -0500
commitda93ffd91dea133963ffceaff24d41bc76b8ff93 (patch)
tree6aac57d5eb712463a852c74e75150331be2298b1 /.claude
parent61e37f55c044ff7bbd41cb142ce9dfe232934216 (diff)
downloadrulesets-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.md92
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.