@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,9 +1,10 @@
1
1
  import {
2
2
  ConfigSchema,
3
+ defaultTeamsDir,
3
4
  getStage,
4
5
  isStageSkipped,
5
6
  shouldAutoSkip
6
- } from "./chunk-QHV5KZKX.js";
7
+ } from "./chunk-TNWFMRIA.js";
7
8
  import {
8
9
  SddError
9
10
  } from "./chunk-MHODNFIL.js";
@@ -15,11 +16,16 @@ import {
15
16
  ensureDir,
16
17
  guidancePaths,
17
18
  indexDbPath,
19
+ listDirs,
20
+ listFiles,
18
21
  memoryPathRel,
19
22
  pathExists,
23
+ readText,
20
24
  readYaml,
25
+ removePath,
21
26
  sddRoot,
22
27
  searchConfigPath,
28
+ writeText,
23
29
  writeYaml
24
30
  } from "./chunk-O5SP6X5R.js";
25
31
 
@@ -101,6 +107,56 @@ async function getHeadSha(projectRoot, runGit = runGitHead) {
101
107
  return sha.toLowerCase();
102
108
  }
103
109
 
110
+ // src/role/types.ts
111
+ function emptyRoleLists() {
112
+ return {
113
+ stages: [],
114
+ owns: [],
115
+ inputs: [],
116
+ outputs: [],
117
+ collaborates: [],
118
+ reviews: [],
119
+ approvers: [],
120
+ never: [],
121
+ askHumanWhen: [],
122
+ skills: [],
123
+ rules: [],
124
+ goals: []
125
+ };
126
+ }
127
+ var WORK_TYPES = [
128
+ "route",
129
+ "classify",
130
+ "check",
131
+ "draft",
132
+ "design",
133
+ "code",
134
+ "review"
135
+ ];
136
+ function isWorkType(value) {
137
+ return WORK_TYPES.includes(value);
138
+ }
139
+ function workKind(workType) {
140
+ return workType === "route" || workType === "classify" || workType === "check" ? "decide" : "reason";
141
+ }
142
+ var ROLE_TEXT = {
143
+ needsJob: "A role needs a job.",
144
+ notFound: "Role not found.",
145
+ exists: "Role already exists.",
146
+ badName: "A role name is not valid.",
147
+ self: "A role cannot review itself.",
148
+ x: "X is not a role tool.",
149
+ needsSection: "This needs an accepted section before a draft.",
150
+ noModel: "No role model is configured.",
151
+ cannot: "A role cannot do that.",
152
+ askFirst: "Ask first. This stays stopped.",
153
+ choice: "Allow once, Always allow, or Deny.",
154
+ unavailable: "This action is not available in this slice.",
155
+ expired: "Expired. The action did not run.",
156
+ runNotFound: "Run not found.",
157
+ badYaml: "role.yaml is invalid"
158
+ };
159
+
104
160
  // src/search/tokenize.ts
105
161
  import { encode } from "gpt-tokenizer/encoding/cl100k_base";
