@skitterbyte/skitterspec 16.3.2 → 16.4.0

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.
@@ -66,17 +66,23 @@ Beneath it, `skitterspec spec-env live <take|release|abort|status>` is the engin
66
66
 
67
67
  **Ticketing-provider sync (opt-in, a separate package).** The base is
68
68
  tracker-free: it knows nothing about any specific ticketing system. A
69
- ticketing provider is installed as its own distribution that plugs into two named
70
- **seams** in the shared skills (`/spec` Phase E, `/spec-go` step 3b) and fulfils a
71
- skill-name + CLI contract. Sync is **one-way**: the repo is the source of truth
72
- and the tracker is a **generated mirror**. It ships `/spec-push` (repo→tracker;
73
- computes a create/update plan against a committed last-pushed snapshot and applies
74
- it) and `/spec-status` (read-only drift report — what would push, and whether the
75
- tracker's workflow-state drifted), backed by a `spec-sync` CLI. There is no
76
- content pull — the tracker is never read back or merged. When a provider is
77
- present, `/spec` also links the spec to the tracker. With no provider installed
78
- the seams are empty and every skill behaves as a plain filesystem workflow. See
79
- the provider package's own docs for its config and field reference.
69
+ ticketing provider is installed as its own distribution that plugs into named
70
+ **seams** in the shared skills and fulfils a skill-name + CLI contract.
71
+ Sync is **one-way**: the repo is the source of truth and the tracker is a
72
+ **generated mirror**. It ships `/spec-push` (repo→tracker; computes a create/update plan
73
+ against a committed last-pushed snapshot and applies it) and `/spec-status`
74
+ (read-only drift report — what would push, and whether the tracker's
75
+ workflow-state drifted), backed by a `spec-sync` CLI. There is no content pull —
76
+ the tracker is never read back or merged.
77
+
78
+ **Every skill that moves a spec through the lifecycle carries a seam**, so the
79
+ mirror keeps up without anyone remembering to push: `/spec`, `/spec-bug` and
80
+ `/spec-hotfix` link the spec they create; `/spec-go` refreshes it as work starts;
81
+ `/spec-complete`, `/spec-cancel` and `/spec-review` refresh it after they change
82
+ it. `/spec-to-main` and `/spec-live` carry none — they change no status.
83
+ With no provider installed the seams are empty and every skill behaves as a plain
84
+ filesystem workflow. See the provider package's own docs for its config and field
85
+ reference.
80
86
 
81
87
  ## Project conventions (fill this in)
82
88
 
@@ -158,6 +158,8 @@ The **State log** is the folder/status audit trail; later transitions
158
158
  (`/spec-complete`, `/spec-cancel`) append a row. The **Changelog** is for the
159
159
  fix narrative and decisions — keep them separate.
160
160
 
161
+
162
+
161
163
  ## 5. Drive to GREEN
162
164
 
163
165
  - Implement the **minimal, root-cause** fix. Match surrounding code; honour all
@@ -51,6 +51,8 @@ Add a **Changelog** entry:
51
51
  `git mv "specs/<bucket>/<name>" "specs/cancelled/<name>"` (preserve history;
52
52
  move the whole folder).
53
53
 
54
+
55
+
54
56
  Then **commit the cancellation edits** — this skill wrote them, so it commits
55
57
  them:
56
58
 
@@ -53,6 +53,8 @@ Before marking complete, confirm the work is actually finished:
53
53
  move the whole folder). The `specs/complete/` folder is the record of finished
54
54
  specs — `git log`/the per-spec State log give the completion order.
55
55
 
56
+
57
+
56
58
  Then **commit the completion edits** — steps 3–4 are this skill's own output, so
57
59
  it finishes its own work rather than handing you a dirty tree:
58
60
 
@@ -128,14 +130,14 @@ is finished — e.g. to run a later phase in CI or a shared test env? Use
128
130
  recover those commits onto the branch, then re-run.
129
131
  - *no worktree* — the spec is live but its worktree is gone. Re-isolate it with
130
132
  `skitterspec spec-env up <name>`, then re-run.
131
- Relay the diagnostic to the user and **stop** — do not proceed to teardown.
133
+ Relay the diagnostic to the user and **stop** — do not tear anything down.
132
134
  2. **Plan + execute.** Run `skitterspec spec-env integrate <name>` and run the
133
135
  printed commands **in order**:
