@benjaminthomas/spec-driven-dev 1.11.0 → 1.11.1

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
@@ -263,7 +263,22 @@ Risk = 0.30 × Churn + 0.35 × Bug Density + 0.20 × Fan-out + 0.15 × Security
263
263
  | Fan-out | 20% | how many other files import it, against the repo's 90th percentile |
264
264
  | Security | 15% | share of its commits that were security fixes (security, CVE, XSS, injection…) |
265
265
 
266
- Tiers: critical ≥ 75, high ≥ 50, medium ≥ 25. When `risk.enabled` is on, the skills use it:
266
+ Tiers: critical ≥ 75, high ≥ 50, medium ≥ 25. Files added in the last 90 days with fewer than 3
267
+ commits, or not committed yet, are labelled **new** instead of being scored, because too little history isn't the same as
268
+ safe. Each new file shows the riskiest file it imports or is imported by, and inherits that file's
269
+ care when it's high or critical.
270
+
271
+ **When it updates:** setup scores the whole history once. After that it re-scores at every batch
272
+ commit in `spec-implement` and at `spec-checkpoint`. Each run takes under a second and uses no model,
273
+ so later batches see the new files and the extra fan-out they add. Lookups for the files a ticket
274
+ touches (`--files=`) always read the live history. Commits made outside the pipeline are picked up
275
+ at the next run. It doesn't hook into every commit.
276
+
277
+ `risk.md` is regenerated on every run, so the first run adds it to `.gitignore`, the way you'd ignore
278
+ a build output. Committing it would cause merge conflicts between branches. `doctor` flags it if
279
+ it's committed.
280
+
281
+ When `risk.enabled` is on, the skills use it:
267
282
  - `spec-investigate` digs into high and critical files in the change surface (past fix commits,
268
283
  dependents) and lists them as **Risk Hotspots**;
269
284
  - `spec-create` raises those tasks one tier and requires a test aimed at each file's past failure
@@ -726,16 +741,16 @@ skips that step rather than failing.
726
741
 
727
742
  | Skill | Stage | SKILL.md lines | Words | Supporting files |
728
743
  |---|---|---:|---:|---:|
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 |
744
+ | `spec-setup` | 0 · configure | 162 | 1,545 | 3 |
745
+ | `spec-investigate` | 1 · understand | 373 | 3,211 | 1 |
746
+ | `spec-create` | 2 · plan | 198 | 1,782 | 6 |
747
+ | `spec-implement` | 3 · build | 327 | 2,753 | 3 |
733
748
  | `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 |
749
+ | `spec-review` | 5 · review | 137 | 1,294 | 1 |
750
+ | `spec-checkpoint` | 6 · commit | 140 | 1,144 | 0 |
736
751
  | `spec-ship` | 7 · PR | 230 | 1,388 | 0 |
737
752
  | `generative-seo` | standalone | 255 | 2,209 | 7 |
738
- | **Total** | | **1,965** | **16,585** | **22** |
753
+ | **Total** | | **1,985** | **16,883** | **22** |
739
754
 
740
755
  - **8** skills: **1** setup + **6** workflow stages + **1** standalone
741
756
  - **4** tools supported natively (Claude Code, Codex CLI, Cursor, Antigravity), plus an MCP fallback
@@ -787,7 +802,7 @@ plugin/evals/ `claude plugin eval` cases
787
802
  1. Increase `version` in both `package.json` and `plugin/.claude-plugin/plugin.json` (the check fails if
788
803
  they differ).
789
804
  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`.
805
+ 3. Tag the release and push the tag: `git tag v1.11.1 && git push origin v1.11.1`.
791
806
  4. Watch the **publish** run in the repo's Actions tab. When it's green, the new version is on
792
807
  npm. Claude Code plugin users get it from the marketplace once the commit is on `main`.
793
808
 
package/bin/cli.mjs CHANGED
@@ -593,6 +593,9 @@ function doctor() {
593
593
  if (c.context?.graph && !["auto", "off"].includes(c.context.graph)) problems.push(`context.graph "${c.context.graph}" must be auto or off`);
594
594
  const repoRoots = c.layout === "multi-repo" ? (c.repos || []).map((r) => path.resolve(root, expandHome(r.path))) : [root];
595
595
  checkContext(specs, c.specs_dir || "specs", repoRoots, problems, notes);
596
+ const rmd = path.join(specs, "context", "risk.md"), rtop = run("git", ["rev-parse", "--show-toplevel"], root);
597
+ if (rtop && fs.existsSync(rmd) && run("git", ["ls-files", path.relative(path.resolve(rtop), real(rmd))], rtop))
598
+ 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
599
  if (c.risk) {
597
600
  if (c.risk.enabled && !["on", "off"].includes(c.risk.enabled)) problems.push(`risk.enabled "${c.risk.enabled}" must be on or off`);
598
601
  problems.push(...weightProblems(c.risk.weights));
@@ -641,6 +644,19 @@ function context() {
641
644
  }
642
645
  }
643
646
 
647
+ // risk.md is regenerated on every run (it carries a date), so committing it means noisy diffs and merge
648
+ // conflicts between branches. Keep it out of git like a build output; anyone can recreate it in a second.
649
+ function ignoreGenerated(file) {
650
+ const top = run("git", ["rev-parse", "--show-toplevel"], path.dirname(file));
651
+ if (!top) return; // e.g. a multi-repo workspace folder that isn't a repo
652
+ const rel = path.relative(path.resolve(top), real(file)).split(path.sep).join("/");
653
+ if (rel.startsWith("..") || run("git", ["check-ignore", "--no-index", rel], top)) return;
654
+ const gi = path.join(top, ".gitignore");
655
+ const text = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
656
+ fs.writeFileSync(gi, `${text}${text && !text.endsWith("\n") ? "\n" : ""}# GitRisk scores, regenerated by spec-driven-dev risk\n${rel}\n`);
657
+ console.log(` Added ${rel} to .gitignore (it's regenerated on every run, so it shouldn't be committed).`);
658
+ }
659
+
644
660
  function loadConfig() {
645
661
  const file = findConfig(DIR);
646
662
  return { file, root: file ? path.dirname(file) : gitTop(DIR), c: file ? parseYaml(fs.readFileSync(file, "utf8")) : {} };
@@ -660,7 +676,9 @@ function risk() {
660
676
  const norm = (f) => f.replace(/\\/g, "/").replace(/^\.\//, "");
661
677
  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
678
  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})`);
679
+ else for (const h of hits) console.log(h.score === null ? `${h.file}: not scored (not a code file, or doesn't exist)`
680
+ : 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."}`
681
+ : `${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
682
  return;
665
683
  }
666
684
  if (flag("json")) { console.log(JSON.stringify({ weights, since, rows }, null, 2)); return; }
@@ -668,10 +686,11 @@ function risk() {
668
686
  fs.mkdirSync(path.join(specs, "context"), { recursive: true });
669
687
  const out = path.join(specs, "context", "risk.md");
670
688
  fs.writeFileSync(out, riskMarkdown({ rows, repos: scored, weights, since, top: Number(opt("top")) || 40 }));
689
+ ignoreGenerated(out);
671
690
  const count = (t) => rows.filter((r) => r.tier === t).length;
672
691
  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}`);
692
+ console.log(` ${count("critical")} critical · ${count("high")} high · ${count("medium")} medium · ${count("new")} new. Top 5:`);
693
+ 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
694
  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
695
  }
677
696
 
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.1",
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.1",
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: