@structured-vibe-coding/core 0.18.0 → 0.20.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.
Files changed (31) hide show
  1. package/defaults/teams/research/roles/researcher.md +29 -0
  2. package/defaults/teams/research/team.yaml +7 -0
  3. package/defaults/teams/software/roles/architect.md +31 -0
  4. package/defaults/teams/software/roles/dev-lead.md +32 -0
  5. package/defaults/teams/software/roles/developer.md +30 -0
  6. package/defaults/teams/software/roles/product-manager.md +32 -0
  7. package/defaults/teams/software/roles/product-owner.md +30 -0
  8. package/defaults/teams/software/roles/qa.md +35 -0
  9. package/defaults/teams/software/routing.yaml +4 -0
  10. package/defaults/teams/software/team.yaml +24 -0
  11. package/defaults/teams/solo/roles/developer.md +28 -0
  12. package/defaults/teams/solo/team.yaml +7 -0
  13. package/defaults/templates/plan.md +17 -0
  14. package/defaults/templates/qa-testing.md +11 -0
  15. package/defaults/templates/review.md +11 -0
  16. package/defaults/templates/signoff.md +7 -0
  17. package/defaults/templates/story.md +13 -0
  18. package/defaults/templates/task.md +11 -0
  19. package/defaults/workflows/team-software.yaml +293 -0
  20. package/dist/{agents-AKSUXQHN.js → agents-YN3Z3VOH.js} +3 -3
  21. package/dist/{change-context-REE7UKU7.js → change-context-CZT3U7MZ.js} +1 -1
  22. package/dist/{chunk-QGP43XCN.js → chunk-EDXT3RTN.js} +1 -1
  23. package/dist/{chunk-44LQ5BKZ.js → chunk-HUSXETPS.js} +833 -4
  24. package/dist/{chunk-LHSR7JG2.js → chunk-NY5FFWXG.js} +306 -31
  25. package/dist/{chunk-4PIRWYRA.js → chunk-O4OQWVC7.js} +166 -81
  26. package/dist/{chunk-QHV5KZKX.js → chunk-TNWFMRIA.js} +139 -10
  27. package/dist/{gather-ZNV2NAUN.js → gather-ZKVMJW5L.js} +1 -1
  28. package/dist/{greenfield-I5S5OS2D.js → greenfield-O2DI27HY.js} +3 -3
  29. package/dist/index.d.ts +833 -10
  30. package/dist/index.js +3357 -511
  31. package/package.json +2 -2
@@ -1,13 +1,17 @@
1
1
  import {
2
2
  buildAgentPrompt,
3
+ canonicalRoleName,
3
4
  defaultConfig,
4
5
  formatStatus,
5
- loadConfig
6
- } from "./chunk-44LQ5BKZ.js";
6
+ loadConfig,
7
+ readRole,
8
+ readTeamConfig
9
+ } from "./chunk-HUSXETPS.js";
7
10
  import {
8
11
  buildContext,
12
+ defaultMemoryDir,
9
13
  getActiveChangeId
10
- } from "./chunk-QHV5KZKX.js";
14
+ } from "./chunk-TNWFMRIA.js";
11
15
  import {
12
16
  SddError
13
17
  } from "./chunk-MHODNFIL.js";