106
162
  function countTokens(text) {
@@ -111,8 +167,8 @@ function ftsTokens(query) {
111
167
  let current = "";
112
168
  for (const ch of query) {
113
169
  const code = ch.codePointAt(0) ?? 0;
114
- const ok = code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code > 127;
115
- if (ok) {
170
+ const ok3 = code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code > 127;
171
+ if (ok3) {
116
172
  current += ch;
117
173
  } else if (current) {
118
174
  tokens.push(current);
@@ -2416,6 +2472,737 @@ async function executeSearch(projectRoot, config, query, scope, budget) {
2416
2472
  return provider.search(query, options);
2417
2473
  }
2418
2474
 
2475
+ // src/role/models/schema.ts
2476
+ import { join as join6 } from "pathe";
2477
+ import { parse as parseYaml } from "yaml";
2478
+ import { z as z2 } from "zod";
2479
+ var idSchema = z2.string().trim().min(1);
2480
+ var envName = z2.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
2481
+ var confidence = z2.number().gte(0).lte(1);
2482
+ var WorkTypeSchema = z2.enum(WORK_TYPES);
2483
+ var workSlots = z2.record(WorkTypeSchema, idSchema);
2484
+ var ModelEntrySchema = z2.discriminatedUnion("provider", [
2485
+ z2.object({
2486
+ kind: z2.literal("decide"),
2487
+ provider: z2.literal("rules")
2488
+ }).strict(),
2489
+ z2.object({
2490
+ kind: z2.enum(["decide", "reason"]),
2491
+ provider: z2.literal("command"),
2492
+ command_env: envName,
2493
+ min_confidence: confidence.optional()
2494
+ }).strict(),
2495
+ z2.object({
2496
+ kind: z2.literal("decide"),
2497
+ provider: z2.literal("ollama"),
2498
+ model: z2.string().trim().min(1),
2499
+ base_url_env: envName,
2500
+ min_confidence: confidence.optional()
2501
+ }).strict()
2502
+ ]);
2503
+ var ModelsConfigSchema = z2.object({
2504
+ version: z2.literal(1),
2505
+ models: z2.record(idSchema, ModelEntrySchema),
2506
+ defaults: z2.object({
2507
+ by_work_type: workSlots,
2508
+ fallback_reason: idSchema,
2509
+ fallback_decide: idSchema
2510
+ }).strict(),
2511
+ overrides: z2.object({
2512
+ bots: z2.record(idSchema, workSlots).optional(),
2513
+ stages: z2.record(idSchema, workSlots).optional(),
2514
+ roles: z2.record(idSchema, workSlots).optional()
2515
+ }).strict().optional()
2516
+ }).strict().superRefine((config, ctx) => {
2517
+ if (Object.keys(config.models).length === 0) {
2518
+ ctx.addIssue({ code: "custom", path: ["models"], message: "needs a model" });
2519
+ }
2520
+ expectSlot(ctx, config.models, config.defaults.fallback_reason, "reason", [
2521
+ "defaults",
2522
+ "fallback_reason"
2523
+ ]);
2524
+ expectSlot(ctx, config.models, config.defaults.fallback_decide, "decide", [
2525
+ "defaults",
2526
+ "fallback_decide"
2527
+ ]);
2528
+ expectMap(ctx, config.models, config.defaults.by_work_type, ["defaults", "by_work_type"]);
2529
+ expectNamed(ctx, config.models, config.overrides?.bots, ["overrides", "bots"]);
2530
+ expectNamed(ctx, config.models, config.overrides?.stages, ["overrides", "stages"]);
2531
+ expectNamed(ctx, config.models, config.overrides?.roles, ["overrides", "roles"]);
2532
+ });
2533
+ var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
2534
+ var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=\S+$/;
2535
+ function expectSlot(ctx, models, id, kind, path) {
2536
+ if (!id) return;
2537
+ const model = models[id];
2538
+ if (!model) {
2539
+ ctx.addIssue({ code: "custom", path, message: "unknown model" });
2540
+ return;
2541
+ }
2542
+ if (model.kind !== kind) {
2543
+ ctx.addIssue({ code: "custom", path, message: `kind must be ${kind}` });
2544
+ }
2545
+ }
2546
+ function expectMap(ctx, models, map, path) {
2547
+ if (!map) return;
2548
+ for (const [work, id] of Object.entries(map)) {
2549
+ if (!isWorkType(work)) continue;
2550
+ expectSlot(ctx, models, id, workKind(work), [...path, work]);
2551
+ }
2552
+ }
2553
+ function expectNamed(ctx, models, named, path) {
2554
+ if (!named) return;
2555
+ for (const [name, map] of Object.entries(named)) {
2556
+ expectMap(ctx, models, map, [...path, name]);
2557
+ }
2558
+ }
2559
+ function formatModelsError(error) {
2560
+ const issue = error.issues[0];
2561
+ const path = issue?.path.join(".") || "models";
2562
+ const message = issue?.message ?? "invalid";
2563
+ return `models.yaml is invalid: ${path} ${message}`;
2564
+ }
2565
+ function looksSecret(value) {
2566
+ if (value.includes("-----BEGIN")) return true;
2567
+ if (!/\s/.test(value) && ASSIGNMENT.test(value)) return true;
2568
+ if (!/\s/.test(value) && value.length >= 32 && !ENV_NAME.test(value)) return true;
2569
+ return false;
2570
+ }
2571
+ function containsSecret(value) {
2572
+ if (typeof value === "string") return looksSecret(value);
2573
+ if (Array.isArray(value)) return value.some(containsSecret);
2574
+ if (value && typeof value === "object") {
2575
+ return Object.entries(value).some(([key, entry]) => {
2576
+ if (key === "model" && typeof entry === "string") {
2577
+ if (entry.includes("-----BEGIN")) return true;
2578
+ if (!/\s/.test(entry) && ASSIGNMENT.test(entry)) return true;
2579
+ return false;
2580
+ }
2581
+ return containsSecret(entry);
2582
+ });
2583
+ }
2584
+ return false;
2585
+ }
2586
+ function modelsPath(root) {
2587
+ return join6(root, ".sdd", "models.yaml");
2588
+ }
2589
+ async function loadModels(root) {
2590
+ const path = modelsPath(root);
2591
+ if (!await pathExists(path)) return null;
2592
+ let raw;
2593
+ try {
2594
+ raw = parseYaml(await readText(path));
2595
+ } catch {
2596
+ return { error: "models.yaml is invalid: models could not be parsed" };
2597
+ }
2598
+ if (containsSecret(raw)) return { error: "models.yaml has a secret value." };
2599
+ const parsed = ModelsConfigSchema.safeParse(raw);
2600
+ if (!parsed.success) return { error: formatModelsError(parsed.error) };
2601
+ return { config: parsed.data };
2602
+ }
2603
+ async function checkModels(root) {
2604
+ const loaded = await loadModels(root);
2605
+ if (loaded === null) return { ok: true };
2606
+ if ("error" in loaded) return { ok: false, message: loaded.error };
2607
+ return { ok: true };
2608
+ }
2609
+
2610
+ // src/role/profile.ts
2611
+ import { readFile as readFile4, writeFile } from "fs/promises";
2612
+ import { join as join7 } from "pathe";
2613
+ import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
2614
+ var NAME_LIMIT = 80;
2615
+ function canonicalRoleName(input) {
2616
+ const name = input.trim();
2617
+ if (!name || name === "." || name === "..") return null;
2618
+ if (name.length > NAME_LIMIT) return null;
2619
+ if (name.includes("/") || name.includes("\\") || name.includes("\0")) return null;
2620
+ return name;
2621
+ }
2622
+ function roleFilePath(root, name) {
2623
+ return join7(root, ".sdd", "roles", `${name}.md`);
2624
+ }
2625
+ function roleContextDir(root, name) {
2626
+ return join7(root, ".sdd", "role-context", name);
2627
+ }
2628
+ function conversationDir(root, name) {
2629
+ return join7(root, ".sdd", "role-runs", "conversation", name);
2630
+ }
2631
+ function memoryDir(root, name, team = false) {
2632
+ if (team) return join7(root, ".sdd", "agents", name);
2633
+ return join7(root, ".sdd", "role-runs", "memory", name);
2634
+ }
2635
+ function allowFilePath(root) {
2636
+ return join7(root, ".sdd", "role-runs", "allow.json");
2637
+ }
2638
+ function ok(stdout = "") {
2639
+ return { exitCode: 0, stdout };
2640
+ }
2641
+ function fail(stdout) {
2642
+ return { exitCode: 1, stdout };
2643
+ }
2644
+ function asText(value) {
2645
+ return typeof value === "string" ? value.trim() : "";
2646
+ }
2647
+ var LIST_FIELDS = [
2648
+ ["stages", "stages"],
2649
+ ["owns", "owns"],
2650
+ ["inputs", "inputs"],
2651
+ ["outputs", "outputs"],
2652
+ ["collaborates", "collaborates"],
2653
+ ["reviews", "reviews"],
2654
+ ["approvers", "approvers"],
2655
+ ["never", "never"],
2656
+ ["askHumanWhen", "ask_human_when"],
2657
+ ["skills", "skills"],
2658
+ ["rules", "rules"],
2659
+ ["goals", "goals"]
2660
+ ];
2661
+ function stringList(value) {
2662
+ if (value === void 0) return [];
2663
+ if (!Array.isArray(value)) return null;
2664
+ const out = [];
2665
+ for (const item of value) {
2666
+ if (typeof item !== "string" || !item.trim()) return null;
2667
+ out.push(item.trim());
2668
+ }
2669
+ return out;
2670
+ }
2671
+ function parseRoleDocument(name, raw) {
2672
+ const text = raw.replace(/^\uFEFF/, "");
2673
+ const nl = text.indexOf("\n");
2674
+ if (nl < 0 || text.slice(0, nl).trim() !== "---") return null;
2675
+ const rest = text.slice(nl + 1);
2676
+ const close = /\r?\n---\s*(?:\r?\n|$)/.exec(rest);
2677
+ if (!close) return null;
2678
+ let data;
2679
+ try {
2680
+ data = parseYaml2(rest.slice(0, close.index));
2681
+ } catch {
2682
+ return null;
2683
+ }
2684
+ if (!data || typeof data !== "object" || Array.isArray(data)) return null;
2685
+ const rec = data;
2686
+ const lists = emptyRoleLists();
2687
+ for (const [field2, key] of LIST_FIELDS) {
2688
+ const parsed = stringList(rec[key]);
2689
+ if (!parsed) return null;
2690
+ lists[field2] = parsed;
2691
+ }
2692
+ const body = rest.slice(close.index + close[0].length).trim();
2693
+ return {
2694
+ name,
2695
+ label: asText(rec.label),
2696
+ job: asText(rec.job),
2697
+ description: body,
2698
+ askFirst: rec.ask_first === true,
2699
+ pinned: rec.pinned === true,
2700
+ hidden: rec.hidden === true,
2701
+ ...lists
2702
+ };
2703
+ }
2704
+ function renderRole(role) {
2705
+ const record = {
2706
+ name: role.name,
2707
+ label: role.label,
2708
+ job: role.job,
2709
+ ask_first: role.askFirst,
2710
+ pinned: role.pinned,
2711
+ hidden: role.hidden
2712
+ };
2713
+ for (const [field2, key] of LIST_FIELDS) {
2714
+ if (role[field2].length > 0) record[key] = role[field2];
2715
+ }
2716
+ const front = stringifyYaml(record, { lineWidth: 100 }).trimEnd();
2717
+ const description = role.description.trim();
2718
+ if (!description) return `---
2719
+ ${front}
2720
+ ---
2721
+ `;
2722
+ return `---
2723
+ ${front}
2724
+ ---
2725
+ ${description}
2726
+ `;
2727
+ }
2728
+ async function rolesListingStage(root, stageId) {
2729
+ const dir = join7(root, ".sdd", "roles");
2730
+ if (!await pathExists(dir)) return [];
2731
+ const names = [];
2732
+ for (const file of (await listFiles(dir)).filter((item) => item.endsWith(".md")).sort()) {
2733
+ const name = file.slice(0, -".md".length);
2734
+ const role = await readRole(root, name);
2735
+ if (!role) return { error: `Role file is invalid: ${name}.` };
2736
+ if (role.stages.includes(stageId)) names.push(role.name);
2737
+ }
2738
+ return names;
2739
+ }
2740
+ async function readRole(root, name) {
2741
+ const canonical = canonicalRoleName(name);
2742
+ if (!canonical) return null;
2743
+ const path = roleFilePath(root, canonical);
2744
+ if (!await pathExists(path)) return null;
2745
+ try {
2746
+ return parseRoleDocument(canonical, await readText(path));
2747
+ } catch {
2748
+ return null;
2749
+ }
2750
+ }
2751
+ async function writeRole(root, role) {
2752
+ await writeText(roleFilePath(root, role.name), renderRole(role));
2753
+ }
2754
+ async function createRole(root, input) {
2755
+ const name = canonicalRoleName(input.name);
2756
+ if (!name) return fail(ROLE_TEXT.badName);
2757
+ const job = input.job.trim();
2758
+ if (!job) return fail(ROLE_TEXT.needsJob);
2759
+ if (await pathExists(roleFilePath(root, name))) return fail(ROLE_TEXT.exists);
2760
+ const lists = emptyRoleLists();
2761
+ for (const [field2] of LIST_FIELDS) {
2762
+ lists[field2] = input[field2] ?? [];
2763
+ }
2764
+ await writeRole(root, {
2765
+ name,
2766
+ label: input.label.trim(),
2767
+ job,
2768
+ description: (input.description ?? "").trim(),
2769
+ askFirst: input.askFirst === true,
2770
+ pinned: false,
2771
+ hidden: false,
2772
+ ...lists
2773
+ });
2774
+ return ok();
2775
+ }
2776
+ async function editRole(root, nameInput, patch) {
2777
+ const name = canonicalRoleName(nameInput);
2778
+ if (!name) return fail(ROLE_TEXT.badName);
2779
+ const role = await readRole(root, name);
2780
+ if (!role) return fail(ROLE_TEXT.notFound);
2781
+ if (patch.job !== void 0 && !patch.job.trim()) return fail(ROLE_TEXT.needsJob);
2782
+ if (patch.label !== void 0) role.label = patch.label.trim();
2783
+ if (patch.job !== void 0) role.job = patch.job.trim();
2784
+ if (patch.description !== void 0) role.description = patch.description.trim();
2785
+ if (patch.askFirst !== void 0) role.askFirst = patch.askFirst;
2786
+ if (patch.stages !== void 0) role.stages = patch.stages;
2787
+ await writeRole(root, role);
2788
+ return ok();
2789
+ }
2790
+ async function setFlag(root, nameInput, flag) {
2791
+ const name = canonicalRoleName(nameInput);
2792
+ if (!name) return fail(ROLE_TEXT.badName);
2793
+ const role = await readRole(root, name);
2794
+ if (!role) return fail(ROLE_TEXT.notFound);
2795
+ role[flag] = true;
2796
+ await writeRole(root, role);
2797
+ return ok();
2798
+ }
2799
+ function pinRole(root, name) {
2800
+ return setFlag(root, name, "pinned");
2801
+ }
2802
+ function hideRole(root, name) {
2803
+ return setFlag(root, name, "hidden");
2804
+ }
2805
+ async function showRoles(root, opts = {}) {
2806
+ if (opts.name !== void 0) {
2807
+ const name = canonicalRoleName(opts.name);
2808
+ if (!name) return fail(ROLE_TEXT.badName);
2809
+ const path = roleFilePath(root, name);
2810
+ if (!await pathExists(path) || !await readRole(root, name)) return fail(ROLE_TEXT.notFound);
2811
+ return ok((await readText(path)).replace(/\s*$/, ""));
2812
+ }
2813
+ const dir = join7(root, ".sdd", "roles");
2814
+ if (!await pathExists(dir)) return ok();
2815
+ const roles = [];
2816
+ for (const file of await listFiles(dir)) {
2817
+ if (!file.endsWith(".md")) continue;
2818
+ const role = await readRole(root, file.slice(0, -".md".length));
2819
+ if (!role) continue;
2820
+ if (role.hidden && !opts.all) continue;
2821
+ roles.push(role);
2822
+ }
2823
+ roles.sort((a, b) => {
2824
+ if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
2825
+ if (a.name < b.name) return -1;
2826
+ if (a.name > b.name) return 1;
2827
+ return 0;
2828
+ });
2829
+ return ok(roles.map((role) => `${role.name} ${role.label}`).join("\n"));
2830
+ }
2831
+ async function duplicateRole(root, nameInput) {
2832
+ const name = canonicalRoleName(nameInput);
2833
+ if (!name) return fail(ROLE_TEXT.badName);
2834
+ const source = await readRole(root, name);
2835
+ if (!source) return fail(ROLE_TEXT.notFound);
2836
+ const copyName = canonicalRoleName(`${name} copy`);
2837
+ if (!copyName) return fail(ROLE_TEXT.badName);
2838
+ if (await pathExists(roleFilePath(root, copyName))) return fail(ROLE_TEXT.exists);
2839
+ await writeRole(root, { ...source, name: copyName });
2840
+ return ok();
2841
+ }
2842
+ async function deleteRole(root, nameInput) {
2843
+ const name = canonicalRoleName(nameInput);
2844
+ if (!name) return fail(ROLE_TEXT.badName);
2845
+ const path = roleFilePath(root, name);
2846
+ if (!await pathExists(path)) return fail(ROLE_TEXT.notFound);
2847
+ await removePath(path);
2848
+ await removePath(conversationDir(root, name));
2849
+ await removePath(roleContextDir(root, name));
2850
+ await removeBotBrain(root, name);
2851
+ await removePath(memoryDir(root, name));
2852
+ await removeAllowKey(root, name);
2853
+ return ok();
2854
+ }
2855
+ async function removeBotBrain(root, roleName) {
2856
+ const teamPath = join7(root, ".sdd", "team.yaml");
2857
+ if (!await pathExists(teamPath)) return;
2858
+ let raw;
2859
+ try {
2860
+ raw = parseYaml2(await readText(teamPath));
2861
+ } catch {
2862
+ return;
2863
+ }
2864
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
2865
+ const bots = raw.bots;
2866
+ if (!Array.isArray(bots)) return;
2867
+ for (const bot of bots) {
2868
+ if (!bot || typeof bot !== "object" || Array.isArray(bot)) continue;
2869
+ const rec = bot;
2870
+ const botName = typeof rec.name === "string" ? rec.name : "";
2871
+ const botRole = typeof rec.role === "string" ? rec.role : "";
2872
+ if (botName !== roleName && botRole !== roleName) continue;
2873
+ if (!canonicalRoleName(botName)) continue;
2874
+ const dir = memoryDir(root, botName, true);
2875
+ await removePath(join7(dir, "memory.md"));
2876
+ await removePath(join7(dir, "lessons.jsonl"));
2877
+ }
2878
+ }
2879
+ async function removeAllowKey(root, name) {
2880
+ const path = allowFilePath(root);
2881
+ if (!await pathExists(path)) return;
2882
+ let data;
2883
+ try {
2884
+ data = JSON.parse(await readText(path));
2885
+ } catch {
2886
+ return;
2887
+ }
2888
+ if (!data || typeof data !== "object" || Array.isArray(data)) return;
2889
+ const rec = data;
2890
+ if (!(name in rec)) return;
2891
+ delete rec[name];
2892
+ await writeFile(path, `${JSON.stringify(rec, null, 2)}
2893
+ `, "utf8");
2894
+ }
2895
+ async function listConversationRoles(root) {
2896
+ return listDirs(join7(root, ".sdd", "role-runs", "conversation"));
2897
+ }
2898
+ async function readAllowFile(root) {
2899
+ const path = allowFilePath(root);
2900
+ if (!await pathExists(path)) return null;
2901
+ try {
2902
+ const data = JSON.parse(await readFile4(path, "utf8"));
2903
+ if (!data || typeof data !== "object" || Array.isArray(data)) return null;
2904
+ return data;
2905
+ } catch {
2906
+ return null;
2907
+ }
2908
+ }
2909
+
2910
+ // src/role/schema.ts
2911
+ import { z as z3 } from "zod";
2912
+ var nameSchema = z3.string().trim().min(1);
2913
+ var botSchema = z3.object({
2914
+ name: nameSchema,
2915
+ role: nameSchema,
2916
+ wip: z3.number().int().positive()
2917
+ }).strict();
2918
+ var humanSchema = z3.object({
2919
+ name: nameSchema
2920
+ }).strict();
2921
+ var TeamSchema = z3.object({
2922
+ template: nameSchema.optional(),
2923
+ branch: z3.string().trim().min(1),
2924
+ rooms: z3.object({
2925
+ commit: z3.boolean()
2926
+ }).strict(),
2927
+ bots: z3.array(botSchema).min(1),
2928
+ humans: z3.array(humanSchema).default([]),
2929
+ role_wip: z3.record(z3.string().trim().min(1), z3.number().int().positive()).optional(),
2930
+ limits: z3.object({
2931
+ loop_max_steps: z3.number().int().positive().optional(),
2932
+ room_rounds: z3.number().int().positive().optional()
2933
+ }).strict().refine((limits) => limits.loop_max_steps !== void 0 || limits.room_rounds !== void 0, {
2934
+ message: "needs a limit"
2935
+ }).optional()
2936
+ }).strict().superRefine((team, ctx) => {
2937
+ if (!team.branch.includes("{bot}")) {
2938
+ ctx.addIssue({
2939
+ code: "custom",
2940
+ path: ["branch"],
2941
+ message: "must include {bot}"
2942
+ });
2943
+ }
2944
+ const names = /* @__PURE__ */ new Set();
2945
+ for (const [index, bot] of team.bots.entries()) {
2946
+ if (names.has(bot.name)) {
2947
+ ctx.addIssue({
2948
+ code: "custom",
2949
+ path: ["bots", index, "name"],
2950
+ message: "bot name is repeated"
2951
+ });
2952
+ }
2953
+ names.add(bot.name);
2954
+ }
2955
+ });
2956
+ function formatTeamError(error) {
2957
+ const issue = error.issues[0];
2958
+ const path = issue?.path.join(".") || "team";
2959
+ const message = issue?.message ?? "invalid";
2960
+ return `team.yaml is invalid: ${path} ${message}`;
2961
+ }
2962
+
2963
+ // src/role/team.ts
2964
+ import { spawnSync } from "child_process";
2965
+ import { join as join8 } from "pathe";
2966
+ import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
2967
+ function ok2(stdout = "") {
2968
+ return { exitCode: 0, stdout };
2969
+ }
2970
+ function fail2(stdout) {
2971
+ return { exitCode: 1, stdout };
2972
+ }
2973
+ function gitUserName(root) {
2974
+ const result = spawnSync("git", ["-C", root, "config", "user.name"], { encoding: "utf8" });
2975
+ if (result.status !== 0) return null;
2976
+ const name = result.stdout.trim();
2977
+ return name || null;
2978
+ }
2979
+ async function listTeamTemplates() {
2980
+ return (await listDirs(defaultTeamsDir())).sort();
2981
+ }
2982
+ async function templateRoleFiles(template) {
2983
+ const dir = join8(defaultTeamsDir(), template, "roles");
2984
+ if (!await pathExists(dir)) return null;
2985
+ const files = (await listFiles(dir)).filter((file) => file.endsWith(".md")).sort();
2986
+ const roles = [];
2987
+ for (const file of files) {
2988
+ roles.push({ name: file.slice(0, -".md".length), raw: await readText(join8(dir, file)) });
2989
+ }
2990
+ return roles;
2991
+ }
2992
+ async function listRoleTemplates() {
2993
+ const ids = [];
2994
+ for (const template of await listTeamTemplates()) {
2995
+ const roles = await templateRoleFiles(template);
2996
+ if (!roles) continue;
2997
+ for (const role of roles) ids.push(`${template}/${role.name}`);
2998
+ }
2999
+ return ids;
3000
+ }
3001
+ async function loadTemplateTeam(template) {
3002
+ const path = join8(defaultTeamsDir(), template, "team.yaml");
3003
+ if (!await pathExists(path)) return `Team template "${template}" was not found.`;
3004
+ let raw;
3005
+ try {
3006
+ raw = parseYaml3(await readText(path));
3007
+ } catch {
3008
+ return `Team template "${template}" is invalid.`;
3009
+ }
3010
+ const parsed = TeamSchema.safeParse(raw);
3011
+ if (!parsed.success) return `Team template "${template}" is invalid.`;
3012
+ return parsed.data;
3013
+ }
3014
+ function availableTeams(names) {
3015
+ return names.join(", ") || "(none)";
3016
+ }
3017
+ async function initTeam(root, template, gitUser) {
3018
+ const templates = await listTeamTemplates();
3019
+ if (!template.trim()) {
3020
+ return fail2(`Pass --template. Available: ${availableTeams(templates)}.`);
3021
+ }
3022
+ if (!templates.includes(template)) {
3023
+ return fail2(
3024
+ `Team template "${template}" was not found. Available: ${availableTeams(templates)}.`
3025
+ );
3026
+ }
3027
+ const loaded = await loadTemplateTeam(template);
3028
+ if (typeof loaded === "string") return fail2(loaded);
3029
+ const roles = await templateRoleFiles(template);
3030
+ if (!roles || roles.length === 0) return fail2(`Team template "${template}" is invalid.`);
3031
+ for (const role of roles) {
3032
+ const parsed = parseRoleDocument(role.name, role.raw);
3033
+ if (!parsed || !parsed.job) return fail2(`Team template "${template}" is invalid.`);
3034
+ }
3035
+ const botRoles = new Set(loaded.bots.map((bot) => bot.role));
3036
+ for (const roleName of botRoles) {
3037
+ if (!roles.some((role) => role.name === roleName)) {
3038
+ return fail2(`Team template "${template}" is invalid.`);
3039
+ }
3040
+ }
3041
+ const teamPath = join8(root, ".sdd", "team.yaml");
3042
+ if (await pathExists(teamPath)) {
3043
+ return fail2("Team config already exists at .sdd/team.yaml. Nothing was written.");
3044
+ }
3045
+ const routingSrc = join8(defaultTeamsDir(), template, "routing.yaml");
3046
+ const routingDest = join8(root, ".sdd", "routing.yaml");
3047
+ const copyRouting = await pathExists(routingSrc);
3048
+ if (copyRouting && await pathExists(routingDest)) {
3049
+ return fail2("Routing config already exists at .sdd/routing.yaml. Nothing was written.");
3050
+ }
3051
+ const existing = [];
3052
+ for (const role of roles) {
3053
+ if (await pathExists(roleFilePath(root, role.name))) existing.push(role.name);
3054
+ }
3055
+ if (existing.length > 0) {
3056
+ return fail2(`Role already exists: ${existing.join(", ")}. Nothing was written.`);
3057
+ }
3058
+ const humans = loaded.humans.length > 0 ? loaded.humans : gitUser ? [{ name: gitUser }] : [];
3059
+ const team = { ...loaded, template, humans };
3060
+ const checked = TeamSchema.safeParse(team);
3061
+ if (!checked.success) return fail2(formatTeamError(checked.error));
3062
+ for (const role of roles) {
3063
+ await writeText(join8(root, ".sdd", "roles", `${role.name}.md`), role.raw);
3064
+ }
3065
+ await writeText(teamPath, stringifyYaml2(checked.data, { lineWidth: 100 }));
3066
+ if (copyRouting) await writeText(routingDest, await readText(routingSrc));
3067
+ await syncRoomIgnore(root, checked.data);
3068
+ return ok2(`Wrote .sdd/team.yaml from ${template}.`);
3069
+ }
3070
+ async function readTeamConfig(root) {
3071
+ const path = join8(root, ".sdd", "team.yaml");
3072
+ if (!await pathExists(path)) {
3073
+ return { error: "No team config. Run sdd team init --template software." };
3074
+ }
3075
+ let raw;
3076
+ try {
3077
+ raw = parseYaml3(await readText(path));
3078
+ } catch {
3079
+ return { error: "team.yaml is invalid: team could not be parsed" };
3080
+ }
3081
+ const parsed = TeamSchema.safeParse(raw);
3082
+ if (!parsed.success) return { error: formatTeamError(parsed.error) };
3083
+ await syncRoomIgnore(root, parsed.data);
3084
+ return { team: parsed.data };
3085
+ }
3086
+ function formatTeam(team, gitUser) {
3087
+ const lines = [
3088
+ `template: ${team.template ?? ""}`,
3089
+ `branch: ${team.branch}`,
3090
+ `rooms.commit: ${team.rooms.commit}`,
3091
+ "bots:"
3092
+ ];
3093
+ for (const bot of team.bots) {
3094
+ lines.push(`- ${bot.name} role ${bot.role} wip ${bot.wip}`);
3095
+ }
3096
+ lines.push("humans:");
3097
+ if (team.humans.length === 0) lines.push("- (none)");
3098
+ for (const human of team.humans) {
3099
+ const you = gitUser !== null && human.name === gitUser ? " (you)" : "";
3100
+ lines.push(`- ${human.name}${you}`);
3101
+ }
3102
+ if (team.role_wip && Object.keys(team.role_wip).length > 0) {
3103
+ lines.push("role_wip:");
3104
+ for (const name of Object.keys(team.role_wip).sort()) {
3105
+ lines.push(`- ${name} ${team.role_wip[name]}`);
3106
+ }
3107
+ }
3108
+ if (team.limits?.loop_max_steps) {
3109
+ lines.push(`limits.loop_max_steps: ${team.limits.loop_max_steps}`);
3110
+ }
3111
+ if (team.limits?.room_rounds) {
3112
+ lines.push(`limits.room_rounds: ${team.limits.room_rounds}`);
3113
+ }
3114
+ return lines.join("\n");
3115
+ }
3116
+ async function syncRoomIgnore(root, team) {
3117
+ const file = join8(root, ".sdd", ".gitignore");
3118
+ const existed = await pathExists(file);
3119
+ const original = existed ? await readText(file) : "";
3120
+ const lines = original.split(/\r?\n/);
3121
+ if (!team.rooms.commit) {
3122
+ if (lines.some((line) => line.trim() === "rooms/")) return;
3123
+ const next = original.length > 0 && !original.endsWith("\n") ? `${original}
3124
+ ` : original;
3125
+ await writeText(file, `${next}rooms/
3126
+ `);
3127
+ return;
3128
+ }
3129
+ if (!existed) return;
3130
+ const kept = lines.filter((line) => line.trim() !== "rooms/");
3131
+ if (kept.length === lines.length) return;
3132
+ const body = kept.join("\n");
3133
+ await writeText(file, body.length > 0 && !body.endsWith("\n") ? `${body}
3134
+ ` : body);
3135
+ }
3136
+ function resolveBot(team, name) {
3137
+ const byName = team.bots.find((bot) => bot.name === name);
3138
+ if (byName) return byName;
3139
+ const byRole = team.bots.filter((bot) => bot.role === name);
3140
+ if (byRole.length === 1) return byRole[0];
3141
+ if (byRole.length > 1) return { error: "Name the bot." };
3142
+ return { error: "Bot not found." };
3143
+ }
3144
+ async function showTeam(root, gitUser) {
3145
+ const loaded = await readTeamConfig(root);
3146
+ if ("error" in loaded) return fail2(loaded.error);
3147
+ return ok2(formatTeam(loaded.team, gitUser));
3148
+ }
3149
+ async function checkTeam(root) {
3150
+ const loaded = await readTeamConfig(root);
3151
+ if ("error" in loaded) return fail2(loaded.error);
3152
+ const models = await checkModels(root);
3153
+ if (!models.ok) return fail2(models.message);
3154
+ return ok2("Team config is valid.");
3155
+ }
3156
+ async function resolveRoleTemplate(template) {
3157
+ const ids = await listRoleTemplates();
3158
+ const trimmed = template.trim();
3159
+ if (!trimmed) {
3160
+ return { error: `Pass --template. Available: ${ids.join(", ") || "(none)"}.` };
3161
+ }
3162
+ if (ids.includes(trimmed)) {
3163
+ const [team, role] = trimmed.split("/");
3164
+ const roles = await templateRoleFiles(team);
3165
+ const found = roles?.find((item) => item.name === role);
3166
+ if (!found) return { error: `Role template "${trimmed}" was not found.` };
3167
+ return { id: trimmed, raw: found.raw };
3168
+ }
3169
+ const matches = ids.filter((id) => id.endsWith(`/${trimmed}`));
3170
+ if (matches.length === 1) {
3171
+ const resolved = await resolveRoleTemplate(matches[0]);
3172
+ return resolved;
3173
+ }
3174
+ if (matches.length > 1) {
3175
+ return {
3176
+ error: `Role template "${trimmed}" matches ${matches.join(" and ")}. Pass one of those.`
3177
+ };
3178
+ }
3179
+ return { error: `Role template "${trimmed}" was not found. Available: ${ids.join(", ")}.` };
3180
+ }
3181
+ async function createRoleFromTemplate(root, input) {
3182
+ const resolved = await resolveRoleTemplate(input.template);
3183
+ if ("error" in resolved) return fail2(resolved.error);
3184
+ const parsed = parseRoleDocument(input.name, resolved.raw);
3185
+ if (!parsed || !parsed.job) return fail2(`Role template "${resolved.id}" is invalid.`);
3186
+ return createRole(root, {
3187
+ name: input.name,
3188
+ label: input.label?.trim() || parsed.label,
3189
+ job: input.job?.trim() || parsed.job,
3190
+ description: input.description?.trim() || parsed.description,
3191
+ askFirst: parsed.askFirst,
3192
+ owns: parsed.owns,
3193
+ inputs: parsed.inputs,
3194
+ outputs: parsed.outputs,
3195
+ collaborates: parsed.collaborates,
3196
+ reviews: parsed.reviews,
3197
+ approvers: parsed.approvers,
3198
+ never: parsed.never,
3199
+ askHumanWhen: parsed.askHumanWhen,
3200
+ skills: parsed.skills,
3201
+ rules: parsed.rules,
3202
+ goals: parsed.goals
3203
+ });
3204
+ }
3205
+
2419
3206
  // src/agent-handoff.ts
2420
3207
  function formatStatus(ctx) {
2421
3208
  const lines = [];
@@ -2472,6 +3259,11 @@ async function buildAgentPrompt(ctx, config, projectRoot, budget, runtime) {
2472
3259
  }
2473
3260
  parts.push(`- Workflow: ${ctx.meta.workflow}`);
2474
3261
  parts.push(`- Change path: ${ctx.displayPath}`);
3262
+ if (stage?.owner) parts.push(`- Owner: ${stage.owner}`);
3263
+ if (stage?.collaborates?.length) parts.push(`- Works with: ${stage.collaborates.join(", ")}`);
3264
+ if (stage?.gate?.approvers?.length) {
3265
+ parts.push(`- Approvers: ${stage.gate.approvers.join(", ")}`);
3266
+ }
2475
3267
  parts.push("");
2476
3268
  const paths = guidancePaths(config);
2477
3269
  parts.push(`## Constraints`);
@@ -2530,7 +3322,7 @@ async function buildAgentPrompt(ctx, config, projectRoot, budget, runtime) {
2530
3322
  }
2531
3323
  try {
2532
3324
  const { loadMcpConfig } = await import("./sources-3TXUPU6V.js");
2533
- const { gatherMcpContextForChange, formatMcpContextForHandoff } = await import("./gather-ZNV2NAUN.js");
3325
+ const { gatherMcpContextForChange, formatMcpContextForHandoff } = await import("./gather-ZKVMJW5L.js");
2534
3326
  const mcpCfg = await loadMcpConfig(projectRoot);
2535
3327
  if (mcpCfg.auto_fetch_on_handoff && mcpCfg.sources.length) {
2536
3328
  const fetched = await gatherMcpContextForChange(projectRoot, ctx);
@@ -2580,6 +3372,10 @@ export {
2580
3372
  defaultConfig,
2581
3373
  mergeConfigDefaults,
2582
3374
  getHeadSha,
3375
+ WORK_TYPES,
3376
+ isWorkType,
3377
+ workKind,
3378
+ ROLE_TEXT,
2583
3379
  countTokens,
2584
3380
  ftsTokens,
2585
3381
  SearchConfigError,
@@ -2617,12 +3413,45 @@ export {
2617
3413
  keywordHits,
2618
3414
  listScopeHits,
2619
3415
  rankScope,
3416
+ searchProject,
2620
3417
  registerSearchProvider,
2621
3418
  clearRegisteredSearchProviders,
2622
3419
  fallbackWarning,
2623
3420
  resolveSearchProvider,
2624
3421
  buildRetrievedInner,
2625
3422
  executeSearch,
3423
+ ModelsConfigSchema,
3424
+ loadModels,
3425
+ checkModels,
3426
+ canonicalRoleName,
3427
+ roleFilePath,
3428
+ roleContextDir,
3429
+ conversationDir,
3430
+ memoryDir,
3431
+ allowFilePath,
3432
+ parseRoleDocument,
3433
+ rolesListingStage,
3434
+ readRole,
3435
+ createRole,
3436
+ editRole,
3437
+ pinRole,
3438
+ hideRole,
3439
+ showRoles,
3440
+ duplicateRole,
3441
+ deleteRole,
3442
+ listConversationRoles,
3443
+ readAllowFile,
3444
+ TeamSchema,
3445
+ gitUserName,
3446
+ listTeamTemplates,
3447
+ listRoleTemplates,
3448
+ initTeam,
3449
+ readTeamConfig,
3450
+ formatTeam,
3451
+ resolveBot,
3452
+ showTeam,
3453
+ checkTeam,
3454
+ createRoleFromTemplate,
2626
3455
  formatStatus,
2627
3456
  buildAgentPrompt
2628
3457
  };