134
136
  - `git -C <worktree> rebase <base>` — replay the branch onto base.
135
137
  - `git -C <mainRepoPath> merge --ff-only <branch>` — fast-forward base.
136
138
  On a **rebase conflict** (non-zero exit), run
137
139
  `git -C <worktree> rebase --abort`, relay the conflict, and **stop** — leave it
138
- to the user; do not offer teardown.
140
+ to the user; do not tear anything down.
139
141
  On a **no-op** ("already landed"), just say so and continue.
140
142
  3. **Re-test on base.** Run the project's test command from the primary checkout;
141
143
  it must be **green** before you call the landing done.
@@ -144,9 +146,22 @@ is finished — e.g. to run a later phase in CI or a shared test env? Use
144
146
 
145
147
  ## 7. Tear down the environment (opt-in, only if configured)
146
148
 
147
- **Only when `specs/.core/env.config.json` exists**, offer don't force — to
148
- reclaim the finished spec's environment. On confirmation, run the `spec-env` CLI
149
- directly (the old `/spec-env-down` skill is gone — teardown is folded in here):
149
+ **Only when `specs/.core/env.config.json` exists.** Reclaiming the environment is
150
+ what completing a spec *is*, so sub-steps 1–3 run **automatically do not ask**.
151
+ Run the `spec-env` CLI directly (the old `/spec-env-down` skill is gone —
152
+ teardown is folded in here).
153
+
154
+ **The precondition is that the work actually landed.** Only tear down when step 6
155
+ completed: it landed (or reported "already landed") **and** the base suite came
156
+ back green. A rebase conflict, a work-loss abort or a red suite means step 6 told
157
+ you to stop — tear nothing down, because the worktree is where the user picks the
158
+ problem up. That precondition is what makes a confirmation redundant: by the time
159
+ you get here the branch is an ancestor of base and the engine's guards have
160
+ nothing left to protect.
161
+
162
+ **Opt-out:** if the user passed **`--keep-env`**, skip sub-steps 1–3, say the
163
+ worktree and branch are being kept, and go straight to sub-step 4. Mention
164
+ `skitterspec spec-env up <name>` re-attaches it later either way.
150
165
 
151
166
  1. **Disconnect the proxy if this spec is connected.** If `.spec-env/connected`
152
167
  names this spec, run `skitterspec spec-env connect main` first so the
@@ -154,20 +169,28 @@ directly (the old `/spec-env-down` skill is gone — teardown is folded in here)
154
169
  2. **Stop its host dev servers:** `skitterspec spec-env dev down <name>` (a
155
170
  no-op when none are running / configured).
156
171
  3. **Remove worktree + stack + slot:** run `skitterspec spec-env down <name>`
157
- and execute the commands it prints, in order. After a **feature/bug** landing
158
- the branch is merged into base, so teardown needs **no `--force`** and deletes
159
- the branch (`git branch -d`). After a **hotfix** landing the branch isn't
160
- merged (it was tagged + cherry-picked), but its head is captured by the deploy
161
- tag, so teardown still needs no `--force` and drops the branch with
162
- `git branch -D` (the tag holds the commits). It still respects the guards (won't
163
- destroy a dirty, or unpushed-and-unlanded, worktree without `--force`).
172
+ and execute the commands it prints, in order. After a landing — merged into
173
+ base for a **feature/bug**, captured by the deploy tag for a **hotfix**
174
+ teardown needs **no `--force`** and drops the branch with `git branch -D`,
175
+ which is safe precisely because the commits are already somewhere else. It
176
+ still respects the guards (won't destroy a dirty, or unpushed-and-unlanded,
177
+ worktree without `--force`), so if it *does* refuse, relay that and stop
178
+ rather than reaching for `--force`.
164
179
  4. **Reap orphaned test-DB volumes:** run `skitterspec spec-env prune`. It lists
165
180
  Docker volumes in the repo namespace that belong to **no live spec** (no
166
181
  worktree) — leftovers from declined/aborted teardowns, manual
167
182
  `git worktree remove`, or `--keep-volumes`. Show the user the orphan list and,
168
183
  **only on their confirmation**, execute the printed `docker volume rm`
