@benjaminthomas/spec-driven-dev 1.4.1 → 1.5.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.
package/README.md CHANGED
@@ -12,8 +12,8 @@ the PR.
12
12
  It's a set of eight [Agent Skills](https://agentskills.io), plain Markdown instruction files that
13
13
  AI coding tools load on demand. It works with Claude Code, Codex CLI, Cursor, and Google Antigravity,
14
14
  in any language or framework, and in a single repo, a monorepo, or several repos that change
15
- together. Install it from npm with `npm install -g @benjaminthomas/spec-driven-dev`, then run
16
- `spec-driven-dev` in your project.
15
+ together, even when those repos live in different folders. To set it up, run
16
+ `npx @benjaminthomas/spec-driven-dev@latest` in your project.
17
17
 
18
18
  ---
19
19
 
@@ -54,17 +54,21 @@ start a new session and continue where you left off.
54
54
 
55
55
  ## Quick start (5 minutes)
56
56
 
57
- **1. Install the CLI from npm** (once per machine):
57
+ **1. Run the installer in your project** (for several repos, run it inside any one of them):
58
58
 
59
59
  ```bash
60
- npm install -g @benjaminthomas/spec-driven-dev
60
+ npx @benjaminthomas/spec-driven-dev@latest
61
61
  ```
62
62
 
63
- **2. Run it in your project folder** (or, for several repos, in their shared parent folder):
63
+ This one command downloads the tool, installs the skills, and sets up the project. It asks its
64
+ questions in your terminal, so run it in a real terminal, not through another tool. If there's no
65
+ terminal to answer in, it says so and uses the detected values.
64
66
 
65
- ```bash
66
- spec-driven-dev
67
- ```
67
+ > **Why not `npm install -g`?** npm doesn't let a package ask questions while it installs, and an
68
+ > install doesn't know which project it's for. That's why setup is a separate step you run inside
69
+ > the project. If you'll use the `doctor`/`status` commands often, you can also install it
70
+ > globally: `npm install -g @benjaminthomas/spec-driven-dev`, then run `spec-driven-dev` in place of
71
+ > the `npx …` command.
68
72
 
69
73
  It:
70
74
  1. **Finds your AI tools** (Claude Code, Cursor, Codex CLI, Antigravity) and installs the skills
@@ -89,6 +93,11 @@ words in quotes.
89
93
  /spec-ship "ship it"
90
94
  ```
91
95
 
96
+ **Fewer stops:** after reviewing the spec, say **"approved, run it through"**. The workflow then
97
+ implements, verifies, and commits in one go. It stops at the first failure, and it always asks
98
+ before pushing or opening a PR. To make this the default after approval, choose `run-through` during
99
+ setup (`pipeline.after_approval` in `.spec-driven.yml`).
100
+
92
101
  That's the whole workflow. Every stage can be used on its own as well: use `/spec-create` straight
93
102
  from a planning chat, or `/spec-checkpoint` on any work in progress.
94
103
 
@@ -115,9 +124,9 @@ can usually just press Enter):
115
124
  | Which AI tools get the skills | tool folders in your home directory and commands on your `PATH` | Claude Code, Cursor |
116
125
  | This project only, or all projects | (you choose) | `project` |
117
126
  | Project layout | git repos and workspace files (`pnpm-workspace.yaml`, `turbo.json`, `nx.json`, `go.work`, …) | `single`, `monorepo`, or `multi-repo` |
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` |
127
+ | *multi-repo:* which repos nearby belong | git repos up to two folders down, grouped by GitHub owner | `frontend` |
128
+ | *multi-repo:* repos in other places | (you type paths: absolute, `~/…`, or relative) | `~/Work/backend, /srv/database` |
129
+ | *multi-repo:* where the config lives | the repos' shared parent folder, or `~/.spec-driven/<name>/` | `~/Projects` |
121
130
  | *monorepo:* package folders | the workspaces your `package.json`/`pnpm-workspace.yaml` declares, or `apps/*`, `packages/*` | `apps/web, packages/ui` |
122
131
  | Issue tracker | ticket keys in branch names and commits | Jira, key `STP` |
123
132
  | Base branch / PR target | default branch, `staging`/`develop` branches | `staging` |
@@ -125,6 +134,7 @@ can usually just press Enter):
125
134
  | Commit style | your recent commit messages | `conventional` (`feat: …`) or `ticket` (`STP-123: …`) |
126
135
  | Lint / typecheck / test / build / dev commands | `package.json` scripts and lockfile (per repo or package) | `pnpm run lint` |
127
136
  | Where specs go | (you choose) | `specs/` |
