# Vendor Sync

> Pull latest upstream changes from vendor subtrees into this project. Use when the user says 'sync vendor', 'pull upstream', 'update subtrees', 'vendor pull', 'vendor sync', 'sync vendors', 'vendor changes', or wants to update any vendor package from its upstream repository. Also use when the user mentions a vendor package name followed by 'pull', 'sync', or 'update'.

- Skill: `voxpelli/vendor-sync` (Agent Skill)
- Install (CLI): `npx skillmds@latest add voxpelli/vendor-sync`
- Raw SKILL.md: https://api.skillmd.com/api/skills/voxpelli/vendor-sync/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: voxpelli (https://skillmd.com/u/voxpelli)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/voxpelli/vendor-sync

---


# Vendor Sync

Pull latest upstream changes into one or more vendor subtrees, cross-reference
the diff against open upstream issue tracking entries, and re-link workspace
packages.

## Registry

Read the vendor subtree registry from `.claude/vendor-registry.json`. Each
entry has the shape:

```json
{ "prefix": "vendor/pkg", "remote": "remote-name", "branch": "branch-name", "package": "@scope/pkg-name" }
```

- **prefix** — the local `vendor/` directory used as the subtree prefix
- **remote** — the git remote alias configured in this repo
- **branch** — the upstream branch to pull from
- **package** — the npm package name; maps to `UPSTREAM-<name>.md` filename
  (slashes → `--`, drop leading `@`): e.g. `@voxpelli/pkg` → `UPSTREAM-voxpelli--pkg.md`

Optional fields:

- **local-path** — alternative on-disk path for the subtree if it does not
  live at `prefix` (rare; used for monorepo layouts or CI checkout paths).
  When absent, skills use `prefix` as the on-disk location.

If `.claude/vendor-registry.json` does not exist, offer to create it via
workflow 0 (Bootstrap registry). Do not attempt to read the subtree
table from any other source.

### Local override file

`.claude/vendor-registry.local.json` is a gitignored companion that overrides
fields in the committed `.claude/vendor-registry.json`. It mirrors the
`settings.local.json` convention: machine-specific state stays out of version
control, while the committed registry documents the shared schema.

```json
[
  { "package": "@scope/pkg-name", "local-path": "/abs/path/to/checkout" }
]
```

Resolution rules:

1. Read `.claude/vendor-registry.json`.
2. If `.claude/vendor-registry.local.json` exists, read it and merge on top,
   matching entries by the `package` key (the most stable identifier across
   machines). Fields present in `.local.json` win; absent fields keep the
   base value.
3. Entries in `.local.json` whose `package` does not appear in the base
   registry are ignored — the base registry remains the authoritative source
   of which subtrees exist.

Vendor subtrees almost always live at their `prefix` and don't need local
overrides — this companion exists for symmetry with `synergy-registry.local.json`
and for the rare monorepo or non-standard checkout case. Never commit
`.claude/vendor-registry.local.json`: it encodes machine-specific paths.

## Workflow

### 0. Bootstrap registry

Run this workflow when the user wants to create `.claude/vendor-registry.json`
from scratch, or when workflow 1 (Determine scope) redirects here because no
registry exists. The flow derives most fields from the working tree and prompts
only at the residuals.

> **Precondition: the vendor subtree must already exist on disk under
> `vendor/`.** This workflow registers an existing subtree; it does NOT
> add new ones. To add a new subtree, first run
> `git subtree add --prefix vendor/<name> <remote> <branch> --squash`
> (the user runs this themselves), then invoke workflow 0 (Bootstrap
> registry) to register it.

1. **Detect candidate vendor directories.**

   ```bash
   find vendor/ -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort
   ```

   If the command returns nothing (no `vendor/` directory at all, or it is
   empty), ask the user for an explicit subtree path. If multiple candidates
   are returned, process them one at a time — one preview-confirm cycle per
   entry — rather than batching, so each entry can be derived and corrected
   independently.

