@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. It asks its
67
- questions in your terminal, so run it in a real terminal, not through another tool. If there's no
68
- terminal to answer in, it says so and uses the detected values.
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 stops, changes nothing, and tells you to run
583
- it in a terminal window or pass flags such as `--layout=multi-repo --repos=…`.
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 and run-through on/off from a menu
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 | 162 | 1,545 | 3 |
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** | | **1,985** | **16,883** | **22** |
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.11.1 && git push origin v1.11.1`.
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 on or off from a menu (GitRisk, run-through); or --risk=on|off --after-approval=…
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
- if (["install", "setup", "settings"].includes(command) && !process.stdin.isTTY && !args.some((a) => ANSWER_FLAGS.some((f) => a === `--${f}` || a.startsWith(`--${f}=`)))) {
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 === "install" ? "" : 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 askText("Folder for specs (one sub-folder per feature)", "specs");
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
- const repoRoots = c.layout === "multi-repo" ? (c.repos || []).map((r) => path.resolve(root, expandHome(r.path))) : [root];
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) given.forEach((s) => save(s, opt(s.flag)));
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.11.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.11.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. **Risk scores.** For an existing project, run `spec-driven-dev risk` (or
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
- 4. **Offer, don't do:** ask whether to add this one line to `CLAUDE.md` / `AGENTS.md` (whichever
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
- 5. Re-run `npx -y @benjaminthomas/spec-driven-dev doctor` if available. It checks the size limits
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)