@structured-vibe-coding/core 0.19.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.
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  mcpTimeoutMs,
12
12
  toProcessEnv,
13
13
  withMcpSource
14
- } from "./chunk-H55RFAEI.js";
14
+ } from "./chunk-EDXT3RTN.js";
15
15
  import {
16
16
  McpConfigSchema,
17
17
  McpInvokeSchema,
@@ -68,7 +68,7 @@ import {
68
68
  renderThinAgent,
69
69
  saveGeneratedManifest,
70
70
  writeAgentHandoff
71
- } from "./chunk-26YG6A4M.js";
71
+ } from "./chunk-O4OQWVC7.js";
72
72
  import {
73
73
  ChangeMetricsSchema,
74
74
  DecisionMetricSchema,
@@ -100,7 +100,7 @@ import {
100
100
  switchWorkflow,
101
101
  transitionPhase,
102
102
  updateFeatureStatus
103
- } from "./chunk-2FJBFYVJ.js";
103
+ } from "./chunk-NY5FFWXG.js";
104
104
  import {
105
105
  EMBEDDINGS_UNAVAILABLE,
106
106
  EMBEDDING_MODEL,
@@ -178,7 +178,9 @@ import {
178
178
  resolveBot,
179
179
  resolveEmbedder,
180
180
  resolveSearchProvider,
181
+ roleContextDir,
181
182
  roleFilePath,
183
+ rolesListingStage,
182
184
  rrf,
183
185
  saveConfig,
184
186
  scopeForPath,
@@ -188,7 +190,7 @@ import {
188
190
  showTeam,
189
191
  workKind,
190
192
  writePackedCache
191
- } from "./chunk-TGRV6WGS.js";
193
+ } from "./chunk-HUSXETPS.js";
192
194
  import {
193
195
  ArtifactDefSchema,
194
196
  ChangeMetaSchema,
@@ -232,7 +234,7 @@ import {
232
234
  todayIsoDate,
233
235
  uniqueChangeDirName,
234
236
  workflowUnavailableMessage
235
- } from "./chunk-HG7TPW5V.js";
237
+ } from "./chunk-TNWFMRIA.js";
236
238
  import {
237
239
  SddError,
238
240
  errorMessage,
@@ -1393,7 +1395,7 @@ ${output}`);
1393
1395
  status = failed.length ? "fail" : "pass";
1394
1396
  reason = failed.length ? `failed: ${failed.join(", ")}` : void 0;
1395
1397
  }
1396
- const ok8 = status === "pass";
1398
+ const ok10 = status === "pass";
1397
1399
  const checklist = stage.gate?.checklist ?? [];
1398
1400
  const lines = buildSummaryLines(
1399
1401
  ctx.meta.title,
@@ -1444,7 +1446,7 @@ ${output}`);
1444
1446
  ctx.meta.verify_results = {
1445
1447
  ...ctx.meta.verify_results,
1446
1448
  [stage.id]: {
1447
- ok: ok8,
1449
+ ok: ok10,
1448
1450
  status,
1449
1451
  ...results.length && headSha2 ? { head_sha: headSha2 } : {},
1450
1452
  at: nowIso(),
@@ -1460,7 +1462,7 @@ ${output}`);
1460
1462
  checklist,
1461
1463
  evidencePath: evidenceDir,
1462
1464
  resultsPath: summaryPath,
1463
- ok: ok8
1465
+ ok: ok10
1464
1466
  };
1465
1467
  }
1466
1468
 
@@ -1873,10 +1875,10 @@ function formatMarkdown(input) {
1873
1875
  `;
1874
1876
  return { markdown: packed ? frame + packed : frame, truncated: input.summary.truncated };
1875
1877
  }
1876
- function formatJsonSummary(summary, gaps, slices, ok8) {
1878
+ function formatJsonSummary(summary, gaps, slices, ok10) {
1877
1879
  return JSON.stringify(
1878
1880
  {
1879
- ok: ok8,
1881
+ ok: ok10,
1880
1882
  summary,
1881
1883
  gaps,
1882
1884
  slices: slices.map((s) => ({
@@ -2641,11 +2643,11 @@ Run: \`sdd refine${plan.focusStageId ? ` ${plan.focusStageId}` : ""}\`
2641
2643
  }
2642
2644
 
2643
2645
  // src/role/handoff.ts
2644
- import { join as join16 } from "pathe";
2646
+ import { join as join17 } from "pathe";
2645
2647
  import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
2646
2648
 
2647
2649
  // src/role/routing.ts
2648
- import { join as join15 } from "pathe";
2650
+ import { join as join16 } from "pathe";
2649
2651
  import { parse as parseYaml2 } from "yaml";
2650
2652
  import { z as z6 } from "zod";
2651
2653
 
@@ -3064,19 +3066,143 @@ async function decide(input) {
3064
3066
 
3065
3067
  // src/role/run.ts
3066
3068
  import { appendFile as appendFile5 } from "fs/promises";
3067
- import { isAbsolute as isAbsolute4, join as join14 } from "pathe";
3069
+ import { isAbsolute as isAbsolute4, join as join15 } from "pathe";
3068
3070
 
3069
- // src/team/facts.ts
3070
- import { appendFile } from "fs/promises";
3071
+ // src/role/context.ts
3071
3072
  import { join as join9 } from "pathe";
3073
+ var NO_SLICE = "No repo slice matched this role.";
3072
3074
  function ok(stdout = "") {
3073
3075
  return { exitCode: 0, stdout };
3074
3076
  }
3075
3077
  function fail(stdout) {
3076
3078
  return { exitCode: 1, stdout };
3077
3079
  }
3080
+ function contextPath(root, name) {
3081
+ return join9(roleContextDir(root, name), "context.md");
3082
+ }
3083
+ function knowledgePath(root, name) {
3084
+ return join9(roleContextDir(root, name), "knowledge.md");
3085
+ }
3086
+ async function readStoredContext(root, name) {
3087
+ const path = contextPath(root, name);
3088
+ if (!await pathExists(path)) return null;
3089
+ const raw = await readText(path);
3090
+ const match = /^---\r?\nsource: (provided|generated)\r?\n---\r?\n?/.exec(raw);
3091
+ if (!match) return { source: "provided", body: raw.replace(/\s*$/, "") };
3092
+ const source = match[1] === "generated" ? "generated" : "provided";
3093
+ return { source, body: raw.slice(match[0].length).replace(/\s*$/, "") };
3094
+ }
3095
+ async function roleContextBody(root, name) {
3096
+ const stored = await readStoredContext(root, name);
3097
+ return stored?.body.trim() ?? "";
3098
+ }
3099
+ async function readKnowledge(root, name) {
3100
+ const path = knowledgePath(root, name);
3101
+ if (!await pathExists(path)) return "";
3102
+ return (await readText(path)).trim();
3103
+ }
3104
+ async function writeKnowledge(root, name, text2) {
3105
+ await ensureDir(roleContextDir(root, name));
3106
+ const body = text2.trim();
3107
+ if (!body) {
3108
+ await removePath(knowledgePath(root, name));
3109
+ return;
3110
+ }
3111
+ await writeText(knowledgePath(root, name), `${body}
3112
+ `);
3113
+ }
3114
+ function renderStored(source, body) {
3115
+ return `---
3116
+ source: ${source}
3117
+ ---
3118
+ ${body.trim()}
3119
+ `;
3120
+ }
3121
+ function buildGeneratedContext(input) {
3122
+ const rows = [`Role: ${input.name}`, `Job: ${input.job}`];
3123
+ if (input.knowledge.trim()) rows.push("", "Knowledge:", input.knowledge.trim());
3124
+ rows.push("", "Repo:", input.repo.trim() || NO_SLICE);
3125
+ return rows.join("\n");
3126
+ }
3127
+ async function repoSlice(root, name, job, knowledge) {
3128
+ const query = [name, job, knowledge].filter((part) => part.trim()).join("\n");
3129
+ let seeds = [];
3130
+ try {
3131
+ const focused = await resolveFocus({ projectRoot: root, query });
3132
+ seeds = focused.plan.seedPaths.filter((rel) => !isSecretPath(rel)).slice(0, 12);
3133
+ } catch {
3134
+ seeds = [];
3135
+ }
3136
+ try {
3137
+ const found = await generateCodeContext({
3138
+ projectRoot: root,
3139
+ query,
3140
+ paths: seeds.length > 0 ? seeds : void 0,
3141
+ caps: { maxSlices: 8, maxTokensApprox: 2e3, maxLinesPerSlice: 40, maxFiles: 12 }
3142
+ });
3143
+ if (found.ok && found.slices.length > 0 && found.markdown.trim()) return found.markdown.trim();
3144
+ } catch {
3145
+ }
3146
+ const blocks = [];
3147
+ for (const rel of seeds) {
3148
+ const abs = join9(root, rel);
3149
+ if (!await pathExists(abs)) continue;
3150
+ let text2 = "";
3151
+ try {
3152
+ text2 = await readText(abs);
3153
+ } catch {
3154
+ continue;
3155
+ }
3156
+ const body = text2.split(/\r?\n/).slice(0, 40).join("\n").trim();
3157
+ if (!body) continue;
3158
+ blocks.push(`${rel}
3159
+ ${body}`);
3160
+ if (blocks.length >= 8) break;
3161
+ }
3162
+ return blocks.join("\n\n") || NO_SLICE;
3163
+ }
3164
+ async function setRoleContext(root, input) {
3165
+ const name = canonicalRoleName(input.name);
3166
+ if (!name) return fail(ROLE_TEXT.badName);
3167
+ const role = await readRole(root, name);
3168
+ if (!role) return fail(ROLE_TEXT.notFound);
3169
+ if (input.knowledge !== void 0) await writeKnowledge(root, name, input.knowledge);
3170
+ let knowledge = await readKnowledge(root, name);
3171
+ const extra = input.generate && input.text !== void 0 ? input.text.trim() : "";
3172
+ if (extra && input.knowledge === void 0) {
3173
+ await writeKnowledge(root, name, extra);
3174
+ knowledge = extra;
3175
+ } else if (extra) {
3176
+ knowledge = [knowledge, extra].filter((part) => part.length > 0).join("\n");
3177
+ }
3178
+ if (input.text !== void 0 && !input.generate) {
3179
+ const text2 = input.text.trim();
3180
+ if (!text2) return fail("A context needs text.");
3181
+ await ensureDir(roleContextDir(root, name));
3182
+ await writeText(contextPath(root, name), renderStored("provided", text2));
3183
+ return ok(text2);
3184
+ }
3185
+ const saved = await readStoredContext(root, name);
3186
+ if (!input.generate && input.knowledge !== void 0) return ok(saved?.body || knowledge);
3187
+ if (!input.generate && saved) return ok(saved.body);
3188
+ const repo = await repoSlice(root, name, role.job, knowledge);
3189
+ const body = buildGeneratedContext({ name: role.name, job: role.job, knowledge, repo });
3190
+ await ensureDir(roleContextDir(root, name));
3191
+ await writeText(contextPath(root, name), renderStored("generated", body));
3192
+ return ok(body);
3193
+ }
3194
+
3195
+ // src/team/facts.ts
3196
+ import { appendFile } from "fs/promises";
3197
+ import { join as join10 } from "pathe";
3198
+ function ok2(stdout = "") {
3199
+ return { exitCode: 0, stdout };
3200
+ }
3201
+ function fail2(stdout) {
3202
+ return { exitCode: 1, stdout };
3203
+ }
3078
3204
  function factsPath(root) {
3079
- return join9(root, ".sdd", "facts.jsonl");
3205
+ return join10(root, ".sdd", "facts.jsonl");
3080
3206
  }
3081
3207
  function sharesWords(left, right) {
3082
3208
  const bag = wordsOf(left);
@@ -3135,13 +3261,13 @@ async function setFact(root, input) {
3135
3261
  const rawValue = input.value ?? "";
3136
3262
  const rawSource = input.source ?? "";
3137
3263
  if (rawKey.includes("\n") || rawKey.includes("\r") || rawValue.includes("\n") || rawValue.includes("\r") || rawSource.includes("\n") || rawSource.includes("\r")) {
3138
- return fail("A fact is one line.");
3264
+ return fail2("A fact is one line.");
3139
3265
  }
3140
3266
  const key = rawKey.trim();
3141
3267
  const value = rawValue.trim();
3142
- if (!key || !value) return fail("A fact needs a key and a value.");
3268
+ if (!key || !value) return fail2("A fact needs a key and a value.");
3143
3269
  if (key.includes("/") || key.includes("\\") || key.includes("\0")) {
3144
- return fail("A fact key is not valid.");
3270
+ return fail2("A fact key is not valid.");
3145
3271
  }
3146
3272
  if (await contradicts(root, value)) return escalate(root, key);
3147
3273
  const fact = {
@@ -3152,33 +3278,33 @@ async function setFact(root, input) {
3152
3278
  source: rawSource.trim() || "person"
3153
3279
  };
3154
3280
  const file = factsPath(root);
3155
- await ensureDir(join9(file, ".."));
3281
+ await ensureDir(join10(file, ".."));
3156
3282
  await appendFile(file, `${JSON.stringify(fact)}
3157
3283
  `, "utf8");
3158
- return ok("Fact set.");
3284
+ return ok2("Fact set.");
3159
3285
  }
3160
3286
  async function showFact(root, key) {
3161
3287
  const name = (key ?? "").trim();
3162
- if (!name) return fail("A fact needs a key and a value.");
3288
+ if (!name) return fail2("A fact needs a key and a value.");
3163
3289
  const facts = await getFacts(root, { keys: [name] });
3164
3290
  const hit = facts[0];
3165
- if (!hit) return fail("Fact not set.");
3166
- return ok(hit.value);
3291
+ if (!hit) return fail2("Fact not set.");
3292
+ return ok2(hit.value);
3167
3293
  }
3168
3294
  async function listFacts(root, query) {
3169
3295
  const facts = await getFacts(root, query ? { query } : void 0);
3170
- if (facts.length === 0) return ok("No facts.");
3171
- return ok(facts.map((fact) => `${fact.key}: ${fact.value}`).join("\n"));
3296
+ if (facts.length === 0) return ok2("No facts.");
3297
+ return ok2(facts.map((fact) => `${fact.key}: ${fact.value}`).join("\n"));
3172
3298
  }
3173
3299
  async function contradicts(root, value) {
3174
3300
  if (!/\b(ignore|skip|override)\b/i.test(value)) return false;
3175
3301
  let text2 = "";
3176
3302
  try {
3177
3303
  const config = await loadConfig(root);
3178
- const file = join9(root, guidancePaths(config).constitution);
3304
+ const file = join10(root, guidancePaths(config).constitution);
3179
3305
  if (await pathExists(file)) text2 = await readText(file);
3180
3306
  } catch {
3181
- const file = join9(root, "memory", "constitution.md");
3307
+ const file = join10(root, "memory", "constitution.md");
3182
3308
  if (await pathExists(file)) text2 = await readText(file);
3183
3309
  }
3184
3310
  for (const line of text2.split(/\r?\n/)) {
@@ -3197,33 +3323,33 @@ async function escalate(root, key) {
3197
3323
  } catch {
3198
3324
  changeId = null;
3199
3325
  }
3200
- if (!changeId) return fail("A fact contradicts the constitution.");
3326
+ if (!changeId) return fail2("A fact contradicts the constitution.");
3201
3327
  return recordDecisionRequest(root, { note, change: changeId });
3202
3328
  }
3203
3329
 
3204
3330
  // src/team/memory.ts
3205
3331
  import { appendFile as appendFile2 } from "fs/promises";
3206
- import { join as join10 } from "pathe";
3332
+ import { join as join11 } from "pathe";
3207
3333
  var BRAIN_FILES = ["memory.md", "lessons.jsonl"];
3208
- function ok2(stdout = "") {
3334
+ function ok3(stdout = "") {
3209
3335
  return { exitCode: 0, stdout };
3210
3336
  }
3211
- function fail2(stdout) {
3337
+ function fail3(stdout) {
3212
3338
  return { exitCode: 1, stdout };
3213
3339
  }
3214
3340
  async function knownBot(root, name) {
3215
3341
  const team = await readTeamConfig(root);
3216
- if ("error" in team) return fail2(team.error);
3217
- if (!team.team.bots.some((bot) => bot.name === name)) return fail2("Bot not found.");
3342
+ if ("error" in team) return fail3(team.error);
3343
+ if (!team.team.bots.some((bot) => bot.name === name)) return fail3("Bot not found.");
3218
3344
  return null;
3219
3345
  }
3220
3346
  async function readMemory(root, bot) {
3221
- const file = join10(memoryDir2(root, bot, true), "memory.md");
3347
+ const file = join11(memoryDir2(root, bot, true), "memory.md");
3222
3348
  if (!await pathExists(file)) return "";
3223
3349
  return readText(file);
3224
3350
  }
3225
3351
  async function readLessons(root, bot) {
3226
- const file = join10(memoryDir2(root, bot, true), "lessons.jsonl");
3352
+ const file = join11(memoryDir2(root, bot, true), "lessons.jsonl");
3227
3353
  if (!await pathExists(file)) return [];
3228
3354
  const lessons = [];
3229
3355
  for (const line of (await readText(file)).split(/\r?\n/)) {
@@ -3247,35 +3373,35 @@ async function readLessons(root, bot) {
3247
3373
  return lessons;
3248
3374
  }
3249
3375
  async function appendLesson(root, bot, lesson) {
3250
- const file = join10(memoryDir2(root, bot, true), "lessons.jsonl");
3251
- await ensureDir(join10(file, ".."));
3376
+ const file = join11(memoryDir2(root, bot, true), "lessons.jsonl");
3377
+ await ensureDir(join11(file, ".."));
3252
3378
  await appendFile2(file, `${JSON.stringify(lesson)}
3253
3379
  `, "utf8");
3254
3380
  }
3255
3381
  async function showMemory(root, bot) {
3256
3382
  const name = canonicalRoleName(bot);
3257
- if (!name) return fail2(ROLE_TEXT.badName);
3383
+ if (!name) return fail3(ROLE_TEXT.badName);
3258
3384
  const missing = await knownBot(root, name);
3259
3385
  if (missing) return missing;
3260
3386
  const text2 = await readMemory(root, name);
3261
- if (!text2) return ok2("No memory.");
3262
- return ok2(text2.endsWith("\n") ? text2.slice(0, -1) : text2);
3387
+ if (!text2) return ok3("No memory.");
3388
+ return ok3(text2.endsWith("\n") ? text2.slice(0, -1) : text2);
3263
3389
  }
3264
3390
  async function addLesson(root, bot, text2) {
3265
3391
  const name = canonicalRoleName(bot);
3266
- if (!name) return fail2(ROLE_TEXT.badName);
3392
+ if (!name) return fail3(ROLE_TEXT.badName);
3267
3393
  const missing = await knownBot(root, name);
3268
3394
  if (missing) return missing;
3269
- if (text2.includes("\n") || text2.includes("\r")) return fail2("A lesson is one line.");
3395
+ if (text2.includes("\n") || text2.includes("\r")) return fail3("A lesson is one line.");
3270
3396
  const words = text2.trim();
3271
- if (!words) return fail2("A lesson needs text.");
3397
+ if (!words) return fail3("A lesson needs text.");
3272
3398
  await appendLesson(root, name, {
3273
3399
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3274
3400
  by: gitUserName(root) ?? "unknown",
3275
3401
  text: words,
3276
3402
  source: "person"
3277
3403
  });
3278
- return ok2("Lesson added.");
3404
+ return ok3("Lesson added.");
3279
3405
  }
3280
3406
  async function addReviewerLesson(root, bot, lesson) {
3281
3407
  const text2 = lesson.text.trim();
@@ -3291,30 +3417,30 @@ async function addReviewerLesson(root, bot, lesson) {
3291
3417
  }
3292
3418
  async function migrateTeamMemory(root) {
3293
3419
  const team = await readTeamConfig(root);
3294
- if ("error" in team) return fail2(team.error);
3295
- const base = join10(root, ".sdd", "role-runs", "memory");
3420
+ if ("error" in team) return fail3(team.error);
3421
+ const base = join11(root, ".sdd", "role-runs", "memory");
3296
3422
  const lines = [];
3297
3423
  for (const name of (await listDirs(base)).sort()) {
3298
3424
  if (!canonicalRoleName(name)) continue;
3299
3425
  for (const file of BRAIN_FILES) {
3300
- const from = join10(base, name, file);
3426
+ const from = join11(base, name, file);
3301
3427
  if (!await pathExists(from)) continue;
3302
3428
  const fromRel = `.sdd/role-runs/memory/${name}/${file}`;
3303
3429
  const toRel = `.sdd/agents/${name}/${file}`;
3304
- const to = join10(memoryDir2(root, name, true), file);
3430
+ const to = join11(memoryDir2(root, name, true), file);
3305
3431
  if (await pathExists(to)) {
3306
3432
  lines.push(`left ${fromRel}`);
3307
3433
  continue;
3308
3434
  }
3309
- await ensureDir(join10(to, ".."));
3435
+ await ensureDir(join11(to, ".."));
3310
3436
  await writeText(to, await readText(from));
3311
3437
  await removePath(from);
3312
3438
  lines.push(`moved ${fromRel} -> ${toRel}`);
3313
3439
  }
3314
- if ((await listFiles(join10(base, name))).length === 0) await removePath(join10(base, name));
3440
+ if ((await listFiles(join11(base, name))).length === 0) await removePath(join11(base, name));
3315
3441
  }
3316
- if (lines.length === 0) return ok2("Nothing to migrate.");
3317
- return ok2(lines.join("\n"));
3442
+ if (lines.length === 0) return ok3("Nothing to migrate.");
3443
+ return ok3(lines.join("\n"));
3318
3444
  }
3319
3445
 
3320
3446
  // src/team/brain.ts
@@ -3327,11 +3453,13 @@ async function loadBrain(root, cfg, bot, item, budgetTokens) {
3327
3453
  const file = roleFilePath(root, spec.role);
3328
3454
  if (!await pathExists(file)) return { error: ROLE_TEXT.notFound };
3329
3455
  const roleText = await readText(file);
3456
+ const context = await roleContextBody(root, spec.role);
3330
3457
  const memoryAll = await readMemory(root, spec.name);
3331
3458
  const lessonsNewest = (await readLessons(root, spec.name)).slice().reverse();
3332
3459
  const factsNewest = (await getFacts(root, { query: item.text })).slice().reverse();
3333
3460
  const packed = pack({
3334
3461
  roleText,
3462
+ context,
3335
3463
  memoryAll,
3336
3464
  lessons: lessonsNewest,
3337
3465
  facts: factsNewest,
@@ -3353,12 +3481,15 @@ function pack(input) {
3353
3481
  let memory = input.memoryAll;
3354
3482
  let lessons = input.lessons.slice();
3355
3483
  let facts = input.facts.slice();
3484
+ let context = input.context;
3356
3485
  let warning = "";
3357
3486
  const itemBlock = `Item:
3358
3487
  ${input.item}`;
3359
3488
  const render = () => {
3360
3489
  const parts = [input.roleText.trimEnd()];
3361
3490
  if (warning) parts.push(warning);
3491
+ if (context.trim()) parts.push(`Context:
3492
+ ${context.trimEnd()}`);
3362
3493
  if (memory.trim()) parts.push(`Memory:
3363
3494
  ${memory.trimEnd()}`);
3364
3495
  if (lessons.length > 0) {
@@ -3377,9 +3508,11 @@ ${facts.map((fact) => `${fact.key}: ${fact.value}`).join("\n")}`);
3377
3508
  memory = "";
3378
3509
  lessons = [];
3379
3510
  facts = [];
3511
+ context = "";
3380
3512
  warning = input.memoryAll.trim() ? WARNING : "";
3381
3513
  return { memory, lessons, facts, prompt: render(), warning };
3382
3514
  }
3515
+ if (countTokens(render()) > input.budget && context.trim()) context = "";
3383
3516
  while (countTokens(render()) > input.budget && facts.length > 0) facts.pop();
3384
3517
  while (countTokens(render()) > input.budget && lessons.length > 0) lessons.pop();
3385
3518
  if (countTokens(render()) > input.budget && memory.trim()) {
@@ -3400,12 +3533,12 @@ ${facts.map((fact) => `${fact.key}: ${fact.value}`).join("\n")}`);
3400
3533
 
3401
3534
  // src/team/loop.ts
3402
3535
  import { appendFile as appendFile4 } from "fs/promises";
3403
- import { basename, join as join13 } from "pathe";
3536
+ import { basename, join as join14 } from "pathe";
3404
3537
  import { z as z5 } from "zod";
3405
3538
 
3406
3539
  // src/team/room.ts
3407
3540
  import { appendFile as appendFile3 } from "fs/promises";
3408
- import { join as join12 } from "pathe";
3541
+ import { join as join13 } from "pathe";
3409
3542
  import { z as z4 } from "zod";
3410
3543
 
3411
3544
  // src/role/prompts.ts
@@ -3434,7 +3567,7 @@ function revisionPrompt(role, draft, gaps) {
3434
3567
  import { spawn as spawn4 } from "child_process";
3435
3568
  import { mkdtemp, rm } from "fs/promises";
3436
3569
  import { tmpdir } from "os";
3437
- import { isAbsolute as isAbsolute3, join as join11 } from "pathe";
3570
+ import { isAbsolute as isAbsolute3, join as join12 } from "pathe";
3438
3571
  import { z as z3 } from "zod";
3439
3572
  var OUTPUT_CAP = 4e3;
3440
3573
  var SCOPES = /* @__PURE__ */ new Set(["code", "specs", "memory"]);
@@ -3475,8 +3608,8 @@ function asRecord(value) {
3475
3608
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
3476
3609
  return value;
3477
3610
  }
3478
- function result(name, ok8, text2) {
3479
- return { ok: ok8, name, text: clip(text2) };
3611
+ function result(name, ok10, text2) {
3612
+ return { ok: ok10, name, text: clip(text2) };
3480
3613
  }
3481
3614
  async function git(cwd, args) {
3482
3615
  return new Promise((resolve) => {
@@ -3525,7 +3658,7 @@ function evidenceRel(changesPath, changeId, sha) {
3525
3658
  }
3526
3659
  async function evidenceFile(root, changeId, sha) {
3527
3660
  const config = await loadConfig(root);
3528
- return join11(changePath(root, config, changeId), "evidence", `${sha}.json`);
3661
+ return join12(changePath(root, config, changeId), "evidence", `${sha}.json`);
3529
3662
  }
3530
3663
  async function evidenceSupports(root, changeId, cite) {
3531
3664
  const sha = await headOf(root);
@@ -3533,7 +3666,7 @@ async function evidenceSupports(root, changeId, cite) {
3533
3666
  const config = await loadConfig(root);
3534
3667
  const expected = evidenceRel(config.changes_path, changeId, sha);
3535
3668
  if (cite.trim().replace(/\\/g, "/").replace(/^\.\//, "") !== expected) return false;
3536
- const file = join11(root, ...expected.split("/"));
3669
+ const file = join12(root, ...expected.split("/"));
3537
3670
  if (!await pathExists(file)) return false;
3538
3671
  let parsed;
3539
3672
  try {
@@ -3564,7 +3697,7 @@ async function allowList(root) {
3564
3697
  const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
3565
3698
  for (const command of stage?.verify?.commands ?? []) stageRuns.push(command.run);
3566
3699
  }
3567
- const file = join11(root, ".sdd", "tools.yaml");
3700
+ const file = join12(root, ".sdd", "tools.yaml");
3568
3701
  if (!await pathExists(file)) return stageRuns;
3569
3702
  let raw;
3570
3703
  try {
@@ -3584,7 +3717,7 @@ async function readFileTool(root, args) {
3584
3717
  const rel = typeof args.path === "string" ? args.path : "";
3585
3718
  if (!safePath(rel)) return result("read_file", false, "That path is outside the project.");
3586
3719
  if (isSecretPath(rel)) return result("read_file", false, "That path is not readable.");
3587
- const file = join11(root, ...rel.split(/[/\\]/));
3720
+ const file = join12(root, ...rel.split(/[/\\]/));
3588
3721
  if (!await pathExists(file)) return result("read_file", false, "That path is not readable.");
3589
3722
  try {
3590
3723
  const text2 = await readText(file);
@@ -3641,7 +3774,7 @@ async function appendRun(root, changeId, sha, command, exitCode, output) {
3641
3774
  }
3642
3775
  }
3643
3776
  body.runs.push({ command, exitCode, output: clip(output) });
3644
- await ensureDir(join11(file, ".."));
3777
+ await ensureDir(join12(file, ".."));
3645
3778
  await writeText(file, `${JSON.stringify(body, null, 2)}
3646
3779
  `);
3647
3780
  const config = await loadConfig(root);
@@ -3661,7 +3794,7 @@ async function runCheck(root, args) {
3661
3794
  if (!change) return result("run_check", false, "No active change.");
3662
3795
  const sha = await headOf(root);
3663
3796
  if (!sha) return result("run_check", false, "Git HEAD is unavailable.");
3664
- const dir = await mkdtemp(join11(tmpdir(), "sdd-check-"));
3797
+ const dir = await mkdtemp(join12(tmpdir(), "sdd-check-"));
3665
3798
  const added = await git(root, ["worktree", "add", "--detach", dir, sha]);
3666
3799
  if (added.code !== 0) {
3667
3800
  await rm(dir, { recursive: true, force: true });
@@ -3688,7 +3821,7 @@ async function writeFileTool(root, args) {
3688
3821
  if (!safePath(rel) || typeof contents !== "string" || isSecretPath(rel)) {
3689
3822
  return result("write_file", false, "That file cannot be written.");
3690
3823
  }
3691
- await writeText(join11(root, ...rel.split(/[/\\]/)), contents);
3824
+ await writeText(join12(root, ...rel.split(/[/\\]/)), contents);
3692
3825
  return result("write_file", true, `Wrote ${rel}.`);
3693
3826
  }
3694
3827
  function branchName(team, bot, changeId) {
@@ -3764,10 +3897,10 @@ var RoomEntrySchema = z4.object({
3764
3897
  sha: z4.string().optional(),
3765
3898
  evidence: z4.array(z4.string()).optional()
3766
3899
  }).strict();
3767
- function ok3(stdout = "") {
3900
+ function ok4(stdout = "") {
3768
3901
  return { exitCode: 0, stdout };
3769
3902
  }
3770
- function fail3(stdout) {
3903
+ function fail4(stdout) {
3771
3904
  return { exitCode: 1, stdout };
3772
3905
  }
3773
3906
  function safeId(value) {
@@ -3775,13 +3908,13 @@ function safeId(value) {
3775
3908
  }
3776
3909
  function oneLine(value, empty, broken) {
3777
3910
  const raw = value ?? "";
3778
- if (raw.includes("\n") || raw.includes("\r")) return fail3(broken);
3911
+ if (raw.includes("\n") || raw.includes("\r")) return fail4(broken);
3779
3912
  const words = raw.trim();
3780
- if (!words) return fail3(empty);
3913
+ if (!words) return fail4(empty);
3781
3914
  return words;
3782
3915
  }
3783
3916
  function roomPath(root, change, stage) {
3784
- return join12(root, ".sdd", "rooms", change, `${stage}.jsonl`);
3917
+ return join13(root, ".sdd", "rooms", change, `${stage}.jsonl`);
3785
3918
  }
3786
3919
  function roomRounds(team) {
3787
3920
  return team.limits?.room_rounds ?? 2;
@@ -3820,21 +3953,28 @@ async function openChange(root, change) {
3820
3953
  try {
3821
3954
  config = await loadConfig(root);
3822
3955
  } catch (error) {
3823
- return fail3(error instanceof Error ? error.message : "Config is invalid.");
3956
+ return fail4(error instanceof Error ? error.message : "Config is invalid.");
3824
3957
  }
3825
3958
  const id = (change?.trim() || await getActiveChangeId(root, config) || "").trim();
3826
- if (!id) return fail3("No active change.");
3827
- if (!safeId(id)) return fail3("A change id is not valid.");
3959
+ if (!id) return fail4("No active change.");
3960
+ if (!safeId(id)) return fail4("A change id is not valid.");
3828
3961
  try {
3829
3962
  const ctx = await buildContext(root, config, id);
3830
3963
  return { id, path: ctx.path, stage: ctx.meta.stage, workflow: ctx.workflow, meta: ctx.meta };
3831
3964
  } catch (error) {
3832
- return fail3(error instanceof Error ? error.message : "Change not found.");
3965
+ return fail4(error instanceof Error ? error.message : "Change not found.");
3833
3966
  }
3834
3967
  }
3835
3968
  function isFailure(value) {
3836
3969
  return "exitCode" in value;
3837
3970
  }
3971
+ function stageMemberNames(stage, listed) {
3972
+ const named = [stage.owner, ...stage.collaborates ?? []].filter(
3973
+ (name) => !!name
3974
+ );
3975
+ if (named.length > 0) return named;
3976
+ return listed;
3977
+ }
3838
3978
  function memberBots(team, names) {
3839
3979
  const bots = [];
3840
3980
  const seen = /* @__PURE__ */ new Set();
@@ -3852,7 +3992,7 @@ function memberBots(team, names) {
3852
3992
  async function draftOf(changePath2, stage) {
3853
3993
  const rel = stage?.artifacts[0]?.path;
3854
3994
  if (!rel || rel.includes("..")) return "";
3855
- const file = join12(changePath2, rel);
3995
+ const file = join13(changePath2, rel);
3856
3996
  if (!await pathExists(file)) return "";
3857
3997
  return readText(file);
3858
3998
  }
@@ -3861,7 +4001,7 @@ function formatEntry(entry) {
3861
4001
  return `${entry.round} ${entry.from} ${entry.type}${cite} ${entry.text}`;
3862
4002
  }
3863
4003
  async function appendEntry(file, entry) {
3864
- await ensureDir(join12(file, ".."));
4004
+ await ensureDir(join13(file, ".."));
3865
4005
  await appendFile3(file, `${JSON.stringify(entry)}
3866
4006
  `, "utf8");
3867
4007
  }
@@ -3870,36 +4010,36 @@ async function consensusLine(changePath2, stage, sha, entries) {
3870
4010
  const agreed = found.agreed.join(",") || "none";
3871
4011
  const open = found.open.map((entry) => entry.cite || entry.text).join(",") || "none";
3872
4012
  await appendFile3(
3873
- join12(changePath2, "decisions.md"),
4013
+ join13(changePath2, "decisions.md"),
3874
4014
  `${nowIso()} room ${stage} ${sha ?? "none"} agreed ${agreed} open ${open}
3875
4015
  `
3876
4016
  );
3877
4017
  }
3878
4018
  async function showRoom(root, input) {
3879
4019
  const team = await readTeamConfig(root);
3880
- if ("error" in team) return fail3(team.error);
4020
+ if ("error" in team) return fail4(team.error);
3881
4021
  const opened = await openChange(root, input.change);
3882
4022
  if (isFailure(opened)) return opened;
3883
4023
  const stage = (input.stage?.trim() || opened.stage).trim();
3884
- if (!safeId(stage)) return fail3("A stage id is not valid.");
4024
+ if (!safeId(stage)) return fail4("A stage id is not valid.");
3885
4025
  const entries = await readEntries(roomPath(root, opened.id, stage));
3886
- if (entries.length === 0) return ok3("No room.");
3887
- return ok3(entries.map(formatEntry).join("\n"));
4026
+ if (entries.length === 0) return ok4("No room.");
4027
+ return ok4(entries.map(formatEntry).join("\n"));
3888
4028
  }
3889
4029
  async function postRoom(root, input) {
3890
4030
  const team = await readTeamConfig(root);
3891
- if ("error" in team) return fail3(team.error);
4031
+ if ("error" in team) return fail4(team.error);
3892
4032
  const opened = await openChange(root, input.change);
3893
4033
  if (isFailure(opened)) return opened;
3894
4034
  const stage = (input.stage?.trim() || opened.stage).trim();
3895
- if (!safeId(stage)) return fail3("A stage id is not valid.");
4035
+ if (!safeId(stage)) return fail4("A stage id is not valid.");
3896
4036
  const text2 = oneLine(input.text, "A room entry needs text.", "A room entry is one line.");
3897
4037
  if (typeof text2 !== "string") return text2;
3898
4038
  const type = input.type?.trim() || "post";
3899
- if (!ROOM_TYPES.includes(type)) return fail3("A room entry is not valid.");
4039
+ if (!ROOM_TYPES.includes(type)) return fail4("A room entry is not valid.");
3900
4040
  const cite = input.cite?.trim() ?? "";
3901
- if (cite && (cite.includes("\n") || cite.includes("\r"))) return fail3("A cite is one line.");
3902
- if (type === "objection" && !cite) return fail3("An objection needs a cite.");
4041
+ if (cite && (cite.includes("\n") || cite.includes("\r"))) return fail4("A cite is one line.");
4042
+ if (type === "objection" && !cite) return fail4("An objection needs a cite.");
3903
4043
  const stageDef = getStage(opened.workflow, opened.meta, stage);
3904
4044
  const needed = await reviewNeedsEvidence(
3905
4045
  root,
@@ -3909,7 +4049,7 @@ async function postRoom(root, input) {
3909
4049
  text2,
3910
4050
  cite
3911
4051
  );
3912
- if (needed) return fail3(needed);
4052
+ if (needed) return fail4(needed);
3913
4053
  const evidence = [];
3914
4054
  for (const item of input.evidence ?? []) {
3915
4055
  const line = oneLine(item, "Evidence needs text.", "Evidence is one line.");
@@ -3931,12 +4071,12 @@ async function postRoom(root, input) {
3931
4071
  if (sha) entry.sha = sha;
3932
4072
  if (evidence.length > 0) entry.evidence = evidence;
3933
4073
  await appendEntry(file, entry);
3934
- return ok3("Posted.");
4074
+ return ok4("Posted.");
3935
4075
  }
3936
4076
  async function classifyReply(root, text2, env, runCommand2) {
3937
4077
  const loaded = await loadModels(root);
3938
4078
  if (loaded === null) return "post";
3939
- if ("error" in loaded) return fail3(loaded.error);
4079
+ if ("error" in loaded) return fail4(loaded.error);
3940
4080
  const decision = await decide({
3941
4081
  models: loaded.config,
3942
4082
  ctx: { workType: "classify" },
@@ -3950,31 +4090,38 @@ async function classifyReply(root, text2, env, runCommand2) {
3950
4090
  return decision.pick;
3951
4091
  }
3952
4092
  async function reviewRoom(root, input) {
3953
- if (!input.model) return fail3(ROLE_TEXT.noModel);
4093
+ if (!input.model) return fail4(ROLE_TEXT.noModel);
3954
4094
  const loaded = await readTeamConfig(root);
3955
- if ("error" in loaded) return fail3(loaded.error);
4095
+ if ("error" in loaded) return fail4(loaded.error);
3956
4096
  const opened = await openChange(root, input.change);
3957
4097
  if (isFailure(opened)) return opened;
3958
4098
  const stageId = (input.stage?.trim() || opened.stage).trim();
3959
- if (!safeId(stageId)) return fail3("A stage id is not valid.");
4099
+ if (!safeId(stageId)) return fail4("A stage id is not valid.");
3960
4100
  const stage = getStage(opened.workflow, opened.meta, stageId);
3961
- if (!stage) return fail3("Stage not found.");
3962
- const names = input.roles && input.roles.length > 0 ? input.roles : [stage.owner, ...stage.collaborates ?? []].filter((name) => !!name);
4101
+ if (!stage) return fail4("Stage not found.");
4102
+ const baked = stageMemberNames(stage, []);
4103
+ let listed = [];
4104
+ if ((!input.roles || input.roles.length === 0) && baked.length === 0) {
4105
+ const found = await rolesListingStage(root, stageId);
4106
+ if ("error" in found) return fail4(found.error);
4107
+ listed = found;
4108
+ }
4109
+ const names = input.roles && input.roles.length > 0 ? input.roles : stageMemberNames(stage, listed);
3963
4110
  const bots = memberBots(loaded.team, names);
3964
- if ("error" in bots) return fail3(bots.error);
4111
+ if ("error" in bots) return fail4(bots.error);
3965
4112
  const file = roomPath(root, opened.id, stageId);
3966
4113
  const prior = await readEntries(file);
3967
4114
  const highest = prior.reduce((max, entry) => Math.max(max, entry.round), 0);
3968
4115
  const round = highest + 1;
3969
4116
  const cap = roomRounds(loaded.team);
3970
- if (round > cap) return ok3("Round limit reached.");
4117
+ if (round > cap) return ok4("Round limit reached.");
3971
4118
  const draft = await draftOf(opened.path, stage);
3972
4119
  const before = consensus(prior.filter((entry) => entry.round === highest));
3973
4120
  const sha = await headSha(root);
3974
4121
  const written = [];
3975
4122
  for (const bot of bots) {
3976
4123
  const role = await readRole(root, bot.role);
3977
- if (!role) return fail3(ROLE_TEXT.notFound);
4124
+ if (!role) return fail4(ROLE_TEXT.notFound);
3978
4125
  const section = {
3979
4126
  id: stageId,
3980
4127
  decision: "room",
@@ -3984,7 +4131,8 @@ async function reviewRoom(root, input) {
3984
4131
  if (section.checks.length === 0) section.checks = ["room"];
3985
4132
  const rows = [critiquePrompt(role, section, draft || "(none)"), "Room:"];
3986
4133
  for (const entry2 of prior) rows.push(formatEntry(entry2));
3987
- const owner = stage.owner === bot.role || stage.owner === bot.name;
4134
+ const listedOwner = stage.owner ?? (listed.length === 1 ? listed[0] : void 0);
4135
+ const owner = listedOwner === bot.role || listedOwner === bot.name;
3988
4136
  if (owner && before.open.length > 0) {
3989
4137
  rows.push(
3990
4138
  revisionPrompt(
@@ -4000,7 +4148,7 @@ async function reviewRoom(root, input) {
4000
4148
  try {
4001
4149
  raw = await input.model.complete({ roleId: role.name, prompt: rows.join("\n") });
4002
4150
  } catch {
4003
- return fail3("A room entry is not valid.");
4151
+ return fail4("A room entry is not valid.");
4004
4152
  }
4005
4153
  const stored = await entryFromReply(root, raw, input.env, input.runCommand);
4006
4154
  if (isFailure(stored)) return stored;
@@ -4012,7 +4160,7 @@ async function reviewRoom(root, input) {
4012
4160
  stored.text,
4013
4161
  stored.cite ?? ""
4014
4162
  );
4015
- if (needed) return fail3(needed);
4163
+ if (needed) return fail4(needed);
4016
4164
  const entry = {
4017
4165
  ts: nowIso(),
4018
4166
  from: bot.name,
@@ -4033,9 +4181,9 @@ async function reviewRoom(root, input) {
4033
4181
  note: `Room ${stageId} still open after ${round} rounds.`
4034
4182
  });
4035
4183
  if (asked.exitCode !== 0) return asked;
4036
- return ok3("Decision needed.");
4184
+ return ok4("Decision needed.");
4037
4185
  }
4038
- return ok3(`Round ${round}.`);
4186
+ return ok4(`Round ${round}.`);
4039
4187
  }
4040
4188
  async function entryFromReply(root, raw, env = process.env, runCommand2) {
4041
4189
  let value;
@@ -4052,18 +4200,18 @@ async function entryFromReply(root, raw, env = process.env, runCommand2) {
4052
4200
  }
4053
4201
  const text2 = raw.trim();
4054
4202
  if (!text2 || text2.includes("\n") || text2.includes("\r"))
4055
- return fail3("A room entry is not valid.");
4203
+ return fail4("A room entry is not valid.");
4056
4204
  const typed = await classifyReply(root, text2, env, runCommand2);
4057
4205
  if (typeof typed !== "string") return typed;
4058
4206
  return checked(typed, text2, "");
4059
4207
  }
4060
4208
  function checked(type, text2, cite) {
4061
- if (!ROOM_TYPES.includes(type)) return fail3("A room entry is not valid.");
4209
+ if (!ROOM_TYPES.includes(type)) return fail4("A room entry is not valid.");
4062
4210
  const words = oneLine(text2, "A room entry needs text.", "A room entry is one line.");
4063
4211
  if (typeof words !== "string") return words;
4064
4212
  const citeWords = cite.trim();
4065
- if (citeWords && (cite.includes("\n") || cite.includes("\r"))) return fail3("A cite is one line.");
4066
- if (type === "objection" && !citeWords) return fail3("An objection needs a cite.");
4213
+ if (citeWords && (cite.includes("\n") || cite.includes("\r"))) return fail4("A cite is one line.");
4214
+ if (type === "objection" && !citeWords) return fail4("An objection needs a cite.");
4067
4215
  return citeWords ? { type, text: words, cite: citeWords } : { type, text: words };
4068
4216
  }
4069
4217
  async function activeRoom(root) {
@@ -4143,14 +4291,14 @@ var LoopStepSchema = z5.discriminatedUnion("step", [
4143
4291
  askStep,
4144
4292
  doneStep
4145
4293
  ]);
4146
- function ok4(stdout = "") {
4294
+ function ok5(stdout = "") {
4147
4295
  return { exitCode: 0, stdout };
4148
4296
  }
4149
- function fail4(stdout) {
4297
+ function fail5(stdout) {
4150
4298
  return { exitCode: 1, stdout };
4151
4299
  }
4152
4300
  async function appendLine(file, value) {
4153
- await ensureDir(join13(file, ".."));
4301
+ await ensureDir(join14(file, ".."));
4154
4302
  await appendFile4(file, `${JSON.stringify(value)}
4155
4303
  `, "utf8");
4156
4304
  }
@@ -4184,13 +4332,13 @@ ${piece}`.trim().slice(-8e3);
4184
4332
  raw = await input.model.complete({ roleId: input.roleName, prompt: promptFor() });
4185
4333
  } catch {
4186
4334
  await appendLine(input.file, { type: "bad_call", roleId: input.roleName });
4187
- return fail4("A loop step is not valid.");
4335
+ return fail5("A loop step is not valid.");
4188
4336
  }
4189
4337
  taken += 1;
4190
4338
  const step = parseStep(raw);
4191
4339
  if (!step) {
4192
4340
  await appendLine(input.file, { type: "bad_call", roleId: input.roleName });
4193
- return fail4("A loop step is not valid.");
4341
+ return fail5("A loop step is not valid.");
4194
4342
  }
4195
4343
  if (step.kind === "understand") {
4196
4344
  await appendLine(input.file, { type: "step", step: "understand" });
@@ -4242,7 +4390,7 @@ ${piece}`.trim().slice(-8e3);
4242
4390
  await appendLine(input.file, { type: "step", step: "done", summary: body.summary });
4243
4391
  await copyReviewer();
4244
4392
  await postLoopDone(input.root, input.brain.bot.name, body.summary, cites[0]);
4245
- return ok4(body.summary);
4393
+ return ok5(body.summary);
4246
4394
  }
4247
4395
  }
4248
4396
  return ask(input.root, input.file, `Stopped after ${max} steps.`, ["continue", "stop"]);
@@ -4253,7 +4401,7 @@ async function ask(root, file, question, options) {
4253
4401
  if (note && !note.includes("\n") && !note.includes("\r")) {
4254
4402
  await recordDecisionRequest(root, { note });
4255
4403
  }
4256
- return ok4(question);
4404
+ return ok5(question);
4257
4405
  }
4258
4406
  function parseStep(raw) {
4259
4407
  let value;
@@ -4292,7 +4440,7 @@ async function priorLesson(root, logName, file, roleName) {
4292
4440
  const names = (await listFiles(dir)).filter((item) => item.endsWith(".jsonl")).sort();
4293
4441
  const index = names.indexOf(basename(file));
4294
4442
  if (index <= 0) return null;
4295
- const prior = join13(dir, names[index - 1]);
4443
+ const prior = join14(dir, names[index - 1]);
4296
4444
  if (!await pathExists(prior)) return null;
4297
4445
  let fromChange = null;
4298
4446
  let fromOther = null;
@@ -4330,10 +4478,10 @@ var FORBIDDEN = /* @__PURE__ */ new Set([
4330
4478
  ]);
4331
4479
  var UNAVAILABLE = /* @__PURE__ */ new Set(["commit", "push", "tag", "pull_request", "merge", "deploy"]);
4332
4480
  var CLOSED = /* @__PURE__ */ new Set(["applied", "denied", "rejected", "unavailable", "stopped", "expired"]);
4333
- function ok5(stdout = "", runId) {
4481
+ function ok6(stdout = "", runId) {
4334
4482
  return runId ? { exitCode: 0, stdout, runId } : { exitCode: 0, stdout };
4335
4483
  }
4336
- function fail5(stdout, runId) {
4484
+ function fail6(stdout, runId) {
4337
4485
  return runId ? { exitCode: 1, stdout, runId } : { exitCode: 1, stdout };
4338
4486
  }
4339
4487
  function linesOf(...parts) {
@@ -4470,7 +4618,7 @@ function parseReply(raw) {
4470
4618
  return { text: rec.text, gaps, changed, action };
4471
4619
  }
4472
4620
  async function appendJson(file, value) {
4473
- await ensureDir(join14(file, ".."));
4621
+ await ensureDir(join15(file, ".."));
4474
4622
  await appendFile5(file, `${JSON.stringify(value)}
4475
4623
  `, "utf8");
4476
4624
  }
@@ -4487,7 +4635,7 @@ async function readJsonLines(file) {
4487
4635
  return lines;
4488
4636
  }
4489
4637
  async function timeoutMinutes(root) {
4490
- const path = join14(root, ".sdd", "role.yaml");
4638
+ const path = join15(root, ".sdd", "role.yaml");
4491
4639
  if (!await pathExists(path)) return 10;
4492
4640
  let data;
4493
4641
  try {
@@ -4506,13 +4654,21 @@ async function timeoutMinutes(root) {
4506
4654
  function citeOk(cite, section) {
4507
4655
  return section.checks.includes(cite) || cite === `${section.id}:${section.decision}`;
4508
4656
  }
4509
- function authorPrompt(role, task, kind, section) {
4510
- const rows = [
4511
- `Job: ${role.job}`,
4512
- `Description: ${role.description}`,
4513
- `Task: ${task}`,
4514
- `Kind: ${kind}`
4515
- ];
4657
+ function purviewLines(role) {
4658
+ const rows = [];
4659
+ if (role.stages.length > 0) rows.push(`Stages: ${role.stages.join(", ")}`);
4660
+ if (role.owns.length > 0) rows.push(`Owns: ${role.owns.join(", ")}`);
4661
+ if (role.skills.length > 0) rows.push(`Skills: ${role.skills.join(", ")}`);
4662
+ if (role.never.length > 0) rows.push(`Never: ${role.never.join(", ")}`);
4663
+ if (role.askHumanWhen.length > 0) rows.push(`Ask when: ${role.askHumanWhen.join(", ")}`);
4664
+ if (rows.length > 0) rows.push("Stay inside this role.");
4665
+ return rows;
4666
+ }
4667
+ function authorPrompt(role, task, kind, section, context = "", guide = false) {
4668
+ const rows = [`Job: ${role.job}`, `Description: ${role.description}`, ...purviewLines(role)];
4669
+ if (context.trim()) rows.push(`Context:
4670
+ ${context.trim()}`);
4671
+ rows.push(`Task: ${task}`, `Kind: ${kind}`);
4516
4672
  if (section) {
4517
4673
  rows.push(
4518
4674
  `Section: ${section.id}`,
@@ -4522,12 +4678,32 @@ function authorPrompt(role, task, kind, section) {
4522
4678
  );
4523
4679
  rows.push("Section text:", section.body);
4524
4680
  }
4681
+ if (guide) {
4682
+ rows.push('If one fact is missing, reply with one line that starts with "Question: ".');
4683
+ rows.push("Otherwise write the draft for this stage.");
4684
+ }
4525
4685
  return rows.join("\n");
4526
4686
  }
4687
+ function neverBlocks(role, action) {
4688
+ const words = action.words.trim().toLowerCase();
4689
+ const name = action.name.trim().toLowerCase();
4690
+ return role.never.some((item) => {
4691
+ const line = item.trim().toLowerCase();
4692
+ return line.length > 0 && (line === words || line === name);
4693
+ });
4694
+ }
4695
+ function lockedToChange(role) {
4696
+ return role.never.some((item) => item.trim().toLowerCase() === "write product code");
4697
+ }
4698
+ function outsideChange(changeDir, rel) {
4699
+ const path = rel.replace(/\\/g, "/").replace(/^\.\//, "");
4700
+ const dir = changeDir.replace(/\\/g, "/").replace(/\/$/, "");
4701
+ return path !== dir && !path.startsWith(`${dir}/`);
4702
+ }
4527
4703
  async function acceptedFile(root, changeId) {
4528
- if (!await pathExists(configPath(root))) return join14(root, "changes", changeId, "accepted.md");
4704
+ if (!await pathExists(configPath(root))) return join15(root, "changes", changeId, "accepted.md");
4529
4705
  const config = await loadConfig(root);
4530
- return join14(changesDir(root, config), changeId, "accepted.md");
4706
+ return join15(changesDir(root, config), changeId, "accepted.md");
4531
4707
  }
4532
4708
  async function callModel(model, file, roleId, prompt, pass) {
4533
4709
  let raw;
@@ -4582,24 +4758,51 @@ async function applyAction(root, file, roleName, action, mode) {
4582
4758
  await appendJson(file, { type: "rejected" });
4583
4759
  return { exitCode: 1, stdout: ROLE_TEXT.cannot, applied: false };
4584
4760
  }
4585
- await writeText(join14(root, ...path.split(/[/\\]/)), contents);
4761
+ await writeText(join15(root, ...path.split(/[/\\]/)), contents);
4586
4762
  if (mode === "always")
4587
4763
  await saveRule(root, roleName, { name: action.name, inputs: action.inputs });
4588
4764
  await appendJson(file, { type: "applied", name: "write_file" });
4589
4765
  return { exitCode: 0, stdout: "", applied: true };
4590
4766
  }
4591
- async function finishRun(root, file, role, final, minutes, now, runId) {
4767
+ async function finishGuideRun(root, file, role, final, runId, changeDir) {
4768
+ if (final.text.trim().startsWith("Question:")) return ok6(final.text, runId);
4769
+ const action = final.action;
4770
+ if (!action) return ok6(final.text, runId);
4771
+ if (FORBIDDEN.has(action.name) || UNAVAILABLE.has(action.name) || neverBlocks(role, action)) {
4772
+ await appendJson(file, { type: "rejected" });
4773
+ return fail6("This role does not do that.", runId);
4774
+ }
4775
+ if (action.name !== "write_file") {
4776
+ await appendJson(file, { type: "rejected" });
4777
+ return fail6(ROLE_TEXT.cannot, runId);
4778
+ }
4779
+ const path = action.inputs.path;
4780
+ const contents = action.inputs.contents;
4781
+ if (typeof path !== "string" || typeof contents !== "string" || !isSafeProductPath(path)) {
4782
+ await appendJson(file, { type: "rejected" });
4783
+ return fail6(ROLE_TEXT.cannot, runId);
4784
+ }
4785
+ if (isSecretPath(path) || lockedToChange(role) && outsideChange(changeDir, path)) {
4786
+ await appendJson(file, { type: "rejected" });
4787
+ return fail6("That file is outside this role.", runId);
4788
+ }
4789
+ const applied = await applyAction(root, file, role.name, action, "once");
4790
+ if (applied.exitCode !== 0) return fail6(applied.stdout || ROLE_TEXT.cannot, runId);
4791
+ return ok6(final.text, runId);
4792
+ }
4793
+ async function finishRun(root, file, role, final, minutes, now, runId, guide) {
4794
+ if (guide) return finishGuideRun(root, file, role, final, runId, guide.changeDir);
4592
4795
  const action = final.action;
4593
- if (!action) return ok5(final.text, runId);
4796
+ if (!action) return ok6(final.text, runId);
4594
4797
  if (FORBIDDEN.has(action.name) || !UNAVAILABLE.has(action.name) && action.name !== "write_file") {
4595
4798
  await appendJson(file, { type: "rejected" });
4596
- return fail5(linesOf(final.text, ROLE_TEXT.cannot), runId);
4799
+ return fail6(linesOf(final.text, ROLE_TEXT.cannot), runId);
4597
4800
  }
4598
4801
  const rule = await readRule(root, role.name);
4599
4802
  if (!role.askFirst && rule && rule.name === action.name && sameInputs(rule.inputs, action.inputs)) {
4600
4803
  const applied = await applyAction(root, file, role.name, action, "always");
4601
4804
  const stdout = applied.applied ? final.text : linesOf(final.text, applied.stdout);
4602
- return applied.exitCode === 0 ? ok5(stdout, runId) : fail5(stdout, runId);
4805
+ return applied.exitCode === 0 ? ok6(stdout, runId) : fail6(stdout, runId);
4603
4806
  }
4604
4807
  const expiresAt = new Date(now.getTime() + minutes * 6e4).toISOString();
4605
4808
  await appendJson(file, {
@@ -4607,12 +4810,12 @@ async function finishRun(root, file, role, final, minutes, now, runId) {
4607
4810
  action: { name: action.name, words: action.words, inputs: action.inputs },
4608
4811
  expiresAt
4609
4812
  });
4610
- if (role.askFirst) return ok5(linesOf(action.words, ROLE_TEXT.askFirst), runId);
4611
- return ok5(linesOf(action.words, ROLE_TEXT.choice), runId);
4813
+ if (role.askFirst) return ok6(linesOf(action.words, ROLE_TEXT.askFirst), runId);
4814
+ return ok6(linesOf(action.words, ROLE_TEXT.choice), runId);
4612
4815
  }
4613
4816
  async function settleRoleReply(root, file, role, reply, now, runId) {
4614
4817
  const minutes = await timeoutMinutes(root);
4615
- if (minutes === "invalid") return fail5(ROLE_TEXT.badYaml, runId);
4818
+ if (minutes === "invalid") return fail6(ROLE_TEXT.badYaml, runId);
4616
4819
  return finishRun(
4617
4820
  root,
4618
4821
  file,
@@ -4627,27 +4830,27 @@ async function latestRunFile(root, name) {
4627
4830
  const dir = conversationDir(root, name);
4628
4831
  const files = (await listFiles(dir)).filter((file) => file.endsWith(".jsonl")).sort();
4629
4832
  const last = files[files.length - 1];
4630
- return last ? join14(dir, last) : null;
4833
+ return last ? join15(dir, last) : null;
4631
4834
  }
4632
4835
  async function runRole(root, input) {
4633
4836
  const name = canonicalRoleName(input.name);
4634
- if (!name) return fail5(ROLE_TEXT.badName);
4837
+ if (!name) return fail6(ROLE_TEXT.badName);
4635
4838
  if (input.stop) {
4636
- if (!await readRole(root, name)) return fail5(ROLE_TEXT.notFound);
4839
+ if (!await readRole(root, name)) return fail6(ROLE_TEXT.notFound);
4637
4840
  const latest = await latestRunFile(root, name);
4638
- if (!latest) return fail5(ROLE_TEXT.runNotFound);
4841
+ if (!latest) return fail6(ROLE_TEXT.runNotFound);
4639
4842
  await appendJson(latest, { type: "stopped" });
4640
- return ok5();
4843
+ return ok6();
4641
4844
  }
4642
4845
  let roleName = name;
4643
4846
  let team = null;
4644
4847
  let botName = "";
4645
- if (await pathExists(join14(root, ".sdd", "team.yaml"))) {
4848
+ if (await pathExists(join15(root, ".sdd", "team.yaml"))) {
4646
4849
  const loaded = await readTeamConfig(root);
4647
- if ("error" in loaded) return fail5(loaded.error);
4850
+ if ("error" in loaded) return fail6(loaded.error);
4648
4851
  const resolved = resolveBot(loaded.team, name);
4649
4852
  if ("error" in resolved) {
4650
- if (resolved.error === "Name the bot.") return fail5(resolved.error);
4853
+ if (resolved.error === "Name the bot.") return fail6(resolved.error);
4651
4854
  } else {
4652
4855
  team = loaded.team;
4653
4856
  botName = resolved.name;
@@ -4655,41 +4858,41 @@ async function runRole(root, input) {
4655
4858
  }
4656
4859
  }
4657
4860
  const role = await readRole(root, roleName);
4658
- if (!role) return fail5(ROLE_TEXT.notFound);
4659
- if (!role.job.trim()) return fail5(ROLE_TEXT.needsJob);
4861
+ if (!role) return fail6(ROLE_TEXT.notFound);
4862
+ if (!role.job.trim()) return fail6(ROLE_TEXT.needsJob);
4660
4863
  const task = input.task ?? "";
4661
4864
  let withName = "";
4662
4865
  if (input.withRole !== void 0 && input.withRole.trim() !== "") {
4663
4866
  const canonical = canonicalRoleName(input.withRole);
4664
- if (!canonical) return fail5(ROLE_TEXT.badName);
4665
- if (canonical === name) return fail5(ROLE_TEXT.self);
4867
+ if (!canonical) return fail6(ROLE_TEXT.badName);
4868
+ if (canonical === name) return fail6(ROLE_TEXT.self);
4666
4869
  withName = canonical;
4667
4870
  }
4668
- if (mentionsX(task)) return fail5(ROLE_TEXT.x);
4871
+ if (mentionsX(task)) return fail6(ROLE_TEXT.x);
4669
4872
  const kind = input.kind?.trim() || "reading";
4670
4873
  let section;
4671
4874
  if (kind !== "reading") {
4672
- if (!DRAFT_KINDS.has(kind)) return fail5(ROLE_TEXT.needsSection);
4875
+ if (!DRAFT_KINDS.has(kind)) return fail6(ROLE_TEXT.needsSection);
4673
4876
  const changeId = input.change?.trim() ?? "";
4674
4877
  const sectionId = input.section?.trim() ?? "";
4675
- if (!safeChangeId(changeId) || !sectionId) return fail5(ROLE_TEXT.needsSection);
4878
+ if (!safeChangeId(changeId) || !sectionId) return fail6(ROLE_TEXT.needsSection);
4676
4879
  const acceptedPath = await acceptedFile(root, changeId);
4677
- if (!await pathExists(acceptedPath)) return fail5(ROLE_TEXT.needsSection);
4880
+ if (!await pathExists(acceptedPath)) return fail6(ROLE_TEXT.needsSection);
4678
4881
  const parsed = parseSection(await readText(acceptedPath), sectionId);
4679
- if (!parsed) return fail5(ROLE_TEXT.needsSection);
4882
+ if (!parsed) return fail6(ROLE_TEXT.needsSection);
4680
4883
  section = parsed;
4681
4884
  }
4682
4885
  let critic = null;
4683
4886
  if (withName && section) {
4684
4887
  critic = await readRole(root, withName);
4685
- if (!critic) return fail5(ROLE_TEXT.notFound);
4888
+ if (!critic) return fail6(ROLE_TEXT.notFound);
4686
4889
  }
4687
4890
  const minutes = await timeoutMinutes(root);
4688
- if (minutes === "invalid") return fail5(ROLE_TEXT.badYaml);
4689
- if (!input.model) return fail5(ROLE_TEXT.noModel);
4891
+ if (minutes === "invalid") return fail6(ROLE_TEXT.badYaml);
4892
+ if (!input.model) return fail6(ROLE_TEXT.noModel);
4690
4893
  const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
4691
4894
  const runId = runIdFrom(now);
4692
- const file = join14(conversationDir(root, name), `${runId}.jsonl`);
4895
+ const file = join15(conversationDir(root, name), `${runId}.jsonl`);
4693
4896
  if (section) {
4694
4897
  await appendJson(file, {
4695
4898
  type: "checks",
@@ -4698,17 +4901,17 @@ async function runRole(root, input) {
4698
4901
  decision: section.decision
4699
4902
  });
4700
4903
  }
4701
- if (team && botName) {
4904
+ if (team && botName && !input.guide) {
4702
4905
  let budget = 12e3;
4703
4906
  try {
4704
4907
  const search = await loadSearchConfig(root);
4705
4908
  budget = budgetForStage(search, void 0);
4706
4909
  } catch (error) {
4707
- if (error instanceof SearchConfigError) return fail5("search.yaml is invalid", runId);
4910
+ if (error instanceof SearchConfigError) return fail6("search.yaml is invalid", runId);
4708
4911
  throw error;
4709
4912
  }
4710
4913
  const brain = await loadBrain(root, team, botName, { text: task }, budget);
4711
- if ("error" in brain) return fail5(brain.error, runId);
4914
+ if ("error" in brain) return fail6(brain.error, runId);
4712
4915
  return runLoop({
4713
4916
  root,
4714
4917
  cfg: team,
@@ -4723,10 +4926,17 @@ async function runRole(root, input) {
4723
4926
  input.model,
4724
4927
  file,
4725
4928
  role.name,
4726
- authorPrompt(role, task, kind, section),
4929
+ authorPrompt(
4930
+ role,
4931
+ task,
4932
+ kind,
4933
+ section,
4934
+ await roleContextBody(root, role.name),
4935
+ input.guide !== void 0
4936
+ ),
4727
4937
  1
4728
4938
  );
4729
- if (!draft) return fail5("", runId);
4939
+ if (!draft) return fail6("", runId);
4730
4940
  let final = draft;
4731
4941
  if (critic && section) {
4732
4942
  const accepted = section;
@@ -4737,10 +4947,10 @@ async function runRole(root, input) {
4737
4947
  critiquePrompt(critic, accepted, draft.text),
4738
4948
  2
4739
4949
  );
4740
- if (!critique) return fail5(draft.text, runId);
4950
+ if (!critique) return fail6(draft.text, runId);
4741
4951
  if (critique.gaps.some((gap) => !citeOk(gap.cite, accepted))) {
4742
4952
  await appendJson(file, { type: "bad_call", roleId: critic.name });
4743
- return fail5(draft.text, runId);
4953
+ return fail6(draft.text, runId);
4744
4954
  }
4745
4955
  if (critique.gaps.length > 0) {
4746
4956
  const revision = await callModel(
@@ -4750,16 +4960,16 @@ async function runRole(root, input) {
4750
4960
  revisionPrompt(role, draft.text, critique.gaps),
4751
4961
  3
4752
4962
  );
4753
- if (!revision) return fail5(draft.text, runId);
4963
+ if (!revision) return fail6(draft.text, runId);
4754
4964
  const cites = critique.gaps.map((gap) => gap.cite);
4755
4965
  if (revision.changed === null || !sameCiteSet(revision.changed, cites)) {
4756
4966
  await appendJson(file, { type: "bad_call", roleId: role.name });
4757
- return fail5(draft.text, runId);
4967
+ return fail6(draft.text, runId);
4758
4968
  }
4759
4969
  final = revision;
4760
4970
  }
4761
4971
  }
4762
- return finishRun(root, file, role, final, minutes, now, runId);
4972
+ return finishRun(root, file, role, final, minutes, now, runId, input.guide);
4763
4973
  }
4764
4974
  function openPending(lines) {
4765
4975
  let pending = null;
@@ -4793,7 +5003,7 @@ async function findRun(root, runId) {
4793
5003
  if (!safeRunId(runId)) return null;
4794
5004
  const hits = [];
4795
5005
  for (const roleName of await listConversationRoles(root)) {
4796
- const file = join14(conversationDir(root, roleName), `${runId}.jsonl`);
5006
+ const file = join15(conversationDir(root, roleName), `${runId}.jsonl`);
4797
5007
  if (await pathExists(file)) hits.push({ file, roleName });
4798
5008
  }
4799
5009
  if (hits.length === 1) return hits[0];
@@ -4805,24 +5015,24 @@ async function findRun(root, runId) {
4805
5015
  }
4806
5016
  async function allowRole(root, input) {
4807
5017
  const found = await findRun(root, input.runId);
4808
- if (!found) return fail5(ROLE_TEXT.runNotFound);
5018
+ if (!found) return fail6(ROLE_TEXT.runNotFound);
4809
5019
  const pending = openPending(await readJsonLines(found.file));
4810
- if (!pending) return fail5(ROLE_TEXT.runNotFound);
5020
+ if (!pending) return fail6(ROLE_TEXT.runNotFound);
4811
5021
  const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
4812
5022
  const expires = Date.parse(pending.expiresAt);
4813
5023
  if (!Number.isFinite(expires) || now.getTime() >= expires) {
4814
5024
  await appendJson(found.file, { type: "expired" });
4815
- return fail5(ROLE_TEXT.expired);
5025
+ return fail6(ROLE_TEXT.expired);
4816
5026
  }
4817
5027
  if (input.mode === "deny") {
4818
5028
  await appendJson(found.file, { type: "denied" });
4819
- return ok5("", input.runId);
5029
+ return ok6("", input.runId);
4820
5030
  }
4821
5031
  const role = await readRole(root, found.roleName);
4822
- if (!role) return fail5(ROLE_TEXT.notFound);
4823
- if (role.askFirst) return ok5(ROLE_TEXT.askFirst, input.runId);
5032
+ if (!role) return fail6(ROLE_TEXT.notFound);
5033
+ if (role.askFirst) return ok6(ROLE_TEXT.askFirst, input.runId);
4824
5034
  const applied = await applyAction(root, found.file, found.roleName, pending, input.mode);
4825
- return applied.exitCode === 0 ? ok5(applied.stdout, input.runId) : fail5(applied.stdout, input.runId);
5035
+ return applied.exitCode === 0 ? ok6(applied.stdout, input.runId) : fail6(applied.stdout, input.runId);
4826
5036
  }
4827
5037
 
4828
5038
  // src/role/routing.ts
@@ -4857,10 +5067,10 @@ var RoutingSchema = z6.object({
4857
5067
  seen.add(rule.id);
4858
5068
  }
4859
5069
  });
4860
- function ok6(stdout = "") {
5070
+ function ok7(stdout = "") {
4861
5071
  return { exitCode: 0, stdout };
4862
5072
  }
4863
- function fail6(stdout) {
5073
+ function fail7(stdout) {
4864
5074
  return { exitCode: 1, stdout };
4865
5075
  }
4866
5076
  function formatRoutingError(error) {
@@ -4889,7 +5099,7 @@ function cleanItem(input) {
4889
5099
  function inboxFile(root, name) {
4890
5100
  const canonical = canonicalRoleName(name);
4891
5101
  if (!canonical) return null;
4892
- return join15(root, ".sdd", "agents", canonical, "inbox.jsonl");
5102
+ return join16(root, ".sdd", "agents", canonical, "inbox.jsonl");
4893
5103
  }
4894
5104
  async function writeHandoffInboxes(root, record) {
4895
5105
  const seen = /* @__PURE__ */ new Set();
@@ -4902,7 +5112,7 @@ async function writeHandoffInboxes(root, record) {
4902
5112
  }
4903
5113
  }
4904
5114
  async function readRouting(root) {
4905
- const path = join15(root, ".sdd", "routing.yaml");
5115
+ const path = join16(root, ".sdd", "routing.yaml");
4906
5116
  if (!await pathExists(path)) return { rules: [] };
4907
5117
  let raw;
4908
5118
  try {
@@ -4915,14 +5125,14 @@ async function readRouting(root) {
4915
5125
  return parsed.data;
4916
5126
  }
4917
5127
  async function loadRoles(root) {
4918
- const dir = join15(root, ".sdd", "roles");
5128
+ const dir = join16(root, ".sdd", "roles");
4919
5129
  const files = (await listFiles(dir)).filter((file) => file.endsWith(".md")).sort();
4920
5130
  const roles = [];
4921
5131
  for (const file of files) {
4922
5132
  const name = file.slice(0, -".md".length);
4923
5133
  let parsed;
4924
5134
  try {
4925
- parsed = parseRoleDocument(name, await readText(join15(dir, file)));
5135
+ parsed = parseRoleDocument(name, await readText(join16(dir, file)));
4926
5136
  } catch {
4927
5137
  return { error: `Role file is invalid: ${name}.` };
4928
5138
  }
@@ -5005,6 +5215,24 @@ async function matchRoute(root, input) {
5005
5215
  }
5006
5216
  };
5007
5217
  }
5218
+ if (item.stage) {
5219
+ const named = roles.filter((role) => role.stages.includes(item.stage ?? ""));
5220
+ if (named.length > 1) {
5221
+ return { ok: true, match: { kind: "ask", reason: "Two roles list this stage." } };
5222
+ }
5223
+ if (named.length === 1) {
5224
+ const role = named[0]?.name ?? "";
5225
+ return {
5226
+ ok: true,
5227
+ match: {
5228
+ kind: "role",
5229
+ role,
5230
+ reason: `Role ${role} lists stage ${item.stage}.`,
5231
+ rule: "role-stage"
5232
+ }
5233
+ };
5234
+ }
5235
+ }
5008
5236
  if (item.stage) {
5009
5237
  const owner = await stageOwner(root, item.stage);
5010
5238
  if (owner && typeof owner === "object") return { ok: false, message: owner.error };
@@ -5080,17 +5308,17 @@ async function routeMetrics(root) {
5080
5308
  }
5081
5309
  async function routeRole(root, input, deps) {
5082
5310
  const item = await prepareItem(root, input);
5083
- if ("error" in item) return fail6(item.error);
5311
+ if ("error" in item) return fail7(item.error);
5084
5312
  const matched = await matchRoute(root, item);
5085
- if (!matched.ok) return fail6(matched.message);
5313
+ if (!matched.ok) return fail7(matched.message);
5086
5314
  if (matched.match.kind !== "ask" || matched.match.reason !== "No rule matched.") {
5087
- return ok6(formatHit(matched.match));
5315
+ return ok7(formatHit(matched.match));
5088
5316
  }
5089
5317
  const loaded = await loadModels(root);
5090
- if (loaded === null) return ok6(formatHit(matched.match));
5091
- if ("error" in loaded) return fail6(loaded.error);
5318
+ if (loaded === null) return ok7(formatHit(matched.match));
5319
+ if ("error" in loaded) return fail7(loaded.error);
5092
5320
  const roles = await loadRoles(root);
5093
- if ("error" in roles) return fail6(roles.error);
5321
+ if ("error" in roles) return fail7(roles.error);
5094
5322
  const names = roles.map((role) => role.name).sort((left, right) => left.localeCompare(right));
5095
5323
  const options = names.includes("human") ? names : [...names, "human"];
5096
5324
  const decision = await decide({
@@ -5107,10 +5335,10 @@ async function routeRole(root, input, deps) {
5107
5335
  });
5108
5336
  if (decision.pick !== "human" && names.includes(decision.pick)) {
5109
5337
  const id = decision.choice?.id ?? "model";
5110
- return ok6(`role ${decision.pick}
5338
+ return ok7(`role ${decision.pick}
5111
5339
  reason Model ${id} picked ${decision.pick}.`);
5112
5340
  }
5113
- return ok6("Ask the human.\nNo rule matched.");
5341
+ return ok7("Ask the human.\nNo rule matched.");
5114
5342
  }
5115
5343
  async function readInbox(root, name) {
5116
5344
  const file = inboxFile(root, name);
@@ -5186,20 +5414,20 @@ async function queueNames(root, bot) {
5186
5414
  }
5187
5415
  const team = await readTeamConfig(root);
5188
5416
  if ("team" in team) return team.team.bots.map((item) => item.name);
5189
- const dirs = (await listDirs(join15(root, ".sdd", "agents"))).sort();
5417
+ const dirs = (await listDirs(join16(root, ".sdd", "agents"))).sort();
5190
5418
  if (dirs.length === 0) return { error: team.error };
5191
5419
  return dirs;
5192
5420
  }
5193
5421
  async function queueRole(root, bot) {
5194
5422
  const names = await queueNames(root, bot?.trim());
5195
- if ("error" in names) return fail6(names.error);
5423
+ if ("error" in names) return fail7(names.error);
5196
5424
  const blocks = [];
5197
5425
  for (const name of names) {
5198
5426
  const entries = await readInbox(root, name);
5199
- if ("error" in entries) return fail6(entries.error);
5427
+ if ("error" in entries) return fail7(entries.error);
5200
5428
  blocks.push(formatInbox(name, entries));
5201
5429
  }
5202
- return ok6(blocks.join("\n\n"));
5430
+ return ok7(blocks.join("\n\n"));
5203
5431
  }
5204
5432
  function itemId(item) {
5205
5433
  if (item.id) return item.id;
@@ -5212,26 +5440,26 @@ function itemId(item) {
5212
5440
  }
5213
5441
  async function takeRole(root, input) {
5214
5442
  const item = await prepareItem(root, input);
5215
- if ("error" in item) return fail6(item.error);
5443
+ if ("error" in item) return fail7(item.error);
5216
5444
  const matched = await matchRoute(root, item);
5217
- if (!matched.ok) return fail6(matched.message);
5445
+ if (!matched.ok) return fail7(matched.message);
5218
5446
  const match = matched.match;
5219
- if (match.kind === "ask") return fail6(formatHit(match));
5447
+ if (match.kind === "ask") return fail7(formatHit(match));
5220
5448
  const team = await readTeamConfig(root);
5221
- if ("error" in team) return fail6(team.error);
5449
+ if ("error" in team) return fail7(team.error);
5222
5450
  const bots = team.team.bots.filter((bot) => bot.role === match.role);
5223
- if (bots.length === 0) return fail6(`No bot has role ${match.role}.`);
5451
+ if (bots.length === 0) return fail7(`No bot has role ${match.role}.`);
5224
5452
  const requested = input.bot?.trim();
5225
- if (requested && !canonicalRoleName(requested)) return fail6(ROLE_TEXT.badName);
5453
+ if (requested && !canonicalRoleName(requested)) return fail7(ROLE_TEXT.badName);
5226
5454
  if (requested && !bots.some((bot) => bot.name === requested)) {
5227
5455
  const known = team.team.bots.some((bot) => bot.name === requested);
5228
- if (!known) return fail6("Bot not found.");
5229
- return fail6(`${requested} is not a ${match.role}.`);
5456
+ if (!known) return fail7("Bot not found.");
5457
+ return fail7(`${requested} is not a ${match.role}.`);
5230
5458
  }
5231
5459
  const counts = /* @__PURE__ */ new Map();
5232
5460
  for (const bot of team.team.bots) {
5233
5461
  const entries = await readInbox(root, bot.name);
5234
- if ("error" in entries) return fail6(entries.error);
5462
+ if ("error" in entries) return fail7(entries.error);
5235
5463
  counts.set(
5236
5464
  bot.name,
5237
5465
  entries.filter((entry) => entry.type === "item" && entry.status === "active").length
@@ -5240,19 +5468,19 @@ async function takeRole(root, input) {
5240
5468
  const roleCount = bots.reduce((sum, bot) => sum + (counts.get(bot.name) ?? 0), 0);
5241
5469
  const roleCap = team.team.role_wip?.[match.role];
5242
5470
  if (roleCap !== void 0 && roleCount >= roleCap) {
5243
- return fail6(`${match.role} is at the limit.`);
5471
+ return fail7(`${match.role} is at the limit.`);
5244
5472
  }
5245
5473
  const chosen = requested ? bots.find((bot) => bot.name === requested) : bots.find((bot) => (counts.get(bot.name) ?? 0) < bot.wip);
5246
5474
  if (!chosen || (counts.get(chosen.name) ?? 0) >= chosen.wip) {
5247
- return fail6(`${requested || bots[0]?.name || match.role} is at the limit.`);
5475
+ return fail7(`${requested || bots[0]?.name || match.role} is at the limit.`);
5248
5476
  }
5249
5477
  const id = itemId(item);
5250
- if (!id) return fail6("An item needs an id.");
5478
+ if (!id) return fail7("An item needs an id.");
5251
5479
  for (const bot of team.team.bots) {
5252
5480
  const entries = await readInbox(root, bot.name);
5253
- if ("error" in entries) return fail6(entries.error);
5481
+ if ("error" in entries) return fail7(entries.error);
5254
5482
  if (entries.some((entry) => entry.type === "item" && entry.status === "active" && entry.id === id)) {
5255
- return fail6(`${id} is already active.`);
5483
+ return fail7(`${id} is already active.`);
5256
5484
  }
5257
5485
  }
5258
5486
  const record = {
@@ -5267,9 +5495,9 @@ async function takeRole(root, input) {
5267
5495
  if (item.artifact) record.artifact = item.artifact;
5268
5496
  if (item.skill) record.skill = item.skill;
5269
5497
  const file = inboxFile(root, chosen.name);
5270
- if (!file) return fail6(ROLE_TEXT.badName);
5498
+ if (!file) return fail7(ROLE_TEXT.badName);
5271
5499
  await appendJson(file, record);
5272
- return ok6(`${chosen.name} took ${id}
5500
+ return ok7(`${chosen.name} took ${id}
5273
5501
  role ${match.role}
5274
5502
  rule ${match.rule}`);
5275
5503
  }
@@ -5277,10 +5505,10 @@ rule ${match.rule}`);
5277
5505
  // src/role/handoff.ts
5278
5506
  var ID = /^[a-z][a-z0-9-]{0,39}$/;
5279
5507
  var ROLE_YAML = ["role.yaml"];
5280
- function ok7(stdout = "", runId) {
5508
+ function ok8(stdout = "", runId) {
5281
5509
  return runId ? { exitCode: 0, stdout, runId } : { exitCode: 0, stdout };
5282
5510
  }
5283
- function fail7(stdout, runId) {
5511
+ function fail8(stdout, runId) {
5284
5512
  return runId ? { exitCode: 1, stdout, runId } : { exitCode: 1, stdout };
5285
5513
  }
5286
5514
  function mentionsX2(task) {
@@ -5291,10 +5519,10 @@ function safeChangeId2(id) {
5291
5519
  return !id.includes("/") && !id.includes("\\") && !id.includes("\0");
5292
5520
  }
5293
5521
  function yamlPath(root) {
5294
- return join16(root, ".sdd", ...ROLE_YAML);
5522
+ return join17(root, ".sdd", ...ROLE_YAML);
5295
5523
  }
5296
5524
  function planPath(root, changeId, sectionId) {
5297
- return join16(root, ".sdd", "role-runs", "plans", changeId, `${sectionId}.json`);
5525
+ return join17(root, ".sdd", "role-runs", "plans", changeId, `${sectionId}.json`);
5298
5526
  }
5299
5527
  async function loadYaml(root) {
5300
5528
  const path = yamlPath(root);
@@ -5325,11 +5553,11 @@ async function messagingOn(root) {
5325
5553
  }
5326
5554
  async function setRoleMessaging(root, enabled) {
5327
5555
  const loaded = await loadYaml(root);
5328
- if (loaded === "invalid") return fail7(ROLE_TEXT.badYaml);
5556
+ if (loaded === "invalid") return fail8(ROLE_TEXT.badYaml);
5329
5557
  const raw = loaded === "missing" ? {} : loaded;
5330
5558
  raw.role_messaging = enabled;
5331
5559
  await writeText(yamlPath(root), stringifyYaml(raw));
5332
- return ok7(enabled ? "Messaging between roles is on." : "Messaging between roles is off.");
5560
+ return ok8(enabled ? "Messaging between roles is on." : "Messaging between roles is off.");
5333
5561
  }
5334
5562
  function field(body, prefix) {
5335
5563
  const found = [];
@@ -5428,26 +5656,26 @@ function receiverPrompt(role, task, section) {
5428
5656
  async function handoffRole(root, input) {
5429
5657
  const from = canonicalRoleName(input.from);
5430
5658
  const to = canonicalRoleName(input.to);
5431
- if (!from || !to) return fail7(ROLE_TEXT.badName);
5432
- if (from === to) return fail7("A role cannot hand off to itself.");
5659
+ if (!from || !to) return fail8(ROLE_TEXT.badName);
5660
+ if (from === to) return fail8("A role cannot hand off to itself.");
5433
5661
  const sender = await readRole(root, from);
5434
5662
  const receiver = await readRole(root, to);
5435
- if (!sender || !receiver) return fail7(ROLE_TEXT.notFound);
5663
+ if (!sender || !receiver) return fail8(ROLE_TEXT.notFound);
5436
5664
  const task = input.task ?? "";
5437
- if (mentionsX2(task)) return fail7(ROLE_TEXT.x);
5665
+ if (mentionsX2(task)) return fail8(ROLE_TEXT.x);
5438
5666
  const loaded = await acceptedFacts(root, input.change?.trim() ?? "", input.section?.trim() ?? "");
5439
- if (!loaded) return fail7(ROLE_TEXT.needsSection);
5667
+ if (!loaded) return fail8(ROLE_TEXT.needsSection);
5440
5668
  if (loaded.current.owners.length > 0 && !loaded.current.owners.includes(to)) {
5441
- return fail7("One owner handles this section.");
5669
+ return fail8("One owner handles this section.");
5442
5670
  }
5443
5671
  const messaging = await messagingOn(root);
5444
- if (messaging === false) return ok7("Messaging between roles is off.");
5445
- if (!input.model) return fail7(ROLE_TEXT.noModel);
5446
- if (messaging === "invalid") return fail7(ROLE_TEXT.badYaml);
5672
+ if (messaging === false) return ok8("Messaging between roles is off.");
5673
+ if (!input.model) return fail8(ROLE_TEXT.noModel);
5674
+ if (messaging === "invalid") return fail8(ROLE_TEXT.badYaml);
5447
5675
  const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
5448
5676
  const runId = runIdFrom(now);
5449
- const senderFile = join16(conversationDir(root, from), `${runId}.jsonl`);
5450
- const receiverFile = join16(conversationDir(root, to), `${runId}.jsonl`);
5677
+ const senderFile = join17(conversationDir(root, from), `${runId}.jsonl`);
5678
+ const receiverFile = join17(conversationDir(root, to), `${runId}.jsonl`);
5451
5679
  const record = handoffRecord(from, to, input.change?.trim() ?? "", loaded.current);
5452
5680
  await appendJson(senderFile, record);
5453
5681
  await appendJson(receiverFile, record);
@@ -5460,12 +5688,12 @@ async function handoffRole(root, input) {
5460
5688
  });
5461
5689
  } catch {
5462
5690
  await appendJson(receiverFile, { type: "bad_call", roleId: to });
5463
- return fail7("", runId);
5691
+ return fail8("", runId);
5464
5692
  }
5465
5693
  const parsed = parseReply(raw);
5466
5694
  if (!parsed) {
5467
5695
  await appendJson(receiverFile, { type: "bad_call", roleId: to });
5468
- return fail7("", runId);
5696
+ return fail8("", runId);
5469
5697
  }
5470
5698
  await appendJson(receiverFile, { type: "model", pass: 1, roleId: to, text: parsed.text });
5471
5699
  const settled = await settleRoleReply(root, receiverFile, receiver, parsed, now, runId);
@@ -5482,7 +5710,7 @@ async function handoffRole(root, input) {
5482
5710
  }
5483
5711
  async function showRoleGroup(root, changeId) {
5484
5712
  const path = safeChangeId2(changeId) ? await acceptedFile(root, changeId) : "";
5485
- if (!path || !await pathExists(path)) return ok7("No roles in this group.");
5713
+ if (!path || !await pathExists(path)) return ok8("No roles in this group.");
5486
5714
  const seen = /* @__PURE__ */ new Set();
5487
5715
  const owners = [];
5488
5716
  for (const section of factsFrom(await readText(path))) {
@@ -5492,7 +5720,7 @@ async function showRoleGroup(root, changeId) {
5492
5720
  owners.push(owner);
5493
5721
  }
5494
5722
  }
5495
- return ok7(owners.length > 0 ? owners.join("\n") : "No roles in this group.");
5723
+ return ok8(owners.length > 0 ? owners.join("\n") : "No roles in this group.");
5496
5724
  }
5497
5725
  function distinctServices(all) {
5498
5726
  const services = /* @__PURE__ */ new Map();
@@ -5620,20 +5848,20 @@ function leadPrompt(task, services) {
5620
5848
  }
5621
5849
  async function connectionResult(root, changeId, section, now) {
5622
5850
  const messaging = await messagingOn(root);
5623
- if (messaging === "invalid") return fail7(ROLE_TEXT.badYaml);
5624
- if (!messaging) return ok7("Messaging between roles is off.");
5851
+ if (messaging === "invalid") return fail8(ROLE_TEXT.badYaml);
5852
+ if (!messaging) return ok8("Messaging between roles is off.");
5625
5853
  const owners = unique(section.owners);
5626
5854
  const runId = runIdFrom(now);
5627
5855
  for (let i = 0; i < owners.length - 1; i++) {
5628
5856
  const from = owners[i] ?? "";
5629
5857
  const to = owners[i + 1] ?? "";
5630
5858
  const record = handoffRecord(from, to, changeId, section);
5631
- await appendJson(join16(conversationDir(root, from), `${runId}.jsonl`), record);
5632
- await appendJson(join16(conversationDir(root, to), `${runId}.jsonl`), record);
5859
+ await appendJson(join17(conversationDir(root, from), `${runId}.jsonl`), record);
5860
+ await appendJson(join17(conversationDir(root, to), `${runId}.jsonl`), record);
5633
5861
  await writeHandoffInboxes(root, record);
5634
5862
  }
5635
5863
  const lines = ["Connection hands off. Lead is absent.", ...owners];
5636
- return ok7(lines.join("\n"));
5864
+ return ok8(lines.join("\n"));
5637
5865
  }
5638
5866
  function freshPlan(task, nodes) {
5639
5867
  return { status: "pending", rebuilds: 0, stalled: false, task, nodes, results: [] };
@@ -5642,7 +5870,7 @@ async function planNetwork(root, input) {
5642
5870
  const changeId = input.change?.trim() ?? "";
5643
5871
  const sectionId = input.section?.trim() ?? "";
5644
5872
  const loaded = await acceptedFacts(root, changeId, sectionId);
5645
- if (!loaded) return fail7(ROLE_TEXT.needsSection);
5873
+ if (!loaded) return fail8(ROLE_TEXT.needsSection);
5646
5874
  const task = input.task ?? "";
5647
5875
  const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
5648
5876
  const connection = loaded.all.find(
@@ -5650,23 +5878,23 @@ async function planNetwork(root, input) {
5650
5878
  );
5651
5879
  if (connection?.connection) return connectionResult(root, changeId, connection, now);
5652
5880
  const services = distinctServices(loaded.all);
5653
- if (services.size === 0) return ok7("One scope. No lead.");
5881
+ if (services.size === 0) return ok8("One scope. No lead.");
5654
5882
  if (services.size === 1) {
5655
5883
  const entry = [...services.entries()][0];
5656
- if (!entry) return ok7("One scope. No lead.");
5884
+ if (!entry) return ok8("One scope. No lead.");
5657
5885
  const [name, section] = entry;
5658
5886
  const owner = section.owners[0];
5659
- if (!owner) return fail7("This service needs an owner.");
5660
- if (!await readRole(root, owner)) return fail7(ROLE_TEXT.notFound);
5661
- return ok7(`Service ${name} runs ${owner}.`);
5887
+ if (!owner) return fail8("This service needs an owner.");
5888
+ if (!await readRole(root, owner)) return fail8(ROLE_TEXT.notFound);
5889
+ return ok8(`Service ${name} runs ${owner}.`);
5662
5890
  }
5663
5891
  const picked = await pickLead(root, loaded.all, services);
5664
- if (picked.kind === "none") return ok7("One scope. No lead.");
5665
- if (picked.kind === "missing") return fail7(picked.sentence);
5892
+ if (picked.kind === "none") return ok8("One scope. No lead.");
5893
+ if (picked.kind === "missing") return fail8(picked.sentence);
5666
5894
  const lead = await readRole(root, picked.name);
5667
- if (!lead) return fail7(ROLE_TEXT.notFound);
5668
- if (await messagingOn(root) === "invalid") return fail7(ROLE_TEXT.badYaml);
5669
- if (!input.model) return fail7(ROLE_TEXT.noModel);
5895
+ if (!lead) return fail8(ROLE_TEXT.notFound);
5896
+ if (await messagingOn(root) === "invalid") return fail8(ROLE_TEXT.badYaml);
5897
+ if (!input.model) return fail8(ROLE_TEXT.noModel);
5670
5898
  return storeLeadPlan(
5671
5899
  root,
5672
5900
  changeId,
@@ -5684,17 +5912,17 @@ async function storeLeadPlan(root, changeId, sectionId, task, lead, services, al
5684
5912
  try {
5685
5913
  raw = await model.complete({ roleId: lead.name, prompt: leadPrompt(task, services) });
5686
5914
  } catch {
5687
- return fail7("");
5915
+ return fail8("");
5688
5916
  }
5689
5917
  const parsed = await parsePlan(root, raw, contextOf(all));
5690
- if (previous && (parsed === "invalid" || parsed === "deep")) return fail7(planFailure(parsed));
5691
- if (parsed === "invalid") return fail7("The plan is not valid.");
5692
- if (parsed === "deep") return fail7("A plan is deeper than 2.");
5918
+ if (previous && (parsed === "invalid" || parsed === "deep")) return fail8(planFailure(parsed));
5919
+ if (parsed === "invalid") return fail8("The plan is not valid.");
5920
+ if (parsed === "deep") return fail8("A plan is deeper than 2.");
5693
5921
  if (previous && sameArtifacts(previous.nodes, parsed)) {
5694
5922
  previous.stalled = true;
5695
5923
  previous.rebuilds += 1;
5696
5924
  await writePlan(root, changeId, sectionId, previous);
5697
- return ok7("Stopped. The plan made no progress.");
5925
+ return ok8("Stopped. The plan made no progress.");
5698
5926
  }
5699
5927
  const next = previous ? {
5700
5928
  ...previous,
@@ -5705,7 +5933,7 @@ async function storeLeadPlan(root, changeId, sectionId, task, lead, services, al
5705
5933
  results: []
5706
5934
  } : freshPlan(task, parsed);
5707
5935
  await writePlan(root, changeId, sectionId, next);
5708
- return ok7(["Accept this plan before anyone writes.", ...planLines(parsed)].join("\n"));
5936
+ return ok8(["Accept this plan before anyone writes.", ...planLines(parsed)].join("\n"));
5709
5937
  }
5710
5938
  function planFailure(parsed) {
5711
5939
  return parsed === "deep" ? "A plan is deeper than 2." : "The plan is not valid.";
@@ -5718,13 +5946,13 @@ function sameArtifacts(left, right) {
5718
5946
  async function acceptNetwork(root, input) {
5719
5947
  const changeId = input.change?.trim() ?? "";
5720
5948
  const sectionId = input.section?.trim() ?? "";
5721
- if (!safeChangeId2(changeId) || !sectionId) return fail7("No plan is stored.");
5949
+ if (!safeChangeId2(changeId) || !sectionId) return fail8("No plan is stored.");
5722
5950
  const plan = await readPlan(root, changeId, sectionId);
5723
- if (!plan) return fail7("No plan is stored.");
5724
- if (plan.status !== "pending") return fail7("The plan is not waiting for acceptance.");
5951
+ if (!plan) return fail8("No plan is stored.");
5952
+ if (plan.status !== "pending") return fail8("The plan is not waiting for acceptance.");
5725
5953
  plan.status = "accepted";
5726
5954
  await writePlan(root, changeId, sectionId, plan);
5727
- return ok7("Plan accepted.");
5955
+ return ok8("Plan accepted.");
5728
5956
  }
5729
5957
  function walk(nodes) {
5730
5958
  const flat = [];
@@ -5745,16 +5973,16 @@ function ready(node, results) {
5745
5973
  async function runNetwork(root, input) {
5746
5974
  const changeId = input.change?.trim() ?? "";
5747
5975
  const sectionId = input.section?.trim() ?? "";
5748
- if (!safeChangeId2(changeId) || !sectionId) return fail7("No plan is stored.");
5976
+ if (!safeChangeId2(changeId) || !sectionId) return fail8("No plan is stored.");
5749
5977
  const plan = await readPlan(root, changeId, sectionId);
5750
- if (!plan) return fail7("No plan is stored.");
5751
- if (plan.status === "pending") return ok7("Accept this plan before anyone writes.");
5752
- if (plan.status === "done") return ok7("The parent is done.");
5753
- if (plan.status !== "accepted" && plan.status !== "open") return fail7("No plan is stored.");
5754
- if (await messagingOn(root) === "invalid") return fail7(ROLE_TEXT.badYaml);
5978
+ if (!plan) return fail8("No plan is stored.");
5979
+ if (plan.status === "pending") return ok8("Accept this plan before anyone writes.");
5980
+ if (plan.status === "done") return ok8("The parent is done.");
5981
+ if (plan.status !== "accepted" && plan.status !== "open") return fail8("No plan is stored.");
5982
+ if (await messagingOn(root) === "invalid") return fail8(ROLE_TEXT.badYaml);
5755
5983
  const minutes = await timeoutMinutes(root);
5756
- if (minutes === "invalid") return fail7(ROLE_TEXT.badYaml);
5757
- if (!input.model) return fail7(ROLE_TEXT.noModel);
5984
+ if (minutes === "invalid") return fail8(ROLE_TEXT.badYaml);
5985
+ if (!input.model) return fail8(ROLE_TEXT.noModel);
5758
5986
  const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
5759
5987
  const summaries = [];
5760
5988
  const called = /* @__PURE__ */ new Set();
@@ -5774,7 +6002,7 @@ async function runNetwork(root, input) {
5774
6002
  const when = new Date(now.getTime() + index * 1e3);
5775
6003
  index += 1;
5776
6004
  const runId = runIdFrom(when);
5777
- const file = join16(conversationDir(root, owner.name), `${runId}.jsonl`);
6005
+ const file = join17(conversationDir(root, owner.name), `${runId}.jsonl`);
5778
6006
  let raw;
5779
6007
  try {
5780
6008
  raw = await input.model.complete({
@@ -5818,7 +6046,7 @@ async function runNetwork(root, input) {
5818
6046
  const waiting = all.some((node) => !plan.results.some((item) => item.decision === node.decision));
5819
6047
  plan.status = waiting ? "open" : "done";
5820
6048
  await writePlan(root, changeId, sectionId, plan);
5821
- if (missingOwner) return fail7(ROLE_TEXT.notFound);
6049
+ if (missingOwner) return fail8(ROLE_TEXT.notFound);
5822
6050
  const lines = [...summaries];
5823
6051
  if (waiting) {
5824
6052
  lines.push("A branch is waiting for its prerequisites.");
@@ -5826,25 +6054,25 @@ async function runNetwork(root, input) {
5826
6054
  } else {
5827
6055
  lines.push("The parent is done.");
5828
6056
  }
5829
- return ok7(lines.join("\n"));
6057
+ return ok8(lines.join("\n"));
5830
6058
  }
5831
6059
  async function rebuildNetwork(root, input) {
5832
6060
  const changeId = input.change?.trim() ?? "";
5833
6061
  const sectionId = input.section?.trim() ?? "";
5834
- if (!safeChangeId2(changeId) || !sectionId) return fail7("No plan is stored.");
6062
+ if (!safeChangeId2(changeId) || !sectionId) return fail8("No plan is stored.");
5835
6063
  const plan = await readPlan(root, changeId, sectionId);
5836
- if (!plan) return fail7("No plan is stored.");
6064
+ if (!plan) return fail8("No plan is stored.");
5837
6065
  const allowed = plan.rebuilds === 0 || plan.rebuilds === 1 && !plan.stalled && plan.status === "accepted";
5838
- if (!allowed) return fail7("A second rebuild does not start.");
6066
+ if (!allowed) return fail8("A second rebuild does not start.");
5839
6067
  const loaded = await acceptedFacts(root, changeId, sectionId);
5840
- if (!loaded) return fail7(ROLE_TEXT.needsSection);
5841
- if (!input.model) return fail7(ROLE_TEXT.noModel);
6068
+ if (!loaded) return fail8(ROLE_TEXT.needsSection);
6069
+ if (!input.model) return fail8(ROLE_TEXT.noModel);
5842
6070
  const services = distinctServices(loaded.all);
5843
6071
  const picked = await pickLead(root, loaded.all, services);
5844
- if (picked.kind === "missing") return fail7(picked.sentence);
5845
- if (picked.kind !== "lead") return fail7("This module needs a lead.");
6072
+ if (picked.kind === "missing") return fail8(picked.sentence);
6073
+ if (picked.kind !== "lead") return fail8("This module needs a lead.");
5846
6074
  const lead = await readRole(root, picked.name);
5847
- if (!lead) return fail7(ROLE_TEXT.notFound);
6075
+ if (!lead) return fail8(ROLE_TEXT.notFound);
5848
6076
  const before = JSON.stringify(plan);
5849
6077
  const result2 = await storeLeadPlan(
5850
6078
  root,
@@ -5911,6 +6139,378 @@ async function pickLead(root, all, services) {
5911
6139
  return { kind: "none" };
5912
6140
  }
5913
6141
 
6142
+ // src/role/guide.ts
6143
+ import { join as join18 } from "pathe";
6144
+ var PLANNING = /* @__PURE__ */ new Set([
6145
+ "intent",
6146
+ "vision",
6147
+ "requirements",
6148
+ "feature",
6149
+ "stories",
6150
+ "clarify_intent",
6151
+ "brainstorm",
6152
+ "hl_arb",
6153
+ "research",
6154
+ "outcome",
6155
+ "plan"
6156
+ ]);
6157
+ var ARCHITECTURE = /* @__PURE__ */ new Set([
6158
+ "design",
6159
+ "architecture",
6160
+ "clarify_design",
6161
+ "lld",
6162
+ "db_design",
6163
+ "code_research"
6164
+ ]);
6165
+ var CONSTRUCTION = /* @__PURE__ */ new Set([
6166
+ "tasks",
6167
+ "clarify_tasks",
6168
+ "implement",
6169
+ "local_verify",
6170
+ "acceptance",
6171
+ "pr_review"
6172
+ ]);
6173
+ var DUTIES = [
6174
+ {
6175
+ name: "planner",
6176
+ label: "Planner",
6177
+ job: "Plan the change",
6178
+ bucket: "planning",
6179
+ owns: ["the request", "the plan"],
6180
+ skills: ["plan the change"],
6181
+ never: ["accept a spec", "write product code"],
6182
+ askHumanWhen: ["Who is this for?", "How will you know it worked?"],
6183
+ description: "The planner proposes. The person decides."
6184
+ },
6185
+ {
6186
+ name: "architect",
6187
+ label: "Architect",
6188
+ job: "Name the modules",
6189
+ bucket: "architecture",
6190
+ owns: ["the design"],
6191
+ skills: ["name the modules"],
6192
+ never: ["accept a spec", "write product code"],
6193
+ askHumanWhen: ["Which module owns this change?"],
6194
+ description: "The architect names the modules. The person decides."
6195
+ },
6196
+ {
6197
+ name: "builder",
6198
+ label: "Builder",
6199
+ job: "Build the change",
6200
+ bucket: "construction",
6201
+ owns: ["the change pack"],
6202
+ skills: ["build the change"],
6203
+ never: ["accept a spec", "generate a release"],
6204
+ askHumanWhen: [],
6205
+ description: "The builder builds the change. The person decides."
6206
+ }
6207
+ ];
6208
+ function ok9(stdout) {
6209
+ return { exitCode: 0, stdout };
6210
+ }
6211
+ function fail9(stdout) {
6212
+ return { exitCode: 1, stdout };
6213
+ }
6214
+ function statePath(root) {
6215
+ return join18(root, ".sdd", "role-runs", "guide.yaml");
6216
+ }
6217
+ function dutyForStage(id, allIds) {
6218
+ if (PLANNING.has(id)) return "planning";
6219
+ if (ARCHITECTURE.has(id)) return "architecture";
6220
+ if (CONSTRUCTION.has(id)) return "construction";
6221
+ const index = allIds.indexOf(id);
6222
+ const archAt = allIds.findIndex((item) => ARCHITECTURE.has(item));
6223
+ const buildAt = allIds.findIndex((item) => CONSTRUCTION.has(item));
6224
+ if (archAt >= 0 && index >= 0 && index < archAt) return "planning";
6225
+ if (buildAt >= 0 && index >= buildAt) return "construction";
6226
+ if (archAt >= 0 && (buildAt < 0 || index < buildAt)) return "architecture";
6227
+ const third = Math.max(1, Math.ceil(allIds.length / 3));
6228
+ if (index < third) return "planning";
6229
+ if (index < third * 2) return "architecture";
6230
+ return "construction";
6231
+ }
6232
+ function asText(value) {
6233
+ return typeof value === "string" ? value.trim() : "";
6234
+ }
6235
+ function parseState(value) {
6236
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
6237
+ const rec = value;
6238
+ const change = asText(rec.change);
6239
+ const request = asText(rec.request);
6240
+ if (!change || !request || !Array.isArray(rec.answers)) return null;
6241
+ const answers = [];
6242
+ for (const item of rec.answers) {
6243
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
6244
+ const row = item;
6245
+ const role = asText(row.role);
6246
+ const question = asText(row.question);
6247
+ const answer = asText(row.answer);
6248
+ if (!role || !question || !answer) return null;
6249
+ answers.push({ role, question, answer });
6250
+ }
6251
+ let pending;
6252
+ if (rec.pending !== void 0) {
6253
+ if (!rec.pending || typeof rec.pending !== "object" || Array.isArray(rec.pending)) return null;
6254
+ const row = rec.pending;
6255
+ const role = asText(row.role);
6256
+ const question = asText(row.question);
6257
+ const kind = row.kind === "model" ? "model" : row.kind === "role" ? "role" : "";
6258
+ if (!role || !question || !kind) return null;
6259
+ pending = { role, kind, question };
6260
+ }
6261
+ const state = { change, request, answers };
6262
+ if (pending) state.pending = pending;
6263
+ return state;
6264
+ }
6265
+ async function readState(root) {
6266
+ const path = statePath(root);
6267
+ if (!await pathExists(path)) return null;
6268
+ try {
6269
+ const parsed = parseState(await readYaml(path));
6270
+ return parsed ?? "invalid";
6271
+ } catch {
6272
+ return "invalid";
6273
+ }
6274
+ }
6275
+ async function writeState(root, state) {
6276
+ await ensureDir(join18(root, ".sdd", "role-runs"));
6277
+ const body = {
6278
+ change: state.change,
6279
+ request: state.request,
6280
+ answers: state.answers
6281
+ };
6282
+ if (state.pending) body.pending = state.pending;
6283
+ await writeYaml(statePath(root), body);
6284
+ }
6285
+ function questionText(role, question) {
6286
+ return [`Question:`, `${role} asks:`, question, `Answer with: sdd go --answer "..."`.trim()].join(
6287
+ "\n"
6288
+ );
6289
+ }
6290
+ function nextRoleQuestion(role, answers) {
6291
+ const answered = new Set(
6292
+ answers.filter((item) => item.role === role.name).map((item) => item.question)
6293
+ );
6294
+ return role.askHumanWhen.find((item) => !answered.has(item));
6295
+ }
6296
+ function taskFor(request, role, answers) {
6297
+ const rows = [`Request: ${request}`];
6298
+ const mine = answers.filter((item) => item.role === role);
6299
+ if (mine.length > 0) {
6300
+ rows.push("Answers:");
6301
+ for (const item of mine) rows.push(`- ${item.question}: ${item.answer}`);
6302
+ }
6303
+ return rows.join("\n");
6304
+ }
6305
+ async function roleFiles(root) {
6306
+ const names = await listFiles(join18(root, ".sdd", "roles"));
6307
+ return names.filter((name) => name.endsWith(".md"));
6308
+ }
6309
+ async function prepareGuide(root, input = {}) {
6310
+ const workflowName = input.workflow?.trim() || "feature";
6311
+ try {
6312
+ if (!await isInitialized(root)) {
6313
+ await initProject({ projectRoot: root, agents: input.agents ?? false });
6314
+ }
6315
+ const workflow = await loadWorkflow(root, workflowName);
6316
+ const ids = workflow.stages.map((stage) => stage.id);
6317
+ const existing = await roleFiles(root);
6318
+ if (existing.length === 0) {
6319
+ for (const duty of DUTIES) {
6320
+ const { bucket, ...role } = duty;
6321
+ const stages = ids.filter((id) => dutyForStage(id, ids) === bucket);
6322
+ const created = await createRole(root, { ...role, stages });
6323
+ if (created.exitCode !== 0) return created;
6324
+ }
6325
+ }
6326
+ const lines = ["Ready.", `Workflow: ${workflowName}.`];
6327
+ if (existing.length > 0) {
6328
+ lines.push("Roles are already here.");
6329
+ } else {
6330
+ for (const duty of DUTIES) {
6331
+ const stages = ids.filter((id) => dutyForStage(id, ids) === duty.bucket);
6332
+ lines.push(`${duty.name}: ${stages.join(", ") || "no stage on this workflow"}`);
6333
+ }
6334
+ }
6335
+ lines.push('Next: sdd go "what you want to build"');
6336
+ return ok9(lines.join("\n"));
6337
+ } catch (error) {
6338
+ return fail9(error instanceof Error ? error.message : String(error));
6339
+ }
6340
+ }
6341
+ async function writeDraft(ctxPath, stage, role, text2, request) {
6342
+ const body = [
6343
+ `# ${stage.title ?? stage.id}`,
6344
+ "",
6345
+ `${role} drafted this stage. The person has not signed it.`,
6346
+ "",
6347
+ text2.trim(),
6348
+ "",
6349
+ `Request: ${request}`,
6350
+ ""
6351
+ ].join("\n");
6352
+ for (const artifact of stage.artifacts) {
6353
+ if (!artifact.required) continue;
6354
+ const abs = join18(ctxPath, artifact.path);
6355
+ if (await pathExists(abs)) {
6356
+ const current = await readText(abs);
6357
+ if (isSubstantiveArtifactContent(current)) continue;
6358
+ }
6359
+ await writeText(abs, body);
6360
+ }
6361
+ }
6362
+ function optionalStage(stage) {
6363
+ return stage.optional || stage.kind === "clarify" || stage.kind === "brainstorm";
6364
+ }
6365
+ async function runGuide(root, input = {}) {
6366
+ const prepared = await prepareGuide(root, {
6367
+ workflow: input.workflow,
6368
+ agents: input.agents
6369
+ });
6370
+ if (prepared.exitCode !== 0) return prepared;
6371
+ const title = input.title?.trim() ?? "";
6372
+ const answer = input.answer?.trim() ?? "";
6373
+ if (input.answer !== void 0 && !answer) return fail9("An answer needs text.");
6374
+ let loaded;
6375
+ try {
6376
+ loaded = await readState(root);
6377
+ } catch (error) {
6378
+ return fail9(error instanceof Error ? error.message : String(error));
6379
+ }
6380
+ if (loaded === "invalid") return fail9("The guide file is invalid.");
6381
+ const config = await loadConfig(root);
6382
+ let state = loaded;
6383
+ if (title) {
6384
+ try {
6385
+ const ctx = await createChange({
6386
+ projectRoot: root,
6387
+ config,
6388
+ title,
6389
+ workflowName: input.workflow?.trim() || "feature"
6390
+ });
6391
+ state = { change: ctx.id, request: title, answers: [] };
6392
+ await writeState(root, state);
6393
+ } catch (error) {
6394
+ return fail9(error instanceof Error ? error.message : String(error));
6395
+ }
6396
+ } else if (!state) {
6397
+ const active = await getActiveChangeId(root, config);
6398
+ if (!active) return prepared;
6399
+ try {
6400
+ const ctx = await buildContext(root, config, active);
6401
+ state = { change: active, request: ctx.meta.title, answers: [] };
6402
+ } catch (error) {
6403
+ return fail9(error instanceof Error ? error.message : String(error));
6404
+ }
6405
+ }
6406
+ if (answer && !state.pending) return fail9("No question is waiting.");
6407
+ if (answer && state.pending) {
6408
+ state.answers.push({
6409
+ role: state.pending.role,
6410
+ question: state.pending.question,
6411
+ answer
6412
+ });
6413
+ state.pending = void 0;
6414
+ await writeState(root, state);
6415
+ } else if (state.pending) {
6416
+ return ok9(questionText(state.pending.role, state.pending.question));
6417
+ }
6418
+ const lines = [];
6419
+ let lastRole = "";
6420
+ for (let step = 0; step < 16; step++) {
6421
+ let ctx;
6422
+ try {
6423
+ ctx = await buildContext(root, config, state.change);
6424
+ } catch (error) {
6425
+ return fail9(error instanceof Error ? error.message : String(error));
6426
+ }
6427
+ const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
6428
+ if (!stage) return fail9("No active change.");
6429
+ if (optionalStage(stage) && stage.skippable) {
6430
+ const before = ctx.meta.stage;
6431
+ try {
6432
+ await skipStage(root, config, ctx.id, stage.id, "No role runs this optional stage.");
6433
+ } catch (error) {
6434
+ return fail9(error instanceof Error ? error.message : String(error));
6435
+ }
6436
+ const after = await buildContext(root, config, state.change);
6437
+ if (after.meta.stage === before) return fail9("This stage stayed put.");
6438
+ lines.push(`Skipped ${stage.id}.`);
6439
+ continue;
6440
+ }
6441
+ const routed = await matchRoute(root, { stage: stage.id });
6442
+ if (!routed.ok) return fail9(routed.message);
6443
+ if (routed.match.kind === "ask") {
6444
+ return ok9(["Stopped.", "Ask the human.", routed.match.reason, ...lines].join("\n"));
6445
+ }
6446
+ const role = await readRole(root, routed.match.role);
6447
+ if (!role) return fail9(ROLE_TEXT.notFound);
6448
+ if (lastRole && lastRole !== role.name) lines.push(`Handoff: ${lastRole} to ${role.name}.`);
6449
+ const question = nextRoleQuestion(role, state.answers);
6450
+ if (question) {
6451
+ state.pending = { role: role.name, kind: "role", question };
6452
+ await writeState(root, state);
6453
+ return ok9(
6454
+ [
6455
+ "Question:",
6456
+ ...lines,
6457
+ `${role.name} asks:`,
6458
+ question,
6459
+ 'Answer with: sdd go --answer "..."'
6460
+ ].join("\n")
6461
+ );
6462
+ }
6463
+ if (!input.model) return fail9(ROLE_TEXT.noModel);
6464
+ const ran = await runRole(root, {
6465
+ name: role.name,
6466
+ task: taskFor(state.request, role.name, state.answers),
6467
+ guide: { changeDir: ctx.displayPath },
6468
+ model: input.model,
6469
+ now: input.now
6470
+ });
6471
+ if (ran.exitCode !== 0) {
6472
+ return fail9([ran.stdout, ...lines].filter((line) => line.length > 0).join("\n"));
6473
+ }
6474
+ const text2 = ran.stdout.trim();
6475
+ if (text2.startsWith("Question:")) {
6476
+ const asked = text2.replace(/^Question:\s*/, "").trim();
6477
+ state.pending = { role: role.name, kind: "model", question: asked || text2 };
6478
+ await writeState(root, state);
6479
+ return ok9(
6480
+ [
6481
+ "Question:",
6482
+ ...lines,
6483
+ `${role.name} asks:`,
6484
+ state.pending.question,
6485
+ 'Answer with: sdd go --answer "..."'
6486
+ ].join("\n")
6487
+ );
6488
+ }
6489
+ await writeDraft(ctx.path, stage, role.name, text2, state.request);
6490
+ try {
6491
+ const advanced = await advanceStage(root, config, ctx.id);
6492
+ lines.push(`${role.name} drafted ${stage.id}.`);
6493
+ lastRole = role.name;
6494
+ if (!advanced.to) {
6495
+ await writeState(root, state);
6496
+ return ok9(
6497
+ [
6498
+ "Done.",
6499
+ ...lines,
6500
+ "No further stages. Run `sdd complete` when ready.",
6501
+ "The person has not signed this change."
6502
+ ].join("\n")
6503
+ );
6504
+ }
6505
+ } catch (error) {
6506
+ await writeState(root, state);
6507
+ const message = error instanceof Error ? error.message : String(error);
6508
+ return ok9(["Stopped.", `${role.name} drafted ${stage.id}.`, message, ...lines].join("\n"));
6509
+ }
6510
+ }
6511
+ return fail9("The guide stopped after 16 steps.");
6512
+ }
6513
+
5914
6514
  // src/role/models/show.ts
5915
6515
  function formatModelChoice(workType, choice) {
5916
6516
  return `${workType} ${choice.id} ${choice.kind} ${choice.provider} ${choice.why}`;
@@ -5949,7 +6549,7 @@ function explainModel(config, workType, ctx) {
5949
6549
  }
5950
6550
 
5951
6551
  // src/stories.ts
5952
- import { join as join17 } from "pathe";
6552
+ import { join as join19 } from "pathe";
5953
6553
  var STORY_ID = /^S-\d+$/i;
5954
6554
  var EPIC_KEY = /^[A-Za-z0-9][A-Za-z0-9-]{0,39}$/;
5955
6555
  function validBranchName(name) {
@@ -5991,7 +6591,7 @@ function parseStoryDraft(input) {
5991
6591
  async function metasIn(dir) {
5992
6592
  const out = [];
5993
6593
  for (const id of await listDirs(dir)) {
5994
- const path = join17(dir, id, "meta.yaml");
6594
+ const path = join19(dir, id, "meta.yaml");
5995
6595
  if (!await pathExists(path)) continue;
5996
6596
  try {
5997
6597
  out.push(ChangeMetaSchema.parse(await readYaml(path)));
@@ -6212,6 +6812,7 @@ export {
6212
6812
  displayChangePath,
6213
6813
  domainsDir,
6214
6814
  duplicateRole,
6815
+ dutyForStage,
6215
6816
  editRole,
6216
6817
  embedSearchQuery,
6217
6818
  emptyChangeMetrics,
@@ -6324,6 +6925,7 @@ export {
6324
6925
  planAgentIntegration,
6325
6926
  planNetwork,
6326
6927
  postRoom,
6928
+ prepareGuide,
6327
6929
  productTeamHome,
6328
6930
  promoteGreenfieldToMemory,
6329
6931
  queueRole,
@@ -6365,8 +6967,10 @@ export {
6365
6967
  resolveSearchProvider,
6366
6968
  resolveStages,
6367
6969
  reviewRoom,
6970
+ roleContextBody,
6368
6971
  routeRole,
6369
6972
  rrf,
6973
+ runGuide,
6370
6974
  runLocalVerify,
6371
6975
  runLoop,
6372
6976
  runNetwork,
@@ -6382,6 +6986,7 @@ export {
6382
6986
  selectModel,
6383
6987
  setActiveChange,
6384
6988
  setFact,
6989
+ setRoleContext,
6385
6990
  setRoleMessaging,
6386
6991
  shouldAutoSkip,
6387
6992
  showFact,