2. **Auto-derive fields for each candidate.** For a candidate at `<dir>`:

   - `prefix` — the directory path itself (e.g. `vendor/foo`).
   - `branch` — query the upstream branch from the candidate remote once it
     is known (see step 3):

     ```bash
     git remote show <remote> 2>/dev/null | grep 'HEAD branch' | awk '{print $NF}'
     ```

     If the command fails, returns empty, or yields the literal `(unknown)`
     (which `git remote show` emits when the remote has no HEAD set), default
     to `main`. Treat any captured value containing parentheses or other
     non-`[A-Za-z0-9._/-]` characters the same way.
   - `package` — read `<dir>/package.json` if present:

     ```bash
     node -e "try{const p=require('./<dir>/package.json');const n=(p&&typeof p==='object'?p.name:'')||'';if(n)console.log(n)}catch{}"
     ```

     If the output is blank (empty or whitespace only), ask the user for
     the package name.

3. **Prompt only the residuals.** Use at most two `AskUserQuestion` calls (each
   `header` field stays within the 12-character SDK cap):

   - `header: "Remote"` — present a menu of remote aliases gathered from
     `git remote -v | awk '{print $1}' | sort -u | grep -v '^origin$'`. If
     the list is empty, ask the user to type both an alias and the upstream
     URL, and instruct them to run `git remote add <alias> <url>` themselves
     before continuing — do not run `git remote add` automatically.
   - `header: "Local path"` — only ask when the on-disk subtree lives
     somewhere other than `prefix` (rare monorepo or alt-checkout case). The
     answer feeds `.claude/vendor-registry.local.json`, never the committed
     base registry.

4. **Preview both files in a single message** before writing. Show the
   proposed `.claude/vendor-registry.json` entry and, when a `local-path` was
   provided, the proposed `.claude/vendor-registry.local.json` entry. Ask
   `Confirm? [yes / edit / skip]`. On `edit`, re-prompt the affected derived
   field individually. On `skip`, **discard this candidate (do not write)
   and continue to the next candidate** in the multi-candidate batch — do
   NOT abort the entire workflow on a per-entry skip. After all candidates
   are processed, the final report (step 8 below) lists which entries were
   written and which were skipped. On `yes`, proceed to step 5.

5. **Write both files** via the `Write` tool. Always write base and
   `.local.json` separately — never embed `local-path` in the committed
   registry. If the base registry already exists (returning user adding a
   second entry), this workflow does not append; that case is tracked
   separately and falls back to manual editing for now.

6. **Verify round-trip parse.** Use the `Read` tool to re-read each written
   file, then validate the JSON via
   `node -e 'JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"))' <path>`
   (any non-zero exit signals invalid JSON). Confirm the required fields
   are present:

   - Base entry must have `prefix`, `remote`, `branch`, `package`.
   - `.local.json` entry, when written, must have `package` and `local-path`.

   On base-registry parse failure or missing required fields, abort
   workflow 0 (Bootstrap registry) entirely — report the failure and offer
   to re-run. On
   `.local.json` parse failure only, warn and continue without the local
   override (the base registry alone is still usable).

7. **Check `.local.json` gitignore status.** When a `.local.json` was
   written, run:

   ```bash
   git check-ignore -q .claude/vendor-registry.local.json
   ```

   Exit status semantics: `0` = file is gitignored (no action); `1` = file
   is **not** gitignored — warn the user with the exact line to add: "Add
   `.claude/*.local.json` to your `.gitignore` (covers both
   vendor-registry.local.json and synergy-registry.local.json, and is
   forward-compatible with future `.local.json` registries)." Do not
   auto-edit `.gitignore` — it is user-owned. `128` = the check itself
   failed (not a git repo, or another git error) — report the underlying
   error and skip the gitignore warning rather than emitting a
   false-positive.

8. **Resume to workflow 1 (Determine scope).** Once verification passes, the
   newly created registry is ready for the rest of the sync flow.

The `UPSTREAM-<package>.md` filename for each new entry follows the standard
convention: slashes → `--`, drop leading `@` (e.g. `@scope/pkg` →
`UPSTREAM-scope--pkg.md`). Workflow 1 (Determine scope) and downstream steps
will create these files lazily; workflow 0 (Bootstrap registry) does not pre-create them.