169
- commands. Non-fatal: if prune can't run (Docker down) or the user declines,
170
- report it and finish completing anyway never block the spec on it. Skip when
171
- Docker isn't in use (the command self-reports "no orphaned volumes").
184
+ commands. **This one still asks**, unlike 1–3: it reaps volumes belonging to
185
+ *other* specs, and this spec having landed cleanly says nothing about those.
186
+ Non-fatal: if prune can't run (Docker down) or the user declines, report it and
187
+ finish completing anyway — never block the spec on it. Skip when Docker isn't
188
+ in use (the command self-reports "no orphaned volumes").
189
+
190
+ **Say what you reclaimed.** With no confirmation step the user never saw this
191
+ coming, so the final report must name the worktree path removed and the branch
192
+ deleted (or, under `--keep-env`, that both were kept). A teardown nobody
193
+ authorised and nobody was told about is the one way this step can lose someone's
194
+ place.
172
195
 
173
196
  If `env.config.json` is absent, skip this entirely — behave exactly as before.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: spec-hotfix
3
- description: Fix a production bug on the exact released version — fork a worktree from a release tag, drive it red→green like /spec-bug, then land it by tagging a new patch (for CI/CD to deploy) and cherry-picking the fix back onto main. ALWAYS starts from a base tag and works on the hotfix's own branch, never on main. Creates specs/in-progress/hotfix-<name>/00-overview.md. Use when the user says "/spec-hotfix", "hotfix <tag>", "prod is broken on <version>", "patch the released version", or needs a fix shipped against a tagged release rather than main.
3
+ description: Fix a production bug on the exact released version — fork a worktree from a release tag, drive it red→green like /spec-bug, then land it by tagging a new patch (for CI/CD to deploy) and cherry-picking the fix back onto main. ALWAYS starts from a base tag and works on the hotfix's own branch, never on main. Can start from a tracker issue when a provider is installed (`/spec-hotfix <tag> <ISSUE-REF>`), adopting it as the hotfix's issue. Creates specs/in-progress/hotfix-<name>/00-overview.md. Use when the user says "/spec-hotfix", "hotfix <tag>", "prod is broken on <version>", "patch the released version", or needs a fix shipped against a tagged release rather than main.
4
4
  ---
5
5
 
6
6
  # /spec-hotfix — fix a released version, tag it, cherry-pick back to main
@@ -20,11 +20,24 @@ Spec type convention (see `.claude/rules/spec-planning.md`):
20
20
  tag + cherry-pick — it needs the isolation engine (`specs/.core/env.config.json`).
21
21
  If isolation is absent, say so and stop; there is no in-place path.
22
22
 
23
+
24
+
23
25
  ## 1. Establish the base version (the tag)
24
26
 
25
- - Take the release tag from the argument — `/spec-hotfix <tag> <name>` (e.g.
26
- `/spec-hotfix v33.16.4 login-crash`). If it's missing, **ask which version prod
27
- is running** don't guess.
27
+ - **Read the arguments.** `/spec-hotfix <tag> <name>` (e.g.
28
+ `/spec-hotfix v33.16.4 login-crash`). An argument shaped like an issue
29
+ referenceletters, a hyphen, digits (`SKI-123`) —
30
+ is **always a reference, never a name**, so `/spec-hotfix v33.16.4 SKI-123` and
31
+ `/spec-hotfix SKI-123`
32
+ both mean "adopt that issue". Release tags don't take that shape, so the two
33
+ can't be confused. With no name and no reference, ask what to call it.
34
+ - Take the release tag from the argument. If it's missing,
35
+ **ask which version prod is running** — don't guess. When an issue was adopted
36
+ above, **offer any versions it mentions** as suggestions — clearly labelled as
37
+ the reporter's words, not a default — and still wait for the answer. A reporter
38
+ usually names
39
+ the version they *saw* the bug on, which is not necessarily what is deployed,
40
+ and a hotfix forked from the wrong tag fails late.
28
41
  - **Verify the tag exists** before anything else:
29
42
  `git rev-parse --verify <tag>^{commit}`. If it doesn't resolve, stop and ask.
30
43
 
@@ -156,6 +169,8 @@ place of the table when the spec touches no external surface.>
156
169
  Keep the **State log** (state transitions) separate from the **Changelog** (fix
157
170
  narrative and decisions).
158
171
 
172
+
173
+
159
174
  ## 6. Drive to GREEN
160
175
 
161
176
  - Implement the **minimal, root-cause** fix on the branch. Match surrounding code;
@@ -82,6 +82,8 @@ reading the code, do that instead of asking.
82
82
  re-grooming, knock it back to `Draft` (set `> **Status:** Draft` and append a
83
83
  **State log** row).
