Guide users through `pulp upgrade` — discover new CLI releases, interpret migration notes for the hop they're performing, and apply breaking-change fixes (CMake macro renames, API surface changes, config file moves). Handles "upgrade pulp", "what's new", "migrate my project", "show breaking changes", and the `/upgrade` slash command. Pairs with the embedded migration index shipped in the CLI binary (release-discovery Slice 3, #571).
Install to Claude Code
npx -y skills add danielraffel/pulp --skill upgrade --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Upgrade?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/danielraffel-upgrade)More formats (shields.io, HTML) on the badges page.
---
name: upgrade
description: |
Guide users through `pulp upgrade` — discover new CLI releases, interpret
migration notes for the hop they're performing, and apply breaking-change
fixes (CMake macro renames, API surface changes, config file moves).
Handles "upgrade pulp", "what's new", "migrate my project", "show
breaking changes", and the `/upgrade` slash command. Pairs with the
embedded migration index shipped in the CLI binary (release-discovery
Slice 3, #571).
requires:
tools:
- pulp
---
# upgrade
## When this skill applies
- User asks to upgrade, update, or bump the Pulp CLI.
- User runs `/upgrade` in Claude Code.
- User asks "what changed", "what's new", "what breaks", or "how do I
migrate" after bumping an SDK / CLI version.
- A `pulp doctor --versions` run surfaces CLI-vs-SDK skew the user wants
to resolve.
This skill does NOT cover:
- Bumping the **project's** pinned SDK version — that's the
`cli-maintenance` skill + `pulp project bump`.
- Bumping the Pulp framework/source checkout version — that's
`pulp version bump` plus the release workflow.
- Shipping a PR from a dev branch — that's the `ci` skill via `pulp pr`.
- Updating the Shipyard pin — that's the Dependency Update Workflow in
`CLAUDE.md` (runs through `tools/deps/audit.py`).
## Mental model
Four independently-versioned surfaces, two of which this skill acts on
directly and one of which it may hand off to `pulp project bump`:
| Surface | Source of truth | How to upgrade |
|---------|----------------|----------------|
| Pulp CLI / SDK | `~/.pulp/bin/pulp` (installed binary) | `pulp upgrade` |
| Consumer project SDK pin | project `pulp.toml` `sdk_version` + `find_package(Pulp X.Y.Z ...)` | `pulp project bump` |
| Pulp source checkout version | Pulp repo `CMakeLists.txt` / release metadata | `pulp version bump` + release workflow |
| Claude plugin | `.claude-plugin/plugin.json` | `/plugin install pulp` in Claude Code |
| Shipyard pin | `tools/install-shipyard.sh` | Dependency Update Workflow (out of scope) |
`pulp upgrade` downloads the new CLI binary and replaces the installed
one. After the Phase 8 Rust cutover, release archives are dual-binary:
Rust `pulp` is the user-facing CLI and sibling `pulp-cpp` is installed
as the C++ fallthrough delegate. A healthy install has both
`~/.pulp/bin/pulp` and `~/.pulp/bin/pulp-cpp`; use `PULP_USE_CPP=1
pulp <args>` or direct `pulp-cpp <args>` only for rollback/debug
comparisons. Do not hand-swap binaries in user/system locations.
The C++ `cmd_upgrade.cpp` path still matters for pre-cutover users
upgrading into a Rust release: it must copy the archive's sibling
payloads, including `pulp-cpp`, before replacing the running `pulp`
binary (#1673).
`pulp upgrade --notes` prints migration notes for the hop without
downloading anything. `pulp upgrade --notes --json` emits the same data
as a stable-shape JSON document for agent consumption (the `/upgrade`
slash command is the primary consumer).
Use `pulp project bump` only after deciding a consumer project should
move its SDK pin. It operates on the active project or the registry
(`--all`); it does not upgrade the global CLI and it refuses to treat
the Pulp source checkout as a consumer project.
### `pulp upgrade` updates BOTH CLI and SDK by default
Pre-#2087 `pulp upgrade` only swapped the CLI binary. Users who ran it
then built a project that still resolved its (months-old) SDK and
silently missed every framework fix in between (the Spectr incident
2026-05-15). Post-#2087:
- Default: fetch latest CLI binary + run `pulp sdk install --version
<new>` immediately after the swap, so the matching SDK lands at
`~/.pulp/sdk/<new>/` in the same invocation.
- `--cli-only` flag keeps the pre-#2087 behavior for the rare case
where a user wants the new CLI paired with their current SDK.
- The SDK install is best-effort: a transient network failure logs a
warning and tells the user to retry with `pulp sdk install`. The CLI
swap is not rolled back — the user can always retry SDK install.
- When run from inside a project whose `pulp.toml` pins an explicit
SDK version, the SDK install happens (so the new version is
available globally) but the project's pin is left ALONE. A clear
notice prints — "Project X stays on pinned SDK Y.Y.Y; latest
available is Z.Z.Z — `pulp project unpin` to start tracking latest."
Pinning is sacred when the user has explicitly pinned.
`pulp project pin <version>` is the new primary name for what
`pulp project bump` did. `bump` survives as a deprecated alias for
one minor release. New docs and skill examples should use `pin`.
`pulp project unpin` (new) flips a project back to floating mode
(`sdk_version = "latest"` in pulp.toml).
New projects created via `pulp create` default to floating mode
(`sdk_version = "latest"`), so they pick up framework fixes
automatically. `pulp create --pin` writes the exact version instead
for users who want reproducibility from day one.
## Discovery workflow (`pulp` on PATH is assumed)
The skill shells out to `pulp` — no hardcoded paths, no env-var
requirement. If `pulp` is not on PATH, tell the user to install first
(`curl -fsSL https://www.generouscorp.com/pulp/install.sh | sh`)
and stop.
Note: a successful `pulp upgrade` now **self-heals PATH** — after the
binary swap it appends the CLI's own directory to the user's shell
profile when it isn't already on `$PATH` (honoring `PULP_NO_MODIFY_PATH`).
This closes the gap where a CLI first installed via a source / SDK-prefix
install (`cmake --install --prefix ~/pulp-sdk` → `~/pulp-sdk/bin/pulp`)
could upgrade successfully yet still be "command not found" in a fresh
shell. So after an upgrade the user may need to restart their shell or
`source` the named profile, but no longer has to add PATH by hand. See
the `cli-maintenance` skill for the implementation
(`upgrade_install::ensure_dir_on_path`).
### Plugin ↔ CLI skew banner (Slice 6, #551)
Before running any `pulp` command, source the shared skew-check helper
so the user sees a single-line hint when the installed CLI is older
than the plugin's declared `min_cli_version`:
```bash
source "$(git rev-parse --show-toplevel 2>/dev/null || echo .)/tools/scripts/cli_version_check.sh"
pulp_cli_version_check # no-ops silently if already checked this session
```
Behaviour:
- Banner (stderr, at most once per session):
`[pulp] Claude plugin requires CLI >= v<MIN> but installed CLI is v<HAVE>. Run \`pulp upgrade\` or \`/upgrade\` in Claude Code.`
- Silent when CLI ≥ min, when the plugin manifest omits
`min_cli_version` (older plugin builds), or when either version is
non-numeric (dev builds).
- Overrides: `PULP_SKEW_CHECK_DISABLE=1` turns it off;
`PULP_SKEW_CHECK_CACHE` overrides the session-marker directory.
The same skew logic is surfaced by `pulp doctor --versions` inline, so
users who prefer running the diagnostic directly see the finding there
too — the helper is a convenience for skill authors, not a second
source of truth.
1. **Identify the active plugin directory** (so the skill can resolve
`docs/migrations/` for full-body lookups):
```bash
pulp doctor --versions --json
```
The JSON output contains `plugin_json_path` — walk up two levels
(`dirname $(dirname $plugin_json_path)`) to reach the plugin root.
2. **Report the version skew at a glance** so the user sees where they are
before deciding what to do. The JSON response also includes `cli`,
`plugin`, `project_sdk`, and `findings[]` — surface those in a
compact table.
3. **Fetch applicable migration notes** for the current hop.
**Important:** user-invoked `pulp upgrade` and `pulp upgrade
--install` force a synchronous release refresh instead of trusting
the 24h cache. This is deliberate: v0.78.3 showed that a cache
written minutes before a release can make a just-published version
invisible. `pulp upgrade --check-only` remains the lightweight
cache-aware probe; if you need release-fresh notes, capture the
`Latest:` value from a refreshed/default `pulp upgrade` run and
forward it as `--to "$LATEST"`.
In CI / sandbox lanes, `PULP_UPDATE_CHECK_DISABLED=1` makes
`--check-only` network-free. If the cache is empty in that mode, the
command reports the installed CLI version plus an explicit
disabled/not-queried latest line instead of querying GitHub Releases.
```bash
pulp upgrade --notes --json # defaults: from = installed CLI, to = cached latest
pulp upgrade --notes --json --to "$LATEST" # recommended — use value captured from refreshed upgrade check
pulp upgrade --notes --json --from X --to Y # explicit hop
```
## Release verification gotcha
When verifying a newly published release, test from the previously
published CLI with a deliberately fresh cache that still names the old
release:
```bash
PULP_HOME="$(mktemp -d)" pulp upgrade --check-only --json
# seed/update-cache.json to latest_version=<previous> with a current timestamp
pulp upgrade
```
The default `pulp upgrade` path must refresh through GitHub and report
the new tag immediately. Then run `pulp upgrade --install --to X.Y.Z`
in an isolated install directory and confirm a follow-up `pulp upgrade`
reports both `Installed` and `Latest` as the new version. This catches
the stale-cache failure from #1685 before the release is announced.
Stable-shape output (do NOT rename these keys — they are a public
surface the skill depends on, see `tools/cli/migration_index.hpp`):
```json
{
"from": "0.27.0",
"to": "0.30.0",
"entries": [
{
"version": "0.28.0",
"breaking": false,
"summary": "…",
"applies_if":"cli_version_from < 0.28.0 && cli_version_to >= 0.28.0",
"body": "…"
}
]
}
```
4. **Render the notes inline in the chat**. For each entry:
- If `breaking == true`, prefix with a loud tag (e.g. "BREAKING").
- Print the `summary` on the first line.
- Include the `body` verbatim — it's already Markdown.
## Interpreting migration notes
Each note describes one release's visible behaviour shift. Read them
like a pro-developer changelog, not a marketing blurb.
### Breaking vs non-breaking
- `breaking: true` — requires user action: code change, config edit,
habit change. Do not skip. Offer to grep the project for the affected
symbols as a follow-up.
- `breaking: false` — informational. The upgrade works without
intervention, but something worth knowing has changed.
### `applies_if` expressions
The filter runs in the CLI — by the time the skill receives JSON,
non-applicable entries are already stripped. You'll only see entries
whose `applies_if` matched the hop. Still: print the expression alongside
the note so a curious user can verify why it applied.
Grammar: Boolean combinations (`&&`, `||`, parentheses) over comparisons
of `cli_version_from` / `cli_version_to` against a literal semver. Six
operators: `<`, `<=`, `>`, `>=`, `==`, `!=`. See
`docs/migrations/README.md` for full details.
## Common breaking-change patterns
These are the patterns that have shown up in Pulp migration notes. When
you spot one, suggest the exact grep / replacement to the user instead
of leaving them to figure it out from a prose paragraph.
### CMake macro renames
**Pattern.** A `pulp_add_*` function in `tools/cmake/Pulp*.cmake` is
renamed, folded into `pulp_add_plugin(FORMATS ...)`, or split out of
one macro into several.
**What to do.**
```bash
# Find every call site.
grep -rn "pulp_add_ios_auv3\|pulp_add_auv3\|pulp_add_vst3_only" .
```
Propose the replacement form explicitly; do NOT rewrite the user's
CMakeLists.txt without confirmation. For unified-form migrations, show
a diff like:
```diff
- pulp_add_ios_auv3(MyPlugin …)
+ pulp_add_plugin(MyPlugin FORMATS AUv3 …)
```
### API surface changes
**Pattern.** A public C++ header under `core/*/include/pulp/**` adds,
removes, or renames a symbol that processors / view code use.
**What to do.**
```bash
# Find call sites of the old symbol.
grep -rn "OldProcessorAPI\|deprecated_function_name" .
```
Read the migration body for the replacement signature. If the change is
a deprecation (old API still works, emits a warning), flag that —
teams may want to migrate at their own pace.
### Config file / path moves
**Pattern.** A file Pulp reads (e.g. `~/.pulp/config.toml`,
`pulp.toml`, `.pulp/projects.json`) changes location or key shape.
**What to do.** Most config moves are handled by the CLI on first run
(it migrates the old location to the new one and prints a one-line
notice). The note tells you whether manual action is needed. If it is,
quote the exact `pulp config set` / `pulp config unset` commands from
the body.
### CLI flag / subcommand changes
**Pattern.** A subcommand is renamed (e.g. `pulp check` → `pulp doctor`),
a flag changes shape (`--sign-key` → `--ed25519-key`), or an exit code
semantic changes (silent-success → loud-failure as in #295 / #560).
**What to do.** If the user has CI scripts (`ci/`, `.github/workflows`,
`tools/local-ci/`, team-specific shell scripts), grep those for the
old invocation first — those are the scripts that'll silently break.
```bash
grep -rn "pulp <old-command>\|--<old-flag>" ci/ .github/ tools/ || true
```
## Bypass trailers
Slice 4 itself doesn't add new bypass trailers — it consumes Slice 3's.
But users will ask about trailers around upgrade-triggered PRs, so keep
the syntax handy (full table in `CLAUDE.md` → "Versioning & Skill-Sync
Policy"):
| Gate | Trailer (tip commit only, NEVER PR body) |
|------|-------------------------------------------|
| Version bump | `Version-Bump: <surface>=<patch\|minor\|major\|skip> reason="..."` |
| Skill update | `Skill-Update: skip skill=<name> reason="..."` |
| Auto-release | `Release: skip reason="..."` |
Trailer blocks must be **contiguous** — no blank lines between
trailers, or `git interpret-trailers --parse` stops reading. When
amending, rewrite the whole block, don't append.
## Decision tree (used by the `/upgrade` slash command)
```
pulp doctor --versions --json → parse cli / plugin / project_sdk / findings
pulp upgrade --notes --json → parse entries[]
present table: CLI vX.Y.Z → vA.B.C (<hop>)
plugin vP.Q.R → vP'.Q'.R' (if drift)
project SDK vS.T.U (skew warn from findings)
list applicable notes (inline, breaking flagged)
ask via AskUserQuestion:
- "Upgrade CLI + bump project SDK" → `pulp upgrade` + `pulp project bump` in the active project
- "Upgrade CLI only" → `pulp upgrade`
- "Bump project SDK only" → `pulp project bump` in the active project or `--all` when explicitly requested
- "Dismiss" → no action, remind user they can re-run `/upgrade`
```
## Testing the slash command
The `/upgrade` command shells out to `pulp upgrade --notes --json`.
A regression test lives in `test/test_cli_shellout.cpp` (the
`[issue-549]` tag) — it asserts that a synthetic hop produces the keys
the slash command depends on. If you add or rename a key in
`render_notes_json` (`tools/cli/migration_runtime.cpp`), update this
skill AND the slash command AND the test in the same PR.
## References
- Design: `planning/release-discovery-ux-design-2026-04-20.md` Section C + Slice 4
- CLI surface: `tools/cli/cmd_upgrade.cpp`, `tools/cli/migration_index.hpp`
- Migration doc schema: `docs/migrations/README.md`
- Slice 1 (diagnostics): #546 (`pulp doctor --versions`)
- Slice 2 (update check): #562 (`pulp upgrade --check-only`)
- Slice 3 (migration index): #571 (`pulp upgrade --notes --json`)
- Slice 4 (this skill): #549
- Slice 6 (plugin ↔ CLI skew): #551 (`min_cli_version` +
`tools/scripts/cli_version_check.sh` + `plugin_min_cli` JSON field)
Scanned 6/11/2026
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!