### 1. Determine scope

Read and parse `.claude/vendor-registry.json`. If
`.claude/vendor-registry.local.json` exists, read it and merge on top per the
resolution rules in the Registry section above (per-entry merge by `package`
key; fields in `.local.json` win). Then determine which subtrees to pull:

- If the user names a specific package (by prefix directory, remote alias, or
  package name), pull only that entry
- Match casual names: "auth" → the entry whose remote or package contains
  "auth"; resolve ambiguity by asking
- If the user says "all" or does not specify, pull all entries

### 2. Check working tree

```bash
git status
```

Subtree pulls create merge commits; the working tree must be clean. If there
are uncommitted changes, warn the user and ask whether to proceed.

### 3. Pull each subtree

Before pulling, capture the current HEAD so later steps can diff accurately
even if conflict resolution adds extra commits:

```bash
PRE_PULL_HEAD=$(git rev-parse HEAD)
```

For each selected entry, run:

```bash
git subtree pull --prefix <prefix> <remote> <branch> --squash
```

Use the git remote alias from the registry, not a full URL. The `--squash`
flag collapses upstream history into a single merge commit.

If the pull reports "Already up to date", say so and skip the remaining steps
for that entry.

### 4. Resolve conflicts

First check whether any conflicts exist:

```bash
git diff --name-only --diff-filter=U
```

If the output is empty, no conflicts — skip to step 5. If conflicts exist, for
modify/delete conflicts (upstream deleted a file that was locally modified from
a previous squash pull), always accept the upstream version:

- Deletions: `git rm <file>`
- Content conflicts: `git checkout --theirs <file>`

Vendor directories must mirror upstream exactly.

### 5. Clean stale vendor node\_modules

Remove any leftover `node_modules/` inside each pulled vendor directory before
running npm install. These are untracked artifacts from previous installs that
can poison npm workspace resolution:

```bash
rm -rf <prefix>/node_modules
```

Run this for every entry that was pulled.

### 6. Re-link workspaces

```bash
npm install
```

This updates workspace symlinks after subtree contents change.

### 7. Cross-reference changelog against UPSTREAM tracking files

For each pulled subtree that has a `CHANGELOG.md` (or `CHANGES.md`, `HISTORY.md`)
in its prefix directory, extract the changelog entries added by the pull:

```bash
git diff $PRE_PULL_HEAD -- <prefix>/CHANGELOG.md
```

Parse the added lines (those starting with `+`) to identify new changelog entries
(bug fixes, features, breaking changes). Compare each entry against the open items
in the corresponding `UPSTREAM-<package>.md` file — including `## Upstream
Opportunities` entries. If a changelog entry clearly addresses an open UPSTREAM
item (matching keywords, issue references, or described behavior), flag it as a
candidate for auto-resolution with your confidence level. For Upstream
Opportunities: if a changelog entry mentions a merged feature that matches a local
opportunity by keyword or upstream PR URL, flag as a **contribution-resolved**
event at high confidence.

- **High confidence** — changelog explicitly mentions the bug/feature by name or
  references the same upstream issue URL
- **Medium confidence** — changelog describes a fix/feature in the same area as
  the UPSTREAM entry but doesn't reference it directly
- **Low confidence** — only a vague topical match; mention but don't auto-resolve

For high-confidence matches, proceed to resolve the entry in step 8. For medium
confidence, note the match in the report and let the user decide. Skip low
confidence matches in the resolution step but mention them in the report.

If no changelog file exists in the pulled prefix, skip this step for that entry
and rely solely on the code diff cross-reference in step 8.

### 8. Cross-reference code diff against UPSTREAM tracking files

This is the primary resolution mechanism — do not defer this to the retro.

For each pulled subtree, capture the full sync diff scoped to the vendor
directory (not just the summary):

```bash
git show HEAD -- <prefix>
```

