@benjaminthomas/spec-driven-dev 1.11.0 → 1.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,9 +63,11 @@ start a new session and continue where you left off.
63
63
  npx @benjaminthomas/spec-driven-dev@latest
64
64
  ```
65
65
 
66
- This one command downloads the tool, installs the skills, and sets up the project. It asks its
67
- questions in your terminal, so run it in a real terminal, not through another tool. If there's no
68
- terminal to answer in, it says so and uses the detected values.
66
+ This one command downloads the tool, installs the skills, and sets up the project. In a terminal
67
+ window it asks its setup questions there. Run it with `!` inside Claude Code (or any other place
68
+ without a terminal to answer in) and it installs the skills for the AI tools it finds, then tells
69
+ you to say **"run spec-setup"**, which asks the same questions in the chat. It never writes the
70
+ settings file from guesses.
69
71
 
70
72
  > **Why not `npm install -g`?** npm doesn't let a package ask questions while it installs, and an
71
73
  > install doesn't know which project it's for. That's why setup is a separate step you run inside
@@ -263,7 +265,22 @@ Risk = 0.30 × Churn + 0.35 × Bug Density + 0.20 × Fan-out + 0.15 × Security
263
265
  | Fan-out | 20% | how many other files import it, against the repo's 90th percentile |
264
266
  | Security | 15% | share of its commits that were security fixes (security, CVE, XSS, injection…) |
265
267
 
266
- Tiers: critical ≥ 75, high ≥ 50, medium ≥ 25. When `risk.enabled` is on, the skills use it:
268
+ Tiers: critical ≥ 75, high ≥ 50, medium ≥ 25. Files added in the last 90 days with fewer than 3
269
+ commits, or not committed yet, are labelled **new** instead of being scored, because too little history isn't the same as
270
+ safe. Each new file shows the riskiest file it imports or is imported by, and inherits that file's
271
+ care when it's high or critical.
272
+
273
+ **When it updates:** setup scores the whole history once. After that it re-scores at every batch
274
+ commit in `spec-implement` and at `spec-checkpoint`. Each run takes under a second and uses no model,
275
+ so later batches see the new files and the extra fan-out they add. Lookups for the files a ticket
276
+ touches (`--files=`) always read the live history. Commits made outside the pipeline are picked up
277
+ at the next run. It doesn't hook into every commit.
278
+
279
+ `risk.md` is regenerated on every run, so the first run adds it to `.gitignore`, the way you'd ignore
280
+ a build output. Committing it would cause merge conflicts between branches. `doctor` flags it if
281
+ it's committed.
282
+
283
+ When `risk.enabled` is on, the skills use it:
267
284
  - `spec-investigate` digs into high and critical files in the change surface (past fix commits,
268
285
  dependents) and lists them as **Risk Hotspots**;
269
286
  - `spec-create` raises those tasks one tier and requires a test aimed at each file's past failure
@@ -564,8 +581,9 @@ STP-123" or "ship it" picks the right skill.
564
581
  Update to 1.5.1 or later (`npx @benjaminthomas/spec-driven-dev@latest --version`). Earlier versions
565
582
  crashed on Node 14 or 16, which nvm often switches to inside older projects, and wrote a config
566
583
  silently when there was no terminal. Now it runs on Node 14+. With no terminal to answer in (a `!`
567
- command in Claude Code, an editor task, piped input), it stops, changes nothing, and tells you to run
568
- it in a terminal window or pass flags such as `--layout=multi-repo --repos=…`.
584
+ command in Claude Code, an editor task, piped input), it installs the skills and hands the questions
585
+ to your AI tool: say "run spec-setup" there. `spec-driven-dev setup` on its own still needs a
586
+ terminal, or flags such as `--layout=multi-repo --repos=…`.
569
587
 
570
588
  **Setup didn't ask me about repos.**
571
589
  It only asks about repos for a multi-repo or monorepo layout. If it detected `single`, type
@@ -726,16 +744,16 @@ skips that step rather than failing.
726
744
 
727
745
  | Skill | Stage | SKILL.md lines | Words | Supporting files |
728
746
  |---|---|---:|---:|---:|
729
- | `spec-setup` | 0 · configure | 162 | 1,534 | 3 |
730
- | `spec-investigate` | 1 · understand | 370 | 3,152 | 1 |
731
- | `spec-create` | 2 · plan | 195 | 1,746 | 6 |
732
- | `spec-implement` | 3 · build | 322 | 2,683 | 3 |
747
+ | `spec-setup` | 0 · configure | 162 | 1,545 | 3 |
748
+ | `spec-investigate` | 1 · understand | 373 | 3,211 | 1 |
749
+ | `spec-create` | 2 · plan | 198 | 1,782 | 6 |
750
+ | `spec-implement` | 3 · build | 327 | 2,753 | 3 |
733
751
  | `spec-verify` | 4 · prove | 163 | 1,557 | 1 |
734
- | `spec-review` | 5 · review | 135 | 1,270 | 1 |
735
- | `spec-checkpoint` | 6 · commit | 133 | 1,046 | 0 |
752
+ | `spec-review` | 5 · review | 137 | 1,294 | 1 |
753
+ | `spec-checkpoint` | 6 · commit | 140 | 1,144 | 0 |
736
754
  | `spec-ship` | 7 · PR | 230 | 1,388 | 0 |
737
755
  | `generative-seo` | standalone | 255 | 2,209 | 7 |
738
- | **Total** | | **1,965** | **16,585** | **22** |
756
+ | **Total** | | **1,985** | **16,883** | **22** |
739
757
 
740
758
  - **8** skills: **1** setup + **6** workflow stages + **1** standalone
741
759
  - **4** tools supported natively (Claude Code, Codex CLI, Cursor, Antigravity), plus an MCP fallback
@@ -787,7 +805,7 @@ plugin/evals/ `claude plugin eval` cases
787
805
  1. Increase `version` in both `package.json` and `plugin/.claude-plugin/plugin.json` (the check fails if
788
806
  they differ).
789
807
  2. Add a `CHANGELOG.md` entry, then commit and push.
790
- 3. Tag the release and push the tag: `git tag v1.11.0 && git push origin v1.11.0`.
808
+ 3. Tag the release and push the tag: `git tag v1.11.2 && git push origin v1.11.2`.
791
809
  4. Watch the **publish** run in the repo's Actions tab. When it's green, the new version is on
792
810
  npm. Claude Code plugin users get it from the marketplace once the commit is on `main`.
793
811
 
package/bin/cli.mjs CHANGED
@@ -48,13 +48,19 @@ if (flag("version") || flag("v")) {
48
48
  // No terminal to answer in (e.g. `!` in Claude Code, an IDE task, a pipe) and nothing passed explicitly:
49
49
  // stop rather than write a config from guesses.
50
50
  const ANSWER_FLAGS = ["risk", "yes", "y", "layout", "repos", "packages", "tools", "workspace", "after-approval", "skills", "global", "no-setup", "project"];
51
- if (["install", "setup", "settings"].includes(command) && !process.stdin.isTTY && !args.some((a) => ANSWER_FLAGS.some((f) => a === `--${f}` || a.startsWith(`--${f}=`)))) {
51
+ const NO_TTY = !process.stdin.isTTY && !args.some((a) => ANSWER_FLAGS.some((f) => a === `--${f}` || a.startsWith(`--${f}=`)));
52
+ // Installing the skills needs no answers, so the default command still does that without a terminal
53
+ // (e.g. `!` in Claude Code) and hands the questions to the spec-setup skill, which asks them in the chat.
54
+ const HANDOFF = command === "install" && NO_TTY;
55
+ if (["setup", "settings"].includes(command) && NO_TTY) {
52
56
  console.log(`✗ No interactive terminal, so ${command === "settings" ? "the settings menu can't open" : "setup can't ask its questions"}. Nothing was changed.
