xtrm-tools 0.7.17 → 0.7.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.xtrm/config/hooks.json +2 -0
  2. package/.xtrm/config/instructions/agents-top.md +2 -1
  3. package/.xtrm/registry.json +429 -712
  4. package/.xtrm/skills/default/creating-service-skills/scripts/bootstrap.py +82 -156
  5. package/.xtrm/skills/default/creating-service-skills/scripts/scaffolder.py +73 -121
  6. package/.xtrm/skills/default/hook-development/references/patterns.md +1 -1
  7. package/.xtrm/skills/default/last30days/scripts/test-v1-vs-v2.sh +2 -2
  8. package/.xtrm/skills/default/planning/SKILL.md +75 -29
  9. package/.xtrm/skills/default/releasing/SKILL.md +163 -57
  10. package/.xtrm/skills/default/security-pipeline/SKILL.md +192 -0
  11. package/.xtrm/skills/default/security-pipeline/scripts/security-bootstrap.sh +294 -0
  12. package/.xtrm/skills/default/security-pipeline/templates/.githooks/pre-push.template +39 -0
  13. package/.xtrm/skills/default/security-pipeline/templates/.github/workflows/gitleaks.yml +33 -0
  14. package/.xtrm/skills/default/security-pipeline/templates/.github/workflows/osv-scanner.yml +33 -0
  15. package/.xtrm/skills/default/security-pipeline/templates/.github/workflows/semgrep.yml +41 -0
  16. package/.xtrm/skills/default/security-pipeline/templates/.gitleaks.toml +44 -0
  17. package/.xtrm/skills/default/security-pipeline/templates/.pre-commit-config.yaml +67 -0
  18. package/.xtrm/skills/default/security-pipeline/templates/.semgrepignore +46 -0
  19. package/.xtrm/skills/default/security-pipeline/templates/scripts/security-scan.sh +57 -0
  20. package/.xtrm/skills/default/security-pipeline/templates/scripts/semgrep-diff.sh +68 -0
  21. package/.xtrm/skills/default/session-close-report/SKILL.md +167 -6
  22. package/.xtrm/skills/default/sync-docs/SKILL.md +1 -1
  23. package/.xtrm/skills/default/update-xt/SKILL.md +270 -4
  24. package/.xtrm/skills/default/updating-service-skills/scripts/drift_detector.py +22 -0
  25. package/.xtrm/skills/default/using-script-specialists/SKILL.md +7 -5
  26. package/.xtrm/skills/default/using-specialists/SKILL.md +13 -12
  27. package/.xtrm/skills/default/using-specialists-auto/SKILL.md +137 -0
  28. package/.xtrm/skills/default/using-specialists-v2/SKILL.md +14 -21
  29. package/.xtrm/skills/default/using-specialists-v3/SKILL.md +533 -21
  30. package/.xtrm/skills/default/vaultctl/SKILL.md +2 -2
  31. package/CHANGELOG.md +82 -3
  32. package/cli/dist/index.cjs +12425 -3770
  33. package/cli/dist/index.cjs.map +1 -1
  34. package/cli/package.json +9 -3
  35. package/package.json +27 -7
  36. package/packages/pi-extensions/package.json +1 -1
  37. package/.xtrm/skills/default/planning/evals/evals.json +0 -19
  38. package/.xtrm/skills/default/quality-gates/evals/evals.json +0 -181
  39. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/FINAL-EVAL-SUMMARY.md +0 -75
  40. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/edge-case-auto-fix-verification/with_skill/outputs/response.md +0 -59
  41. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/edge-case-mixed-language-project/with_skill/outputs/response.md +0 -60
  42. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/eval-summary.md +0 -105
  43. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/partial-install-python-only/with_skill/outputs/response.md +0 -93
  44. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/python-refactor-request/with_skill/outputs/response.md +0 -104
  45. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/quality-gate-error-fix/with_skill/outputs/response.md +0 -74
  46. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/should-not-trigger-general-chat/with_skill/outputs/response.md +0 -18
  47. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/should-not-trigger-math-question/with_skill/outputs/response.md +0 -18
  48. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/should-not-trigger-unrelated-coding/with_skill/outputs/response.md +0 -56
  49. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/tdd-guard-blocking-confusion/with_skill/outputs/response.md +0 -67
  50. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/typescript-feature-with-tests/with_skill/outputs/response.md +0 -97
  51. package/.xtrm/skills/default/sync-docs/evals/evals.json +0 -89
  52. package/.xtrm/skills/default/test-planning/evals/evals.json +0 -23
  53. package/.xtrm/skills/default/using-specialists/SKILL.safe.md +0 -1082
  54. package/.xtrm/skills/default/using-specialists/SKILL.ultra.md +0 -1082
  55. package/.xtrm/skills/default/using-specialists/evals/evals.json +0 -68
  56. package/.xtrm/skills/default/using-specialists-v3/evals/evals.json +0 -89
  57. package/packages/pi-extensions/.serena/project.yml +0 -130
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # Run semgrep against the diff between HEAD and origin/main.
3
+ # Used by pre-push hook so pre-existing debt doesn't block unrelated pushes.
4
+ # CI's full scan remains the source of truth for absolute findings.
5
+
6
+ set -euo pipefail
7
+
8
+ if ! command -v semgrep >/dev/null; then
9
+ echo "semgrep not installed — skipping (CI covers it)"
10
+ exit 0
11
+ fi
12
+
13
+ # Derive base ref dynamically. Order:
14
+ # 1. branch's tracked upstream ('@{u}') — most reliable
15
+ # 2. common default branches if their *remote* version exists (origin/*)
16
+ # 3. local default branches IF different from current branch
17
+ # We refuse to use the current branch as its own baseline because then
18
+ # merge-base resolves to HEAD and --baseline-commit=HEAD silently scans
19
+ # nothing on every push.
20
+ HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)
21
+ HEAD_SHA=$(git rev-parse HEAD)
22
+
23
+ upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)
24
+ BASE_REF=""
25
+ if [ -n "$upstream" ]; then
26
+ BASE_REF="$upstream"
27
+ else
28
+ for cand in origin/main origin/master main master; do
29
+ git rev-parse --verify "$cand" >/dev/null 2>&1 || continue
30
+ # Skip a local-branch candidate that IS the current branch
31
+ [ "$cand" = "$HEAD_BRANCH" ] && continue
32
+ BASE_REF="$cand"
33
+ break
34
+ done
35
+ fi
36
+
37
+ [ -n "$BASE_REF" ] && git fetch "${BASE_REF%%/*}" "${BASE_REF#*/}" --quiet 2>/dev/null || true
38
+
39
+ SEMGREP_BASELINE_ARGS=()
40
+ if [ -n "$BASE_REF" ]; then
41
+ BASE=$(git merge-base HEAD "$BASE_REF" 2>/dev/null || true)
42
+ # merge-base==HEAD here means branch is at upstream tip — legitimate empty
43
+ # diff. Pass --baseline-commit so semgrep produces an empty result rather
44
+ # than falling back to a full scan.
45
+ [ -n "$BASE" ] && SEMGREP_BASELINE_ARGS=(--baseline-commit="$BASE")
46
+ fi
47
+ # Last-resort: no upstream resolved at all. rev-list can equal HEAD on single
48
+ # -commit histories; reject and full-scan in that case.
49
+ if [ ${#SEMGREP_BASELINE_ARGS[@]} -eq 0 ]; then
50
+ BASE=$(git rev-list HEAD --max-count=50 | tail -1)
51
+ if [ -n "$BASE" ] && [ "$BASE" != "$HEAD_SHA" ]; then
52
+ SEMGREP_BASELINE_ARGS=(--baseline-commit="$BASE")
53
+ else
54
+ echo "[semgrep-diff] no usable baseline (no upstream, single-commit branch, or pushing default branch directly) — running full scan"
55
+ fi
56
+ fi
57
+
58
+ exec semgrep scan \
59
+ --config=p/default \
60
+ --config=p/security-audit \
61
+ --config=p/secrets \
62
+ --config=p/python \
63
+ --config=p/dockerfile \
64
+ --config=p/github-actions \
65
+ "${SEMGREP_BASELINE_ARGS[@]}" \
66
+ --error \
67
+ --quiet \
68
+ --skip-unknown-extensions
@@ -41,6 +41,52 @@ are factually superseded.
41
41
 
42
42
  ## Workflow
43
43
 
44
+ ### 0. Cleanup before reporting (MANDATORY)
45
+
46
+ A report on a dirty session is misleading. Before selecting or generating any
47
+ report, verify and clean up everything this session opened. The report must
48
+ reflect a clean terminal state.
49
+
50
+ ```bash
51
+ # 0a. Worktrees opened during the session
52
+ git worktree list # any feature/fix/chore worktrees still here?
53
+ # Remove every worktree this session created (or that a stopped specialist left):
54
+ git worktree remove <path> # for each stale entry
55
+ git branch -D <branch> # only after confirming merged or abandoned
56
+ git worktree prune # drop stale metadata
57
+
58
+ # 0b. Specialist jobs still running or waiting
59
+ sp ps # MUST be empty (or only intentionally kept-alive jobs)
60
+ sp stop <job-id> # for any leftover running/waiting job
61
+ # After every sp stop, re-check sp ps and git worktree list — sp stop should
62
+ # clean its worktree, but verify.
63
+
64
+ # 0c. Stale background processes from the session
65
+ ps -ef | grep -E '(serena|gitnexus|specialists|sp-serve|sp-script|pi[ -]|claude)' | grep -v grep
66
+ # Kill anything you launched that is still running and no longer needed.
67
+ # Be especially careful with:
68
+ # - serena MCP servers (often leak when an MCP host crashes)
69
+ # - gitnexus index processes (`npx gitnexus analyze` can outlive its terminal)
70
+ # - sp-serve / sp-script tmux sessions
71
+ # - orphaned `pi` or `claude` processes from interactive sessions
72
+
73
+ tmux ls 2>/dev/null # any sp-* or xt-* tmux sessions left?
74
+ tmux kill-session -t <name> # for each stale session
75
+
76
+ # 0d. Tmp dirs the session created (only if large or sensitive)
77
+ ls -la /tmp/sp-serve-* /tmp/sp-script-* 2>/dev/null
78
+ ```
79
+
80
+ Do not skip any sub-step. If a process refuses to stop cleanly, document it in
81
+ the **Problems Encountered** section of the report so the next agent knows.
82
+
83
+ A clean session ends with:
84
+ - `git worktree list` showing only the main worktree (plus any intentional ones)
85
+ - `sp ps` showing 0 jobs (or only intentional keep-alive)
86
+ - no leaked `serena` / `gitnexus` / `specialists` / `sp-serve` / `sp-script`
87
+ processes from this session
88
+ - no orphaned tmux sessions matching `sp-*` or `xt-*`
89
+
44
90
  ### 1. Select report: update existing or generate new
45
91
 
46
92
  For same-day update:
@@ -115,8 +161,8 @@ delete this section. If prior dispatches exist, keep and extend them.
115
161
  #### Problems Encountered
116
162
  Every problem hit during the session. Root Cause and Resolution columns are
117
163
  mandatory. Include: bugs discovered, wrong approaches tried, blockers hit,
118
- tooling failures. If no problems exist anywhere in the same-day report, delete
119
- this section entirely.
164
+ tooling failures, and any cleanup-step failures from Step 0 above. If no
165
+ problems exist anywhere in the same-day report, delete this section entirely.
120
166
 
121
167
  #### Code Changes
122
168
  The skeleton lists files. Add narrative:
@@ -127,7 +173,8 @@ The skeleton lists files. Add narrative:
127
173
  to the final pushed stack
128
174
 
129
175
  #### Documentation Updates
130
- List doc changes, skill updates, memory saves, CHANGELOG entries.
176
+ List doc changes, skill updates, memory saves, CHANGELOG entries
177
+ (see Step 5 — due-diligence sweep — and Step 6 — CHANGELOG sync).
131
178
  Delete if no doc work happened.
132
179
 
133
180
  #### Open Issues with Context
@@ -162,12 +209,108 @@ Ensure all frontmatter counts are accurate after filling/updating:
162
209
  - `issues_closed` — actual closed issue count represented in the report
163
210
  - `commits` — commit count represented in the report, if known
164
211
 
165
- ### 5. Commit the report
212
+ ### 5. Due-diligence sweep (paranoid mode — assume you forgot something)
213
+
214
+ Step 0 cleaned the *process* state. This step audits the *content* state.
215
+ Cleanup work the orchestrator usually forgets at session close, ranked by
216
+ how often it gets missed:
217
+
218
+ - **Service skills**: did this session touch any code under a service
219
+ registered in `.claude/skills/service-registry.json` (or equivalent
220
+ registry)? If yes, the service skill's SKILL.md or diagnostic scripts
221
+ are likely drifted. Run `/updating-service-skills` (or
222
+ `service-skills-sync` specialist) and let it scan. If no registry exists,
223
+ skip — but check whether the project keeps service skills under
224
+ `.xtrm/skills/user/packs/<service>/` and treat them the same.
225
+ - **Docs SSOT**: did the session change architecture, migrations, public
226
+ APIs, or service ownership? If yes, run `/sync-docs` (or the
227
+ `sync-docs` specialist) for any drifted doc. Skip if changes are
228
+ pure-internal (refactors with no observable surface change).
229
+ - **Memories**: every `bd close` should have triggered a memory-gate ack.
230
+ Run `bd memories <topic>` to confirm anything genuinely novel landed.
231
+ If you saw a real surprise but acked "nothing novel" out of haste,
232
+ go back and `bd remember` it now.
233
+ - **CLAUDE.md / project guide**: did this session add or remove a
234
+ service, change a key port, change a top-level workflow command, or
235
+ change how tools are wired? If yes, append/correct in CLAUDE.md before
236
+ commit — the file is loaded automatically by every future session.
237
+ - **Evidence artifacts**: did the session generate reports, dashboards,
238
+ CSVs, or figures intended to be persisted (`scripts/outputs/`,
239
+ `docs/review/`, etc.)? Confirm they are committed; otherwise either
240
+ commit them or document in the report why they were not kept.
241
+ - **Decisions**: did the session make a non-obvious architectural call
242
+ (deprecating a service, schema choice, dependency swap)? Record via
243
+ `bd decision` if the project uses it, otherwise note in the report.
244
+ - **Tests**: did new behavior land without tests? If yes, file a test
245
+ follow-up bead (`discovered-from:<impl-id>`) before closing — do not
246
+ let untested behavior leave the session silent.
247
+ - **Skill packs (`.xtrm/skills/`)**: did you edit a skill in this
248
+ project? If the canonical version lives in xtrm-tools, mirror the edit
249
+ there too (or note that `xt update` will overwrite the local mirror on
250
+ next sync, which makes the local edit ephemeral).
251
+ - **Open beads created mid-session**: every bead filed this session
252
+ should be either closed, scheduled with a parent, or marked with clear
253
+ context. Run `bd list --status=open --created-by=me` (or equivalent)
254
+ and confirm none are floating without a parent or follow-up note.
255
+
256
+ If any item above turns up real work, do it now or file a follow-up bead
257
+ linked `discovered-from:<this-session-root>` so the next agent picks it up.
258
+ A clean session means none of these were forgotten — the report should be
259
+ able to honestly claim "due-diligence sweep clean."
260
+
261
+ ### 6. Sync CHANGELOG.md (MANDATORY when user-facing changes shipped)
262
+
263
+ The session report is for the next *agent*; CHANGELOG.md is for downstream
264
+ *consumers*. Both must stay in sync — the report alone is not enough.
265
+
266
+ ```bash
267
+ ls CHANGELOG.md 2>/dev/null # confirm the project keeps one
268
+ git tag --sort=-v:refname | head -3 # last release tag
269
+ git log <last-tag>..HEAD --oneline # what is missing
270
+ ```
166
271
 
167
- Reports are versioned handoff artifacts and should be tracked.
272
+ Decision tree:
273
+
274
+ - **A release was cut this session** (new tag, e.g. via `/releasing` or
275
+ `changelog-keeper`): the new version section already exists. Verify it
276
+ contains every user-facing change from the session and that
277
+ `[Unreleased]` is empty. Stop — release flow owns CHANGELOG.
278
+ - **No release was cut**: append every user-facing change from the session
279
+ to the existing `[Unreleased]` block at the top of CHANGELOG.md. Use
280
+ Keep a Changelog categories: `### Added` / `### Changed` / `### Deprecated`
281
+ / `### Removed` / `### Fixed` / `### Security`. One bullet per change,
282
+ lead with the affected subsystem or symbol, include the bead ID(s) when
283
+ available — same prose density as prior `[Unreleased]` entries.
284
+ - **No user-facing change shipped** (pure orchestration, doc-only edits to
285
+ internal-handoff files like reports/skills, refactors with no observable
286
+ effect): skip — do not pollute `[Unreleased]` with internal noise. Note
287
+ the skip in the Documentation Updates section so it is auditable.
288
+
289
+ What counts as user-facing for `[Unreleased]`:
290
+ - new or removed CLI flags, commands, env vars, config keys
291
+ - new or removed services / containers / jobs an operator deploys
292
+ - schema migrations that downstream consumers see
293
+ - new or removed API/MCP/REST endpoints, tools, or response fields
294
+ - bug fixes that change observable behavior
295
+ - security-relevant changes
296
+
297
+ What does NOT belong in `[Unreleased]`:
298
+ - session reports themselves
299
+ - skill or memory edits that only affect agents
300
+ - refactors with byte-identical observable behavior
301
+ - per-issue notes that already live in beads
302
+
303
+ If the project has no CHANGELOG.md, skip silently — do not create one
304
+ without operator direction.
305
+
306
+ ### 7. Commit the report (and CHANGELOG if updated)
307
+
308
+ Reports are versioned handoff artifacts and should be tracked. If Step 5
309
+ modified `CHANGELOG.md`, fold it into the same commit so the report and
310
+ changelog ship together.
168
311
 
169
312
  ```bash
170
- git add .xtrm/reports/
313
+ git add .xtrm/reports/ CHANGELOG.md # CHANGELOG.md only if changed
171
314
  git commit -m "session report: <date>"
172
315
  ```
173
316
 
@@ -175,11 +318,29 @@ If you updated an existing same-day report after an earlier report commit, commi
175
318
  that update with the same message style or fold it into the current final commit
176
319
  before push.
177
320
 
321
+ ### 8. Final cleanup verification (MANDATORY)
322
+
323
+ After committing, re-run the Step 0 checks one more time:
324
+
325
+ ```bash
326
+ git worktree list
327
+ sp ps
328
+ ps -ef | grep -E '(serena|gitnexus|specialists|sp-serve|sp-script)' | grep -v grep
329
+ tmux ls 2>/dev/null
330
+ ```
331
+
332
+ If any of these show session-leaked artifacts, stop them now or document them
333
+ in the report. Do not consider the session "closed" until this verification is
334
+ clean.
335
+
178
336
  ## Quality bar
179
337
 
180
338
  The reference is `~/projects/specialists/.xtrm/reports/2026-03-30-orchestration-session.md`.
181
339
  Every report must match that level of detail. Specifically:
182
340
 
341
+ - Step 0 cleanup performed before report generation; Step 8 verification clean.
342
+ - Step 5 due-diligence sweep performed; service skills, docs, memories, CLAUDE.md, evidence, decisions, tests, and skill mirrors checked (or skipped with reason).
343
+ - Step 6 CHANGELOG sync performed when user-facing changes shipped (or skip noted).
183
344
  - No empty `<!-- FILL -->` markers left in the final output
184
345
  - No duplicate same-day reports unless explicitly requested by the operator
185
346
  - Every closed issue has context, not just an ID
@@ -78,7 +78,7 @@ and stop.
78
78
 
79
79
  ```bash
80
80
  python3 .xtrm/skills/default/sync-docs/scripts/drift_detector.py scan --json \
81
- | jq '[.[] | select(.path == "<YOUR_DOC>")]'
81
+ | jq '[.stale[]? | select(.doc == "<YOUR_DOC>")]'
82
82
  ```
83
83
 
84
84
  If your doc reports stale, capture the list of commits since `synced_at` — those are your candidate commits for Phase 3.
@@ -28,7 +28,8 @@ This is what a correctly installed project looks like. Check each item.
28
28
  | `.xtrm/skills/active/` | Flat directory of symlinks to `../default/<skill>` |
29
29
  | `active/pi/` subdirectory | Must NOT exist (stale — old runtime split) |
30
30
  | `active/claude/` subdirectory | Must NOT exist (stale — old runtime split) |
31
- | `.pi/settings.json` `.skills` array | Must include `"../.xtrm/skills/active"` |
31
+ | `.pi/settings.json` `.skills` array | Must include `"../.xtrm/skills/active"` (project-local, wins) |
32
+ | `.pi/settings.json` `.skills` array | Must include `"~/.xtrm/skills/default"` (user-level fallback — xtrm-4h6u) |
32
33
  | `.pi/settings.json` `.skills` array | Must NOT include `"../.xtrm/skills/active/pi"` (old path) |
33
34
 
34
35
  ### Hooks wiring
@@ -65,10 +66,10 @@ readlink .claude/skills
65
66
  ls .xtrm/skills/active/pi 2>/dev/null && echo "STALE: active/pi exists"
66
67
  ls .xtrm/skills/active/claude 2>/dev/null && echo "STALE: active/claude exists"
67
68
 
68
- # 5. Pi settings skills entry
69
+ # 5. Pi settings skills entries (both must be present since xtrm-4h6u)
69
70
  node -e "const s=require('./.pi/settings.json'); console.log(s.skills)" 2>/dev/null
70
- # Expected to include: ../.xtrm/skills/active
71
- # Stale if includes: ../.xtrm/skills/active/pi
71
+ # Expected to include BOTH: ../.xtrm/skills/active AND ~/.xtrm/skills/default
72
+ # Stale if only first entry present, or if includes: ../.xtrm/skills/active/pi
72
73
 
73
74
  # 6. Active view integrity (all entries must be valid symlinks)
74
75
  for f in .xtrm/skills/active/*; do [ -L "$f" ] || echo "NOT A SYMLINK: $f"; done
@@ -167,6 +168,16 @@ cd cli && npm run build
167
168
  xt init -y # now runs with updated code
168
169
  ```
169
170
 
171
+ **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/`:
172
+
173
+ ```bash
174
+ git worktree add --detach /tmp/xt-build HEAD
175
+ cd /tmp/xt-build/cli && npm ci && npm run build
176
+ cp dist/index.cjs <worktree-root>/cli/dist/index.cjs
177
+ cp dist/index.cjs.map <worktree-root>/cli/dist/index.cjs.map
178
+ git worktree remove /tmp/xt-build --force
179
+ ```
180
+
170
181
  ## Verification
171
182
 
172
183
  After all fixes, confirm canonical state is restored:
@@ -198,6 +209,261 @@ If `xt status` still shows drift after targeted fixes, run the full sync:
198
209
  xt init
199
210
  ```
200
211
 
212
+ ## Multi-Repo Sweep (Fleet Update)
213
+
214
+ For updating **many repos at once** after an xtrm-tools upgrade — much lighter than
215
+ running `xt init -y` per repo. The right pattern when you've just rebuilt xtrm-tools
216
+ locally or pulled a new tag.
217
+
218
+ ### Dry-run discovery first
219
+
220
+ ```bash
221
+ xt update --root ~/dev # walk the tree
222
+ xt update --root ~/projects/mercury # walk another tree
223
+ ```
224
+
225
+ Output classifies each discovered repo by `.xtrm/` state:
226
+
227
+ | Status | Meaning | Action |
228
+ |--------|---------|--------|
229
+ | `refreshed` | `.xtrm/registry.json` present; drift vs current package detected | `--apply` will reinstall managed assets |
230
+ | `already-current` | `.xtrm/registry.json` present; no drift | no action |
231
+ | `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. |
232
+ | `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 |
233
+
234
+ Transient worktree paths under `.worktrees/` (specialists) or `.xtrm/worktrees/`
235
+ (`xt claude` / `xt pi`) are **skipped** automatically — they're not real repos to
236
+ refresh.
237
+
238
+ ### Apply
239
+
240
+ ```bash
241
+ xt update --apply --root ~/dev
242
+ xt update --apply --root ~/projects/mercury
243
+ ```
244
+
245
+ What `--apply` does for each managed repo:
246
+ - Runs the install flow with `force=true` — refreshes `.xtrm/config`, `.xtrm/hooks`, `.xtrm/skills/default` (mirror), `.pi/settings.json`, `.mcp.json`.
247
+ - 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).
248
+ - Globally installs any missing xt-managed Pi packages.
249
+ - Does NOT touch `incomplete` repos (deliberate — auto-fix would be destructive).
250
+
251
+ ### Bootstrapping `incomplete` repos
252
+
253
+ Two scenarios:
254
+
255
+ **A. The repo legitimately needs full xtrm management:**
256
+
257
+ ```bash
258
+ cd <repo>
259
+ xt init -y # scaffolds .xtrm/{config,hooks,skills} AND seeds registry.json
260
+ xt update --apply --repo . # bring everything in sync (registry-driven)
261
+ ```
262
+
263
+ `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:
264
+
265
+ ```bash
266
+ cp "$(npm root -g)/xtrm-tools/.xtrm/registry.json" .xtrm/
267
+ ```
268
+
269
+ **B. The repo is intentionally not xtrm-managed.** Leave the `.xtrm/` partial dir
270
+ alone; `incomplete` is just a status row, not an error. If you want it to stop
271
+ appearing, remove the orphaned `.xtrm/` directory.
272
+
273
+ ### When a repo fails
274
+
275
+ Common failure modes and fixes:
276
+
277
+ | Error | Cause | Fix |
278
+ |-------|-------|-----|
279
+ | `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. |
280
+ | `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. |
281
+ | `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. |
282
+
283
+ ## Migrating a dev-linked project to a real consumer install
284
+
285
+ A project ends up with `.xtrm/skills/default` (or another `.xtrm/` asset) as a **symlink** back to the dev tree when:
286
+ - xtrm-tools was `npm link`-ed globally (`/home/<user>/.nvm/.../node_modules/xtrm-tools` → `/home/<user>/dev/xtrm-tools/`), AND
287
+ - 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).
288
+
289
+ `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).
290
+
291
+ ### When to migrate
292
+
293
+ - Before publishing a consumer-facing release of the dependent project.
294
+ - Before handing the project to another developer / machine.
295
+ - When you want `xt update --apply` to actually *write files into the project* rather than no-op.
296
+
297
+ ### Detection
298
+
299
+ ```bash
300
+ # Is .xtrm/skills/default a symlink, and where does it point?
301
+ readlink <repo>/.xtrm/skills/default
302
+ # If empty / not-a-symlink: nothing to migrate.
303
+ # If points anywhere outside <repo>/: needs migration.
304
+ ```
305
+
306
+ ### Recipe
307
+
308
+ ```bash
309
+ cd <repo>
310
+
311
+ # 1. Remove the symlink (does NOT touch the real files in the dev tree).
312
+ rm .xtrm/skills/default
313
+
314
+ # 2. Re-run init — copies real files from the installed xtrm-tools package
315
+ # into .xtrm/skills/default/ AND seeds .xtrm/registry.json (xtrm-ya2i).
316
+ xt init -y
317
+
318
+ # 3. If the symlink was committed (git ls-files showed it as mode 120000),
319
+ # flip the tracked entry to a real directory:
320
+ git rm --cached .xtrm/skills/default 2>/dev/null # ok if it was untracked
321
+ git add .xtrm/skills/default
322
+ git commit -m "chore: replace dev symlink with real xtrm skills payload"
323
+
324
+ # 4. Optional sanity: confirm no more symlinks point outside the repo.
325
+ find .xtrm -type l -lname '/*' -o -type l ! -lname '../*' -a ! -lname './*'
326
+ # Empty output means clean.
327
+ ```
328
+
329
+ ### What `npm install -g xtrm-tools` alone does
330
+
331
+ 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.
332
+
333
+ To get true isolation (real files inside `<repo>/.xtrm/skills/default/`), the recipe above is still required.
334
+
335
+ ## Worktree hygiene: `.beads/` and `core.hooksPath`
336
+
337
+ Modern bd 1.0.3 stores `core.hooksPath` as an **absolute parent path** at `bd init`
338
+ time (e.g. `$HOME/repo/.beads/hooks`), so worktrees inherit parent hooks via
339
+ shared git config — no on-disk `.beads/` is needed inside a worktree. Since
340
+ `xtrm-cbjo` (xtrm-tools commit `937b151`) and `unitAI-yvqmf` (specialists commit
341
+ `986bc8e4`), `xt claude` / `xt pi` / `sp run` worktrees do **not** create a
342
+ `.beads/` symlink; they `rm -rf <worktree>/.beads` and `git update-index
343
+ --skip-worktree --` on tracked `.beads/*` paths. This eliminates the
344
+ squash-merge `.beads`-wipe hazard documented in projects/infra PR #39.
345
+
346
+ ### Audit your `core.hooksPath` once (xtrm-2s44)
347
+
348
+ If your bd was installed before 1.0.3, `core.hooksPath` may be the relative
349
+ string `.beads/hooks`, which would resolve against a worktree's cwd — i.e.,
350
+ the (now-missing) worktree-local `.beads/hooks/`. To survey:
351
+
352
+ ```bash
353
+ for r in ~/dev/*/ ~/projects/*/*/; do
354
+ [ -d "$r/.git" ] && [ -d "$r/.beads" ] || continue
355
+ hp=$(git -C "$r" config core.hooksPath 2>/dev/null || echo "<unset>")
356
+ case "$hp" in
357
+ /*) cat="ABSOLUTE" ;;
358
+ "<unset>") cat="UNSET" ;;
359
+ .beads/hooks) cat="RELATIVE-BD <- needs fix" ;;
360
+ *) cat="OTHER (project .githooks chain — leave alone)" ;;
361
+ esac
362
+ printf "%-50s %s\n" "${r#$HOME/} $cat" "$hp"
363
+ done
364
+ ```
365
+
366
+ Classification:
367
+ - `ABSOLUTE` — correct, no action.
368
+ - `RELATIVE-BD` (literal `.beads/hooks` or `./.beads/hooks`) — rewrite once:
369
+ ```bash
370
+ git -C <repo> config core.hooksPath "$(realpath <repo>/.beads/hooks)"
371
+ ```
372
+ - `OTHER` like `.githooks` — project-specific hook chain, leave alone. bd in
373
+ these repos works via direct invocation (not git hooks), so worktree hygiene
374
+ is unaffected.
375
+ - `UNSET` — no hooks wired anywhere; same outcome as `OTHER`.
376
+
377
+ Survey across `~/dev` + `~/projects/mercury` on 2026-05-12 returned **0 repos
378
+ needing the fix**. The safety net in `launchWorktreeSession` /
379
+ `provisionWorktree` (`normalizeParentHooksPath`) auto-rewrites on next worktree
380
+ creation if a relative `.beads/hooks` ever does appear, so the survey is mostly
381
+ defensive.
382
+
383
+ ### Worktree-internal artifact inventory (xtrm-x80f)
384
+
385
+ A worktree is a partial clone with extras: bd metadata, npm caches, runtime
386
+ state, per-worktree settings. None of these belong on a chain branch — but
387
+ the moment any of them get staged via `git add -A` or a checkpoint commit,
388
+ they can ride a PR into `main`. The matrix below documents what is protected
389
+ by which mechanism. Audit it whenever you add a new per-worktree artifact.
390
+
391
+ | Artifact | Source | Mechanism in a worktree | Status |
392
+ |----------|--------|-------------------------|--------|
393
+ | `.beads/*` | bd tracked dir | rm + `skip-worktree` (xtrm-cbjo) | ✅ |
394
+ | `.beads-credential-key`, `.beads/dolt-monitor.pid`, `.beads/dolt-server.activity` | bd runtime | gitignored at parent | ✅ |
395
+ | `.pi/npm/` | npm cache | gitignored + symlink to parent | ✅ |
396
+ | `.pi/extensions/` | pi runtime | gitignored under `.xtrm/extensions/**/.pi/` | ✅ |
397
+ | `.specialists/default` | (xtrm-tools: untracked) | symlink to parent in worktree | ✅ |
398
+ | `.specialists/user` | tracked (.json overrides) | symlink to parent in worktree | ⚠️ merge-hazard candidate, tracked at follow-up bead |
399
+ | `.specialists/{jobs,ready,trace.jsonl,db/*}` | runtime state | gitignored at parent | ✅ |
400
+ | `.claude/skills` | install symlink | gitignored | ✅ |
401
+ | `.claude/settings.local.json` | per-worktree write (`launchWorktreeSession`) | gitignored (user-global + project) | ✅ |
402
+ | `.claude/worktrees/`, `.claude/tdd-guard/data/` | runtime | gitignored | ✅ |
403
+ | `.xtrm/worktrees/`, `.xtrm/skills/active/`, `.xtrm/session-meta.json`, `.xtrm/statusline-claim`, `.xtrm/debug.db` | runtime | gitignored | ✅ |
404
+ | `AGENTS.md`, `CLAUDE.md` | tracked | gitnexus stat-counter scrubbed (xtrm-c6sf), build-gate prevents reintroduction | ✅ |
405
+ | `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) | ✅ |
406
+ | `.gitnexus/` | runtime | gitignored | ✅ |
407
+ | `.dolt/`, `*.db` | runtime | gitignored | ✅ |
408
+
409
+ The remaining ⚠️ is `.specialists/user/*.json`: the symlink swap in
410
+ `ensureWorktreeSpecialists` has the same shape as the pre-fix `.beads`
411
+ problem — a chain-branch checkpoint could capture the dir→symlink delta and
412
+ squash-merge would wipe the parent's `.specialists/user/`. Lower urgency
413
+ than `.beads` (smaller blast radius, files are intentional overrides) but
414
+ worth resolving with the same skip-worktree pattern when convenient.
415
+
416
+ The defense-in-depth pre-push guard in `xt end`
417
+ (`findBeadsSymlinkIntroductions`) currently only checks `.beads/*`. Extend
418
+ to `.specialists/*` if/when the symlink swap there becomes the next chain
419
+ of work.
420
+
421
+ ## Pre-Release Validation Methodology
422
+
423
+ Before publishing a new xtrm-tools version, validate the operator-facing CLI locally
424
+ against every consumer repo. This is the procedure that surfaced two release-blockers
425
+ in 2026-05-12 alone (`xtrm-16ec` yaml-null crash, `xtrm-ny61` worktree over-discovery).
426
+
427
+ ### Procedure
428
+
429
+ ```bash
430
+ # 1. Build dist from the local checkout
431
+ cd /path/to/xtrm-tools && npm run build --workspace cli
432
+
433
+ # 2. Link globally so `xt` runs local source
434
+ npm link
435
+
436
+ # 3. Sweep across all consumer trees (dry-run first)
437
+ xt update --root ~/dev
438
+ xt update --root ~/projects/mercury
439
+
440
+ # 4. Identify failed/incomplete rows. Fix any real bugs in xtrm-tools FIRST,
441
+ # then re-build + re-link + re-sweep.
442
+
443
+ # 5. Once dry-run is clean, apply across the fleet:
444
+ xt update --apply --root ~/dev
445
+ xt update --apply --root ~/projects/mercury
446
+
447
+ # 6. Cut the public release only after the local apply succeeds end-to-end.
448
+ ```
449
+
450
+ ### Why this beats publishing first and patching later
451
+
452
+ - A published `0.7.X` that crashes on a default-config consumer repo wastes a
453
+ version number — users see "upgrade and immediately break" and lose trust.
454
+ - Bugs that only manifest on real consumer state (comments-only YAML, transient
455
+ worktrees, drifted PACK metadata) are invisible from xtrm-tools' own test
456
+ suite — only a real sweep catches them.
457
+ - `npm link` flips between local-source-globally and published-version-globally
458
+ in seconds (`npm unlink` reverts), so the validation cost is minimal.
459
+
460
+ ### Watch-fors during the sweep
461
+
462
+ - **Pi packages shown as `missing` when `npm ls -g` confirms them installed** —
463
+ detection bug, filed at `xtrm-ntf8`. Not a real problem; packages work.
464
+ - **xtrm-tools itself appearing as `failed` with "Source and destination..."** —
465
+ expected when xtrm-tools is npm-linked into itself; not a release blocker.
466
+
201
467
  ## Reporting to the user
202
468
 
203
469
  After completing detection + remediation + verification, give the user a concise
@@ -28,6 +28,7 @@ from bootstrap import ( # noqa: E402
28
28
  RootResolutionError,
29
29
  find_service_for_path,
30
30
  get_project_root,
31
+ get_registry_path,
31
32
  get_service,
32
33
  load_registry,
33
34
  save_registry,
@@ -110,6 +111,22 @@ def check_drift_from_hook_stdin() -> None:
110
111
  sys.exit(0)
111
112
 
112
113
 
114
+ def _print_missing_registry_hint(project_root: str | None = None) -> None:
115
+ if project_root is None:
116
+ try:
117
+ project_root = get_project_root()
118
+ except RootResolutionError:
119
+ project_root = "."
120
+
121
+ root = Path(project_root)
122
+ expected = (
123
+ f"Registry not found. Expected one of: {root / 'service-registry.json'}, "
124
+ f"{root / '.claude/skills/service-registry.json'}, "
125
+ f"{root / '.xtrm/skills/user/packs/*/service-registry.json'}"
126
+ )
127
+ print(expected, file=sys.stderr)
128
+
129
+
113
130
  def update_sync_time(service_id: str, project_root: str | None = None) -> bool:
114
131
  """Update last_sync timestamp for a service in the registry."""
115
132
  try:
@@ -140,6 +157,11 @@ def scan_drift(project_root: str | None = None) -> list[dict]:
140
157
  return []
141
158
 
142
159
  root = Path(project_root)
160
+ registry_path = get_registry_path(project_root)
161
+ if not registry_path.exists():
162
+ _print_missing_registry_hint(project_root)
163
+ return []
164
+
143
165
  registry = load_registry(project_root)
144
166
  drifted: list[dict] = []
145
167
 
@@ -9,7 +9,9 @@ description: >
9
9
  output immediately, or when the work is a single LLM call with structured
10
10
  input/output. Do NOT use for tracked agent work — that belongs to
11
11
  `using-specialists-v2`.
12
- version: 1.0
12
+ version: 1.1.0
13
+ updated: 2026-05-06
14
+ synced_at: a0e54d0c
13
15
  ---
14
16
 
15
17
  # Script-Class Specialists
@@ -54,7 +56,7 @@ A spec is rejected at request time (`specialist_load_error`) if any of:
54
56
  - `execution.interactive` is `true`
55
57
  - `execution.requires_worktree` is `true`
56
58
  - `execution.permission_required` is anything other than `READ_ONLY`
57
- - `skills.scripts` is non-empty
59
+ - `skills.scripts` is non-empty (always rejected; no `--allow-local-scripts` bypass)
58
60
  - `prompt.task_template` is missing
59
61
  - a referenced `$var` in the chosen template is not supplied (`template_variable_missing`)
60
62
 
@@ -101,9 +103,9 @@ sp script <specialist-name> \
101
103
  Behaviour:
102
104
 
103
105
  - Loads the spec via `SpecialistLoader` (same loader as `sp run`).
104
- - Renders `prompt.task_template` (or named template) with `--vars`.
105
- - Spawns `pi --mode json --no-session --no-extensions --no-tools` with the
106
- resolved model.
106
+ - Renders `prompt.task_template` (or named template) with `--vars`, then feeds the rendered prompt via stdin.
107
+ - `--db-path /path/to/observability.db` is an exact SQLite file path; omit it to use the project default `.specialists/db/observability.db`.
108
+ - Spawns `pi` in JSON mode with no session, no extensions, no tools, and offline; forwards the resolved model, optional `--thinking`, and `--system-prompt` when `prompt.system` is set (full override, not append).
107
109
  - Returns the final assistant text on stdout. With `--json`, returns the full
108
110
  `ScriptGenerateResult` envelope.
109
111
  - Writes one row to `.specialists/db/observability.db` (same writer as `sp run`).