Vendor sync
Claude Code plugin that automates the sprint workflow for projects using beads and Basic Memory
npx -y skills add voxpelli/claude-beads --skill vendor-syncAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.md
17.0 KB, as published. Nobody here has run it
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:
{ "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>.mdfilename (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 useprefixas 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.
[
{ "package": "@scope/pkg-name", "local-path": "/abs/path/to/checkout" }
]
Resolution rules:
- Read
.claude/vendor-registry.json. - If
.claude/vendor-registry.local.jsonexists, read it and merge on top, matching entries by thepackagekey (the most stable identifier across machines). Fields present in.local.jsonwin; absent fields keep the base value. - Entries in
.local.jsonwhosepackagedoes 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 rungit subtree add --prefix vendor/<name> <remote> <branch> --squash(the user runs this themselves), then invoke workflow 0 (Bootstrap registry) to register it.
-
Detect candidate vendor directories.
find vendor/ -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sortIf 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. -
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):git remote show <remote> 2>/dev/null | grep 'HEAD branch' | awk '{print $NF}'If the command fails, returns empty, or yields the literal
(unknown)(whichgit remote showemits when the remote has no HEAD set), default tomain. Treat any captured value containing parentheses or other non-[A-Za-z0-9._/-]characters the same way. -
package— read<dir>/package.jsonif present: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.
-
-
Prompt only the residuals. Use at most two
AskUserQuestioncalls (eachheaderfield stays within the 12-character SDK cap):header: "Remote"— present a menu of remote aliases gathered fromgit 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 rungit remote add <alias> <url>themselves before continuing — do not rungit remote addautomatically.header: "Local path"— only ask when the on-disk subtree lives somewhere other thanprefix(rare monorepo or alt-checkout case). The answer feeds.claude/vendor-registry.local.json, never the committed base registry.
-
Preview both files in a single message before writing. Show the proposed
.claude/vendor-registry.jsonentry and, when alocal-pathwas provided, the proposed.claude/vendor-registry.local.jsonentry. AskConfirm? [yes / edit / skip]. Onedit, re-prompt the affected derived field individually. Onskip, 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. Onyes, proceed to step 5. -
Write both files via the
Writetool. Always write base and.local.jsonseparately — never embedlocal-pathin 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. -
Verify round-trip parse. Use the
Readtool to re-read each written file, then validate the JSON vianode -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.jsonentry, when written, must havepackageandlocal-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.jsonparse failure only, warn and continue without the local override (the base registry alone is still usable). - Base entry must have
-
Check
.local.jsongitignore status. When a.local.jsonwas written, run:git check-ignore -q .claude/vendor-registry.local.jsonExit 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.jsonto your.gitignore(covers both vendor-registry.local.json and synergy-registry.local.json, and is forward-compatible with future.local.jsonregistries)." 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. -
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
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:
PRE_PULL_HEAD=$(git rev-parse HEAD)
For each selected entry, run:
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:
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:
rm -rf <prefix>/node_modules
Run this for every entry that was pulled.
6. Re-link workspaces
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:
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):
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:
- Delete the entry from the tracking file
- For vendor files: if the section is now empty, restore the
_No entries yet._placeholder - 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:
- Call
mcp__basic-memory__search_noteswith the package name - If a note exists, call
mcp__basic-memory__read_noteto check for a matching friction entry - If found, call
mcp__basic-memory__edit_notewithfind_replaceto 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)_Useexpected_replacements=1. Always match against the note's exact text.
- Bugs / Feature Requests:
- 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:
npm run check && npm test— if both scripts existnpm run check— if onlycheckexistsnpm test— if onlytestexists
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-*.mdfiles (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.jsonand 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 pullerrors because git cannot identify the subtree prefix correctly, use the explicit merge fallback:
Then proceed from step 4 (conflict resolution) normally.git fetch <remote> <branch> git merge -X subtree=<prefix> --squash <remote>/<branch>