@benjaminthomas/spec-driven-dev 1.4.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -28
- package/bin/cli.mjs +127 -26
- package/bin/spec-driven-dev.cjs +10 -0
- package/package.json +3 -3
- 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 |
|
|
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`:
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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,25 @@ 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 anything.**
|
|
379
|
+
Update to 1.5.1 or later (`npx @benjaminthomas/spec-driven-dev@latest --version`). Earlier versions
|
|
380
|
+
crashed on Node 14 or 16, which nvm often switches to inside older projects, and wrote a config
|
|
381
|
+
silently when there was no terminal. Now it runs on Node 14+. With no terminal to answer in (a `!`
|
|
382
|
+
command in Claude Code, an editor task, piped input), it stops, changes nothing, and tells you to run
|
|
383
|
+
it in a terminal window or pass flags such as `--layout=multi-repo --repos=…`.
|
|
384
|
+
|
|
385
|
+
**Setup didn't ask me about repos.**
|
|
386
|
+
It only asks about repos for a multi-repo or monorepo layout. If it detected `single`, type
|
|
387
|
+
`multi-repo` at the layout question, then add your other repos by path. If it asked nothing at all,
|
|
388
|
+
there was no terminal to type answers into (it prints a note saying so), so run it directly in a
|
|
389
|
+
terminal. Check which version ran with `npx @benjaminthomas/spec-driven-dev@latest --version`.
|
|
390
|
+
|
|
347
391
|
**Can I skip stages?**
|
|
348
392
|
Yes. Every skill works on its own. `spec-verify` even works without a spec: it asks what to check.
|
|
349
393
|
|
|
@@ -418,7 +462,8 @@ can otherwise reuse an older copy it cached. What changed in each version is in
|
|
|
418
462
|
`npx github:benjaminthomas/spec-driven-dev` installs straight from GitHub instead, for example to
|
|
419
463
|
try an unreleased commit on `main`.
|
|
420
464
|
|
|
421
|
-
**Requirements:** Node.js
|
|
465
|
+
**Requirements:** Node.js 14 or newer and git. The installer has no dependencies, and CI tests it on
|
|
466
|
+
Node 14, 16, 18, 20, 22, and 24.
|
|
422
467
|
|
|
423
468
|
### Claude Code (plugin, with automatic updates)
|
|
424
469
|
|
|
@@ -487,15 +532,15 @@ skips that step rather than failing.
|
|
|
487
532
|
|
|
488
533
|
| Skill | Stage | SKILL.md lines | Words | Supporting files |
|
|
489
534
|
|---|---|---:|---:|---:|
|
|
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 |
|
|
535
|
+
| `spec-setup` | 0 · configure | 114 | 1,022 | 1 |
|
|
536
|
+
| `spec-investigate` | 1 · understand | 325 | 2,741 | 1 |
|
|
537
|
+
| `spec-create` | 2 · plan | 157 | 1,415 | 4 |
|
|
538
|
+
| `spec-implement` | 3 · build | 273 | 2,109 | 3 |
|
|
539
|
+
| `spec-verify` | 4 · prove | 155 | 1,492 | 1 |
|
|
540
|
+
| `spec-checkpoint` | 5 · commit | 108 | 795 | 0 |
|
|
541
|
+
| `spec-ship` | 6 · PR | 220 | 1,268 | 0 |
|
|
497
542
|
| `generative-seo` | standalone | 255 | 2,209 | 7 |
|
|
498
|
-
| **Total** | | **1,
|
|
543
|
+
| **Total** | | **1,607** | **13,051** | **17** |
|
|
499
544
|
|
|
500
545
|
- **8** skills: **1** setup + **6** workflow stages + **1** standalone
|
|
501
546
|
- **4** tools supported natively (Claude Code, Codex CLI, Cursor, Antigravity), plus an MCP fallback
|
package/bin/cli.mjs
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
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";
|
|
14
15
|
import { execFileSync } from "node:child_process";
|
|
15
|
-
import { createInterface } from "node:readline/promises
|
|
16
|
+
import { createInterface } from "node:readline"; // callback API: readline/promises needs Node 17+
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
17
18
|
|
|
18
19
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
@@ -35,15 +36,35 @@ 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
|
+
// No terminal to answer in (e.g. `!` in Claude Code, an IDE task, a pipe) and nothing passed explicitly:
|
|
44
|
+
// stop rather than write a config from guesses.
|
|
45
|
+
const ANSWER_FLAGS = ["yes", "y", "layout", "repos", "packages", "tools", "workspace", "after-approval", "skills", "global", "no-setup"];
|
|
46
|
+
if (["install", "setup"].includes(command) && !process.stdin.isTTY && !args.some((a) => ANSWER_FLAGS.some((f) => a === `--${f}` || a.startsWith(`--${f}=`)))) {
|
|
47
|
+
console.log(`✗ No interactive terminal, so setup can't ask its questions. Nothing was changed.
|
|
48
|
+
|
|
49
|
+
This happens when it runs through another program: a "!" command in Claude Code, an editor task,
|
|
50
|
+
or piped input. Either:
|
|
51
|
+
• run it yourself in a terminal window (Terminal, iTerm, or your editor's terminal panel):
|
|
52
|
+
npx @benjaminthomas/spec-driven-dev@latest ${command === "setup" ? "setup" : ""}
|
|
53
|
+
• or give the answers as flags, for example:
|
|
54
|
+
npx @benjaminthomas/spec-driven-dev@latest setup --layout=multi-repo --repos=~/Projects/frontend,~/Projects/backend
|
|
55
|
+
(add --yes to accept everything it detects)`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
if (flag("help") || flag("h")) {
|
|
39
|
-
console.log(fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1,
|
|
60
|
+
console.log(fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1, 11).join("\n").replace(/^\/\/ ?/gm, ""));
|
|
40
61
|
process.exit(0);
|
|
41
62
|
}
|
|
42
63
|
|
|
43
64
|
const rl = YES ? null : createInterface({ input: process.stdin, output: process.stdout });
|
|
44
65
|
async function ask(q, def = "") {
|
|
45
66
|
if (YES) return def;
|
|
46
|
-
const a = (await rl.question(`${q}${def ? ` [${def}]` : ""}:
|
|
67
|
+
const a = (await new Promise((resolve) => rl.question(`${q}${def ? ` [${def}]` : ""}: `, resolve))).trim();
|
|
47
68
|
return a || def;
|
|
48
69
|
}
|
|
49
70
|
async function pick(q, items, defaults) {
|
|
@@ -60,6 +81,17 @@ const run = (cmd, argv, cwd) => {
|
|
|
60
81
|
};
|
|
61
82
|
const onPath = (bin) => !!run(process.platform === "win32" ? "where" : "which", [bin]);
|
|
62
83
|
|
|
84
|
+
// fs.cpSync needs Node 16.7+; this keeps Node 14 working. Follows symlinks, skips .DS_Store.
|
|
85
|
+
function copyDir(from, to) {
|
|
86
|
+
fs.mkdirSync(to, { recursive: true });
|
|
87
|
+
for (const e of fs.readdirSync(from)) {
|
|
88
|
+
if (e === ".DS_Store") continue;
|
|
89
|
+
const src = path.join(from, e), dst = path.join(to, e);
|
|
90
|
+
if (fs.statSync(src).isDirectory()) copyDir(src, dst);
|
|
91
|
+
else fs.copyFileSync(src, dst);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
// ---------- install ----------
|
|
64
96
|
async function install() {
|
|
65
97
|
const detected = Object.keys(TOOLS).filter((t) => fs.existsSync(path.join(HOME, TOOLS[t].home)) || onPath(TOOLS[t].bin));
|
|
@@ -88,7 +120,7 @@ async function install() {
|
|
|
88
120
|
for (const s of skills) {
|
|
89
121
|
const to = path.join(dest, s);
|
|
90
122
|
fs.rmSync(to, { recursive: true, force: true });
|
|
91
|
-
|
|
123
|
+
copyDir(path.join(SKILLS_DIR, s), to);
|
|
92
124
|
}
|
|
93
125
|
console.log(`✓ ${skills.length} skills → ${dest}`);
|
|
94
126
|
}
|
|
@@ -103,6 +135,21 @@ const isRepo = (d) => run("git", ["rev-parse", "--is-inside-work-tree"], d) ===
|
|
|
103
135
|
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } };
|
|
104
136
|
const subdirs = (d) => { try { return fs.readdirSync(d, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name); } catch { return []; } };
|
|
105
137
|
|
|
138
|
+
// Git repos up to two folders down (e.g. ~/work/client/api). A git worktree has a .git *file*;
|
|
139
|
+
// it belongs to its main repo, so it is skipped. Repos aren't searched inside.
|
|
140
|
+
function findRepos(dir, depth = 2, prefix = "") {
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const n of subdirs(dir)) {
|
|
143
|
+
if (n === "node_modules") continue;
|
|
144
|
+
const full = path.join(dir, n);
|
|
145
|
+
let isGitDir = false;
|
|
146
|
+
try { isGitDir = fs.statSync(path.join(full, ".git")).isDirectory(); } catch {}
|
|
147
|
+
if (isGitDir) out.push(prefix + n);
|
|
148
|
+
else if (depth > 1) out.push(...findRepos(full, depth - 1, `${prefix}${n}/`));
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
106
153
|
function detectLayout(dir) {
|
|
107
154
|
if (isRepo(dir)) {
|
|
108
155
|
const top = run("git", ["rev-parse", "--show-toplevel"], dir);
|
|
@@ -121,8 +168,7 @@ function detectLayout(dir) {
|
|
|
121
168
|
const mono = MARKERS.some((m) => fs.existsSync(path.join(top, m))) || pkg?.workspaces || cargo || packages.length > 1;
|
|
122
169
|
return { layout: mono ? "monorepo" : "single", root: top, repos: mono ? packages : [] };
|
|
123
170
|
}
|
|
124
|
-
|
|
125
|
-
const repos = subdirs(dir).filter((n) => { try { return fs.statSync(path.join(dir, n, ".git")).isDirectory(); } catch { return false; } });
|
|
171
|
+
const repos = findRepos(dir);
|
|
126
172
|
return repos.length ? { layout: "multi-repo", root: dir, repos } : { layout: "single", root: dir, repos: [] };
|
|
127
173
|
}
|
|
128
174
|
|
|
@@ -182,13 +228,15 @@ function toYaml(c) {
|
|
|
182
228
|
out += `tracker:\n type: ${c.tracker.type}\n key: ${y(c.tracker.key)}\n url: ${y(c.tracker.url)}\n`;
|
|
183
229
|
out += `git:\n${cmds(c.git, " ")}\n`;
|
|
184
230
|
out += `commands:\n${cmds(c.commands, " ")}\n dev_url: ${y(c.devUrl)}\n`;
|
|
231
|
+
out += `pipeline:\n after_approval: ${c.afterApproval || "stop"} # stop | run-through (implement → verify → checkpoint; ship always asks)\n`;
|
|
185
232
|
return out + `specs_dir: ${y(c.specsDir)}\n`;
|
|
186
233
|
}
|
|
187
234
|
|
|
188
235
|
const LAYOUTS = ["single", "monorepo", "multi-repo"];
|
|
189
236
|
const splitList = (v) => (v || "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
190
237
|
// Paths are stored relative to the config's folder when inside it, absolute otherwise.
|
|
191
|
-
const
|
|
238
|
+
const real = (p) => { try { return fs.realpathSync(p); } catch { return p; } }; // /var vs /private/var etc.
|
|
239
|
+
const relOrAbs = (root, p) => { const r = path.relative(real(root), real(p)); return r.startsWith("..") || path.isAbsolute(r) ? real(p) : r || "."; };
|
|
192
240
|
|
|
193
241
|
async function chooseLayout(detected) {
|
|
194
242
|
const forced = opt("layout");
|
|
@@ -201,41 +249,74 @@ async function chooseLayout(detected) {
|
|
|
201
249
|
}
|
|
202
250
|
}
|
|
203
251
|
|
|
204
|
-
|
|
252
|
+
const expandHome = (p) => p.replace(/^~(?=$|\/)/, HOME);
|
|
253
|
+
|
|
254
|
+
function commonParent(paths) {
|
|
255
|
+
if (!paths.length) return "";
|
|
256
|
+
let parts = paths[0].split(path.sep);
|
|
257
|
+
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); }
|
|
258
|
+
return parts.join(path.sep) || path.sep;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// multi-repo: the repos can live anywhere (/Projects/frontend, /Work/backend, …). Pick the ones found
|
|
262
|
+
// nearby, add any others by path, then decide where the config lives.
|
|
205
263
|
async function chooseRepos(d) {
|
|
206
|
-
// Inside one repo but multi-repo chosen → the workspace is the folder that holds this repo.
|
|
207
264
|
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 : [];
|
|
265
|
+
const scanRoot = insideRepo ? path.dirname(d.root) : d.root;
|
|
211
266
|
let picked = [];
|
|
212
267
|
const fromFlag = opt("repos");
|
|
213
268
|
if (fromFlag) {
|
|
214
|
-
picked = splitList(fromFlag).map((p) => path.resolve(
|
|
269
|
+
picked = splitList(fromFlag).map((p) => path.resolve(DIR, expandHome(p)));
|
|
215
270
|
} else {
|
|
271
|
+
const found = findRepos(scanRoot);
|
|
216
272
|
if (found.length) {
|
|
217
273
|
const owners = {};
|
|
218
|
-
for (const r of found)
|
|
274
|
+
for (const r of found) { const o = gitFacts(path.join(scanRoot, r))?.owner || "?"; (owners[o] = owners[o] || []).push(r); }
|
|
219
275
|
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(
|
|
276
|
+
const here = insideRepo ? [path.relative(scanRoot, d.root)] : [];
|
|
277
|
+
console.log(`Found ${found.length} git repos under ${scanRoot}. Which ones belong to this workspace?`);
|
|
278
|
+
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)])]))
|
|
279
|
+
.map((r) => path.join(scanRoot, r));
|
|
224
280
|
} else if (insideRepo) picked = [d.root];
|
|
225
|
-
|
|
226
|
-
|
|
281
|
+
console.log("Repos in other places can be part of the same workspace (e.g. ~/Work/backend, /srv/database).");
|
|
282
|
+
const extra = await ask("Other repo paths, comma-separated (empty for none)", "");
|
|
283
|
+
picked.push(...splitList(extra).map((p) => path.resolve(DIR, expandHome(p))));
|
|
227
284
|
}
|
|
228
|
-
const
|
|
285
|
+
const abs = [];
|
|
229
286
|
for (const p of [...new Set(picked)]) {
|
|
230
287
|
if (!fs.existsSync(p)) console.log(` ✗ ${p}: folder not found — skipped`);
|
|
231
288
|
else if (!isRepo(p)) console.log(` ✗ ${p}: not a git repo — skipped`);
|
|
232
|
-
else
|
|
289
|
+
else abs.push(run("git", ["rev-parse", "--show-toplevel"], p) || p);
|
|
233
290
|
}
|
|
234
|
-
if (!
|
|
291
|
+
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).");
|
|
292
|
+
|
|
293
|
+
// Config location: the repos' shared parent folder, unless that's too broad (home or /), in which
|
|
294
|
+
// case a named folder under ~/.spec-driven/. Repos outside it find the config via the registry.
|
|
295
|
+
const parent = commonParent([...new Set(abs.map(real))].map((p) => path.dirname(p)));
|
|
296
|
+
const tooBroad = !parent || parent === HOME || parent === path.parse(parent).root || HOME.startsWith(parent + path.sep);
|
|
297
|
+
// Ran in a workspace folder and every repo is under it → keep the config right there.
|
|
298
|
+
const allUnderScan = !insideRepo && abs.every((p) => real(p).startsWith(real(scanRoot) + path.sep));
|
|
299
|
+
const defRoot = opt("workspace") ? path.resolve(DIR, expandHome(opt("workspace")))
|
|
300
|
+
: allUnderScan ? scanRoot
|
|
301
|
+
: tooBroad ? path.join(HOME, ".spec-driven", path.basename(abs[0]) + "-workspace") : parent;
|
|
302
|
+
const root = path.resolve(DIR, expandHome(await ask("Where should this workspace's .spec-driven.yml live", defRoot)));
|
|
303
|
+
fs.mkdirSync(root, { recursive: true });
|
|
304
|
+
const repos = [...new Set(abs)].map((p) => relOrAbs(root, p));
|
|
235
305
|
console.log(` Workspace repos: ${repos.join(", ")}`);
|
|
236
306
|
return { root, repos };
|
|
237
307
|
}
|
|
238
308
|
|
|
309
|
+
// ~/.spec-driven/workspaces.json maps each multi-repo config to its repos, so a repo outside the
|
|
310
|
+
// config's folder (e.g. /Work/backend for a config in /Projects) can still find it.
|
|
311
|
+
const REGISTRY = () => path.join(HOME, ".spec-driven", "workspaces.json");
|
|
312
|
+
function readRegistry() { return readJson(REGISTRY()) || {}; }
|
|
313
|
+
function register(file, repoPaths) {
|
|
314
|
+
const reg = readRegistry();
|
|
315
|
+
reg[file] = repoPaths;
|
|
316
|
+
fs.mkdirSync(path.dirname(REGISTRY()), { recursive: true });
|
|
317
|
+
fs.writeFileSync(REGISTRY(), JSON.stringify(reg, null, 2) + "\n");
|
|
318
|
+
}
|
|
319
|
+
|
|
239
320
|
// monorepo: confirm or edit the package folders (relative to the repo root).
|
|
240
321
|
async function choosePackages(d) {
|
|
241
322
|
const root = d.root;
|
|
@@ -250,8 +331,13 @@ async function choosePackages(d) {
|
|
|
250
331
|
}
|
|
251
332
|
|
|
252
333
|
async function setup() {
|
|
334
|
+
if (opt("after-approval") && !["stop", "run-through"].includes(opt("after-approval"))) throw new Error("--after-approval must be stop or run-through");
|
|
253
335
|
console.log("\nProject setup — detecting how this project works…");
|
|
254
336
|
const d = detectLayout(DIR);
|
|
337
|
+
console.log(d.layout === "multi-repo" ? `Detected: ${d.repos.length} git repos under ${d.root} (multi-repo).`
|
|
338
|
+
: d.layout === "monorepo" ? `Detected: monorepo at ${d.root} with ${d.repos.length} package(s)${d.repos.length ? `: ${d.repos.join(", ")}` : ""}.`
|
|
339
|
+
: isRepo(d.root) ? `Detected: single repo at ${d.root}. If it works together with other repos, choose multi-repo.`
|
|
340
|
+
: `Detected: no git repo in ${d.root}. Choose multi-repo to list your repos, wherever they are.`);
|
|
255
341
|
const layout = await chooseLayout(d.layout);
|
|
256
342
|
const { root, repos } = layout === "multi-repo" ? await chooseRepos(d) : layout === "monorepo" ? await choosePackages(d) : { root: d.layout === "multi-repo" ? DIR : d.root, repos: [] };
|
|
257
343
|
if (layout === "single" && !isRepo(root)) console.log(` • ${root} isn't a git repo — run setup inside the repo, or choose multi-repo.`);
|
|
@@ -274,9 +360,17 @@ async function setup() {
|
|
|
274
360
|
const branchName = await ask("Branch name pattern ({ticket} {slug} {type})", g.branchName);
|
|
275
361
|
const commitStyle = await ask("Commit style (conventional/ticket/freeform)", g.commitStyle);
|
|
276
362
|
const specsDir = await ask("Where specs/<feature>/ folders go", "specs");
|
|
363
|
+
console.log("After you approve a spec, the workflow can stop and wait for you at each stage, or run");
|
|
364
|
+
console.log("implement → verify → checkpoint in one go (it stops at any failure, and never pushes without asking).");
|
|
365
|
+
let afterApproval = opt("after-approval");
|
|
366
|
+
if (!afterApproval) for (;;) {
|
|
367
|
+
afterApproval = await ask("After approval: stop or run-through", "stop");
|
|
368
|
+
if (["stop", "run-through"].includes(afterApproval)) break;
|
|
369
|
+
console.log(" Please type stop or run-through");
|
|
370
|
+
}
|
|
277
371
|
|
|
278
372
|
const config = {
|
|
279
|
-
layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "",
|
|
373
|
+
layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "", afterApproval,
|
|
280
374
|
git: { remote: "origin", base_branch: base, pr_target: pr, branch_name: branchName, commit_style: commitStyle },
|
|
281
375
|
commands: layout === "single" ? commandsFor(repoDirs[0]) : commandsFor(root),
|
|
282
376
|
repos: repos.map((r) => {
|
|
@@ -286,6 +380,7 @@ async function setup() {
|
|
|
286
380
|
}),
|
|
287
381
|
};
|
|
288
382
|
fs.writeFileSync(file, toYaml(config));
|
|
383
|
+
if (layout === "multi-repo") register(real(file), repos.map((r) => real(path.resolve(root, r))));
|
|
289
384
|
console.log(`✓ Wrote ${file}`);
|
|
290
385
|
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
386
|
console.log(' Fill in repo roles, dev_url, and non-Node commands by asking your AI tool to "run spec-setup".');
|
|
@@ -331,8 +426,13 @@ function findConfig(dir) {
|
|
|
331
426
|
for (let d = dir; ; d = path.dirname(d)) {
|
|
332
427
|
const f = path.join(d, ".spec-driven.yml");
|
|
333
428
|
if (fs.existsSync(f)) return f;
|
|
334
|
-
if (d === path.dirname(d) || d === HOME)
|
|
429
|
+
if (d === path.dirname(d) || d === HOME) break;
|
|
335
430
|
}
|
|
431
|
+
// Not above us: a multi-repo workspace elsewhere may list this repo.
|
|
432
|
+
const here = real(dir);
|
|
433
|
+
for (const [file, repos] of Object.entries(readRegistry()))
|
|
434
|
+
if (fs.existsSync(file) && repos.some((r) => here === r || here.startsWith(r + path.sep))) return file;
|
|
435
|
+
return null;
|
|
336
436
|
}
|
|
337
437
|
|
|
338
438
|
function doctor() {
|
|
@@ -345,6 +445,7 @@ function doctor() {
|
|
|
345
445
|
if (!["single", "monorepo", "multi-repo"].includes(c.layout)) problems.push(`layout "${c.layout}" must be single, monorepo, or multi-repo`);
|
|
346
446
|
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
447
|
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`);
|
|
448
|
+
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
449
|
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
450
|
const repos = c.layout === "single" ? [{ path: "." }] : Array.isArray(c.repos) ? c.repos : [];
|
|
350
451
|
if (c.layout !== "single" && !repos.length) problems.push(`layout is ${c.layout} but repos: is empty`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Launcher: plain CommonJS so it runs on any Node and can explain a too-old version,
|
|
3
|
+
// instead of the ESM entry failing with a syntax error.
|
|
4
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
5
|
+
if (major < 14) {
|
|
6
|
+
console.error(`✗ spec-driven-dev needs Node.js 14 or newer; this is ${process.version}.`);
|
|
7
|
+
console.error(" If you use nvm, run it from a folder without an old .nvmrc, or: nvm exec 20 npx @benjaminthomas/spec-driven-dev@latest");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
import("./cli.mjs").catch((e) => { console.error(e); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@benjaminthomas/spec-driven-dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Spec-driven AI development workflow (investigate → spec → implement → verify → ship) as Agent Skills for Claude Code, Cursor, Codex CLI and Antigravity.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": { "spec-driven-dev": "bin/
|
|
6
|
+
"bin": { "spec-driven-dev": "bin/spec-driven-dev.cjs" },
|
|
7
7
|
"files": ["bin", "skills"],
|
|
8
8
|
"scripts": { "test": "python3 scripts/check.py && node scripts/test-cli.mjs" },
|
|
9
|
-
"engines": { "node": ">=
|
|
9
|
+
"engines": { "node": ">=14" },
|
|
10
10
|
"keywords": ["agent-skills", "claude-code", "cursor", "codex", "antigravity", "spec-driven-development", "ai-agents"],
|
|
11
11
|
"repository": { "type": "git", "url": "git+https://github.com/benjaminthomas/spec-driven-dev.git" },
|
|
12
12
|
"license": "MIT",
|
|
@@ -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.
|