| name | update-xt |
| description | Update an xtrm-initialized project to match the current canonical install state, and run one-time migrations to the global scope. Use this skill whenever the user asks to update, upgrade, repair, or re-sync xtrm in a project — or when they say "xt is out of date", "skills aren't loading", "hooks aren't firing", "the install looks wrong", or "I just pulled new xtrm changes". Also covers `xt migrate` (move per-repo skills/hooks into the global tree), `xt migrate --restore`, the source-repo guard, and repairing broken `local-legacy` packs. Triggers when the agent detects stale paths like .claude/skills → active/claude or .pi/settings.json pointing to active/pi. Proactively suggest after any xtrm-tools upgrade.
|
| disable-model-invocation | true |
update-xt
Reconcile project's xtrm installation against canonical state. Detect drift, apply targeted fixes, verify wiring.
Upgrade note
This release retires active/. xt update --apply reconciles .claude/skills/ and .pi/skills/ automatically.
xtrm-tools ≥ 0.10.5: xt migrate and xt update --apply now auto-stage their tracked mods+deletions (restricted to .xtrm/, .claude/, .pi/, .githooks/ — user's unrelated in-flight work is left alone) and untrack known runtime paths from the index. Never commits. Operator finalizes with a single git commit. Fresh xt init also seeds a canonical .gitignore block covering runtime state (state.json, worktrees, .pi/skills, .xtrm/cache, .xtrm/statusline-claim). Result: fleet events never leave the ~300-file dirty state that pre-0.10.5 migrations did.
xtrm-tools ≥ 0.10.5: ~/.xtrm/config/hooks.json self-heals via content-fingerprint drift detection (not version-string equality). A stale worktree source can no longer downgrade the global hook payload silently — bootstrap refreshes on any content change even if the version stamp matches.
Canonical State (current)
Skills wiring
| Check | Expected value |
|---|
~/.claude/skills symlink target | ~/.xtrm/skills/default |
~/.pi/agent/skills symlink target | ~/.xtrm/skills/default |
project .claude/skills | Real directory with managed per-skill symlinks |
project .pi/skills | Real directory with managed per-skill symlinks |
project .pi/settings.json .skills array | User entries only |
Hooks wiring
| Check | Expected value |
|---|
.claude/settings.json or ~/.claude/settings.json | Has hooks block with commands containing /.xtrm/hooks/ paths |
| Hooks events covered | At minimum: SessionStart, PreToolUse, PostToolUse, Stop |
Project bootstrap
| Check | Expected value |
|---|
.beads/ exists | Yes |
CLAUDE.md or AGENTS.md exists | Yes |
Update maintenance
| Check | Expected value |
|---|
| bd auto-stage patch | .beads/config.yaml has export.git-add: false |
| bd pre-commit shim | active pre-commit hook contains `git add -f .beads/issues.jsonl 2>/dev/null |
| Hook path handling | Honor core.hooksPath; bd v1.0.3 .beads/hooks/pre-commit is valid when the file exists |
xt update --all-repos | Dry-runs ~/dev + ~/projects; --apply patches and commits each changed repo |
| Dependency maintenance | xt init / xt update report bd + GitNexus installed/latest, run bd doctor, and refresh stale GitNexus indexes |
Detection
Run these in order. Report what passes and what drifts. The active/ directory is retired in v2 — .claude/skills/ and .pi/skills/ are now real directories populated with per-skill symlinks pointing at ~/.xtrm/skills/default/<name>/ (managed) or <repo>/.xtrm/skills/<pack>/<name>/ (project pack).
xt status
xt claude status
[ -d .claude/skills ] && [ ! -L .claude/skills ] || echo "DRIFT: .claude/skills missing or is a symlink"
[ -d .pi/skills ] && [ ! -L .pi/skills ] || echo "DRIFT: .pi/skills missing or is a symlink"
for f in .claude/skills/* .pi/skills/*; do
[ -L "$f" ] && [ -e "$f" ] || echo "DRIFT: $f not a valid symlink"
done
[ -e .xtrm/skills/active ] && echo "STALE: .xtrm/skills/active still present (retired in v2)"
[ -d .xtrm/skills/default ] && echo "V1 LAYOUT: run xt migrate skills --apply to move to v2 global-only"
[ -d .xtrm/skills/optional ] && echo "V1 LAYOUT: .xtrm/skills/optional still per-repo"
[ -d .xtrm/skills/user/packs ] && echo "V1 SHIM: user/packs/ nesting — v2 uses flat .xtrm/skills/<pack>/"
node -e "const s=require('./.pi/settings.json'); console.log(s.skills)" 2>/dev/null
readlink ~/.claude/skills
readlink ~/.pi/agent/skills
[ -d ~/.xtrm/skills/default ] &&
grep -n .beads/config.yaml 2>/dev/null ||
hp=$(git config --get core.hooksPath 2>/dev/null || )
[ -n ]; hook=; hook=;
grep -n 2>/dev/null ||
xt update --repo .
Implementation Self-Check
Do not trust the surface commands alone. Before claiming that xt init handles
drift correctly, verify the underlying implementation behavior in the CLI source.
Required checks:
| File | What to verify |
|---|
cli/src/core/drift.ts | Drift is classified by comparing installed user file hashes against registry hashes from the package payload |
cli/src/core/registry-scaffold.ts | Drifted files are reported and skipped by default unless force is enabled |
cli/src/commands/init.ts | xt init calls the registry install step with force: false |
What you must confirm from code before reporting success:
xt init does check for local drift between the user's .xtrm files and the
package payload that bootstrapped them.
- That check is hash-based for registry-managed
.xtrm files, not just a loose
status heuristic.
xt init -y is non-destructive for drifted .xtrm files by default. It
preserves local edits unless a separate force path is used.
If the implementation no longer matches those rules, stop and report the mismatch
instead of repeating this skill's older assumptions.
Remediation
Two commands cover almost all drift. Know which fixes what:
| Command | Fixes |
|---|
xt claude install | Hooks wiring only (settings.json hooks block) |
xt update --apply --repo . | Registry-managed assets, bd auto-stage patch, bd/GitNexus maintenance, Pi package assurance |
xt update --apply --all-repos | Standard local fleet sweep (~/dev + ~/projects), with per-repo commits for changed repos |
xt init -y | Full bootstrap: hooks, runtime skill dirs (.claude/skills, .pi/skills), Pi settings, all phases |
xt migrate skills --apply --repo . | v1→v2 move: removes per-repo .xtrm/skills/{default,optional}, preserves genuine local overrides in local-legacy |
xt migrate hooks --apply --repo . | v1→v2 move for hooks (~/.xtrm/hooks) |
xt migrate --restore <backup.tgz> --apply --repo . | Undo a migrate from the tarball at ~/.xtrm/migration-backups/ |
Fix: Runtime skill dir missing or drifted (v2)
.claude/skills/ and .pi/skills/ are populated by xt update --apply/xt init and reconciled against state.json.managedLinks. If a repo is on v2 already, xt update --apply --repo . self-heals dangling symlinks (memory xtrm-4cqxc).
xt update --apply --repo .
Fix: Repo still on v1 layout (.xtrm/skills/default present)
v1→v2 is a one-shot migration. Sequence:
xt update --apply --repo .
xt migrate skills --dry-run --repo .
xt migrate skills --apply --yes --repo .
After migrate: .xtrm/skills/default and .xtrm/skills/optional are removed; only project packs remain under .xtrm/skills/<pack>/. Runtime views (.claude/skills, .pi/skills) are rebuilt automatically by the migrator using ~/.xtrm/skills/default as the canonical source.
Fix: local-legacy pack has legitimate overrides but many false-positives
xtrm-tools ≥ 0.10.4: fixed. walkDir in the migrator now excludes __pycache__, .pytest_cache, .serena, .mypy_cache, .ruff_cache, node_modules, .venv, workspace/, .pyc, .pyo. New migrations produce minimal local-legacy.
For repos migrated on ≤ 0.10.3 the legacy dir may still carry pyc noise. Clean before re-migrating:
find .xtrm/skills/default -type d -name __pycache__ -exec rm -rf {} +
find .xtrm/skills/local-legacy -type d -name __pycache__ -exec rm -rf {} +
xt migrate skills --dry-run --repo .
Fix: Hooks not wired
xt claude install
Rewires from .xtrm/config/hooks.json into .claude/settings.json.
Fix: Pi settings stale path
Covered by xt init -y. If you need to target it alone:
xt pi install
Fix: beads not initialized
bd init
Fix: bd auto-stage patch missing
Covered by xt update --apply --repo . and xt init -y. The patch keeps bd writes quiet during normal work while still staging the latest JSONL snapshot during commits.
xt update --apply --repo .
Expected end state:
.beads/config.yaml contains export.git-add: false
- the active pre-commit hook contains
git add -f .beads/issues.jsonl 2>/dev/null || true
- if
core.hooksPath=.beads/hooks and .beads/hooks/pre-commit exists, treat it as valid bd v1.0.3 behavior — do not rewrite it just because it is under .beads/
If updating xtrm-tools itself (not a consumer project)
After merging changes to cli/src/, the dist must be rebuilt before xt picks up
the new logic. Skipping this causes verification to report stale errors even after
xt init runs.
cd cli && npm run build
xt init -y
Worktree caveat: npm run build from inside .xtrm/worktrees/<name>/cli/ is blocked by a guard script — building from a worktree contaminates dist with worktree-specific absolute paths. If you're working in a worktree, build from a detached worktree outside .xtrm/:
git worktree add --detach /tmp/xt-build HEAD
cd /tmp/xt-build/cli && npm ci && npm run build
cp dist/index.cjs <worktree-root>/cli/dist/index.cjs
cp dist/index.cjs.map <worktree-root>/cli/dist/index.cjs.map
git worktree remove /tmp/xt-build --force
Verification
After all fixes, confirm canonical state is restored:
xt claude status
xt status
[ -d .claude/skills ] && [ ! -L .claude/skills ] && echo "OK: .claude/skills real dir"
[ -d .pi/skills ] && [ ! -L .pi/skills ] && echo "OK: .pi/skills real dir"
readlink .claude/skills/using-xtrm 2>/dev/null | grep -q '/.xtrm/skills/default/' && echo "OK: managed symlink"
Also restate the implementation-level conclusion in your report:
xt init verified drift against package registry hashes
- local drifted
.xtrm files were preserved by default
- no forced overwrite path was used unless explicitly requested
If xt status still shows drift after targeted fixes, run the full sync:
xt init
Multi-Repo Sweep (Fleet Update)
For updating many repos at once after an xtrm-tools upgrade — much lighter than
running xt init -y per repo. The right pattern when you've just rebuilt xtrm-tools
locally or pulled a new tag.
Dry-run discovery first
xt update --all-repos
xt update --root ~/dev
xt update --root ~/projects/<your-fleet>
Output classifies each discovered repo by .xtrm/ state:
| Status | Meaning | Action |
|---|
refreshed | .xtrm/registry.json present; drift vs current package detected | --apply will reinstall managed assets |
already-current | .xtrm/registry.json present; no drift | no action |
incomplete | .xtrm/ directory exists but .xtrm/registry.json is missing | xt init -y now seeds registry.json automatically (xtrm-ya2i, xtrm-tools ≥ 0.7.18). Older .xtrm/ dirs created before that fix still need the recipe below. |
failed | Hard error during drift check or install | inspect reason — common: PACK metadata drift, missing source files, fs-extra refusing to copy onto a symlink |
Transient worktree paths under .worktrees/ (specialists) or .xtrm/worktrees/
(xt claude / xt pi) are skipped automatically — they're not real repos to
refresh.
Apply
xt update --apply --all-repos
xt update --apply --root ~/dev
xt update --apply --root ~/projects/<your-fleet>
What --apply does for each managed repo:
- Runs the install flow with
force=true — refreshes .xtrm/config, .xtrm/hooks, .xtrm/skills/default (mirror), .pi/settings.json, .mcp.json.
- Writes
dolt.shared-server: true into .beads/config.yaml if not already set (so the worktree's bd routes to the shared dolt server instead of spawning per-worktree subprocesses).
- Applies the bd auto-stage patch:
export.git-add: false plus a pre-commit JSONL stage shim outside bd-managed markers.
- Reports bd/GitNexus maintenance; on apply, attempts safe non-major CLI upgrades, runs
bd doctor --fix --yes, and refreshes stale GitNexus indexes.
- Globally installs any missing xt-managed Pi packages.
- Does NOT touch
incomplete repos (deliberate — auto-fix would be destructive).
- With
--all-repos --apply, commits each changed repo independently as chore: apply bd auto-stage patch (xtrm-tools auto-applied).
Bootstrapping incomplete repos
Two scenarios:
A. The repo legitimately needs full xtrm management:
cd <repo>
xt init -y
xt update --apply --repo .
xt init -y now snapshots .xtrm/registry.json from the installed xtrm-tools package automatically (xtrm-ya2i). The previous manual cp /path/to/xtrm-tools/.xtrm/registry.json .xtrm/ step is no longer needed on xtrm-tools ≥ 0.7.18. If you're on an older version (or the registry is missing for some other reason), fall back to:
cp "$(npm root -g)/xtrm-tools/.xtrm/registry.json" .xtrm/
B. The repo is intentionally not xtrm-managed. Leave the .xtrm/ partial dir
alone; incomplete is just a status row, not an error. If you want it to stop
appearing, remove the orphaned .xtrm/ directory.
When a repo fails
Common failure modes and fixes:
| Error | Cause | Fix |
|---|
Source and destination must not be the same | npm link'd xtrm-tools + repo has symlinked .xtrm/skills/default → xtrm-tools (link chain collapses to same canonical path) | Functionally fine — repo is already in sync via the live symlinks, not a real failure. If you want to fully decouple the project from the dev tree, follow the migration recipe below. |
PACK_METADATA_MISMATCH: metadata-only: X, filesystem-only: Y | Either a user pack renamed a skill without updating PACK.json, or a pre-fix xt migrate created an invalid local-legacy pack (wrong name and/or skills[] listing dirs that lack SKILL.md) | For a hand-authored user pack: edit PACK.json so the listed skill names match the directory names, re-run. For local-legacy: see the repair recipe under "Migration to global scope" below. |
Cannot read properties of null (reading 'dolt') | Repo's .beads/config.yaml is comments-only (fresh bd init default); pre-xtrm-16ec xtrm crashes parsing it | Upgrade xtrm-tools to ≥ 0.7.18; the parse result is coerced to {} defensively now. |
Migration to global scope (xt migrate)
Move per-repo .xtrm/skills and .xtrm/hooks payloads into the global tree
(~/.xtrm/skills, ~/.xtrm/hooks) so consumer repos only carry pointers.
This is a one-time destructive operation per repo; every run creates a
tarball backup at ~/.xtrm/migration-backups/.
Feature-flag gates
XTRM_GLOBAL_SKILLS=1 — opts into the global-skills model at runtime.
XTRM_GLOBAL_HOOKS=1 — opts into the global-hooks model at runtime.
xt migrate runs without the flags but warns that migration "may be
premature" — global runtime resolution still needs the flag to activate.
Commands
xt migrate skills --dry-run --repo <path>
xt migrate skills --apply --yes --repo <path>
xt migrate hooks --apply --yes --repo <path>
xt migrate all --apply --yes --repo <path>
xt migrate --restore <backup.tgz> --apply --repo <path>
xt migrate --restore <backup.tgz> --apply --force --repo <path>
Restoring a hooks-*.tgz also rewrites .claude/settings.json and
.pi/agent/settings.json from the sidecar <tgz>.settings.json that
xt migrate hooks --apply wrote alongside the tarball — no separate
settings restore is needed.
What xt migrate skills --apply does
- SHA-256 verifies each file under
.xtrm/skills/{default,optional} against
the corresponding file under ~/.xtrm/skills.
- Files that differ or are absent globally are copied into
.xtrm/skills/user/packs/local-legacy/ with their full nested path
preserved (default/foo/SKILL.md → local-legacy/foo/SKILL.md).
Top-level source-pack PACK.json files are skipped so they never
overwrite the authoritative one.
- Writes an authoritative
local-legacy/PACK.json with
name: "local-legacy", schemaVersion: "1", and skills[] filtered to
only those top-level dirs that actually contain a SKILL.md.
- Tarballs the whole
.xtrm/skills/ tree to
~/.xtrm/migration-backups/skills-<repo>-<ts>.tgz.
- Removes
.xtrm/skills/default and .xtrm/skills/optional. Leaves
active/, user/, state.json alone.
- Records the migration in
~/.xtrm/known-repos.json and appends
skills.migrate.ok (or skills.migrate.diverged) events to
~/.xtrm/logs/skills-migration.jsonl.
xt migrate hooks --apply is the same shape targeting .xtrm/hooks/ plus
the settings.json sidecar.
Source-repo guard
xt migrate --apply refuses to run on the xtrm-tools source repo itself:
package.json.name === 'xtrm-tools', OR
scripts/gen-registry.mjs exists, OR
scripts/vendor-specialists-skills.mjs exists
Failure mode is exit 1 with an explicit refusal message naming the marker.
Dry-run is always permitted. Maintainer escape hatch: --force-source
(undocumented; do not use on the real source tree — you will corrupt the
canonical payload).
Ordering caveat: restore only reverses migrate
migrate → restore is a clean roundtrip: the tarball snapshots the
pre-migrate .xtrm/skills/ and extracts it back verbatim.
migrate → xt update --apply → restore is not clean: the update
between the two operations refreshes canonical default/ with the current
package payload, so a subsequent restore drops the older tarball on top
and leaves a mixed working tree. If you need to fully revert after
running an update, use git checkout -- .xtrm/skills && git clean -fd .xtrm/skills instead of --restore.
Repairing a broken local-legacy pack
Repos migrated by an older xt migrate may have a local-legacy/PACK.json
with the source pack's name (e.g. "xt-optional"), a flattened tree
(all files collapsed to the pack root, only the last of each name
surviving), and/or a skills[] listing dirs that lack a SKILL.md.
Symptom: xt update --apply fails with PACK_METADATA_MISMATCH or
"Invalid pack metadata: name must match directory 'local-legacy'".
Surgical repair (no re-migrate needed):
LEGACY=<repo>/.xtrm/skills/user/packs/local-legacy
SKILLS=$(for d in "$LEGACY"/*/; do
n=$(basename "$d")
[ -f "$d/SKILL.md" ] && echo "$n"
done | jq -R . | jq -s .)
jq --argjson skills "$SKILLS" \
'.name = "local-legacy"
| .schemaVersion = "1"
| .version = (.version // "0.0.0")
| .skills = $skills' \
"$LEGACY/PACK.json" > "$LEGACY/PACK.json.new"
mv "$LEGACY/PACK.json.new" "$LEGACY/PACK.json"
xt update --repo <repo>
New migrations (xtrm-tools ≥ this release) author the pack correctly on
first run; the recipe above is only for pre-fix state on the disk.
Migrating a dev-linked project to a real consumer install
A project ends up with .xtrm/skills/default (or another .xtrm/ asset) as a symlink back to the dev tree when:
- xtrm-tools was
npm link-ed globally (/home/<user>/.nvm/.../node_modules/xtrm-tools → /home/<user>/dev/xtrm-tools/), AND
- the project's
.xtrm/skills/default was manually replaced with a symlink to the npm-global path (common dev-loop shortcut so skill edits propagate instantly).
installFromRegistry's scaffoldSkillsDefaultFromPackage has an intentional branch (registry-scaffold.ts:104): "if target is a symlink whose realpath equals the package realpath → noop". This preserves the dev symlink on every xt update. The arrangement is functional but the project is invisibly coupled to whatever lives in the dev tree (or whatever the global npm path points to).
When to migrate
- Before publishing a consumer-facing release of the dependent project.
- Before handing the project to another developer / machine.
- When you want
xt update --apply to actually write files into the project rather than no-op.
Detection
readlink <repo>/.xtrm/skills/default
Recipe
cd <repo>
rm .xtrm/skills/default
xt init -y
git rm --cached .xtrm/skills/default 2>/dev/null
git add .xtrm/skills/default
git commit -m "chore: replace dev symlink with real xtrm skills payload"
find .xtrm -type l -lname '/*' -o -type l ! -lname '../*' -a ! -lname './*'
What npm install -g xtrm-tools alone does
Replacing the npm link with a real npm install (npm install -g xtrm-tools) breaks the dev-tree coupling — the global path becomes real files at the published version — but it does not remove the project's symlink. The symlink still points at the global npm path, which now resolves to immutable published files. The project keeps working but stays pinned to the npm-installed version forever, and .xtrm/skills/default remains a symlink on disk.
To get true isolation (real files inside <repo>/.xtrm/skills/default/), the recipe above is still required.
Worktree hygiene: .beads/ and core.hooksPath
Modern bd 1.0.3 stores core.hooksPath as an absolute parent path at bd init
time (e.g. $HOME/repo/.beads/hooks), so worktrees inherit parent hooks via
shared git config — no on-disk .beads/ is needed inside a worktree. Since
xtrm-cbjo (xtrm-tools commit 937b151) and unitAI-yvqmf (specialists commit
986bc8e4), xt claude / xt pi / sp run worktrees do not create a
.beads/ symlink; they rm -rf <worktree>/.beads and git update-index --skip-worktree -- on tracked .beads/* paths. This eliminates the
squash-merge .beads-wipe hazard documented in projects/infra PR #39.
Audit your core.hooksPath once (xtrm-2s44)
If your bd was installed before 1.0.3, core.hooksPath may be the relative
string .beads/hooks, which would resolve against a worktree's cwd — i.e.,
the (now-missing) worktree-local .beads/hooks/. To survey:
for r in ~/dev/*/ ~/projects/*/*/; do
[ -d "$r/.git" ] && [ -d "$r/.beads" ] || continue
hp=$(git -C "$r" config core.hooksPath 2>/dev/null || echo "<unset>")
case "$hp" in
/*) cat="ABSOLUTE" ;;
"<unset>") cat="UNSET" ;;
.beads/hooks) cat="RELATIVE-BD <- needs fix" ;;
*) cat="OTHER (project .githooks chain — leave alone)" ;;
esac
printf "%-50s %s\n" "${r#$HOME/} $cat" "$hp"
done
Classification:
A fleet survey across ~/dev + ~/projects/<your-fleet> returned 0 repos
needing the fix. The safety net in launchWorktreeSession /
provisionWorktree (normalizeParentHooksPath) auto-rewrites on next worktree
creation if a relative .beads/hooks ever does appear, so the survey is mostly
defensive.
Worktree-internal artifact inventory (xtrm-x80f)
A worktree is a partial clone with extras: bd metadata, npm caches, runtime
state, per-worktree settings. None of these belong on a chain branch — but
the moment any of them get staged via git add -A or a checkpoint commit,
they can ride a PR into main. The matrix below documents what is protected
by which mechanism. Audit it whenever you add a new per-worktree artifact.
| Artifact | Source | Mechanism in a worktree | Status |
|---|
.beads/* | bd tracked dir | rm + skip-worktree (xtrm-cbjo) | ✅ |
.beads-credential-key, .beads/dolt-monitor.pid, .beads/dolt-server.activity | bd runtime | gitignored at parent | ✅ |
.pi/npm/ | npm cache | gitignored + symlink to parent | ✅ |
.pi/extensions/ | pi runtime | gitignored under .xtrm/extensions/**/.pi/ | ✅ |
.specialists/default | (xtrm-tools: untracked) | symlink to parent in worktree | ✅ |
.specialists/user | tracked (.json overrides) | symlink to parent in worktree | ⚠️ merge-hazard candidate, tracked at follow-up bead |
.specialists/{jobs,ready,trace.jsonl,db/*} | runtime state | gitignored at parent | ✅ |
.claude/skills | install symlink | gitignored | ✅ |
.claude/settings.local.json | per-worktree write (launchWorktreeSession) | gitignored (user-global + project) | ✅ |
.claude/worktrees/, .claude/tdd-guard/data/ | runtime | gitignored | ✅ |
.xtrm/worktrees/, .xtrm/skills/active/, .xtrm/session-meta.json, .xtrm/statusline-claim, .xtrm/debug.db | runtime | gitignored | ✅ |
AGENTS.md, CLAUDE.md | tracked | gitnexus stat-counter scrubbed (xtrm-c6sf), build-gate prevents reintroduction | ✅ |
pnpm-workspace.yaml, cli/pnpm-workspace.yaml |
The remaining ⚠️ is .specialists/user/*.json: the symlink swap in
ensureWorktreeSpecialists has the same shape as the pre-fix .beads
problem — a chain-branch checkpoint could capture the dir→symlink delta and
squash-merge would wipe the parent's .specialists/user/. Lower urgency
than .beads (smaller blast radius, files are intentional overrides) but
worth resolving with the same skip-worktree pattern when convenient.
The defense-in-depth pre-push guard in xt end
(findBeadsSymlinkIntroductions) currently only checks .beads/*. Extend
to .specialists/* if/when the symlink swap there becomes the next chain
of work.
Pre-Release Validation Methodology
Before publishing a new xtrm-tools version, validate the operator-facing CLI locally
against every consumer repo. This is the procedure that surfaced two release-blockers
in 2026-05-12 alone (xtrm-16ec yaml-null crash, xtrm-ny61 worktree over-discovery).
Procedure
cd /path/to/xtrm-tools && npm run build --workspace cli
npm link
xt update --all-repos
xt update --root ~/dev
xt update --root ~/projects/<your-fleet>
xt update --apply --all-repos
xt update --apply --root ~/dev
xt update --apply --root ~/projects/<your-fleet>
Why this beats publishing first and patching later
- A published
0.7.X that crashes on a default-config consumer repo wastes a
version number — users see "upgrade and immediately break" and lose trust.
- Bugs that only manifest on real consumer state (comments-only YAML, transient
worktrees, drifted PACK metadata) are invisible from xtrm-tools' own test
suite — only a real sweep catches them.
npm link flips between local-source-globally and published-version-globally
in seconds (npm unlink reverts), so the validation cost is minimal.
Watch-fors during the sweep
- Pi packages shown as
missing when npm ls -g confirms them installed —
detection bug, filed at xtrm-ntf8. Not a real problem; packages work.
- xtrm-tools itself appearing as
failed with "Source and destination..." —
expected when xtrm-tools is npm-linked into itself; not a release blocker.
Reporting to the user
After completing detection + remediation + verification, give the user a concise
summary:
## xtrm update complete
✓ .claude/skills — real dir, N per-skill symlinks (all valid)
✓ .pi/skills — real dir, N per-skill symlinks (all valid)
✓ Global source: ~/.claude/skills → ~/.xtrm/skills/default
✓ v1 residuals: none (.xtrm/skills/active, default/, optional/ absent post-migrate)
✓ Hooks wired (X events, Y commands)
✓ .pi/settings.json skills entry: current
[Any items that could not be auto-fixed, with manual instructions]
If anything could not be fixed automatically (e.g. missing .pi/settings.json,
no beads config), explain the manual step clearly — don't just report failure.