84
84
 
85
+
86
+
85
87
  ## 5. Report
86
88
 
87
89
  Summarise the drift found, what you changed, any questions still open, and
@@ -1,6 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
3
 
4
+ // This package's bin/, src/ and assets/ are COMPOSED by scripts/build-dist.js and
5
+ // gitignored, not committed — so a checkout linked before a build (or after a
6
+ // `git clean`) has a working binary with nothing behind it. Say that, instead of
7
+ // letting `require` raise MODULE_NOT_FOUND on an internal path the caller has no
8
+ // way to interpret.
9
+ //
10
+ // Inline rather than shared: a helper would have to live in src/, which is
11
+ // exactly what may be missing. In the workspace packages src/ always exists, so
12
+ // this is inert there.
13
+ const { existsSync } = require('node:fs')
14
+ const { join } = require('node:path')
15
+ if (!existsSync(join(__dirname, '..', 'src'))) {
16
+ console.error(
17
+ 'skitterspec: no build output — this package\'s bin/src/assets are composed, not committed.\n' +
18
+ ' run "npm run build" in the skitterspec repo, then try again.',
19
+ )
20
+ process.exit(1)
21
+ }
22
+
4
23
  const { run } = require('../src/cli.js')
5
24
 
6
25
  run(process.argv.slice(2)).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skitterbyte/skitterspec",
3
- "version": "16.3.2",
3
+ "version": "16.4.0",
4
4
  "description": "Spec-driven development for Claude Code — a tracker-free filesystem workflow: lifecycle skills and per-spec isolation. For Linear sync, install @skitterbyte/skitterspec-linear instead.",
