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