@benjaminthomas/spec-driven-dev 1.11.2 → 1.13.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,26 +63,21 @@ 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. 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.
71
-
72
- > **Why not `npm install -g`?** npm doesn't let a package ask questions while it installs, and an
73
- > install doesn't know which project it's for. That's why setup is a separate step you run inside
74
- > the project. If you'll use the `doctor`/`status` commands often, you can also install it
75
- > globally: `npm install -g @benjaminthomas/spec-driven-dev`, then run `spec-driven-dev` in place of
76
- > the `npx …` command.
77
-
78
- It:
79
- 1. **Finds your AI tools** (Claude Code, Cursor, Codex CLI, Antigravity) and installs the skills
80
- into each one you choose, for this project or for all projects.
81
- 2. **Sets up your project** by detecting its layout, issue tracker, branch conventions, and
82
- commands, then asking you to confirm. The answers go into `.spec-driven.yml`
83
- ([details below](#project-setup-your-teams-rules)).
84
-
85
- **3. In your AI tool, run each stage in order.** You can type the slash command or just say the
66
+ It installs the skills into the AI tools you pick (Claude Code, Cursor, Codex CLI, Antigravity),
67
+ for this project or all your projects. Each step has its own colour so it's easy to follow. It also
68
+ works with `!` inside Claude Code: it then installs for the tools it finds.
69
+
70
+ > **Why not `npm install -g`?** You can: `npm install -g @benjaminthomas/spec-driven-dev`, then run
71
+ > `spec-driven-dev` in place of the `npx …` command. Handy if you use `doctor`/`status` often.
72
+
73
+ **2. Say "run spec-setup" in your AI tool.** Setup is a conversation, so the agent can read the code
74
+ and you can change any answer as you go. It asks how the project is organised (one repo, a monorepo,
75
+ or several repos that change together), your tracker, branches and commit style, where specs live
76
+ and whether your team shares them, then looks through the code and saves the project memory
77
+ ([details below](#project-setup-your-teams-rules)). If Claude Code or Codex is installed, the
78
+ installer offers to open it with "run spec-setup" already typed in.
79
+
80
+ **3. Then run each stage in order.** You can type the slash command or just say the
86
81
  words in quotes.
87
82
 
88
83
  ```
@@ -145,36 +140,24 @@ others `fix/STP-123-short-name`. Some have one repo, others have six. None of th
145
140
  skills. Each project describes its own rules in **`.spec-driven.yml`**, and every skill reads that
146
141
  file before doing anything.
147
142
 
148
- **How the file gets created:** `spec-driven-dev` (or `spec-driven-dev setup` to run only this
149
- step) detects the answers from your repos and asks you to confirm them. In your AI tool you can also
150
- say **"run spec-setup"**: that does a deeper version that also reads `CLAUDE.md`/`AGENTS.md`, CI
151
- config, and recent PRs, and fills in what the installer can't (such as each repo's role, or commands
152
- for stacks other than Node). If a skill finds no config, it runs this setup first.
153
-
154
- **What `spec-driven-dev` asks, in order.** Each question is a menu: move with **↑/↓**, tick with
155
- **Space**, and confirm with **Enter**. What it detected is pre-selected and marked *(detected)*, so you
156
- can usually just press Enter. Repo paths are added one at a time: paste a path (quotes, `~`, and
157
- Windows paths like `C:\work\api` all work). A path that doesn't exist or isn't a git repo is flagged
158
- straight away, so you can fix it before moving on. Press Enter on an empty line when you're done.
159
- **Ctrl+C** cancels without changing anything.
143
+ **How the file gets created:** in your AI tool, say **"run spec-setup"**. It detects what it can
144
+ (layout, tracker, branches, commit style per repo, commands, written rules in `CLAUDE.md`/`AGENTS.md`,
145
+ CI config, recent PR targets), shows you a draft marked *detected* or *guess*, and asks only about
146
+ the guesses, in one round. You can correct anything in plain words ("actually it's Jira, key STP")
147
+ before it writes the file. If any skill finds no config, it runs this setup first.
160
148
 
161
- | Question | Detected from | Example answer |
149
+ | What it asks | Detected from | Example answer |
162
150
  |---|---|---|
163
- | Which AI tools get the skills | tool folders in your home directory and commands on your `PATH` | Claude Code, Cursor |
164
- | This project only, or all projects | (you choose) | `project` |
165
- | Project layout | git repos and workspace files (`pnpm-workspace.yaml`, `turbo.json`, `nx.json`, `go.work`, …) | `single`, `monorepo`, or `multi-repo` |
166
- | *multi-repo:* which repos nearby belong | git repos up to two folders down, grouped by GitHub owner | `frontend` |
167
- | *multi-repo:* repos in other places | (you type paths: absolute, `~/…`, or relative) | `~/Work/backend, /srv/database` |
168
- | *multi-repo:* where the config lives | the repos' shared parent folder, or `~/.spec-driven/<name>/` | `~/Projects` |
169
- | *monorepo:* package folders | the workspaces your `package.json`/`pnpm-workspace.yaml` declares, or `apps/*`, `packages/*` | `apps/web, packages/ui` |
151
+ | How the project is organised | git repos and workspace files (`pnpm-workspace.yaml`, `turbo.json`, `nx.json`, `go.work`, …). A Shopify app's `extensions/` count as one app | `single`, `monorepo`, or `multi-repo` |
152
+ | *multi-repo:* which repos you work on | git repos nearby, grouped by GitHub owner, plus the team's `team-repos.yml` | `soreto-shopify, reverb-backend` |
153
+ | *multi-repo:* where specs live | (you choose) | a dedicated specs repo, a code repo, or this machine only |
170
154
  | Issue tracker | ticket keys in branch names and commits | Jira, key `STP` |
171
- | Base branch / PR target | default branch, `staging`/`develop` branches | `staging` |
155
+ | Base branch / PR target | default branch, `staging`/`develop`, recent PR targets | `staging` |
172
156
  | Branch name pattern | your recent branch names | `{ticket}` → `STP-123` |
173
- | Commit style | your recent commit messages | `conventional` (`feat: …`) or `ticket` (`STP-123: …`) |
174
- | Lint / typecheck / test / build / dev commands | `package.json` scripts and lockfile (per repo or package) | `pnpm run lint` |
175
- | Where specs go | (you choose) | `specs/` |
157
+ | Commit style | recent commit messages, judged per repo | `ticket` (`STP-123: …`), with one repo on `conventional` |
158
+ | Commands | `package.json` scripts, lockfile, Makefile, CI | `npm run lint` |
159
+ | Share specs with the team | your `.gitignore` today | everything / project memory only / local |
176
160
  | After you approve a spec | (you choose) | `stop` (wait at each stage) or `run-through` |
177
- | Model and effort per task tier | written with the defaults above | edit `models:` / `effort:` later |
178
161
 
179
162
  **Where the file lives, by layout:**
180
163
 
@@ -184,34 +167,23 @@ straight away, so you can fix it before moving on. Press Enter on an empty line
184
167
  | **monorepo** | one repo, many apps/packages | the repo root (commit it) | tasks name their package, and checks run per package |
185
168
  | **multi-repo** | several repos that change together, in any folders | their shared parent folder, or `~/.spec-driven/<name>/` (personal to your machine) | investigation searches every listed repo, tasks stay inside one repo each, and `spec-ship` opens one PR per repo, each linking the others |
186
169
 
187
- **Repos in different places.** A multi-repo workspace doesn't need its repos in one folder. For
188
- example, from inside `/Projects/frontend`:
170
+ **Repos in different places.** A multi-repo workspace doesn't need its repos in one folder
171
+ (`/Projects/frontend`, `~/Work/backend`, `/srv/database`). The config goes in their shared parent
172
+ folder, or `~/.spec-driven/<name>/` when that would be your home folder, and is recorded in
173
+ `~/.spec-driven/workspaces.json` so every repo finds it.
189
174
 
190
- ```
191
- ✔ How is this project organised? › Multiple repos
192
- ✔ Which of these repos (found under /Projects) belong to this workspace? › frontend
193
- ? Add a repo from another folder — paste its path, or press Enter to finish (1 selected) › ~/Work/nope
194
- › Folder not found: /Users/you/Work/nope
195
- ? Add a repo from another folder — paste its path, or press Enter to finish (1 selected) › ~/Work/backend
196
- ✓ added /Users/you/Work/backend
197
- ? Add a repo from another folder — paste its path, or press Enter to finish (2 selected) › /srv/database
198
- ✓ added /srv/database
199
- ? Add a repo from another folder — paste its path, or press Enter to finish (3 selected) ›
200
- ✔ Folder for this workspace's settings file (.spec-driven.yml) › ~/.spec-driven/frontend-workspace
201
- ```
175
+ **Teams where people work on different repos.** One developer has the Shopify app and the backend,
176
+ another the dashboard and the backend. Each person's workspace lists only the repos they have, and
177
+ specs go in a place everyone has: a small dedicated specs repo is best. Its `team-repos.yml` lists
178
+ every team repo with its git URL, so setup offers them, `doctor` shows which aren't cloned yet (with
179
+ the clone command), and an investigation stops and says so when a ticket needs a repo that isn't on
180
+ this machine.
202
181
 
203
- - **Where the config goes:** the repos' shared parent folder, or `~/.spec-driven/<name>/` when that
204
- parent would be your home folder or `/`.
205
- - **How every repo finds it:** setup records the workspace in `~/.spec-driven/workspaces.json`, so
206
- `/Work/backend` finds its config even though the config isn't above it. The skills, `doctor`,
207
- and `status` all look there.
208
- - **Paths are checked:** ones that don't exist or aren't git repos are skipped with a message, and
209
- setup stops if a workspace would end up with no repos.
210
-
211
- Every question also has a flag, for scripts and CI:
212
- `--layout=multi-repo --repos=~/Projects/frontend,~/Work/backend --workspace=~/Projects`,
213
- `--layout=monorepo --packages=apps/web,packages/ui`, `--after-approval=run-through`,
214
- `--project=new|existing`, `--tools=cursor,codex`, `--global`, `--yes` (accept everything detected), and `--version`.
182
+ **For scripts and CI** there's `spec-driven-dev setup` with flags (it asks nothing):
183
+ `--yes`, `--layout=multi-repo --repos=~/Projects/frontend,~/Work/backend --workspace=~/Projects`,
184
+ `--layout=monorepo --packages=apps/web,packages/ui`, `--specs-dir=<path>`, `--share=all|memory|none`,
185
+ `--after-approval=run-through`, `--project=new|existing`. Installer flags: `--tools=cursor,codex`,
186
+ `--global`, `--no-setup`, `--version`.
215
187
 
216
188
  To change the rules later, edit the file or run setup again, then run `spec-driven-dev doctor`
217
189
  to confirm the file still matches your repos. Setup never overwrites the file without
@@ -249,6 +221,25 @@ lines that point to files that no longer exist. If you install
249
221
  [graphify](https://github.com/Graphify-Labs/graphify), the skills use it for cheaper code
250
222
  navigation. It's optional (`context.graph: off` turns it off), and nothing is installed for you.
251
223
 
224
+ ### Sharing specs with your team
225
+
226
+ Setup asks whether to commit the specs folder, and shows what your `.gitignore` does today:
227
+
228
+ | Choice | What's committed | Good for |
229
+ |---|---|---|
230
+ | **Share everything** (recommended) | plans, investigations, reviews and project memory | teams that review plans in PRs, and handovers |
231
+ | **Share project memory only** | `specs/CONTEXT.md` and `specs/context/` | sharing what the project knows, keeping per-ticket files local |
232
+ | **Keep everything local** | nothing (`specs/` is ignored) | trying it out, or personal use |
233
+
234
+ Setup changes `.gitignore` only after you agree, edits one marked block, and replaces only lines about
235
+ the specs folder. `risk.md` is always ignored. Change your mind any time with
236
+ `spec-driven-dev settings --share=all|memory|none`, or say "share the specs with the team" in your AI
237
+ tool. `doctor` warns if `.gitignore` drifts from your choice.
238
+
239
+ **Multi-repo:** the workspace folder usually isn't a git repo, so specs kept there stay on your
240
+ machine. To share them, choose a specs folder inside the repo your team treats as home, for example
241
+ `--specs-dir=frontend/specs`.
242
+
252
243
  ### GitRisk: which files have a history of breaking
253
244
 
254
245
  `spec-driven-dev risk` scores every tracked code file from git history alone, with no model and no
@@ -568,6 +559,11 @@ confirm. To change it, edit `git.base_branch` / `git.branch_name` or run setup a
568
559
  For a single repo or monorepo, yes: the whole team then follows the same rules. A multi-repo
569
560
  config sits outside any repo, so it stays on your machine.
570
561
 
562
+ **Should I commit the specs?**
563
+ Setup asks. Sharing everything is recommended: teammates and reviewers see the plans and the
564
+ reasoning, and the project memory builds up for everyone. See
565
+ [Sharing specs with your team](#sharing-specs-with-your-team).
566
+
571
567
  **Does it work with my stack?**
572
568
  Yes. No skill assumes a language or framework. Each one finds the repo's own commands.
573
569
 
@@ -577,19 +573,19 @@ run one after another, stopping at the first failure or review blocker. Two step
577
573
  spec, and pushing or opening the PR. You don't need slash commands either. Saying "investigate
578
574
  STP-123" or "ship it" picks the right skill.
579
575
 
580
- **Setup didn't ask me anything.**
581
- Update to 1.5.1 or later (`npx @benjaminthomas/spec-driven-dev@latest --version`). Earlier versions
582
- crashed on Node 14 or 16, which nvm often switches to inside older projects, and wrote a config
583
- silently when there was no terminal. Now it runs on Node 14+. With no terminal to answer in (a `!`
584
- command in Claude Code, an editor task, piped input), it installs the skills and hands the questions
585
- to your AI tool: say "run spec-setup" there. `spec-driven-dev setup` on its own still needs a
586
- terminal, or flags such as `--layout=multi-repo --repos=…`.
576
+ **The installer didn't ask me about my project.**
577
+ That's expected from 1.13.0: the installer only installs the skills. Project setup happens in your
578
+ AI tool: say "run spec-setup". It runs on Node 14+, and works from `!` inside Claude Code too.
579
+
580
+ **npm prints "Unknown project config" warnings.**
581
+ They come from your project's `.npmrc`, not from this package: settings such as `auto-install-peers`
582
+ or `shamefully-hoist` are for pnpm, and npm warns about them on every `npm`/`npx` command in that
583
+ folder. If the project uses npm (it has a `package-lock.json`), delete those lines. If it uses pnpm,
584
+ run `pnpm dlx @benjaminthomas/spec-driven-dev` instead.
587
585
 
588
- **Setup didn't ask me about repos.**
589
- It only asks about repos for a multi-repo or monorepo layout. If it detected `single`, type
590
- `multi-repo` at the layout question, then add your other repos by path. If it asked nothing at all,
591
- there was no terminal to type answers into (it prints a note saying so), so run it directly in a
592
- terminal. Check which version ran with `npx @benjaminthomas/spec-driven-dev@latest --version`.
586
+ **Setup thought my repo was a monorepo (or a single repo).**
587
+ Tell it in the chat: "this is one repo that works with reverb-backend". A Shopify app's
588
+ `extensions/` folder is treated as part of one app from 1.13.0.
593
589
 
594
590
  **Can I skip stages?**
595
591
  Yes. Every skill works on its own. `spec-verify` even works without a spec: it asks what to check.
@@ -639,7 +635,7 @@ Then, in a project:
639
635
 
640
636
  ```bash
641
637
  spec-driven-dev # detect tools, install skills, set up the project
642
- spec-driven-dev setup # only (re)run project setup
638
+ spec-driven-dev setup --yes # only (re)run project setup
643
639
  spec-driven-dev --global # install skills for all projects, not just this one
644
640
  spec-driven-dev --tools=cursor,codex # pick tools without being asked
645
641
  spec-driven-dev --skills=spec-create,spec-ship # install only some skills
@@ -647,7 +643,7 @@ spec-driven-dev --yes # accept every detected defaul
647
643
  spec-driven-dev doctor # check .spec-driven.yml against the real repos
648
644
  spec-driven-dev status # list unfinished specs and each one's next step
649
645
  spec-driven-dev context --areas=billing # print the project memory (index, or chosen areas)
650
- spec-driven-dev settings # switch GitRisk and run-through on/off from a menu
646
+ spec-driven-dev settings # switch GitRisk, run-through and sharing from a menu
651
647
  spec-driven-dev risk # GitRisk: score files from git history → specs/context/risk.md
652
648
  spec-driven-dev risk-eval --tickets=tickets.md # compare agent behaviour with the risk signal on vs off
653
649
  # (--runs=3 --arms=on --timeout=20 --model=sonnet)
@@ -655,7 +651,7 @@ spec-driven-dev --help # show all of the above
655
651
  ```
656
652
 
657
653
  **Without installing:** replace `spec-driven-dev` with `npx @benjaminthomas/spec-driven-dev`, for
658
- example `npx @benjaminthomas/spec-driven-dev setup`. That downloads and runs the latest version once,
654
+ example `npx @benjaminthomas/spec-driven-dev doctor`. That downloads and runs the latest version once,
659
655
  which is handy in CI or on a machine you won't use again.
660
656
 
661
657
  `doctor` checks that every branch, folder, and `npm run` script named in the config exists, and
@@ -689,8 +685,8 @@ install `ticket-to-pr@spec-driven-dev`; the old name no longer gets updates.
689
685
 
690
686
  To see unfinished specs and their next step at any time, run `spec-driven-dev status`
691
687
  (for example, `specs/stp-123: 3/5 tasks done → next: spec-implement`). If
692
- you use the plugin, `spec-driven-dev` skips Claude Code so you don't get every skill twice. Run
693
- `spec-driven-dev setup` for the project config.
688
+ you use the plugin, `spec-driven-dev` skips Claude Code so you don't get every skill twice. Then say
689
+ "run spec-setup" in Claude Code for the project config.
694
690
 
695
691
  ### By hand
696
692
 
@@ -744,8 +740,8 @@ skips that step rather than failing.
744
740
 
745
741
  | Skill | Stage | SKILL.md lines | Words | Supporting files |
746
742
  |---|---|---:|---:|---:|
747
- | `spec-setup` | 0 · configure | 162 | 1,545 | 3 |
748
- | `spec-investigate` | 1 · understand | 373 | 3,211 | 1 |
743
+ | `spec-setup` | 0 · configure | 210 | 2,201 | 3 |
744
+ | `spec-investigate` | 1 · understand | 385 | 3,325 | 1 |
749
745
  | `spec-create` | 2 · plan | 198 | 1,782 | 6 |
750
746
  | `spec-implement` | 3 · build | 327 | 2,753 | 3 |
751
747
  | `spec-verify` | 4 · prove | 163 | 1,557 | 1 |
@@ -753,7 +749,7 @@ skips that step rather than failing.
753
749
  | `spec-checkpoint` | 6 · commit | 140 | 1,144 | 0 |
754
750
  | `spec-ship` | 7 · PR | 230 | 1,388 | 0 |
755
751
  | `generative-seo` | standalone | 255 | 2,209 | 7 |
756
- | **Total** | | **1,985** | **16,883** | **22** |
752
+ | **Total** | | **2,045** | **17,653** | **22** |
757
753
 
758
754
  - **8** skills: **1** setup + **6** workflow stages + **1** standalone
759
755
  - **4** tools supported natively (Claude Code, Codex CLI, Cursor, Antigravity), plus an MCP fallback
@@ -777,6 +773,7 @@ bin/spec-driven-dev.cjs launcher (runs on any Node, explains if it's too o
777
773
  bin/cli.mjs the `spec-driven-dev` CLI: installer, setup menus, doctor, status, context, risk
778
774
  bin/risk.mjs GitRisk scoring (churn, bug density, fan-out, security)
779
775
  bin/risk-eval.mjs risk on/off experiment harness
776
+ bin/share.mjs what's committed from the specs folder (.gitignore block)
780
777
  package.json npm package (ships bin/ and plugin/skills/)
781
778
  mcp-server/ MCP fallback server
782
779
  CHANGELOG.md what changed in each version
@@ -805,7 +802,7 @@ plugin/evals/ `claude plugin eval` cases
805
802
  1. Increase `version` in both `package.json` and `plugin/.claude-plugin/plugin.json` (the check fails if
806
803
  they differ).
807
804
  2. Add a `CHANGELOG.md` entry, then commit and push.
808
- 3. Tag the release and push the tag: `git tag v1.11.2 && git push origin v1.11.2`.
805
+ 3. Tag the release and push the tag: `git tag v1.13.0 && git push origin v1.13.0`.
809
806
  4. Watch the **publish** run in the repo's Actions tab. When it's green, the new version is on
810
807
  npm. Claude Code plugin users get it from the marketplace once the commit is on `main`.
811
808
 
package/bin/cli.mjs CHANGED
@@ -2,17 +2,17 @@
2
2
  // spec-driven-dev installer (npm i -g @benjaminthomas/spec-driven-dev): detects your AI coding tools, copies the skills into each one, and
3
3
  // runs project discovery (layout, tracker, branches, commands) to write .spec-driven.yml.
4
4
  //
5
- // spec-driven-dev install + project setup (interactive)
6
- // spec-driven-dev setup project setup only
5
+ // spec-driven-dev install the skills, then hand over to your AI tool ("run spec-setup")
6
+ // spec-driven-dev setup scripted setup for CI: needs --yes or answer flags (people: say "run spec-setup")
7
7
  // spec-driven-dev doctor check .spec-driven.yml against the real repos (exit 1 on problems)
8
8
  // spec-driven-dev status list specs with unfinished 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");
@@ -38,32 +39,46 @@ const args = process.argv.slice(2);
38
39
  const flag = (n) => args.includes(`--${n}`);
39
40
  const opt = (n) => args.find((a) => a.startsWith(`--${n}=`))?.split("=")[1];
40
41
  const command = args.find((a) => !a.startsWith("-")) || "install";
41
- const YES = flag("yes") || flag("y") || !process.stdin.isTTY || ["doctor", "status", "context", "risk", "risk-eval"].includes(command);
42
+ const YES = flag("yes") || flag("y") || !process.stdin.isTTY || ["doctor", "status", "context", "risk", "risk-eval", "setup"].includes(command);
42
43
  const DIR = path.resolve(opt("dir") || process.cwd());
43
44
 
45
+ // Colour only for a person at a terminal; pipes, CI and NO_COLOR get plain text (and the tests stay plain).
46
+ const COLOR = (process.stdout.isTTY || !!process.env.FORCE_COLOR) && !process.env.NO_COLOR && process.env.TERM !== "dumb";
47
+ const paint = (code) => (t) => (COLOR ? `\x1b[${code}m${t}\x1b[0m` : t);
48
+ const ink = { green: paint("32"), yellow: paint("33"), red: paint("31"), cyan: paint("36"), magenta: paint("35"), blue: paint("34"), bold: paint("1"), dim: paint("2") };
49
+ // Each stage gets its own colour so the steps are easy to tell apart while scrolling.
50
+ const step = (n, title, color) => console.log(`\n${color(ink.bold(`━━ ${n} · ${title} `))}${color("━".repeat(Math.max(4, 44 - title.length)))}`);
51
+ if (COLOR) {
52
+ const plain = console.log.bind(console);
53
+ console.log = (...a) => {
54
+ const t = a.join(" ");
55
+ plain(t.replace(/^(\s*)(✓)/gm, (_, s, g) => s + ink.green(g)).replace(/^(\s*)(✗.*)$/gm, (_, s, g) => s + ink.red(g))
56
+ .replace(/^(\s*)(•)/gm, (_, s, g) => s + ink.yellow(g)).replace(/^(Next:.*)$/gm, (_, g) => ink.cyan(ink.bold(g))));
57
+ };
58
+ }
59
+
44
60
  if (flag("version") || flag("v")) {
45
61
  console.log(JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version);
46
62
  process.exit(0);
47
63
  }
48
64
  // No terminal to answer in (e.g. `!` in Claude Code, an IDE task, a pipe) and nothing passed explicitly:
49
65
  // 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"];
66
+ const ANSWER_FLAGS = ["share", "specs-dir", "risk", "yes", "y", "layout", "repos", "packages", "tools", "workspace", "after-approval", "skills", "global", "no-setup", "project"];
51
67
  const NO_TTY = !process.stdin.isTTY && !args.some((a) => ANSWER_FLAGS.some((f) => a === `--${f}` || a.startsWith(`--${f}=`)));
52
68
  // Installing the skills needs no answers, so the default command still does that without a terminal
53
69
  // (e.g. `!` in Claude Code) and hands the questions to the spec-setup skill, which asks them in the chat.
54
70
  const HANDOFF = command === "install" && NO_TTY;
55
- if (["setup", "settings"].includes(command) && NO_TTY) {
71
+ // Setup is a conversation in the AI tool now; the setup command stays for scripts and CI (flags only).
72
+ const SETUP_IN_AGENT = command === "setup" && !args.some((a) => ANSWER_FLAGS.some((f) => a === `--${f}` || a.startsWith(`--${f}=`)));
73
+ if (command === "settings" && NO_TTY) {
56
74
  console.log(`✗ No interactive terminal, so ${command === "settings" ? "the settings menu can't open" : "setup can't ask its questions"}. Nothing was changed.
57
75
 
58
76
  This happens when it runs through another program: a "!" command in Claude Code, an editor task,
59
77
  or piped input. Either:
60
78
  • run it yourself in a terminal window (Terminal, iTerm, or your editor's terminal panel):
61
- npx @benjaminthomas/spec-driven-dev@latest ${command}${command === "setup" ? `
62
- • or, in your AI tool (Claude Code, Cursor, Codex), say "run spec-setup": it asks the same
63
- questions in the chat` : ""}
64
- • or give the answers as flags, for example:
65
- npx @benjaminthomas/spec-driven-dev@latest ${command === "settings" ? "settings --risk=off" : "setup --layout=multi-repo --repos=~/Projects/frontend,~/Projects/backend"}
66
- (add --yes to accept everything it detects)`);
79
+ npx @benjaminthomas/spec-driven-dev@latest settings
80
+ • or give the answer as a flag, for example:
81
+ npx @benjaminthomas/spec-driven-dev@latest settings --risk=off`);
67
82
  process.exit(1);
68
83
  }
69
84
 
@@ -112,6 +127,7 @@ function copyDir(from, to) {
112
127
 
113
128
  // ---------- install ----------
114
129
  async function install() {
130
+ step(1, "Install the skills", ink.blue);
115
131
  const detected = Object.keys(TOOLS).filter((t) => fs.existsSync(path.join(HOME, TOOLS[t].home)) || onPath(TOOLS[t].bin));
116
132
  let tools = opt("tools")?.split(",");
117
133
  if (!tools) tools = await askMulti("Which AI coding tools should get the skills?",
@@ -144,7 +160,26 @@ async function install() {
144
160
  console.log(`✓ ${skills.length} skills → ${dest}`);
145
161
  }
146
162
  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.");
147
- return dests.length;
163
+ return dests.length ? tools : [];
164
+ }
165
+
166
+ // The setup questions (layout, repos, tracker, branches, sharing) and the audit need a model, so they
167
+ // happen in the AI tool. Claude Code and Codex can be opened with the request already typed in.
168
+ const LAUNCHERS = [["claude", "Claude Code"], ["codex", "Codex"]];
169
+ async function handOver(tools, onlySetup) {
170
+ const name = path.basename(gitTop(DIR));
171
+ step(onlySetup ? 1 : 2, `Set up ${name} in your AI tool`, ink.magenta);
172
+ console.log(`
173
+ ${onlySetup ? "Setup happens in your AI tool now, so it can read the code and you can change any answer as you go." : "Skills installed."}
174
+ Next: set up ${name}. In your AI tool, say "run spec-setup". It asks how the project is organised
175
+ (single repo, monorepo, or several repos), your tracker, branches and whether to share specs,
176
+ then looks through the code. Start a new session if your tool doesn't recognise it yet.${onlySetup ? `
177
+ For scripts and CI: spec-driven-dev setup --yes [--layout=… --repos=… --share=…]` : ""}`);
178
+ const launcher = LAUNCHERS.find(([bin]) => (onlySetup || tools.includes(bin)) && onPath(bin));
179
+ if (!launcher || !process.stdin.isTTY || YES || onlySetup) return;
180
+ if (!(await askConfirm(`Open ${launcher[1]} here now and start it? (takes a few minutes, uses your ${launcher[1]} plan)`, true))) return;
181
+ const { spawnSync } = await import("node:child_process");
182
+ spawnSync(launcher[0], ["run spec-setup"], { cwd: DIR, stdio: "inherit" });
148
183
  }
149
184
 
150
185
  // ---------- discovery ----------
@@ -185,7 +220,9 @@ function detectLayout(dir) {
185
220
  const base = g.replace(/\/\*\*?$/, "");
186
221
  return base === g ? [g.replace(/\/$/, "")] : subdirs(path.join(top, base)).map((n) => `${base}/${n}`);
187
222
  }))].filter(hasManifest);
188
- const mono = MARKERS.some((m) => fs.existsSync(path.join(top, m))) || pkg?.workspaces || cargo || packages.length > 1;
223
+ // A Shopify app keeps its app extensions in npm workspaces under extensions/: one app, not a monorepo.
224
+ const shopifyApp = fs.readdirSync(top).some((f) => /^shopify\.app.*\.toml$/.test(f)) && declared.length > 0 && declared.every((g) => g.startsWith("extensions/"));
225
+ const mono = !shopifyApp && (MARKERS.some((m) => fs.existsSync(path.join(top, m))) || pkg?.workspaces || cargo || packages.length > 1);
189
226
  return { layout: mono ? "monorepo" : "single", root: top, repos: mono ? packages : [] };
190
227
  }
191
228
  const repos = findRepos(dir);
@@ -236,11 +273,12 @@ function infer(facts) {
236
273
  const y = (v) => JSON.stringify(v ?? ""); // JSON strings are valid YAML scalars
237
274
  function toYaml(c) {
238
275
  const cmds = (o, pad) => Object.entries(o).map(([k, v]) => `${pad}${k}: ${y(v)}`).join("\n");
239
- let out = `# .spec-driven.yml — written by spec-driven-dev setup. Edit freely, or re-run setup.\n# Every spec-* skill reads this first. Field reference: skills/spec-setup/references/config-template.yml\nversion: 1\nlayout: ${c.layout}\n`;
276
+ let out = `# .spec-driven.yml — written by spec-driven-dev. Edit freely, or say \"run spec-setup\" in your AI tool to review it.\n# Every spec-* skill reads this first. Field reference: skills/spec-setup/references/config-template.yml\nversion: 1\nlayout: ${c.layout}\n`;
240
277
  if (c.repos.length) {
241
278
  out += "repos:\n" + c.repos.map((r) => {
242
279
  let e = ` - path: ${y(r.path)}\n role: ${y(r.role)}\n`;
243
- if (r.base && r.base !== c.git.base_branch) e += ` git:\n base_branch: ${y(r.base)}\n pr_target: ${y(r.base)}\n`;
280
+ const g = [r.base && r.base !== c.git.base_branch ? ` base_branch: ${y(r.base)}\n pr_target: ${y(r.base)}\n` : "", r.commitStyle ? ` commit_style: ${y(r.commitStyle)}\n` : ""].join("");
281
+ if (g) e += ` git:\n${g}`;
244
282
  if (r.commands && Object.values(r.commands).some(Boolean)) e += ` commands:\n${cmds(r.commands, " ")}\n`;
245
283
  return e;
246
284
  }).join("");
@@ -252,7 +290,7 @@ function toYaml(c) {
252
290
  .map(([tool, m]) => ` ${tool}:\n` + MODEL_KEYS.map((t) => ` ${t}: ${m[t]}\n`).join("")).join("");
253
291
  out += "effort: # effort / reasoning level per tier (Claude Code, Codex): low | medium | high | xhigh | max | default\n"
254
292
  + MODEL_KEYS.map((t) => ` ${t}: ${EFFORT_DEFAULTS[t]}\n`).join("");
255
- 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`;
293
+ 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`;
256
294
  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";
257
295
  out += `pipeline:\n after_approval: ${c.afterApproval || "stop"} # stop | run-through (implement → verify → review → checkpoint; ship always asks)\n`;
258
296
  return out + `specs_dir: ${y(c.specsDir)}\n`;
@@ -460,7 +498,8 @@ async function setup() {
460
498
  { title: `${ex}-123: add login${det("ticket", g.commitStyle)}`, value: "ticket", description: "ticket ID first" },
461
499
  { title: `Add login${det("freeform", g.commitStyle)}`, value: "freeform", description: "no fixed format" },
462
500
  ], g.commitStyle);
463
- const specsDir = await askText("Folder for specs (one sub-folder per feature)", "specs");
501
+ const specsDir = await chooseSpecsDir(layout, root, repos);
502
+ const share = await chooseShare(root, specsDir);
464
503
  const afterApproval = opt("after-approval") || await askSelect("After you approve a spec…", [
465
504
  { title: "Stop after each stage", value: "stop", description: "you start implement, verify and checkpoint yourself" },
466
505
  { 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" },
@@ -473,18 +512,21 @@ async function setup() {
473
512
  ], pd.kind);
474
513
 
475
514
  const config = {
476
- project, layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "", afterApproval,
515
+ project, share: share || "none", layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "", afterApproval,
477
516
  git: { remote: "origin", base_branch: base, pr_target: pr, branch_name: branchName, commit_style: commitStyle },
478
517
  commands: layout === "single" ? commandsFor(repoDirs[0]) : commandsFor(root),
479
518
  repos: repos.map((r) => {
480
519
  const f = layout === "multi-repo" ? gitFacts(path.resolve(root, r)) : null;
481
520
  // a repo without an integration branch must say so, or it inherits the top-level base (e.g. staging) it doesn't have
482
- return { path: r, role: "", base: f ? f.integration || "default" : "", commands: commandsFor(path.resolve(root, r)) };
521
+ // each repo keeps its own commit style when it differs from the workspace's (one repo uses STP-123:, another feat:)
522
+ const own = f && f.subjects.length >= 5 ? infer([f]).commitStyle : "";
523
+ return { path: r, role: "", base: f ? f.integration || "default" : "", commitStyle: own && own !== commitStyle ? own : "", commands: commandsFor(path.resolve(root, r)) };
483
524
  }),
484
525
  };
485
526
  fs.writeFileSync(file, toYaml(config));
486
527
  if (layout === "multi-repo") register(real(file), repos.map((r) => real(path.resolve(root, r))));
487
528
  console.log(`✓ Wrote ${file}`);
529
+ if (share) await applyShare(root, specsDir, share, { quietIfSame: true });
488
530
  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.");
489
531
  console.log(project === "existing"
490
532
  ? ` 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.`
@@ -552,8 +594,9 @@ function findConfig(dir) {
552
594
  }
553
595
 
554
596
  function doctor() {
597
+ if (COLOR) step("✓", "Check the settings", ink.cyan);
555
598
  const file = findConfig(DIR);
556
- if (!file) throw new Error(`No .spec-driven.yml in ${DIR} or above. Run: spec-driven-dev setup (or npx @benjaminthomas/spec-driven-dev setup)`);
599
+ if (!file) throw new Error(`No .spec-driven.yml in ${DIR} or above. In your AI tool, say "run spec-setup".`);
557
600
  const root = path.dirname(file);
558
601
  let c;
559
602
  try { c = parseYaml(fs.readFileSync(file, "utf8")); } catch (e) { throw new Error(`${file} could not be read: ${e.message}`); }
@@ -598,8 +641,27 @@ function doctor() {
598
641
  if (!fs.existsSync(specs)) notes.push(`${c.specs_dir || "specs"}/ doesn't exist yet — created on first spec`);
599
642
  if (c.context?.project && !["new", "existing"].includes(c.context.project)) problems.push(`context.project "${c.context.project}" must be new or existing`);
600
643
  if (c.context?.graph && !["auto", "off"].includes(c.context.graph)) problems.push(`context.graph "${c.context.graph}" must be auto or off`);
601
- const repoRoots = c.layout === "multi-repo" ? (c.repos || []).map((r) => path.resolve(root, expandHome(r.path))) : [root];
644
+ // Multi-repo context names paths from the workspace folder (soreto-shopify/app/) or from inside a repo (app/).
645
+ const repoRoots = c.layout === "multi-repo" ? [root, ...(c.repos || []).map((r) => path.resolve(root, expandHome(r.path)))] : [root];
602
646
  checkContext(specs, c.specs_dir || "specs", repoRoots, problems, notes);
647
+ // The team's repo list (in the specs location) vs the repos on this machine.
648
+ const team = parseYaml(fs.existsSync(path.join(specs, "team-repos.yml")) ? fs.readFileSync(path.join(specs, "team-repos.yml"), "utf8") : "").repos;
649
+ if (Array.isArray(team)) {
650
+ const mine = new Set((c.repos || []).map((r) => path.basename(String(r.path || "").replace(/\/+$/, ""))));
651
+ for (const t of team) {
652
+ if (!t.name || mine.has(t.name)) continue;
653
+ const here = path.join(root, t.name);
654
+ notes.push(isRepo(here) ? `${t.name} is cloned at ${here} but isn't in this workspace — say "add ${t.name} to the workspace" if you work on it`
655
+ : `team repo ${t.name} isn't on this machine (fine if you don't work on it)${t.url ? `: git clone ${t.url} ${here}` : ""}`);
656
+ }
657
+ }
658
+ if (c.context?.share) {
659
+ if (!SHARE_MODES.includes(c.context.share)) problems.push(`context.share "${c.context.share}" must be all, memory or none`);
660
+ const g = specsInGit(root, c.specs_dir || "specs");
661
+ 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`);
662
+ else if (g && SHARE_MODES.includes(c.context.share) && shareState(g.top, g.rel) !== c.context.share)
663
+ 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}`);
664
+ }
603
665
  const rmd = path.join(specs, "context", "risk.md"), rtop = run("git", ["rev-parse", "--show-toplevel"], root);
604
666
  if (rtop && fs.existsSync(rmd) && run("git", ["ls-files", path.relative(path.resolve(rtop), real(rmd))], rtop))
605
667
  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)`);
@@ -651,6 +713,60 @@ function context() {
651
713
  }
652
714
  }
653
715
 
716
+ // ---------- sharing specs through git ----------
717
+ function specsInGit(root, specsDir) {
718
+ const abs = path.resolve(root, specsDir);
719
+ let probe = abs;
720
+ while (!fs.existsSync(probe)) probe = path.dirname(probe);
721
+ const top = run("git", ["rev-parse", "--show-toplevel"], probe);
722
+ if (!top) return null;
723
+ 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("/");
724
+ return rel.startsWith("..") ? null : { top, rel };
725
+ }
726
+
727
+ async function chooseSpecsDir(layout, root, repos) {
728
+ if (opt("specs-dir")) return opt("specs-dir");
729
+ if (layout !== "multi-repo" || isRepo(root)) return askText("Folder for specs (one sub-folder per feature)", "specs");
730
+ // The workspace folder isn't a repo, so specs kept there can never be shared through git.
731
+ return askSelect("Where should specs and project memory live?", [
732
+ { 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" },
733
+ ...repos.map((r) => ({ title: `Inside ${r}/specs`, value: `${r}/specs`.replace(/^\.\//, ""), description: "can be shared with the team through that repo" })),
734
+ ], "specs");
735
+ }
736
+
737
+ async function chooseShare(root, specsDir) {
738
+ const g = specsInGit(root, specsDir);
739
+ if (opt("share") && !SHARE_MODES.includes(opt("share"))) throw new Error("--share must be all, memory or none");
740
+ 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; }
741
+ if (!g.rel) { console.log(` • ${specsDir} is a repo of its own, so everything in it is shared.`); return "all"; }
742
+ const now = shareState(g.top, g.rel);
743
+ if (now !== "all") console.log(` • Your .gitignore currently makes ${g.rel}: ${shareTitle[now].toLowerCase()}.`);
744
+ return opt("share") || askSelect("Share specs and project memory with your team?", [
745
+ { title: `Share everything${now === "all" ? " (current)" : ""} (recommended)`, value: "all", description: "commit plans, investigations, reviews and project memory; teammates and PR reviewers see the reasoning" },
746
+ { title: `Share project memory only${now === "memory" ? " (current)" : ""}`, value: "memory", description: "commit CONTEXT.md and context/; keep each feature's working files local" },
747
+ { title: `Keep everything local${now === "none" ? " (current)" : ""}`, value: "none", description: "ignore the specs folder; nothing is shared" },
748
+ ], now === "custom" ? "all" : now);
749
+ }
750
+
751
+ async function applyShare(root, specsDir, mode, { quietIfSame } = {}) {
752
+ const g = specsInGit(root, specsDir);
753
+ if (!g) { console.log(` • ${specsDir} isn't inside a git repo, so there's nothing to share or ignore.`); return; }
754
+ if (!g.rel) mode = "all"; // a dedicated specs repo is shared by definition
755
+ if (quietIfSame && shareState(g.top, g.rel) === mode && mode !== "all") return;
756
+ const gi = path.join(g.top, ".gitignore");
757
+ const before = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
758
+ const plan = planShare(before, g.rel, mode);
759
+ if (plan.text === before) return;
760
+ if (plan.removed.length && !opt("share") && !(await askConfirm(`To ${shareTitle[mode].toLowerCase()}, these .gitignore lines would be replaced: ${plan.removed.join(", ")}. Go ahead?`, true))) {
761
+ console.log(" • Left .gitignore as it was. Change it later with: spec-driven-dev settings");
762
+ return;
763
+ }
764
+ fs.writeFileSync(gi, plan.text);
765
+ console.log(` ✓ .gitignore updated: ${shareTitle[mode].toLowerCase()}${plan.removed.length ? ` (replaced: ${plan.removed.join(", ")})` : ""}.`);
766
+ const now = shareState(g.top, g.rel);
767
+ 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`);
768
+ }
769
+
654
770
  // risk.md is regenerated on every run (it carries a date), so committing it means noisy diffs and merge
655
771
  // conflicts between branches. Keep it out of git like a build output; anyone can recreate it in a second.
656
772
  function ignoreGenerated(file) {
@@ -732,21 +848,27 @@ const SETTINGS = [
732
848
  get: (c) => c.pipeline?.after_approval || "stop", options: [
733
849
  { value: "stop", title: "Stop after each stage", description: "you start implement, verify, review and commit yourself" },
734
850
  { value: "run-through", title: "Run straight through", description: "implement → verify → review → commit; stops at any failure, never pushes" }] },
851
+ { block: "context", key: "share", flag: "share", name: "Share specs with the team", about: "what's committed from the specs folder",
852
+ get: (c) => c.context?.share || "all", after: (file, c, v) => applyShare(path.dirname(file), c.specs_dir || "specs", v), options: [
853
+ { value: "all", title: "Share everything", description: "commit plans, investigations, reviews and project memory" },
854
+ { value: "memory", title: "Share project memory only", description: "commit CONTEXT.md and context/; keep feature files local" },
855
+ { value: "none", title: "Keep everything local", description: "ignore the specs folder" }] },
735
856
  ];
736
857
 
737
858
  async function settings() {
738
859
  const file = findConfig(DIR);
739
- if (!file) throw new Error(`No .spec-driven.yml in ${DIR} or above. Run: spec-driven-dev setup`);
860
+ if (!file) throw new Error(`No .spec-driven.yml in ${DIR} or above. In your AI tool, say "run spec-setup" first.`);
740
861
  let text = fs.readFileSync(file, "utf8");
741
862
  const label = (s, v) => s.options.find((o) => o.value === v)?.title || v;
742
- const save = (s, v) => {
863
+ const save = async (s, v) => {
743
864
  if (!s.options.some((o) => o.value === v)) throw new Error(`--${s.flag} must be ${s.options.map((o) => o.value).join(" or ")}`);
744
865
  text = setConfigValue(text, s.block, s.key, v);
745
866
  fs.writeFileSync(file, text);
746
867
  console.log(`✓ ${s.name}: ${label(s, v)} (saved in ${file})`);
868
+ if (s.after) await s.after(file, parseYaml(text), v);
747
869
  };
748
870
  const given = SETTINGS.filter((s) => opt(s.flag));
749
- if (given.length) given.forEach((s) => save(s, opt(s.flag)));
871
+ if (given.length) for (const s of given) await save(s, opt(s.flag));
750
872
  else for (;;) {
751
873
  const c = parseYaml(text);
752
874
  const pick = await askSelect("Settings: pick one to change", [
@@ -754,7 +876,7 @@ async function settings() {
754
876
  { title: "Done", value: null }], null);
755
877
  if (!pick) break;
756
878
  const v = await askSelect(`${pick.name}`, pick.options, pick.get(c));
757
- if (v !== pick.get(c)) save(pick, v);
879
+ if (v !== pick.get(c)) await save(pick, v);
758
880
  }
759
881
  const c = parseYaml(text);
760
882
  if (c.risk?.enabled !== "off" && !fs.existsSync(path.join(path.dirname(file), c.specs_dir || "specs", "context", "risk.md"))) {
@@ -795,20 +917,14 @@ try {
795
917
  else if (command === "risk") risk();
796
918
  else if (command === "settings") await settings();
797
919
  else if (command === "risk-eval") await riskEvalCmd();
798
- else if (command === "install" && HANDOFF) {
799
- 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");
800
- const n = await install();
801
- if (n) console.log(`
802
- Next: in your AI tool, say "run spec-setup". It asks the setup questions in the chat
803
- (single repo, monorepo or multi-repo, tracker, branches) and writes .spec-driven.yml.
804
- If your AI tool doesn't recognise it yet, start a new session so it loads the new skills.
805
- To answer in a terminal instead, open Terminal or iTerm and run: npx @benjaminthomas/spec-driven-dev setup`);
806
- } else if (command === "install") {
807
- await install();
808
- if (!flag("no-setup") && (await askConfirm("Set up this project now?", true))) await setup();
809
- } else if (command === "setup") await setup();
920
+ else if (command === "install") {
921
+ if (HANDOFF) console.log("No interactive terminal (for example a \"!\" command in Claude Code), so the skills go to the AI tools it finds, for this project.\n");
922
+ const tools = await install();
923
+ if (tools.length && !flag("no-setup")) await handOver(tools);
924
+ } else if (SETUP_IN_AGENT) handOver([], true);
925
+ else if (command === "setup") await setup();
810
926
  else throw new Error(`Unknown command "${command}" — use install (default), setup, settings, doctor, status, context, risk, or risk-eval`);
811
- if (["install", "setup"].includes(command) && !HANDOFF) console.log('\nNext: in your AI tool, say "investigate <ticket>" or "create a spec".');
927
+ if (command === "setup" && !SETUP_IN_AGENT) console.log('\nNext: in your AI tool, say "investigate <ticket>" or "create a spec".');
812
928
  } catch (e) {
813
929
  console.error(`✗ ${e.message}`);
814
930
  process.exitCode = 1;
package/bin/share.mjs ADDED
@@ -0,0 +1,49 @@
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
+ const under = (specs, p) => (specs ? `${specs}/${p}` : p); // specs can be a whole repo (a dedicated specs repo)
16
+
17
+ export function shareState(top, specs) {
18
+ const memory = !ignored(top, under(specs, "CONTEXT.md")) && !ignored(top, under(specs, "context/notes.md"));
19
+ const features = !ignored(top, under(specs, "some-feature/README.md"));
20
+ return features && memory ? "all" : memory ? "memory" : features ? "custom" : "none";
21
+ }
22
+
23
+ export function blockFor(specs, mode) {
24
+ if (!specs) return [BEGIN, "/context/risk.md", END]; // a specs repo exists to be shared: only the generated file is ignored
25
+ const lines = mode === "none" ? [`${specs}/`]
26
+ : mode === "memory" ? [`${specs}/*`, `!${specs}/CONTEXT.md`, `!${specs}/context/`, `${specs}/context/risk.md`]
27
+ : [`${specs}/context/risk.md`];
28
+ return [BEGIN, ...lines, END];
29
+ }
30
+
31
+ // New .gitignore text for a mode, plus which existing lines would go, so the caller can ask first.
32
+ export function planShare(text, specs, mode) {
33
+ const s = specs.replace(/^\/+|\/+$/g, "");
34
+ const about = new Set([s, `${s}/`, `/${s}`, `/${s}/`, `${s}/*`, `/${s}/*`, `${s}/**`, `!${s}/CONTEXT.md`, `!${s}/context/`, `${s}/context/risk.md`, `/${s}/context/risk.md`,
35
+ "# GitRisk scores, regenerated by spec-driven-dev risk"]);
36
+ const kept = [], removed = [];
37
+ let inBlock = false;
38
+ for (const line of text.split("\n")) {
39
+ if (line === BEGIN) { inBlock = true; continue; }
40
+ if (inBlock) { if (line === END) inBlock = false; continue; }
41
+ if (about.has(line.trim())) { if (!line.startsWith("# GitRisk") && !line.trim().endsWith("risk.md")) removed.push(line.trim()); continue; }
42
+ kept.push(line);
43
+ }
44
+ while (kept.length && kept[kept.length - 1] === "") kept.pop();
45
+ const body = kept.join("\n");
46
+ return { text: `${body}${body ? "\n\n" : ""}${blockFor(s, mode).join("\n")}\n`, removed };
47
+ }
48
+
49
+ 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.2",
3
+ "version": "1.13.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.2",
3
+ "version": "1.13.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",
@@ -65,6 +65,18 @@ Cheap sources first, code second:
65
65
 
66
66
  No CONTEXT.md? Investigate as usual, and suggest running `spec-setup` afterwards to create one.
67
67
 
68
+ ## Repos this machine doesn't have (multi-repo)
69
+
70
+ The workspace config lists only the repos cloned here. If the ticket or the evidence points at a
71
+ repo that isn't listed (its name in the ticket, an API or package it owns, a `team-repos.yml` role
72
+ that matches), **stop before concluding** and say so plainly:
73
+
74
+ > This ticket also touches **reverb-react** (the merchant dashboard), which isn't on this machine.
75
+ > Clone it: `git clone <url from team-repos.yml> <workspace>/reverb-react`, then say "add
76
+ > reverb-react to the workspace" and investigate again. Or I can carry on and mark that part UNKNOWN.
77
+
78
+ Never present a cross-repo change as complete when part of it lives in a repo you couldn't read.
79
+
68
80
  ## Risk signal (GitRisk)
69
81
 
70
82
  Use it only when `.spec-driven.yml` has `risk.enabled: on`, or has no `risk:` block, **and**
@@ -17,7 +17,9 @@ license: MIT
17
17
  # spec-setup
18
18
 
19
19
  Learn this project's rules and context once, save them, and let every other `spec-*` skill reuse
20
- them. Team rules live in a config file the team can review and change — never in the skills
20
+ them. This is **the** setup: the npm installer only copies the skills and then sends people here, so
21
+ treat it as their first conversation with the workflow. Explain each question in a line, and let
22
+ them change any earlier answer at any point ("actually it's Jira") before you write anything. Team rules live in a config file the team can review and change — never in the skills
21
23
  themselves. What the project *knows* (areas, conventions, decisions) lives in
22
24
  `{specs_dir}/CONTEXT.md`, kept small so loading it is cheap.
23
25
 
@@ -44,6 +46,11 @@ Read, don't guess. Collect a draft of every field in `references/config-template
44
46
  `package.json`, `turbo.json`, `nx.json`, `lerna.json`, `go.work`, `[workspace]` in
45
47
  `Cargo.toml`, several `apps/*`/`packages/*`/`services/*` with their own manifests) →
46
48
  `monorepo`; list the packages.
49
+ - **Not a monorepo:** a Shopify app (`shopify.app*.toml` at the root) whose workspaces only cover
50
+ `extensions/`. Those are parts of one app deployed together, so it's `single` (or one repo of a
51
+ multi-repo workspace). The same goes for any repo whose "packages" are plug-ins of one app.
52
+ - **Ask when one repo works with others.** Even inside a single repo, ask once: "Does this repo
53
+ change together with other repos (for example a backend)?" A yes means `multi-repo`.
47
54
  - Not inside a git repo, but sibling folders are git repos → candidate `multi-repo`. Group them
48
55
  by remote owner (`git remote get-url origin`) and show the groups — the user picks which repos
49
56
  belong together. Never assume every folder in the parent belongs. Always also ask whether
@@ -68,7 +75,9 @@ Read, don't guess. Collect a draft of every field in `references/config-template
68
75
  `gh repo view --json defaultBranchRef`); integration branches (`staging`, `develop`,
69
76
  `pre-prod`); which branch recent PRs targeted (`gh pr list --state merged --limit 20 --json
70
77
  baseRefName`); the pattern of the last ~20 remote branch names; the commit style from
71
- `git log -30 --format=%s`.
78
+ `git log -30 --format=%s`. Judge commit style **per repo**: when one repo writes `STP-123: …` and
79
+ the others write `feat: …`, keep the majority at the top level and give that repo its own
80
+ `git.commit_style`.
72
81
  5. **Commands, per repo/package.** `package.json` scripts plus the lockfile's package manager,
73
82
  `Makefile`, `pyproject.toml`/`tox.ini`, `Cargo.toml`, `go.mod`, `composer.json`, CI workflow
74
83
  steps (CI is often the most truthful source). Dev URL from the dev script, `.env.example`, or
@@ -82,6 +91,34 @@ Read, don't guess. Collect a draft of every field in `references/config-template
82
91
  pushing. Store it as `pipeline.after_approval`.
83
92
  8. **New or existing project.** Detect it (commit and file counts) and confirm in one line. Store
84
93
  it as `context.project`. This decides what Step 4 does.
94
+ 9. **Where specs live (multi-repo).** One specs folder serves the whole workspace: each ticket gets
95
+ one folder there even when it changes several repos; code, branches and PRs stay in each repo.
96
+ Offer, in this order, with the reason for each:
97
+ - **A dedicated specs repo (recommended for teams where developers work on different subsets of
98
+ repos):** everyone clones it, whatever code repos they have. Use an existing one (path or git
99
+ URL; offer the `git clone` command) or create one: only after a yes, `mkdir` it next to the
100
+ repos and `git init`. Never create a remote repo; say how to push it (`gh repo create`).
101
+ Then set `specs_dir: <specs-repo>` (its root holds `CONTEXT.md`, `context/` and feature folders).
102
+ - **Inside one code repo** (`<repo>/specs`): fine when every developer has that repo.
103
+ - **The workspace folder:** on this machine only, since it isn't a git repo.
104
+ 10. **The team's repo list.** Keep `{specs_dir}/team-repos.yml` (see `references/config-template.yml`)
105
+ listing every repo the team works across with its git URL and a one-line role. Each developer's
106
+ own `.spec-driven.yml` lists only the repos they have. When the file exists, offer its repos
107
+ instead of making people remember them, and for any not cloned here show
108
+ `git clone <url> <workspace>/<name>`. When someone adds a repo, add it to the file too.
109
+ 11. **Sharing with the team.** Find out what git does with the specs folder today:
110
+ `git check-ignore -v {specs_dir}/CONTEXT.md` and `git check-ignore -v {specs_dir}/x/README.md`.
111
+ Then ask once, saying what's true now (e.g. "your .gitignore ignores `specs/`, so nothing is
112
+ shared today"):
113
+ - **Share everything (recommended):** commit plans, investigations, reviews and project memory,
114
+ so teammates and PR reviewers see the reasoning and the memory grows for everyone.
115
+ - **Share project memory only:** commit `CONTEXT.md` and `context/`; feature files stay local.
116
+ - **Keep everything local:** ignore the specs folder.
117
+
118
+ **Multi-repo:** if the config folder isn't a git repo, specs kept there can't be shared. Say so
119
+ and offer `specs_dir: <repo>/specs` inside the repo the team treats as home.
120
+ Store the answer as `context.share`. A dedicated specs repo is shared by nature: set
121
+ `context.share: all`.
85
122
 
86
123
  ## Step 2 — Confirm in one round
87
124
 
@@ -118,17 +155,26 @@ Skip if `{specs_dir}/CONTEXT.md` already exists (offer a refresh instead, see `r
118
155
  intended stack and conventions for a new one. One round of questions, no more.
119
156
  2. Write `{specs_dir}/CONTEXT.md` (and any area files) using `references/context-template.md`.
120
157
  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
158
+ 3. **Apply the sharing choice.** Show the exact `.gitignore` lines that will be replaced or added,
159
+ and change it only after a yes, by running `spec-driven-dev settings --share=<all|memory|none>`
160
+ (it manages one marked block and only replaces lines about the specs folder). Never rewrite
161
+ other `.gitignore` lines.
162
+ 4. **Risk scores.** For an existing project, run `spec-driven-dev risk` (or
122
163
  `npx -y @benjaminthomas/spec-driven-dev risk`) to write `{specs_dir}/context/risk.md`. It uses
123
164
  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
165
  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
166
+ 5. **Offer, don't do:** ask whether to add this one line to `CLAUDE.md` / `AGENTS.md` (whichever
126
167
  the project uses), so hosts reload the pointer after compaction and in every new session:
127
168
  `For spec work, read specs/CONTEXT.md first, then the "Resume here" block of any unfinished spec.`
128
169
  Add it only on a yes.
129
- 5. Re-run `npx -y @benjaminthomas/spec-driven-dev doctor` if available. It checks the size limits
170
+ 6. Re-run `npx -y @benjaminthomas/spec-driven-dev doctor` if available. It checks the size limits
130
171
  and flags context lines that point to files which no longer exist.
131
172
 
173
+ ## After setup
174
+
175
+ Finish with **one** next step, not a list: "Setup's done. Say *investigate STP-123* to start your
176
+ first ticket." If anything was left out (a tracker URL, a repo not cloned), name it in one line.
177
+
132
178
  ## Quick switches (plain words, no YAML)
133
179
 
134
180
  When the user asks to switch a feature on or off, change only that one line of `.spec-driven.yml`,
@@ -140,6 +186,8 @@ keep every other line and comment, and confirm in one sentence. Don't re-run dis
140
186
  | "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
187
  | "run straight through after I approve", "fewer stops" | `pipeline.after_approval: run-through` |
142
188
  | "stop after each stage" | `pipeline.after_approval: stop` |
189
+ | "add reverb-react to the workspace", "I also work on X" | add it under `repos:` (clone first if needed, from `team-repos.yml`), and to `team-repos.yml` if it's new to the team |
190
+ | "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
191
 
144
192
  Reply with what changed and what it means, for example: "GitRisk is now off. Investigations, plans
145
193
  and reviews will ignore risk scores until you turn it back on." People who prefer a menu can run
@@ -97,5 +97,19 @@ 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
- specs_dir: specs # where specs/<feature>/ folders go (relative to this file)
104
+ specs_dir: specs # where specs/<feature>/ folders go (relative to this file).
105
+ # Multi-repo teams: a dedicated specs repo (e.g. soreto-specs) everyone clones.
106
+
107
+ # {specs_dir}/team-repos.yml — every repo the team works across, shared with the specs. Each developer's
108
+ # .spec-driven.yml lists only the repos they have; setup offers the rest with their clone commands.
109
+ # repos:
110
+ # - name: reverb-backend
111
+ # url: git@github.com:acme/reverb-backend.git
112
+ # role: REST API and workers
113
+ # - name: reverb-react
114
+ # url: git@github.com:acme/reverb-react.git
115
+ # role: merchant dashboard