5
5
  "keywords": [
6
6
  "claude",
package/src/cli.js CHANGED
@@ -190,7 +190,7 @@ function specEnvUp(dir, config, specArg) {
190
190
  process.stdout.write('Usage: skitterspec spec-env up <spec>\n')
191
191
  return
192
192
  }
193
- const spec = resolveSpec(specArg, dir, config)
193
+ const spec = resolveSpecWithWorktree(dir, config, specArg)
194
194
 
195
195
  // Live-safe: if this spec is already live on the primary checkout (its branch was
196
196
  // branch-switched in by `live take`), a `git worktree add` would fail — the branch
@@ -357,7 +357,7 @@ function specEnvDown(dir, config, specArg, flags) {
357
357
  process.stdout.write('Usage: skitterspec spec-env down <spec> [--keep-volumes] [--force]\n')
358
358
  return
359
359
  }
360
- const spec = resolveSpec(specArg, dir, config)
360
+ const spec = resolveSpecWithWorktree(dir, config, specArg)
361
361
 
362
362
  // A worktree-only spec never held a slot but its worktree still needs removing,
363
363
  // so "nothing to do" means neither a slot nor a worktree exists.
@@ -459,6 +459,31 @@ function liveWorktreePaths(dir) {
459
459
  return paths
460
460
  }
461
461
 
462
+ // Resolve a spec argument the ONE way every spec-env subcommand resolves it:
463
+ // against the primary checkout first, then the spec's own worktree, then every
464
+ // other checkout git knows about. An in-progress spec is git-mv'd into
465
+ // specs/in-progress/ **on its own branch**, so it exists only in its worktree —
466
+ // a primary-checkout-only lookup fails for exactly the specs these commands
467
+ // serve. The first fallback is the worktree path the config derives for this
468
+ // spec (cheap, no git); the rest come from `git worktree list`, so a worktree
469
+ // provisioned under an older `worktree.root` still resolves. Identity and
470
+ // coordinate tokens always expand against `dir` (the primary checkout), so the
471
+ // answer is identical whether the command was run from main or a worktree.
472
+ function resolveSpecWithWorktree(dir, config, specArg) {
473
+ const { slug } = splitPrefix(path.basename(specArg))
474
+ const { repo, repoSlug } = repoInfo(dir)
475
+ const wtTokens = { repo, repoSlug, slug }
476
+ const worktreeGuess = path.resolve(
477
+ dir,
478
+ expandTokens(config.worktree.root, wtTokens),
479
+ expandTokens(config.worktree.folderPattern, wtTokens),
480
+ )
481
+ const searchDirs = [...new Set([worktreeGuess, ...liveWorktreePaths(dir)])].filter(
482
+ (p) => p !== dir,
483
+ )
484
+ return resolveSpec(specArg, dir, config, { searchDirs })
485
+ }
486
+
462
487
  // Every spec folder name found under specs/* across the given checkout roots.
463
488
  // An in-progress spec lives on its *worktree branch*, not the primary checkout,
464
489
  // so we must scan the worktrees too — otherwise a live spec's DB looks orphaned.
@@ -771,7 +796,7 @@ function specEnvResolve(dir, config, specArg) {
771
796
  process.stdout.write('Usage: skitterspec spec-env resolve <spec>\n')
772
797
  return
773
798
  }
774
- const r = resolveSpec(specArg, dir, config)
799
+ const r = resolveSpecWithWorktree(dir, config, specArg)
775
800
  process.stdout.write(
776
801
  `spec: ${r.folder} (${r.bucket})\n` +
777
802
  `type/slug: ${r.type} / ${r.slug}\n` +
@@ -792,7 +817,7 @@ async function specEnvDev(dir, config, positional) {
792
817
  process.stdout.write('Usage: skitterspec spec-env dev <up|down> <spec>\n')
793
818
  return
794
819
  }
795
- const spec = resolveSpec(specArg, dir, config)
820
+ const spec = resolveSpecWithWorktree(dir, config, specArg)
796
821
  if (!config.dev.length) {
797
822
  process.stdout.write(
798
823
  'spec-env dev: no dev processes configured — set "dev": [...] in env.config.json.\n',
@@ -888,7 +913,7 @@ async function specEnvConnect(dir, config, specArg) {
888
913
  return
889
914
  }
890
915
 
891
- const spec = resolveSpec(target, dir, config)
916
+ const spec = resolveSpecWithWorktree(dir, config, target)
892
917
  const registry = readRegistry(dir, config)
893
918
  if (!Object.prototype.hasOwnProperty.call(registry.slots, spec.folder)) {
894
919
  process.stdout.write(
@@ -984,20 +1009,6 @@ async function specEnvLive(dir, config, positional) {
984
1009
  }
985
1010
  }
986
1011
 
987
- // Resolve a spec, offering its worktree as a fallback search dir — a spec authored
988
- // on its own branch may not exist in the primary checkout's specs/**.
989
- function resolveSpecWithWorktree(dir, config, specArg) {
990
- const { slug } = splitPrefix(path.basename(specArg))
991
- const { repo, repoSlug } = repoInfo(dir)
992
- const wtTokens = { repo, repoSlug, slug }
993
- const worktreeGuess = path.resolve(
994
- dir,
995
- expandTokens(config.worktree.root, wtTokens),
996
- expandTokens(config.worktree.folderPattern, wtTokens),
997
- )
998
- return resolveSpec(specArg, dir, config, { searchDirs: [worktreeGuess] })
999
- }
1000
-
1001
1012
  // Take the running instance: rebase the spec's branch onto base, free it from its
1002
1013
  // worktree, and check it out in the primary checkout so the dev server reloads it.
1003
1014
  async function specEnvLiveTake(dir, config, specArg) {
@@ -217,9 +217,14 @@ function assertPrimaryOnMain(config, git) {
217
217
  * (the primary checkout), so a worktree-only spec resolves to the right base.
218
218
  */
219
219
  function resolveSpec(specArg, dir, config, opts = {}) {
220
- const found = findSpecFolder(specArg, dir, opts.searchDirs || [])
220
+ const searchDirs = opts.searchDirs || []
221
+ const found = findSpecFolder(specArg, dir, searchDirs)
221
222
  if (!found) {
222
- throw new Error(`spec not found under specs/**: ${specArg}`)
223
+ // Name the roots we looked under: the usual cause is a spec that only exists
224
+ // on its own branch, and the message should say where we didn't find it.
225
+ throw new Error(
226
+ `spec not found under specs/**: ${specArg} (searched: ${[dir, ...searchDirs].join(', ')})`,
227
+ )
223
228
  }
224
229
 
225
230
  const { type, slug } = splitPrefix(found.folder)
@@ -33,7 +33,10 @@ function planDown(spec, config, flags, ctx) {
33
33
  // A hotfix lands by tag + cherry-pick, so its branch is never an ancestor of
34
34
  // base — but once its head is captured by a tag (the deploy tag from
35
35
  // `hotfix land`), the commits are recoverable and the branch is safe to drop.
36
- // Treat "reachable from a tag" as landed, alongside merged.
36
+ // Treat "reachable from a tag" as landed, alongside merged. Read twice below:
37
+ // it decides whether the unpushed guard blocks, and whether the branch delete
38
+ // can use `-D` — the same question ("are these commits recoverable?"), so the
39
+ // two must never answer it differently.
37
40
  const landed = Boolean(worktreeState.merged || worktreeState.reachableFromTag)
38
41
 
39
42
  // --- guards (overridable with --force) ---
@@ -88,15 +91,26 @@ function planDown(spec, config, flags, ctx) {
88
91
  )
89
92
 
90
93
  // --- delete the branch ---
91
- // Runs after the worktree remove frees the branch. Normally `-d` (safe: refuses
92
- // an unmerged branch, never -D). A tag-landed hotfix branch is intentionally NOT
93
- // an ancestor of base, so `-d` would refuse it — but its commits are captured by
94
- // the deploy tag, so `-D` is safe *only* in that case. Everything else stays `-d`;
95
- // on a forced teardown of a genuinely unmerged branch it fails loudly and the
96
- // skill relays it rather than -D-ing.
94
+ // Runs after the worktree remove frees the branch. `-D` exactly when we have
95
+ // PROVEN the commits survive the delete, `-d` otherwise.
96
+ //
97
+ // `-d` looks like the safe default and mostly is, but its refusal answers a
98
+ // different question from ours: it also declines a branch that is ahead of its
99
+ // upstream ref, reporting `not yet merged to refs/remotes/origin/<branch>,
100
+ // even though it is merged to HEAD`. That fires on the ordinary spec flow —
101
+ // `/spec-go` pushes the branch when it provisions, and the phase commits after
102
+ // it are landed locally rather than pushed — so teardown meets a branch whose
103
+ // every commit is on `main` and `-d` refuses it. `merged` (HEAD is an ancestor
104
+ // of base) already establishes what we actually care about, and establishes it
105
+ // more strongly than `-d` checks.
106
+ //
107
+ // `reachableFromTag` is the same argument for a hotfix: never an ancestor of
108
+ // base, but its head is captured by the deploy tag.
109
+ //
110
+ // Everything else keeps `-d`, so a forced teardown of a genuinely unlanded
111
+ // branch fails loudly and the skill relays it rather than -D-ing.
97
112
  if (spec.branch) {
98
- const tagLanded = Boolean(worktreeState.reachableFromTag && !worktreeState.merged)
99
- commands.push(`git branch ${tagLanded ? '-D' : '-d'} ${spec.branch}`)
113
+ commands.push(`git branch ${landed ? '-D' : '-d'} ${spec.branch}`)
100
114
  }
101
115
 
102
116
  return { blocked: false, reason: null, commands, backupCommand, backupPath, volumesDropped }
package/src/init.js CHANGED
@@ -558,12 +558,22 @@ function printReport(dir, mode, { diff = false } = {}) {
558
558
  ' at /spec-go (Docker is a per-spec escalation — set > **Stack:** in the spec).\n'
559
559
  : 'Per-spec isolation is opt-in: re-run with --isolation (or copy' +
560
560
  ' specs/.core/env.config.json.example → env.config.json) to enable it.\n'
561
+ // A provider superset ships its own `spec-<provider>-setup` skill; the base
562
+ // ships none. Discovering it from what was actually installed keeps this file
563
+ // tracker-free — it never has to know which tracker (if any) is in the box.
564
+ const setupSkill = SKILLS.find((s) => /^spec-.+-setup$/.test(s))
565
+ const trackerNote = setupSkill
566
+ ? `Tracker sync is opt-in: run /${setupSkill} to configure it` +
567
+ ' (it discovers your workspace and writes the config), or see' +
568
+ ' specs/.core/SETUP.md.\n'
569
+ : ''
561
570
  process.stdout.write(
562
571
  '\nDone. Skills resolve as /spec, /spec-go, /spec-complete, /spec-cancel,' +
563
572
  ' /spec-bug, /spec-review, /spec-init, /spec-connect.\n' +
564
573
  'Next: tailor .claude/rules/spec-planning.md + the CLAUDE.md section to this' +
565
574
  " project's stack, then run /spec.\n" +
566
- isolationNote,
575
+ isolationNote +
576
+ trackerNote,
567
577
  )
568
578
  }
569
579