@benjaminthomas/spec-driven-dev 1.15.0 → 1.16.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 +22 -10
- package/bin/cli.mjs +94 -16
- package/bin/team.mjs +92 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/spec-checkpoint/SKILL.md +1 -1
- package/plugin/skills/spec-create/SKILL.md +1 -1
- package/plugin/skills/spec-implement/SKILL.md +1 -1
- package/plugin/skills/spec-investigate/SKILL.md +1 -1
- package/plugin/skills/spec-review/SKILL.md +1 -1
- package/plugin/skills/spec-setup/SKILL.md +26 -1
- package/plugin/skills/spec-setup/references/config-template.yml +2 -0
- package/plugin/skills/spec-ship/SKILL.md +1 -1
- package/plugin/skills/spec-verify/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -180,6 +180,16 @@ every team repo with its git URL, so setup offers them, `doctor` shows which are
|
|
|
180
180
|
the clone command), and an investigation stops and says so when a ticket needs a repo that isn't on
|
|
181
181
|
this machine.
|
|
182
182
|
|
|
183
|
+
**Joining a team that's already set up.** The first person's setup moves the team's rules (tracker,
|
|
184
|
+
base branch, branch and commit style, per-repo commands, models, GitRisk, sharing) into **`team.yml`
|
|
185
|
+
in the specs repo**, which is committed with the specs. Each person's own `.spec-driven.yml` keeps only
|
|
186
|
+
where their repos are on that machine and personal preferences. A colleague clones the specs repo and
|
|
187
|
+
the code repos they work on, then says "run spec-setup": it finds the team's setup, offers **Join**, and
|
|
188
|
+
asks only which of the team's repos they have. No audit, no convention questions, and nothing shared
|
|
189
|
+
changes. From a terminal: `spec-driven-dev setup --join=../soreto-specs`. Changing a team rule is an
|
|
190
|
+
edit to `team.yml` and a commit, and anything set in your own file overrides it for you only (`doctor`
|
|
191
|
+
lists those overrides). An existing setup can be converted with `spec-driven-dev team init`.
|
|
192
|
+
|
|
183
193
|
**For scripts and CI** there's `spec-driven-dev setup` with flags (it asks nothing):
|
|
184
194
|
`--yes`, `--layout=multi-repo --repos=~/Projects/frontend,~/Work/backend --workspace=~/Projects`,
|
|
185
195
|
`--layout=monorepo --packages=apps/web,packages/ui`, `--specs-dir=<path>`, `--share=all|memory|none`,
|
|
@@ -660,6 +670,8 @@ spec-driven-dev doctor # check .spec-driven.yml again
|
|
|
660
670
|
spec-driven-dev status # list unfinished specs and each one's next step
|
|
661
671
|
spec-driven-dev context --areas=billing # print the project memory (index, or chosen areas)
|
|
662
672
|
spec-driven-dev settings # switch GitRisk, run-through and sharing from a menu
|
|
673
|
+
spec-driven-dev team init # share the team's rules in the specs repo (team.yml)
|
|
674
|
+
spec-driven-dev setup --join=../team-specs # join a team that's already set up
|
|
663
675
|
spec-driven-dev ci --notify # GitHub Actions: refresh GitRisk hotspots on every merge
|
|
664
676
|
spec-driven-dev uninstall # remove the skills, plugin and cache (--all: settings too)
|
|
665
677
|
spec-driven-dev risk # GitRisk: score files from git history → specs/context/risk.md
|
|
@@ -775,16 +787,16 @@ skips that step rather than failing.
|
|
|
775
787
|
|
|
776
788
|
| Skill | Stage | SKILL.md lines | Words | Supporting files |
|
|
777
789
|
|---|---|---:|---:|---:|
|
|
778
|
-
| `spec-setup` | 0 · configure |
|
|
779
|
-
| `spec-investigate` | 1 · understand | 387 | 3,
|
|
780
|
-
| `spec-create` | 2 · plan | 198 | 1,
|
|
781
|
-
| `spec-implement` | 3 · build | 327 | 2,
|
|
782
|
-
| `spec-verify` | 4 · prove | 163 | 1,
|
|
783
|
-
| `spec-review` | 5 · review | 137 | 1,
|
|
784
|
-
| `spec-checkpoint` | 6 · commit | 140 | 1,
|
|
785
|
-
| `spec-ship` | 7 · PR | 230 | 1,
|
|
790
|
+
| `spec-setup` | 0 · configure | 298 | 3,141 | 4 |
|
|
791
|
+
| `spec-investigate` | 1 · understand | 387 | 3,391 | 1 |
|
|
792
|
+
| `spec-create` | 2 · plan | 198 | 1,806 | 6 |
|
|
793
|
+
| `spec-implement` | 3 · build | 327 | 2,777 | 3 |
|
|
794
|
+
| `spec-verify` | 4 · prove | 163 | 1,581 | 1 |
|
|
795
|
+
| `spec-review` | 5 · review | 137 | 1,318 | 1 |
|
|
796
|
+
| `spec-checkpoint` | 6 · commit | 140 | 1,168 | 0 |
|
|
797
|
+
| `spec-ship` | 7 · PR | 230 | 1,412 | 0 |
|
|
786
798
|
| `generative-seo` | standalone | 255 | 2,209 | 7 |
|
|
787
|
-
| **Total** | | **2,
|
|
799
|
+
| **Total** | | **2,135** | **18,803** | **23** |
|
|
788
800
|
|
|
789
801
|
- **8** skills: **1** setup + **6** workflow stages + **1** standalone
|
|
790
802
|
- **4** tools supported natively (Claude Code, Codex CLI, Cursor, Antigravity), plus an MCP fallback
|
|
@@ -837,7 +849,7 @@ plugin/evals/ `claude plugin eval` cases
|
|
|
837
849
|
1. Increase `version` in both `package.json` and `plugin/.claude-plugin/plugin.json` (the check fails if
|
|
838
850
|
they differ).
|
|
839
851
|
2. Add a `CHANGELOG.md` entry, then commit and push.
|
|
840
|
-
3. Tag the release and push the tag: `git tag v1.
|
|
852
|
+
3. Tag the release and push the tag: `git tag v1.16.0 && git push origin v1.16.0`.
|
|
841
853
|
4. Watch the **publish** run in the repo's Actions tab. When it's green, the new version is on
|
|
842
854
|
npm. Claude Code plugin users get it from the marketplace once the commit is on `main`.
|
|
843
855
|
|
package/bin/cli.mjs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
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 team init move the team's shared rules into the specs repo (team.yml) so colleagues can join
|
|
8
|
+
// spec-driven-dev setup --join=<specs repo path or git URL> join a team that's already set up (asks nothing else)
|
|
7
9
|
// spec-driven-dev ci write GitHub Actions that refresh GitRisk hotspots on every merge (--notify also writes the per-repo trigger)
|
|
8
10
|
// spec-driven-dev uninstall remove the skills (all tools), the Claude Code plugin and the download cache; --all also removes the settings file
|
|
9
11
|
// spec-driven-dev doctor check .spec-driven.yml against the real repos (exit 1 on problems)
|
|
@@ -23,6 +25,7 @@ import prompts from "prompts"; // arrow-key menus that work in PowerShell, cmd,
|
|
|
23
25
|
import { fileURLToPath } from "node:url";
|
|
24
26
|
import { scoreRepo, riskMarkdown, normWeights, weightProblems, setConfigValue, riskSummaryBlock, withRiskSummary } from "./risk.mjs";
|
|
25
27
|
import { SHARE_MODES, shareState, planShare, shareTitle } from "./share.mjs";
|
|
28
|
+
import { mergeConfig, overrides, splitConfig, dumpYaml, TEAM_HEADER, PERSONAL_HEADER, teamFileFor, readIf } from "./team.mjs";
|
|
26
29
|
|
|
27
30
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
28
31
|
const SKILLS_DIR = path.join(ROOT, "plugin", "skills");
|
|
@@ -41,7 +44,7 @@ const args = process.argv.slice(2);
|
|
|
41
44
|
const flag = (n) => args.includes(`--${n}`);
|
|
42
45
|
const opt = (n) => args.find((a) => a.startsWith(`--${n}=`))?.split("=")[1];
|
|
43
46
|
const command = args.find((a) => !a.startsWith("-")) || "install";
|
|
44
|
-
const YES = flag("yes") || flag("y") || !process.stdin.isTTY || ["doctor", "status", "context", "risk", "risk-eval", "setup", "ci"].includes(command) || (command === "uninstall" && flag("yes"));
|
|
47
|
+
const YES = flag("yes") || flag("y") || !process.stdin.isTTY || ["doctor", "status", "context", "risk", "risk-eval", "setup", "ci", "team"].includes(command) || (command === "uninstall" && flag("yes"));
|
|
45
48
|
const DIR = path.resolve(opt("dir") || process.cwd());
|
|
46
49
|
|
|
47
50
|
// Colour only for a person at a terminal; pipes, CI and NO_COLOR get plain text (and the tests stay plain).
|
|
@@ -65,7 +68,7 @@ if (flag("version") || flag("v")) {
|
|
|
65
68
|
}
|
|
66
69
|
// No terminal to answer in (e.g. `!` in Claude Code, an IDE task, a pipe) and nothing passed explicitly:
|
|
67
70
|
// stop rather than write a config from guesses.
|
|
68
|
-
const ANSWER_FLAGS = ["share", "specs-dir", "risk", "yes", "y", "layout", "repos", "packages", "tools", "workspace", "after-approval", "skills", "global", "no-setup", "project"];
|
|
71
|
+
const ANSWER_FLAGS = ["join", "share", "specs-dir", "risk", "yes", "y", "layout", "repos", "packages", "tools", "workspace", "after-approval", "skills", "global", "no-setup", "project"];
|
|
69
72
|
const NO_TTY = !process.stdin.isTTY && !args.some((a) => ANSWER_FLAGS.some((f) => a === `--${f}` || a.startsWith(`--${f}=`)));
|
|
70
73
|
// Installing the skills needs no answers, so the default command still does that without a terminal
|
|
71
74
|
// (e.g. `!` in Claude Code) and hands the questions to the spec-setup skill, which asks them in the chat.
|
|
@@ -589,6 +592,21 @@ function configApplies(f, dir) {
|
|
|
589
592
|
return here === real(base) || c.repos.some((r) => { const p = real(path.resolve(base, expandHome(String(r.path || "")))); return here === p || here.startsWith(p + path.sep); });
|
|
590
593
|
}
|
|
591
594
|
|
|
595
|
+
// The effective config: the team's rules (team.yml in the specs repo) with the personal file on top.
|
|
596
|
+
function readConfig(file) {
|
|
597
|
+
const personal = parseYaml(fs.readFileSync(file, "utf8"));
|
|
598
|
+
const tf = teamFileFor(file, personal.specs_dir);
|
|
599
|
+
const team = readIf(tf);
|
|
600
|
+
const c = team ? { ...mergeConfig(personal, parseYaml(team)), _teamFile: tf, _personal: personal, _team: parseYaml(team) } : personal;
|
|
601
|
+
// Repo roles are team knowledge kept in team-repos.yml; fill them in rather than copying them into each person's file.
|
|
602
|
+
const listed = parseYaml(readIf(path.join(path.dirname(tf), "team-repos.yml")) || "").repos;
|
|
603
|
+
if (Array.isArray(listed) && Array.isArray(c.repos)) {
|
|
604
|
+
const roles = new Map(listed.map((r) => [r.name, r.role]));
|
|
605
|
+
c.repos = c.repos.map((r) => (r.role ? r : { ...r, role: roles.get(path.basename(String(r.path || "").replace(/[\\/]+$/, ""))) || r.role }));
|
|
606
|
+
}
|
|
607
|
+
return c;
|
|
608
|
+
}
|
|
609
|
+
|
|
592
610
|
function findConfig(dir) {
|
|
593
611
|
for (let d = dir; ; d = path.dirname(d)) {
|
|
594
612
|
const f = path.join(d, ".spec-driven.yml");
|
|
@@ -608,8 +626,12 @@ function doctor() {
|
|
|
608
626
|
if (!file) throw new Error(`No .spec-driven.yml in ${DIR} or above. In your AI tool, say "run spec-setup".`);
|
|
609
627
|
const root = path.dirname(file);
|
|
610
628
|
let c;
|
|
611
|
-
try { c =
|
|
629
|
+
try { c = readConfig(file); } catch (e) { throw new Error(`${file} could not be read: ${e.message}`); }
|
|
612
630
|
const problems = [], notes = [];
|
|
631
|
+
if (c._teamFile) {
|
|
632
|
+
notes.push(`team rules come from ${c._teamFile}`);
|
|
633
|
+
for (const o of overrides(c._personal, c._team)) notes.push(`your ${o.key} (${JSON.stringify(o.yours)}) overrides the team's (${JSON.stringify(o.team)}) for you only`);
|
|
634
|
+
}
|
|
613
635
|
if (!["single", "monorepo", "multi-repo"].includes(c.layout)) problems.push(`layout "${c.layout}" must be single, monorepo, or multi-repo`);
|
|
614
636
|
if (c.tracker && !["jira", "github", "linear", "none"].includes(c.tracker.type)) problems.push(`tracker.type "${c.tracker.type}" must be jira, github, linear, or none`);
|
|
615
637
|
if (c.git?.commit_style && !["conventional", "ticket", "freeform"].includes(c.git.commit_style)) problems.push(`git.commit_style "${c.git.commit_style}" must be conventional, ticket, or freeform`);
|
|
@@ -707,7 +729,7 @@ function checkContext(specs, specsName, repoRoots, problems, notes) {
|
|
|
707
729
|
function context() {
|
|
708
730
|
const file = findConfig(DIR);
|
|
709
731
|
const root = file ? path.dirname(file) : DIR;
|
|
710
|
-
const specsName = (file &&
|
|
732
|
+
const specsName = (file && readConfig(file).specs_dir) || "specs";
|
|
711
733
|
const index = path.join(root, specsName, "CONTEXT.md");
|
|
712
734
|
const areas = splitList(opt("areas"));
|
|
713
735
|
if (!areas.length) {
|
|
@@ -791,7 +813,7 @@ function ignoreGenerated(file) {
|
|
|
791
813
|
|
|
792
814
|
function loadConfig() {
|
|
793
815
|
const file = findConfig(DIR);
|
|
794
|
-
return { file, root: file ? path.dirname(file) : gitTop(DIR), c: file ?
|
|
816
|
+
return { file, root: file ? path.dirname(file) : gitTop(DIR), c: file ? readConfig(file) : {} };
|
|
795
817
|
}
|
|
796
818
|
|
|
797
819
|
function risk() {
|
|
@@ -857,7 +879,7 @@ async function riskEvalCmd() {
|
|
|
857
879
|
|
|
858
880
|
// Plain-language switches, so anyone can change them without editing YAML.
|
|
859
881
|
const SETTINGS = [
|
|
860
|
-
{ block: "risk", key: "enabled", flag: "risk", name: "GitRisk", about: "extra care on files with a history of bugs",
|
|
882
|
+
{ block: "risk", key: "enabled", flag: "risk", team: true, name: "GitRisk", about: "extra care on files with a history of bugs",
|
|
861
883
|
get: (c) => c.risk?.enabled || "on", options: [
|
|
862
884
|
{ value: "on", title: "On", description: "investigations, plans and reviews look harder at historically risky files" },
|
|
863
885
|
{ value: "off", title: "Off", description: "the skills ignore risk completely" }] },
|
|
@@ -865,7 +887,7 @@ const SETTINGS = [
|
|
|
865
887
|
get: (c) => c.pipeline?.after_approval || "stop", options: [
|
|
866
888
|
{ value: "stop", title: "Stop after each stage", description: "you start implement, verify, review and commit yourself" },
|
|
867
889
|
{ value: "run-through", title: "Run straight through", description: "implement → verify → review → commit; stops at any failure, never pushes" }] },
|
|
868
|
-
{ block: "context", key: "share", flag: "share", name: "Share specs with the team", about: "what's committed from the specs folder",
|
|
890
|
+
{ block: "context", key: "share", flag: "share", team: true, name: "Share specs with the team", about: "what's committed from the specs folder",
|
|
869
891
|
get: (c) => c.context?.share || "all", after: (file, c, v) => applyShare(path.dirname(file), c.specs_dir || "specs", v), options: [
|
|
870
892
|
{ value: "all", title: "Share everything", description: "commit plans, investigations, reviews and project memory" },
|
|
871
893
|
{ value: "memory", title: "Share project memory only", description: "commit CONTEXT.md and context/; keep feature files local" },
|
|
@@ -876,18 +898,22 @@ async function settings() {
|
|
|
876
898
|
const file = findConfig(DIR);
|
|
877
899
|
if (!file) throw new Error(`No .spec-driven.yml in ${DIR} or above. In your AI tool, say "run spec-setup" first.`);
|
|
878
900
|
let text = fs.readFileSync(file, "utf8");
|
|
901
|
+
const tf = readConfig(file)._teamFile;
|
|
879
902
|
const label = (s, v) => s.options.find((o) => o.value === v)?.title || v;
|
|
880
903
|
const save = async (s, v) => {
|
|
881
904
|
if (!s.options.some((o) => o.value === v)) throw new Error(`--${s.flag} must be ${s.options.map((o) => o.value).join(" or ")}`);
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
905
|
+
// Team rules go to the shared team.yml when there is one; the rest stays personal.
|
|
906
|
+
const target = s.team && tf ? tf : file;
|
|
907
|
+
const updated = setConfigValue(fs.readFileSync(target, "utf8"), s.block, s.key, v);
|
|
908
|
+
fs.writeFileSync(target, updated);
|
|
909
|
+
if (target === file) text = updated;
|
|
910
|
+
console.log(`✓ ${s.name}: ${label(s, v)} (saved in ${target}${target === tf ? ": shared with the team, commit it" : ""})`);
|
|
911
|
+
if (s.after) await s.after(file, readConfig(file), v);
|
|
886
912
|
};
|
|
887
913
|
const given = SETTINGS.filter((s) => opt(s.flag));
|
|
888
914
|
if (given.length) for (const s of given) await save(s, opt(s.flag));
|
|
889
915
|
else for (;;) {
|
|
890
|
-
const c =
|
|
916
|
+
const c = readConfig(file);
|
|
891
917
|
const pick = await askSelect("Settings: pick one to change", [
|
|
892
918
|
...SETTINGS.map((s) => ({ title: `${s.name}: ${label(s, s.get(c))}`, description: s.about, value: s })),
|
|
893
919
|
{ title: "Done", value: null }], null);
|
|
@@ -895,7 +921,7 @@ async function settings() {
|
|
|
895
921
|
const v = await askSelect(`${pick.name}`, pick.options, pick.get(c));
|
|
896
922
|
if (v !== pick.get(c)) await save(pick, v);
|
|
897
923
|
}
|
|
898
|
-
const c =
|
|
924
|
+
const c = readConfig(file);
|
|
899
925
|
if (c.risk?.enabled !== "off" && !fs.existsSync(path.join(path.dirname(file), c.specs_dir || "specs", "context", "risk.md"))) {
|
|
900
926
|
if (given.length || !(await askConfirm("GitRisk is on but files haven't been scored yet. Score them now? (uses git only, takes seconds)", true)))
|
|
901
927
|
console.log(" GitRisk is on, but nothing is scored yet. Run: spec-driven-dev risk");
|
|
@@ -942,6 +968,56 @@ async function uninstall() {
|
|
|
942
968
|
console.log("Done. Reinstall any time with: npx @benjaminthomas/spec-driven-dev@latest");
|
|
943
969
|
}
|
|
944
970
|
|
|
971
|
+
// ---------- team: shared rules in the specs repo ----------
|
|
972
|
+
function teamInit() {
|
|
973
|
+
const file = findConfig(DIR);
|
|
974
|
+
if (!file) throw new Error('No .spec-driven.yml found. Say "run spec-setup" in your AI tool first.');
|
|
975
|
+
const personal = parseYaml(fs.readFileSync(file, "utf8"));
|
|
976
|
+
const tf = teamFileFor(file, personal.specs_dir);
|
|
977
|
+
if (fs.existsSync(tf)) throw new Error(`${tf} already exists. To change a team rule, edit it and commit, or say "change our branch rules" in your AI tool.`);
|
|
978
|
+
if (!specsInGit(path.dirname(file), personal.specs_dir || "specs")) console.log(`• ${path.dirname(tf)} isn't in a git repo, so team.yml can't be shared yet. Keep specs in a repo (a dedicated specs repo is best).`);
|
|
979
|
+
const { team, personal: mine } = splitConfig(personal);
|
|
980
|
+
fs.mkdirSync(path.dirname(tf), { recursive: true });
|
|
981
|
+
fs.writeFileSync(tf, TEAM_HEADER + dumpYaml(team));
|
|
982
|
+
fs.copyFileSync(file, `${file}.bak`);
|
|
983
|
+
fs.writeFileSync(file, PERSONAL_HEADER(path.relative(path.dirname(file), tf)) + dumpYaml(mine));
|
|
984
|
+
step(1, "Team rules shared", ink.cyan);
|
|
985
|
+
console.log(`✓ Wrote ${tf} (tracker, git conventions, commands, models, GitRisk, sharing)
|
|
986
|
+
✓ ${file} now keeps only your local settings (the old file is ${path.basename(file)}.bak)
|
|
987
|
+
Next: commit team.yml in the specs repo. Colleagues who clone it run: spec-driven-dev setup --join=<path to the specs repo>
|
|
988
|
+
or say "run spec-setup" in their AI tool, which finds it and offers to join.`);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function join() {
|
|
992
|
+
const target = opt("join");
|
|
993
|
+
let specsDir = /^(https?:|git@|ssh:)/.test(target) ? "" : path.resolve(DIR, expandHome(target));
|
|
994
|
+
const workspace = path.resolve(DIR, expandHome(opt("workspace") || (specsDir ? path.dirname(specsDir) : DIR)));
|
|
995
|
+
if (!specsDir) {
|
|
996
|
+
const name = path.basename(target).replace(/\.git$/, "");
|
|
997
|
+
specsDir = path.join(workspace, name);
|
|
998
|
+
if (!fs.existsSync(specsDir)) execFileSync("git", ["clone", "-q", target, specsDir], { stdio: "inherit" });
|
|
999
|
+
}
|
|
1000
|
+
const team = parseYaml(readIf(path.join(specsDir, "team.yml")) || "");
|
|
1001
|
+
const repoList = parseYaml(readIf(path.join(specsDir, "team-repos.yml")) || "").repos;
|
|
1002
|
+
if (!Object.keys(team).length && !Array.isArray(repoList)) throw new Error(`${specsDir} has no team.yml or team-repos.yml, so there's no team to join. Ask whoever set it up to run: spec-driven-dev team init`);
|
|
1003
|
+
const known = Array.isArray(repoList) ? repoList : (team.repos || []);
|
|
1004
|
+
const wanted = opt("repos") ? splitList(opt("repos")) : known.map((r) => r.name);
|
|
1005
|
+
const have = wanted.filter((n) => isRepo(path.join(workspace, n)));
|
|
1006
|
+
const missing = known.filter((r) => wanted.includes(r.name) && !have.includes(r.name));
|
|
1007
|
+
if (!have.length) throw new Error(`None of the team's repos (${known.map((r) => r.name).join(", ")}) are cloned in ${workspace}. Clone the ones you work on, then join again (or pass --workspace=<folder>).`);
|
|
1008
|
+
const file = path.join(workspace, ".spec-driven.yml");
|
|
1009
|
+
if (fs.existsSync(file) && !flag("force")) throw new Error(`${file} already exists. Add --force to replace it with the team's setup (it's backed up first).`);
|
|
1010
|
+
if (fs.existsSync(file)) fs.copyFileSync(file, `${file}.bak`);
|
|
1011
|
+
const mine = { version: 1, layout: have.length > 1 ? "multi-repo" : "single", repos: have.map((n) => ({ path: n })), specs_dir: path.relative(workspace, specsDir) || ".", pipeline: { after_approval: opt("after-approval") || "stop" } };
|
|
1012
|
+
if (mine.layout === "single") { delete mine.repos; }
|
|
1013
|
+
fs.writeFileSync(file, PERSONAL_HEADER(path.relative(workspace, path.join(specsDir, "team.yml"))) + dumpYaml(mine));
|
|
1014
|
+
if (mine.layout === "multi-repo") register(real(file), have.map((n) => real(path.join(workspace, n))));
|
|
1015
|
+
step(1, "Joined the team", ink.cyan);
|
|
1016
|
+
console.log(`✓ Wrote ${file} with your repos: ${have.join(", ")}
|
|
1017
|
+
Team rules (tracker, branches, commits, commands, GitRisk) come from ${path.join(specsDir, "team.yml")}${Object.keys(team).length ? "" : " (not created yet: team-repos.yml only)"}.`);
|
|
1018
|
+
for (const r of missing) console.log(` • ${r.name} isn't cloned here${r.url ? `: git clone ${r.url} ${path.join(workspace, r.name)}` : ""}`);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
945
1021
|
// ---------- CI: refresh GitRisk hotspots on merge ----------
|
|
946
1022
|
function ghSlug(url) { return (String(url || "").match(/github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/) || [])[1] || ""; }
|
|
947
1023
|
|
|
@@ -1052,7 +1128,7 @@ Next:
|
|
|
1052
1128
|
function status() {
|
|
1053
1129
|
const file = findConfig(DIR);
|
|
1054
1130
|
const root = file ? path.dirname(file) : DIR;
|
|
1055
|
-
const specsName = (file &&
|
|
1131
|
+
const specsName = (file && readConfig(file).specs_dir) || "specs";
|
|
1056
1132
|
const specs = path.join(root, specsName);
|
|
1057
1133
|
const lines = [];
|
|
1058
1134
|
for (const name of subdirs(specs)) {
|
|
@@ -1080,14 +1156,16 @@ try {
|
|
|
1080
1156
|
else if (command === "settings") await settings();
|
|
1081
1157
|
else if (command === "uninstall") await uninstall();
|
|
1082
1158
|
else if (command === "ci") ci();
|
|
1159
|
+
else if (command === "team") { if (args.includes("init")) teamInit(); else throw new Error("Use: spec-driven-dev team init"); }
|
|
1083
1160
|
else if (command === "risk-eval") await riskEvalCmd();
|
|
1084
1161
|
else if (command === "install") {
|
|
1085
1162
|
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");
|
|
1086
1163
|
const tools = await install();
|
|
1087
1164
|
if (tools.length && !flag("no-setup")) await handOver(tools);
|
|
1088
|
-
} else if (
|
|
1165
|
+
} else if (command === "setup" && opt("join")) join();
|
|
1166
|
+
else if (SETUP_IN_AGENT) handOver([], true);
|
|
1089
1167
|
else if (command === "setup") await setup();
|
|
1090
|
-
else throw new Error(`Unknown command "${command}" — use install (default), setup, settings, ci, uninstall, doctor, status, context, risk, or risk-eval`);
|
|
1168
|
+
else throw new Error(`Unknown command "${command}" — use install (default), setup, settings, team, ci, uninstall, doctor, status, context, risk, or risk-eval`);
|
|
1091
1169
|
if (command === "setup" && !SETUP_IN_AGENT) console.log('\nNext: in your AI tool, say "investigate <ticket>" or "create a spec".');
|
|
1092
1170
|
} catch (e) {
|
|
1093
1171
|
console.error(`✗ ${e.message}`);
|
package/bin/team.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The team's shared rules live in the specs repo as team.yml; each person's .spec-driven.yml keeps only
|
|
2
|
+
// local things (where their repos are, which ones they have, personal preferences). The effective
|
|
3
|
+
// config is team.yml with the personal file on top.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
// Shared: decided once by the team. Personal: differs per machine or per person.
|
|
8
|
+
export const TEAM_KEYS = ["tracker", "git", "commands", "models", "effort", "risk", "context"];
|
|
9
|
+
export const PERSONAL_KEYS = ["version", "layout", "repos", "specs_dir", "pipeline"];
|
|
10
|
+
const REPO_TEAM_KEYS = ["role", "git", "commands"];
|
|
11
|
+
|
|
12
|
+
const isObj = (v) => v && typeof v === "object" && !Array.isArray(v);
|
|
13
|
+
export function deepMerge(a, b) {
|
|
14
|
+
if (!isObj(a) || !isObj(b)) return b === undefined ? a : b;
|
|
15
|
+
const out = { ...a };
|
|
16
|
+
for (const [k, v] of Object.entries(b)) out[k] = isObj(v) && isObj(a[k]) ? deepMerge(a[k], v) : v;
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// personal: the parsed .spec-driven.yml; team: the parsed team.yml (or null). Repos match by folder name.
|
|
21
|
+
export function mergeConfig(personal, team) {
|
|
22
|
+
if (!team) return personal;
|
|
23
|
+
const shared = Object.fromEntries(TEAM_KEYS.filter((k) => team[k] !== undefined).map((k) => [k, team[k]]));
|
|
24
|
+
const merged = deepMerge(shared, personal);
|
|
25
|
+
const byName = new Map((Array.isArray(team.repos) ? team.repos : []).map((r) => [r.name, r]));
|
|
26
|
+
if (Array.isArray(personal.repos)) {
|
|
27
|
+
merged.repos = personal.repos.map((r) => {
|
|
28
|
+
const t = byName.get(path.basename(String(r.path || "").replace(/[\\/]+$/, "")));
|
|
29
|
+
return t ? deepMerge(Object.fromEntries(REPO_TEAM_KEYS.filter((k) => t[k] !== undefined).map((k) => [k, t[k]])), r) : r;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return merged;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Leaves that the personal file sets differently from the team, e.g. "git.base_branch".
|
|
36
|
+
export function overrides(personal, team, prefix = "") {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const k of prefix ? Object.keys(personal || {}) : TEAM_KEYS) {
|
|
39
|
+
const p = personal?.[k], t = team?.[k];
|
|
40
|
+
if (p === undefined || t === undefined) continue;
|
|
41
|
+
if (isObj(p) && isObj(t)) out.push(...overrides(p, t, `${prefix}${k}.`));
|
|
42
|
+
else if (JSON.stringify(p) !== JSON.stringify(t)) out.push({ key: `${prefix}${k}`, yours: p, team: t });
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Small YAML writer for nested maps, lists of maps and scalars (JSON strings are valid YAML scalars).
|
|
48
|
+
export function dumpYaml(obj, indent = "") {
|
|
49
|
+
const scalar = (v) => (typeof v === "string" ? (/^[\w./@~-]+$/.test(v) && !/^(on|off|yes|no|true|false|null|\d.*)$/i.test(v) ? v : JSON.stringify(v)) : String(v));
|
|
50
|
+
let out = "";
|
|
51
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
52
|
+
if (v === undefined || v === null || (isObj(v) && !Object.keys(v).length)) continue;
|
|
53
|
+
if (Array.isArray(v)) {
|
|
54
|
+
out += `${indent}${k}:\n`;
|
|
55
|
+
for (const item of v) {
|
|
56
|
+
if (!isObj(item)) { out += `${indent} - ${scalar(item)}\n`; continue; }
|
|
57
|
+
const body = dumpYaml(item, `${indent} `).replace(/^\s+/, "");
|
|
58
|
+
out += `${indent} - ${body}`;
|
|
59
|
+
}
|
|
60
|
+
} else if (isObj(v)) out += `${indent}${k}:\n${dumpYaml(v, `${indent} `)}`;
|
|
61
|
+
else out += `${indent}${k}: ${scalar(v)}\n`;
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Split one config into the team's part and the personal part.
|
|
67
|
+
export function splitConfig(c) {
|
|
68
|
+
const team = { version: 1 };
|
|
69
|
+
for (const k of TEAM_KEYS) if (c[k] !== undefined) team[k] = c[k];
|
|
70
|
+
const repos = (Array.isArray(c.repos) ? c.repos : []).map((r) => {
|
|
71
|
+
const t = { name: path.basename(String(r.path || "").replace(/[\\/]+$/, "")) };
|
|
72
|
+
for (const k of REPO_TEAM_KEYS) if (r[k] !== undefined && r[k] !== "") t[k] = r[k];
|
|
73
|
+
return t;
|
|
74
|
+
}).filter((t) => Object.keys(t).length > 1);
|
|
75
|
+
if (repos.length) team.repos = repos;
|
|
76
|
+
const personal = {};
|
|
77
|
+
for (const k of PERSONAL_KEYS) if (c[k] !== undefined) personal[k] = c[k];
|
|
78
|
+
if (Array.isArray(personal.repos)) personal.repos = personal.repos.map((r) => ({ path: r.path }));
|
|
79
|
+
return { team, personal };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const TEAM_HEADER = `# team.yml — the team's shared rules for spec-driven-dev (tracker, git conventions, commands,
|
|
83
|
+
# models, GitRisk, sharing). Committed in the specs repo, so everyone works the same way. Each
|
|
84
|
+
# person's .spec-driven.yml only says where their repos are; a value set there overrides this file
|
|
85
|
+
# for that person. Change a rule here and commit it (or say "change our branch rules" in your AI tool).
|
|
86
|
+
`;
|
|
87
|
+
export const PERSONAL_HEADER = (teamFile) => `# .spec-driven.yml — your local settings: where your repos are and personal preferences.
|
|
88
|
+
# The team's shared rules are in ${teamFile}. Anything you set here overrides them for you only.
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
export const teamFileFor = (configFile, specsDir) => path.resolve(path.dirname(configFile), specsDir || "specs", "team.yml");
|
|
92
|
+
export const readIf = (f) => (fs.existsSync(f) ? fs.readFileSync(f, "utf8") : null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@benjaminthomas/spec-driven-dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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.16.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",
|
|
@@ -20,7 +20,7 @@ Load `.spec-driven.yml`: walk up from the current directory to the home folder,
|
|
|
20
20
|
found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
|
|
21
21
|
`~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
|
|
22
22
|
whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
|
|
23
|
-
you would detect or assume. This skill uses `commands` (lint/typecheck/test/build), `git.commit_style`, and `layout`/`repos`.
|
|
23
|
+
you would detect or assume. If `{specs_dir}/team.yml` exists, it holds the team's shared rules (tracker, git conventions, commands, models, GitRisk, sharing): read it first, and let `.spec-driven.yml` override it. This skill uses `commands` (lint/typecheck/test/build), `git.commit_style`, and `layout`/`repos`.
|
|
24
24
|
|
|
25
25
|
**Multi-repo:** run this in every listed repo that has changes, making one commit per repo with the same subject. **Monorepo:** run the checks for the packages that changed, or the root commands if that's how the repo works.
|
|
26
26
|
|
|
@@ -23,7 +23,7 @@ Load `.spec-driven.yml`: walk up from the current directory to the home folder,
|
|
|
23
23
|
found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
|
|
24
24
|
`~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
|
|
25
25
|
whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
|
|
26
|
-
you would detect or assume. This skill uses `layout`/`repos` and `specs_dir`.
|
|
26
|
+
you would detect or assume. If `{specs_dir}/team.yml` exists, it holds the team's shared rules (tracker, git conventions, commands, models, GitRisk, sharing): read it first, and let `.spec-driven.yml` override it. This skill uses `layout`/`repos` and `specs_dir`.
|
|
27
27
|
|
|
28
28
|
**Monorepo / multi-repo:** every task names its package or repo, and its file paths are relative to that. Keep each task inside one repo. Cross-repo work becomes separate tasks, with the contract (API shape, event, schema) written into both.
|
|
29
29
|
|
|
@@ -29,7 +29,7 @@ Load `.spec-driven.yml`: walk up from the current directory to the home folder,
|
|
|
29
29
|
found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
|
|
30
30
|
`~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
|
|
31
31
|
whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
|
|
32
|
-
you would detect or assume. This skill uses `layout`/`repos`, `commands` (lint/typecheck/test per repo or package), and `git.commit_style`.
|
|
32
|
+
you would detect or assume. If `{specs_dir}/team.yml` exists, it holds the team's shared rules (tracker, git conventions, commands, models, GitRisk, sharing): read it first, and let `.spec-driven.yml` override it. This skill uses `layout`/`repos`, `commands` (lint/typecheck/test per repo or package), and `git.commit_style`.
|
|
33
33
|
|
|
34
34
|
**Multi-repo:** create worktrees in each repo a task touches, run each repo's own commands in review, and make the batch commit in every repo that changed, using the same subject line.
|
|
35
35
|
|
|
@@ -36,7 +36,7 @@ Load `.spec-driven.yml`: walk up from the current directory to the home folder,
|
|
|
36
36
|
found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
|
|
37
37
|
`~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
|
|
38
38
|
whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
|
|
39
|
-
you would detect or assume. This skill uses `layout`/`repos` (where to look and what each repo does), `tracker`, `git.remote`, `git.base_branch`, `git.branch_name`, and `specs_dir`.
|
|
39
|
+
you would detect or assume. If `{specs_dir}/team.yml` exists, it holds the team's shared rules (tracker, git conventions, commands, models, GitRisk, sharing): read it first, and let `.spec-driven.yml` override it. This skill uses `layout`/`repos` (where to look and what each repo does), `tracker`, `git.remote`, `git.base_branch`, `git.branch_name`, and `specs_dir`.
|
|
40
40
|
|
|
41
41
|
**Monorepo:** name the package(s) involved. **Multi-repo:** search every listed repo whose role fits the ticket, and create the work branch, with the same name, only in the repos the investigation shows need changes.
|
|
42
42
|
|
|
@@ -28,7 +28,7 @@ investigate → create → approve → implement → verify → review → check
|
|
|
28
28
|
Load `.spec-driven.yml`: walk up from the current directory to the home folder, and the first one
|
|
29
29
|
found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
|
|
30
30
|
`~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
|
|
31
|
-
whose repos contain the current directory. Its values beat anything you would detect or assume. This
|
|
31
|
+
whose repos contain the current directory. Its values beat anything you would detect or assume. If `{specs_dir}/team.yml` exists, it holds the team's shared rules (tracker, git conventions, commands, models, GitRisk, sharing): read it first, and let `.spec-driven.yml` override it. This
|
|
32
32
|
skill uses `git` (base branch, PR target), `commands` (lint/typecheck/test), `models`/`effort`,
|
|
33
33
|
`layout`/`repos`, and `specs_dir`.
|
|
34
34
|
|
|
@@ -32,11 +32,30 @@ first one found wins.
|
|
|
32
32
|
|---|---|---|
|
|
33
33
|
| `single` | the repo root | Commit it — the whole team gets the same rules |
|
|
34
34
|
| `monorepo` | the repo root | Commit it |
|
|
35
|
-
| `multi-repo` | the shared parent folder of the repos (usually not itself a git repo) |
|
|
35
|
+
| `multi-repo` | the shared parent folder of the repos (usually not itself a git repo) | The team's rules go in `team.yml` in the specs repo (shared); this file keeps only local paths |
|
|
36
36
|
|
|
37
37
|
A multi-repo config only applies inside the repos it lists. If the current repo sits under that
|
|
38
38
|
folder but isn't listed, treat it as having no config (ask whether to add it).
|
|
39
39
|
|
|
40
|
+
## Step 0 — Join a team that's already set up
|
|
41
|
+
|
|
42
|
+
Before detecting anything, look for a team setup someone already shared: a folder next to this repo
|
|
43
|
+
(or in the workspace folder) containing `team.yml` or `team-repos.yml`, or a specs repo the user
|
|
44
|
+
names (a path, or a git URL to clone). If you find one, say so with what it is, and ask with a menu:
|
|
45
|
+
|
|
46
|
+
> Found your team's setup in ~/Projects/soreto-specs: 5 repos, Jira STP, branches from staging.
|
|
47
|
+
> `Join it (Recommended)`: use the team's rules; I'll only ask which of its repos you have here.
|
|
48
|
+
> `Set up separately`: ignore it and answer everything yourself.
|
|
49
|
+
|
|
50
|
+
**Joining:** ask only which of the team's repos you work on (tick boxes, from `team-repos.yml`;
|
|
51
|
+
mark the ones not cloned here and offer their `git clone` command, cloning only on a yes), then run
|
|
52
|
+
`spec-driven-dev setup --join=<specs repo> --repos=<chosen>` (or `npx -y @benjaminthomas/spec-driven-dev …`).
|
|
53
|
+
Don't audit, don't ask about conventions, and never change the team's files: `CONTEXT.md`,
|
|
54
|
+
`team.yml` and `team-repos.yml` are shared. Then run `doctor`, and finish with "You've joined. Say
|
|
55
|
+
*investigate STP-123* to start." Only `team-repos.yml` and no `team.yml` (an older setup): join
|
|
56
|
+
anyway, and tell the user the team's rules aren't shared yet, so the person who set it up should
|
|
57
|
+
run `spec-driven-dev team init`.
|
|
58
|
+
|
|
40
59
|
## Step 1 — Detect before asking
|
|
41
60
|
|
|
42
61
|
Read, don't guess. Collect a draft of every field in `references/config-template.yml`:
|
|
@@ -204,6 +223,11 @@ for the rest.
|
|
|
204
223
|
under `repos:` where they differ from the top level.
|
|
205
224
|
2. If it's inside a repo, tell the user it's meant to be committed (so teammates get the same
|
|
206
225
|
rules) — don't commit it yourself.
|
|
226
|
+
**Multi-repo with specs in a repo:** run `spec-driven-dev team init` right after writing the
|
|
227
|
+
file. It moves the team's rules (tracker, git conventions, commands, models, GitRisk, sharing)
|
|
228
|
+
into `{specs_dir}/team.yml`, which is committed with the specs, and leaves only local paths in
|
|
229
|
+
`.spec-driven.yml`. Tell the user to commit `team.yml`, and that colleagues then just say
|
|
230
|
+
"run spec-setup" and choose *Join*.
|
|
207
231
|
3. If `npx` is available, run `npx -y @benjaminthomas/spec-driven-dev doctor` from the config's folder. It checks
|
|
208
232
|
that every branch, folder, and `npm run` script in the file really exists. Fix anything it
|
|
209
233
|
reports before finishing.
|
|
@@ -249,6 +273,7 @@ keep every other line and comment, and confirm in one sentence. Don't re-run dis
|
|
|
249
273
|
| "turn GitRisk on", "use the risk scores" | `risk.enabled: on`, then run `spec-driven-dev risk` if `{specs_dir}/context/risk.md` doesn't exist yet |
|
|
250
274
|
| "run straight through after I approve", "fewer stops" | `pipeline.after_approval: run-through` |
|
|
251
275
|
| "stop after each stage" | `pipeline.after_approval: stop` |
|
|
276
|
+
| "change our branch rules", "use main for PRs" (when `team.yml` exists) | edit `team.yml`, since it's a team rule, and remind them to commit it; only change `.spec-driven.yml` if they say it's just for them |
|
|
252
277
|
| "add reverb-react to the workspace", "I also work on X" | add it under `repos:` (clone first if needed, from `team-repos.yml`), and to `team-repos.yml` if it's new to the team |
|
|
253
278
|
| "share the specs with the team", "keep specs local", "share only the project memory" | `context.share: all / none / memory`: show the `.gitignore` change, then run `spec-driven-dev settings --share=…` on a yes |
|
|
254
279
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# .spec-driven.yml — project rules for the spec-driven-dev skills.
|
|
2
|
+
# Multi-repo teams: the shared parts (tracker, git, commands, models, effort, risk, context) go in
|
|
3
|
+
# {specs_dir}/team.yml (spec-driven-dev team init), so colleagues can join; this file then keeps local paths.
|
|
2
4
|
# Written by spec-setup from what it detected + what you confirmed. Edit freely, or re-run
|
|
3
5
|
# spec-setup to update it. Every spec-* skill reads this before doing anything else.
|
|
4
6
|
version: 1
|
|
@@ -22,7 +22,7 @@ Load `.spec-driven.yml`: walk up from the current directory to the home folder,
|
|
|
22
22
|
found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
|
|
23
23
|
`~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
|
|
24
24
|
whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
|
|
25
|
-
you would detect or assume. This skill uses `git.remote`, `git.pr_target`, `git.branch_name`, `tracker` (to link the ticket in the PR), and `layout`/`repos`.
|
|
25
|
+
you would detect or assume. If `{specs_dir}/team.yml` exists, it holds the team's shared rules (tracker, git conventions, commands, models, GitRisk, sharing): read it first, and let `.spec-driven.yml` override it. This skill uses `git.remote`, `git.pr_target`, `git.branch_name`, `tracker` (to link the ticket in the PR), and `layout`/`repos`.
|
|
26
26
|
|
|
27
27
|
**Multi-repo:** push and open one PR per repo that has unpushed commits, and list the sibling PRs in each PR's body so reviewers see the whole change.
|
|
28
28
|
|
|
@@ -29,7 +29,7 @@ Load `.spec-driven.yml`: walk up from the current directory to the home folder,
|
|
|
29
29
|
found wins (a `multi-repo` config only applies inside the repos it lists). If none is found, check
|
|
30
30
|
`~/.spec-driven/workspaces.json`, which maps each multi-repo config to its repo paths. Use the config
|
|
31
31
|
whose repos contain the current directory, because a workspace's repos can live in unrelated folders. Its values beat anything
|
|
32
|
-
you would detect or assume. This skill uses `commands.dev`, `commands.dev_url`, and `layout`/`repos`.
|
|
32
|
+
you would detect or assume. If `{specs_dir}/team.yml` exists, it holds the team's shared rules (tracker, git conventions, commands, models, GitRisk, sharing): read it first, and let `.spec-driven.yml` override it. This skill uses `commands.dev`, `commands.dev_url`, and `layout`/`repos`.
|
|
33
33
|
|
|
34
34
|
**Monorepo / multi-repo:** start every app or service the criteria need (e.g. the API and the frontend), each with its own `dev` command.
|
|
35
35
|
|