@benjaminthomas/spec-driven-dev 1.11.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -63,9 +63,11 @@ start a new session and continue where you left off.
|
|
|
63
63
|
npx @benjaminthomas/spec-driven-dev@latest
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
This one command downloads the tool, installs the skills, and sets up the project.
|
|
67
|
-
|
|
68
|
-
terminal to answer in
|
|
66
|
+
This one command downloads the tool, installs the skills, and sets up the project. In a terminal
|
|
67
|
+
window it asks its setup questions there. Run it with `!` inside Claude Code (or any other place
|
|
68
|
+
without a terminal to answer in) and it installs the skills for the AI tools it finds, then tells
|
|
69
|
+
you to say **"run spec-setup"**, which asks the same questions in the chat. It never writes the
|
|
70
|
+
settings file from guesses.
|
|
69
71
|
|
|
70
72
|
> **Why not `npm install -g`?** npm doesn't let a package ask questions while it installs, and an
|
|
71
73
|
> install doesn't know which project it's for. That's why setup is a separate step you run inside
|
|
@@ -209,7 +211,7 @@ example, from inside `/Projects/frontend`:
|
|
|
209
211
|
Every question also has a flag, for scripts and CI:
|
|
210
212
|
`--layout=multi-repo --repos=~/Projects/frontend,~/Work/backend --workspace=~/Projects`,
|
|
211
213
|
`--layout=monorepo --packages=apps/web,packages/ui`, `--after-approval=run-through`,
|
|
212
|
-
`--project=new|existing`, `--tools=cursor,codex`, `--global`, `--yes` (accept everything detected), and `--version`.
|
|
214
|
+
`--project=new|existing`, `--share=all|memory|none`, `--specs-dir=<path>`, `--tools=cursor,codex`, `--global`, `--yes` (accept everything detected), and `--version`.
|
|
213
215
|
|
|
214
216
|
To change the rules later, edit the file or run setup again, then run `spec-driven-dev doctor`
|
|
215
217
|
to confirm the file still matches your repos. Setup never overwrites the file without
|
|
@@ -247,6 +249,25 @@ lines that point to files that no longer exist. If you install
|
|
|
247
249
|
[graphify](https://github.com/Graphify-Labs/graphify), the skills use it for cheaper code
|
|
248
250
|
navigation. It's optional (`context.graph: off` turns it off), and nothing is installed for you.
|
|
249
251
|
|
|
252
|
+
### Sharing specs with your team
|
|
253
|
+
|
|
254
|
+
Setup asks whether to commit the specs folder, and shows what your `.gitignore` does today:
|
|
255
|
+
|
|
256
|
+
| Choice | What's committed | Good for |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| **Share everything** (recommended) | plans, investigations, reviews and project memory | teams that review plans in PRs, and handovers |
|
|
259
|
+
| **Share project memory only** | `specs/CONTEXT.md` and `specs/context/` | sharing what the project knows, keeping per-ticket files local |
|
|
260
|
+
| **Keep everything local** | nothing (`specs/` is ignored) | trying it out, or personal use |
|
|
261
|
+
|
|
262
|
+
Setup changes `.gitignore` only after you agree, edits one marked block, and replaces only lines about
|
|
263
|
+
the specs folder. `risk.md` is always ignored. Change your mind any time with
|
|
264
|
+
`spec-driven-dev settings --share=all|memory|none`, or say "share the specs with the team" in your AI
|
|
265
|
+
tool. `doctor` warns if `.gitignore` drifts from your choice.
|
|
266
|
+
|
|
267
|
+
**Multi-repo:** the workspace folder usually isn't a git repo, so specs kept there stay on your
|
|
268
|
+
machine. To share them, choose a specs folder inside the repo your team treats as home, for example
|
|
269
|
+
`--specs-dir=frontend/specs`.
|
|
270
|
+
|
|
250
271
|
### GitRisk: which files have a history of breaking
|
|
251
272
|
|
|
252
273
|
`spec-driven-dev risk` scores every tracked code file from git history alone, with no model and no
|
|
@@ -566,6 +587,11 @@ confirm. To change it, edit `git.base_branch` / `git.branch_name` or run setup a
|
|
|
566
587
|
For a single repo or monorepo, yes: the whole team then follows the same rules. A multi-repo
|
|
567
588
|
config sits outside any repo, so it stays on your machine.
|
|
568
589
|
|
|
590
|
+
**Should I commit the specs?**
|
|
591
|
+
Setup asks. Sharing everything is recommended: teammates and reviewers see the plans and the
|
|
592
|
+
reasoning, and the project memory builds up for everyone. See
|
|
593
|
+
[Sharing specs with your team](#sharing-specs-with-your-team).
|
|
594
|
+
|
|
569
595
|
**Does it work with my stack?**
|
|
570
596
|
Yes. No skill assumes a language or framework. Each one finds the repo's own commands.
|
|
571
597
|
|
|
@@ -579,8 +605,9 @@ STP-123" or "ship it" picks the right skill.
|
|
|
579
605
|
Update to 1.5.1 or later (`npx @benjaminthomas/spec-driven-dev@latest --version`). Earlier versions
|
|
580
606
|
crashed on Node 14 or 16, which nvm often switches to inside older projects, and wrote a config
|
|
581
607
|
silently when there was no terminal. Now it runs on Node 14+. With no terminal to answer in (a `!`
|
|
582
|
-
command in Claude Code, an editor task, piped input), it
|
|
583
|
-
|
|
608
|
+
command in Claude Code, an editor task, piped input), it installs the skills and hands the questions
|
|
609
|
+
to your AI tool: say "run spec-setup" there. `spec-driven-dev setup` on its own still needs a
|
|
610
|
+
terminal, or flags such as `--layout=multi-repo --repos=…`.
|
|
584
611
|
|
|
585
612
|
**Setup didn't ask me about repos.**
|
|
586
613
|
It only asks about repos for a multi-repo or monorepo layout. If it detected `single`, type
|
|
@@ -644,7 +671,7 @@ spec-driven-dev --yes # accept every detected defaul
|
|
|
644
671
|
spec-driven-dev doctor # check .spec-driven.yml against the real repos
|
|
645
672
|
spec-driven-dev status # list unfinished specs and each one's next step
|
|
646
673
|
spec-driven-dev context --areas=billing # print the project memory (index, or chosen areas)
|
|
647
|
-
spec-driven-dev settings # switch GitRisk
|
|
674
|
+
spec-driven-dev settings # switch GitRisk, run-through and sharing from a menu
|
|
648
675
|
spec-driven-dev risk # GitRisk: score files from git history → specs/context/risk.md
|
|
649
676
|
spec-driven-dev risk-eval --tickets=tickets.md # compare agent behaviour with the risk signal on vs off
|
|
650
677
|
# (--runs=3 --arms=on --timeout=20 --model=sonnet)
|
|
@@ -741,7 +768,7 @@ skips that step rather than failing.
|
|
|
741
768
|
|
|
742
769
|
| Skill | Stage | SKILL.md lines | Words | Supporting files |
|
|
743
770
|
|---|---|---:|---:|---:|
|
|
744
|
-
| `spec-setup` | 0 · configure |
|
|
771
|
+
| `spec-setup` | 0 · configure | 179 | 1,747 | 3 |
|
|
745
772
|
| `spec-investigate` | 1 · understand | 373 | 3,211 | 1 |
|
|
746
773
|
| `spec-create` | 2 · plan | 198 | 1,782 | 6 |
|
|
747
774
|
| `spec-implement` | 3 · build | 327 | 2,753 | 3 |
|
|
@@ -750,7 +777,7 @@ skips that step rather than failing.
|
|
|
750
777
|
| `spec-checkpoint` | 6 · commit | 140 | 1,144 | 0 |
|
|
751
778
|
| `spec-ship` | 7 · PR | 230 | 1,388 | 0 |
|
|
752
779
|
| `generative-seo` | standalone | 255 | 2,209 | 7 |
|
|
753
|
-
| **Total** | | **
|
|
780
|
+
| **Total** | | **2,002** | **17,085** | **22** |
|
|
754
781
|
|
|
755
782
|
- **8** skills: **1** setup + **6** workflow stages + **1** standalone
|
|
756
783
|
- **4** tools supported natively (Claude Code, Codex CLI, Cursor, Antigravity), plus an MCP fallback
|
|
@@ -774,6 +801,7 @@ bin/spec-driven-dev.cjs launcher (runs on any Node, explains if it's too o
|
|
|
774
801
|
bin/cli.mjs the `spec-driven-dev` CLI: installer, setup menus, doctor, status, context, risk
|
|
775
802
|
bin/risk.mjs GitRisk scoring (churn, bug density, fan-out, security)
|
|
776
803
|
bin/risk-eval.mjs risk on/off experiment harness
|
|
804
|
+
bin/share.mjs what's committed from the specs folder (.gitignore block)
|
|
777
805
|
package.json npm package (ships bin/ and plugin/skills/)
|
|
778
806
|
mcp-server/ MCP fallback server
|
|
779
807
|
CHANGELOG.md what changed in each version
|
|
@@ -802,7 +830,7 @@ plugin/evals/ `claude plugin eval` cases
|
|
|
802
830
|
1. Increase `version` in both `package.json` and `plugin/.claude-plugin/plugin.json` (the check fails if
|
|
803
831
|
they differ).
|
|
804
832
|
2. Add a `CHANGELOG.md` entry, then commit and push.
|
|
805
|
-
3. Tag the release and push the tag: `git tag v1.
|
|
833
|
+
3. Tag the release and push the tag: `git tag v1.12.0 && git push origin v1.12.0`.
|
|
806
834
|
4. Watch the **publish** run in the repo's Actions tab. When it's green, the new version is on
|
|
807
835
|
npm. Claude Code plugin users get it from the marketplace once the commit is on `main`.
|
|
808
836
|
|
package/bin/cli.mjs
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
// spec-driven-dev status list specs with unfinished work and each one's "Resume here" next step
|
|
9
9
|
// spec-driven-dev context print specs/CONTEXT.md; --areas=billing,api prints only those area files
|
|
10
10
|
// spec-driven-dev risk GitRisk: score files from git history into specs/context/risk.md (--files=a,b --json --top=N --since=…)
|
|
11
|
-
// spec-driven-dev settings switch features
|
|
11
|
+
// spec-driven-dev settings switch features from a menu (GitRisk, run-through, sharing); or --risk=on|off --after-approval=… --share=all|memory|none
|
|
12
12
|
// spec-driven-dev risk-eval U8 experiment: run tickets with risk on vs off and compare agent behaviour (--tickets=<file> --runs=N --model=… --arms=on,off --timeout=20)
|
|
13
13
|
// flags: --yes --global --tools=claude,cursor --skills=spec-create,... --no-setup --dir=<path>
|
|
14
14
|
// --layout=single|monorepo|multi-repo --repos=~/Projects/frontend,/srv/backend --workspace=<config dir>
|
|
15
|
-
// --packages=apps/web,packages/ui --after-approval=stop|run-through --project=new|existing --risk=on|off --version
|
|
15
|
+
// --packages=apps/web,packages/ui --after-approval=stop|run-through --project=new|existing --risk=on|off --share=all|memory|none --specs-dir=<path> --version
|
|
16
16
|
import fs from "node:fs";
|
|
17
17
|
import os from "node:os";
|
|
18
18
|
import path from "node:path";
|
|
@@ -20,6 +20,7 @@ import { execFileSync } from "node:child_process";
|
|
|
20
20
|
import prompts from "prompts"; // arrow-key menus that work in PowerShell, cmd, macOS and Linux terminals
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
import { scoreRepo, riskMarkdown, normWeights, weightProblems, setConfigValue } from "./risk.mjs";
|
|
23
|
+
import { SHARE_MODES, shareState, planShare, shareTitle } from "./share.mjs";
|
|
23
24
|
|
|
24
25
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
25
26
|
const SKILLS_DIR = path.join(ROOT, "plugin", "skills");
|
|
@@ -47,14 +48,20 @@ if (flag("version") || flag("v")) {
|
|
|
47
48
|
}
|
|
48
49
|
// No terminal to answer in (e.g. `!` in Claude Code, an IDE task, a pipe) and nothing passed explicitly:
|
|
49
50
|
// stop rather than write a config from guesses.
|
|
50
|
-
const ANSWER_FLAGS = ["risk", "yes", "y", "layout", "repos", "packages", "tools", "workspace", "after-approval", "skills", "global", "no-setup", "project"];
|
|
51
|
-
|
|
51
|
+
const ANSWER_FLAGS = ["share", "specs-dir", "risk", "yes", "y", "layout", "repos", "packages", "tools", "workspace", "after-approval", "skills", "global", "no-setup", "project"];
|
|
52
|
+
const NO_TTY = !process.stdin.isTTY && !args.some((a) => ANSWER_FLAGS.some((f) => a === `--${f}` || a.startsWith(`--${f}=`)));
|
|
53
|
+
// Installing the skills needs no answers, so the default command still does that without a terminal
|
|
54
|
+
// (e.g. `!` in Claude Code) and hands the questions to the spec-setup skill, which asks them in the chat.
|
|
55
|
+
const HANDOFF = command === "install" && NO_TTY;
|
|
56
|
+
if (["setup", "settings"].includes(command) && NO_TTY) {
|
|
52
57
|
console.log(`✗ No interactive terminal, so ${command === "settings" ? "the settings menu can't open" : "setup can't ask its questions"}. Nothing was changed.
|
|
53
58
|
|
|
54
59
|
This happens when it runs through another program: a "!" command in Claude Code, an editor task,
|
|
55
60
|
or piped input. Either:
|
|
56
61
|
• run it yourself in a terminal window (Terminal, iTerm, or your editor's terminal panel):
|
|
57
|
-
npx @benjaminthomas/spec-driven-dev@latest ${command === "
|
|
62
|
+
npx @benjaminthomas/spec-driven-dev@latest ${command}${command === "setup" ? `
|
|
63
|
+
• or, in your AI tool (Claude Code, Cursor, Codex), say "run spec-setup": it asks the same
|
|
64
|
+
questions in the chat` : ""}
|
|
58
65
|
• or give the answers as flags, for example:
|
|
59
66
|
npx @benjaminthomas/spec-driven-dev@latest ${command === "settings" ? "settings --risk=off" : "setup --layout=multi-repo --repos=~/Projects/frontend,~/Projects/backend"}
|
|
60
67
|
(add --yes to accept everything it detects)`);
|
|
@@ -137,7 +144,8 @@ async function install() {
|
|
|
137
144
|
}
|
|
138
145
|
console.log(`✓ ${skills.length} skills → ${dest}`);
|
|
139
146
|
}
|
|
140
|
-
if (!dests.length) console.log("• No tools selected — nothing installed.");
|
|
147
|
+
if (!dests.length) console.log(HANDOFF ? "• No AI coding tools found (Claude Code, Cursor, Codex or Antigravity). Install one, or pass --tools=claude,cursor." : "• No tools selected — nothing installed.");
|
|
148
|
+
return dests.length;
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
// ---------- discovery ----------
|
|
@@ -245,7 +253,7 @@ function toYaml(c) {
|
|
|
245
253
|
.map(([tool, m]) => ` ${tool}:\n` + MODEL_KEYS.map((t) => ` ${t}: ${m[t]}\n`).join("")).join("");
|
|
246
254
|
out += "effort: # effort / reasoning level per tier (Claude Code, Codex): low | medium | high | xhigh | max | default\n"
|
|
247
255
|
+ MODEL_KEYS.map((t) => ` ${t}: ${EFFORT_DEFAULTS[t]}\n`).join("");
|
|
248
|
-
out += `context: # project memory: {specs_dir}/CONTEXT.md (index) + context/<area>.md\n project: ${c.project || "existing"} # existing | new\n graph: auto # auto = use graphify if installed | off\n`;
|
|
256
|
+
out += `context: # project memory: {specs_dir}/CONTEXT.md (index) + context/<area>.md\n project: ${c.project || "existing"} # existing | new\n graph: auto # auto = use graphify if installed | off\n share: ${c.share || "all"} # all | memory | none: what's committed from the specs folder\n`;
|
|
249
257
|
out += "risk: # GitRisk: historical risk per file from git (spec-driven-dev risk). Weights are a hypothesis to calibrate\n enabled: on # on | off (off = skills ignore risk; risk-eval compares the two)\n since: \"2 years ago\"\n weights:\n churn: 0.30\n bug_density: 0.35\n fan_out: 0.20\n security: 0.15\n";
|
|
250
258
|
out += `pipeline:\n after_approval: ${c.afterApproval || "stop"} # stop | run-through (implement → verify → review → checkpoint; ship always asks)\n`;
|
|
251
259
|
return out + `specs_dir: ${y(c.specsDir)}\n`;
|
|
@@ -453,7 +461,8 @@ async function setup() {
|
|
|
453
461
|
{ title: `${ex}-123: add login${det("ticket", g.commitStyle)}`, value: "ticket", description: "ticket ID first" },
|
|
454
462
|
{ title: `Add login${det("freeform", g.commitStyle)}`, value: "freeform", description: "no fixed format" },
|
|
455
463
|
], g.commitStyle);
|
|
456
|
-
const specsDir = await
|
|
464
|
+
const specsDir = await chooseSpecsDir(layout, root, repos);
|
|
465
|
+
const share = await chooseShare(root, specsDir);
|
|
457
466
|
const afterApproval = opt("after-approval") || await askSelect("After you approve a spec…", [
|
|
458
467
|
{ title: "Stop after each stage", value: "stop", description: "you start implement, verify and checkpoint yourself" },
|
|
459
468
|
{ title: "Run straight through", value: "run-through", description: "implement → verify → review → commit in one go; stops at any failure or review blocker, never pushes without asking" },
|
|
@@ -466,7 +475,7 @@ async function setup() {
|
|
|
466
475
|
], pd.kind);
|
|
467
476
|
|
|
468
477
|
const config = {
|
|
469
|
-
project, layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "", afterApproval,
|
|
478
|
+
project, share: share || "none", layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "", afterApproval,
|
|
470
479
|
git: { remote: "origin", base_branch: base, pr_target: pr, branch_name: branchName, commit_style: commitStyle },
|
|
471
480
|
commands: layout === "single" ? commandsFor(repoDirs[0]) : commandsFor(root),
|
|
472
481
|
repos: repos.map((r) => {
|
|
@@ -478,6 +487,7 @@ async function setup() {
|
|
|
478
487
|
fs.writeFileSync(file, toYaml(config));
|
|
479
488
|
if (layout === "multi-repo") register(real(file), repos.map((r) => real(path.resolve(root, r))));
|
|
480
489
|
console.log(`✓ Wrote ${file}`);
|
|
490
|
+
if (share) await applyShare(root, specsDir, share, { quietIfSame: true });
|
|
481
491
|
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.");
|
|
482
492
|
console.log(project === "existing"
|
|
483
493
|
? ` Next: ask your AI tool to "run spec-setup". It audits the ${layout === "multi-repo" ? "repos" : "repo"}, asks a few questions, and saves ${specsDir}/CONTEXT.md.`
|
|
@@ -591,8 +601,16 @@ function doctor() {
|
|
|
591
601
|
if (!fs.existsSync(specs)) notes.push(`${c.specs_dir || "specs"}/ doesn't exist yet — created on first spec`);
|
|
592
602
|
if (c.context?.project && !["new", "existing"].includes(c.context.project)) problems.push(`context.project "${c.context.project}" must be new or existing`);
|
|
593
603
|
if (c.context?.graph && !["auto", "off"].includes(c.context.graph)) problems.push(`context.graph "${c.context.graph}" must be auto or off`);
|
|
594
|
-
|
|
604
|
+
// Multi-repo context names paths from the workspace folder (soreto-shopify/app/) or from inside a repo (app/).
|
|
605
|
+
const repoRoots = c.layout === "multi-repo" ? [root, ...(c.repos || []).map((r) => path.resolve(root, expandHome(r.path)))] : [root];
|
|
595
606
|
checkContext(specs, c.specs_dir || "specs", repoRoots, problems, notes);
|
|
607
|
+
if (c.context?.share) {
|
|
608
|
+
if (!SHARE_MODES.includes(c.context.share)) problems.push(`context.share "${c.context.share}" must be all, memory or none`);
|
|
609
|
+
const g = specsInGit(root, c.specs_dir || "specs");
|
|
610
|
+
if (!g && c.context.share !== "none") notes.push(`context.share is ${c.context.share}, but ${c.specs_dir || "specs"} isn't inside a git repo, so nothing can be shared`);
|
|
611
|
+
else if (g && SHARE_MODES.includes(c.context.share) && shareState(g.top, g.rel) !== c.context.share)
|
|
612
|
+
notes.push(`context.share is ${c.context.share}, but .gitignore makes it "${shareTitle[shareState(g.top, g.rel)].toLowerCase()}". Fix: spec-driven-dev settings --share=${c.context.share}`);
|
|
613
|
+
}
|
|
596
614
|
const rmd = path.join(specs, "context", "risk.md"), rtop = run("git", ["rev-parse", "--show-toplevel"], root);
|
|
597
615
|
if (rtop && fs.existsSync(rmd) && run("git", ["ls-files", path.relative(path.resolve(rtop), real(rmd))], rtop))
|
|
598
616
|
problems.push(`${c.specs_dir || "specs"}/context/risk.md is committed to git. It's regenerated on every run, so it causes merge conflicts. Run: git rm --cached ${path.relative(path.resolve(rtop), real(rmd))} (spec-driven-dev risk adds it to .gitignore)`);
|
|
@@ -644,6 +662,58 @@ function context() {
|
|
|
644
662
|
}
|
|
645
663
|
}
|
|
646
664
|
|
|
665
|
+
// ---------- sharing specs through git ----------
|
|
666
|
+
function specsInGit(root, specsDir) {
|
|
667
|
+
const abs = path.resolve(root, specsDir);
|
|
668
|
+
let probe = abs;
|
|
669
|
+
while (!fs.existsSync(probe)) probe = path.dirname(probe);
|
|
670
|
+
const top = run("git", ["rev-parse", "--show-toplevel"], probe);
|
|
671
|
+
if (!top) return null;
|
|
672
|
+
const rel = path.relative(path.resolve(top), real(probe)).split(path.sep).concat(path.relative(probe, abs).split(path.sep)).filter((x) => x && x !== ".").join("/");
|
|
673
|
+
return rel.startsWith("..") ? null : { top, rel };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function chooseSpecsDir(layout, root, repos) {
|
|
677
|
+
if (opt("specs-dir")) return opt("specs-dir");
|
|
678
|
+
if (layout !== "multi-repo" || isRepo(root)) return askText("Folder for specs (one sub-folder per feature)", "specs");
|
|
679
|
+
// The workspace folder isn't a repo, so specs kept there can never be shared through git.
|
|
680
|
+
return askSelect("Where should specs and project memory live?", [
|
|
681
|
+
{ title: `In this workspace folder (${path.join(root, "specs")})`, value: "specs", description: "on this machine only; it isn't a git repo, so it can't be shared" },
|
|
682
|
+
...repos.map((r) => ({ title: `Inside ${r}/specs`, value: `${r}/specs`.replace(/^\.\//, ""), description: "can be shared with the team through that repo" })),
|
|
683
|
+
], "specs");
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function chooseShare(root, specsDir) {
|
|
687
|
+
const g = specsInGit(root, specsDir);
|
|
688
|
+
if (opt("share") && !SHARE_MODES.includes(opt("share"))) throw new Error("--share must be all, memory or none");
|
|
689
|
+
if (!g) { if (opt("share") && opt("share") !== "none") console.log(` • ${specsDir} isn't inside a git repo, so it can't be shared. Choose a folder inside a repo (--specs-dir) to share it.`); return null; }
|
|
690
|
+
const now = shareState(g.top, g.rel);
|
|
691
|
+
if (now !== "all") console.log(` • Your .gitignore currently makes ${g.rel}: ${shareTitle[now].toLowerCase()}.`);
|
|
692
|
+
return opt("share") || askSelect("Share specs and project memory with your team?", [
|
|
693
|
+
{ title: `Share everything${now === "all" ? " (current)" : ""} (recommended)`, value: "all", description: "commit plans, investigations, reviews and project memory; teammates and PR reviewers see the reasoning" },
|
|
694
|
+
{ title: `Share project memory only${now === "memory" ? " (current)" : ""}`, value: "memory", description: "commit CONTEXT.md and context/; keep each feature's working files local" },
|
|
695
|
+
{ title: `Keep everything local${now === "none" ? " (current)" : ""}`, value: "none", description: "ignore the specs folder; nothing is shared" },
|
|
696
|
+
], now === "custom" ? "all" : now);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function applyShare(root, specsDir, mode, { quietIfSame } = {}) {
|
|
700
|
+
const g = specsInGit(root, specsDir);
|
|
701
|
+
if (!g) { console.log(` • ${specsDir} isn't inside a git repo, so there's nothing to share or ignore.`); return; }
|
|
702
|
+
if (quietIfSame && shareState(g.top, g.rel) === mode && mode !== "all") return;
|
|
703
|
+
const gi = path.join(g.top, ".gitignore");
|
|
704
|
+
const before = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
|
|
705
|
+
const plan = planShare(before, g.rel, mode);
|
|
706
|
+
if (plan.text === before) return;
|
|
707
|
+
if (plan.removed.length && !opt("share") && !(await askConfirm(`To ${shareTitle[mode].toLowerCase()}, these .gitignore lines would be replaced: ${plan.removed.join(", ")}. Go ahead?`, true))) {
|
|
708
|
+
console.log(" • Left .gitignore as it was. Change it later with: spec-driven-dev settings");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
fs.writeFileSync(gi, plan.text);
|
|
712
|
+
console.log(` ✓ .gitignore updated: ${shareTitle[mode].toLowerCase()}${plan.removed.length ? ` (replaced: ${plan.removed.join(", ")})` : ""}.`);
|
|
713
|
+
const now = shareState(g.top, g.rel);
|
|
714
|
+
if (now !== mode) console.log(` • Another ignore rule (a nested .gitignore or your global git excludes) still makes it "${shareTitle[now].toLowerCase()}". Check: git check-ignore -v ${g.rel}/CONTEXT.md`);
|
|
715
|
+
}
|
|
716
|
+
|
|
647
717
|
// risk.md is regenerated on every run (it carries a date), so committing it means noisy diffs and merge
|
|
648
718
|
// conflicts between branches. Keep it out of git like a build output; anyone can recreate it in a second.
|
|
649
719
|
function ignoreGenerated(file) {
|
|
@@ -725,6 +795,11 @@ const SETTINGS = [
|
|
|
725
795
|
get: (c) => c.pipeline?.after_approval || "stop", options: [
|
|
726
796
|
{ value: "stop", title: "Stop after each stage", description: "you start implement, verify, review and commit yourself" },
|
|
727
797
|
{ value: "run-through", title: "Run straight through", description: "implement → verify → review → commit; stops at any failure, never pushes" }] },
|
|
798
|
+
{ block: "context", key: "share", flag: "share", name: "Share specs with the team", about: "what's committed from the specs folder",
|
|
799
|
+
get: (c) => c.context?.share || "all", after: (file, c, v) => applyShare(path.dirname(file), c.specs_dir || "specs", v), options: [
|
|
800
|
+
{ value: "all", title: "Share everything", description: "commit plans, investigations, reviews and project memory" },
|
|
801
|
+
{ value: "memory", title: "Share project memory only", description: "commit CONTEXT.md and context/; keep feature files local" },
|
|
802
|
+
{ value: "none", title: "Keep everything local", description: "ignore the specs folder" }] },
|
|
728
803
|
];
|
|
729
804
|
|
|
730
805
|
async function settings() {
|
|
@@ -732,14 +807,15 @@ async function settings() {
|
|
|
732
807
|
if (!file) throw new Error(`No .spec-driven.yml in ${DIR} or above. Run: spec-driven-dev setup`);
|
|
733
808
|
let text = fs.readFileSync(file, "utf8");
|
|
734
809
|
const label = (s, v) => s.options.find((o) => o.value === v)?.title || v;
|
|
735
|
-
const save = (s, v) => {
|
|
810
|
+
const save = async (s, v) => {
|
|
736
811
|
if (!s.options.some((o) => o.value === v)) throw new Error(`--${s.flag} must be ${s.options.map((o) => o.value).join(" or ")}`);
|
|
737
812
|
text = setConfigValue(text, s.block, s.key, v);
|
|
738
813
|
fs.writeFileSync(file, text);
|
|
739
814
|
console.log(`✓ ${s.name}: ${label(s, v)} (saved in ${file})`);
|
|
815
|
+
if (s.after) await s.after(file, parseYaml(text), v);
|
|
740
816
|
};
|
|
741
817
|
const given = SETTINGS.filter((s) => opt(s.flag));
|
|
742
|
-
if (given.length)
|
|
818
|
+
if (given.length) for (const s of given) await save(s, opt(s.flag));
|
|
743
819
|
else for (;;) {
|
|
744
820
|
const c = parseYaml(text);
|
|
745
821
|
const pick = await askSelect("Settings: pick one to change", [
|
|
@@ -747,7 +823,7 @@ async function settings() {
|
|
|
747
823
|
{ title: "Done", value: null }], null);
|
|
748
824
|
if (!pick) break;
|
|
749
825
|
const v = await askSelect(`${pick.name}`, pick.options, pick.get(c));
|
|
750
|
-
if (v !== pick.get(c)) save(pick, v);
|
|
826
|
+
if (v !== pick.get(c)) await save(pick, v);
|
|
751
827
|
}
|
|
752
828
|
const c = parseYaml(text);
|
|
753
829
|
if (c.risk?.enabled !== "off" && !fs.existsSync(path.join(path.dirname(file), c.specs_dir || "specs", "context", "risk.md"))) {
|
|
@@ -788,12 +864,20 @@ try {
|
|
|
788
864
|
else if (command === "risk") risk();
|
|
789
865
|
else if (command === "settings") await settings();
|
|
790
866
|
else if (command === "risk-eval") await riskEvalCmd();
|
|
791
|
-
else if (command === "install") {
|
|
867
|
+
else if (command === "install" && HANDOFF) {
|
|
868
|
+
console.log("No interactive terminal (for example a \"!\" command in Claude Code), so this installs the skills\nwith the detected defaults and leaves the setup questions to your AI tool.\n");
|
|
869
|
+
const n = await install();
|
|
870
|
+
if (n) console.log(`
|
|
871
|
+
Next: in your AI tool, say "run spec-setup". It asks the setup questions in the chat
|
|
872
|
+
(single repo, monorepo or multi-repo, tracker, branches) and writes .spec-driven.yml.
|
|
873
|
+
If your AI tool doesn't recognise it yet, start a new session so it loads the new skills.
|
|
874
|
+
To answer in a terminal instead, open Terminal or iTerm and run: npx @benjaminthomas/spec-driven-dev setup`);
|
|
875
|
+
} else if (command === "install") {
|
|
792
876
|
await install();
|
|
793
877
|
if (!flag("no-setup") && (await askConfirm("Set up this project now?", true))) await setup();
|
|
794
878
|
} else if (command === "setup") await setup();
|
|
795
879
|
else throw new Error(`Unknown command "${command}" — use install (default), setup, settings, doctor, status, context, risk, or risk-eval`);
|
|
796
|
-
if (["install", "setup"].includes(command)) console.log('\nNext: in your AI tool, say "investigate <ticket>" or "create a spec".');
|
|
880
|
+
if (["install", "setup"].includes(command) && !HANDOFF) console.log('\nNext: in your AI tool, say "investigate <ticket>" or "create a spec".');
|
|
797
881
|
} catch (e) {
|
|
798
882
|
console.error(`✗ ${e.message}`);
|
|
799
883
|
process.exitCode = 1;
|
package/bin/share.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Whether specs and project memory are shared through git: all | memory | none.
|
|
2
|
+
// We manage one marked block in .gitignore and only remove lines that are plainly about the specs folder.
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export const SHARE_MODES = ["all", "memory", "none"];
|
|
6
|
+
const BEGIN = "# spec-driven-dev: what's shared from the specs folder (spec-driven-dev settings --share=…)";
|
|
7
|
+
const END = "# end spec-driven-dev";
|
|
8
|
+
|
|
9
|
+
const git = (cwd, args) => {
|
|
10
|
+
try { return execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim(); } catch { return ""; }
|
|
11
|
+
};
|
|
12
|
+
const ignored = (top, rel) => !!git(top, ["check-ignore", "--no-index", rel]);
|
|
13
|
+
|
|
14
|
+
// What git does today: is the project memory ignored, and is a feature's working folder ignored?
|
|
15
|
+
export function shareState(top, specs) {
|
|
16
|
+
const memory = !ignored(top, `${specs}/CONTEXT.md`) && !ignored(top, `${specs}/context/notes.md`);
|
|
17
|
+
const features = !ignored(top, `${specs}/some-feature/README.md`);
|
|
18
|
+
return features && memory ? "all" : memory ? "memory" : features ? "custom" : "none";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function blockFor(specs, mode) {
|
|
22
|
+
const lines = mode === "none" ? [`${specs}/`]
|
|
23
|
+
: mode === "memory" ? [`${specs}/*`, `!${specs}/CONTEXT.md`, `!${specs}/context/`, `${specs}/context/risk.md`]
|
|
24
|
+
: [`${specs}/context/risk.md`];
|
|
25
|
+
return [BEGIN, ...lines, END];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// New .gitignore text for a mode, plus which existing lines would go, so the caller can ask first.
|
|
29
|
+
export function planShare(text, specs, mode) {
|
|
30
|
+
const s = specs.replace(/^\/+|\/+$/g, "");
|
|
31
|
+
const about = new Set([s, `${s}/`, `/${s}`, `/${s}/`, `${s}/*`, `/${s}/*`, `${s}/**`, `!${s}/CONTEXT.md`, `!${s}/context/`, `${s}/context/risk.md`, `/${s}/context/risk.md`,
|
|
32
|
+
"# GitRisk scores, regenerated by spec-driven-dev risk"]);
|
|
33
|
+
const kept = [], removed = [];
|
|
34
|
+
let inBlock = false;
|
|
35
|
+
for (const line of text.split("\n")) {
|
|
36
|
+
if (line === BEGIN) { inBlock = true; continue; }
|
|
37
|
+
if (inBlock) { if (line === END) inBlock = false; continue; }
|
|
38
|
+
if (about.has(line.trim())) { if (!line.startsWith("# GitRisk") && !line.trim().endsWith("risk.md")) removed.push(line.trim()); continue; }
|
|
39
|
+
kept.push(line);
|
|
40
|
+
}
|
|
41
|
+
while (kept.length && kept[kept.length - 1] === "") kept.pop();
|
|
42
|
+
const body = kept.join("\n");
|
|
43
|
+
return { text: `${body}${body ? "\n\n" : ""}${blockFor(s, mode).join("\n")}\n`, removed };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const shareTitle = { all: "Share everything", memory: "Share project memory only", none: "Keep everything local", custom: "Partly shared (custom rules)" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@benjaminthomas/spec-driven-dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Spec-driven AI development workflow (investigate → spec → implement → verify → review → ship) as Agent Skills for Claude Code, Cursor, Codex CLI and Antigravity.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ticket-to-pr",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Parallel-batch spec-driven feature workflow (spec-setup, spec-investigate, spec-create, spec-implement, spec-verify, spec-review, spec-checkpoint, spec-ship) plus a portable SEO/GEO operations skill (generative-seo) for any codebase.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Benjamin Thomas",
|
|
@@ -82,6 +82,18 @@ Read, don't guess. Collect a draft of every field in `references/config-template
|
|
|
82
82
|
pushing. Store it as `pipeline.after_approval`.
|
|
83
83
|
8. **New or existing project.** Detect it (commit and file counts) and confirm in one line. Store
|
|
84
84
|
it as `context.project`. This decides what Step 4 does.
|
|
85
|
+
9. **Sharing with the team.** Find out what git does with the specs folder today:
|
|
86
|
+
`git check-ignore -v {specs_dir}/CONTEXT.md` and `git check-ignore -v {specs_dir}/x/README.md`.
|
|
87
|
+
Then ask once, saying what's true now (e.g. "your .gitignore ignores `specs/`, so nothing is
|
|
88
|
+
shared today"):
|
|
89
|
+
- **Share everything (recommended):** commit plans, investigations, reviews and project memory,
|
|
90
|
+
so teammates and PR reviewers see the reasoning and the memory grows for everyone.
|
|
91
|
+
- **Share project memory only:** commit `CONTEXT.md` and `context/`; feature files stay local.
|
|
92
|
+
- **Keep everything local:** ignore the specs folder.
|
|
93
|
+
|
|
94
|
+
**Multi-repo:** if the config folder isn't a git repo, specs kept there can't be shared. Say so
|
|
95
|
+
and offer `specs_dir: <repo>/specs` inside the repo the team treats as home.
|
|
96
|
+
Store the answer as `context.share`.
|
|
85
97
|
|
|
86
98
|
## Step 2 — Confirm in one round
|
|
87
99
|
|
|
@@ -118,15 +130,19 @@ Skip if `{specs_dir}/CONTEXT.md` already exists (offer a refresh instead, see `r
|
|
|
118
130
|
intended stack and conventions for a new one. One round of questions, no more.
|
|
119
131
|
2. Write `{specs_dir}/CONTEXT.md` (and any area files) using `references/context-template.md`.
|
|
120
132
|
Keep the index to 60 lines and area files to 150.
|
|
121
|
-
3. **
|
|
133
|
+
3. **Apply the sharing choice.** Show the exact `.gitignore` lines that will be replaced or added,
|
|
134
|
+
and change it only after a yes, by running `spec-driven-dev settings --share=<all|memory|none>`
|
|
135
|
+
(it manages one marked block and only replaces lines about the specs folder). Never rewrite
|
|
136
|
+
other `.gitignore` lines.
|
|
137
|
+
4. **Risk scores.** For an existing project, run `spec-driven-dev risk` (or
|
|
122
138
|
`npx -y @benjaminthomas/spec-driven-dev risk`) to write `{specs_dir}/context/risk.md`. It uses
|
|
123
139
|
git only, no model, and adds `risk.md` to `.gitignore` because it's regenerated on every run. Skip it for a new project with no history. Mention the hotspot count in the
|
|
124
140
|
report. Set `risk.enabled: off` only if the user doesn't want it.
|
|
125
|
-
|
|
141
|
+
5. **Offer, don't do:** ask whether to add this one line to `CLAUDE.md` / `AGENTS.md` (whichever
|
|
126
142
|
the project uses), so hosts reload the pointer after compaction and in every new session:
|
|
127
143
|
`For spec work, read specs/CONTEXT.md first, then the "Resume here" block of any unfinished spec.`
|
|
128
144
|
Add it only on a yes.
|
|
129
|
-
|
|
145
|
+
6. Re-run `npx -y @benjaminthomas/spec-driven-dev doctor` if available. It checks the size limits
|
|
130
146
|
and flags context lines that point to files which no longer exist.
|
|
131
147
|
|
|
132
148
|
## Quick switches (plain words, no YAML)
|
|
@@ -140,6 +156,7 @@ keep every other line and comment, and confirm in one sentence. Don't re-run dis
|
|
|
140
156
|
| "turn GitRisk on", "use the risk scores" | `risk.enabled: on`, then run `spec-driven-dev risk` if `{specs_dir}/context/risk.md` doesn't exist yet |
|
|
141
157
|
| "run straight through after I approve", "fewer stops" | `pipeline.after_approval: run-through` |
|
|
142
158
|
| "stop after each stage" | `pipeline.after_approval: stop` |
|
|
159
|
+
| "share the specs with the team", "keep specs local", "share only the project memory" | `context.share: all / none / memory`: show the `.gitignore` change, then run `spec-driven-dev settings --share=…` on a yes |
|
|
143
160
|
|
|
144
161
|
Reply with what changed and what it means, for example: "GitRisk is now off. Investigations, plans
|
|
145
162
|
and reviews will ignore risk scores until you turn it back on." People who prefer a menu can run
|
|
@@ -97,5 +97,8 @@ pipeline:
|
|
|
97
97
|
context:
|
|
98
98
|
project: existing # existing (audited) | new (intent recorded) — set by setup
|
|
99
99
|
graph: auto # auto = use graphify if installed or graphify-out/ exists | off
|
|
100
|
+
share: all # all = commit specs + project memory (recommended) | memory = commit only
|
|
101
|
+
# CONTEXT.md + context/ | none = ignore the specs folder. risk.md is always ignored.
|
|
102
|
+
# Change with: spec-driven-dev settings --share=…
|
|
100
103
|
|
|
101
104
|
specs_dir: specs # where specs/<feature>/ folders go (relative to this file)
|