@skitterbyte/skitterspec 8.0.0 → 8.0.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skitterbyte/skitterspec",
3
- "version": "8.0.0",
3
+ "version": "8.0.1",
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
@@ -17,7 +17,14 @@ const {
17
17
  freeSlot,
18
18
  portOffset,
19
19
  } = require('./env/registry.js')
20
- const { resolveSpec, resolveBaseBranch, repoInfo, expandTokens, splitPrefix } = require('./env/resolve.js')
20
+ const {
21
+ resolveSpec,
22
+ resolveBaseBranch,
23
+ resolvePrimaryCheckout,
24
+ repoInfo,
25
+ expandTokens,
26
+ splitPrefix,
27
+ } = require('./env/resolve.js')
21
28
  const { ensureWorktreeDirTrusted } = require('./env/trust.js')
22
29
  const { planUp } = require('./env/provision.js')
23
30
  const { planDown } = require('./env/teardown.js')
@@ -336,26 +343,23 @@ function specEnvIntegrate(dir, config, specArg) {
336
343
  return
337
344
  }
338
345
 
339
- // /spec-complete runs this from inside the worktree, but the spec's coordinates
340
- // (worktreePath via {repo}, the base branch) must resolve against the PRIMARY
341
- // checkout. Resolve it first (parent of the shared git dir) and anchor
342
- // everything to it, so integrate works whether invoked from main or a worktree.
343
- const commonDir = gitReader(dir)(['rev-parse', '--git-common-dir'])
344
- const mainRepoPath = commonDir ? path.dirname(path.resolve(dir, commonDir)) : dir
345
-
346
+ // `dir` is already anchored on the primary checkout by the dispatch, so it is
347
+ // both where the spec resolves and the target of the fast-forward — /spec-complete
348
+ // can run this from inside the worktree and still land on main.
349
+ //
346
350
  // A spec authored entirely on its branch may not exist in the primary
347
351
  // checkout's specs/** (it was never committed to base) — but its worktree
348
352
  // does, and the worktree path is derivable from config without the folder.
349
353
  // Offer it as a fallback search location so integrate can still find the spec.
350
354
  const { slug } = splitPrefix(path.basename(specArg))
351
- const { repo, repoSlug } = repoInfo(mainRepoPath)
355
+ const { repo, repoSlug } = repoInfo(dir)
352
356
  const wtTokens = { repo, repoSlug, slug }
353
357
  const worktreeGuess = path.resolve(
354
- mainRepoPath,
358
+ dir,
355
359
  expandTokens(config.worktree.root, wtTokens),
356
360
  expandTokens(config.worktree.folderPattern, wtTokens),
357
361
  )
358
- const spec = resolveSpec(specArg, mainRepoPath, config, { searchDirs: [worktreeGuess] })
362
+ const spec = resolveSpec(specArg, dir, config, { searchDirs: [worktreeGuess] })
359
363
 
360
364
  if (!fs.existsSync(spec.worktreePath)) {
361
365
  process.stdout.write(
@@ -364,14 +368,19 @@ function specEnvIntegrate(dir, config, specArg) {
364
368
  return
365
369
  }
366
370
 
367
- const base = resolveBaseBranch(config, gitReader(mainRepoPath))
371
+ const base = resolveBaseBranch(config, gitReader(dir))
368
372
  const wtGit = gitReader(spec.worktreePath)
369
373
  const status = wtGit(['status', '--porcelain'])
370
374
  const dirty = status !== null && status.length > 0
371
375
  const ahead = wtGit(['rev-list', '--count', `${base}..HEAD`])
372
376
  const aheadOfBase = ahead !== null && Number(ahead) > 0
373
377
 
374
- const plan = planIntegrate(spec, config, { worktreeState: { dirty }, base, aheadOfBase, mainRepoPath })
378
+ const plan = planIntegrate(spec, config, {
379
+ worktreeState: { dirty },
380
+ base,
381
+ aheadOfBase,
382
+ mainRepoPath: dir,
383
+ })
375
384
 
376
385
  if (plan.blocked) {
377
386
  process.stdout.write(`spec-env integrate: blocked — ${plan.reason}.\n`)
@@ -588,6 +597,9 @@ async function specEnv(rest) {
588
597
  else positional.push(args[i])
589
598
  }
590
599
  dir = path.resolve(dir)
600
+ // Anchor on the primary checkout so every subcommand resolves {repo}, worktree
601
+ // paths, and the registry identically whether run from main or a worktree.
602
+ dir = resolvePrimaryCheckout(dir, gitReader(dir))
591
603
 
592
604
  const { config, present } = loadEnvConfig(dir)
593
605
  if (!present) {
@@ -151,6 +151,20 @@ function resolveBaseBranch(config, git) {
151
151
  return 'main'
152
152
  }
153
153
 
154
+ /**
155
+ * Resolve `dir` to the primary checkout root — the parent of the shared git dir.
156
+ * From the primary checkout `git rev-parse --git-common-dir` is `.git` (relative),
157
+ * so the parent is `dir`; from a linked worktree it's the absolute `<main>/.git`,
158
+ * so the parent is `<main>`. Anchoring every `spec-env` command here means they
159
+ * resolve `{repo}` / worktree paths / the registry identically whether run from
160
+ * `main` or a worktree. `git(args)` returns trimmed stdout or `null` (not a repo)
161
+ * — injected for testability; a `null` degrades to `dir` (today's behaviour).
162
+ */
163
+ function resolvePrimaryCheckout(dir, git) {
164
+ const common = git(['rev-parse', '--git-common-dir'])
165
+ return common ? path.dirname(path.resolve(dir, common)) : dir
166
+ }
167
+
154
168
  /**
155
169
  * Resolve a spec argument to its identity + isolation coordinates.
156
170
  * Throws a clear Error when the spec folder can't be found.
@@ -193,6 +207,7 @@ function resolveSpec(specArg, dir, config, opts = {}) {
193
207
  module.exports = {
194
208
  resolveSpec,
195
209
  resolveBaseBranch,
210
+ resolvePrimaryCheckout,
196
211
  branchFor,
197
212
  splitPrefix,
198
213
  repoInfo,