@benjaminthomas/spec-driven-dev 1.4.0 → 1.4.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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # spec-driven-dev
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@benjaminthomas/spec-driven-dev?label=npm)](https://www.npmjs.com/package/@benjaminthomas/spec-driven-dev)
4
+ [![check](https://github.com/benjaminthomas/spec-driven-dev/actions/workflows/check.yml/badge.svg)](https://github.com/benjaminthomas/spec-driven-dev/actions/workflows/check.yml)
5
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
6
+
3
7
  **A step-by-step workflow that gets AI coding agents from "here's a ticket" to "here's a reviewed PR"
4
8
  without guessing.** It first investigates the problem, then writes a plan you approve, then builds it
5
9
  in parallel with tests and review, then checks the real running app, and finally commits and opens
@@ -8,7 +12,8 @@ the PR.
8
12
  It's a set of eight [Agent Skills](https://agentskills.io), plain Markdown instruction files that
9
13
  AI coding tools load on demand. It works with Claude Code, Codex CLI, Cursor, and Google Antigravity,
10
14
  in any language or framework, and in a single repo, a monorepo, or several repos that change
11
- together. One command installs it: `npx @benjaminthomas/spec-driven-dev`.
15
+ together. Install it from npm with `npm install -g @benjaminthomas/spec-driven-dev`, then run
16
+ `spec-driven-dev` in your project.
12
17
 
13
18
  ---
14
19
 
@@ -26,6 +31,7 @@ together. One command installs it: `npx @benjaminthomas/spec-driven-dev`.
26
31
  - [Compatibility](#compatibility)
27
32
  - [By the numbers](#by-the-numbers)
28
33
  - [Working on this repo](#working-on-this-repo)
34
+ - [Changelog](CHANGELOG.md)
29
35
 
30
36
  ---
31
37
 
@@ -48,20 +54,26 @@ start a new session and continue where you left off.
48
54
 
49
55
  ## Quick start (5 minutes)
50
56
 
51
- **1. Install.** In your project folder (or, for several repos, their shared parent folder), run:
57
+ **1. Install the CLI from npm** (once per machine):
58
+
59
+ ```bash
60
+ npm install -g @benjaminthomas/spec-driven-dev
61
+ ```
62
+
63
+ **2. Run it in your project folder** (or, for several repos, in their shared parent folder):
52
64
 
53
65
  ```bash
54
- npx @benjaminthomas/spec-driven-dev
66
+ spec-driven-dev
55
67
  ```
56
68
 
57
- The installer:
69
+ It:
58
70
  1. **Finds your AI tools** (Claude Code, Cursor, Codex CLI, Antigravity) and installs the skills
59
71
  into each one you choose, for this project or for all projects.
60
72
  2. **Sets up your project** by detecting its layout, issue tracker, branch conventions, and
61
73
  commands, then asking you to confirm. The answers go into `.spec-driven.yml`
62
74
  ([details below](#project-setup-your-teams-rules)).
63
75
 
64
- **2. In your project, run each stage in order.** You can type the slash command or just say the
76
+ **3. In your AI tool, run each stage in order.** You can type the slash command or just say the
65
77
  words in quotes.
66
78
 
67
79
  ```
@@ -89,18 +101,24 @@ others `fix/STP-123-short-name`. Some have one repo, others have six. None of th
89
101
  skills. Each project describes its own rules in **`.spec-driven.yml`**, and every skill reads that
90
102
  file before doing anything.
91
103
 
92
- **How the file gets created:** `npx @benjaminthomas/spec-driven-dev` (or `npx @benjaminthomas/spec-driven-dev setup` to run only this
104
+ **How the file gets created:** `spec-driven-dev` (or `spec-driven-dev setup` to run only this
93
105
  step) detects the answers from your repos and asks you to confirm them. In your AI tool you can also
94
106
  say **"run spec-setup"**: that does a deeper version that also reads `CLAUDE.md`/`AGENTS.md`, CI
95
107
  config, and recent PRs, and fills in what the installer can't (such as each repo's role, or commands
96
108
  for stacks other than Node). If a skill finds no config, it runs this setup first.
97
109
 
98
- **What it asks:**
110
+ **What `spec-driven-dev` asks, in order** (every answer comes pre-filled with what it detected, so you
111
+ can usually just press Enter):
99
112
 
100
113
  | Question | Detected from | Example answer |
101
114
  |---|---|---|
115
+ | Which AI tools get the skills | tool folders in your home directory and commands on your `PATH` | Claude Code, Cursor |
116
+ | This project only, or all projects | (you choose) | `project` |
102
117
  | Project layout | git repos and workspace files (`pnpm-workspace.yaml`, `turbo.json`, `nx.json`, `go.work`, …) | `single`, `monorepo`, or `multi-repo` |
103
- | Which repos belong together (multi-repo) | sibling git repos grouped by GitHub owner | `api`, `web`, `worker` |
118
+ | *multi-repo:* workspace folder (where the config goes) | the folder you ran it in, or the parent of the repo you're in | `~/work` |
119
+ | *multi-repo:* which repos belong together | git repos in the workspace folder, grouped by GitHub owner | `api`, `web` |
120
+ | *multi-repo:* repos from other folders | (you type paths) | `~/clients/billing, ../shared-lib` |
121
+ | *monorepo:* package folders | the workspaces your `package.json`/`pnpm-workspace.yaml` declares, or `apps/*`, `packages/*` | `apps/web, packages/ui` |
104
122
  | Issue tracker | ticket keys in branch names and commits | Jira, key `STP` |
105
123
  | Base branch / PR target | default branch, `staging`/`develop` branches | `staging` |
106
124
  | Branch name pattern | your recent branch names | `{ticket}` → `STP-123` |
@@ -116,7 +134,17 @@ for stacks other than Node). If a skill finds no config, it runs this setup firs
116
134
  | **monorepo** | one repo, many apps/packages | the repo root (commit it) | tasks name their package, and checks run per package |
117
135
  | **multi-repo** | several repos that change together | their shared parent folder (personal to your machine) | investigation searches every listed repo, tasks stay inside one repo each, and `spec-ship` opens one PR per repo, each linking the others |
118
136
 
119
- To change the rules later, edit the file or run setup again, then run `npx @benjaminthomas/spec-driven-dev doctor`
137
+ Repos outside the workspace folder are saved with their full path. Paths that don't exist or
138
+ aren't git repos are skipped with a message, and setup stops if a multi-repo workspace ends up with
139
+ no repos. You can choose a different layout from the one detected: for example, choose
140
+ `multi-repo` from inside one repo and it uses the parent folder as the workspace, with your current
141
+ repo pre-selected.
142
+
143
+ Every question also has a flag, for scripts and CI:
144
+ `--layout=multi-repo --repos=api,web,~/clients/billing`, `--layout=monorepo --packages=apps/web,packages/ui`,
145
+ `--tools=cursor,codex`, `--global`, `--yes` (accept everything detected).
146
+
147
+ To change the rules later, edit the file or run setup again, then run `spec-driven-dev doctor`
120
148
  to confirm the file still matches your repos. Setup never overwrites the file without
121
149
  asking. See [`config-template.yml`](skills/spec-setup/references/config-template.yml) for every field.
122
150
 
@@ -326,6 +354,24 @@ Yes. Every skill works on its own. `spec-verify` even works without a spec: it a
326
354
  Yes. Progress is saved in the spec's `README.md` checkboxes and the commit for each batch. Run
327
355
  `spec-implement` again to continue.
328
356
 
357
+ **How do I update?**
358
+ Run `npm install -g @benjaminthomas/spec-driven-dev@latest`, then `spec-driven-dev` in your project.
359
+ Claude Code plugin users run
360
+ `/plugin marketplace update spec-driven-dev` and then update the plugin. Your config is never
361
+ touched by an update.
362
+
363
+ **Can I trust the npm package?**
364
+ Releases are published by this repo's GitHub Actions workflow, not from anyone's laptop, through
365
+ npm [trusted publishing](https://docs.npmjs.com/trusted-publishers). Each version's npm page shows
366
+ a provenance badge linking it to the exact commit and workflow run that built it. The package has
367
+ no dependencies, and it contains only `bin/cli.mjs` and the `skills/` folder.
368
+
369
+ **Something isn't working. Where do I start?**
370
+ Run `spec-driven-dev doctor` in your project. It checks the config against your
371
+ repos and names anything that doesn't match. If that passes, open an
372
+ [issue](https://github.com/benjaminthomas/spec-driven-dev/issues) with the doctor output and the
373
+ skill you were running.
374
+
329
375
  **`spec-verify` says it skipped some UI checks.**
330
376
  Your tool needs browser automation (an MCP browser server, a built-in browser tool, or an
331
377
  extension) for checks that involve the UI. Checks that only touch the API or database work
@@ -338,23 +384,41 @@ everywhere.
338
384
  ### Any tool: npm (recommended)
339
385
 
340
386
  ```bash
341
- npx @benjaminthomas/spec-driven-dev # detect tools, install, set up the project
342
- npx @benjaminthomas/spec-driven-dev setup # only (re)run project setup
343
- npx @benjaminthomas/spec-driven-dev --global # install for all projects, not just this one
344
- npx @benjaminthomas/spec-driven-dev --tools=cursor,codex # pick tools without being asked
345
- npx @benjaminthomas/spec-driven-dev --skills=spec-create,spec-ship # install only some skills
346
- npx @benjaminthomas/spec-driven-dev --yes # accept every detected default (CI-friendly)
347
- npx @benjaminthomas/spec-driven-dev doctor # check .spec-driven.yml against the real repos
348
- npx @benjaminthomas/spec-driven-dev status # list specs with unfinished work
387
+ npm install -g @benjaminthomas/spec-driven-dev # once per machine; adds the `spec-driven-dev` command
388
+ ```
389
+
390
+ Then, in a project:
391
+
392
+ ```bash
393
+ spec-driven-dev # detect tools, install skills, set up the project
394
+ spec-driven-dev setup # only (re)run project setup
395
+ spec-driven-dev --global # install skills for all projects, not just this one
396
+ spec-driven-dev --tools=cursor,codex # pick tools without being asked
397
+ spec-driven-dev --skills=spec-create,spec-ship # install only some skills
398
+ spec-driven-dev --yes # accept every detected default (CI-friendly)
399
+ spec-driven-dev doctor # check .spec-driven.yml against the real repos
400
+ spec-driven-dev status # list specs with unfinished work
401
+ spec-driven-dev --help # show all of the above
349
402
  ```
350
403
 
404
+ **Without installing:** replace `spec-driven-dev` with `npx @benjaminthomas/spec-driven-dev`, for
405
+ example `npx @benjaminthomas/spec-driven-dev setup`. That downloads and runs the latest version once,
406
+ which is handy in CI or on a machine you won't use again.
407
+
351
408
  `doctor` checks that every branch, folder, and `npm run` script named in the config exists, and
352
409
  that each value is one the skills understand. It exits 1 if anything is wrong, so you can run it in
353
410
  CI. `status` prints nothing when all specs are finished.
354
411
 
355
- Run it again at any time to update: it replaces the installed skill folders with the latest
356
- version. `npx github:benjaminthomas/spec-driven-dev` installs straight from GitHub (for example, an unreleased
357
- commit on `main`).
412
+ **Updating:** run `npm install -g @benjaminthomas/spec-driven-dev@latest` to get the newest CLI,
413
+ then `spec-driven-dev` in each project to refresh its skills. This replaces the installed skill
414
+ folders and leaves your `.spec-driven.yml` alone. If you use `npx`, add `@latest`, because `npx`
415
+ can otherwise reuse an older copy it cached. What changed in each version is in
416
+ [CHANGELOG.md](CHANGELOG.md).
417
+
418
+ `npx github:benjaminthomas/spec-driven-dev` installs straight from GitHub instead, for example to
419
+ try an unreleased commit on `main`.
420
+
421
+ **Requirements:** Node.js 18 or newer and git. The installer has no dependencies.
358
422
 
359
423
  ### Claude Code (plugin, with automatic updates)
360
424
 
@@ -369,8 +433,8 @@ The plugin also adds a **session-start reminder**. When you open Claude Code in
369
433
  unfinished specs, Claude is told which ones and what the next step is (for example,
370
434
  `specs/stp-123: 3/5 tasks done → next: spec-implement`). It prints nothing when everything is
371
435
  finished. If
372
- you use the plugin, `npx @benjaminthomas/spec-driven-dev` skips Claude Code so you don't get every skill twice. Run
373
- `npx @benjaminthomas/spec-driven-dev setup` for the project config.
436
+ you use the plugin, `spec-driven-dev` skips Claude Code so you don't get every skill twice. Run
437
+ `spec-driven-dev setup` for the project config.
374
438
 
375
439
  ### By hand
376
440
 
@@ -423,7 +487,7 @@ skips that step rather than failing.
423
487
 
424
488
  | Skill | Stage | SKILL.md lines | Words | Supporting files |
425
489
  |---|---|---:|---:|---:|
426
- | `spec-setup` | 0 · configure | 99 | 857 | 1 |
490
+ | `spec-setup` | 0 · configure | 105 | 935 | 1 |
427
491
  | `spec-investigate` | 1 · understand | 323 | 2,708 | 1 |
428
492
  | `spec-create` | 2 · plan | 149 | 1,328 | 4 |
429
493
  | `spec-implement` | 3 · build | 236 | 1,790 | 3 |
@@ -431,12 +495,12 @@ skips that step rather than failing.
431
495
  | `spec-checkpoint` | 5 · commit | 106 | 762 | 0 |
432
496
  | `spec-ship` | 6 · PR | 218 | 1,235 | 0 |
433
497
  | `generative-seo` | standalone | 255 | 2,209 | 7 |
434
- | **Total** | | **1,539** | **12,348** | **17** |
498
+ | **Total** | | **1,545** | **12,426** | **17** |
435
499
 
436
500
  - **8** skills: **1** setup + **6** workflow stages + **1** standalone
437
501
  - **4** tools supported natively (Claude Code, Codex CLI, Cursor, Antigravity), plus an MCP fallback
438
502
  - **3** layouts: single repo, monorepo, multi-repo
439
- - **1** install command, `npx @benjaminthomas/spec-driven-dev`, plus the Claude Code plugin and an MCP fallback
503
+ - **1** npm package, `@benjaminthomas/spec-driven-dev`, plus the Claude Code plugin and an MCP fallback
440
504
  - **0** stack assumptions: lint, typecheck, test, build, and dev commands come from your repo
441
505
 
442
506
  _The table is generated by `python3 scripts/check.py --fix`; CI fails if it goes out of date._
@@ -450,19 +514,22 @@ skills/<name>/SKILL.md the skill itself (frontmatter: name, description,
450
514
  skills/<name>/references/ templates and prompts the skill loads when needed
451
515
  .claude-plugin/ Claude Code plugin + marketplace manifests
452
516
  .agents/skills/ symlinks so Codex/Antigravity pick the skills up inside this repo
453
- bin/cli.mjs the npx installer + project setup (no dependencies)
517
+ bin/cli.mjs the `spec-driven-dev` CLI: installer, setup, doctor, status (no dependencies)
454
518
  package.json npm package (ships bin/ and skills/)
455
519
  mcp-server/ MCP fallback server
456
520
  hooks/hooks.json Claude Code session-start hook (runs `cli.mjs status`)
457
521
  CHANGELOG.md what changed in each version
458
522
  scripts/check.py consistency checks + metrics generator
459
523
  scripts/test-cli.mjs installer self-test (fake HOME, throwaway repos)
524
+ .github/workflows/check.yml CI: consistency checks + installer self-test on every push and PR
525
+ .github/workflows/publish.yml publishes to npm when a vX.Y.Z tag is pushed
460
526
  evals/ `claude plugin eval` cases
461
527
  ```
462
528
 
463
529
  - **Adding a skill:** create `skills/<name>/SKILL.md`, then add the name to the two manifests, a `.agents/skills/<name>` symlink, the `STAGES` map in
464
530
  `scripts/check.py`, and this README. `python3 scripts/check.py` lists anything you missed.
465
- - **Before pushing:** run `python3 scripts/check.py --fix`. CI (`.github/workflows/check.yml`) runs
531
+ - **Before pushing:** run `python3 scripts/check.py --fix`, then `npm test` (which runs the checks
532
+ and the installer self-test). CI (`.github/workflows/check.yml`) runs
466
533
  the same checks, the installer self-test, and a syntax check on the MCP server.
467
534
  - **Evals:** `claude plugin eval . --runs 1` checks that skills trigger on their phrases and stay
468
535
  within their own stage. These runs use your Claude credentials, so they aren't part of CI.
@@ -476,8 +543,15 @@ evals/ `claude plugin eval` cases
476
543
  - **Releasing:**
477
544
  1. Increase `version` in both `package.json` and `.claude-plugin/plugin.json` (the check fails if
478
545
  they differ).
479
- 2. Add a `CHANGELOG.md` entry.
480
- 3. Push, then `npm publish`.
546
+ 2. Add a `CHANGELOG.md` entry, then commit and push.
547
+ 3. Tag the release and push the tag: `git tag v1.4.1 && git push origin v1.4.1`.
548
+ 4. Watch the **publish** run in the repo's Actions tab. When it's green, the new version is on
549
+ npm. Claude Code plugin users get it from the marketplace once the commit is on `main`.
550
+
551
+ `.github/workflows/publish.yml` then runs the tests and publishes to npm through
552
+ [trusted publishing](https://docs.npmjs.com/trusted-publishers). No npm token or passkey is
553
+ needed, and the package page shows a provenance badge linking the release to the exact commit.
554
+ The workflow refuses to publish if the tag doesn't match `package.json`.
481
555
 
482
556
  ## License
483
557
 
package/bin/cli.mjs CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
- // spec-driven-dev installer: detects your AI coding tools, copies the skills into each one, and
2
+ // spec-driven-dev installer (npm i -g @benjaminthomas/spec-driven-dev): detects your AI coding tools, copies the skills into each one, and
3
3
  // runs project discovery (layout, tracker, branches, commands) to write .spec-driven.yml.
4
4
  //
5
- // npx @benjaminthomas/spec-driven-dev install + project setup (interactive)
6
- // npx @benjaminthomas/spec-driven-dev setup project setup only
7
- // npx @benjaminthomas/spec-driven-dev doctor check .spec-driven.yml against the real repos (exit 1 on problems)
8
- // npx @benjaminthomas/spec-driven-dev status list specs with unfinished tasks (used by the session-start hook)
5
+ // spec-driven-dev install + project setup (interactive)
6
+ // spec-driven-dev setup project setup only
7
+ // spec-driven-dev doctor check .spec-driven.yml against the real repos (exit 1 on problems)
8
+ // spec-driven-dev status list specs with unfinished tasks (used by the session-start hook)
9
9
  // flags: --yes --global --tools=claude,cursor --skills=spec-create,... --no-setup --dir=<path>
10
+ // --layout=single|monorepo|multi-repo --repos=api,../web,/abs/path --packages=apps/web,packages/ui
10
11
  import fs from "node:fs";
11
12
  import os from "node:os";
12
13
  import path from "node:path";
@@ -35,7 +36,7 @@ const YES = flag("yes") || flag("y") || !process.stdin.isTTY || ["doctor", "stat
35
36
  const DIR = path.resolve(opt("dir") || process.cwd());
36
37
 
37
38
  if (flag("help") || flag("h")) {
38
- console.log(fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1, 9).join("\n").replace(/^\/\/ ?/gm, ""));
39
+ console.log(fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1, 10).join("\n").replace(/^\/\/ ?/gm, ""));
39
40
  process.exit(0);
40
41
  }
41
42
 
@@ -184,26 +185,83 @@ function toYaml(c) {
184
185
  return out + `specs_dir: ${y(c.specsDir)}\n`;
185
186
  }
186
187
 
188
+ const LAYOUTS = ["single", "monorepo", "multi-repo"];
189
+ const splitList = (v) => (v || "").split(",").map((x) => x.trim()).filter(Boolean);
190
+ // Paths are stored relative to the config's folder when inside it, absolute otherwise.
191
+ const relOrAbs = (root, p) => { const r = path.relative(root, p); return r.startsWith("..") || path.isAbsolute(r) ? p : r || "."; };
192
+
193
+ async function chooseLayout(detected) {
194
+ const forced = opt("layout");
195
+ if (forced && !LAYOUTS.includes(forced)) throw new Error(`--layout must be one of ${LAYOUTS.join(", ")}`);
196
+ if (forced) return forced;
197
+ for (;;) {
198
+ const a = await ask(`Layout — single, monorepo, or multi-repo (detected: ${detected})`, detected);
199
+ if (LAYOUTS.includes(a)) return a;
200
+ console.log(` Please type one of: ${LAYOUTS.join(", ")}`);
201
+ }
202
+ }
203
+
204
+ // multi-repo: pick from repos found in the workspace folder, then add any others by path.
205
+ async function chooseRepos(d) {
206
+ // Inside one repo but multi-repo chosen → the workspace is the folder that holds this repo.
207
+ const insideRepo = d.layout !== "multi-repo";
208
+ const defRoot = insideRepo ? path.dirname(d.root) : d.root;
209
+ const root = path.resolve(DIR, await ask("Workspace folder (where .spec-driven.yml goes)", defRoot));
210
+ const found = detectLayout(root).layout === "multi-repo" ? detectLayout(root).repos : [];
211
+ let picked = [];
212
+ const fromFlag = opt("repos");
213
+ if (fromFlag) {
214
+ picked = splitList(fromFlag).map((p) => path.resolve(root, p));
215
+ } else {
216
+ if (found.length) {
217
+ const owners = {};
218
+ for (const r of found) (owners[gitFacts(path.join(root, r))?.owner || "?"] ||= []).push(r);
219
+ const biggest = Object.values(owners).sort((a, b) => b.length - a.length)[0] || [];
220
+ const here = insideRepo ? [path.relative(root, d.root)] : [];
221
+ console.log(`Found ${found.length} git repos in ${root}. Which ones change together as one workspace?`);
222
+ picked = (await pick("Repos", found.map((r) => ({ value: r, label: `${r} (${Object.keys(owners).find((o) => owners[o].includes(r))})` })), [...new Set([...here, ...biggest])]))
223
+ .map((r) => path.join(root, r));
224
+ } else if (insideRepo) picked = [d.root];
225
+ const extra = await ask("Add repos from other folders? Paths, comma-separated (absolute, ~/…, or relative to the workspace folder); empty for none", "");
226
+ picked.push(...splitList(extra).map((p) => path.resolve(root, p.replace(/^~(?=$|\/)/, HOME))));
227
+ }
228
+ const repos = [];
229
+ for (const p of [...new Set(picked)]) {
230
+ if (!fs.existsSync(p)) console.log(` ✗ ${p}: folder not found — skipped`);
231
+ else if (!isRepo(p)) console.log(` ✗ ${p}: not a git repo — skipped`);
232
+ else repos.push(relOrAbs(root, p));
233
+ }
234
+ if (!repos.length) throw new Error("A multi-repo workspace needs at least one git repo. Re-run setup and pick or add repos (or use --repos=a,b).");
235
+ console.log(` Workspace repos: ${repos.join(", ")}`);
236
+ return { root, repos };
237
+ }
238
+
239
+ // monorepo: confirm or edit the package folders (relative to the repo root).
240
+ async function choosePackages(d) {
241
+ const root = d.root;
242
+ const fromFlag = opt("packages");
243
+ const answer = fromFlag || (await ask("Package folders, comma-separated, relative to the repo root (empty if none yet)", d.repos.join(",")));
244
+ const pkgs = [];
245
+ for (const p of splitList(answer)) {
246
+ if (fs.existsSync(path.resolve(root, p))) pkgs.push(relOrAbs(root, path.resolve(root, p)));
247
+ else console.log(` ✗ ${p}: folder not found in ${root} — skipped`);
248
+ }
249
+ return { root, repos: pkgs };
250
+ }
251
+
187
252
  async function setup() {
188
253
  console.log("\nProject setup — detecting how this project works…");
189
254
  const d = detectLayout(DIR);
190
- let repos = d.repos;
191
- if (d.layout === "multi-repo") {
192
- const owners = {};
193
- for (const r of repos) (owners[gitFacts(path.join(d.root, r))?.owner || "?"] ||= []).push(r);
194
- const biggest = Object.values(owners).sort((a, b) => b.length - a.length)[0] || [];
195
- console.log(`Found ${repos.length} git repos in ${d.root}. Which ones change together as one workspace?`);
196
- repos = await pick("Repos", repos.map((r) => ({ value: r, label: `${r} (${Object.keys(owners).find((o) => owners[o].includes(r))})` })), biggest);
197
- }
198
- const layout = await ask(`Layout — single, monorepo, or multi-repo (detected: ${d.layout})`, d.layout);
199
- const root = d.root;
255
+ const layout = await chooseLayout(d.layout);
256
+ const { root, repos } = layout === "multi-repo" ? await chooseRepos(d) : layout === "monorepo" ? await choosePackages(d) : { root: d.layout === "multi-repo" ? DIR : d.root, repos: [] };
257
+ if (layout === "single" && !isRepo(root)) console.log(` • ${root} isn't a git repo — run setup inside the repo, or choose multi-repo.`);
200
258
  const file = path.join(root, ".spec-driven.yml");
201
259
  if (fs.existsSync(file) && (await ask(`${file} exists — overwrite? (y/n)`, "n")) !== "y") {
202
260
  console.log("• Kept the existing config. Run the spec-setup skill in your AI tool to review it.");
203
261
  return;
204
262
  }
205
263
 
206
- const repoDirs = layout === "multi-repo" ? repos.map((r) => path.join(root, r)) : [isRepo(root) ? root : DIR];
264
+ const repoDirs = layout === "multi-repo" ? repos.map((r) => path.resolve(root, r)) : [root];
207
265
  const facts = repoDirs.map(gitFacts).filter(Boolean);
208
266
  const g = infer(facts);
209
267
  if (facts.length) console.log(`Detected: tracker ${g.trackerType}${g.key ? ` (${g.key}-…)` : ""}, base ${g.base}, branches "${g.branchName}", commits ${g.commitStyle}`);
@@ -221,10 +279,10 @@ async function setup() {
221
279
  layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "",
222
280
  git: { remote: "origin", base_branch: base, pr_target: pr, branch_name: branchName, commit_style: commitStyle },
223
281
  commands: layout === "single" ? commandsFor(repoDirs[0]) : commandsFor(root),
224
- repos: layout === "single" ? [] : (layout === "multi-repo" ? repos : d.repos).map((r) => {
225
- const f = layout === "multi-repo" ? gitFacts(path.join(root, r)) : null;
282
+ repos: repos.map((r) => {
283
+ const f = layout === "multi-repo" ? gitFacts(path.resolve(root, r)) : null;
226
284
  // a repo without an integration branch must say so, or it inherits the top-level base (e.g. staging) it doesn't have
227
- return { path: r, role: "", base: f ? f.integration || "default" : "", commands: commandsFor(path.join(root, r)) };
285
+ return { path: r, role: "", base: f ? f.integration || "default" : "", commands: commandsFor(path.resolve(root, r)) };
228
286
  }),
229
287
  };
230
288
  fs.writeFileSync(file, toYaml(config));
@@ -279,7 +337,7 @@ function findConfig(dir) {
279
337
 
280
338
  function doctor() {
281
339
  const file = findConfig(DIR);
282
- if (!file) throw new Error(`No .spec-driven.yml in ${DIR} or above. Run: npx @benjaminthomas/spec-driven-dev setup`);
340
+ if (!file) throw new Error(`No .spec-driven.yml in ${DIR} or above. Run: spec-driven-dev setup (or npx @benjaminthomas/spec-driven-dev setup)`);
283
341
  const root = path.dirname(file);
284
342
  let c;
285
343
  try { c = parseYaml(fs.readFileSync(file, "utf8")); } catch (e) { throw new Error(`${file} could not be read: ${e.message}`); }
@@ -291,7 +349,7 @@ function doctor() {
291
349
  const repos = c.layout === "single" ? [{ path: "." }] : Array.isArray(c.repos) ? c.repos : [];
292
350
  if (c.layout !== "single" && !repos.length) problems.push(`layout is ${c.layout} but repos: is empty`);
293
351
  for (const r of repos) {
294
- const dir = path.join(root, r.path || "");
352
+ const dir = path.resolve(root, r.path || ".");
295
353
  const label = r.path === "." ? "repo" : r.path;
296
354
  if (!fs.existsSync(dir)) { problems.push(`${label}: folder not found`); continue; }
297
355
  const gitDir = c.layout === "monorepo" ? root : dir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benjaminthomas/spec-driven-dev",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Spec-driven AI development workflow (investigate → spec → implement → verify → ship) as Agent Skills for Claude Code, Cursor, Codex CLI and Antigravity.",
5
5
  "type": "module",
6
6
  "bin": { "spec-driven-dev": "bin/cli.mjs" },
@@ -41,7 +41,13 @@ Read, don't guess. Collect a draft of every field in `references/config-template
41
41
  `monorepo`; list the packages.
42
42
  - Not inside a git repo, but sibling folders are git repos → candidate `multi-repo`. Group them
43
43
  by remote owner (`git remote get-url origin`) and show the groups — the user picks which repos
44
- belong together. Never assume every folder in the parent belongs.
44
+ belong together. Never assume every folder in the parent belongs. Always also ask whether
45
+ repos in **other folders** belong (e.g. `~/clients/billing`). Check each path is a git repo,
46
+ and store paths inside the workspace as relative and paths outside it as absolute.
47
+ - The user can override the detected layout. If they choose `multi-repo` from inside one repo,
48
+ the workspace folder defaults to that repo's parent, with the current repo pre-selected. If
49
+ they choose `monorepo`, ask for the package folders. Never write a `multi-repo` config with
50
+ no repos.
45
51
  - Otherwise → `single`.
46
52
  2. **Existing written rules.** `CLAUDE.md`, `AGENTS.md`, `CONTRIBUTING.md`, PR templates,
47
53
  `.github/workflows`. Anything stated there is a strong default — quote it back to the user.