@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 +89 -92
- package/bin/cli.mjs +156 -40
- package/bin/share.mjs +49 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/spec-investigate/SKILL.md +12 -0
- package/plugin/skills/spec-setup/SKILL.md +53 -5
- package/plugin/skills/spec-setup/references/config-template.yml +15 -1
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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:**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
|
149
|
+
| What it asks | Detected from | Example answer |
|
|
162
150
|
|---|---|---|
|
|
163
|
-
|
|
|
164
|
-
|
|
|
165
|
-
|
|
|
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
|
|
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 |
|
|
174
|
-
|
|
|
175
|
-
|
|
|
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
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
**
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
589
|
-
|
|
590
|
-
`
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
693
|
-
|
|
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 |
|
|
748
|
-
| `spec-investigate` | 1 · understand |
|
|
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** | | **
|
|
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.
|
|
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
|
|
6
|
-
// spec-driven-dev setup
|
|
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
|
|
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
|
-
|
|
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
|
|
62
|
-
• or
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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)
|
|
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"
|
|
799
|
-
console.log("No interactive terminal (for example a \"!\" command in Claude Code), so
|
|
800
|
-
const
|
|
801
|
-
if (
|
|
802
|
-
|
|
803
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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. **
|
|
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
|
-
|
|
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
|
-
|
|
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
|