137
+ | After you approve a spec | (you choose) | `stop` (wait at each stage) or `run-through` |
128
138
 
129
139
  **Where the file lives, by layout:**
130
140
 
@@ -132,17 +142,32 @@ can usually just press Enter):
132
142
  |---|---|---|---|
133
143
  | **single** | one repo, one app | the repo root (commit it, so the team shares it) | as described below |
134
144
  | **monorepo** | one repo, many apps/packages | the repo root (commit it) | tasks name their package, and checks run per package |
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 |
145
+ | **multi-repo** | several repos that change together, in any folders | their shared parent folder, or `~/.spec-driven/<name>/` (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 |
136
146
 
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.
147
+ **Repos in different places.** A multi-repo workspace doesn't need its repos in one folder. For
148
+ example, from inside `/Projects/frontend`:
149
+
150
+ ```
151
+ Layout — single, monorepo, or multi-repo (detected: single) [single]: multi-repo
152
+ Found 1 git repos under /Projects. Which ones belong to this workspace?
153
+ 1. frontend
154
+ Repos [1]:
155
+ Other repo paths, comma-separated (empty for none): ~/Work/backend, /srv/database
156
+ Where should this workspace's .spec-driven.yml live [~/.spec-driven/frontend-workspace]:
157
+ ```
158
+
159
+ - **Where the config goes:** the repos' shared parent folder, or `~/.spec-driven/<name>/` when that
160
+ parent would be your home folder or `/`.
161
+ - **How every repo finds it:** setup records the workspace in `~/.spec-driven/workspaces.json`, so
162
+ `/Work/backend` finds its config even though the config isn't above it. The skills, `doctor`,
163
+ and `status` all look there.
164
+ - **Paths are checked:** ones that don't exist or aren't git repos are skipped with a message, and
165
+ setup stops if a workspace would end up with no repos.
142
166
 
143
167
  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).
168
+ `--layout=multi-repo --repos=~/Projects/frontend,~/Work/backend --workspace=~/Projects`,
169
+ `--layout=monorepo --packages=apps/web,packages/ui`, `--after-approval=run-through`,
170
+ `--tools=cursor,codex`, `--global`, `--yes` (accept everything detected), and `--version`.
146
171
 
147
172
  To change the rules later, edit the file or run setup again, then run `spec-driven-dev doctor`
148
173
  to confirm the file still matches your repos. Setup never overwrites the file without
@@ -344,6 +369,18 @@ config sits outside any repo, so it stays on your machine.
344
369
  **Does it work with my stack?**
345
370
  Yes. No skill assumes a language or framework. Each one finds the repo's own commands.
346
371
 
372
+ **Do I have to start every stage myself?**
373
+ No. After you approve a spec, say "approved, run it through": implement, verify, and checkpoint then
374
+ run one after another, stopping at the first failure. Two steps always wait for you: approving the
375
+ spec, and pushing or opening the PR. You don't need slash commands either. Saying "investigate
376
+ STP-123" or "ship it" picks the right skill.
377
+
378
+ **Setup didn't ask me about repos.**
379
+ It only asks about repos for a multi-repo or monorepo layout. If it detected `single`, type
380
+ `multi-repo` at the layout question, then add your other repos by path. If it asked nothing at all,
381
+ there was no terminal to type answers into (it prints a note saying so), so run it directly in a
382
+ terminal. Check which version ran with `npx @benjaminthomas/spec-driven-dev@latest --version`.
383
+
347
384
  **Can I skip stages?**
348
385
  Yes. Every skill works on its own. `spec-verify` even works without a spec: it asks what to check.
349
386
 
@@ -487,15 +524,15 @@ skips that step rather than failing.
487
524
 
488
525
  | Skill | Stage | SKILL.md lines | Words | Supporting files |
489
526
  |---|---|---:|---:|---:|
490
- | `spec-setup` | 0 · configure | 105 | 935 | 1 |
491
- | `spec-investigate` | 1 · understand | 323 | 2,708 | 1 |
492
- | `spec-create` | 2 · plan | 149 | 1,328 | 4 |
493
- | `spec-implement` | 3 · build | 236 | 1,790 | 3 |
494
- | `spec-verify` | 4 · prove | 153 | 1,459 | 1 |
495
- | `spec-checkpoint` | 5 · commit | 106 | 762 | 0 |
496
- | `spec-ship` | 6 · PR | 218 | 1,235 | 0 |
527
+ | `spec-setup` | 0 · configure | 114 | 1,022 | 1 |
528
+ | `spec-investigate` | 1 · understand | 325 | 2,741 | 1 |
529
+ | `spec-create` | 2 · plan | 157 | 1,415 | 4 |
530
+ | `spec-implement` | 3 · build | 273 | 2,109 | 3 |
531
+ | `spec-verify` | 4 · prove | 155 | 1,492 | 1 |
532
+ | `spec-checkpoint` | 5 · commit | 108 | 795 | 0 |
533
+ | `spec-ship` | 6 · PR | 220 | 1,268 | 0 |
497
534
  | `generative-seo` | standalone | 255 | 2,209 | 7 |
