# Dev-Toolkit Sync (Step 3d)

The companion MCP server (`multi-agent-toolkit-mcp`) is its own repo with its own registry. Pipeline skills call its tools and several declare a minimum version, so when it moves it has to ship  -  otherwise the pipeline documents a tool no consumer has.

**Resolution**: identical to `/multi-agent:refactor` Step 0c  -  `prefs.global.devToolkit` first, then auto-detect from the `mcpServers` registration (`~/.claude.json`, `~/.claude/settings.json`), then skip. `enabled: false` skips.

**Detect movement**:

```bash
DT="<resolved localPath>"; cd "$DT"
git fetch origin --quiet 2>/dev/null || true
DIRTY=$(git status --porcelain)
AHEAD=$(git log --oneline @{u}..HEAD 2>/dev/null | wc -l | tr -d ' ')
VER=$(node -p "require('./package.json').version")
TAGGED=$(git tag --list "v$VER")
```

Moved = `DIRTY` non-empty, or `AHEAD` > 0, or no tag for `VER`. Nothing moved -> report `multi-agent-toolkit: up to date (v$VER)` and continue with the sync.

**Ship gates**  -  all must pass before commit or publish. A gate failure aborts THIS step only, reported with file + line; the rest of the sync continues.

**Prefer the repo's own gate script.** When the toolkit ships one (`npm run gates`, or `scripts/gates.sh`), run THAT and skip the inline list below - the repo owns the definition, this skill only owns the requirement. The inline gates are the fallback for a repo that has none, and the checklist the gate script itself must cover.

```bash
cd "$DT"
if node -e "process.exit(require('./package.json').scripts?.gates ? 0 : 1)" 2>/dev/null; then
  npm run gates || echo "ABORT: multi-agent-toolkit gates failed"
elif [ -f scripts/gates.sh ]; then
  bash scripts/gates.sh || echo "ABORT: multi-agent-toolkit gates failed"
else
  : # run the inline gates below
fi
```

1. **Syntax**: `node --check index.js` plus every `tools/**/*.js`.
2. **Boot + inventory**: handshake the server over stdio and count the registered tools.

```bash
TOOLS=$(printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"sync","version":"1"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | node index.js 2>/dev/null \
  | node -e 'let b="";process.stdin.on("data",d=>b+=d).on("end",()=>{const m=b.split("\n").filter(Boolean).map(l=>{try{return JSON.parse(l)}catch{return null}}).find(x=>x&&x.id===2&&x.result&&x.result.tools);console.log(m?m.result.tools.length:0)})')
[ "${TOOLS:-0}" -gt 0 ] || echo "ABORT: the toolkit served 0 tools"
```

3. **Advertised-count match**: `$TOOLS` must equal every advertised count (`package.json` `description`, the `README.md` header and its per-family rows). Fix the docs, never the gate  -  a stale count has shipped before.
4. **Packaging**: every directory the server loads at runtime must be inside `files[]`. A missing entry publishes a broken package (this has already caused one patch release).

```bash
PACK=$(npm pack --dry-run 2>&1)
for d in tools/*/; do printf '%s' "$PACK" | grep -q "$d" || echo "ABORT: $d missing from files[]"; done
```

