@benjaminthomas/spec-driven-dev 1.4.0 → 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
@@ -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, even when those repos live in different folders. To set it up, run
16
+ `npx @benjaminthomas/spec-driven-dev@latest` 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,30 @@ 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. Run the installer in your project** (for several repos, run it inside any one of them):
52
58
 
53
59
  ```bash
54
- npx @benjaminthomas/spec-driven-dev
60
+ npx @benjaminthomas/spec-driven-dev@latest
55
61
  ```
56
62
 
57
- The installer:
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.
66
+
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.
72
+
73
+ It:
58
74
  1. **Finds your AI tools** (Claude Code, Cursor, Codex CLI, Antigravity) and installs the skills
59
75
  into each one you choose, for this project or for all projects.
60
76
  2. **Sets up your project** by detecting its layout, issue tracker, branch conventions, and
61
77
  commands, then asking you to confirm. The answers go into `.spec-driven.yml`
62
78
  ([details below](#project-setup-your-teams-rules)).
63
79
 
64
- **2. In your project, run each stage in order.** You can type the slash command or just say the
80
+ **3. In your AI tool, run each stage in order.** You can type the slash command or just say the
65
81
  words in quotes.
66
82
 
67
83
  ```
@@ -77,6 +93,11 @@ words in quotes.
77
93
  /spec-ship "ship it"
78
94
  ```
79
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
+
80
101
  That's the whole workflow. Every stage can be used on its own as well: use `/spec-create` straight
81
102
  from a planning chat, or `/spec-checkpoint` on any work in progress.
82
103
 
@@ -89,24 +110,31 @@ others `fix/STP-123-short-name`. Some have one repo, others have six. None of th
89
110
  skills. Each project describes its own rules in **`.spec-driven.yml`**, and every skill reads that
90
111
  file before doing anything.
91
112
 
92
- **How the file gets created:** `npx @benjaminthomas/spec-driven-dev` (or `npx @benjaminthomas/spec-driven-dev setup` to run only this
113
+ **How the file gets created:** `spec-driven-dev` (or `spec-driven-dev setup` to run only this
93
114
  step) detects the answers from your repos and asks you to confirm them. In your AI tool you can also
94
115
  say **"run spec-setup"**: that does a deeper version that also reads `CLAUDE.md`/`AGENTS.md`, CI
95
116
  config, and recent PRs, and fills in what the installer can't (such as each repo's role, or commands
96
117
  for stacks other than Node). If a skill finds no config, it runs this setup first.
97
118
 
98
- **What it asks:**
119
+ **What `spec-driven-dev` asks, in order** (every answer comes pre-filled with what it detected, so you
120
+ can usually just press Enter):
99
121
 
100
122
  | Question | Detected from | Example answer |
101
123
  |---|---|---|
124
+ | Which AI tools get the skills | tool folders in your home directory and commands on your `PATH` | Claude Code, Cursor |
125
+ | This project only, or all projects | (you choose) | `project` |
102
126
  | 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` |
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` |
130
+ | *monorepo:* package folders | the workspaces your `package.json`/`pnpm-workspace.yaml` declares, or `apps/*`, `packages/*` | `apps/web, packages/ui` |
104
131
  | Issue tracker | ticket keys in branch names and commits | Jira, key `STP` |
105
132
  | Base branch / PR target | default branch, `staging`/`develop` branches | `staging` |
106
133
  | Branch name pattern | your recent branch names | `{ticket}` → `STP-123` |
107
134
  | Commit style | your recent commit messages | `conventional` (`feat: …`) or `ticket` (`STP-123: …`) |
108
135
  | Lint / typecheck / test / build / dev commands | `package.json` scripts and lockfile (per repo or package) | `pnpm run lint` |
109
136
  | Where specs go | (you choose) | `specs/` |
137
+ | After you approve a spec | (you choose) | `stop` (wait at each stage) or `run-through` |
110
138
 
111
139
  **Where the file lives, by layout:**
112
140
 
@@ -114,9 +142,34 @@ for stacks other than Node). If a skill finds no config, it runs this setup firs
114
142
  |---|---|---|---|
115
143
  | **single** | one repo, one app | the repo root (commit it, so the team shares it) | as described below |
116
144
  | **monorepo** | one repo, many apps/packages | the repo root (commit it) | tasks name their package, and checks run per package |
117
- | **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 |
146
+
147
+ **Repos in different places.** A multi-repo workspace doesn't need its repos in one folder. For
148
+ example, from inside `/Projects/frontend`:
118
149
 
119
- To change the rules later, edit the file or run setup again, then run `npx @benjaminthomas/spec-driven-dev doctor`
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.
166
+
167
+ Every question also has a flag, for scripts and CI:
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`.
171
+
172
+ To change the rules later, edit the file or run setup again, then run `spec-driven-dev doctor`
120
173
  to confirm the file still matches your repos. Setup never overwrites the file without
121
174
  asking. See [`config-template.yml`](skills/spec-setup/references/config-template.yml) for every field.
122
175
 
@@ -316,6 +369,18 @@ config sits outside any repo, so it stays on your machine.
316
369
  **Does it work with my stack?**
317
370
  Yes. No skill assumes a language or framework. Each one finds the repo's own commands.
318
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
+
319
384
  **Can I skip stages?**
320
385
  Yes. Every skill works on its own. `spec-verify` even works without a spec: it asks what to check.
321
386
 
@@ -326,6 +391,24 @@ Yes. Every skill works on its own. `spec-verify` even works without a spec: it a
326
391
  Yes. Progress is saved in the spec's `README.md` checkboxes and the commit for each batch. Run
327
392
  `spec-implement` again to continue.
328
393
 
394
+ **How do I update?**
395
+ Run `npm install -g @benjaminthomas/spec-driven-dev@latest`, then `spec-driven-dev` in your project.
396
+ Claude Code plugin users run
397
+ `/plugin marketplace update spec-driven-dev` and then update the plugin. Your config is never
398
+ touched by an update.
399
+
400
+ **Can I trust the npm package?**
401
+ Releases are published by this repo's GitHub Actions workflow, not from anyone's laptop, through
402
+ npm [trusted publishing](https://docs.npmjs.com/trusted-publishers). Each version's npm page shows
403
+ a provenance badge linking it to the exact commit and workflow run that built it. The package has
404
+ no dependencies, and it contains only `bin/cli.mjs` and the `skills/` folder.
405
+
406
+ **Something isn't working. Where do I start?**
407
+ Run `spec-driven-dev doctor` in your project. It checks the config against your
408
+ repos and names anything that doesn't match. If that passes, open an
409
+ [issue](https://github.com/benjaminthomas/spec-driven-dev/issues) with the doctor output and the
410
+ skill you were running.
411
+
329
412
  **`spec-verify` says it skipped some UI checks.**
330
413
  Your tool needs browser automation (an MCP browser server, a built-in browser tool, or an
331
414
  extension) for checks that involve the UI. Checks that only touch the API or database work
@@ -338,23 +421,41 @@ everywhere.
338
421
  ### Any tool: npm (recommended)
339
422
 
340
423
  ```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
424
+ npm install -g @benjaminthomas/spec-driven-dev # once per machine; adds the `spec-driven-dev` command
349
425
  ```
350
426
 
427
+ Then, in a project:
428
+
429
+ ```bash
430
+ spec-driven-dev # detect tools, install skills, set up the project
431
+ spec-driven-dev setup # only (re)run project setup
432
+ spec-driven-dev --global # install skills for all projects, not just this one
433
+ spec-driven-dev --tools=cursor,codex # pick tools without being asked
434
+ spec-driven-dev --skills=spec-create,spec-ship # install only some skills
435
+ spec-driven-dev --yes # accept every detected default (CI-friendly)
436
+ spec-driven-dev doctor # check .spec-driven.yml against the real repos
437
+ spec-driven-dev status # list specs with unfinished work
438
+ spec-driven-dev --help # show all of the above
439
+ ```
440
+
441
+ **Without installing:** replace `spec-driven-dev` with `npx @benjaminthomas/spec-driven-dev`, for
442
+ example `npx @benjaminthomas/spec-driven-dev setup`. That downloads and runs the latest version once,
443
+ which is handy in CI or on a machine you won't use again.
444
+
351
445
  `doctor` checks that every branch, folder, and `npm run` script named in the config exists, and
352
446
  that each value is one the skills understand. It exits 1 if anything is wrong, so you can run it in
353
447
  CI. `status` prints nothing when all specs are finished.
354
448
 
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`).
449
+ **Updating:** run `npm install -g @benjaminthomas/spec-driven-dev@latest` to get the newest CLI,
450
+ then `spec-driven-dev` in each project to refresh its skills. This replaces the installed skill
451
+ folders and leaves your `.spec-driven.yml` alone. If you use `npx`, add `@latest`, because `npx`
452
+ can otherwise reuse an older copy it cached. What changed in each version is in
453
+ [CHANGELOG.md](CHANGELOG.md).
454
+
455
+ `npx github:benjaminthomas/spec-driven-dev` installs straight from GitHub instead, for example to
456
+ try an unreleased commit on `main`.
457
+
458
+ **Requirements:** Node.js 18 or newer and git. The installer has no dependencies.
358
459
 
