This is the v0.X.Y → v0.X.Y+1 (or v0.X+1.0) checklist. Captures the sequence that shipped v0.1.0 so the next release doesn't re-discover it.
The OMC release skill (/oh-my-claudecode:release) handles the generic
ordering — this doc is the agent-tree-specific overlay (project-aware steps,
post-publish smoke test, MCP plugin re-install).
- Logged in to npm as
seungwoolee:npm whoami - Logged in to GitHub via
gh:gh auth status - On
mainbranch with no untracked / uncommitted changes - Working directory at repo root
- npm Granular Access Token with "Bypass 2FA when publishing" enabled —
one-time setup at https://www.npmjs.com/settings/seungwoolee/tokens →
Generate New Token → Granular Access Token → check "Bypass two-factor
authentication when publishing", grant Read+Write on
@seungwoolee/agent-tree, thennpm config set //registry.npmjs.org/:_authToken=npm_XXXXX. Without this, subprocess publish fails:--auth-type=websilent-fails (no stdin/browser orchestration in agent-driven shells), classic-OTP path requires interactive stdin. Token expires per its TTL — re-issue then.
Five sources of truth (six fields) must move together:
package.json→"version": "0.X.Y".claude-plugin/plugin.json→"version": "0.X.Y".claude-plugin/marketplace.json→ bothmetadata.versionANDplugins[0].version→"0.X.Y"src/mcp/server.ts→version: '0.X.Y'in theMcpServer({...})blockskills/agent-tree/SKILL.md→ frontmatterversion: 0.X.Y
Why: the plugin/skill/MCP/marketplace version surfaces in Claude Code's plugin registry — drift causes confusion about which version is loaded. esbuild bakes
package.json#versionintodist/cli.jsvia__PKG_VERSION__, soagent-tree --versionalways matchespackage.jsonafter build.Quick sanity grep before committing:
grep -nE '"version": "0\.[0-9]+\.[0-9]+"' package.json .claude-plugin/*.json grep -nE "version: '0\.[0-9]+\.[0-9]+'" src/mcp/server.ts grep -nE '^version: 0\.[0-9]+\.[0-9]+' skills/agent-tree/SKILL.md # All six occurrences must show the same 0.X.Y.
- Move
## [Unreleased]content →## [v0.X.Y] — YYYY-MM-DD - Add a fresh empty
## [Unreleased]at the top
npm run lint && npm run typecheck && npm test && npm run buildAlready enforced by
prepublishOnly, but run manually first so you can see test output without the publish progress bar fighting for the terminal.dist/ is committed:
dist/*.jsis tracked in git (source-maps are ignored) so thatclaude plugin marketplace add github:lifrary/agent-tree→installworks out of the box.git add -Ain the next step will pick up any regenerated bundle.
git checkout dev
git add -A # safe here — package.json, CHANGELOG, manifests
git commit -m "release: vX.Y.Z"
git push origin dev
git checkout main
git merge --ff-only dev
git tag -a vX.Y.Z -m "Release vX.Y.Z"
git push origin main
git push origin vX.Y.Z# Extracts the just-promoted [vX.Y.Z] section from CHANGELOG
gh release create vX.Y.Z --title "vX.Y.Z" \
--notes-file <(awk -v v="vX.Y.Z" \
'/^## \['v'\]/{flag=1;next} /^## \[/{flag=0} flag' CHANGELOG.md)With a Granular Access Token + 2FA-bypass configured (Prerequisites above), publish proceeds straight through:
npm publish --access publicWithout bypass token, manual OTP fallback (interactive shell only):
npm publish --access public --otp=NNNNNN
prepublishOnlyre-runs lint+typecheck+test+build automatically. If it fails, the publish is aborted before any registry write.Re-trying after an auth fix: skip the second
prepublishOnlywithnpm publish --access public --ignore-scriptswhen lint+typecheck+test+build already passed in the same shell session — esbuild bakespackage.json#versionintodist/cli.jsat build time, so oncedist/matches the bumped version, re-running scripts is wasted ceremony. Validated v0.1.2 publish (2026-04-25).
# In a clean tmp dir — no chance of resolving local node_modules
cd /tmp && rm -rf agent-tree-smoke && mkdir agent-tree-smoke && cd agent-tree-smoke
npm init -y >/dev/null
npm install @seungwoolee/agent-tree
./node_modules/.bin/agent-tree --version # must equal vX.Y.Z
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
| node ./node_modules/@seungwoolee/agent-tree/dist/mcp-server.js \
| grep -oE '"name":"agent_tree_[a-z]+"' | sort -u
# Must list all 5 agent_tree_* tools~/.claude/plugins/local/ is not auto-scanned by Claude Code — a symlink
or copy there does not register the plugin's MCP server or skill. The
CLI-sanctioned install path writes to installed_plugins.json and
settings.json#enabledPlugins, and that registry entry is what triggers the
MCP spawn at session start.
Maintainer (you) — after publishing a new version:
# `claude plugin install` is idempotent when the plugin is already
# registered — it reports "already installed" and does NOT overwrite the
# cache. Must uninstall first to force a fresh copy of the new build.
claude plugin uninstall agent-tree@agent-tree
claude plugin install agent-tree@agent-tree # writes cache/.../<new-version>/Then restart Claude Code.
Bootstrapping a fresh machine (you or external user):
# If using the local repo clone as the marketplace source:
cd <where-you-cloned>
npm install && npm run build # dist/ is gitignored; must exist before install
claude plugin marketplace add "$PWD"
claude plugin install agent-tree@agent-tree
# → Restart Claude CodeExternal users — github-URL marketplace (from v0.1.1 onward):
claude plugin marketplace add github:lifrary/agent-tree
claude plugin install agent-tree@agent-tree
# → Restart Claude CodeThis works because dist/*.js is now committed (.gitignore exempts it);
git clone pulls the built bundle along with the manifests.
- Branch from
main(notdev) — patches must be linear over the released tag - Fix + test on the hotfix branch
- Merge into both
mainanddev - Tag and publish from
main
-
npm publishcannot be undone after 24 hours. If you ship a broken build, bump to vX.Y.Z+1 and republish — nevernpm unpublishan aged version. -
The
prepublishOnlyscript runsnpm run buildand overwritesdist/. This is intentional — it guarantees the published tarball matches the source onmain. Don't disable it. -
GitHub Release notes are best generated from CHANGELOG, not free-form, so the registry / GitHub / repo all tell the same story.
-
macOS / Linux only for the smoke test in step 7. Windows users would need different shell syntax for the JSON-RPC pipe.
-
Tag-push vs
npm publishordering: the canonical sequence in Step 4–6 pushesvX.Y.Ztag beforenpm publish. Ifnpm publishfails at auth (silently-expired.npmrctoken →E401), you're stuck with a live tag pointing to a version the registry doesn't have — fixing requires delete-tag-and-re-tag or a version bump. Defensive alternative proven on v0.1.1 and re-validated on v0.1.2: runnpm publishbeforegit push origin vX.Y.Z(the release commit can still push to main first so CI sees it; only the tag-push waits). Alwaysnpm whoamias a publish preflight — the canonical token-failure mode only surfaces at publish time. -
Subprocess
npm publish --auth-type=websilent-fails: When run inside an agent-driven shell (no interactive stdin / no automated browser orchestration),--auth-type=webexits without performing the auth handshake → publish falls back to anonymous → npm hides "permission denied" behind404 Not Foundon scoped packages (privacy feature, not a missing-package signal). The fix is the bypass-token path in Prerequisites. Discovered v0.1.2 (2026-04-25) when scheduled publish hung on Day 1 (OTP) and silently 404'd on Day 2 (web auth in subprocess). See.claude-sessions/2026-04-24-23-09-v0.1.2-publish-pause.md. -
Granular Access Token defaults to 2FA-required: A freshly issued granular token works for
npm whoamiand read operations immediately, but publish still throwsEOTPuntil you re-issue with the "Bypass two-factor authentication when publishing" checkbox enabled. Easy to miss during initial token setup; the checkbox is on the same form as packages/scopes/permissions, not a separate page. -
Folder-rename hazards (paired-mv discipline, exit-first ordering): Renaming the working tree (
mv ~/Code/<old> ~/Code/<new>) without a pairedmvof the matching Claude Code session storage (mv ~/.claude/projects/-Users-...-<old> -Users-...-<new>) makes the cwd→encoded-path lookup insrc/utils/session_path.tsreturn zero — the tool goes blind to its own JSONL history. Four asymmetries that bite:- inode/FD vs absolute-path resolution: open file descriptors
follow the inode through
mv, but Claude Code's hook payloads (transcript_path, subprocesscwd, permission rules insettings.local.json) hold absolute paths and re-resolve on every use. Mid-sessionmvkeeps the existing FD writing fine but breaks every fresh path resolution → silent permission-prompt regressions and stale-path subprocess respawns until restart. - Paired
~/.claude/projects/mv is mandatory: encoded-cwd rule is/→-, so~/Code/agent-treeresolves to directory-Users-seungwoolee-Code-agent-tree. If only the working tree is renamed, JSONL files become unreachable to both Claude Code (which writes to a fresh dir under the new encoded name) and agent-tree (which reads from the encoded-of-cwd dir). - Encoded-path slash→hyphen rule: a single
/becomes a single-with no escaping, so any literal-already in a folder name can collide with a sibling whose/is at that position. Avoid-in folder names if you anticipate future renames. - Exit-first ordering: do the
mvpost-/exitonly, never mid-session. Mid-sessionmvcorrupts hook permission rules, triggers MCP-server respawn at stale paths, and leaves the agent in a broken state until full restart. The user-side ceremony is:/exit→mvworking tree → pairedmv~/.claude/projects/dir →cd <new>→claude. See.claude-sessions/2026-04-25-18-57-folder-rename-decision.mdfor the full incident log.
- inode/FD vs absolute-path resolution: open file descriptors
follow the inode through
-
Plugin MCP spawns from marketplace source, not cache (verified 2026-04-25 with three independent signals):
ps -ef: argv showsnode <source-cwd>//dist/mcp-server.js— note the doubled slash from${CLAUDE_PLUGIN_ROOT}/+/dist/....lsof -p <pid>: processcwd DIRresolves to the source tree (/Users/seungwoolee/Code/agent-tree) — not~/.claude/plugins/cache/agent-tree/agent-tree/<version>/.- Cache vs source byte-identity at install time:
diff -qondist/mcp-server.jsshows identical content with matching mtime immediately afterclaude plugin install, but cache stays put when source is rebuilt. Combined with signal 1+2, this means the runtime resolves to source — and the cache only matters as an artifact ofclaude plugin install's bookkeeping, not as the loaded bundle. Implication: in-session MCP behavior reflects current source, not the cached snapshot — editingdist/mcp-server.jsin the source tree mid-session affects the next Claude Code restart even without a re-install. Cache file-count + CLI--versionchecks (/pre-publish-audit) are necessary but not sufficient to prove the in-session MCP runtime loads the published code; only a/tmpclean-dir install of the published tarball (/mcp-smoke) exercises the registry-served bundle end-to-end. Stale dev-path MCP processes also survive folder rename —ps -efafter a rename shows entries pointing at the old (now-missing) directory until each spawning Claude Code session is restarted (paired-mv discipline above does not help here; only restart does).
Marketplace type controls the spawn path. Verified by inspecting
~/.claude/settings.json#extraKnownMarketplacesand cross-referencing withps -effor multiple installed plugins:directorysource (this repo's setup;claude plugin marketplace add "$PWD"writes{"source":"directory","path":"<abs cwd>"}) →${CLAUDE_PLUGIN_ROOT}resolves to that registered path, so MCP spawns from source. Cache exists but is not consumed at runtime.githubsource (e.g.,oh-my-claudecode@omcregistered viaclaude plugin marketplace add github:...) →${CLAUDE_PLUGIN_ROOT}resolves to the cache path (~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/), so MCP spawns from cache. Source path doesn't exist locally for this case.- Practical consequence: this repo's
directoryregistration means a folder rename ormvof the source tree breaks the plugin immediately for the next Claude Code session, because theextraKnownMarketplaces.<name>.source.pathis an absolute string and does not auto-update. Either re-runclaude plugin marketplace add "$NEW_PWD"after rename, or hand-edit~/.claude/settings.json#extraKnownMarketplaces.agent-tree.source.path.