5. **stdout hygiene**: `grep -rn "console\.log(" index.js tools/` must be empty  -  stdout carries the JSON-RPC frames, diagnostics belong on `console.error`.
6. **Personal-data scan**: no absolute `/Users/<name>` paths, no tokens, no corporate hostnames in the shipped files.
7. **Version contract**: every pipeline-side minimum (the `cross-cli-contract.md` tool table, `design-check`'s MCP requirement, `apple-archive-compliance`) must be satisfied by the version about to ship. A newly added tool that a pipeline skill now needs -> bump that declared minimum in the pipeline during this same sync.

**Version bump**: patch for fixes and docs, minor for a new tool, major for a removed or renamed tool (pipeline minimums depend on the surface).

**Ship**:

```bash
gh auth switch --user {owner}
git add -A
git commit -F "/tmp/release-msg-$NEW.txt"   # "{type}(v$NEW): {what changed}", written with the file tool
git tag "v$NEW" && git push origin main --tags
```

Publish to the registry declared in `publishConfig` through a throwaway userconfig. Never edit `~/.npmrc`, and never a bare `npm publish`  -  it lands on whichever registry the ambient config happens to name.

**The token depends on the registry AND on the package scope.** The registry picks
the credential *type*; the scope picks the *account*. Resolving on host alone
misroutes on any machine with more than one GitHub identity, which is the common
case for anyone with a work and a personal account.

| Registry | Credential type | Account chosen by |
|---|---|---|
| `npm.pkg.github.com` | GitHub PAT with `write:packages` (Classic; fine-grained PATs are not fully supported for Packages) | the package scope, e.g. `@{owner}` → the `{owner}` account's PAT |
| `registry.npmjs.org` | npm token (logical key `npm`) | the npm account that owns the scope |

Resolve the scope first, then the key:

```bash
SCOPE=$(node -p "(require('./package.json').name.match(/^@([^/]+)/)||[])[1] || ''")
# Prefer a scope-specific mapping; fall back to the generic key only when the
# machine has exactly one GitHub identity.
KEY=$(node -e '
  const fs=require("fs"),os=require("os"),p=os.homedir()+"/.claude/multi-agent-preferences.json";
  const km=(JSON.parse(fs.readFileSync(p,"utf8")).global||{}).keychainMapping||{};
  const scope=process.argv[1];
  process.stdout.write(km[`github_${scope}`] ? `github_${scope}` : "github");
' "$SCOPE")
```

**Pre-flight the scope, do not learn it from a 403.** GitHub tells you a token's
scopes on any authenticated request, so check before uploading rather than after:

```bash
scope_ok() {  # $1 = token; prints the login, non-zero when write:packages is absent
  local hdrs; hdrs=$(curl -sI -H "Authorization: token $1" https://api.github.com/user)
  printf '%s' "$hdrs" | grep -i '^x-oauth-scopes:' | grep -q 'write:packages'
}
```

**Candidate order for a `npm.pkg.github.com` publish.** The scope matters more than
where the token is stored, and the two are not correlated: on a machine with a work
and a personal identity, the hand-made PAT in the credential store may be the wrong
account or the right account without `write:packages`, while the `gh` CLI's own
OAuth token for that account often has it.

1. `github_<scope>` from `keychainMapping` (a PAT deliberately onboarded for this scope)
2. `gh auth token -u <scope>`  -  gh's stored OAuth token for that account
3. the generic `github` key  -  only when the machine has one GitHub identity

Take the first candidate that passes `scope_ok` AND whose `login` matches the
package scope. If none qualifies, stop before `npm publish` and report which
candidates were tried, what login each resolved to, and which scope was missing.
That report is the actionable output; a 403 body is not.

Measured on this machine, which is why the order is what it is:

| Candidate | login | scopes |
|---|---|---|
| `keychainMapping.github` | corporate EMU account | cannot publish to a personal scope under any grant |
| the personal PAT in the store | personal | `admin:public_key, gist, read:org, repo`  -  no `write:packages` |
| `gh auth token -u {owner}` | personal | `gist, read:org, repo, workflow, write:packages` ✓ |

**Two 403s mean two different things, and neither says "wrong token" plainly:**

- `Unauthorized: As an Enterprise Managed User, you cannot access this content`  -
  the resolved token belongs to a corporate EMU account, which cannot publish to a
  personal scope at all. The mapping points at the wrong identity. Map the personal
  account's PAT under `github_<scope>` and re-run; do not "fix" this by granting
  the EMU token more scopes, because no scope makes an EMU account able to write
  to a personal namespace.
- `The token provided does not match expected scopes`  -  right account, missing
  permission. The PAT needs **Classic** with `write:packages` (plus `repo` for a
  private package). Regenerate it at
  `https://github.com/settings/tokens/new?scopes=write:packages,read:packages,repo`
  and re-onboard via `/multi-agent:setup`.

Report which of the two it was. "Permission denied" alone sends the user to
regenerate a token that was never the problem.

```bash
NPMRC=$(mktemp); trap 'rm -f "$NPMRC"' EXIT
REG=$(node -p "require('./package.json').publishConfig?.registry || 'https://registry.npmjs.org'")
HOST=${REG#https://}; HOST=${HOST%/}
case "$HOST" in npm.pkg.github.com*) KEY=github ;; *) KEY=npm ;; esac
TOKEN=$(bash "$HOME/.claude/lib/credential-store.sh" get "$KEY")
[ -n "$TOKEN" ] || echo "ABORT: no '$KEY' token - onboard it via /multi-agent:setup before publishing"
printf '%s\n' "registry=$REG" "//$HOST/:_authToken=$TOKEN" > "$NPMRC"
npm publish --userconfig "$NPMRC"
```

Verify the publish landed: on GitHub Packages `npm view <package> version --userconfig "$NPMRC"`; on npmjs read the registry directly (`curl -s https://registry.npmjs.org/<package>/latest`) rather than `npm view`, which can answer from a stale cache.

**Required release** (optional; ask first even in `release` mode, it halts every older install): `npm dist-tag add "$(node -p "require('./package.json').name")@$VER" required --userconfig "$NPMRC"`. Out of band, so promote or demote any time. Contract: `multi-agent-refs/rules.md` "Supported Version Gate".

**Approval**: outside autopilot and outside `release`, ask before shipping.

> "multi-agent-toolkit moved (N changed files, M unpushed commits, v$VER). What should I do?"

- Commit + push (no publish)
- Commit + push + publish v$NEW
- Skip this step

With `release`, or in autopilot, run the full ship path without asking. If the working copy holds band-E work from `/multi-agent:refactor` that was never approved, do not ship it silently  -  report it and ask.

**Consumer note**: a path-based stdio registration (`node <localPath>/index.js`) picks the change up on the next MCP restart with no reinstall; a packaged registration (`npx <package>`) needs the published version. State which one the resolved config uses in the Step 6 report.

---
