@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 +64 -27
- package/bin/cli.mjs +100 -23
- package/package.json +1 -1
- package/skills/spec-checkpoint/SKILL.md +3 -1
- package/skills/spec-create/SKILL.md +10 -2
- package/skills/spec-implement/SKILL.md +39 -2
- package/skills/spec-investigate/SKILL.md +3 -1
- package/skills/spec-setup/SKILL.md +9 -0
- package/skills/spec-setup/references/config-template.yml +6 -0
- package/skills/spec-ship/SKILL.md +3 -1
- package/skills/spec-verify/SKILL.md +3 -1
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
|
|
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.
|
|
57
|
+
**1. Run the installer in your project** (for several repos, run it inside any one of them):
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
|
|
60
|
+
npx @benjaminthomas/spec-driven-dev@latest
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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:*
|
|
119
|
-
| *multi-repo:*
|
|
120
|
-
| *multi-repo:*
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
repo
|
|
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
|
|
145
|
-
`--
|
|
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 |
|
|
491
|
-
| `spec-investigate` | 1 · understand |
|
|
492
|
-
| `spec-create` | 2 · plan |
|
|
493
|
-
| `spec-implement` | 3 · build |
|
|
494
|
-
| `spec-verify` | 4 · prove |
|
|
495
|
-
| `spec-checkpoint` | 5 · commit |
|
|
496
|
-
| `spec-ship` | 6 · PR |
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
221
|
-
console.log(`Found ${found.length} git repos
|
|
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(
|
|
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
|
-
|
|
226
|
-
|
|
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
|
|
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
|
|
265
|
+
else abs.push(run("git", ["rev-parse", "--show-toplevel"], p) || p);
|
|
233
266
|
}
|
|
234
|
-
if (!
|
|
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)
|
|
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.
|
|
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).
|
|
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).
|
|
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.
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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.
|