@@ -26,7 +30,7 @@ import {
26
30
  // src/agents.ts
27
31
  import { constants } from "fs";
28
32
  import { access, lstat as lstat2, readdir, rmdir, unlink } from "fs/promises";
29
- import { dirname as dirname2, join as join4 } from "pathe";
33
+ import { dirname, join as join4 } from "pathe";
30
34
 
31
35
  // src/generated.ts
32
36
  import { createHash } from "crypto";
@@ -152,12 +156,17 @@ async function reconcileGeneratedTree(projectRoot, sourceDir, destinationDir, de
152
156
  kept.push({ path: projectRelative, reason: "unreadable" });
153
157
  continue;
154
158
  }
155
- const previousHash = manifest.files[projectRelative];
159
+ const currentHash = contentHash(current);
160
+ let previousHash = manifest.files[projectRelative];
156
161
  if (!previousHash) {
157
- kept.push({ path: projectRelative, reason: "not-generated" });
158
- continue;
162
+ if (currentHash !== generatedHash) {
163
+ kept.push({ path: projectRelative, reason: "not-generated" });
164
+ continue;
165
+ }
166
+ manifest.files[projectRelative] = generatedHash;
167
+ previousHash = generatedHash;
159
168
  }
160
- if (contentHash(current) !== previousHash) {
169
+ if (currentHash !== previousHash) {
161
170
  kept.push({ path: projectRelative, reason: "edited" });
162
171
  continue;
163
172
  }
@@ -184,47 +193,7 @@ function describeKept(k) {
184
193
  }
185
194
 
186
195
  // src/guidance.ts
187
- import { join as join3 } from "pathe";
188
-
189
- // src/defaults.ts
190
- import { existsSync } from "fs";
191
- import { fileURLToPath } from "url";
192
- import { dirname, join as join2 } from "pathe";
193
- function defaultsRoot() {
194
- const here = dirname(fileURLToPath(import.meta.url));
195
- const candidates = [
196
- join2(here, "..", "defaults"),
197
- // npm package: dist/../defaults
198
- join2(here, "defaults"),
199
- // same-dir defaults if present
200
- join2(here, "..", "..", "defaults")
201
- // src/ during some test runners
202
- ];
203
- for (const c of candidates) {
204
- if (existsSync(c)) return c;
205
- }
206
- return candidates[0];
207
- }
208
- function defaultWorkflowsDir() {
209
- return join2(defaultsRoot(), "workflows");
210
- }
211
- function defaultTemplatesDir() {
212
- return join2(defaultsRoot(), "templates");
213
- }
214
- function defaultMemoryDir() {
215
- return join2(defaultsRoot(), "memory");
216
- }
217
- function defaultMcpYamlPath() {
218
- return join2(defaultsRoot(), "mcp.yaml");
219
- }
220
- function defaultPricesYamlPath() {
221
- return join2(defaultsRoot(), "prices.yaml");
222
- }
223
- function defaultSearchYamlPath() {
224
- return join2(defaultsRoot(), "search.yaml");
225
- }
226
-
227
- // src/guidance.ts
196
+ import { join as join2 } from "pathe";
228
197
  function renderSddReadme(config) {
229
198
  const paths = guidancePaths(config);
230
199
  return `# Structured Vibe Coding (SDD)
@@ -302,14 +271,14 @@ async function refreshConfiguredGuidance(projectRoot, config) {
302
271
  await writeConfiguredReadme(projectRoot, config);
303
272
  }
304
273
  async function writeConfiguredMemoryIndex(memoryRoot, config) {
305
- const indexPath = join3(memoryRoot, "index.md");
274
+ const indexPath = join2(memoryRoot, "index.md");
306
275
  const paths = guidancePaths(config);
307
276
  if (paths.memory === "memory" && paths.changes === "changes") return;
308
277
  if (!await pathExists(indexPath)) return;
309
278
  const existing = await readText(indexPath);
310
279
  const state = generatedState(existing);
311
280
  if (state === "edited") return;
312
- const shippedPath = join3(defaultMemoryDir(), "index.md");
281
+ const shippedPath = join2(defaultMemoryDir(), "index.md");
313
282
  const shipped = await pathExists(shippedPath) ? await readText(shippedPath) : "";
314
283
  if (state !== "unmodified" && existing.replace(/\r\n/g, "\n").trim() !== shipped.trim()) return;
315
284
  const body = renderMemoryIndex(config);
@@ -317,7 +286,7 @@ async function writeConfiguredMemoryIndex(memoryRoot, config) {
317
286
  await writeText(indexPath, markGenerated(body));
318
287
  }
319
288
  async function writeConfiguredReadme(projectRoot, config) {
320
- const readmePath = join3(sddRoot(projectRoot), "README.md");
289
+ const readmePath = join2(sddRoot(projectRoot), "README.md");
321
290
  if (!await pathExists(readmePath)) return;
322
291
  const existing = await readText(readmePath);
323
292
  if (generatedState(existing) !== "unmodified") return;
@@ -327,6 +296,95 @@ async function writeConfiguredReadme(projectRoot, config) {
327
296
  await writeText(readmePath, marked);
328
297
  }
329
298
 
299
+ // src/role/entrypoint-team.ts
300
+ import { homedir } from "os";
301
+ import { join as join3 } from "pathe";
302
+ function productTeamHome() {
303
+ const fromEnv = process.env.SDD_HOME?.trim();
304
+ if (fromEnv) return fromEnv;
305
+ return homedir();
306
+ }
307
+ function oneLine(value) {
308
+ const text = value.replace(/\s+/g, " ").replaceAll("|", "/").trim();
309
+ return text || "not named";
310
+ }
311
+ function toMember(role, name, order, source) {
312
+ return {
313
+ order,
314
+ name,
315
+ job: oneLine(role?.job ?? ""),
316
+ accepts: role && role.approvers.length > 0 ? oneLine(role.approvers.join(", ")) : "not named",
317
+ file: source === "repo" ? `.sdd/roles/${name}.md` : `~/.sdd/roles/${name}.md`,
318
+ source
319
+ };
320
+ }
321
+ async function listedRoles(root) {
322
+ const dir = join3(root, ".sdd", "roles");
323
+ if (!await pathExists(dir)) return [];
324
+ const listed = [];
325
+ for (const file of await listFiles(dir)) {
326
+ if (!file.endsWith(".md")) continue;
327
+ const name = file.slice(0, -".md".length);
328
+ if (canonicalRoleName(name) !== name) continue;
329
+ listed.push({ name, role: await readRole(root, name) });
330
+ }
331
+ return listed;
332
+ }
333
+ function byName(listed) {
334
+ return listed.filter((item) => !item.role?.hidden).map((item) => item.name).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
335
+ }
336
+ async function membersFrom(root, source) {
337
+ const listed = await listedRoles(root);
338
+ const teamFile = await pathExists(join3(root, ".sdd", "team.yaml"));
339
+ if (!teamFile && listed.length === 0) return null;
340
+ const known = new Map(listed.map((item) => [item.name, item.role]));
341
+ let names;
342
+ if (teamFile) {
343
+ const loaded = await readTeamConfig(root);
344
+ if ("team" in loaded) {
345
+ const seen = /* @__PURE__ */ new Set();
346
+ names = [];
347
+ for (const bot of loaded.team.bots) {
348
+ if (seen.has(bot.role)) continue;
349
+ seen.add(bot.role);
350
+ names.push(bot.role);
351
+ }
352
+ } else {
353
+ names = byName(listed);
354
+ }
355
+ } else {
356
+ names = byName(listed);
357
+ }
358
+ return names.map((name, index) => toMember(known.get(name) ?? null, name, index + 1, source));
359
+ }
360
+ async function loadProductTeam(projectRoot) {
361
+ const repo = await membersFrom(projectRoot, "repo");
362
+ if (repo) return repo;
363
+ const user = await membersFrom(productTeamHome(), "user");
364
+ return user ?? [];
365
+ }
366
+ function renderProductTeamSection(members) {
367
+ if (members.length === 0) return "";
368
+ const lines = [
369
+ "## Product team",
370
+ "",
371
+ "Run these product roles in order. The stage hat (planner, implementer, reviewer) is how the host behaves inside one role's turn. It does not replace this team. Do not ask the human to repeat this roster.",
372
+ ""
373
+ ];
374
+ if (members[0]?.source === "user") {
375
+ lines.push(
376
+ "Source: user default (`~/.sdd`). It applies only because this repo has no product team.",
377
+ ""
378
+ );
379
+ }
380
+ for (const member of members) {
381
+ lines.push(
382
+ `${member.order}. \`${member.name}\` \u2014 ${member.job}. Accepts: ${member.accepts}. File: \`${member.file}\``
383
+ );
384
+ }
385
+ return lines.join("\n");
386
+ }
387
+
330
388
  // src/agents.ts