498
- | **Total** | | **1,545** | **12,426** | **17** |
535
+ | **Total** | | **1,607** | **13,051** | **17** |
499
536
 
500
537
  - **8** skills: **1** setup + **6** workflow stages + **1** standalone
501
538
  - **4** tools supported natively (Claude Code, Codex CLI, Cursor, Antigravity), plus an MCP fallback
package/bin/cli.mjs CHANGED
@@ -7,7 +7,8 @@
7
7
  // spec-driven-dev doctor check .spec-driven.yml against the real repos (exit 1 on problems)
8
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
+ // --layout=single|monorepo|multi-repo --repos=~/Projects/frontend,/srv/backend --workspace=<config dir>
11
+ // --packages=apps/web,packages/ui --after-approval=stop|run-through --version
11
12
  import fs from "node:fs";
12
13
  import os from "node:os";
13
14
  import path from "node:path";
@@ -35,8 +36,15 @@ const command = args.find((a) => !a.startsWith("-")) || "install";
35
36
  const YES = flag("yes") || flag("y") || !process.stdin.isTTY || ["doctor", "status"].includes(command);
36
37
  const DIR = path.resolve(opt("dir") || process.cwd());
37
38
 
39
+ if (flag("version") || flag("v")) {
40
+ console.log(JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version);
41
+ process.exit(0);
42
+ }
43
+ if (["install", "setup"].includes(command) && !process.stdin.isTTY && !flag("yes") && !flag("y"))
44
+ console.log("• No interactive terminal detected, so no questions will be asked: using the detected values.\n Run it directly in a terminal to answer them, or pass flags (--layout, --repos, --tools …). Add --yes to hide this note.");
45
+
38
46
  if (flag("help") || flag("h")) {
39
- console.log(fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1, 10).join("\n").replace(/^\/\/ ?/gm, ""));
47
+ console.log(fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1, 11).join("\n").replace(/^\/\/ ?/gm, ""));
40
48
  process.exit(0);
41
49
  }
42
50
 
@@ -103,6 +111,21 @@ const isRepo = (d) => run("git", ["rev-parse", "--is-inside-work-tree"], d) ===
103
111
  const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } };
104
112
  const subdirs = (d) => { try { return fs.readdirSync(d, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name); } catch { return []; } };
105
113
 
114
+ // Git repos up to two folders down (e.g. ~/work/client/api). A git worktree has a .git *file*;
115
+ // it belongs to its main repo, so it is skipped. Repos aren't searched inside.
116
+ function findRepos(dir, depth = 2, prefix = "") {
117
+ const out = [];
118
+ for (const n of subdirs(dir)) {
119
+ if (n === "node_modules") continue;
120
+ const full = path.join(dir, n);
121
+ let isGitDir = false;
122
+ try { isGitDir = fs.statSync(path.join(full, ".git")).isDirectory(); } catch {}
123
+ if (isGitDir) out.push(prefix + n);
124
+ else if (depth > 1) out.push(...findRepos(full, depth - 1, `${prefix}${n}/`));
125
+ }
126
+ return out;
127
+ }
128
+
106
129
  function detectLayout(dir) {
107
130
  if (isRepo(dir)) {
108
131
  const top = run("git", ["rev-parse", "--show-toplevel"], dir);
@@ -121,8 +144,7 @@ function detectLayout(dir) {
121
144
  const mono = MARKERS.some((m) => fs.existsSync(path.join(top, m))) || pkg?.workspaces || cargo || packages.length > 1;
122
145
  return { layout: mono ? "monorepo" : "single", root: top, repos: mono ? packages : [] };
123
146
  }
124
- // a git worktree has a .git *file*; it belongs to its main repo, so skip it
125
- const repos = subdirs(dir).filter((n) => { try { return fs.statSync(path.join(dir, n, ".git")).isDirectory(); } catch { return false; } });
147
+ const repos = findRepos(dir);
126
148
  return repos.length ? { layout: "multi-repo", root: dir, repos } : { layout: "single", root: dir, repos: [] };
127
149
  }
128
150
 
@@ -182,13 +204,15 @@ function toYaml(c) {
182
204
  out += `tracker:\n type: ${c.tracker.type}\n key: ${y(c.tracker.key)}\n url: ${y(c.tracker.url)}\n`;
183
205
  out += `git:\n${cmds(c.git, " ")}\n`;
184
206
  out += `commands:\n${cmds(c.commands, " ")}\n dev_url: ${y(c.devUrl)}\n`;
207
+ out += `pipeline:\n after_approval: ${c.afterApproval || "stop"} # stop | run-through (implement → verify → checkpoint; ship always asks)\n`;
185
208
  return out + `specs_dir: ${y(c.specsDir)}\n`;
186
209
  }
187
210
 
188
211
  const LAYOUTS = ["single", "monorepo", "multi-repo"];
189
212
  const splitList = (v) => (v || "").split(",").map((x) => x.trim()).filter(Boolean);
190
213
  // 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 || "."; };
214
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return p; } }; // /var vs /private/var etc.
215
+ const relOrAbs = (root, p) => { const r = path.relative(real(root), real(p)); return r.startsWith("..") || path.isAbsolute(r) ? real(p) : r || "."; };
192
216
 
