@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 +24 -9
- package/bin/cli.mjs +22 -3
- package/bin/risk.mjs +40 -8
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/spec-checkpoint/SKILL.md +8 -1
- package/plugin/skills/spec-create/SKILL.md +3 -0
- package/plugin/skills/spec-implement/SKILL.md +5 -0
- package/plugin/skills/spec-implement/references/review-prompt-template.md +1 -1
- package/plugin/skills/spec-investigate/SKILL.md +3 -0
- package/plugin/skills/spec-review/SKILL.md +3 -1
- package/plugin/skills/spec-setup/SKILL.md +1 -1
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.
|
|
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,
|
|
730
|
-
| `spec-investigate` | 1 · understand |
|
|
731
|
-
| `spec-create` | 2 · plan |
|
|
732
|
-
| `spec-implement` | 3 · build |
|
|
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 |
|
|
735
|
-
| `spec-checkpoint` | 6 · commit |
|
|
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,
|
|
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.
|
|
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}:
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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.
|
|
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.
|
|
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**:
|
|
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.
|
|
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:
|