331
389
  var AGENT_INTEGRATIONS = [
332
390
  {
@@ -546,7 +604,7 @@ async function planOtherHostRemoval(projectRoot, keep) {
546
604
  async function removalProblems(projectRoot, plan) {
547
605
  const problems = [];
548
606
  for (const rel of plan.remove) {
549
- const dir = dirname2(rel);
607
+ const dir = dirname(rel);
550
608
  if (!await canAccess(join4(projectRoot, dir), constants.W_OK)) {
551
609
  problems.push(
552
610
  `Cannot remove ${rel} (another host's sdd file): ${dir}/ is not writable (permission denied). Make it writable or delete the file yourself.`
@@ -565,12 +623,12 @@ async function executeRemoval(projectRoot, plan) {
565
623
  throw new SddError(describeFsError(err, "remove", rel), { cause: err });
566
624
  }
567
625
  removed.push(rel);
568
- let dir = dirname2(rel);
626
+ let dir = dirname(rel);
569
627
  while (dir !== "." && dir !== "") {
570
628
  const top = !dir.includes("/");
571
629
  if (top && !PRUNABLE_PARENTS.includes(dir)) break;
572
630
  ancestors.add(dir);
573
- dir = dirname2(dir);
631
+ dir = dirname(dir);
574
632
  }
575
633
  }
576
634
  const dirs = [...ancestors].sort((a, b) => b.split("/").length - a.split("/").length);
@@ -633,11 +691,13 @@ var SDD_AGENT_ROLES = [
633
691
  {
634
692
  id: "sdd",
635
693
  description: "Default SDD agent. Use for any Spec-Driven Development work on the active change pack.",
636
- roleLine: "Router: inspect stage in active-context; plan or implement accordingly.",
694
+ roleLine: "Router: send the stage to the role that owns it, one item at a time.",
637
695
  roleRules: [
638
- "If stage is intent/feature/design/tasks/stories/research: act as planner (artifacts, little or no product code).",
639
- "If stage is implement: act as implementer (scoped code only).",
640
- "If stage is local_verify: act as reviewer (gaps vs acceptance; fixes only).",
696
+ "Owns: which role works this stage.",
697
+ "Never: accept a spec or record a decision.",
698
+ "Checks: the stage matches the work.",
699
+ "Ask when: the stage is missing or a hard gate is blocked.",
700
+ "Route with sdd role route. Rules in .sdd/routing.yaml win, then a role owns list. One item at a time. No owner asks the human.",
641
701
  "If kickoff/brief says refine or analyze: follow <changes_path>/<id>/refine-brief.md \u2014 stage-scoped refine; never edit constitution; do not sdd next."
642
702
  ]
643
703
  },
@@ -646,6 +706,10 @@ var SDD_AGENT_ROLES = [
646
706
  description: "SDD planner. Use when filling intent, design, research, stories, or tasks \u2014 not large code changes.",
647
707
  roleLine: "Planner: produce/update stage markdown only.",
648
708
  roleRules: [
709
+ "Owns: stage markdown, not product code.",
710
+ "Never: accept a spec.",
711
+ "Checks: the artifact says what changes and how to tell.",
712
+ "Ask when: the scope is missing.",
649
713
  "Do not make large product code changes unless the user explicitly overrides.",
650
714
  "Keep artifacts short and actionable."
651
715
  ]
@@ -655,6 +719,10 @@ var SDD_AGENT_ROLES = [
655
719
  description: "SDD implementer. Use when coding the active change (implement stage).",
656
720
  roleLine: "Implementer: code only for the active change pack.",
657
721
  roleRules: [
722
+ "Owns: code for this change pack.",
723
+ "Never: accept a spec or widen the change.",
724
+ "Checks: the diff matches the tasks and acceptance.",
725
+ "Ask when: a task or a design constraint is missing.",
658
726
  "Follow tasks.md / acceptance; honor arb-decision and design constraints.",
659
727
  "Do not expand scope beyond the change title/intent."
660
728
  ]
@@ -664,20 +732,24 @@ var SDD_AGENT_ROLES = [
664
732
  description: "SDD reviewer. Use before verify/complete or to check work against the change pack.",
665
733
  roleLine: "Reviewer: compare work to acceptance and stage artifacts.",
666
734
  roleRules: [
735
+ "Owns: the gap list.",
736
+ "Never: accept a spec or treat a claim as evidence.",
737
+ "Checks: the change matches acceptance.",
738
+ "Ask when: acceptance does not say how to tell it worked.",
667
739
  "List gaps; suggest minimal fixes only.",
668
740
  "Remind user to run sdd verify before sdd complete."
669
741
  ]
670
742
  }
671
743
  ];
672
- function agentFiles(integ, config) {
744
+ function agentFiles(integ, config, team = []) {
673
745
  const roles = integ.rolesToInstall?.length ? SDD_AGENT_ROLES.filter((r) => integ.rolesToInstall.includes(r.id)) : SDD_AGENT_ROLES;
674
746
  return [
675
- { rel: join4(".sdd", "protocol.md"), content: renderProtocolMd(config) },
747
+ { rel: join4(".sdd", "protocol.md"), content: renderProtocolMd(config, team) },
676
748
  ...roles.map((role) => ({
677
749
  rel: integ.rolePath(role.id),
678
- content: renderThinAgent(role, config)
750
+ content: renderThinAgent(role, config, team)
679
751
  })),
680
- { rel: "AGENTS.md", content: renderAgentsMd(integ, config) }
752
+ { rel: "AGENTS.md", content: renderAgentsMd(integ, config, team) }
681
753
  ];
682
754
  }
683
755
  async function planAgentIntegration(opts) {
@@ -687,7 +759,8 @@ async function planAgentIntegration(opts) {
687
759
  const problems = [];
688
760
  const writes = [];
689
761
  const config = await pathExists(configPath(root)) ? await loadConfig(root) : defaultConfig();
690
- for (const { rel, content } of agentFiles(integ, config)) {
762
+ const team = await loadProductTeam(root);
763
+ for (const { rel, content } of agentFiles(integ, config, team)) {
691
764
  const full = join4(root, rel);
692
765
  if (!rel.startsWith(".sdd/")) {
693
766
  const link = await symlinkOnPath(root, rel);
@@ -698,8 +771,8 @@ async function planAgentIntegration(opts) {
698
771
  }
699
772
  const kind = await pathKind(full);
700
773
  if (kind === "missing") {
701
- let anc = dirname2(rel);
702
- while (anc !== "." && await pathKind(join4(root, anc)) === "missing") anc = dirname2(anc);
774
+ let anc = dirname(rel);
775
+ while (anc !== "." && await pathKind(join4(root, anc)) === "missing") anc = dirname(anc);
703
776
  const ancFull = anc === "." ? root : join4(root, anc);
704
777
  const ancKind = await pathKind(ancFull);
705
778
  if (ancKind !== "dir" && ancKind !== "symlink") {
@@ -834,9 +907,12 @@ async function installAgentIntegrations(opts) {
834
907
  }
835
908
  return { created, skipped, targets: opts.targets };
836
909
  }
837
- function renderThinAgent(role, config) {
910
+ function renderThinAgent(role, config, team = []) {
838
911
  const paths = guidancePaths(config ?? { memory_path: "memory", changes_path: "changes" });
839
912
  const rules = role.roleRules.map((r) => `- ${r.replaceAll("<changes_path>", paths.changes)}`).join("\n");
913
+ const productTeam = team.length > 0 ? `
914
+ ${renderProductTeamSection(team)}
915
+ ` : "";
840
916
  return `---
841
917
  name: ${role.id}
842
918
  description: ${role.description}
@@ -856,7 +932,7 @@ ${role.roleLine}
856
932
  ## Role rules
857
933
 
858
934
  ${rules}
859
-
935
+ ${productTeam}
860
936
  ## Done
861
937
 
862
938
  Remind the human: \`sdd verify\` then \`sdd next\` / \`sdd complete\` as appropriate.
@@ -879,6 +955,7 @@ async function loadInstalledAgent(projectRoot) {
879
955
  async function writeAgentHandoff(projectRoot, config, changeId, budget, runtime) {
880
956
  const ctx = await buildContext(projectRoot, config, changeId);
881
957
  const body = await buildAgentPrompt(ctx, config, projectRoot, budget, runtime);
958
+ const team = renderProductTeamSection(await loadProductTeam(projectRoot));
882
959
  const outPath = join4(sddRoot(projectRoot), "handoff.md");
883
960
  const header = `# SDD handoff (auto)
884
961
 
@@ -890,7 +967,10 @@ Also read: \`.sdd/active-context.md\`, \`.sdd/protocol.md\`, \`${guidancePaths(c
890
967
  ---
891
968
 
892
969
  `;
893
- await writeText(outPath, header + body + "\n");
970
+ const teamBlock = team ? `
971
+ ${team}
972
+ ` : "";
973
+ await writeText(outPath, header + teamBlock + body + "\n");
894
974
  return outPath;
895
975
  }
896
976
  function agentKickoffMessage(opts) {
@@ -920,7 +1000,8 @@ function agentKickoffMessage(opts) {
920
1000
  }
921
1001
  async function refreshGeneratedAgentFiles(projectRoot, config) {
922
1002
  const installed = await loadInstalledAgent(projectRoot);
923
- const files = installed ? agentFiles(installed.integration, config) : [{ rel: join4(".sdd", "protocol.md"), content: renderProtocolMd(config) }];
1003
+ const team = await loadProductTeam(projectRoot);
1004
+ const files = installed ? agentFiles(installed.integration, config, team) : [{ rel: join4(".sdd", "protocol.md"), content: renderProtocolMd(config, team) }];
924
1005
  for (const { rel, content } of files) {
925
1006
  const full = join4(projectRoot, rel);
926
1007
  if (!await pathExists(full)) {
@@ -945,6 +1026,10 @@ async function refreshActiveAgentContext(projectRoot) {
945
1026
  await refreshGeneratedAgentFiles(projectRoot, config);
946
1027
  const activeId = await getActiveChangeId(projectRoot, config);
947
1028
  const outPath = join4(markerDir, "active-context.md");
1029
+ const team = renderProductTeamSection(await loadProductTeam(projectRoot));
1030
+ const teamBlock = team ? `
1031
+ ${team}
1032
+ ` : "";
948
1033
  if (!activeId) {
949
1034
  const empty = `# SDD active context
950
1035
 
@@ -953,7 +1038,7 @@ _No active change._ Run \`sdd new "\u2026"\` or **SDD: New Change** in the IDE.
953
1038
  Then re-run \`sdd agents refresh\` (or advance a stage) to update this file.
954
1039
 
955
1040
  Protocol: \`.sdd/protocol.md\`
956
- `;
1041
+ ${teamBlock}`;
957
1042
  await writeText(outPath, empty);
958
1043
  return outPath;
959
1044
  }
@@ -971,16 +1056,17 @@ Protocol: \`.sdd/protocol.md\`
971
1056
  \`\`\`
972
1057
  ${status}
973
1058
  \`\`\`
974
-
1059
+ ${teamBlock}
975
1060
  ${handoff}
976
1061
  `;
977
1062
  await writeText(outPath, body);
978
1063
  return outPath;
979
1064
  }
980
1065
  var PROTOCOL_MD = renderProtocolMd({ memory_path: "memory", changes_path: "changes" });
981
- function renderProtocolMd(config) {
1066
+ function renderProtocolMd(config, team = []) {
982
1067
  const paths = guidancePaths(config);
983
1068
  const fence = "```";
1069
+ const teamLines = team.length > 0 ? ["", ...renderProductTeamSection(team).split("\n")] : [];
984
1070
  return [
985
1071
  "# SDD protocol",
986
1072
  "",
@@ -1003,6 +1089,7 @@ function renderProtocolMd(config) {
1003
1089
  `3. \`${paths.constitution}\` if present (non-negotiables agents must not violate)`,
1004
1090
  "4. `meta.yaml` + artifacts for the **current stage** (and prior stages if needed)",
1005
1091
  `5. \`${paths.memoryIndex}\` (documentation map) if present, then linked memory pages when architecture or conventions matter`,
1092
+ ...teamLines,
1006
1093
  "",
1007
1094
  "## Stage behavior",
1008
1095
  "",
@@ -1051,8 +1138,9 @@ function renderProtocolMd(config) {
1051
1138
  ""
1052
1139
  ].join("\n");
1053
1140
  }
1054
- function renderAgentsMd(integ, config) {
1141
+ function renderAgentsMd(integ, config, team = []) {
1055
1142
  const paths = guidancePaths(config ?? { memory_path: "memory", changes_path: "changes" });
1143
+ const teamLines = team.length > 0 ? ["", ...renderProductTeamSection(team).split("\n")] : [];
1056
1144
  return [
1057
1145
  "# Agents",
1058
1146
  "",
@@ -1067,6 +1155,7 @@ function renderAgentsMd(integ, config) {
1067
1155
  `| Constitution | \`${paths.constitution}\` if present |`,
1068
1156
  `| Doc map (stable) | \`${paths.memoryIndex}\` if present |`,
1069
1157
  `| Active change | \`${paths.changes}/<id>/\` |`,
1158
+ ...teamLines,
1070
1159
  "",
1071
1160
  "| AI agent | Files |",
1072
1161
  "|----------|--------|",
@@ -1085,16 +1174,12 @@ export {
1085
1174
  saveGeneratedManifest,
1086
1175
  reconcileGeneratedTree,
1087
1176
  describeKept,
1088
- defaultsRoot,
1089
- defaultWorkflowsDir,
1090
- defaultTemplatesDir,
1091
- defaultMemoryDir,
1092
- defaultMcpYamlPath,
1093
- defaultPricesYamlPath,
1094
- defaultSearchYamlPath,
1095
1177
  renderSddReadme,
1096
1178
  renderMemoryIndex,
1097
1179
  refreshConfiguredGuidance,
1180
+ productTeamHome,
1181
+ loadProductTeam,
1182
+ renderProductTeamSection,
1098
1183
  AGENT_INTEGRATIONS,
1099
1184
  ALL_AGENT_TARGETS,
1100
1185
  DEFAULT_INIT_INTEGRATION,
@@ -21,6 +21,8 @@ var GateSchema = z.object({
21
21
  type: z.enum(["none", "soft", "hard"]).default("soft"),
22
22
  mode: z.enum(["none", "human_approval", "checklist"]).default("checklist"),
23
23
  checklist: z.array(z.string()).default([]),
24
+ /** Role names, or human for a person listed on the team. */
25
+ approvers: z.array(z.string().trim().min(1)).optional(),
24
26
  overridable: z.boolean().default(true)
25
27
  });
26
28
  var ArtifactDefSchema = z.object({
@@ -46,6 +48,15 @@ var StageSchema = z.object({
46
48
  kind: z.enum(["standard", "brainstorm", "clarify"]).default("standard"),
47
49
  /** When true, stage is optional (skippable); agents may skip if not needed */
48
50
  optional: z.boolean().default(false),
51
+ /** Role that produces this stage. */
52
+ owner: z.string().trim().min(1).optional(),
53
+ /** Roles who work with the owner on this stage. */
54
+ collaborates: z.array(z.string().trim().min(1)).optional(),
55
+ /**
56
+ * One item per matching file when the stage starts.
57
+ * A directory, a slash, and one star in the file name: tasks/T-*.md
58
+ */
59
+ items_from: z.string().regex(/^(?!.*\.\.)[A-Za-z0-9._-]+\/[A-Za-z0-9._-]*\*[A-Za-z0-9._-]*$/).optional(),
49
60
  artifacts: z.array(ArtifactDefSchema).default([]),
50
61
  gate: GateSchema.default({}),
51
62
  skippable: z.boolean().default(true),
@@ -170,11 +181,55 @@ var ChangeMetaSchema = z.object({
170
181
 
171
182
  // src/slug.ts
172
183
  var SLUG_MAX = 60;
184
+ function isSpace(ch) {
185
+ if (ch.length !== 1) return false;
186
+ const code = ch.charCodeAt(0);
187
+ return code === 9 || code === 10 || code === 11 || code === 12 || code === 13 || code === 32 || code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
188
+ }
189
+ function isDigit(ch) {
190
+ return ch.length === 1 && ch >= "0" && ch <= "9";
191
+ }
192
+ function isSlugChar(ch) {
193
+ return ch.length === 1 && (ch >= "a" && ch <= "z" || ch >= "0" && ch <= "9");
194
+ }
173
195
  function trailingIssueRef(input) {
174
- return input.match(/#\s*(\d+)\s*\)?\s*$/)?.[1];
196
+ let i = input.length - 1;
197
+ while (i >= 0 && isSpace(input[i] ?? "")) i -= 1;
198
+ if (i >= 0 && input[i] === ")") {
199
+ i -= 1;
200
+ while (i >= 0 && isSpace(input[i] ?? "")) i -= 1;
201
+ }
202
+ const digits = [];
203
+ while (i >= 0 && isDigit(input[i] ?? "")) {
204
+ digits.push(input[i] ?? "");
205
+ i -= 1;
206
+ }
207
+ if (digits.length === 0) return void 0;
208
+ while (i >= 0 && isSpace(input[i] ?? "")) i -= 1;
209
+ if (input[i] !== "#") return void 0;
210
+ digits.reverse();
211
+ return digits.join("");
212
+ }
213
+ function collapse(input) {
214
+ let out = "";
215
+ let dash = false;
216
+ for (const ch of input) {
217
+ if (ch === "'" || ch === '"') continue;
218
+ if (isSlugChar(ch)) {
219
+ out += ch;
220
+ dash = false;
221
+ continue;
222
+ }
223
+ if (!dash && out.length > 0) {
224
+ out += "-";
225
+ dash = true;
226
+ }
227
+ }
228
+ if (out.endsWith("-")) return out.slice(0, -1);
229
+ return out;
175
230
  }
176
231
  function slugify(input) {
177
- const normalized = input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
232
+ const normalized = collapse(input.toLowerCase().trim());
178
233
  if (normalized.length <= SLUG_MAX) return normalized;
179
234
  const issue = trailingIssueRef(input);
180
235
  const tokens = normalized.split("-").filter(Boolean);
@@ -212,17 +267,75 @@ function uniqueChangeDirName(base, taken) {
212
267
  }
213
268
 
214
269
  // src/workflow.ts
215
- import { join } from "pathe";
270
+ import { join as join2 } from "pathe";
271
+
272
+ // src/defaults.ts
273
+ import { existsSync } from "fs";
274
+ import { fileURLToPath } from "url";
275
+ import { dirname, join } from "pathe";
276
+ function defaultsRoot() {
277
+ const here = dirname(fileURLToPath(import.meta.url));
278
+ const candidates = [
279
+ join(here, "..", "defaults"),
280
+ // npm package: dist/../defaults
281
+ join(here, "defaults"),
282
+ // same-dir defaults if present
283
+ join(here, "..", "..", "defaults")
284
+ // src/ during some test runners
285
+ ];
286
+ for (const c of candidates) {
287
+ if (existsSync(c)) return c;
288
+ }
289
+ return candidates[0];
290
+ }
291
+ function defaultWorkflowsDir() {
292
+ return join(defaultsRoot(), "workflows");
293
+ }
294
+ function defaultTeamsDir() {
295
+ return join(defaultsRoot(), "teams");
296
+ }
297
+ function defaultTemplatesDir() {
298
+ return join(defaultsRoot(), "templates");
299
+ }
300
+ function defaultMemoryDir() {
301
+ return join(defaultsRoot(), "memory");
302
+ }
303
+ function defaultMcpYamlPath() {
304
+ return join(defaultsRoot(), "mcp.yaml");
305
+ }
306
+ function defaultPricesYamlPath() {
307
+ return join(defaultsRoot(), "prices.yaml");
308
+ }
309
+ function defaultSearchYamlPath() {
310
+ return join(defaultsRoot(), "search.yaml");
311
+ }
312
+
313
+ // src/workflow.ts
314
+ function workflowNamesIn(files) {
315
+ return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => f.replace(/\.ya?ml$/, "")).sort();
316
+ }
216
317
  async function listWorkflowNames(projectRoot) {
217
318
  const dir = workflowsDir(projectRoot);
218
- const files = await listFiles(dir);
219
- return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => f.replace(/\.ya?ml$/, "")).sort();
319
+ return workflowNamesIn(await listFiles(dir));
320
+ }
321
+ async function listBundledWorkflowNames() {
322
+ return workflowNamesIn(await listFiles(defaultWorkflowsDir()));
323
+ }
324
+ async function missingBundledWorkflows(projectRoot) {
325
+ const present = new Set(await listWorkflowNames(projectRoot));
326
+ return (await listBundledWorkflowNames()).filter((name) => !present.has(name));
327
+ }
328
+ function workflowUnavailableMessage(name, available, bundled) {
329
+ const listed = available.join(", ") || "(none)";
330
+ const base = `Workflow "${name}" is not available. Available: ${listed}`;
331
+ if (!bundled.includes(name)) return base;
332
+ return `${base}. This CLI ships ${name}. Run \`sdd init --here --force\` to copy new defaults.`;
220
333
  }
221
334
  async function loadWorkflow(projectRoot, name) {
222
335
  const dir = workflowsDir(projectRoot);
223
336
  const candidates = [`${name}.yaml`, `${name}.yml`];
224
337
  for (const file of candidates) {
225
- const path = join(dir, file);
338
+ const path = join2(dir, file);
226
339
  if (await pathExists(path)) {
227
340
  const raw = await readYaml(path);
228
341
  const workflow = WorkflowSchema.parse(raw);
@@ -231,9 +344,14 @@ async function loadWorkflow(projectRoot, name) {
231
344
  return workflow;
232
345
  }
233
346
  }
234
- throw new Error(
235
- `Workflow "${name}" not found in ${dir}. Available: ${(await listWorkflowNames(projectRoot)).join(", ") || "(none)"}`
236
- );
347
+ const available = await listWorkflowNames(projectRoot);
348
+ const bundled = await listBundledWorkflowNames();
349
+ const listed = available.join(", ") || "(none)";
350
+ let message = `Workflow "${name}" not found in ${dir}. Available: ${listed}`;
351
+ if (bundled.includes(name)) {
352
+ message += `. This CLI ships ${name}. Run \`sdd init --here --force\` to copy new defaults.`;
353
+ }
354
+ throw new Error(message);
237
355
  }
238
356
  function resolveStages(workflow, meta) {
239
357
  const base = [...workflow.stages];
@@ -314,7 +432,7 @@ async function recommendWorkflow(projectRoot, title, config, opts) {
314
432
  if (opts?.preferred) {
315
433
  if (!available.includes(opts.preferred)) {
316
434
  throw new Error(
317
- `Workflow "${opts.preferred}" is not available. Available: ${available.join(", ") || "(none)"}`
435
+ workflowUnavailableMessage(opts.preferred, available, await listBundledWorkflowNames())
318
436
  );
319
437
  }
320
438
  if (config.allowed_workflows?.length && !config.allowed_workflows.includes(opts.preferred)) {
@@ -460,7 +578,18 @@ export {
460
578
  nowIso,
461
579
  changeDirName,
462
580
  uniqueChangeDirName,
581
+ defaultsRoot,
582
+ defaultWorkflowsDir,
583
+ defaultTeamsDir,
584
+ defaultTemplatesDir,
585
+ defaultMemoryDir,
586
+ defaultMcpYamlPath,
587
+ defaultPricesYamlPath,
588
+ defaultSearchYamlPath,
463
589
  listWorkflowNames,
590
+ listBundledWorkflowNames,
591
+ missingBundledWorkflows,
592
+ workflowUnavailableMessage,
464
593
  loadWorkflow,
465
594
  resolveStages,
466
595
  isStageSkipped,
@@ -3,7 +3,7 @@ import {
3
3
  gatherMcpContext,
4
4
  gatherMcpContextForChange,
5
5
  listMatchingSourceIds
6
- } from "./chunk-QGP43XCN.js";
6
+ } from "./chunk-EDXT3RTN.js";
7
7
  import "./chunk-4BRO7BOJ.js";
8
8
  import "./chunk-MHODNFIL.js";
9
9
  import "./chunk-O5SP6X5R.js";