193
217
  async function chooseLayout(detected) {
194
218
  const forced = opt("layout");
@@ -201,41 +225,74 @@ async function chooseLayout(detected) {
201
225
  }
202
226
  }
203
227
 
204
- // multi-repo: pick from repos found in the workspace folder, then add any others by path.
228
+ const expandHome = (p) => p.replace(/^~(?=$|\/)/, HOME);
229
+
230
+ function commonParent(paths) {
231
+ if (!paths.length) return "";
232
+ let parts = paths[0].split(path.sep);
233
+ for (const p of paths.slice(1)) { const q = p.split(path.sep); let i = 0; while (i < parts.length && parts[i] === q[i]) i++; parts = parts.slice(0, i); }
234
+ return parts.join(path.sep) || path.sep;
235
+ }
236
+
237
+ // multi-repo: the repos can live anywhere (/Projects/frontend, /Work/backend, …). Pick the ones found
238
+ // nearby, add any others by path, then decide where the config lives.
205
239
  async function chooseRepos(d) {
206
- // Inside one repo but multi-repo chosen → the workspace is the folder that holds this repo.
207
240
  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 : [];
241
+ const scanRoot = insideRepo ? path.dirname(d.root) : d.root;
211
242
  let picked = [];
212
243
  const fromFlag = opt("repos");
213
244
  if (fromFlag) {
214
- picked = splitList(fromFlag).map((p) => path.resolve(root, p));
245
+ picked = splitList(fromFlag).map((p) => path.resolve(DIR, expandHome(p)));
215
246
  } else {
247
+ const found = findRepos(scanRoot);
216
248
  if (found.length) {
217
249
  const owners = {};
218
- for (const r of found) (owners[gitFacts(path.join(root, r))?.owner || "?"] ||= []).push(r);
250
+ for (const r of found) (owners[gitFacts(path.join(scanRoot, r))?.owner || "?"] ||= []).push(r);
219
251
  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));
252
+ const here = insideRepo ? [path.relative(scanRoot, d.root)] : [];
253
+ console.log(`Found ${found.length} git repos under ${scanRoot}. Which ones belong to this workspace?`);
254
+ picked = (await pick("Repos", found.map((r) => ({ value: r, label: `${r} (${Object.keys(owners).find((o) => owners[o].includes(r))})` })), [...new Set([...here, ...(insideRepo ? [] : biggest)])]))
255
+ .map((r) => path.join(scanRoot, r));
224
256
  } 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))));
257
+ console.log("Repos in other places can be part of the same workspace (e.g. ~/Work/backend, /srv/database).");
258
+ const extra = await ask("Other repo paths, comma-separated (empty for none)", "");
259
+ picked.push(...splitList(extra).map((p) => path.resolve(DIR, expandHome(p))));
227
260
  }
