@structured-vibe-coding/core 0.21.0 → 0.23.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
@@ -1,32 +1,11 @@
1
1
  import {
2
- SDD_CORE_VERSION,
3
- callMcpSourceTool,
4
2
  explainMatch,
5
3
  formatMcpContextForHandoff,
6
4
  gatherMcpContext,
7
5
  gatherMcpContextForChange,
8
6
  listMatchingSourceIds,
9
- listMcpSourceTools,
10
- matchMcpSources,
11
- mcpTimeoutMs,
12
- toProcessEnv,
13
- withMcpSource
14
- } from "./chunk-ICRQOQWE.js";
15
- import {
16
- McpConfigSchema,
17
- McpInvokeSchema,
18
- McpSourceSchema,
19
- McpWhenSchema,
20
- addMcpSource,
21
- defaultMcpConfig,
22
- ensureMcpConfig,
23
- interpolate,
24
- interpolateArgs,
25
- loadMcpConfig,
26
- mcpConfigPath,
27
- removeMcpSource,
28
- saveMcpConfig
29
- } from "./chunk-4BRO7BOJ.js";
7
+ matchMcpSources
8
+ } from "./chunk-PFJFWZOM.js";
30
9
  import {
31
10
  AGENT_INTEGRATIONS,
32
11
  AGENT_TARGET_OPTIONS,
@@ -68,7 +47,7 @@ import {
68
47
  renderThinAgent,
69
48
  saveGeneratedManifest,
70
49
  writeAgentHandoff
71
- } from "./chunk-5GLH6ZWU.js";
50
+ } from "./chunk-GP477MAV.js";
72
51
  import {
73
52
  ChangeMetricsSchema,
74
53
  DecisionMetricSchema,
@@ -83,6 +62,8 @@ import {
83
62
  emptyChangeMetrics,
84
63
  fallbackIncompleteStageId,
85
64
  findFeaturesBacklogPath,
65
+ h2Title,
66
+ headingsFilled,
86
67
  importSessionMetrics,
87
68
  incompletePriorStageErrors,
88
69
  isSubstantiveArtifactContent,
@@ -100,7 +81,7 @@ import {
100
81
  switchWorkflow,
101
82
  transitionPhase,
102
83
  updateFeatureStatus
103
- } from "./chunk-Z2ULYVHO.js";
84
+ } from "./chunk-KMITXC7I.js";
104
85
  import {
105
86
  EMBEDDINGS_UNAVAILABLE,
106
87
  EMBEDDING_MODEL,
@@ -164,6 +145,7 @@ import {
164
145
  memoryDir as memoryDir2,
165
146
  mergeConfigDefaults,
166
147
  mergeRankedHits,
148
+ openMcpSearchProvider,
167
149
  packHits,
168
150
  parseRoleDocument,
169
151
  pinRole,
@@ -188,9 +170,10 @@ import {
188
170
  searchProject,
189
171
  showRoles,
190
172
  showTeam,
173
+ splitFrontmatter,
191
174
  workKind,
192
175
  writePackedCache
193
- } from "./chunk-AERAWOAK.js";
176
+ } from "./chunk-RXQH2THQ.js";
194
177
  import {
195
178
  ArtifactDefSchema,
196
179
  ChangeMetaSchema,
@@ -236,10 +219,31 @@ import {
236
219
  workflowUnavailableMessage
237
220
  } from "./chunk-TNWFMRIA.js";
238
221
  import {
222
+ SDD_CORE_VERSION,
239
223
  SddError,
224
+ callMcpSourceTool,
240
225
  errorMessage,
241
- isSddError
242
- } from "./chunk-MHODNFIL.js";
226
+ isSddError,
227
+ listMcpSourceTools,
228
+ mcpTimeoutMs,
229
+ toProcessEnv,
230
+ withMcpSource
231
+ } from "./chunk-OY47H6R5.js";
232
+ import {
233
+ McpConfigSchema,
234
+ McpInvokeSchema,
235
+ McpSourceSchema,
236
+ McpWhenSchema,
237
+ addMcpSource,
238
+ defaultMcpConfig,
239
+ ensureMcpConfig,
240
+ interpolate,
241
+ interpolateArgs,
242
+ loadMcpConfig,
243
+ mcpConfigPath,
244
+ removeMcpSource,
245
+ saveMcpConfig
246
+ } from "./chunk-4BRO7BOJ.js";
243
247
  import {
244
248
  SDD_DIR,
245
249
  activePointerPath,
@@ -1395,7 +1399,7 @@ ${output}`);
1395
1399
  status = failed.length ? "fail" : "pass";
1396
1400
  reason = failed.length ? `failed: ${failed.join(", ")}` : void 0;
1397
1401
  }
1398
- const ok11 = status === "pass";
1402
+ const ok12 = status === "pass";
1399
1403
  const checklist = stage.gate?.checklist ?? [];
1400
1404
  const lines = buildSummaryLines(
1401
1405
  ctx.meta.title,
@@ -1446,7 +1450,7 @@ ${output}`);
1446
1450
  ctx.meta.verify_results = {
1447
1451
  ...ctx.meta.verify_results,
1448
1452
  [stage.id]: {
1449
- ok: ok11,
1453
+ ok: ok12,
1450
1454
  status,
1451
1455
  ...results.length && headSha2 ? { head_sha: headSha2 } : {},
1452
1456
  at: nowIso(),
@@ -1462,7 +1466,7 @@ ${output}`);
1462
1466
  checklist,
1463
1467
  evidencePath: evidenceDir,
1464
1468
  resultsPath: summaryPath,
1465
- ok: ok11
1469
+ ok: ok12
1466
1470
  };
1467
1471
  }
1468
1472
 
@@ -1875,10 +1879,10 @@ function formatMarkdown(input) {
1875
1879
  `;
1876
1880
  return { markdown: packed ? frame + packed : frame, truncated: input.summary.truncated };
1877
1881
  }
1878
- function formatJsonSummary(summary, gaps, slices, ok11) {
1882
+ function formatJsonSummary(summary, gaps, slices, ok12) {
1879
1883
  return JSON.stringify(
1880
1884
  {
1881
- ok: ok11,
1885
+ ok: ok12,
1882
1886
  summary,
1883
1887
  gaps,
1884
1888
  slices: slices.map((s) => ({
@@ -3562,6 +3566,43 @@ function revisionPrompt(role, draft, gaps) {
3562
3566
  "changed must list every gap cite."
3563
3567
  ].join("\n");
3564
3568
  }
3569
+ function guideDraftPrompt(input) {
3570
+ const rows = [`Job: ${input.role.job}`, `Description: ${input.role.description}`];
3571
+ if (input.role.stages.length > 0) rows.push(`Stages: ${input.role.stages.join(", ")}`);
3572
+ if (input.role.owns.length > 0) rows.push(`Owns: ${input.role.owns.join(", ")}`);
3573
+ if (input.role.skills.length > 0) rows.push(`Skills: ${input.role.skills.join(", ")}`);
3574
+ if (input.role.never.length > 0) rows.push(`Never: ${input.role.never.join(", ")}`);
3575
+ if (input.role.askHumanWhen.length > 0) {
3576
+ rows.push(`Ask when: ${input.role.askHumanWhen.join(", ")}`);
3577
+ }
3578
+ if (rows.length > 2) rows.push("Stay inside this role.");
3579
+ rows.push(`Request: ${input.request}`);
3580
+ if (input.answers.length > 0) {
3581
+ rows.push("Answers:");
3582
+ for (const item of input.answers) rows.push(`- ${item.question}: ${item.answer}`);
3583
+ }
3584
+ rows.push(`Stage: ${input.stageId}`);
3585
+ if (input.summary.trim()) rows.push(`Summary: ${input.summary.trim()}`);
3586
+ if (input.instructions.trim()) rows.push(`Instructions:
3587
+ ${input.instructions.trim()}`);
3588
+ rows.push(`Artifact: ${input.artifactPath}`);
3589
+ if (input.headings.length > 0) {
3590
+ rows.push("Headings:");
3591
+ for (const heading of input.headings) rows.push(`- ${heading}`);
3592
+ }
3593
+ if (input.earlier.length > 0) {
3594
+ rows.push("Earlier:");
3595
+ for (const file of input.earlier) rows.push(`## ${file.path}`, file.body.trim());
3596
+ }
3597
+ rows.push('If one fact is missing, reply with JSON {"text":"Question: ..."}');
3598
+ rows.push('Otherwise reply with JSON {"text":"...","parts":[{"heading":"...","text":"..."}]}');
3599
+ rows.push("Use only the headings listed above.");
3600
+ rows.push("Put this artifact's words under those headings.");
3601
+ rows.push("Do not rename a heading.");
3602
+ rows.push("Do not paste an earlier artifact.");
3603
+ rows.push("Do not name a module, a path, or a command result that is not in this prompt.");
3604
+ return rows.join("\n");
3605
+ }
3565
3606
 
3566
3607
  // src/team/tools.ts
3567
3608
  import { spawn as spawn4 } from "child_process";
@@ -3608,8 +3649,8 @@ function asRecord(value) {
3608
3649
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
3609
3650
  return value;
3610
3651
  }
3611
- function result(name, ok11, text2) {
3612
- return { ok: ok11, name, text: clip(text2) };
3652
+ function result(name, ok12, text2) {
3653
+ return { ok: ok12, name, text: clip(text2) };
3613
3654
  }
3614
3655
  async function git(cwd, args) {
3615
3656
  return new Promise((resolve) => {
@@ -6139,6 +6180,194 @@ async function pickLead(root, all, services) {
6139
6180
  return { kind: "none" };
6140
6181
  }
6141
6182
 
6183
+ // src/role/connector.ts
6184
+ import { spawn as spawn5 } from "child_process";
6185
+ var X_LINE = "A connector cannot read or post on X.";
6186
+ var NOT_IN_SLICE = "That connector is not in this slice.";
6187
+ var DOES_NOT_RUN = "That connector action does not run in this slice.";
6188
+ var BAD_NUMBER = "A GitHub issue number is a positive integer.";
6189
+ var QA_LINE = "A routine cannot write a QA result.";
6190
+ var BAD_ROUTINES = "Routines on this role are invalid.";
6191
+ var NO_ROUTINE = "That routine is not on this role.";
6192
+ var READ_FIELDS = /* @__PURE__ */ new Set(["name", "read", "record"]);
6193
+ function ok9(stdout) {
6194
+ return { exitCode: 0, stdout };
6195
+ }
6196
+ function fail9(stdout) {
6197
+ return { exitCode: 1, stdout };
6198
+ }
6199
+ function positiveInteger(record) {
6200
+ if (!/^[1-9][0-9]*$/.test(record)) return null;
6201
+ const number = Number(record);
6202
+ if (!Number.isSafeInteger(number)) return null;
6203
+ return number;
6204
+ }
6205
+ function missingLine(number) {
6206
+ return `GitHub issue ${number} was not found.`;
6207
+ }
6208
+ function unreadLine(number) {
6209
+ return `GitHub issue ${number} could not be read.`;
6210
+ }
6211
+ function couldNotOpen(number) {
6212
+ return `A change could not be opened for GitHub issue ${number}.`;
6213
+ }
6214
+ function cleanLine(value) {
6215
+ if (typeof value !== "string") return null;
6216
+ const text2 = value.trim();
6217
+ if (!text2 || text2.includes("\n") || text2.includes("\r")) return null;
6218
+ return text2;
6219
+ }
6220
+ function ghIssueSource() {
6221
+ return {
6222
+ async read(number) {
6223
+ const run = await spawnGh(number);
6224
+ if (!run.ok) return { ok: false, missing: run.missing };
6225
+ let parsed;
6226
+ try {
6227
+ parsed = JSON.parse(run.stdout);
6228
+ } catch {
6229
+ return { ok: false, missing: false };
6230
+ }
6231
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6232
+ return { ok: false, missing: false };
6233
+ }
6234
+ const rec = parsed;
6235
+ const title = cleanLine(rec.title);
6236
+ const state = cleanLine(rec.state);
6237
+ const found = typeof rec.number === "number" ? rec.number : Number(rec.number);
6238
+ if (found !== number || !title || !state) return { ok: false, missing: false };
6239
+ return { ok: true, issue: { number, title, state } };
6240
+ }
6241
+ };
6242
+ }
6243
+ function spawnGh(number) {
6244
+ return new Promise((resolve) => {
6245
+ const child = spawn5("gh", ["issue", "view", String(number), "--json", "number,title,state"], {
6246
+ stdio: ["ignore", "pipe", "pipe"]
6247
+ });
6248
+ const out = [];
6249
+ const err = [];
6250
+ child.stdout?.on("data", (chunk) => out.push(chunk));
6251
+ child.stderr?.on("data", (chunk) => err.push(chunk));
6252
+ child.on("error", () => resolve({ ok: false, missing: false }));
6253
+ child.on("close", (code) => {
6254
+ const stdout = Buffer.concat(out).toString("utf8");
6255
+ const stderr = Buffer.concat(err).toString("utf8");
6256
+ if (code === 0) {
6257
+ resolve({ ok: true, stdout });
6258
+ return;
6259
+ }
6260
+ const text2 = `${stdout}
6261
+ ${stderr}`.toLowerCase();
6262
+ resolve({
6263
+ ok: false,
6264
+ missing: text2.includes("could not resolve") || text2.includes("not found")
6265
+ });
6266
+ });
6267
+ });
6268
+ }
6269
+ function threeLines(issue) {
6270
+ return `${issue.number}
6271
+ ${issue.title}
6272
+ ${issue.state}`;
6273
+ }
6274
+ async function readConnector(input) {
6275
+ const verb = input.verb.trim();
6276
+ const connector = input.connector.trim();
6277
+ const record = input.record.trim();
6278
+ if (connector === "x") return fail9(X_LINE);
6279
+ if (verb !== "read" && verb !== "seed") return fail9(DOES_NOT_RUN);
6280
+ if (connector !== "github-issue") return fail9(NOT_IN_SLICE);
6281
+ const number = positiveInteger(record);
6282
+ if (number === null) return fail9(BAD_NUMBER);
6283
+ const source = input.source ?? ghIssueSource();
6284
+ let read;
6285
+ try {
6286
+ read = await source.read(number);
6287
+ } catch {
6288
+ return fail9(unreadLine(number));
6289
+ }
6290
+ if (!read.ok) return fail9(read.missing ? missingLine(number) : unreadLine(number));
6291
+ if (read.issue.number !== number) return fail9(unreadLine(number));
6292
+ const title = cleanLine(read.issue.title);
6293
+ const state = cleanLine(read.issue.state);
6294
+ if (!title || !state) return fail9(unreadLine(number));
6295
+ if (verb === "read") return ok9(threeLines({ number, title, state }));
6296
+ if (!input.projectRoot || !input.config) return fail9(couldNotOpen(number));
6297
+ try {
6298
+ const ctx = await createChange({
6299
+ projectRoot: input.projectRoot,
6300
+ config: input.config,
6301
+ title,
6302
+ workflowName: "feature",
6303
+ epic: `issue-${number}`
6304
+ });
6305
+ return ok9(`${number}
6306
+ ${ctx.id}
6307
+ ${ctx.meta.stage}`);
6308
+ } catch {
6309
+ return fail9(couldNotOpen(number));
6310
+ }
6311
+ }
6312
+ function classifyRoutines(value) {
6313
+ if (!Array.isArray(value)) return { ok: false, reason: "invalid" };
6314
+ for (const item of value) {
6315
+ if (item && typeof item === "object" && !Array.isArray(item) && Object.hasOwn(item, "write")) {
6316
+ return { ok: false, reason: "qa" };
6317
+ }
6318
+ }
6319
+ const items = [];
6320
+ const names = /* @__PURE__ */ new Set();
6321
+ for (const item of value) {
6322
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
6323
+ return { ok: false, reason: "invalid" };
6324
+ }
6325
+ const rec = item;
6326
+ const keys = Object.keys(rec);
6327
+ if (keys.length !== 3 || keys.some((key) => !READ_FIELDS.has(key))) {
6328
+ return { ok: false, reason: "invalid" };
6329
+ }
6330
+ const name = rec.name;
6331
+ const read = rec.read;
6332
+ const record = rec.record;
6333
+ if (typeof name !== "string" || typeof read !== "string" || typeof record !== "string") {
6334
+ return { ok: false, reason: "invalid" };
6335
+ }
6336
+ const trimmed = { name: name.trim(), read: read.trim(), record: record.trim() };
6337
+ if (!trimmed.name || !trimmed.read || !trimmed.record || names.has(trimmed.name)) {
6338
+ return { ok: false, reason: "invalid" };
6339
+ }
6340
+ names.add(trimmed.name);
6341
+ items.push(trimmed);
6342
+ }
6343
+ return { ok: true, items };
6344
+ }
6345
+ async function runRoutine(root, roleName, routineName, source) {
6346
+ const name = canonicalRoleName(roleName);
6347
+ if (!name) return fail9(ROLE_TEXT.notFound);
6348
+ const path = roleFilePath(root, name);
6349
+ if (!await pathExists(path)) return fail9(ROLE_TEXT.notFound);
6350
+ let raw = "";
6351
+ try {
6352
+ raw = await readText(path);
6353
+ } catch {
6354
+ return fail9(ROLE_TEXT.notFound);
6355
+ }
6356
+ const split = splitFrontmatter(raw);
6357
+ if (!split || !Object.hasOwn(split.data, "routines")) return fail9(NO_ROUTINE);
6358
+ const classified = classifyRoutines(split.data.routines);
6359
+ if (!classified.ok) return fail9(classified.reason === "qa" ? QA_LINE : BAD_ROUTINES);
6360
+ const routine = classified.items.find((item) => item.name === routineName.trim());
6361
+ if (!routine) return fail9(NO_ROUTINE);
6362
+ if (routine.read === "x") return fail9(X_LINE);
6363
+ return readConnector({
6364
+ verb: "read",
6365
+ connector: routine.read,
6366
+ record: routine.record,
6367
+ source
6368
+ });
6369
+ }
6370
+
6142
6371
  // src/role/guide.ts
6143
6372
  import { join as join18 } from "pathe";
6144
6373
  var PLANNING = /* @__PURE__ */ new Set([
@@ -6205,10 +6434,10 @@ var DUTIES = [
6205
6434
  description: "The builder builds the change. The person decides."
6206
6435
  }
6207
6436
  ];
6208
- function ok9(stdout) {
6437
+ function ok10(stdout) {
6209
6438
  return { exitCode: 0, stdout };
6210
6439
  }
6211
- function fail9(stdout) {
6440
+ function fail10(stdout) {
6212
6441
  return { exitCode: 1, stdout };
6213
6442
  }
6214
6443
  function statePath(root) {
@@ -6293,15 +6522,6 @@ function nextRoleQuestion(role, answers) {
6293
6522
  );
6294
6523
  return role.askHumanWhen.find((item) => !answered.has(item));
6295
6524
  }
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
6525
  async function roleFiles(root) {
6306
6526
  const names = await listFiles(join18(root, ".sdd", "roles"));
6307
6527
  return names.filter((name) => name.endsWith(".md"));
@@ -6333,35 +6553,218 @@ async function prepareGuide(root, input = {}) {
6333
6553
  }
6334
6554
  }
6335
6555
  lines.push('Next: sdd go "what you want to build"');
6336
- return ok9(lines.join("\n"));
6556
+ return ok10(lines.join("\n"));
6337
6557
  } catch (error) {
6338
- return fail9(error instanceof Error ? error.message : String(error));
6558
+ return fail10(error instanceof Error ? error.message : String(error));
6339
6559
  }
6340
6560
  }
6341
- async function writeDraft(ctxPath, stage, role, text2, request) {
6342
- const body = [
6561
+ function partText(text2) {
6562
+ return text2.split(/\r?\n/).filter((line) => h2Title(line) === null).join("\n").trim();
6563
+ }
6564
+ function draftHeader(stage, role) {
6565
+ return [
6343
6566
  `# ${stage.title ?? stage.id}`,
6344
6567
  "",
6345
6568
  `${role} drafted this stage. The person has not signed it.`,
6346
- "",
6347
- text2.trim(),
6348
- "",
6349
- `Request: ${request}`,
6350
6569
  ""
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;
6570
+ ];
6571
+ }
6572
+ function plainBody(stage, role, request, text2) {
6573
+ return [...draftHeader(stage, role), text2.trim(), "", `Request: ${request}`, ""].join("\n");
6574
+ }
6575
+ function headedBody(stage, role, request, headings, parts) {
6576
+ const matched = /* @__PURE__ */ new Map();
6577
+ for (const part of parts) {
6578
+ const heading = part.heading.trim();
6579
+ const text2 = partText(part.text);
6580
+ if (!headings.includes(heading) || !text2) continue;
6581
+ const list = matched.get(heading) ?? [];
6582
+ list.push(text2);
6583
+ matched.set(heading, list);
6584
+ }
6585
+ const rows = draftHeader(stage, role);
6586
+ for (const heading of headings) {
6587
+ rows.push(`## ${heading}`, "", (matched.get(heading) ?? []).join("\n\n"), "");
6588
+ }
6589
+ rows.push(`Request: ${request}`, "");
6590
+ return rows.join("\n");
6591
+ }
6592
+ var RESULTS_LINES = {
6593
+ Results: "No command output is stored for this change.",
6594
+ "Commands run": "This guide did not run a verify command.",
6595
+ "Sign-off": "The person has not signed it."
6596
+ };
6597
+ function resultsBody(stage, role, request, headings) {
6598
+ if (headings.length === 0) {
6599
+ return plainBody(stage, role, request, Object.values(RESULTS_LINES).join(" "));
6600
+ }
6601
+ return headedBody(
6602
+ stage,
6603
+ role,
6604
+ request,
6605
+ headings,
6606
+ headings.map((heading) => ({
6607
+ heading,
6608
+ text: RESULTS_LINES[heading] ?? RESULTS_LINES.Results
6609
+ }))
6610
+ );
6611
+ }
6612
+ function parseGuideReply(raw) {
6613
+ let value;
6614
+ try {
6615
+ value = JSON.parse(raw);
6616
+ } catch {
6617
+ return null;
6618
+ }
6619
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
6620
+ const rec = value;
6621
+ if (typeof rec.text !== "string") return null;
6622
+ if (rec.parts === void 0) return { text: rec.text, parts: [] };
6623
+ if (!Array.isArray(rec.parts)) return null;
6624
+ const parts = [];
6625
+ for (const part of rec.parts) {
6626
+ if (!part || typeof part !== "object" || Array.isArray(part)) return null;
6627
+ const item = part;
6628
+ if (typeof item.heading !== "string" || typeof item.text !== "string") return null;
6629
+ parts.push({ heading: item.heading, text: item.text });
6630
+ }
6631
+ return { text: rec.text, parts };
6632
+ }
6633
+ function missingHeading(content, headings) {
6634
+ for (const heading of headings) {
6635
+ if (!headingsFilled(content, [heading])) return heading;
6636
+ }
6637
+ return null;
6638
+ }
6639
+ function isResultsFile(path) {
6640
+ const name = path.replace(/\\/g, "/");
6641
+ return name === "local-test-results.md" || name.endsWith("/local-test-results.md");
6642
+ }
6643
+ async function templateHeadings(root, template) {
6644
+ const name = template?.trim();
6645
+ if (!name) return [];
6646
+ const path = join18(templatesDir(root), name.replace(/^templates\//, ""));
6647
+ if (!await pathExists(path)) return [];
6648
+ const headings = [];
6649
+ for (const line of (await readText(path)).split(/\r?\n/)) {
6650
+ const title = h2Title(line);
6651
+ if (title) headings.push(title);
6652
+ }
6653
+ return headings;
6654
+ }
6655
+ async function artifactReady(abs, headings) {
6656
+ if (!await pathExists(abs)) return false;
6657
+ const current = await readText(abs);
6658
+ if (!isSubstantiveArtifactContent(current)) return false;
6659
+ return headings.length === 0 || headingsFilled(current, headings);
6660
+ }
6661
+ async function readyArtifacts(changeDir, stages, currentPath) {
6662
+ const files = [];
6663
+ for (const stage of stages) {
6664
+ for (const artifact of stage.artifacts) {
6665
+ if (artifact.required === false || artifact.path === currentPath) continue;
6666
+ const abs = join18(changeDir, artifact.path);
6667
+ if (!await pathExists(abs)) continue;
6668
+ const body = await readText(abs);
6669
+ if (!isSubstantiveArtifactContent(body)) continue;
6670
+ files.push({ path: artifact.path, body });
6358
6671
  }
6359
- await writeText(abs, body);
6360
6672
  }
6673
+ return files;
6674
+ }
6675
+ async function writeOne(abs, body) {
6676
+ await ensureDir(join18(abs, ".."));
6677
+ await writeText(abs, body);
6361
6678
  }
6362
6679
  function optionalStage(stage) {
6363
6680
  return stage.optional || stage.kind === "clarify" || stage.kind === "brainstorm";
6364
6681
  }
6682
+ async function leaveStage(root, config, changeId, stageId, roleName, drafted, lines, state) {
6683
+ const note = drafted ? `${roleName} drafted ${stageId}.` : `${roleName} left ${stageId}.`;
6684
+ try {
6685
+ const advanced = await advanceStage(root, config, changeId);
6686
+ lines.push(note);
6687
+ if (!advanced.to) {
6688
+ await writeState(root, state);
6689
+ return ok10(
6690
+ [
6691
+ "Done.",
6692
+ ...lines,
6693
+ "No further stages. Run `sdd complete` when ready.",
6694
+ "The person has not signed this change."
6695
+ ].join("\n")
6696
+ );
6697
+ }
6698
+ return "next";
6699
+ } catch (error) {
6700
+ await writeState(root, state);
6701
+ const message = error instanceof Error ? error.message : String(error);
6702
+ return ok10(["Stopped.", note, message, ...lines].join("\n"));
6703
+ }
6704
+ }
6705
+ async function draftStage(root, config, ctx, state, stage, role, required, input, lines) {
6706
+ if (required.length === 0) {
6707
+ return leaveStage(root, config, ctx.id, stage.id, role.name, false, lines, state);
6708
+ }
6709
+ const answers = state.answers.filter((item) => item.role === role.name);
6710
+ for (const artifact of required) {
6711
+ const headings = await templateHeadings(root, artifact.template);
6712
+ const abs = join18(ctx.path, artifact.path);
6713
+ if (await artifactReady(abs, headings)) continue;
6714
+ if (isResultsFile(artifact.path)) {
6715
+ await writeOne(abs, resultsBody(stage, role.name, state.request, headings));
6716
+ continue;
6717
+ }
6718
+ if (!input.model) return fail10(ROLE_TEXT.noModel);
6719
+ let raw = "";
6720
+ try {
6721
+ raw = await input.model.complete({
6722
+ roleId: role.name,
6723
+ prompt: guideDraftPrompt({
6724
+ role,
6725
+ request: state.request,
6726
+ answers,
6727
+ stageId: stage.id,
6728
+ summary: stage.summary ?? "",
6729
+ instructions: stage.agent_context?.instructions ?? "",
6730
+ artifactPath: artifact.path,
6731
+ headings,
6732
+ earlier: await readyArtifacts(ctx.path, ctx.workflow.stages, artifact.path)
6733
+ })
6734
+ });
6735
+ } catch {
6736
+ return fail10("A draft is not valid.");
6737
+ }
6738
+ const parsed = parseGuideReply(raw);
6739
+ if (!parsed) return fail10("A draft is not valid.");
6740
+ const asked = parsed.text.trim();
6741
+ if (asked.startsWith("Question:")) {
6742
+ state.pending = {
6743
+ role: role.name,
6744
+ kind: "model",
6745
+ question: asked.replace(/^Question:\s*/, "").trim() || asked
6746
+ };
6747
+ await writeState(root, state);
6748
+ return ok10(
6749
+ [
6750
+ "Question:",
6751
+ ...lines,
6752
+ `${role.name} asks:`,
6753
+ state.pending.question,
6754
+ 'Answer with: sdd go --answer "..."'
6755
+ ].join("\n")
6756
+ );
6757
+ }
6758
+ const body = headings.length > 0 ? headedBody(stage, role.name, state.request, headings, parsed.parts) : plainBody(stage, role.name, state.request, parsed.text);
6759
+ const missing = headings.length > 0 ? missingHeading(body, headings) : null;
6760
+ if (missing) {
6761
+ await writeState(root, state);
6762
+ return ok10(["Stopped.", `${missing} is still empty.`, ...lines].join("\n"));
6763
+ }
6764
+ await writeOne(abs, body);
6765
+ }
6766
+ return leaveStage(root, config, ctx.id, stage.id, role.name, true, lines, state);
6767
+ }
6365
6768
  async function runGuide(root, input = {}) {
6366
6769
  const prepared = await prepareGuide(root, {
6367
6770
  workflow: input.workflow,
@@ -6370,14 +6773,14 @@ async function runGuide(root, input = {}) {
6370
6773
  if (prepared.exitCode !== 0) return prepared;
6371
6774
  const title = input.title?.trim() ?? "";
6372
6775
  const answer = input.answer?.trim() ?? "";
6373
- if (input.answer !== void 0 && !answer) return fail9("An answer needs text.");
6776
+ if (input.answer !== void 0 && !answer) return fail10("An answer needs text.");
6374
6777
  let loaded;
6375
6778
  try {
6376
6779
  loaded = await readState(root);
6377
6780
  } catch (error) {
6378
- return fail9(error instanceof Error ? error.message : String(error));
6781
+ return fail10(error instanceof Error ? error.message : String(error));
6379
6782
  }
6380
- if (loaded === "invalid") return fail9("The guide file is invalid.");
6783
+ if (loaded === "invalid") return fail10("The guide file is invalid.");
6381
6784
  const config = await loadConfig(root);
6382
6785
  let state = loaded;
6383
6786
  if (title) {
@@ -6391,7 +6794,7 @@ async function runGuide(root, input = {}) {
6391
6794
  state = { change: ctx.id, request: title, answers: [] };
6392
6795
  await writeState(root, state);
6393
6796
  } catch (error) {
6394
- return fail9(error instanceof Error ? error.message : String(error));
6797
+ return fail10(error instanceof Error ? error.message : String(error));
6395
6798
  }
6396
6799
  } else if (!state) {
6397
6800
  const active = await getActiveChangeId(root, config);
@@ -6400,10 +6803,10 @@ async function runGuide(root, input = {}) {
6400
6803
  const ctx = await buildContext(root, config, active);
6401
6804
  state = { change: active, request: ctx.meta.title, answers: [] };
6402
6805
  } catch (error) {
6403
- return fail9(error instanceof Error ? error.message : String(error));
6806
+ return fail10(error instanceof Error ? error.message : String(error));
6404
6807
  }
6405
6808
  }
6406
- if (answer && !state.pending) return fail9("No question is waiting.");
6809
+ if (answer && !state.pending) return fail10("No question is waiting.");
6407
6810
  if (answer && state.pending) {
6408
6811
  state.answers.push({
6409
6812
  role: state.pending.role,
@@ -6413,7 +6816,7 @@ async function runGuide(root, input = {}) {
6413
6816
  state.pending = void 0;
6414
6817
  await writeState(root, state);
6415
6818
  } else if (state.pending) {
6416
- return ok9(questionText(state.pending.role, state.pending.question));
6819
+ return ok10(questionText(state.pending.role, state.pending.question));
6417
6820
  }
6418
6821
  const lines = [];
6419
6822
  let lastRole = "";
@@ -6422,35 +6825,35 @@ async function runGuide(root, input = {}) {
6422
6825
  try {
6423
6826
  ctx = await buildContext(root, config, state.change);
6424
6827
  } catch (error) {
6425
- return fail9(error instanceof Error ? error.message : String(error));
6828
+ return fail10(error instanceof Error ? error.message : String(error));
6426
6829
  }
6427
6830
  const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
6428
- if (!stage) return fail9("No active change.");
6831
+ if (!stage) return fail10("No active change.");
6429
6832
  if (optionalStage(stage) && stage.skippable) {
6430
6833
  const before = ctx.meta.stage;
6431
6834
  try {
6432
6835
  await skipStage(root, config, ctx.id, stage.id, "No role runs this optional stage.");
6433
6836
  } catch (error) {
6434
- return fail9(error instanceof Error ? error.message : String(error));
6837
+ return fail10(error instanceof Error ? error.message : String(error));
6435
6838
  }
6436
6839
  const after = await buildContext(root, config, state.change);
6437
- if (after.meta.stage === before) return fail9("This stage stayed put.");
6840
+ if (after.meta.stage === before) return fail10("This stage stayed put.");
6438
6841
  lines.push(`Skipped ${stage.id}.`);
6439
6842
  continue;
6440
6843
  }
6441
6844
  const routed = await matchRoute(root, { stage: stage.id });
6442
- if (!routed.ok) return fail9(routed.message);
6845
+ if (!routed.ok) return fail10(routed.message);
6443
6846
  if (routed.match.kind === "ask") {
6444
- return ok9(["Stopped.", "Ask the human.", routed.match.reason, ...lines].join("\n"));
6847
+ return ok10(["Stopped.", "Ask the human.", routed.match.reason, ...lines].join("\n"));
6445
6848
  }
6446
6849
  const role = await readRole(root, routed.match.role);
6447
- if (!role) return fail9(ROLE_TEXT.notFound);
6850
+ if (!role) return fail10(ROLE_TEXT.notFound);
6448
6851
  if (lastRole && lastRole !== role.name) lines.push(`Handoff: ${lastRole} to ${role.name}.`);
6449
6852
  const question = nextRoleQuestion(role, state.answers);
6450
6853
  if (question) {
6451
6854
  state.pending = { role: role.name, kind: "role", question };
6452
6855
  await writeState(root, state);
6453
- return ok9(
6856
+ return ok10(
6454
6857
  [
6455
6858
  "Question:",
6456
6859
  ...lines,
@@ -6460,55 +6863,12 @@ async function runGuide(root, input = {}) {
6460
6863
  ].join("\n")
6461
6864
  );
6462
6865
  }
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
- }
6866
+ const required = stage.artifacts.filter((artifact) => artifact.required !== false);
6867
+ const moved = await draftStage(root, config, ctx, state, stage, role, required, input, lines);
6868
+ if (moved !== "next") return moved;
6869
+ lastRole = role.name;
6510
6870
  }
6511
- return fail9("The guide stopped after 16 steps.");
6871
+ return fail10("The guide stopped after 16 steps.");
6512
6872
  }
6513
6873
 
6514
6874
  // src/role/setup-tour.ts
@@ -6531,10 +6891,10 @@ var SETUP_WHY = {
6531
6891
  check: "This is what is already in place.",
6532
6892
  start: "The first change is optional. sdd go asks the role for that stage."
6533
6893
  };
6534
- function ok10(stdout) {
6894
+ function ok11(stdout) {
6535
6895
  return { exitCode: 0, stdout };
6536
6896
  }
6537
- function fail10(stdout) {
6897
+ function fail11(stdout) {
6538
6898
  return { exitCode: 1, stdout };
6539
6899
  }
6540
6900
  function parseSetupStep(raw) {
@@ -6575,7 +6935,7 @@ function withRoleHints(result2) {
6575
6935
  'Add context with sdd role context <name> --text "..."'
6576
6936
  ];
6577
6937
  const body = result2.stdout.trim();
6578
- return ok10(body.length > 0 ? `${body}
6938
+ return ok11(body.length > 0 ? `${body}
6579
6939
  ${hints.join("\n")}` : hints.join("\n"));
6580
6940
  }
6581
6941
  function messageOf(error) {
@@ -6588,31 +6948,31 @@ async function runSetupStep(root, input, env = process.env) {
6588
6948
  if (!await isInitialized(root)) {
6589
6949
  await initProject({ projectRoot: root, agents: false });
6590
6950
  }
6591
- return ok10("Folder: ready.");
6951
+ return ok11("Folder: ready.");
6592
6952
  }
6593
6953
  if (input.step === "workflow") {
6594
6954
  const lines = [`Workflow: ${workflow}.`];
6595
6955
  if ((await roleNames(root)).length > 0) lines.push("Roles are already here.");
6596
- return ok10(lines.join("\n"));
6956
+ return ok11(lines.join("\n"));
6597
6957
  }
6598
6958
  if (input.step === "host") {
6599
6959
  const installed = await loadInstalledAgent(root);
6600
- if (installed) return ok10(`Host: ${installed.ai}.`);
6601
- if (!input.host) return ok10("Host: none.");
6960
+ if (installed) return ok11(`Host: ${installed.ai}.`);
6961
+ if (!input.host) return ok11("Host: none.");
6602
6962
  if (!await isInitialized(root)) {
6603
6963
  await initProject({ projectRoot: root, agents: input.host });
6604
6964
  } else {
6605
6965
  await installAgentIntegration({ projectRoot: root, target: input.host });
6606
6966
  }
6607
6967
  const after = await loadInstalledAgent(root);
6608
- return ok10(`Host: ${after?.ai ?? input.host}.`);
6968
+ return ok11(`Host: ${after?.ai ?? input.host}.`);
6609
6969
  }
6610
6970
  if (input.step === "roles") {
6611
6971
  const choice = input.roles ?? "duties";
6612
6972
  if (choice === "keep") {
6613
6973
  const names = await roleNames(root);
6614
6974
  const line = names.length > 0 ? "Roles are already here." : "Roles: none.";
6615
- return withRoleHints(ok10(line));
6975
+ return withRoleHints(ok11(line));
6616
6976
  }
6617
6977
  if (choice === "software") {
6618
6978
  return withRoleHints(await initTeam(root, "software", gitUserName(root)));
@@ -6621,17 +6981,17 @@ async function runSetupStep(root, input, env = process.env) {
6621
6981
  }
6622
6982
  if (input.step === "model") {
6623
6983
  const state = await modelState(root, env);
6624
- if (state === "configured") return ok10("Model: configured.");
6625
- if (typeof state === "object") return fail10(state.error);
6626
- if (input.requireModel) return fail10("No role model is configured.");
6627
- return ok10("No role model is configured.");
6984
+ if (state === "configured") return ok11("Model: configured.");
6985
+ if (typeof state === "object") return fail11(state.error);
6986
+ if (input.requireModel) return fail11("No role model is configured.");
6987
+ return ok11("No role model is configured.");
6628
6988
  }
6629
6989
  if (input.step === "check") {
6630
- return ok10(formatSetupStatus(await readSetupStatus(root, env)));
6990
+ return ok11(formatSetupStatus(await readSetupStatus(root, env)));
6631
6991
  }
6632
- return ok10('Next: sdd go "what you want to build"');
6992
+ return ok11('Next: sdd go "what you want to build"');
6633
6993
  } catch (error) {
6634
- return fail10(messageOf(error));
6994
+ return fail11(messageOf(error));
6635
6995
  }
6636
6996
  }
6637
6997
 
@@ -6976,6 +7336,7 @@ export {
6976
7336
  getHeadSha,
6977
7337
  getIntegration,
6978
7338
  getStage,
7339
+ ghIssueSource,
6979
7340
  gitUserName,
6980
7341
  guidancePaths,
6981
7342
  handoffRole,
@@ -7040,6 +7401,7 @@ export {
7040
7401
  modelFromEnv,
7041
7402
  nextStageId,
7042
7403
  nowIso,
7404
+ openMcpSearchProvider,
7043
7405
  optionForTarget,
7044
7406
  packHits,
7045
7407
  parseAgentTargets,
@@ -7059,6 +7421,7 @@ export {
7059
7421
  queueRole,
7060
7422
  rankScope,
7061
7423
  readChangeMetrics,
7424
+ readConnector,
7062
7425
  readHandoffCodePoints,
7063
7426
  readSetupStatus,
7064
7427
  readTeamConfig,
@@ -7104,6 +7467,7 @@ export {
7104
7467
  runLoop,
7105
7468
  runNetwork,
7106
7469
  runRole,
7470
+ runRoutine,
7107
7471
  runSetupStep,
7108
7472
  saveChangeMeta,
7109
7473
  saveConfig,