name: update-xt description: > Update an xtrm-initialized project to match the current canonical install state. Use this skill whenever the user asks to update, upgrade, repair, or re-sync xtrm in a project — or when they say something like "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 triggers when the agent detects stale paths like .claude/skills → active/claude (old structure) or .pi/settings.json pointing to active/pi (old structure). Proactively suggest running this skill after any xtrm-tools upgrade.
update-xt
Reconcile a project's xtrm installation against the current canonical state. Detect drift, apply targeted fixes, verify everything is wired correctly.
Canonical State (current)
This is what a correctly installed project looks like. Check each item.
Skills wiring
| Check | Expected value |
|---|---|
.claude/skills symlink target |
../.xtrm/skills/active |
.xtrm/skills/active/ |
Flat directory of symlinks to ../default/<skill> |
active/pi/ subdirectory |
Must NOT exist (stale — old runtime split) |
active/claude/ subdirectory |
Must NOT exist (stale — old runtime split) |
.pi/settings.json .skills array |
Must include "../.xtrm/skills/active" (project-local, wins) |
.pi/settings.json .skills array |
Must include "~/.xtrm/skills/default" (user-level fallback — xtrm-4h6u) |
.pi/settings.json .skills array |
Must NOT include "../.xtrm/skills/active/pi" (old path) |
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.
# 1. High-level status — shows pending syncs
xt status
# 2. Claude hook wiring
xt claude status
# 3. Skills symlink
readlink .claude/skills
# Expected: ../.xtrm/skills/active
# Stale: ../.xtrm/skills/active/claude
# 4. Stale runtime subdirs (should return nothing)
ls .xtrm/skills/active/pi 2>/dev/null && echo "STALE: active/pi exists"
ls .xtrm/skills/active/claude 2>/dev/null && echo "STALE: active/claude exists"
# 5. Pi settings skills entries (both must be present since xtrm-4h6u)
node -e "const s=require('./.pi/settings.json'); console.log(s.skills)" 2>/dev/null
# Expected to include BOTH: ../.xtrm/skills/active AND ~/.xtrm/skills/default
# Stale if only first entry present, or if includes: ../.xtrm/skills/active/pi
# 6. Active view integrity (all entries must be valid symlinks)
for f in .xtrm/skills/active/*; do [ -L "$f" ] || echo "NOT A SYMLINK: $f"; done
# 7. bd auto-stage patch state (xtrm-h9hqg)
grep -n '^export\.git-add: false' .beads/config.yaml 2>/dev/null || echo "STALE: bd auto-stage config not disabled"
hp=$(git config --get core.hooksPath 2>/dev/null || true)
if [ -n "$hp" ]; then hook="$hp/pre-commit"; else hook=".git/hooks/pre-commit"; fi
grep -n 'git add -f .beads/issues.jsonl 2>/dev/null || true' "$hook" 2>/dev/null || echo "STALE: bd pre-commit stage shim missing"
# 8. update dry-run (includes dependency maintenance summary)
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 initdoes check for local drift between the user's.xtrmfiles and the package payload that bootstrapped them.- That check is hash-based for registry-managed
.xtrmfiles, not just a loose status heuristic. xt init -yis non-destructive for drifted.xtrmfiles 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 |
Skills symlink, active/ view rebuild, Pi settings, all phases |
Fix: Skills symlink stale or active/ view wrong
xt claude install does NOT rebuild skills. Only xt init does (Phase 6b).
xt init -y will repair missing/outdated registry-managed files, but it will
preserve locally drifted .xtrm files by default.
xt init -y
Fix: Stale active/pi or active/claude subdirs
xt init rebuilds active/ atomically — it does NOT remove old subdirs left over
from a previous layout. After xt init -y confirms the flat view is working, remove
the stale dirs manually:
rm -rf .xtrm/skills/active/pi
rm -rf .xtrm/skills/active/claude
Verify flat active/ is intact:
ls .xtrm/skills/active/
# Should show skill dirs directly (clean-code, deepwiki, ...) — NOT pi/ or claude/ subdirs
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.yamlcontainsexport.git-add: false- the active pre-commit hook contains
git add -f .beads/issues.jsonl 2>/dev/null || true - if
core.hooksPath=.beads/hooksand.beads/hooks/pre-commitexists, 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 # now runs with updated code
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
# Should show: ✓ Claude hooks wired
# Should show: ✓ claude CLI available
xt status
# Should show no pending changes (or only optional ones)
readlink .claude/skills
# Must output: ../.xtrm/skills/active
node -e "const s=require('./.pi/settings.json'); console.log(s.skills.includes('../.xtrm/skills/active'))" 2>/dev/null
# Must output: true
Also restate the implementation-level conclusion in your report:
xt initverified drift against package registry hashes- local drifted
.xtrmfiles 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 # standard local sweep: ~/dev + ~/projects
xt update --root ~/dev # walk one explicit tree
xt update --root ~/projects/mercury # walk another explicit tree
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/mercury
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: trueinto.beads/config.yamlif 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: falseplus 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
incompleterepos (deliberate — auto-fix would be destructive). - With
--all-repos --apply, commits each changed repo independently aschore: 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 # scaffolds .xtrm/{config,hooks,skills} AND seeds registry.json
xt update --apply --repo . # bring everything in sync (registry-driven)
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 |
A user-skill-pack (.xtrm/skills/user/packs/<name>/PACK.json) lists a skill that has been renamed on disk |
Edit PACK.json so the listed skill names match the directory names; re-run. |
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. |
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/defaultwas 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 --applyto actually write files into the project rather than no-op.
Detection
# Is .xtrm/skills/default a symlink, and where does it point?
readlink <repo>/.xtrm/skills/default
# If empty / not-a-symlink: nothing to migrate.
# If points anywhere outside <repo>/: needs migration.
Recipe
cd <repo>
# 1. Remove the symlink (does NOT touch the real files in the dev tree).
rm .xtrm/skills/default
# 2. Re-run init — copies real files from the installed xtrm-tools package
# into .xtrm/skills/default/ AND seeds .xtrm/registry.json (xtrm-ya2i).
xt init -y
# 3. If the symlink was committed (git ls-files showed it as mode 120000),
# flip the tracked entry to a real directory:
git rm --cached .xtrm/skills/default 2>/dev/null # ok if it was untracked
git add .xtrm/skills/default
git commit -m "chore: replace dev symlink with real xtrm skills payload"
# 4. Optional sanity: confirm no more symlinks point outside the repo.
find .xtrm -type l -lname '/*' -o -type l ! -lname '../*' -a ! -lname './*'
# Empty output means clean.
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:
ABSOLUTE— correct, no action.RELATIVE-BD(literal.beads/hooksor./.beads/hooks) — rewrite once:git -C <repo> config core.hooksPath "$(realpath <repo>/.beads/hooks)"OTHERlike.githooks— project-specific hook chain, leave alone. bd in these repos works via direct invocation (not git hooks), so worktree hygiene is unaffected.UNSET— no hooks wired anywhere; same outcome asOTHER.
Survey across ~/dev + ~/projects/mercury on 2026-05-12 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 |
generated by pnpm in an npm-workspaces repo when specialist tooling shells out to pnpm | gitignored (xtrm-ombq) | ✅ |
.gitnexus/ |
runtime | gitignored | ✅ |
.dolt/, *.db |
runtime | gitignored | ✅ |
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
# 1. Build dist from the local checkout
cd /path/to/xtrm-tools && npm run build --workspace cli
# 2. Link globally so `xt` runs local source
npm link
# 3. Sweep across all consumer trees (dry-run first)
xt update --all-repos
# or target explicit roots:
xt update --root ~/dev
xt update --root ~/projects/mercury
# 4. Identify failed/incomplete rows. Fix any real bugs in xtrm-tools FIRST,
# then re-build + re-link + re-sweep.
# 5. Once dry-run is clean, apply across the fleet:
xt update --apply --all-repos
# or target explicit roots:
xt update --apply --root ~/dev
xt update --apply --root ~/projects/mercury
# 6. Cut the public release only after the local apply succeeds end-to-end.
Why this beats publishing first and patching later
- A published
0.7.Xthat 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 linkflips between local-source-globally and published-version-globally in seconds (npm unlinkreverts), so the validation cost is minimal.
Watch-fors during the sweep
- Pi packages shown as
missingwhennpm ls -gconfirms them installed — detection bug, filed atxtrm-ntf8. Not a real problem; packages work. - xtrm-tools itself appearing as
failedwith "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 → ../.xtrm/skills/active
✓ active/ view: N skills (flat, all valid symlinks)
✓ active/pi and active/claude stale dirs: removed
✓ 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.