53
57
 
54
58
  This happens when it runs through another program: a "!" command in Claude Code, an editor task,
55
59
  or piped input. Either:
56
60
  • run it yourself in a terminal window (Terminal, iTerm, or your editor's terminal panel):
57
- npx @benjaminthomas/spec-driven-dev@latest ${command === "install" ? "" : command}
61
+ npx @benjaminthomas/spec-driven-dev@latest ${command}${command === "setup" ? `
62
+ • or, in your AI tool (Claude Code, Cursor, Codex), say "run spec-setup": it asks the same
63
+ questions in the chat` : ""}
58
64
  • or give the answers as flags, for example:
59
65
  npx @benjaminthomas/spec-driven-dev@latest ${command === "settings" ? "settings --risk=off" : "setup --layout=multi-repo --repos=~/Projects/frontend,~/Projects/backend"}
60
66
  (add --yes to accept everything it detects)`);
@@ -137,7 +143,8 @@ async function install() {
137
143
  }
138
144
  console.log(`✓ ${skills.length} skills → ${dest}`);
139
145
  }
140
- if (!dests.length) console.log("• No tools selected — nothing installed.");
146
+ 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;
141
148
  }
142
149
 
143
150
  // ---------- discovery ----------
@@ -593,6 +600,9 @@ function doctor() {
593
600
  if (c.context?.graph && !["auto", "off"].includes(c.context.graph)) problems.push(`context.graph "${c.context.graph}" must be auto or off`);
594
601
  const repoRoots = c.layout === "multi-repo" ? (c.repos || []).map((r) => path.resolve(root, expandHome(r.path))) : [root];
595
602
  checkContext(specs, c.specs_dir || "specs", repoRoots, problems, notes);
603
+ const rmd = path.join(specs, "context", "risk.md"), rtop = run("git", ["rev-parse", "--show-toplevel"], root);
604
+ if (rtop && fs.existsSync(rmd) && run("git", ["ls-files", path.relative(path.resolve(rtop), real(rmd))], rtop))
605
+ 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)`);
596
606
  if (c.risk) {
597
607
  if (c.risk.enabled && !["on", "off"].includes(c.risk.enabled)) problems.push(`risk.enabled "${c.risk.enabled}" must be on or off`);
598
608
  problems.push(...weightProblems(c.risk.weights));
@@ -641,6 +651,19 @@ function context() {
641
651
  }
642
652
  }
