@benjaminthomas/spec-driven-dev 1.5.1 → 1.7.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 +46 -17
- package/bin/cli.mjs +125 -55
- package/package.json +33 -8
- package/skills/spec-create/SKILL.md +9 -0
- package/skills/spec-create/references/complexity-rubric.md +57 -0
- package/skills/spec-create/references/readme-template.md +6 -4
- package/skills/spec-create/references/task-template.md +6 -0
- package/skills/spec-implement/SKILL.md +25 -0
- package/skills/spec-investigate/SKILL.md +5 -0
- package/skills/spec-investigate/references/report-template.md +5 -0
- package/skills/spec-setup/SKILL.md +4 -1
- package/skills/spec-setup/references/config-template.yml +22 -0
package/README.md
CHANGED
|
@@ -93,6 +93,23 @@ words in quotes.
|
|
|
93
93
|
/spec-ship "ship it"
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
**The right model for each task.** `spec-create` rates every task **light**, **standard**, or
|
|
97
|
+
**deep**, based on how much it touches and how risky it is (data, security, concurrency, public
|
|
98
|
+
contracts, unclear requirements). `spec-implement` then runs each task on the model for its tier:
|
|
99
|
+
|
|
100
|
+
| Tier | Typical task | Claude Code default |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| light | copy change, config value, small UI tweak following an existing pattern | `haiku` |
|
|
103
|
+
| standard | a normal feature slice: route + service + tests | `sonnet` |
|
|
104
|
+
| deep | migrations with backfills, auth/permissions, concurrency, cross-repo contracts | `opus` |
|
|
105
|
+
|
|
106
|
+
- **Reviews:** each batch's review runs on at least `standard`, and on the batch's highest tier.
|
|
107
|
+
- **Escalation:** a task that still fails after a fix round is retried one tier up.
|
|
108
|
+
- **Tools that run one model per session** (Cursor, Codex) don't switch silently. Before a heavier
|
|
109
|
+
batch, they pause and tell you which model to switch to.
|
|
110
|
+
- **Changing the models:** edit the `models:` section of `.spec-driven.yml`, for example to use a
|
|
111
|
+
newer model for `deep`.
|
|
112
|
+
|
|
96
113
|
**Fewer stops:** after reviewing the spec, say **"approved, run it through"**. The workflow then
|
|
97
114
|
implements, verifies, and commits in one go. It stops at the first failure, and it always asks
|
|
98
115
|
before pushing or opening a PR. To make this the default after approval, choose `run-through` during
|
|
@@ -116,8 +133,12 @@ say **"run spec-setup"**: that does a deeper version that also reads `CLAUDE.md`
|
|
|
116
133
|
config, and recent PRs, and fills in what the installer can't (such as each repo's role, or commands
|
|
117
134
|
for stacks other than Node). If a skill finds no config, it runs this setup first.
|
|
118
135
|
|
|
119
|
-
**What `spec-driven-dev` asks, in order
|
|
120
|
-
|
|
136
|
+
**What `spec-driven-dev` asks, in order.** Each question is a menu: move with **↑/↓**, tick with
|
|
137
|
+
**Space**, and confirm with **Enter**. What it detected is pre-selected and marked *(detected)*, so you
|
|
138
|
+
can usually just press Enter. Repo paths are added one at a time: paste a path (quotes, `~`, and
|
|
139
|
+
Windows paths like `C:\work\api` all work). A path that doesn't exist or isn't a git repo is flagged
|
|
140
|
+
straight away, so you can fix it before moving on. Press Enter on an empty line when you're done.
|
|
141
|
+
**Ctrl+C** cancels without changing anything.
|
|
121
142
|
|
|
122
143
|
| Question | Detected from | Example answer |
|
|
123
144
|
|---|---|---|
|
|
@@ -135,6 +156,7 @@ can usually just press Enter):
|
|
|
135
156
|
| Lint / typecheck / test / build / dev commands | `package.json` scripts and lockfile (per repo or package) | `pnpm run lint` |
|
|
136
157
|
| Where specs go | (you choose) | `specs/` |
|
|
137
158
|
| After you approve a spec | (you choose) | `stop` (wait at each stage) or `run-through` |
|
|
159
|
+
| Model per task tier | written with defaults: Claude Code `haiku`/`sonnet`/`opus`, others `default` | edit `models:` later |
|
|
138
160
|
|
|
139
161
|
**Where the file lives, by layout:**
|
|
140
162
|
|
|
@@ -148,12 +170,16 @@ can usually just press Enter):
|
|
|
148
170
|
example, from inside `/Projects/frontend`:
|
|
149
171
|
|
|
150
172
|
```
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
173
|
+
✔ How is this project organised? › Multiple repos
|
|
174
|
+
✔ Which of these repos (found under /Projects) belong to this workspace? › frontend
|
|
175
|
+
? Add a repo from another folder — paste its path, or press Enter to finish (1 selected) › ~/Work/nope
|
|
176
|
+
› Folder not found: /Users/you/Work/nope
|
|
177
|
+
? Add a repo from another folder — paste its path, or press Enter to finish (1 selected) › ~/Work/backend
|
|
178
|
+
✓ added /Users/you/Work/backend
|
|
179
|
+
? Add a repo from another folder — paste its path, or press Enter to finish (2 selected) › /srv/database
|
|
180
|
+
✓ added /srv/database
|
|
181
|
+
? Add a repo from another folder — paste its path, or press Enter to finish (3 selected) ›
|
|
182
|
+
✔ Folder for this workspace's settings file (.spec-driven.yml) › ~/.spec-driven/frontend-workspace
|
|
157
183
|
```
|
|
158
184
|
|
|
159
185
|
- **Where the config goes:** the repos' shared parent folder, or `~/.spec-driven/<name>/` when that
|
|
@@ -408,7 +434,8 @@ touched by an update.
|
|
|
408
434
|
Releases are published by this repo's GitHub Actions workflow, not from anyone's laptop, through
|
|
409
435
|
npm [trusted publishing](https://docs.npmjs.com/trusted-publishers). Each version's npm page shows
|
|
410
436
|
a provenance badge linking it to the exact commit and workflow run that built it. The package has
|
|
411
|
-
|
|
437
|
+
one small dependency (`prompts`, for the setup menus), and it contains only `bin/` and the `skills/`
|
|
438
|
+
folder.
|
|
412
439
|
|
|
413
440
|
**Something isn't working. Where do I start?**
|
|
414
441
|
Run `spec-driven-dev doctor` in your project. It checks the config against your
|
|
@@ -462,8 +489,9 @@ can otherwise reuse an older copy it cached. What changed in each version is in
|
|
|
462
489
|
`npx github:benjaminthomas/spec-driven-dev` installs straight from GitHub instead, for example to
|
|
463
490
|
try an unreleased commit on `main`.
|
|
464
491
|
|
|
465
|
-
**Requirements:** Node.js 14 or newer and git
|
|
466
|
-
|
|
492
|
+
**Requirements:** Node.js 14 or newer and git, on macOS, Linux, or Windows (PowerShell or cmd). The
|
|
493
|
+
installer has one small dependency, [`prompts`](https://www.npmjs.com/package/prompts), for its menus.
|
|
494
|
+
CI tests it on Node 14, 16, 18, 20, 22, and 24, and on Windows and macOS.
|
|
467
495
|
|
|
468
496
|
### Claude Code (plugin, with automatic updates)
|
|
469
497
|
|
|
@@ -532,15 +560,15 @@ skips that step rather than failing.
|
|
|
532
560
|
|
|
533
561
|
| Skill | Stage | SKILL.md lines | Words | Supporting files |
|
|
534
562
|
|---|---|---:|---:|---:|
|
|
535
|
-
| `spec-setup` | 0 · configure |
|
|
536
|
-
| `spec-investigate` | 1 · understand |
|
|
537
|
-
| `spec-create` | 2 · plan |
|
|
538
|
-
| `spec-implement` | 3 · build |
|
|
563
|
+
| `spec-setup` | 0 · configure | 117 | 1,068 | 1 |
|
|
564
|
+
| `spec-investigate` | 1 · understand | 330 | 2,794 | 1 |
|
|
565
|
+
| `spec-create` | 2 · plan | 166 | 1,503 | 5 |
|
|
566
|
+
| `spec-implement` | 3 · build | 298 | 2,375 | 3 |
|
|
539
567
|
| `spec-verify` | 4 · prove | 155 | 1,492 | 1 |
|
|
540
568
|
| `spec-checkpoint` | 5 · commit | 108 | 795 | 0 |
|
|
541
569
|
| `spec-ship` | 6 · PR | 220 | 1,268 | 0 |
|
|
542
570
|
| `generative-seo` | standalone | 255 | 2,209 | 7 |
|
|
543
|
-
| **Total** | | **1,
|
|
571
|
+
| **Total** | | **1,649** | **13,504** | **18** |
|
|
544
572
|
|
|
545
573
|
- **8** skills: **1** setup + **6** workflow stages + **1** standalone
|
|
546
574
|
- **4** tools supported natively (Claude Code, Codex CLI, Cursor, Antigravity), plus an MCP fallback
|
|
@@ -559,7 +587,8 @@ skills/<name>/SKILL.md the skill itself (frontmatter: name, description,
|
|
|
559
587
|
skills/<name>/references/ templates and prompts the skill loads when needed
|
|
560
588
|
.claude-plugin/ Claude Code plugin + marketplace manifests
|
|
561
589
|
.agents/skills/ symlinks so Codex/Antigravity pick the skills up inside this repo
|
|
562
|
-
bin/
|
|
590
|
+
bin/spec-driven-dev.cjs launcher (runs on any Node, explains if it's too old)
|
|
591
|
+
bin/cli.mjs the `spec-driven-dev` CLI: installer, setup menus, doctor, status
|
|
563
592
|
package.json npm package (ships bin/ and skills/)
|
|
564
593
|
mcp-server/ MCP fallback server
|
|
565
594
|
hooks/hooks.json Claude Code session-start hook (runs `cli.mjs status`)
|
package/bin/cli.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import fs from "node:fs";
|
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { execFileSync } from "node:child_process";
|
|
16
|
-
import
|
|
16
|
+
import prompts from "prompts"; // arrow-key menus that work in PowerShell, cmd, macOS and Linux terminals
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
19
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
@@ -61,19 +61,25 @@ if (flag("help") || flag("h")) {
|
|
|
61
61
|
process.exit(0);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
// Every question goes through these helpers. With --yes (or answers given as flags) they return the default.
|
|
65
|
+
const cancelled = () => { console.log("\n✗ Setup cancelled. Nothing was changed."); process.exit(1); };
|
|
66
|
+
async function ask(q) { const r = await prompts({ name: "v", ...q }, { onCancel: cancelled }); return r.v; }
|
|
67
|
+
async function askText(message, initial = "", validate) {
|
|
68
|
+
if (YES) return initial;
|
|
69
|
+
return String((await ask({ type: "text", message, initial, validate })) ?? "").trim();
|
|
69
70
|
}
|
|
70
|
-
async function
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
async function askSelect(message, choices, initialValue) {
|
|
72
|
+
if (YES) return initialValue;
|
|
73
|
+
const initial = Math.max(0, choices.findIndex((c) => c.value === initialValue));
|
|
74
|
+
return ask({ type: "select", message, choices, initial, hint: "↑/↓ then Enter" });
|
|
75
|
+
}
|
|
76
|
+
async function askMulti(message, choices) {
|
|
77
|
+
if (YES) return choices.filter((c) => c.selected).map((c) => c.value);
|
|
78
|
+
return ask({ type: "multiselect", message, choices, instructions: false, hint: "↑/↓ move · Space select · A all · Enter confirm" });
|
|
79
|
+
}
|
|
80
|
+
async function askConfirm(message, initial = true) {
|
|
81
|
+
if (YES) return initial;
|
|
82
|
+
return ask({ type: "toggle", message, initial, active: "yes", inactive: "no" });
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
const run = (cmd, argv, cwd) => {
|
|
@@ -96,10 +102,8 @@ function copyDir(from, to) {
|
|
|
96
102
|
async function install() {
|
|
97
103
|
const detected = Object.keys(TOOLS).filter((t) => fs.existsSync(path.join(HOME, TOOLS[t].home)) || onPath(TOOLS[t].bin));
|
|
98
104
|
let tools = opt("tools")?.split(",");
|
|
99
|
-
if (!tools)
|
|
100
|
-
|
|
101
|
-
tools = await pick("Tools", Object.entries(TOOLS).map(([value, t]) => ({ value, label: t.label + (detected.includes(value) ? " ✓ detected" : "") })), detected);
|
|
102
|
-
}
|
|
105
|
+
if (!tools) tools = await askMulti("Which AI coding tools should get the skills?",
|
|
106
|
+
Object.entries(TOOLS).map(([value, t]) => ({ value, title: t.label + (detected.includes(value) ? " (installed)" : ""), selected: detected.includes(value) })));
|
|
103
107
|
const bad = tools.filter((t) => !TOOLS[t]);
|
|
104
108
|
if (bad.length) throw new Error(`Unknown tool(s): ${bad.join(", ")} — expected ${Object.keys(TOOLS).join(", ")}`);
|
|
105
109
|
|
|
@@ -110,7 +114,10 @@ async function install() {
|
|
|
110
114
|
tools = tools.filter((t) => t !== "claude");
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
const global = flag("global") || (
|
|
117
|
+
const global = flag("global") || (await askSelect("Install the skills for…", [
|
|
118
|
+
{ title: "This project only", value: false, description: "skills go in this folder, e.g. .cursor/skills/" },
|
|
119
|
+
{ title: "All my projects", value: true, description: "skills go in your home folder, e.g. ~/.cursor/skills/" },
|
|
120
|
+
], false));
|
|
114
121
|
const skills = opt("skills")?.split(",") || ALL_SKILLS;
|
|
115
122
|
const unknown = skills.filter((s) => !ALL_SKILLS.includes(s));
|
|
116
123
|
if (unknown.length) throw new Error(`Unknown skill(s): ${unknown.join(", ")} — expected ${ALL_SKILLS.join(", ")}`);
|
|
@@ -152,7 +159,7 @@ function findRepos(dir, depth = 2, prefix = "") {
|
|
|
152
159
|
|
|
153
160
|
function detectLayout(dir) {
|
|
154
161
|
if (isRepo(dir)) {
|
|
155
|
-
const top = run("git", ["rev-parse", "--show-toplevel"], dir);
|
|
162
|
+
const top = path.resolve(run("git", ["rev-parse", "--show-toplevel"], dir)); // C:/x → C:\x on Windows
|
|
156
163
|
const pkg = readJson(path.join(top, "package.json"));
|
|
157
164
|
const cargo = fs.existsSync(path.join(top, "Cargo.toml")) && fs.readFileSync(path.join(top, "Cargo.toml"), "utf8").includes("[workspace]");
|
|
158
165
|
const hasManifest = (rel) => MANIFESTS.some((m) => fs.existsSync(path.join(top, rel, m)));
|
|
@@ -228,11 +235,22 @@ function toYaml(c) {
|
|
|
228
235
|
out += `tracker:\n type: ${c.tracker.type}\n key: ${y(c.tracker.key)}\n url: ${y(c.tracker.url)}\n`;
|
|
229
236
|
out += `git:\n${cmds(c.git, " ")}\n`;
|
|
230
237
|
out += `commands:\n${cmds(c.commands, " ")}\n dev_url: ${y(c.devUrl)}\n`;
|
|
238
|
+
out += "models: # model per task tier (light / standard / deep); \"default\" = the session's model\n" + Object.entries(MODEL_DEFAULTS)
|
|
239
|
+
.map(([tool, m]) => ` ${tool}:\n` + TIERS.map((t) => ` ${t}: ${m[t]}\n`).join("")).join("");
|
|
231
240
|
out += `pipeline:\n after_approval: ${c.afterApproval || "stop"} # stop | run-through (implement → verify → checkpoint; ship always asks)\n`;
|
|
232
241
|
return out + `specs_dir: ${y(c.specsDir)}\n`;
|
|
233
242
|
}
|
|
234
243
|
|
|
235
244
|
const LAYOUTS = ["single", "monorepo", "multi-repo"];
|
|
245
|
+
const TIERS = ["light", "standard", "deep"];
|
|
246
|
+
// Aliases, not dated model IDs, so they keep pointing at the current models. Other tools default to
|
|
247
|
+
// the session's own model; spec-implement then tells you when to switch.
|
|
248
|
+
const MODEL_DEFAULTS = {
|
|
249
|
+
claude: { light: "haiku", standard: "sonnet", deep: "opus" },
|
|
250
|
+
cursor: { light: "default", standard: "default", deep: "default" },
|
|
251
|
+
codex: { light: "default", standard: "default", deep: "default" },
|
|
252
|
+
antigravity: { light: "default", standard: "default", deep: "default" },
|
|
253
|
+
};
|
|
236
254
|
const splitList = (v) => (v || "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
237
255
|
// Paths are stored relative to the config's folder when inside it, absolute otherwise.
|
|
238
256
|
const real = (p) => { try { return fs.realpathSync(p); } catch { return p; } }; // /var vs /private/var etc.
|
|
@@ -242,14 +260,25 @@ async function chooseLayout(detected) {
|
|
|
242
260
|
const forced = opt("layout");
|
|
243
261
|
if (forced && !LAYOUTS.includes(forced)) throw new Error(`--layout must be one of ${LAYOUTS.join(", ")}`);
|
|
244
262
|
if (forced) return forced;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
263
|
+
const tag = (v) => (v === detected ? " (detected)" : "");
|
|
264
|
+
return askSelect("How is this project organised?", [
|
|
265
|
+
{ title: `Single repo${tag("single")}`, value: "single", description: "one git repo, one app" },
|
|
266
|
+
{ title: `Monorepo${tag("monorepo")}`, value: "monorepo", description: "one git repo with several apps or packages" },
|
|
267
|
+
{ title: `Multiple repos${tag("multi-repo")}`, value: "multi-repo", description: "several git repos that change together, in any folders" },
|
|
268
|
+
], detected);
|
|
250
269
|
}
|
|
251
270
|
|
|
252
|
-
const expandHome = (p) => p.replace(/^~(
|
|
271
|
+
const expandHome = (p) => p.replace(/^~(?=$|[\\/])/, HOME);
|
|
272
|
+
// Accept what people paste: surrounding quotes, ~, relative paths, Windows or POSIX separators.
|
|
273
|
+
const cleanPath = (v) => path.resolve(DIR, expandHome(String(v).trim().replace(/^(["'])(.*)\1$/, "$2").trim()));
|
|
274
|
+
const gitTop = (p) => { const t = run("git", ["rev-parse", "--show-toplevel"], p); return t ? path.resolve(t) : p; }; // git prints C:/x on Windows
|
|
275
|
+
function repoPathProblem(v) {
|
|
276
|
+
if (!String(v || "").trim()) return null;
|
|
277
|
+
const p = cleanPath(v);
|
|
278
|
+
if (!fs.existsSync(p)) return `Folder not found: ${p}`;
|
|
279
|
+
if (!isRepo(p)) return `Not a git repo: ${p}`;
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
253
282
|
|
|
254
283
|
function commonParent(paths) {
|
|
255
284
|
if (!paths.length) return "";
|
|
@@ -266,7 +295,7 @@ async function chooseRepos(d) {
|
|
|
266
295
|
let picked = [];
|
|
267
296
|
const fromFlag = opt("repos");
|
|
268
297
|
if (fromFlag) {
|
|
269
|
-
picked = splitList(fromFlag).map(
|
|
298
|
+
picked = splitList(fromFlag).map(cleanPath);
|
|
270
299
|
} else {
|
|
271
300
|
const found = findRepos(scanRoot);
|
|
272
301
|
if (found.length) {
|
|
@@ -274,19 +303,25 @@ async function chooseRepos(d) {
|
|
|
274
303
|
for (const r of found) { const o = gitFacts(path.join(scanRoot, r))?.owner || "?"; (owners[o] = owners[o] || []).push(r); }
|
|
275
304
|
const biggest = Object.values(owners).sort((a, b) => b.length - a.length)[0] || [];
|
|
276
305
|
const here = insideRepo ? [path.relative(scanRoot, d.root)] : [];
|
|
277
|
-
|
|
278
|
-
picked = (await
|
|
306
|
+
const pre = new Set([...here, ...(insideRepo ? [] : biggest)]);
|
|
307
|
+
picked = (await askMulti(`Which of these repos (found under ${scanRoot}) belong to this workspace?`,
|
|
308
|
+
found.map((r) => { const o = Object.keys(owners).find((k) => owners[k].includes(r)); return { value: r, title: r + (o && o !== "?" ? ` (${o})` : ""), selected: pre.has(r) }; })))
|
|
279
309
|
.map((r) => path.join(scanRoot, r));
|
|
280
310
|
} else if (insideRepo) picked = [d.root];
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
311
|
+
// Repos elsewhere: one path at a time, checked as you type, so a typo can be fixed straight away.
|
|
312
|
+
if (!YES) for (;;) {
|
|
313
|
+
const v = await askText(`Add a repo from another folder — paste its path, or press Enter to finish${picked.length ? ` (${picked.length} selected)` : ""}`, "",
|
|
314
|
+
(x) => repoPathProblem(x) || true);
|
|
315
|
+
if (!v) break;
|
|
316
|
+
picked.push(cleanPath(v));
|
|
317
|
+
console.log(` ✓ added ${gitTop(cleanPath(v))}`);
|
|
318
|
+
}
|
|
284
319
|
}
|
|
285
320
|
const abs = [];
|
|
286
321
|
for (const p of [...new Set(picked)]) {
|
|
287
322
|
if (!fs.existsSync(p)) console.log(` ✗ ${p}: folder not found — skipped`);
|
|
288
323
|
else if (!isRepo(p)) console.log(` ✗ ${p}: not a git repo — skipped`);
|
|
289
|
-
else abs.push(
|
|
324
|
+
else abs.push(gitTop(p));
|
|
290
325
|
}
|
|
291
326
|
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
327
|
|
|
@@ -299,7 +334,7 @@ async function chooseRepos(d) {
|
|
|
299
334
|
const defRoot = opt("workspace") ? path.resolve(DIR, expandHome(opt("workspace")))
|
|
300
335
|
: allUnderScan ? scanRoot
|
|
301
336
|
: tooBroad ? path.join(HOME, ".spec-driven", path.basename(abs[0]) + "-workspace") : parent;
|
|
302
|
-
const root =
|
|
337
|
+
const root = cleanPath(await askText("Folder for this workspace's settings file (.spec-driven.yml)", defRoot));
|
|
303
338
|
fs.mkdirSync(root, { recursive: true });
|
|
304
339
|
const repos = [...new Set(abs)].map((p) => relOrAbs(root, p));
|
|
305
340
|
console.log(` Workspace repos: ${repos.join(", ")}`);
|
|
@@ -321,9 +356,17 @@ function register(file, repoPaths) {
|
|
|
321
356
|
async function choosePackages(d) {
|
|
322
357
|
const root = d.root;
|
|
323
358
|
const fromFlag = opt("packages");
|
|
324
|
-
|
|
359
|
+
let chosen = fromFlag ? splitList(fromFlag) : d.repos.length
|
|
360
|
+
? await askMulti("Which packages are part of this monorepo?", d.repos.map((r) => ({ value: r, title: r, selected: true })))
|
|
361
|
+
: [];
|
|
362
|
+
if (!fromFlag && !YES) for (;;) {
|
|
363
|
+
const v = await askText("Add another package folder (relative to the repo root), or press Enter to finish", "",
|
|
364
|
+
(x) => !String(x || "").trim() || fs.existsSync(path.resolve(root, String(x).trim())) || `Folder not found in ${root}`);
|
|
365
|
+
if (!v) break;
|
|
366
|
+
chosen.push(v);
|
|
367
|
+
}
|
|
325
368
|
const pkgs = [];
|
|
326
|
-
for (const p of
|
|
369
|
+
for (const p of chosen) {
|
|
327
370
|
if (fs.existsSync(path.resolve(root, p))) pkgs.push(relOrAbs(root, path.resolve(root, p)));
|
|
328
371
|
else console.log(` ✗ ${p}: folder not found in ${root} — skipped`);
|
|
329
372
|
}
|
|
@@ -342,7 +385,7 @@ async function setup() {
|
|
|
342
385
|
const { root, repos } = layout === "multi-repo" ? await chooseRepos(d) : layout === "monorepo" ? await choosePackages(d) : { root: d.layout === "multi-repo" ? DIR : d.root, repos: [] };
|
|
343
386
|
if (layout === "single" && !isRepo(root)) console.log(` • ${root} isn't a git repo — run setup inside the repo, or choose multi-repo.`);
|
|
344
387
|
const file = path.join(root, ".spec-driven.yml");
|
|
345
|
-
if (fs.existsSync(file) && (await
|
|
388
|
+
if (fs.existsSync(file) && !(await askConfirm(`${file} already exists. Replace it?`, false))) {
|
|
346
389
|
console.log("• Kept the existing config. Run the spec-setup skill in your AI tool to review it.");
|
|
347
390
|
return;
|
|
348
391
|
}
|
|
@@ -352,22 +395,44 @@ async function setup() {
|
|
|
352
395
|
const g = infer(facts);
|
|
353
396
|
if (facts.length) console.log(`Detected: tracker ${g.trackerType}${g.key ? ` (${g.key}-…)` : ""}, base ${g.base}, branches "${g.branchName}", commits ${g.commitStyle}`);
|
|
354
397
|
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
398
|
+
const det = (v, d) => (v === d ? " (detected)" : "");
|
|
399
|
+
const trackerType = await askSelect("Where do your tickets live?", [
|
|
400
|
+
{ title: `Jira${det("jira", g.trackerType)}`, value: "jira" },
|
|
401
|
+
{ title: `GitHub Issues${det("github", g.trackerType)}`, value: "github" },
|
|
402
|
+
{ title: `Linear${det("linear", g.trackerType)}`, value: "linear" },
|
|
403
|
+
{ title: `No tracker${det("none", g.trackerType)}`, value: "none", description: "work from transcripts, docs, or plain descriptions" },
|
|
404
|
+
], g.trackerType);
|
|
405
|
+
const key = trackerType === "jira" || trackerType === "linear" ? (await askText("Ticket key prefix (the STP in STP-123)", g.key)).toUpperCase() : "";
|
|
406
|
+
const url = trackerType === "none" ? "" : await askText("Tracker URL (optional, press Enter to skip)", "");
|
|
407
|
+
const branchChoices = (initial) => [
|
|
408
|
+
{ title: "Each repo's default branch", value: "default", description: "e.g. main or master" },
|
|
409
|
+
...[...new Set(facts.map((f) => f.integration).filter(Boolean).concat(initial !== "default" ? [initial] : []))].map((b) => ({ title: b, value: b })),
|
|
410
|
+
{ title: "Another branch…", value: "__other" },
|
|
411
|
+
];
|
|
412
|
+
const pickBranch = async (message, initial) => {
|
|
413
|
+
const v = await askSelect(message, branchChoices(initial), initial);
|
|
414
|
+
return v === "__other" ? askText("Branch name", "") : v;
|
|
415
|
+
};
|
|
416
|
+
const base = await pickBranch("Which branch is new work started from?", g.base);
|
|
417
|
+
const pr = await pickBranch("Which branch do pull requests go into?", base);
|
|
418
|
+
const ex = key || "STP";
|
|
419
|
+
let branchName = await askSelect("How are work branches named?", [
|
|
420
|
+
{ title: `${ex}-123${det("{ticket}", g.branchName)}`, value: "{ticket}", description: "just the ticket ID" },
|
|
421
|
+
{ title: `feat/${ex}-123-add-login${det("{type}/{ticket}-{slug}", g.branchName)}`, value: "{type}/{ticket}-{slug}" },
|
|
422
|
+
{ title: `feat/add-login${det("{type}/{slug}", g.branchName)}`, value: "{type}/{slug}", description: "no ticket ID" },
|
|
423
|
+
{ title: "Custom pattern…", value: "__custom" },
|
|
424
|
+
], g.branchName);
|
|
425
|
+
if (branchName === "__custom") branchName = await askText("Pattern using {ticket}, {slug}, {type}", g.branchName, (x) => /\{(ticket|slug)\}/.test(x) || "Include {ticket} or {slug}");
|
|
426
|
+
const commitStyle = await askSelect("How are commit messages written?", [
|
|
427
|
+
{ title: `feat: add login${det("conventional", g.commitStyle)}`, value: "conventional", description: "conventional commits" },
|
|
428
|
+
{ title: `${ex}-123: add login${det("ticket", g.commitStyle)}`, value: "ticket", description: "ticket ID first" },
|
|
429
|
+
{ title: `Add login${det("freeform", g.commitStyle)}`, value: "freeform", description: "no fixed format" },
|
|
430
|
+
], g.commitStyle);
|
|
431
|
+
const specsDir = await askText("Folder for specs (one sub-folder per feature)", "specs");
|
|
432
|
+
const afterApproval = opt("after-approval") || await askSelect("After you approve a spec…", [
|
|
433
|
+
{ title: "Stop after each stage", value: "stop", description: "you start implement, verify and checkpoint yourself" },
|
|
434
|
+
{ title: "Run straight through", value: "run-through", description: "implement → verify → commit in one go; stops at any failure, never pushes without asking" },
|
|
435
|
+
], "stop");
|
|
371
436
|
|
|
372
437
|
const config = {
|
|
373
438
|
layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "", afterApproval,
|
|
@@ -412,7 +477,8 @@ function parseYaml(text) {
|
|
|
412
477
|
}
|
|
413
478
|
const m = line.match(/^([\w-]+):\s*(.*)$/);
|
|
414
479
|
if (!m) continue;
|
|
415
|
-
const [, key,
|
|
480
|
+
const [, key, rawVal] = m;
|
|
481
|
+
const val = rawVal.replace(/(^|\s+)#.*$/, "").trim(); // `key: # comment` opens a block
|
|
416
482
|
if (val === "") {
|
|
417
483
|
const next = text.split("\n").slice(text.split("\n").indexOf(raw) + 1).find((l) => l.trim() && !l.trim().startsWith("#"));
|
|
418
484
|
parent[key] = next && next.trim().startsWith("- ") ? [] : {};
|
|
@@ -445,6 +511,11 @@ function doctor() {
|
|
|
445
511
|
if (!["single", "monorepo", "multi-repo"].includes(c.layout)) problems.push(`layout "${c.layout}" must be single, monorepo, or multi-repo`);
|
|
446
512
|
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`);
|
|
447
513
|
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`);
|
|
514
|
+
for (const [tool, m] of Object.entries(c.models || {})) {
|
|
515
|
+
if (typeof m !== "object") { problems.push(`models.${tool} must list light / standard / deep`); continue; }
|
|
516
|
+
for (const t of TIERS) if (!m[t]) problems.push(`models.${tool}.${t} is missing (use "default" for the session's model)`);
|
|
517
|
+
for (const k of Object.keys(m)) if (!TIERS.includes(k)) problems.push(`models.${tool}.${k}: unknown tier (use light, standard, deep)`);
|
|
518
|
+
}
|
|
448
519
|
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`);
|
|
449
520
|
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}`);
|
|
450
521
|
const repos = c.layout === "single" ? [{ path: "." }] : Array.isArray(c.repos) ? c.repos : [];
|
|
@@ -504,7 +575,7 @@ try {
|
|
|
504
575
|
else if (command === "status") status();
|
|
505
576
|
else if (command === "install") {
|
|
506
577
|
await install();
|
|
507
|
-
if (!flag("no-setup") && (
|
|
578
|
+
if (!flag("no-setup") && (await askConfirm("Set up this project now?", true))) await setup();
|
|
508
579
|
} else if (command === "setup") await setup();
|
|
509
580
|
else throw new Error(`Unknown command "${command}" — use install (default), setup, doctor, or status`);
|
|
510
581
|
if (["install", "setup"].includes(command)) console.log('\nNext: in your AI tool, say "investigate <ticket>" or "create a spec".');
|
|
@@ -512,5 +583,4 @@ try {
|
|
|
512
583
|
console.error(`✗ ${e.message}`);
|
|
513
584
|
process.exitCode = 1;
|
|
514
585
|
} finally {
|
|
515
|
-
rl?.close();
|
|
516
586
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@benjaminthomas/spec-driven-dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
"bin": {
|
|
7
|
+
"spec-driven-dev": "bin/spec-driven-dev.cjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"skills"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "python3 scripts/check.py && node scripts/test-cli.mjs"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=14"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"agent-skills",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"cursor",
|
|
23
|
+
"codex",
|
|
24
|
+
"antigravity",
|
|
25
|
+
"spec-driven-development",
|
|
26
|
+
"ai-agents"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/benjaminthomas/spec-driven-dev.git"
|
|
31
|
+
},
|
|
12
32
|
"license": "MIT",
|
|
13
|
-
"publishConfig": {
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"prompts": "2.4.2"
|
|
38
|
+
}
|
|
14
39
|
}
|
|
@@ -68,6 +68,11 @@ Think carefully about granularity. Too coarse and agents can't work in parallel.
|
|
|
68
68
|
|
|
69
69
|
Tests ship with the code they cover: fill in each task's **Tests** section (see the task template) instead of creating separate "write tests" tasks. Only add a standalone testing task for cross-cutting suites (e.g. a new E2E harness) or when the user asks for one.
|
|
70
70
|
|
|
71
|
+
**Rate each task's complexity.** Read `references/complexity-rubric.md` and give every task a tier
|
|
72
|
+
(`light`, `standard`, or `deep`) with the signals that justify it. `spec-implement` uses the tier to
|
|
73
|
+
pick the model for that task, so a risky task under-rated as `light` is the costly mistake. Round up
|
|
74
|
+
when unsure. If one task scores `deep` mainly because it's doing too much, split it first.
|
|
75
|
+
|
|
71
76
|
### Step 4: Build the Dependency Graph
|
|
72
77
|
|
|
73
78
|
For each task, identify:
|
|
@@ -130,6 +135,10 @@ Files created:
|
|
|
130
135
|
- action-required.md
|
|
131
136
|
- tasks/ ({T} task files)
|
|
132
137
|
|
|
138
|
+
Model plan: {n} light · {n} standard · {n} deep
|
|
139
|
+
(models from .spec-driven.yml → light: {model}, standard: {model}, deep: {model})
|
|
140
|
+
Deep tasks: {task-nn — main reason}
|
|
141
|
+
|
|
133
142
|
Batch breakdown:
|
|
134
143
|
- Batch 1: {count} tasks (parallel) — {brief description}
|
|
135
144
|
- Batch 2: {count} tasks (parallel) — {brief description}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Complexity Rubric
|
|
2
|
+
|
|
3
|
+
Use this to give every task a **tier**: `light`, `standard`, or `deep`. `spec-implement` runs the
|
|
4
|
+
task on the model mapped to that tier in `.spec-driven.yml` (`models:`). Rate from the evidence in
|
|
5
|
+
the task file and investigation, not from how long the description is.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
- Scoring
|
|
9
|
+
- Tier from score
|
|
10
|
+
- Overrides
|
|
11
|
+
- Examples
|
|
12
|
+
- What to write in the task file
|
|
13
|
+
|
|
14
|
+
## Scoring
|
|
15
|
+
|
|
16
|
+
Add one point for each signal that applies:
|
|
17
|
+
|
|
18
|
+
| Signal | Point when… |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Spread | the task changes more than 3 files, or files in more than one module/package/repo |
|
|
21
|
+
| New design | there's no existing pattern in the repo to copy (new abstraction, algorithm, or integration) |
|
|
22
|
+
| Data | it changes a schema, migration, persisted format, or data backfill |
|
|
23
|
+
| Concurrency | it involves async ordering, queues, retries, locking, caching, or idempotency |
|
|
24
|
+
| Security | it touches auth, permissions, tenant isolation, secrets, payments, or untrusted input |
|
|
25
|
+
| Public contract | it changes an API, event, or interface that other code or other repos depend on |
|
|
26
|
+
| Ambiguity | acceptance criteria rely on judgement, or the investigation left UNKNOWNs in this area |
|
|
27
|
+
| Hard to verify | correctness can't be fully shown by the tests listed (timing, performance, UI feel) |
|
|
28
|
+
|
|
29
|
+
## Tier from score
|
|
30
|
+
|
|
31
|
+
| Score | Tier | Typical work |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| 0–1 | `light` | copy change, config value, small UI tweak, adding a field following an existing pattern |
|
|
34
|
+
| 2–3 | `standard` | a normal feature slice: a route + service + tests following existing patterns |
|
|
35
|
+
| 4+ | `deep` | cross-cutting design, migrations with backfills, auth/permission logic, concurrency fixes |
|
|
36
|
+
|
|
37
|
+
## Overrides
|
|
38
|
+
|
|
39
|
+
- **Security or Data alone make a task at least `standard`**, even if nothing else applies.
|
|
40
|
+
- **Security + Data, or Security + Concurrency, make it `deep`**, whatever the score.
|
|
41
|
+
- If the score is borderline, round up: a tier too high costs money, a tier too low costs rework.
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
- "Rename the Export button to Download CSV" → 0 → `light`
|
|
46
|
+
- "Add `exported_at` column and show it on the orders page" → Data, Spread → 2 → `standard`
|
|
47
|
+
- "Make the CSV export stream rows instead of loading 10k into memory" → New design, Concurrency, Hard to verify → 3 → `standard`
|
|
48
|
+
- "Enforce tenant isolation on all export endpoints and backfill missing tenant IDs" → Security + Data → `deep`
|
|
49
|
+
|
|
50
|
+
## What to write in the task file
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
## Complexity
|
|
54
|
+
|
|
55
|
+
**Tier:** standard
|
|
56
|
+
**Signals:** Data (adds `exported_at` migration), Spread (api + web)
|
|
57
|
+
```
|
|
@@ -29,10 +29,12 @@ graph TD
|
|
|
29
29
|
|
|
30
30
|
## Batches
|
|
31
31
|
|
|
32
|
-
| Batch | Tasks | Description |
|
|
33
|
-
|
|
34
|
-
| 1 | task-01, task-02 | {Brief description of what this batch accomplishes} |
|
|
35
|
-
| 2 | task-03 | {Brief description} |
|
|
32
|
+
| Batch | Tasks | Tiers | Description |
|
|
33
|
+
|------|-------|-------|-------------|
|
|
34
|
+
| 1 | task-01, task-02 | light, standard | {Brief description of what this batch accomplishes} |
|
|
35
|
+
| 2 | task-03 | deep | {Brief description} |
|
|
36
|
+
|
|
37
|
+
**Model plan:** {n} light · {n} standard · {n} deep. Deep tasks: {task-03 — reason}.
|
|
36
38
|
|
|
37
39
|
## Task Status
|
|
38
40
|
|
|
@@ -15,6 +15,11 @@ pending
|
|
|
15
15
|
|
|
16
16
|
{N}
|
|
17
17
|
|
|
18
|
+
## Complexity
|
|
19
|
+
|
|
20
|
+
**Tier:** {light | standard | deep}
|
|
21
|
+
**Signals:** {which rubric signals applied, with the evidence — or "none"}
|
|
22
|
+
|
|
18
23
|
## Description
|
|
19
24
|
|
|
20
25
|
{2-5 sentences describing what this task accomplishes and why it matters in the context of the overall feature. Include enough context that an agent unfamiliar with the planning conversation understands the purpose.}
|
|
@@ -82,5 +87,6 @@ pending
|
|
|
82
87
|
- The **Dependencies** section includes prose context (not just filenames) so the agent understands its inputs without reading other files. This is the most important part of making task files self-contained.
|
|
83
88
|
- **Files to Create** and **Files to Modify** make the task's scope explicit. Tasks in the same batch should not overlap on these lists.
|
|
84
89
|
- **Technical Details** captures everything from the planning conversation — CLI commands, schemas, code snippets, file paths, env vars, API endpoints. If it was discussed during planning and is relevant to this task, it belongs here.
|
|
90
|
+
- **Complexity** comes from `complexity-rubric.md`. `spec-implement` picks the coder's model from this tier, so rate honestly: under-rating a risky task is the expensive mistake.
|
|
85
91
|
- **Tests** names the tests the coder writes alongside the code. A behavioural acceptance criterion without a matching test is a gap — add the test or explain why it can only be checked by `spec-verify`.
|
|
86
92
|
- **Acceptance Criteria** should be specific enough that completion can be verified objectively (by a code review agent or a human).
|
|
@@ -108,6 +108,25 @@ Read `references/coder-prompt-template.md` and construct each agent's prompt by
|
|
|
108
108
|
|
|
109
109
|
The coder agents should NOT commit their changes — the orchestrator handles commits after review.
|
|
110
110
|
|
|
111
|
+
**Pick each coder's model from the task's tier.** Read the task's `## Complexity` tier (treat a
|
|
112
|
+
missing tier as `standard`) and look up the model in `.spec-driven.yml` → `models.<tool>.<tier>`,
|
|
113
|
+
where `<tool>` is the host you're running in (`claude`, `codex`, `cursor`, `antigravity`). Without a
|
|
114
|
+
`models` section, use `models.claude` defaults: `light → haiku`, `standard → sonnet`,
|
|
115
|
+
`deep → opus`.
|
|
116
|
+
|
|
117
|
+
- **Host can set a model per agent** (e.g. Claude Code's Agent tool `model` parameter): dispatch
|
|
118
|
+
each coder with its tier's model.
|
|
119
|
+
- **Host can't** (one model per session, as in Cursor or Codex): don't silently run deep tasks on a
|
|
120
|
+
light model. Before dispatching a batch, compare its highest tier with the model you're running
|
|
121
|
+
on. If the batch needs a stronger one, stop and say:
|
|
122
|
+
`Batch {N} has deep tasks ({task-nn: reason}). Switch to {models.<tool>.deep} for this batch, then
|
|
123
|
+
say "continue".` If the batch only has light tasks and you're on the deep model, mention that a
|
|
124
|
+
cheaper model would do, but carry on.
|
|
125
|
+
- If the mapped value is `default`, use whatever model the session already runs, without
|
|
126
|
+
recommending a switch.
|
|
127
|
+
|
|
128
|
+
Report the model used for each task in the batch summary.
|
|
129
|
+
|
|
111
130
|
**Isolate parallel coders in git worktrees when the host allows it.** Coders sharing one working
|
|
112
131
|
tree can clobber each other's edits and each other's lint/test runs. If your host can run an agent
|
|
113
132
|
in its own worktree (e.g. Claude Code's `isolation: "worktree"`), use it for every coder in a
|
|
@@ -143,6 +162,9 @@ Read `references/review-prompt-template.md` and construct the review prompt by f
|
|
|
143
162
|
- **{task_summaries}**: for each task in this batch, the task title and the coder agent's completion summary
|
|
144
163
|
- **{verification_commands}**: `commands.lint`/`typecheck`/`test` from `.spec-driven.yml` for each repo or package touched, or the project's own commands if there's no config (e.g., `pnpm lint && pnpm typecheck && pnpm test`)
|
|
145
164
|
|
|
165
|
+
Run the reviewer on the model for the **highest tier in the batch**, and never below `standard`: the
|
|
166
|
+
review is where deep mistakes get caught.
|
|
167
|
+
|
|
146
168
|
The review agent should:
|
|
147
169
|
1. Run lint, typecheck, and tests
|
|
148
170
|
2. Verify acceptance criteria and the tests named in each task file
|
|
@@ -160,6 +182,9 @@ If the review returns **FAIL**:
|
|
|
160
182
|
- **{issues}**: the specific issues for this task group
|
|
161
183
|
- **{task_content}**: the original task file for context
|
|
162
184
|
4. After fix agents complete, re-run the review (Step 6)
|
|
185
|
+
5. **Escalate on repeat failure.** If a task's issues survive a fix cycle, run its next fix one tier
|
|
186
|
+
higher (`light → standard → deep`). Note the escalation in the batch report. When a task needed
|
|
187
|
+
escalating, its tier was probably wrong, which is useful feedback for future specs.
|
|
163
188
|
|
|
164
189
|
**Cap at 3 review cycles per batch.** If the third review still fails, stop and report to the user:
|
|
165
190
|
|
|
@@ -288,6 +288,11 @@ gitignores `specs/`, matching whatever this repo already does there). Use the st
|
|
|
288
288
|
Every section should be specific and evidence-backed, or explicitly say "None found" /
|
|
289
289
|
"Not applicable to this ticket" rather than being left looking like an oversight.
|
|
290
290
|
|
|
291
|
+
**Complexity Estimate** uses the same signals and tiers `spec-create` will apply per task: spread,
|
|
292
|
+
new design, data, concurrency, security, public contract, ambiguity, hard to verify. Security plus
|
|
293
|
+
data, or security plus concurrency, means `deep`. Say which model is worth planning this on, since a
|
|
294
|
+
deep ticket deserves the strongest model for `spec-create` too.
|
|
295
|
+
|
|
291
296
|
**Recommended Implementation Direction** describes the technically-supported direction the
|
|
292
297
|
evidence points to — reusing existing patterns the investigation actually found, not a novel
|
|
293
298
|
architecture invented on the spot. It is not permission to start building; flag any business
|
|
@@ -48,6 +48,11 @@ Copy this structure into `specs/{feature-slug}/investigation.md`.
|
|
|
48
48
|
### Proposed Solutions Discussed
|
|
49
49
|
### Open Questions
|
|
50
50
|
### Conflicts / Discrepancies
|
|
51
|
+
## Complexity Estimate
|
|
52
|
+
- Overall tier: (light / standard / deep): the tier of the hardest part of the work
|
|
53
|
+
- Signals: (which of spread, new design, data, concurrency, security, public contract, ambiguity, hard to verify apply, each with its evidence)
|
|
54
|
+
- Suggested model for planning this: (the `deep` model if the tier is deep; otherwise the session's model is fine)
|
|
55
|
+
|
|
51
56
|
## Recommended Implementation Direction
|
|
52
57
|
## References
|
|
53
58
|
```
|
|
@@ -69,7 +69,10 @@ Read, don't guess. Collect a draft of every field in `references/config-template
|
|
|
69
69
|
steps (CI is often the most truthful source). Dev URL from the dev script, `.env.example`, or
|
|
70
70
|
framework defaults.
|
|
71
71
|
|
|
72
|
-
6. **
|
|
72
|
+
6. **Models per tier.** Write the `models:` block from the template. Ask which models the user
|
|
73
|
+
has for each tool they installed, only if they want to tune it: the Claude defaults (haiku /
|
|
74
|
+
sonnet / opus) work as-is, and `default` is safe for the rest.
|
|
75
|
+
7. **After approval.** Ask whether an approved spec should stop after each stage (`stop`), or
|
|
73
76
|
run implement → verify → checkpoint in one go (`run-through`), stopping at any failure and never
|
|
74
77
|
pushing. Store it as `pipeline.after_approval`.
|
|
75
78
|
|
|
@@ -42,6 +42,28 @@ 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
|
+
# Which model runs each task tier (spec-create rates every task light / standard / deep).
|
|
46
|
+
# Values are whatever your tool accepts; "default" means "whatever model the session is using".
|
|
47
|
+
# Hosts that can pick a model per agent (Claude Code) switch automatically; others (Cursor, Codex)
|
|
48
|
+
# tell you which model to switch to before a heavier batch.
|
|
49
|
+
models:
|
|
50
|
+
claude:
|
|
51
|
+
light: haiku
|
|
52
|
+
standard: sonnet
|
|
53
|
+
deep: opus
|
|
54
|
+
cursor:
|
|
55
|
+
light: default
|
|
56
|
+
standard: default
|
|
57
|
+
deep: default
|
|
58
|
+
codex:
|
|
59
|
+
light: default
|
|
60
|
+
standard: default
|
|
61
|
+
deep: default
|
|
62
|
+
antigravity:
|
|
63
|
+
light: default
|
|
64
|
+
standard: default
|
|
65
|
+
deep: default
|
|
66
|
+
|
|
45
67
|
pipeline:
|
|
46
68
|
# stop — every stage waits for you (default)
|
|
47
69
|
# run-through — after you approve a spec: implement → verify → checkpoint in one go,
|