228
- const repos = [];
261
+ const abs = [];
229
262
  for (const p of [...new Set(picked)]) {
230
263
  if (!fs.existsSync(p)) console.log(` ✗ ${p}: folder not found — skipped`);
231
264
  else if (!isRepo(p)) console.log(` ✗ ${p}: not a git repo — skipped`);
232
- else repos.push(relOrAbs(root, p));
265
+ else abs.push(run("git", ["rev-parse", "--show-toplevel"], p) || p);
233
266
  }
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).");
267
+ if (!abs.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).");
268
+
269
+ // Config location: the repos' shared parent folder, unless that's too broad (home or /), in which
270
+ // case a named folder under ~/.spec-driven/. Repos outside it find the config via the registry.
271
+ const parent = commonParent([...new Set(abs.map(real))].map((p) => path.dirname(p)));
272
+ const tooBroad = !parent || parent === HOME || parent === path.parse(parent).root || HOME.startsWith(parent + path.sep);
273
+ // Ran in a workspace folder and every repo is under it → keep the config right there.
274
+ const allUnderScan = !insideRepo && abs.every((p) => real(p).startsWith(real(scanRoot) + path.sep));
275
+ const defRoot = opt("workspace") ? path.resolve(DIR, expandHome(opt("workspace")))
276
+ : allUnderScan ? scanRoot
277
+ : tooBroad ? path.join(HOME, ".spec-driven", path.basename(abs[0]) + "-workspace") : parent;
278
+ const root = path.resolve(DIR, expandHome(await ask("Where should this workspace's .spec-driven.yml live", defRoot)));
279
+ fs.mkdirSync(root, { recursive: true });
280
+ const repos = [...new Set(abs)].map((p) => relOrAbs(root, p));
235
281
  console.log(` Workspace repos: ${repos.join(", ")}`);
236
282
  return { root, repos };
237
283
  }
238
284
 
285
+ // ~/.spec-driven/workspaces.json maps each multi-repo config to its repos, so a repo outside the
286
+ // config's folder (e.g. /Work/backend for a config in /Projects) can still find it.
287
+ const REGISTRY = () => path.join(HOME, ".spec-driven", "workspaces.json");
288
+ function readRegistry() { return readJson(REGISTRY()) || {}; }
289
+ function register(file, repoPaths) {
290
+ const reg = readRegistry();
291
+ reg[file] = repoPaths;
292
+ fs.mkdirSync(path.dirname(REGISTRY()), { recursive: true });
293
+ fs.writeFileSync(REGISTRY(), JSON.stringify(reg, null, 2) + "\n");
294
+ }
295
+
239
296
  // monorepo: confirm or edit the package folders (relative to the repo root).
240
297
  async function choosePackages(d) {
241
298
  const root = d.root;
@@ -250,8 +307,13 @@ async function choosePackages(d) {
250
307
  }
251
308
 
252
309
  async function setup() {
310
+ if (opt("after-approval") && !["stop", "run-through"].includes(opt("after-approval"))) throw new Error("--after-approval must be stop or run-through");
253
311
  console.log("\nProject setup — detecting how this project works…");
254
312
  const d = detectLayout(DIR);
313
+ console.log(d.layout === "multi-repo" ? `Detected: ${d.repos.length} git repos under ${d.root} (multi-repo).`
314
+ : d.layout === "monorepo" ? `Detected: monorepo at ${d.root} with ${d.repos.length} package(s)${d.repos.length ? `: ${d.repos.join(", ")}` : ""}.`
315
+ : isRepo(d.root) ? `Detected: single repo at ${d.root}. If it works together with other repos, choose multi-repo.`
316
+ : `Detected: no git repo in ${d.root}. Choose multi-repo to list your repos, wherever they are.`);
255
317
  const layout = await chooseLayout(d.layout);
256
318
  const { root, repos } = layout === "multi-repo" ? await chooseRepos(d) : layout === "monorepo" ? await choosePackages(d) : { root: d.layout === "multi-repo" ? DIR : d.root, repos: [] };
257
319
  if (layout === "single" && !isRepo(root)) console.log(` • ${root} isn't a git repo — run setup inside the repo, or choose multi-repo.`);
@@ -274,9 +336,17 @@ async function setup() {
274
336
  const branchName = await ask("Branch name pattern ({ticket} {slug} {type})", g.branchName);
275
337
  const commitStyle = await ask("Commit style (conventional/ticket/freeform)", g.commitStyle);
276
338
  const specsDir = await ask("Where specs/<feature>/ folders go", "specs");
339
+ console.log("After you approve a spec, the workflow can stop and wait for you at each stage, or run");
340
+ console.log("implement → verify → checkpoint in one go (it stops at any failure, and never pushes without asking).");
341
+ let afterApproval = opt("after-approval");
342
+ if (!afterApproval) for (;;) {
343
+ afterApproval = await ask("After approval: stop or run-through", "stop");
344
+ if (["stop", "run-through"].includes(afterApproval)) break;
345
+ console.log(" Please type stop or run-through");
346
+ }
277
347
 
278
348
  const config = {
279
- layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "",
349
+ layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "", afterApproval,
280
350
  git: { remote: "origin", base_branch: base, pr_target: pr, branch_name: branchName, commit_style: commitStyle },
281
351
  commands: layout === "single" ? commandsFor(repoDirs[0]) : commandsFor(root),
282
352
  repos: repos.map((r) => {
@@ -286,6 +356,7 @@ async function setup() {
286
356
  }),
287
357
  };
288
358
  fs.writeFileSync(file, toYaml(config));
359
+ if (layout === "multi-repo") register(real(file), repos.map((r) => real(path.resolve(root, r))));
289
360
  console.log(`✓ Wrote ${file}`);
290
361
  console.log(isRepo(root) ? " Commit it so your team shares the same rules." : " This folder isn't a git repo, so the config is personal to this machine.");
291
362
  console.log(' Fill in repo roles, dev_url, and non-Node commands by asking your AI tool to "run spec-setup".');
@@ -331,8 +402,13 @@ function findConfig(dir) {
331
402
  for (let d = dir; ; d = path.dirname(d)) {
332
403
  const f = path.join(d, ".spec-driven.yml");
333
404
  if (fs.existsSync(f)) return f;
334
- if (d === path.dirname(d) || d === HOME) return null;
405
+ if (d === path.dirname(d) || d === HOME) break;
335
406
  }
407
+ // Not above us: a multi-repo workspace elsewhere may list this repo.
408
+ const here = real(dir);
409
+ for (const [file, repos] of Object.entries(readRegistry()))
410
+ if (fs.existsSync(file) && repos.some((r) => here === r || here.startsWith(r + path.sep))) return file;
411
+ return null;
336
412
  }
337
413
 
338
414
  function doctor() {
@@ -345,6 +421,7 @@ function doctor() {
345
421
  if (!["single", "monorepo", "multi-repo"].includes(c.layout)) problems.push(`layout "${c.layout}" must be single, monorepo, or multi-repo`);
346
422
  if (c.tracker && !["jira", "github", "linear", "none"].includes(c.tracker.type)) problems.push(`tracker.type "${c.tracker.type}" must be jira, github, linear, or none`);
347
423
  if (c.git?.commit_style && !["conventional", "ticket", "freeform"].includes(c.git.commit_style)) problems.push(`git.commit_style "${c.git.commit_style}" must be conventional, ticket, or freeform`);
424
+ if (c.pipeline?.after_approval && !["stop", "run-through"].includes(c.pipeline.after_approval)) problems.push(`pipeline.after_approval "${c.pipeline.after_approval}" must be stop or run-through`);
348
425
  if (c.git?.branch_name && !/\{(ticket|slug)\}/.test(c.git.branch_name)) problems.push(`git.branch_name "${c.git.branch_name}" needs {ticket} or {slug}`);
349
426
  const repos = c.layout === "single" ? [{ path: "." }] : Array.isArray(c.repos) ? c.repos : [];
350
427
  if (c.layout !== "single" && !repos.length) problems.push(`layout is ${c.layout} but repos: is empty`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benjaminthomas/spec-driven-dev",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
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" },
@@ -17,7 +17,9 @@ Create a checkpoint commit that captures all current changes — but only after
17
17
  ## Project config (read first)
18
18
 
19
19
  Load `.spec-driven.yml`: walk up from the current directory to the home folder, and the first one
20
- found wins (a `multi-repo` config only applies inside the repos it lists). Its values beat anything
20
+ found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
21
+ `~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
22
+ whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
21
23
  you would detect or assume. This skill uses `commands` (lint/typecheck/test/build), `git.commit_style`, and `layout`/`repos`.
22
24
 
23
25
  **Multi-repo:** run this in every listed repo that has changes, making one commit per repo with the same subject. **Monorepo:** run the checks for the packages that changed, or the root commands if that's how the repo works.
@@ -20,7 +20,9 @@ The key insight: implementation plans that live in a single file are either too
20
20
  ## Project config (read first)
21
21
 
22
22
  Load `.spec-driven.yml`: walk up from the current directory to the home folder, and the first one
23
- found wins (a `multi-repo` config only applies inside the repos it lists). Its values beat anything
23
+ found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
24
+ `~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
25
+ whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
24
26
  you would detect or assume. This skill uses `layout`/`repos` and `specs_dir`.
25
27
 
26
28
  **Monorepo / multi-repo:** every task names its package or repo, and its file paths are relative to that. Keep each task inside one repo. Cross-repo work becomes separate tasks, with the contract (API shape, event, schema) written into both.
@@ -136,9 +138,15 @@ Batch breakdown:
136
138
  Next steps:
137
139
  1. Review action-required.md for tasks you need to complete manually
138
140
  2. Review the requirements and task files
139
- 3. Use /spec-implement to start parallel implementation
141
+ 3. When you're happy with it, either:
142
+ - say "implement" to build it and stop for review, or
143
+ - say "approved, run it through" to build, verify and commit in one go (stops at any failure,
144
+ never pushes without asking)
140
145
  ```
141
146
 
147
+ If `.spec-driven.yml` has `pipeline.after_approval: run-through`, say that this project runs through
148
+ by default after approval. Never start implementing from this skill.
149
+
142
150
  ## Critical Rules
143
151
 
144
152
  - Every task file must be fully self-contained — this is the entire point of the spec structure. A coder agent reading only that file must know exactly what to do.
@@ -4,7 +4,7 @@ description: >
4
4
  Orchestrate parallel implementation of a feature specification by dispatching coder agents
5
5
  batch-by-batch with code review gates between batches. Use this skill when the user says
6
6
  "implement this feature", "start implementing", "run the spec", "execute the plan",
7
- "continue implementing", or wants to begin coding a previously planned feature from a
7
+ "continue implementing", "approved, run it through", or wants to begin coding a previously planned feature from a
8
8
  specs/{feature}/ folder. Also use when the user says "/spec-implement" (or their host's
9
9
  equivalent invocation syntax) or drags a spec folder into the conversation and asks to
10
10
  implement it. This skill does NOT write code itself — it orchestrates coder subagents that
@@ -26,7 +26,9 @@ The orchestrator never writes code itself. Its job is to:
26
26
  ## Project config (read first)
27
27
 
28
28
  Load `.spec-driven.yml`: walk up from the current directory to the home folder, and the first one
29
- found wins (a `multi-repo` config only applies inside the repos it lists). Its values beat anything
29
+ found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
30
+ `~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
31
+ whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
30
32
  you would detect or assume. This skill uses `layout`/`repos`, `commands` (lint/typecheck/test per repo or package), and `git.commit_style`.
31
33
 
32
34
  **Multi-repo:** create worktrees in each repo a task touches, run each repo's own commands in review, and make the batch commit in every repo that changed, using the same subject line.
@@ -220,6 +222,41 @@ Next steps:
220
222
  - Push and create a PR when ready
221
223
  ```
222
224
 
225
+ ## Run-through mode (after approval)
226
+
227
+ Run-through applies when the user asks for it ("approved, run it through", "build, verify and
228
+ commit it") or when `.spec-driven.yml` has `pipeline.after_approval: run-through` and the user has
229
+ approved the spec. In that case, don't stop after Step 9. Continue:
230
+
231
+ ```
232
+ Run-through progress:
233
+ - [ ] Implement all batches (Steps 1–9) — every review gate passed
234
+ - [ ] Verify — follow the spec-verify skill; every acceptance criterion PASS
235
+ - [ ] Checkpoint — follow the spec-checkpoint skill; every check passes, commit made
236
+ - [ ] Stop and hand off to spec-ship
237
+ ```
238
+
239
+ 1. **Gate on each stage.** Move on only if the previous stage fully passed. On any failure (a review
240
+ gate exhausted its 3 cycles, a criterion FAILed or was SKIPPED, a check failed), stop the
241
+ run-through and report where it stopped, why, and the command to resume (e.g. "fix X, then say
242
+ 'continue the run-through'").
243
+ 2. **Verify, then checkpoint.** Follow those skills' own instructions in full, including the
244
+ verification report. Don't shortcut them because you're running them in sequence.
245
+ 3. **Never push.** The run-through ends at a local commit. Finish with the summary below and ask
246
+ whether to run `spec-ship`. `spec-ship` still shows exactly what will be pushed and waits for a
247
+ yes.
248
+ 4. **No approval, no run-through.** If the spec hasn't been reviewed and approved in this
249
+ conversation, or the user only said "implement", do the normal Steps 1–9 and stop.
250
+
251
+ ```
252
+ Run-through complete for "{feature}":
253
+ - Implement: {N} batches, all review gates PASS
254
+ - Verify: {P}/{P} criteria PASS (specs/{feature}/verification-report.md)
255
+ - Checkpoint: {commit hash} — {subject}
256
+
257
+ Ready to push and open a PR. Run spec-ship? (it will show exactly what gets pushed first)
258
+ ```
259
+
223
260
  ## Error Handling
224
261
 
225
262
  - **Coder agent failure**: mark the task as failed, report to user, continue with remaining tasks in the batch. The review step will catch integration issues.
@@ -33,7 +33,9 @@ ready-to-use work branch; a human decides when to run `spec-create` next.
33
33
  ## Project config (read first)
34
34
 
35
35
  Load `.spec-driven.yml`: walk up from the current directory to the home folder, and the first one
36
- found wins (a `multi-repo` config only applies inside the repos it lists). Its values beat anything
36
+ found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
37
+ `~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
38
+ whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
37
39
  you would detect or assume. This skill uses `layout`/`repos` (where to look and what each repo does), `tracker`, `git.remote`, `git.base_branch`, `git.branch_name`, and `specs_dir`.
38
40
 
39
41
  **Monorepo:** name the package(s) involved. **Multi-repo:** search every listed repo whose role fits the ticket, and create the work branch, with the same name, only in the repos the investigation shows need changes.
@@ -44,6 +44,11 @@ Read, don't guess. Collect a draft of every field in `references/config-template
44
44
  belong together. Never assume every folder in the parent belongs. Always also ask whether
45
45
  repos in **other folders** belong (e.g. `~/clients/billing`). Check each path is a git repo,
46
46
  and store paths inside the workspace as relative and paths outside it as absolute.
47
+ - The repos of one workspace can live in unrelated places (`/Projects/frontend`,
48
+ `~/Work/backend`, `/srv/database`). Put the config in their shared parent folder, or in
49
+ `~/.spec-driven/<name>/` when that parent would be the home folder or `/`. Then record it in
50
+ `~/.spec-driven/workspaces.json` (`{"<config path>": ["<repo path>", …]}`) so every repo can
51
+ find it.
47
52
  - The user can override the detected layout. If they choose `multi-repo` from inside one repo,
48
53
  the workspace folder defaults to that repo's parent, with the current repo pre-selected. If
49
54
  they choose `monorepo`, ask for the package folders. Never write a `multi-repo` config with
@@ -64,6 +69,10 @@ Read, don't guess. Collect a draft of every field in `references/config-template
64
69
  steps (CI is often the most truthful source). Dev URL from the dev script, `.env.example`, or
65
70
  framework defaults.
66
71
 
72
+ 6. **After approval.** Ask whether an approved spec should stop after each stage (`stop`), or
73
+ run implement → verify → checkpoint in one go (`run-through`), stopping at any failure and never
74
+ pushing. Store it as `pipeline.after_approval`.
75
+
67
76
  ## Step 2 — Confirm in one round
68
77
 
69
78
  Show the draft as a short table grouped by layout, tracker, git, commands, and specs location.
@@ -42,4 +42,10 @@ commands:
42
42
  dev: "" # how spec-verify starts the app
43
43
  dev_url: "" # where it answers, e.g. http://localhost:3000
44
44
 
45
+ pipeline:
46
+ # stop — every stage waits for you (default)
47
+ # run-through — after you approve a spec: implement → verify → checkpoint in one go,
48
+ # stopping at any failure. Pushing / opening the PR (spec-ship) always asks first.
49
+ after_approval: stop
50
+
45
51
  specs_dir: specs # where specs/<feature>/ folders go (relative to this file)
@@ -19,7 +19,9 @@ This skill complements `spec-checkpoint`. Use `spec-checkpoint` to commit locall
19
19
  ## Project config (read first)
20
20
 
21
21
  Load `.spec-driven.yml`: walk up from the current directory to the home folder, and the first one
22
- found wins (a `multi-repo` config only applies inside the repos it lists). Its values beat anything
22
+ found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
23
+ `~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
24
+ whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
23
25
  you would detect or assume. This skill uses `git.remote`, `git.pr_target`, `git.branch_name`, `tracker` (to link the ticket in the PR), and `layout`/`repos`.
24
26
 
25
27
  **Multi-repo:** push and open one PR per repo that has unpushed commits, and list the sibling PRs in each PR's body so reviewers see the whole change.
@@ -26,7 +26,9 @@ by the real code path. It produces a report the user can trust before shipping.
26
26
  ## Project config (read first)
27
27
 
28
28
  Load `.spec-driven.yml`: walk up from the current directory to the home folder, and the first one
29
- found wins (a `multi-repo` config only applies inside the repos it lists). Its values beat anything
29
+ found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
30
+ `~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
31
+ whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
30
32
  you would detect or assume. This skill uses `commands.dev`, `commands.dev_url`, and `layout`/`repos`.
31
33
 
32
34
  **Monorepo / multi-repo:** start every app or service the criteria need (e.g. the API and the frontend), each with its own `dev` command.