643
653
 
654
+ // risk.md is regenerated on every run (it carries a date), so committing it means noisy diffs and merge
655
+ // conflicts between branches. Keep it out of git like a build output; anyone can recreate it in a second.
656
+ function ignoreGenerated(file) {
657
+ const top = run("git", ["rev-parse", "--show-toplevel"], path.dirname(file));
658
+ if (!top) return; // e.g. a multi-repo workspace folder that isn't a repo
659
+ const rel = path.relative(path.resolve(top), real(file)).split(path.sep).join("/");
660
+ if (rel.startsWith("..") || run("git", ["check-ignore", "--no-index", rel], top)) return;
661
+ const gi = path.join(top, ".gitignore");
662
+ const text = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
663
+ fs.writeFileSync(gi, `${text}${text && !text.endsWith("\n") ? "\n" : ""}# GitRisk scores, regenerated by spec-driven-dev risk\n${rel}\n`);
664
+ console.log(` Added ${rel} to .gitignore (it's regenerated on every run, so it shouldn't be committed).`);
665
+ }
666
+
644
667
  function loadConfig() {
645
668
  const file = findConfig(DIR);
646
669
  return { file, root: file ? path.dirname(file) : gitTop(DIR), c: file ? parseYaml(fs.readFileSync(file, "utf8")) : {} };
@@ -660,7 +683,9 @@ function risk() {
660
683
  const norm = (f) => f.replace(/\\/g, "/").replace(/^\.\//, "");
661
684
  const hits = wanted.map((f) => rows.find((r) => r.file === norm(f) || r.file.endsWith("/" + norm(f))) || { file: norm(f), score: null, tier: "no history" });
662
685
  if (flag("json")) console.log(JSON.stringify(hits, null, 2));
663
- else for (const h of hits) console.log(h.score === null ? `${h.file}: no history (new, untracked or not code)` : `${h.file}: ${h.score} ${h.tier} (churn ${h.factors.churn}, bug density ${h.factors.bug_density}, fan-out ${h.factors.fan_out}, security ${h.factors.security})`);
686
+ else for (const h of hits) console.log(h.score === null ? `${h.file}: not scored (not a code file, or doesn't exist)`
687
+ : h.tier === "new" ? `${h.file}: new${h.committed ? "" : ", not committed yet"} — too little history to score.${h.near ? ` Connects to ${h.near.file} (${h.near.score} ${h.near.tier})${["high", "critical"].includes(h.near.tier) ? ": treat it as " + h.near.tier : ""}.` : " Give it standard care."}`
688
+ : `${h.file}: ${h.score} ${h.tier} (churn ${h.factors.churn}, bug density ${h.factors.bug_density}, fan-out ${h.factors.fan_out}, security ${h.factors.security})`);
664
689
  return;
665
690
  }
666
691
  if (flag("json")) { console.log(JSON.stringify({ weights, since, rows }, null, 2)); return; }
@@ -668,10 +693,11 @@ function risk() {
668
693
  fs.mkdirSync(path.join(specs, "context"), { recursive: true });
669
694
  const out = path.join(specs, "context", "risk.md");
670
695
  fs.writeFileSync(out, riskMarkdown({ rows, repos: scored, weights, since, top: Number(opt("top")) || 40 }));
696
+ ignoreGenerated(out);
671
697
  const count = (t) => rows.filter((r) => r.tier === t).length;
672
698
  console.log(`✓ Scored ${rows.length} files from ${scored.reduce((t, s) => t + s.commits, 0)} commits → ${path.relative(DIR, out) || out}`);
673
- console.log(` ${count("critical")} critical · ${count("high")} high · ${count("medium")} medium. Top 5:`);
674
- for (const r of rows.slice(0, 5)) console.log(` ${String(r.score).padStart(5)} ${r.tier.padEnd(8)} ${r.file}`);
699
+ console.log(` ${count("critical")} critical · ${count("high")} high · ${count("medium")} medium · ${count("new")} new. Top 5:`);
700
+ for (const r of rows.filter((x) => x.tier !== "new").slice(0, 5)) console.log(` ${String(r.score).padStart(5)} ${r.tier.padEnd(8)} ${r.file}`);
675
701
  if (c.risk?.enabled === "off") console.log(" Note: risk.enabled is off in .spec-driven.yml, so the skills won't use this until you turn it on.");
676
702
  }
677
703
 
@@ -769,12 +795,20 @@ try {
769
795
  else if (command === "risk") risk();
770
796
  else if (command === "settings") await settings();
771
797
  else if (command === "risk-eval") await riskEvalCmd();
772
- else if (command === "install") {
798
+ else if (command === "install" && HANDOFF) {
799
+ console.log("No interactive terminal (for example a \"!\" command in Claude Code), so this installs the skills\nwith the detected defaults and leaves the setup questions to your AI tool.\n");
800
+ const n = await install();
801
+ if (n) console.log(`
802
+ Next: in your AI tool, say "run spec-setup". It asks the setup questions in the chat
803
+ (single repo, monorepo or multi-repo, tracker, branches) and writes .spec-driven.yml.
804
+ If your AI tool doesn't recognise it yet, start a new session so it loads the new skills.
805
+ To answer in a terminal instead, open Terminal or iTerm and run: npx @benjaminthomas/spec-driven-dev setup`);
806
+ } else if (command === "install") {
773
807
  await install();
774
808
  if (!flag("no-setup") && (await askConfirm("Set up this project now?", true))) await setup();
775
809
  } else if (command === "setup") await setup();
776
810
  else throw new Error(`Unknown command "${command}" — use install (default), setup, settings, doctor, status, context, risk, or risk-eval`);
777
- if (["install", "setup"].includes(command)) console.log('\nNext: in your AI tool, say "investigate <ticket>" or "create a spec".');
811
+ if (["install", "setup"].includes(command) && !HANDOFF) console.log('\nNext: in your AI tool, say "investigate <ticket>" or "create a spec".');
778
812
  } catch (e) {
779
813
  console.error(`✗ ${e.message}`);
780
814
  process.exitCode = 1;
package/bin/risk.mjs CHANGED
@@ -7,12 +7,13 @@ import { execFileSync } from "node:child_process";
7
7
 
8
8
  export const DEFAULT_WEIGHTS = { churn: 0.3, bug_density: 0.35, fan_out: 0.2, security: 0.15 };
9
9
  export const TIERS = [[75, "critical"], [50, "high"], [25, "medium"], [0, "low"]];
10
+ const NEW_DAYS = 90; // a file added within this many days with fewer than MIN_COMMITS commits (or not committed yet) has too little history to score fairly
10
11
  const MIN_COMMITS = 3; // shortcut: a file with 1 commit that was a fix would score 100% bug density; divide by at least 3. Replace with Bayesian smoothing if calibration shows it matters.
11
12
 
12
13
  // Classification uses the conventional-commit type when the repo uses one, and message keywords otherwise.
13
14
  const BUG = /^(fix|hotfix|bugfix|revert)(\(.+\))?!?:|\b(fix(e[sd]|ing)?|bug|hotfix|regression|broken|crash(es|ed)?|patch(ed)?|defect)\b/i;
14
15
  const SECURITY = /\b(security|cve-\d+|xss|csrf|ssrf|sqli|injection|vuln(erability)?|exploit|sanitiz(e|ation)|escap(e|ing) (html|input|output)|auth(entication|orization)? (bypass|fix)|privilege|secret(s)? (leak|exposed)|leak(ed|s)? (token|key|secret|credential)|rate.?limit|helmet|csp)\b/i;
15
- const SKIP = /(^|\/)(node_modules|dist|build|out|vendor|coverage|\.next|graphify-out|specs)\/|\.(md|mdx|txt|lock|log|png|jpe?g|gif|svg|ico|webp|pdf|zip|map|snap|csv)$|(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|go\.sum|poetry\.lock|composer\.lock)$/i;
16
+ const SKIP = /(^|\/)(node_modules|dist|build|out|vendor|coverage|\.next|graphify-out|specs|experiments)\/|(^|\/)(\.spec-driven\.yml|\.gitignore|\.gitattributes|\.gitmodules)$|\.(md|mdx|txt|lock|log|png|jpe?g|gif|svg|ico|webp|pdf|zip|map|snap|csv)$|(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|go\.sum|poetry\.lock|composer\.lock)$/i;
16
17
  const CODE = /\.(m?[jt]sx?|cjs|py|rb|go|rs|java|kt|php|cs|swift|scala|vue|svelte)$/i;
17
18
 
18
19
  const git = (cwd, args) => {
@@ -50,13 +51,25 @@ function fanOut(repo, files) {
50
51
  }
51
52
  for (const t of targets) if (t !== f) dependents.get(t).add(f);
52
53
  }
53
- return new Map([...dependents].map(([f, s]) => [f, s.size]));
54
+ const imports = new Map(files.map((f) => [f, new Set()]));
55
+ for (const [t, srcs] of dependents) for (const f of srcs) imports.get(f).add(t);
56
+ return { counts: new Map([...dependents].map(([f, s]) => [f, s.size])), dependents, imports };
54
57
  }
55
58
 
56
- // Score every tracked code file in one repo. `prefix` labels files in multi-repo workspaces.
59
+ // Score every code file in one repo, including new files not committed yet. `prefix` labels files in multi-repo workspaces.
57
60
  export function scoreRepo(repo, { since = "2 years ago", weights = DEFAULT_WEIGHTS, prefix = "" } = {}) {
58
- const files = git(repo, ["ls-files"]).split("\n").filter((f) => f && !SKIP.test(f));
61
+ const committed = git(repo, ["ls-files"]).split("\n").filter((f) => f && !SKIP.test(f));
62
+ const untracked = git(repo, ["ls-files", "--others", "--exclude-standard"]).split("\n").filter((f) => f && !SKIP.test(f));
63
+ const files = [...committed, ...untracked];
59
64
  const tracked = new Set(files);
65
+ // When each file was first added (all history, not just `since`), to tell new files from quiet old ones.
66
+ const added = new Map();
67
+ for (const block of git(repo, ["log", "--diff-filter=A", "--format=%x1e%ct", "--name-only"]).split("\x1e").slice(1)) {
68
+ const [ts, ...names] = block.split("\n");
69
+ for (const f of names) if (f) added.set(f, Number(ts)); // log is newest first, so the last write is the first add
70
+ }
71
+ const cutoff = Date.now() / 1000 - NEW_DAYS * 86400;
72
+ const recent = (f) => !added.has(f) || added.get(f) >= cutoff;
60
73
  const stats = new Map(files.map((f) => [f, { commits: 0, bug: 0, security: 0 }]));
61
74
  let commits = 0;
62
75
  // \x1e starts each commit; the subject follows, then the changed file names.
@@ -72,7 +85,9 @@ export function scoreRepo(repo, { since = "2 years ago", weights = DEFAULT_WEIGH
72
85
  if (c.security) s.security++;
73
86
  }
74
87
  }
75
- const fan = fanOut(repo, files);
88
+ // "New" = recent AND too few commits to judge. A young file that's already been fixed a lot has real history.
89
+ const isNew = new Set(files.filter((f) => untracked.includes(f) || (recent(f) && stats.get(f).commits < MIN_COMMITS)));
90
+ const graph = fanOut(repo, files), fan = graph.counts;
76
91
  const expChurn = p90([...stats.values()].map((s) => s.commits)), expFan = p90([...fan.values()]);
77
92
  const rows = files.map((f) => {
78
93
  const s = stats.get(f), denom = Math.max(s.commits, MIN_COMMITS);
@@ -84,9 +99,20 @@ export function scoreRepo(repo, { since = "2 years ago", weights = DEFAULT_WEIGH
84
99
  };
85
100
  const score = Object.keys(DEFAULT_WEIGHTS).reduce((t, k) => t + (Number(weights[k]) || 0) * factors[k], 0);
86
101
  const r = (x) => Math.round(x * 10) / 10;
87
- return { file: prefix ? `${prefix}/${f}` : f, score: r(score), tier: tierOf(score), commits: s.commits, bug_fixes: s.bug, security_fixes: s.security, dependents: fan.get(f) || 0,
102
+ return { file: prefix ? `${prefix}/${f}` : f, path: f, score: r(score), tier: tierOf(score), commits: s.commits, bug_fixes: s.bug, security_fixes: s.security, dependents: fan.get(f) || 0,
88
103
  factors: { churn: r(factors.churn), bug_density: r(factors.bug_density), fan_out: r(factors.fan_out), security: r(factors.security) } };
89
104
  });
105
+ // New files: too little history for a fair score, so label them "new" and show the riskiest established
106
+ // file they import or are imported by. New code next to a hotspot deserves the hotspot's care.
107
+ const byPath = new Map(rows.map((x) => [x.path, x]));
108
+ for (const x of rows) {
109
+ if (!isNew.has(x.path)) continue;
110
+ x.tier = "new";
111
+ x.committed = !untracked.includes(x.path);
112
+ const near = [...graph.imports.get(x.path), ...graph.dependents.get(x.path)].map((n) => byPath.get(n)).filter((n) => n && !isNew.has(n.path)).sort((a, b) => b.score - a.score)[0];
113
+ if (near) x.near = { file: near.file, score: near.score, tier: near.tier };
114
+ }
115
+ for (const x of rows) delete x.path;
90
116
  return { commits, head: git(repo, ["rev-parse", "--short", "HEAD"]).trim(), rows };
91
117
  }
92
118
 
@@ -110,7 +136,9 @@ export function weightProblems(w) {
110
136
  export function riskMarkdown({ rows, repos, weights, since, top }) {
111
137
  const pct = (x) => `${Math.round(x * 100)}%`;
112
138
  const w = weights;
113
- const shown = rows.filter((r) => r.score > 0).slice(0, Math.min(top, 120));
139
+ const fresh = rows.filter((r) => r.tier === "new");
140
+ const freshShown = fresh.filter((r) => r.near).sort((a, b) => b.near.score - a.near.score).slice(0, 15);
141
+ const shown = rows.filter((r) => r.score > 0 && r.tier !== "new").slice(0, Math.min(top, 120 - freshShown.length));
114
142
  const lines = [
115
143
  "# Risk",
116
144
  "<!-- Generated by `spec-driven-dev risk`. Don't edit by hand: re-run it. Skills read this only when risk.enabled is on. -->",
@@ -123,7 +151,11 @@ export function riskMarkdown({ rows, repos, weights, since, top }) {
123
151
  "|---|---|---|---|---|---|---|---|",
124
152
  ...shown.map((r) => `| \`${r.file}\` | ${r.score} | ${r.tier} | ${r.factors.churn} | ${r.factors.bug_density} | ${r.factors.fan_out} | ${r.factors.security} | ${r.commits} (${r.bug_fixes} / ${r.security_fixes}) |`),
125
153
  ];
126
- if (rows.length > shown.length) lines.push("", `${rows.length - shown.length} more files not shown. \`spec-driven-dev risk --files=a,b\` prints any file's score.`);
154
+ if (fresh.length) {
155
+ lines.push("", `## New files (${fresh.length})`, "", `Added in the last ${NEW_DAYS} days with fewer than ${MIN_COMMITS} commits, or not committed yet, so there's too little history to score. Give them standard care, and the care of the riskiest file they connect to when that file is high or critical.`);
156
+ if (freshShown.length) lines.push("", "| New file | Connects to | Its score / tier |", "|---|---|---|", ...freshShown.map((r) => `| \`${r.file}\` | \`${r.near.file}\` | ${r.near.score} ${r.near.tier} |`));
157
+ }
158
+ if (rows.length > shown.length + freshShown.length) lines.push("", `${rows.length - shown.length - freshShown.length} more files not shown. \`spec-driven-dev risk --files=a,b\` prints any file's score.`);
127
159
  return lines.join("\n") + "\n";
128
160
  }
129
161
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benjaminthomas/spec-driven-dev",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
4
4
  "description": "Spec-driven AI development workflow (investigate → spec → implement → verify → review → ship) as Agent Skills for Claude Code, Cursor, Codex CLI and Antigravity.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticket-to-pr",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
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",
@@ -66,9 +66,16 @@ Skip this step unless the changes belong to a spec (`{specs_dir}/{feature}/` exi
66
66
  one line to CONTEXT.md's **Specs** list. Follow the rules in
67
67
  references/context-template.md inside the installed `spec-setup` skill folder (not this repo): dated, with a source, added at the end, within the size
68
68
  limits, superseded lines moved to `context/archive.md`.
69
+ - **New areas and files.** Compare `git diff --name-status --diff-filter=A <base>...HEAD` with
70
+ CONTEXT.md's **Areas** table. Add a row for any new folder that holds a real area (not a
71
+ single helper), and one line per important new file (an entry point, a shared module others
72
+ will import) to its area file. Stay within the size limits.
73
+ - **Areas that grew.** If the feature changed what an existing area does (new commands,
74
+ responsibilities, or sub-folders), update that row's "What it does" in **Areas** so the
75
+ map stays true. Keep it to one line.
69
76
  - Show the context changes in the report so the user can correct them.
70
77
  3. If `risk.enabled` is on and the CLI is available, re-run `spec-driven-dev risk` so the scores
71
- include this work. It's git-only, so it costs no tokens.
78
+ include this work. It's git-only, so it costs no tokens. `risk.md` is gitignored: don't stage it.
72
79
  4. No CONTEXT.md? Skip the distilling and suggest running `spec-setup` to create one.
73
80
 
74
81
  ### Step 3: Stage Everything
@@ -57,6 +57,9 @@ creates or modifies a **high** or **critical** file (from the investigation's **
57
57
  - Its **Tests** must include at least one test aimed at that past failure pattern, not just the new
58
58
  behaviour.
59
59
 
60
+ A task that creates a **new** file next to a high or critical one (it imports it, or the hotspot will
61
+ import it) gets the same treatment, because the new code inherits that file's failure modes.
62
+
60
63
  ## When to Use
61
64
 
62
65
  - After a planning conversation where requirements and technical details have been discussed
@@ -225,6 +225,11 @@ After the batch passes review (or the user chooses to proceed):
225
225
  2. **Update README.md**: change `- [ ]` to `- [x]` for each completed task
226
226
  3. **Update memory**: append a log entry (what this batch decided or changed from the spec) and
227
227
  overwrite the README's **Resume here** (`Stage: batch {N}/{total} done`, `Next: …`).
228
+ **Refresh risk** after the commit when `risk.enabled` is on and the CLI is available: run
229
+ `spec-driven-dev risk`. It takes well under a second with no model. New files from this batch
230
+ show as `new`, and files they import gain fan-out, so later batches and the reviewers see
231
+ current scores, not the ones from before this feature started. `risk.md` is gitignored (the
232
+ command adds it), so never stage or commit it.
228
233
  4. **Commit the batch**:
229
234
  ```
230
235
  git add -A
@@ -25,7 +25,7 @@ You are reviewing the combined output of Batch {batch_number} of a feature imple
25
25
  - No duplicate or conflicting definitions
26
26
  - Shared state (env vars, config) is consistent
27
27
  4. **Code Quality**: Check for security issues, performance problems, and adherence to project conventions.
28
- 5. **Risk**: for every task with a filled **Risk** section, check the code handles the past failure pattern it names, and that the test aimed at it exists and would fail without the fix.
28
+ 5. **Risk**: new files show as tier `new`, which means unproven, not safe; review them at standard care, or at the tier of the hotspot they connect to. For every task with a filled **Risk** section, check the code handles the past failure pattern it names, and that the test aimed at it exists and would fail without the fix.
29
29
  6. **Scope**: Verify each task stayed within its defined scope (Files to Create/Modify). Flag any unexpected file changes.
30
30
 
31
31
  ## Verdict
@@ -80,6 +80,9 @@ clean.
80
80
  breaking there in the report's **Risk Hotspots** section.
81
81
  3. A score is a reason to look harder, never evidence. Hotspot findings still need their
82
82
  VERIFIED / INFERRED labels.
83
+ 4. **New files** (added in the last 90 days with fewer than 3 commits, or not committed yet) show as tier `new`: too little
84
+ history to score, which is not the same as safe. Give them standard care. When one connects to a
85
+ high or critical file (`Connects to …` in the lookup), treat it at that file's tier.
83
86
 
84
87
  ## Invocation
85
88
 
@@ -77,7 +77,9 @@ review on a weaker model than the code was written with misses things.
77
77
  **Risk.** When `risk.enabled` is on and `{specs_dir}/context/risk.md` exists, look up the changed
78
78
  files (`spec-driven-dev risk --files=…`). Give **high** and **critical** files a line-by-line read
79
79
  of their error paths and callers, check that the tests target their past failure patterns, and
80
- put the score in any finding on them. When risk is `off`, skip this and don't mention it.
80
+ put the score in any finding on them. New files show as tier `new`: review them at standard care,
81
+ or at the tier of the high or critical file they connect to. When risk is `off`, skip this and
82
+ don't mention it.
81
83
  4. **Run the checks.**4. **Run the checks.** Run `commands.lint`, `typecheck`, and `test` once if verify didn't just run
82
84
  them on this exact commit. A failing check is a blocker.
83
85
 
@@ -120,7 +120,7 @@ Skip if `{specs_dir}/CONTEXT.md` already exists (offer a refresh instead, see `r
120
120
  Keep the index to 60 lines and area files to 150.
121
121
  3. **Risk scores.** For an existing project, run `spec-driven-dev risk` (or
122
122
  `npx -y @benjaminthomas/spec-driven-dev risk`) to write `{specs_dir}/context/risk.md`. It uses
123
- git only, no model. Skip it for a new project with no history. Mention the hotspot count in the
123
+ 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
124
  report. Set `risk.enabled: off` only if the user doesn't want it.
125
125
  4. **Offer, don't do:** ask whether to add this one line to `CLAUDE.md` / `AGENTS.md` (whichever
126
126
  the project uses), so hosts reload the pointer after compaction and in every new session: