@benjaminthomas/spec-driven-dev 1.14.1 → 1.15.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 +18 -3
- package/bin/cli.mjs +120 -4
- package/bin/risk.mjs +15 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/spec-setup/SKILL.md +64 -13
- package/plugin/skills/spec-setup/references/audit.md +2 -1
- package/plugin/skills/spec-setup/references/context-template.md +10 -0
- package/plugin/skills/spec-setup/references/repo-picker.md +5 -0
package/README.md
CHANGED
|
@@ -294,6 +294,20 @@ Off means every skill ignores risk completely.
|
|
|
294
294
|
hypothesis: change them under `risk.weights` in `.spec-driven.yml` once you've backtested them
|
|
295
295
|
against your own past bugs.
|
|
296
296
|
|
|
297
|
+
**Keep the hotspots current on every merge (GitHub Actions).** With a dedicated specs repo on GitHub:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
spec-driven-dev ci --notify
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
This writes `.github/workflows/gitrisk.yml` into the specs repo, plus, with `--notify`, a small
|
|
304
|
+
`gitrisk-notify.yml` into each code repo on your machine. When something lands on a repo's base
|
|
305
|
+
branch, it pings the specs repo, which checks out every repo in `team-repos.yml` with full history,
|
|
306
|
+
runs `risk`, and commits the **Risk hotspots** in `CONTEXT.md` only when they changed. A date-only
|
|
307
|
+
change isn't committed. It also runs nightly, and on demand from the Actions tab. You add one secret,
|
|
308
|
+
`GITRISK_TOKEN` (read access to the code repos, write access to the specs repo), and commit the
|
|
309
|
+
workflow files yourself. The command prints the exact steps.
|
|
310
|
+
|
|
297
311
|
**Does it change what the agent does?** `spec-driven-dev risk-eval --tickets=tickets.md` runs each
|
|
298
312
|
ticket twice, in fresh clones with risk on and off, through investigate and create on the same
|
|
299
313
|
model. It then compares what the agent did:
|
|
@@ -646,6 +660,7 @@ spec-driven-dev doctor # check .spec-driven.yml again
|
|
|
646
660
|
spec-driven-dev status # list unfinished specs and each one's next step
|
|
647
661
|
spec-driven-dev context --areas=billing # print the project memory (index, or chosen areas)
|
|
648
662
|
spec-driven-dev settings # switch GitRisk, run-through and sharing from a menu
|
|
663
|
+
spec-driven-dev ci --notify # GitHub Actions: refresh GitRisk hotspots on every merge
|
|
649
664
|
spec-driven-dev uninstall # remove the skills, plugin and cache (--all: settings too)
|
|
650
665
|
spec-driven-dev risk # GitRisk: score files from git history → specs/context/risk.md
|
|
651
666
|
spec-driven-dev risk-eval --tickets=tickets.md # compare agent behaviour with the risk signal on vs off
|
|
@@ -760,7 +775,7 @@ skips that step rather than failing.
|
|
|
760
775
|
|
|
761
776
|
| Skill | Stage | SKILL.md lines | Words | Supporting files |
|
|
762
777
|
|---|---|---:|---:|---:|
|
|
763
|
-
| `spec-setup` | 0 · configure |
|
|
778
|
+
| `spec-setup` | 0 · configure | 273 | 2,829 | 4 |
|
|
764
779
|
| `spec-investigate` | 1 · understand | 387 | 3,367 | 1 |
|
|
765
780
|
| `spec-create` | 2 · plan | 198 | 1,782 | 6 |
|
|
766
781
|
| `spec-implement` | 3 · build | 327 | 2,753 | 3 |
|
|
@@ -769,7 +784,7 @@ skips that step rather than failing.
|
|
|
769
784
|
| `spec-checkpoint` | 6 · commit | 140 | 1,144 | 0 |
|
|
770
785
|
| `spec-ship` | 7 · PR | 230 | 1,388 | 0 |
|
|
771
786
|
| `generative-seo` | standalone | 255 | 2,209 | 7 |
|
|
772
|
-
| **Total** | | **2,
|
|
787
|
+
| **Total** | | **2,110** | **18,323** | **23** |
|
|
773
788
|
|
|
774
789
|
- **8** skills: **1** setup + **6** workflow stages + **1** standalone
|
|
775
790
|
- **4** tools supported natively (Claude Code, Codex CLI, Cursor, Antigravity), plus an MCP fallback
|
|
@@ -822,7 +837,7 @@ plugin/evals/ `claude plugin eval` cases
|
|
|
822
837
|
1. Increase `version` in both `package.json` and `plugin/.claude-plugin/plugin.json` (the check fails if
|
|
823
838
|
they differ).
|
|
824
839
|
2. Add a `CHANGELOG.md` entry, then commit and push.
|
|
825
|
-
3. Tag the release and push the tag: `git tag v1.
|
|
840
|
+
3. Tag the release and push the tag: `git tag v1.15.0 && git push origin v1.15.0`.
|
|
826
841
|
4. Watch the **publish** run in the repo's Actions tab. When it's green, the new version is on
|
|
827
842
|
npm. Claude Code plugin users get it from the marketplace once the commit is on `main`.
|
|
828
843
|
|
package/bin/cli.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// spec-driven-dev install the skills, then hand over to your AI tool ("run spec-setup")
|
|
6
6
|
// spec-driven-dev setup scripted setup for CI: needs --yes or answer flags (people: say "run spec-setup")
|
|
7
|
+
// spec-driven-dev ci write GitHub Actions that refresh GitRisk hotspots on every merge (--notify also writes the per-repo trigger)
|
|
7
8
|
// spec-driven-dev uninstall remove the skills (all tools), the Claude Code plugin and the download cache; --all also removes the settings file
|
|
8
9
|
// spec-driven-dev doctor check .spec-driven.yml against the real repos (exit 1 on problems)
|
|
9
10
|
// spec-driven-dev status list specs with unfinished work and each one's "Resume here" next step
|
|
@@ -20,7 +21,7 @@ import path from "node:path";
|
|
|
20
21
|
import { execFileSync } from "node:child_process";
|
|
21
22
|
import prompts from "prompts"; // arrow-key menus that work in PowerShell, cmd, macOS and Linux terminals
|
|
22
23
|
import { fileURLToPath } from "node:url";
|
|
23
|
-
import { scoreRepo, riskMarkdown, normWeights, weightProblems, setConfigValue } from "./risk.mjs";
|
|
24
|
+
import { scoreRepo, riskMarkdown, normWeights, weightProblems, setConfigValue, riskSummaryBlock, withRiskSummary } from "./risk.mjs";
|
|
24
25
|
import { SHARE_MODES, shareState, planShare, shareTitle } from "./share.mjs";
|
|
25
26
|
|
|
26
27
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
@@ -40,7 +41,7 @@ const args = process.argv.slice(2);
|
|
|
40
41
|
const flag = (n) => args.includes(`--${n}`);
|
|
41
42
|
const opt = (n) => args.find((a) => a.startsWith(`--${n}=`))?.split("=")[1];
|
|
42
43
|
const command = args.find((a) => !a.startsWith("-")) || "install";
|
|
43
|
-
const YES = flag("yes") || flag("y") || !process.stdin.isTTY || ["doctor", "status", "context", "risk", "risk-eval", "setup"].includes(command) || (command === "uninstall" && flag("yes"));
|
|
44
|
+
const YES = flag("yes") || flag("y") || !process.stdin.isTTY || ["doctor", "status", "context", "risk", "risk-eval", "setup", "ci"].includes(command) || (command === "uninstall" && flag("yes"));
|
|
44
45
|
const DIR = path.resolve(opt("dir") || process.cwd());
|
|
45
46
|
|
|
46
47
|
// Colour only for a person at a terminal; pipes, CI and NO_COLOR get plain text (and the tests stay plain).
|
|
@@ -794,7 +795,10 @@ function loadConfig() {
|
|
|
794
795
|
}
|
|
795
796
|
|
|
796
797
|
function risk() {
|
|
797
|
-
const
|
|
798
|
+
const loaded = loadConfig();
|
|
799
|
+
const { root } = loaded;
|
|
800
|
+
// CI has no personal .spec-driven.yml: --repos and --specs-dir describe the checkout instead.
|
|
801
|
+
const c = opt("repos") ? { ...loaded.c, layout: "multi-repo", repos: splitList(opt("repos")).map((p) => ({ path: p })), specs_dir: opt("specs-dir") || loaded.c.specs_dir } : loaded.c;
|
|
798
802
|
const bad = weightProblems(c.risk?.weights);
|
|
799
803
|
if (bad.length) throw new Error(bad.join("; "));
|
|
800
804
|
const weights = normWeights(c.risk?.weights), since = opt("since") || c.risk?.since || "2 years ago";
|
|
@@ -818,6 +822,11 @@ function risk() {
|
|
|
818
822
|
const out = path.join(specs, "context", "risk.md");
|
|
819
823
|
fs.writeFileSync(out, riskMarkdown({ rows, repos: scored, weights, since, top: Number(opt("top")) || 40 }));
|
|
820
824
|
ignoreGenerated(out);
|
|
825
|
+
const ctx = path.join(specs, "CONTEXT.md");
|
|
826
|
+
if (fs.existsSync(ctx)) {
|
|
827
|
+
fs.writeFileSync(ctx, withRiskSummary(fs.readFileSync(ctx, "utf8"), riskSummaryBlock(rows, new Date().toISOString().slice(0, 10))));
|
|
828
|
+
console.log(` Updated the "Risk hotspots" summary in ${path.relative(DIR, ctx) || ctx}, which is shared with the team.`);
|
|
829
|
+
}
|
|
821
830
|
const count = (t) => rows.filter((r) => r.tier === t).length;
|
|
822
831
|
console.log(`✓ Scored ${rows.length} files from ${scored.reduce((t, s) => t + s.commits, 0)} commits → ${path.relative(DIR, out) || out}`);
|
|
823
832
|
console.log(` ${count("critical")} critical · ${count("high")} high · ${count("medium")} medium · ${count("new")} new. Top 5:`);
|
|
@@ -933,6 +942,112 @@ async function uninstall() {
|
|
|
933
942
|
console.log("Done. Reinstall any time with: npx @benjaminthomas/spec-driven-dev@latest");
|
|
934
943
|
}
|
|
935
944
|
|
|
945
|
+
// ---------- CI: refresh GitRisk hotspots on merge ----------
|
|
946
|
+
function ghSlug(url) { return (String(url || "").match(/github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/) || [])[1] || ""; }
|
|
947
|
+
|
|
948
|
+
function ciFiles({ specsSlug, repos, version }) {
|
|
949
|
+
const checkouts = repos.map((r) => ` - uses: actions/checkout@v4
|
|
950
|
+
with:
|
|
951
|
+
repository: ${r.slug}
|
|
952
|
+
path: ${r.name}
|
|
953
|
+
fetch-depth: 0 # full history: GitRisk reads git log
|
|
954
|
+
token: \${{ secrets.GITRISK_TOKEN }}`).join("\n");
|
|
955
|
+
const specs = `# Refreshes the "Risk hotspots" block in CONTEXT.md from every team repo's git history.
|
|
956
|
+
# Written by \`spec-driven-dev ci\`. Re-run that command after adding a repo to team-repos.yml.
|
|
957
|
+
# Needs a repository secret GITRISK_TOKEN: a token that can read the code repos and write to this repo.
|
|
958
|
+
name: GitRisk
|
|
959
|
+
on:
|
|
960
|
+
repository_dispatch:
|
|
961
|
+
types: [code-merged] # sent by each code repo's gitrisk-notify workflow
|
|
962
|
+
schedule:
|
|
963
|
+
- cron: "0 3 * * *" # nightly safety net
|
|
964
|
+
workflow_dispatch:
|
|
965
|
+
permissions:
|
|
966
|
+
contents: write
|
|
967
|
+
concurrency:
|
|
968
|
+
group: gitrisk
|
|
969
|
+
cancel-in-progress: true
|
|
970
|
+
jobs:
|
|
971
|
+
score:
|
|
972
|
+
runs-on: ubuntu-latest
|
|
973
|
+
steps:
|
|
974
|
+
- uses: actions/checkout@v4
|
|
975
|
+
with:
|
|
976
|
+
path: specs
|
|
977
|
+
token: \${{ secrets.GITRISK_TOKEN }}
|
|
978
|
+
${checkouts}
|
|
979
|
+
- uses: actions/setup-node@v4
|
|
980
|
+
with:
|
|
981
|
+
node-version: 20
|
|
982
|
+
- name: Score files from git history
|
|
983
|
+
run: npx -y @benjaminthomas/spec-driven-dev@${version} risk --dir=. --repos=${repos.map((r) => r.name).join(",")} --specs-dir=specs
|
|
984
|
+
- name: Commit when the hotspots changed (not for a date-only change)
|
|
985
|
+
working-directory: specs
|
|
986
|
+
run: |
|
|
987
|
+
git add CONTEXT.md .gitignore
|
|
988
|
+
if git diff --cached --quiet -I '^Updated [0-9-]+$'; then echo "Hotspots unchanged"; exit 0; fi
|
|
989
|
+
git -c user.name="gitrisk[bot]" -c user.email="gitrisk@users.noreply.github.com" commit -m "chore: refresh GitRisk hotspots"
|
|
990
|
+
git push
|
|
991
|
+
`;
|
|
992
|
+
const notify = (branch) => `# Tells the specs repo to refresh GitRisk hotspots when work lands here.
|
|
993
|
+
# Written by \`spec-driven-dev ci --notify\`. Needs the same GITRISK_TOKEN secret in this repo.
|
|
994
|
+
name: GitRisk notify
|
|
995
|
+
on:
|
|
996
|
+
push:
|
|
997
|
+
branches: [${branch}]
|
|
998
|
+
jobs:
|
|
999
|
+
notify:
|
|
1000
|
+
runs-on: ubuntu-latest
|
|
1001
|
+
steps:
|
|
1002
|
+
- run: gh api repos/${specsSlug}/dispatches -f event_type=code-merged
|
|
1003
|
+
env:
|
|
1004
|
+
GH_TOKEN: \${{ secrets.GITRISK_TOKEN }}
|
|
1005
|
+
`;
|
|
1006
|
+
return { specs, notify };
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function ci() {
|
|
1010
|
+
const { root, c } = loadConfig();
|
|
1011
|
+
if (!c.layout) throw new Error('No .spec-driven.yml found. Say "run spec-setup" in your AI tool first.');
|
|
1012
|
+
const specs = path.resolve(root, c.specs_dir || "specs");
|
|
1013
|
+
const g = specsInGit(root, c.specs_dir || "specs");
|
|
1014
|
+
if (!g) throw new Error(`${specs} isn't inside a git repo, so there's nowhere to run CI. Keep specs in a repo (a dedicated specs repo is best).`);
|
|
1015
|
+
const specsSlug = ghSlug(run("git", ["remote", "get-url", "origin"], g.top));
|
|
1016
|
+
if (!specsSlug) throw new Error(`The specs repo at ${g.top} has no GitHub remote yet. Push it first, e.g.: gh repo create <org>/<name> --private --source ${g.top} --push`);
|
|
1017
|
+
const team = parseYaml(fs.existsSync(path.join(specs, "team-repos.yml")) ? fs.readFileSync(path.join(specs, "team-repos.yml"), "utf8") : "").repos;
|
|
1018
|
+
const list = (Array.isArray(team) && team.length ? team : (c.repos || []).map((r) => ({ name: path.basename(String(r.path)), url: run("git", ["remote", "get-url", "origin"], path.resolve(root, expandHome(String(r.path)))) })))
|
|
1019
|
+
.map((r) => ({ name: r.name, slug: ghSlug(r.url) })).filter((r) => r.name && r.name !== path.basename(g.top));
|
|
1020
|
+
const missing = list.filter((r) => !r.slug);
|
|
1021
|
+
if (missing.length) throw new Error(`No GitHub URL for ${missing.map((r) => r.name).join(", ")}. Add a "url:" for each in team-repos.yml.`);
|
|
1022
|
+
if (!list.length) throw new Error("No code repos to score. List them in team-repos.yml or your workspace.");
|
|
1023
|
+
const version = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version;
|
|
1024
|
+
const { specs: specsYml, notify } = ciFiles({ specsSlug, repos: list, version });
|
|
1025
|
+
if (g.rel) throw new Error(`Specs live inside ${g.top}, a code repo. The CI step needs a dedicated specs repo, so its commits don't land in a code repo. Move specs with spec-setup, or use: spec-driven-dev risk after merges.`);
|
|
1026
|
+
const wf = path.join(g.top, ".github", "workflows", "gitrisk.yml");
|
|
1027
|
+
fs.mkdirSync(path.dirname(wf), { recursive: true });
|
|
1028
|
+
fs.writeFileSync(wf, specsYml);
|
|
1029
|
+
step(1, "GitRisk on every merge", ink.cyan);
|
|
1030
|
+
console.log(`✓ Wrote ${wf}\n Scores ${list.map((r) => r.name).join(", ")} nightly, on demand, and whenever a code repo pings it.`);
|
|
1031
|
+
const wrote = [];
|
|
1032
|
+
for (const r of (c.repos || [])) {
|
|
1033
|
+
const dir = path.resolve(root, expandHome(String(r.path)));
|
|
1034
|
+
if (!flag("notify") || !isRepo(dir) || real(dir) === real(g.top)) continue;
|
|
1035
|
+
const branch = r.git?.base_branch && r.git.base_branch !== "default" ? r.git.base_branch : c.git?.base_branch && c.git.base_branch !== "default" ? c.git.base_branch : run("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], dir).replace(/^origin\//, "") || "main";
|
|
1036
|
+
const f = path.join(dir, ".github", "workflows", "gitrisk-notify.yml");
|
|
1037
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
1038
|
+
fs.writeFileSync(f, notify(branch));
|
|
1039
|
+
wrote.push(`${path.basename(dir)} (on ${branch})`);
|
|
1040
|
+
}
|
|
1041
|
+
if (wrote.length) console.log(`✓ Wrote .github/workflows/gitrisk-notify.yml in: ${wrote.join(", ")}`);
|
|
1042
|
+
else console.log("• Add --notify to also write the small trigger workflow into each code repo on this machine, so a merge refreshes the hotspots straight away.");
|
|
1043
|
+
console.log(`
|
|
1044
|
+
Next:
|
|
1045
|
+
1. Create a GitHub token (fine-grained): read access to ${list.map((r) => r.name).join(", ")}; contents: read & write on ${specsSlug}.
|
|
1046
|
+
2. Add it as a secret named GITRISK_TOKEN in ${specsSlug}${wrote.length ? " and in each code repo" : ""}: gh secret set GITRISK_TOKEN -R ${specsSlug}
|
|
1047
|
+
3. Commit and push the workflow file(s). Nothing was committed for you.
|
|
1048
|
+
4. Try it: gh workflow run GitRisk -R ${specsSlug}`);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
936
1051
|
// Silent unless something is unfinished, so it's cheap to run at the start of a session.
|
|
937
1052
|
function status() {
|
|
938
1053
|
const file = findConfig(DIR);
|
|
@@ -964,6 +1079,7 @@ try {
|
|
|
964
1079
|
else if (command === "risk") risk();
|
|
965
1080
|
else if (command === "settings") await settings();
|
|
966
1081
|
else if (command === "uninstall") await uninstall();
|
|
1082
|
+
else if (command === "ci") ci();
|
|
967
1083
|
else if (command === "risk-eval") await riskEvalCmd();
|
|
968
1084
|
else if (command === "install") {
|
|
969
1085
|
if (HANDOFF) console.log("No interactive terminal (for example a \"!\" command in Claude Code), so the skills go to the AI tools it finds, for this project.\n");
|
|
@@ -971,7 +1087,7 @@ try {
|
|
|
971
1087
|
if (tools.length && !flag("no-setup")) await handOver(tools);
|
|
972
1088
|
} else if (SETUP_IN_AGENT) handOver([], true);
|
|
973
1089
|
else if (command === "setup") await setup();
|
|
974
|
-
else throw new Error(`Unknown command "${command}" — use install (default), setup, settings, uninstall, doctor, status, context, risk, or risk-eval`);
|
|
1090
|
+
else throw new Error(`Unknown command "${command}" — use install (default), setup, settings, ci, uninstall, doctor, status, context, risk, or risk-eval`);
|
|
975
1091
|
if (command === "setup" && !SETUP_IN_AGENT) console.log('\nNext: in your AI tool, say "investigate <ticket>" or "create a spec".');
|
|
976
1092
|
} catch (e) {
|
|
977
1093
|
console.error(`✗ ${e.message}`);
|
package/bin/risk.mjs
CHANGED
|
@@ -167,3 +167,18 @@ export function setConfigValue(text, blockName, key, value) {
|
|
|
167
167
|
const body = line.test(block[0]) ? block[0].replace(line, `$1${value}`) : block[0].replace(new RegExp(`^${blockName}:.*\\n`), `$& ${key}: ${value}\n`);
|
|
168
168
|
return text.replace(block[0], body);
|
|
169
169
|
}
|
|
170
|
+
|
|
171
|
+
// A short, committed summary inside CONTEXT.md, so the hotspots are part of the shared project memory
|
|
172
|
+
// (risk.md itself is gitignored: it's regenerated on every run). Replaced in place between markers.
|
|
173
|
+
const RB = "<!-- risk:start (written by spec-driven-dev risk; don't edit) -->", RE = "<!-- risk:end -->";
|
|
174
|
+
export function riskSummaryBlock(rows, date) {
|
|
175
|
+
const top = rows.filter((r) => r.tier === "critical" || r.tier === "high").slice(0, 5);
|
|
176
|
+
const counts = ["critical", "high", "medium"].map((t) => `${rows.filter((r) => r.tier === t).length} ${t}`).join(" · ");
|
|
177
|
+
return [RB, "## Risk hotspots", `Updated ${date}`, `${counts}. Full table: context/risk.md (regenerate: spec-driven-dev risk).`,
|
|
178
|
+
...(top.length ? top.map((r) => `- \`${r.file}\` ${r.score} ${r.tier}`) : ["- No high or critical files."]), RE].join("\n");
|
|
179
|
+
}
|
|
180
|
+
export function withRiskSummary(text, block) {
|
|
181
|
+
const i = text.indexOf(RB), j = text.indexOf(RE);
|
|
182
|
+
if (i >= 0 && j > i) return text.slice(0, i) + block + text.slice(j + RE.length);
|
|
183
|
+
return `${text.replace(/\n*$/, "")}\n\n${block}\n`;
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@benjaminthomas/spec-driven-dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.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.15.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",
|
|
@@ -125,23 +125,74 @@ Read, don't guess. Collect a draft of every field in `references/config-template
|
|
|
125
125
|
|
|
126
126
|
## How to ask
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
Two kinds of question, asked two different ways:
|
|
129
|
+
|
|
130
|
+
1. **Choices** (layout and repos, sharing, specs location, after-approval mode, a guessed command):
|
|
131
|
+
use the host's **interactive question tool** when it has one (Claude Code: `AskUserQuestion`).
|
|
132
|
+
- Up to 4 questions per call; each needs **2 to 4 options** (a one-option question is rejected
|
|
133
|
+
and the user never sees it; "Other" is added automatically and doesn't count).
|
|
134
|
+
- Put the recommended option first and end its label with "(Recommended)". Put the evidence in
|
|
135
|
+
each option's description (e.g. "called via BACKBONE_URL, app/services/x.ts:105").
|
|
136
|
+
- **Layout and repos are two separate calls, always** (Step 2). Never ask layout together with
|
|
137
|
+
other questions.
|
|
138
|
+
- **Repos are never offered as preset bundles** ("+backend +melissa", "all four"); bundles don't
|
|
139
|
+
scale and stop people mixing their own. Ask in two calls:
|
|
140
|
+
1. **Layout first**, on its own: "Single repo" vs "Several repos that change together"
|
|
141
|
+
(vs "Monorepo" when it fits), recommendation first with the evidence.
|
|
142
|
+
2. **Only if several:** a `multiSelect` question with **one option per repo**: label = repo
|
|
143
|
+
name (the recommended ones first, marked "(Recommended)"), description = what it is plus the
|
|
144
|
+
link evidence. At most 4 options. The menu can't show the rest, so the **question text** must
|
|
145
|
+
end by naming the other repos found ("Also found: a, b, c (no code link).") and saying how to
|
|
146
|
+
add them: "To add any of these, or a repo from another folder, choose *Type something* and
|
|
147
|
+
enter names or paths, e.g. `a, ~/Work/billing`." Put the remaining choices (specs location,
|
|
148
|
+
sharing, pipeline) in this same call.
|
|
149
|
+
- If the tool isn't available (or a call fails), ask the same choices as a short numbered list
|
|
150
|
+
in text instead. Never drop a question.
|
|
151
|
+
2. **Open questions** (what the product does, fragile spots, direction, unwritten rules): plain text
|
|
152
|
+
in **one chat message**, asked **after** the choices are answered. Never turn these into a menu.
|
|
134
153
|
|
|
135
154
|
## Step 2 — Confirm in one round
|
|
136
155
|
|
|
137
156
|
Show the draft as a short table grouped by layout, tracker, git, commands, and specs location.
|
|
138
|
-
Mark each value **detected** (with where it came from) or **guess**. Then ask
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
157
|
+
Mark each value **detected** (with where it came from) or **guess**. Then ask in exactly this order
|
|
158
|
+
(interactive question tool when there is one; see How to ask):
|
|
159
|
+
|
|
160
|
+
**Call 1: layout, alone. Nothing else in this call.** Use these option labels exactly; no repo names
|
|
161
|
+
in them:
|
|
162
|
+
- `Single repo`: "Only <this repo>." (+ the evidence for or against)
|
|
163
|
+
- `Several repos`: "<this repo> changes together with other repos. You'll pick which next."
|
|
164
|
+
- `Monorepo`: only when workspace markers were found.
|
|
165
|
+
|
|
166
|
+
Recommend one (first, with "(Recommended)") and put the evidence in the question text.
|
|
167
|
+
|
|
168
|
+
**Call 2: repos (only if the answer was `Several repos`).** The repo tick-box list (`multiSelect`, one
|
|
169
|
+
option per repo, see `references/repo-picker.md`) and where specs live (a dedicated specs repo, inside
|
|
170
|
+
one code repo, or this machine only).
|
|
171
|
+
|
|
172
|
+
**Call 3: git conventions, always, even when detected.** These decide how every branch and commit is
|
|
173
|
+
made, and detection can be wrong, so confirm them. Four questions, the detected value first with its
|
|
174
|
+
evidence in the description, then the common alternatives:
|
|
175
|
+
- **Base branch** (work starts from): e.g. `staging (Recommended)`: "16 of the last 20 merged PRs";
|
|
176
|
+
`main`; `develop`. In multi-repo, say which repos differ ("staging; soreto-cms has none, uses main").
|
|
177
|
+
- **PR target**: usually the same as the base branch.
|
|
178
|
+
- **Branch names**: e.g. `STP-123 (Recommended)` (bare ticket id, seen in recent branches);
|
|
179
|
+
`feat/STP-123-short-name`; `feat/short-name`.
|
|
180
|
+
- **Commit style**: e.g. `STP-123: message (Recommended)`; `feat: message`; free text. In multi-repo,
|
|
181
|
+
name any repo that uses a different style.
|
|
182
|
+
|
|
183
|
+
**Call 4: tracker, team and pipeline.** The tracker, confirmed like the git conventions (e.g.
|
|
184
|
+
`Jira, key STP (Recommended)`: "STP-123 in recent branches", `GitHub Issues`, `Linear`, `None`), then
|
|
185
|
+
sharing (if specs are inside a repo), after approval (stop / run straight through), and any command
|
|
186
|
+
that's a guess (e.g. typecheck). At most 4; a guessed command that doesn't fit goes in the summary as
|
|
187
|
+
"I'll use X, say if that's wrong".
|
|
188
|
+
|
|
189
|
+
Then the open questions, as plain text (see How to ask).
|
|
190
|
+
|
|
191
|
+
Typical things to flag in the descriptions:
|
|
192
|
+
|
|
193
|
+
- "Your repos default to `master`, but recent PRs targeted `staging`."
|
|
194
|
+
- "Recent branches are bare ticket IDs (`STP-282`)."
|
|
195
|
+
- "These 6 repos share the `acme` GitHub org."
|
|
145
196
|
|
|
146
197
|
If the user says "just use the defaults", accept the detected values and the template defaults
|
|
147
198
|
for the rest.
|
|
@@ -41,7 +41,8 @@ never ask again.
|
|
|
41
41
|
## 3. Discovery questions (one round)
|
|
42
42
|
|
|
43
43
|
Show the draft map first, with each line marked **detected** (with its source) or **guess**. Then
|
|
44
|
-
ask only what the code can't answer, as **plain text in one message
|
|
44
|
+
ask only what the code can't answer, as **plain text in one message**, after the setup choices have
|
|
45
|
+
been answered. These are open questions, so
|
|
45
46
|
never put them in a menu or multiple-choice tool (a menu needs two or more real options, and these
|
|
46
47
|
have none). The user replies in their own words, or "skip":
|
|
47
48
|
|
|
@@ -54,8 +54,18 @@ Project: existing · audited at a1b2c3d (2026-11-10) · graph: none
|
|
|
54
54
|
|
|
55
55
|
## Specs
|
|
56
56
|
- {stp-123: retry failed webhooks, touched billing (2026-11-10)}
|
|
57
|
+
|
|
58
|
+
<!-- risk:start (written by spec-driven-dev risk; don't edit) -->
|
|
59
|
+
## Risk hotspots
|
|
60
|
+
GitRisk on 2026-11-10: 0 critical · 8 high · 30 medium. Full table: context/risk.md (regenerate: spec-driven-dev risk).
|
|
61
|
+
- `app/services/billing.ts` 61.7 high
|
|
62
|
+
<!-- risk:end -->
|
|
57
63
|
```
|
|
58
64
|
|
|
65
|
+
The **Risk hotspots** block is written by `spec-driven-dev risk` (setup, every batch, checkpoint) and
|
|
66
|
+
replaced in place, so don't edit it. It's the shared part of GitRisk: `context/risk.md` itself is
|
|
67
|
+
gitignored because it's regenerated on every run.
|
|
68
|
+
|
|
59
69
|
## context/<area>.md
|
|
60
70
|
|
|
61
71
|
```markdown
|
|
@@ -52,6 +52,11 @@ Group the candidates:
|
|
|
52
52
|
with this one. Some systems connect only through a shared database or a message queue the code
|
|
53
53
|
doesn't name.
|
|
54
54
|
|
|
55
|
+
Ask it with the host's interactive question tool when there is one: after the layout question, a
|
|
56
|
+
`multiSelect` question with **one option per repo** (never bundles of repos), the recommended ones
|
|
57
|
+
first, evidence in each option's description, 2–4 options. End the question text with the other
|
|
58
|
+
repos found by name and how to add them ("choose *Type something* and enter names or paths, e.g.
|
|
59
|
+
`old-admin, ~/Work/billing`"), since the menu only shows 4. Otherwise as text.
|
|
55
60
|
Shape of the question, with placeholders rather than real names:
|
|
56
61
|
|
|
57
62
|
> I found 6 repos under ~/code/github.com/acme. Based on the code, I'd include:
|