359
460
  ### Claude Code (plugin, with automatic updates)
360
461
 
@@ -369,8 +470,8 @@ The plugin also adds a **session-start reminder**. When you open Claude Code in
369
470
  unfinished specs, Claude is told which ones and what the next step is (for example,
370
471
  `specs/stp-123: 3/5 tasks done → next: spec-implement`). It prints nothing when everything is
371
472
  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.
473
+ you use the plugin, `spec-driven-dev` skips Claude Code so you don't get every skill twice. Run
474
+ `spec-driven-dev setup` for the project config.
374
475
 
375
476
  ### By hand
376
477
 
@@ -423,20 +524,20 @@ skips that step rather than failing.
423
524
 
424
525
  | Skill | Stage | SKILL.md lines | Words | Supporting files |
425
526
  |---|---|---:|---:|---:|
426
- | `spec-setup` | 0 · configure | 99 | 857 | 1 |
427
- | `spec-investigate` | 1 · understand | 323 | 2,708 | 1 |
428
- | `spec-create` | 2 · plan | 149 | 1,328 | 4 |
429
- | `spec-implement` | 3 · build | 236 | 1,790 | 3 |
430
- | `spec-verify` | 4 · prove | 153 | 1,459 | 1 |
431
- | `spec-checkpoint` | 5 · commit | 106 | 762 | 0 |
432
- | `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 |
433
534
  | `generative-seo` | standalone | 255 | 2,209 | 7 |
434
- | **Total** | | **1,539** | **12,348** | **17** |
535
+ | **Total** | | **1,607** | **13,051** | **17** |
435
536
 
436
537
  - **8** skills: **1** setup + **6** workflow stages + **1** standalone
437
538
  - **4** tools supported natively (Claude Code, Codex CLI, Cursor, Antigravity), plus an MCP fallback
438
539
  - **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
540
+ - **1** npm package, `@benjaminthomas/spec-driven-dev`, plus the Claude Code plugin and an MCP fallback
440
541
  - **0** stack assumptions: lint, typecheck, test, build, and dev commands come from your repo
441
542
 
442
543
  _The table is generated by `python3 scripts/check.py --fix`; CI fails if it goes out of date._
@@ -450,19 +551,22 @@ skills/<name>/SKILL.md the skill itself (frontmatter: name, description,
450
551
  skills/<name>/references/ templates and prompts the skill loads when needed
451
552
  .claude-plugin/ Claude Code plugin + marketplace manifests
452
553
  .agents/skills/ symlinks so Codex/Antigravity pick the skills up inside this repo
453
- bin/cli.mjs the npx installer + project setup (no dependencies)
554
+ bin/cli.mjs the `spec-driven-dev` CLI: installer, setup, doctor, status (no dependencies)
454
555
  package.json npm package (ships bin/ and skills/)
455
556
  mcp-server/ MCP fallback server
456
557
  hooks/hooks.json Claude Code session-start hook (runs `cli.mjs status`)
457
558
  CHANGELOG.md what changed in each version
458
559
  scripts/check.py consistency checks + metrics generator
459
560
  scripts/test-cli.mjs installer self-test (fake HOME, throwaway repos)
561
+ .github/workflows/check.yml CI: consistency checks + installer self-test on every push and PR
562
+ .github/workflows/publish.yml publishes to npm when a vX.Y.Z tag is pushed
460
563
  evals/ `claude plugin eval` cases
461
564
  ```
462
565
 
463
566
  - **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
567
  `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
568
+ - **Before pushing:** run `python3 scripts/check.py --fix`, then `npm test` (which runs the checks
569
+ and the installer self-test). CI (`.github/workflows/check.yml`) runs
466
570
  the same checks, the installer self-test, and a syntax check on the MCP server.
467
571
  - **Evals:** `claude plugin eval . --runs 1` checks that skills trigger on their phrases and stay
468
572
  within their own stage. These runs use your Claude credentials, so they aren't part of CI.
@@ -476,8 +580,15 @@ evals/ `claude plugin eval` cases
476
580
  - **Releasing:**
477
581
  1. Increase `version` in both `package.json` and `.claude-plugin/plugin.json` (the check fails if
478
582
  they differ).
479
- 2. Add a `CHANGELOG.md` entry.
480
- 3. Push, then `npm publish`.
583
+ 2. Add a `CHANGELOG.md` entry, then commit and push.
584
+ 3. Tag the release and push the tag: `git tag v1.4.1 && git push origin v1.4.1`.
585
+ 4. Watch the **publish** run in the repo's Actions tab. When it's green, the new version is on
586
+ npm. Claude Code plugin users get it from the marketplace once the commit is on `main`.
587
+
588
+ `.github/workflows/publish.yml` then runs the tests and publishes to npm through
589
+ [trusted publishing](https://docs.npmjs.com/trusted-publishers). No npm token or passkey is
590
+ needed, and the package page shows a provenance badge linking the release to the exact commit.
591
+ The workflow refuses to publish if the tag doesn't match `package.json`.
481
592
 
482
593
  ## License
483
594
 
package/bin/cli.mjs CHANGED
@@ -1,12 +1,14 @@
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=~/Projects/frontend,/srv/backend --workspace=<config dir>
11
+ // --packages=apps/web,packages/ui --after-approval=stop|run-through --version
10
12
  import fs from "node:fs";
11
13
  import os from "node:os";
12
14
  import path from "node:path";
@@ -34,8 +36,15 @@ const command = args.find((a) => !a.startsWith("-")) || "install";
34
36
  const YES = flag("yes") || flag("y") || !process.stdin.isTTY || ["doctor", "status"].includes(command);
35
37
  const DIR = path.resolve(opt("dir") || process.cwd());
36
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
+
37
46
  if (flag("help") || flag("h")) {
38
- console.log(fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1, 9).join("\n").replace(/^\/\/ ?/gm, ""));
47
+ console.log(fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1, 11).join("\n").replace(/^\/\/ ?/gm, ""));
39
48
  process.exit(0);
40
49
  }
41
50
 
@@ -102,6 +111,21 @@ const isRepo = (d) => run("git", ["rev-parse", "--is-inside-work-tree"], d) ===
102
111
  const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } };
103
112
  const subdirs = (d) => { try { return fs.readdirSync(d, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name); } catch { return []; } };
104
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
+
105
129
  function detectLayout(dir) {
106
130
  if (isRepo(dir)) {
107
131
  const top = run("git", ["rev-parse", "--show-toplevel"], dir);
@@ -120,8 +144,7 @@ function detectLayout(dir) {
120
144
  const mono = MARKERS.some((m) => fs.existsSync(path.join(top, m))) || pkg?.workspaces || cargo || packages.length > 1;
121
145
  return { layout: mono ? "monorepo" : "single", root: top, repos: mono ? packages : [] };
122
146
  }
123
- // a git worktree has a .git *file*; it belongs to its main repo, so skip it
124
- const repos = subdirs(dir).filter((n) => { try { return fs.statSync(path.join(dir, n, ".git")).isDirectory(); } catch { return false; } });
147
+ const repos = findRepos(dir);
125
148
  return repos.length ? { layout: "multi-repo", root: dir, repos } : { layout: "single", root: dir, repos: [] };
126
149
  }
127
150
 
@@ -181,29 +204,126 @@ function toYaml(c) {
181
204
  out += `tracker:\n type: ${c.tracker.type}\n key: ${y(c.tracker.key)}\n url: ${y(c.tracker.url)}\n`;
182
205
  out += `git:\n${cmds(c.git, " ")}\n`;
183
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`;
184
208
  return out + `specs_dir: ${y(c.specsDir)}\n`;
185
209
  }
186
210
 
211
+ const LAYOUTS = ["single", "monorepo", "multi-repo"];
212
+ const splitList = (v) => (v || "").split(",").map((x) => x.trim()).filter(Boolean);
213
+ // Paths are stored relative to the config's folder when inside it, absolute otherwise.
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 || "."; };
216
+
217
+ async function chooseLayout(detected) {
218
+ const forced = opt("layout");
219
+ if (forced && !LAYOUTS.includes(forced)) throw new Error(`--layout must be one of ${LAYOUTS.join(", ")}`);
220
+ if (forced) return forced;
221
+ for (;;) {
222
+ const a = await ask(`Layout — single, monorepo, or multi-repo (detected: ${detected})`, detected);
223
+ if (LAYOUTS.includes(a)) return a;
224
+ console.log(` Please type one of: ${LAYOUTS.join(", ")}`);
225
+ }
226
+ }
227
+
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.
239
+ async function chooseRepos(d) {
240
+ const insideRepo = d.layout !== "multi-repo";
241
+ const scanRoot = insideRepo ? path.dirname(d.root) : d.root;
242
+ let picked = [];
243
+ const fromFlag = opt("repos");
244
+ if (fromFlag) {
245
+ picked = splitList(fromFlag).map((p) => path.resolve(DIR, expandHome(p)));
246
+ } else {
247
+ const found = findRepos(scanRoot);
248
+ if (found.length) {
249
+ const owners = {};
250
+ for (const r of found) (owners[gitFacts(path.join(scanRoot, r))?.owner || "?"] ||= []).push(r);
251
+ const biggest = Object.values(owners).sort((a, b) => b.length - a.length)[0] || [];
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));
256
+ } else if (insideRepo) picked = [d.root];
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))));
260
+ }
261
+ const abs = [];
262
+ for (const p of [...new Set(picked)]) {
263
+ if (!fs.existsSync(p)) console.log(` ✗ ${p}: folder not found — skipped`);
264
+ else if (!isRepo(p)) console.log(` ✗ ${p}: not a git repo — skipped`);
265
+ else abs.push(run("git", ["rev-parse", "--show-toplevel"], p) || p);
266
+ }
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));
281
+ console.log(` Workspace repos: ${repos.join(", ")}`);
282
+ return { root, repos };
283
+ }
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
+
296
+ // monorepo: confirm or edit the package folders (relative to the repo root).
297
+ async function choosePackages(d) {
298
+ const root = d.root;
299
+ const fromFlag = opt("packages");
300
+ const answer = fromFlag || (await ask("Package folders, comma-separated, relative to the repo root (empty if none yet)", d.repos.join(",")));
301
+ const pkgs = [];
302
+ for (const p of splitList(answer)) {
303
+ if (fs.existsSync(path.resolve(root, p))) pkgs.push(relOrAbs(root, path.resolve(root, p)));
304
+ else console.log(` ✗ ${p}: folder not found in ${root} — skipped`);
305
+ }
306
+ return { root, repos: pkgs };
307
+ }
308
+
187
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");
188
311
  console.log("\nProject setup — detecting how this project works…");
189
312
  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;
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.`);
317
+ const layout = await chooseLayout(d.layout);
318
+ const { root, repos } = layout === "multi-repo" ? await chooseRepos(d) : layout === "monorepo" ? await choosePackages(d) : { root: d.layout === "multi-repo" ? DIR : d.root, repos: [] };
319
+ if (layout === "single" && !isRepo(root)) console.log(` • ${root} isn't a git repo — run setup inside the repo, or choose multi-repo.`);
200
320
  const file = path.join(root, ".spec-driven.yml");
201
321
  if (fs.existsSync(file) && (await ask(`${file} exists — overwrite? (y/n)`, "n")) !== "y") {
202
322
  console.log("• Kept the existing config. Run the spec-setup skill in your AI tool to review it.");
203
323
  return;
204
324
  }
205
325
 
206
- const repoDirs = layout === "multi-repo" ? repos.map((r) => path.join(root, r)) : [isRepo(root) ? root : DIR];
326
+ const repoDirs = layout === "multi-repo" ? repos.map((r) => path.resolve(root, r)) : [root];
207
327
  const facts = repoDirs.map(gitFacts).filter(Boolean);
208
328
  const g = infer(facts);
209
329
  if (facts.length) console.log(`Detected: tracker ${g.trackerType}${g.key ? ` (${g.key}-…)` : ""}, base ${g.base}, branches "${g.branchName}", commits ${g.commitStyle}`);
@@ -216,18 +336,27 @@ async function setup() {
216
336
  const branchName = await ask("Branch name pattern ({ticket} {slug} {type})", g.branchName);
217
337
  const commitStyle = await ask("Commit style (conventional/ticket/freeform)", g.commitStyle);
218
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
+ }
219
347
 
220
348
  const config = {
221
- layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "",
349
+ layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "", afterApproval,
222
350
  git: { remote: "origin", base_branch: base, pr_target: pr, branch_name: branchName, commit_style: commitStyle },
223
351
  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;
352
+ repos: repos.map((r) => {
353
+ const f = layout === "multi-repo" ? gitFacts(path.resolve(root, r)) : null;
226
354
  // 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)) };
355
+ return { path: r, role: "", base: f ? f.integration || "default" : "", commands: commandsFor(path.resolve(root, r)) };
228
356
  }),
229
357
  };
230
358
  fs.writeFileSync(file, toYaml(config));
359
+ if (layout === "multi-repo") register(real(file), repos.map((r) => real(path.resolve(root, r))));
231
360
  console.log(`✓ Wrote ${file}`);
232
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.");
233
362
  console.log(' Fill in repo roles, dev_url, and non-Node commands by asking your AI tool to "run spec-setup".');
@@ -273,13 +402,18 @@ function findConfig(dir) {
273
402
  for (let d = dir; ; d = path.dirname(d)) {
274
403
  const f = path.join(d, ".spec-driven.yml");
275
404
  if (fs.existsSync(f)) return f;
276
- if (d === path.dirname(d) || d === HOME) return null;
405
+ if (d === path.dirname(d) || d === HOME) break;
277
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;
278
412
  }
279
413
 
280
414
  function doctor() {
281
415
  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`);
416
+ 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
417
  const root = path.dirname(file);
284
418
  let c;
285
419
  try { c = parseYaml(fs.readFileSync(file, "utf8")); } catch (e) { throw new Error(`${file} could not be read: ${e.message}`); }
@@ -287,11 +421,12 @@ function doctor() {
287
421
  if (!["single", "monorepo", "multi-repo"].includes(c.layout)) problems.push(`layout "${c.layout}" must be single, monorepo, or multi-repo`);
288
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`);
289
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`);
290
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}`);
291
426
  const repos = c.layout === "single" ? [{ path: "." }] : Array.isArray(c.repos) ? c.repos : [];
292
427
  if (c.layout !== "single" && !repos.length) problems.push(`layout is ${c.layout} but repos: is empty`);
293
428
  for (const r of repos) {
294
- const dir = path.join(root, r.path || "");
429
+ const dir = path.resolve(root, r.path || ".");
295
430
  const label = r.path === "." ? "repo" : r.path;
296
431
  if (!fs.existsSync(dir)) { problems.push(`${label}: folder not found`); continue; }
297
432
  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.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.
@@ -41,7 +41,18 @@ 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 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.
52
+ - The user can override the detected layout. If they choose `multi-repo` from inside one repo,
53
+ the workspace folder defaults to that repo's parent, with the current repo pre-selected. If
54
+ they choose `monorepo`, ask for the package folders. Never write a `multi-repo` config with
55
+ no repos.
45
56
  - Otherwise → `single`.
46
57
  2. **Existing written rules.** `CLAUDE.md`, `AGENTS.md`, `CONTRIBUTING.md`, PR templates,
47
58
  `.github/workflows`. Anything stated there is a strong default — quote it back to the user.
@@ -58,6 +69,10 @@ Read, don't guess. Collect a draft of every field in `references/config-template
58
69
  steps (CI is often the most truthful source). Dev URL from the dev script, `.env.example`, or
59
70
  framework defaults.
60
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
+
61
76
  ## Step 2 — Confirm in one round
62
77
 
63
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.