Read the corresponding `UPSTREAM-<package>.md` file (derived from the
registry `package` field). For each open entry in that file, check whether
the sync diff visibly addresses the issue (bug fixed, feature added, API
changed). If an entry appears resolved by the diff:

1. Delete the entry from the tracking file
2. For vendor files: if the section is now empty, restore the
   `_No entries yet._` placeholder
3. Note each deleted entry in your report

If no tracking file exists for a pulled package, skip this step for that entry.

### 8b. Annotate Basic Memory friction entries

For each entry auto-resolved in steps 7–8, check whether a corresponding
Basic Memory entity note exists with an `## Upstream Friction` section. If
Basic Memory MCP tools are available:

1. Call `mcp__basic-memory__search_notes` with the package name
2. If a note exists, call `mcp__basic-memory__read_note` to check for a
   matching friction entry
3. If found, call `mcp__basic-memory__edit_note` with `find_replace` to append
   an annotation to the matching entry's line. Use entry-type-specific text:
   - **Bugs / Feature Requests:** `_(Resolved by vendor-sync YYYY-MM-DD)_`
   - **Upstream Opportunities:** `_(Contributed upstream, merged YYYY-MM-DD)_`
     Use `expected_replacements=1`. Always match against the note's exact text.
4. Annotation only — never delete entries, never move them to `### Resolved`.
   The upstream-tracker's workflow 6 (Promote to Basic Memory) handles pruning
   during its prune pass.

If Basic Memory tools are not available or no matching entry exists, skip
silently. This annotation step is best-effort — vendor-sync works identically
without it, and stale Basic Memory entries are caught by trend review.

### 9. Verify

Check `package.json` scripts and run the most comprehensive available
verification in order of preference:

1. `npm run check && npm test` — if both scripts exist
2. `npm run check` — if only `check` exists
3. `npm test` — if only `test` exists

Show the last 5 lines of output. If verification fails after a pull, report
the errors. The user may need to adapt app code to upstream API changes.

### 10. Report

Summarize the results:

- Which subtrees were pulled and whether there were changes
- Which UPSTREAM entries were resolved (deleted) and why
- Whether verification passed
- Any app-side implications from the upstream changes (API changes, new
  exports, behavioral differences visible in the diff)
- If any resolved UPSTREAM entries overlap with Extraction Candidates in
  `SYNERGY-*.md` files (e.g., a feature extracted to the shared upstream),
  mention the overlap so the user can update the SYNERGY file via
  `/synergy-tracker`

## Guidelines

- **Division of labor.** vendor-sync owns UPSTREAM auto-resolution during
  subtree pulls. It does not modify SYNERGY files — cross-project pattern
  tracking is managed by `/synergy-tracker`.
- **Registry-first discovery.** Refuses to proceed without
  `.claude/vendor-registry.json` — no guessing at subtree prefixes.
- **Annotation semantics.** BM annotation-only (step 8b) — never delete or
  move entries in `## Upstream Friction`. The upstream-tracker's workflow 6 (Promote to Basic Memory)
  handles pruning during its prune pass.

## Error handling

- **Registry not found** — tell the user this project has no
  `.claude/vendor-registry.json` and offer to run workflow 0 (Bootstrap
  registry) to create it interactively. If the user agrees, run workflow 0
  (Bootstrap registry), then resume. Otherwise stop.
- **No changes** — if a pull reports "Already up to date", skip steps 4–8b for
  that entry and note it in the report.
- **npm install failures** — most commonly caused by stale `node_modules/`
  inside vendor directories (step 5). If install fails after cleaning those,
  check for peer dependency conflicts between vendor devDependencies and the
  root project.
- **Verification failures** — report errors. Do not attempt auto-fixes unless
  the failure is clearly a stale artifact (re-run npm install and retry once).
- **Subtree heuristics fail** — if `git subtree pull` errors because git cannot
  identify the subtree prefix correctly, use the explicit merge fallback:
  ```bash
  git fetch <remote> <branch>
  git merge -X subtree=<prefix> --squash <remote>/<branch>
  ```
  Then proceed from step 4 (conflict resolution) normally.

