1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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.
|