@structured-vibe-coding/core 0.12.1 → 0.13.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
@@ -5,37 +5,67 @@ import {
5
5
  DEFAULT_INIT_INTEGRATION,
6
6
  PROTOCOL_MD,
7
7
  SDD_AGENT_ROLES,
8
- activeStages,
9
8
  agentHostPaths,
10
9
  agentKickoffMessage,
10
+ getIntegration,
11
+ installAgentIntegration,
12
+ installAgentIntegrations,
13
+ integrationKeyFor,
14
+ loadInstalledAgent,
15
+ optionForTarget,
16
+ parseAgentTargets,
17
+ parseIntegration,
18
+ refreshActiveAgentContext,
19
+ removeOtherAgentHosts,
20
+ renderThinAgent,
21
+ writeAgentHandoff
22
+ } from "./chunk-S7SHVKG4.js";
23
+ import {
24
+ defaultConfig,
25
+ isInitialized,
26
+ loadConfig,
27
+ saveConfig
28
+ } from "./chunk-OJFMJHNL.js";
29
+ import {
30
+ advanceStage,
31
+ approveGate,
32
+ canLeaveStage,
33
+ completeChange,
34
+ createChange,
35
+ fallbackIncompleteStageId,
36
+ findFeaturesBacklogPath,
37
+ incompletePriorStageErrors,
38
+ isSubstantiveArtifactContent,
39
+ listBacklogFeatures,
40
+ parseFeaturesBacklog,
41
+ promoteGreenfieldToMemory,
42
+ skipStage,
43
+ startFeatureFromBacklog,
44
+ startGreenfield,
45
+ switchWorkflow,
46
+ updateFeatureStatus
47
+ } from "./chunk-SM734TFW.js";
48
+ import {
11
49
  buildAgentPrompt,
50
+ formatStatus
51
+ } from "./chunk-C725DWSB.js";
52
+ import {
53
+ activeStages,
12
54
  buildContext,
13
55
  changeDirName,
14
56
  clearActiveIf,
15
57
  firstActiveStageId,
16
- formatStatus,
17
58
  getActiveChangeId,
18
- getIntegration,
19
59
  getStage,
20
60
  inferComplexity,
21
- installAgentIntegration,
22
- installAgentIntegrations,
23
- integrationKeyFor,
24
61
  isStageSkipped,
25
62
  listChanges,
26
63
  listWorkflowNames,
27
64
  loadChangeMeta,
28
- loadInstalledAgent,
29
65
  loadWorkflow,
30
66
  nextStageId,
31
67
  nowIso,
32
- optionForTarget,
33
- parseAgentTargets,
34
- parseIntegration,
35
68
  recommendWorkflow,
36
- refreshActiveAgentContext,
37
- removeOtherAgentHosts,
38
- renderThinAgent,
39
69
  resolveChangeId,
40
70
  resolveStages,
41
71
  saveChangeMeta,
@@ -43,9 +73,8 @@ import {
43
73
  shouldAutoSkip,
44
74
  slugify,
45
75
  todayIsoDate,
46
- uniqueChangeDirName,
47
- writeAgentHandoff
48
- } from "./chunk-24S2YWWL.js";
76
+ uniqueChangeDirName
77
+ } from "./chunk-A752UVPF.js";
49
78
  import {
50
79
  ArtifactDefSchema,
51
80
  ChangeMetaSchema,
@@ -53,12 +82,8 @@ import {
53
82
  GateSchema,
54
83
  StageSchema,
55
84
  VerifyCommandSchema,
56
- WorkflowSchema,
57
- defaultConfig,
58
- isInitialized,
59
- loadConfig,
60
- saveConfig
61
- } from "./chunk-KZHNYGD6.js";
85
+ WorkflowSchema
86
+ } from "./chunk-67AUAGNV.js";
62
87
  import {
63
88
  SDD_DIR,
64
89
  activePointerPath,
@@ -88,487 +113,21 @@ import {
88
113
  writeYaml
89
114
  } from "./chunk-EPFURATC.js";
90
115
 
91
- // src/change.ts
92
- import { join as join2 } from "pathe";
93
-
94
- // src/stage-gates.ts
95
- import { join } from "pathe";
96
-
97
- // src/artifacts.ts
98
- function isSubstantiveArtifactContent(content) {
99
- const stripped = content.replace(/<!--[\s\S]*?-->/g, "").replace(/^#{1,6}\s+.*$/gm, "").replace(/^>\s.*$/gm, "").replace(/^\s*[-*+]\s*$/gm, "").replace(/^\s*[-*+]\s*\.\.\.\s*$/gm, "").replace(/^\s*[-*+]\s*TBD\s*$/gim, "").replace(/^\s*[-*+]\s*TODO\s*$/gim, "").replace(/^\s*\|.*\|\s*$/gm, "").replace(/`+/g, "").trim();
100
- if (stripped.length < 48) return false;
101
- const meaningful = stripped.replace(/[-–—_.\s]/g, "").replace(/\b(TBD|TODO|N\/A|none|n\/a)\b/gi, "");
102
- return meaningful.length >= 32;
103
- }
104
- function incompleteArtifactReason(path, content) {
105
- if (content == null) {
106
- return `Missing required artifact: ${path}`;
107
- }
108
- if (!isSubstantiveArtifactContent(content)) {
109
- return [
110
- `Required artifact still incomplete (looks like empty template/stub): ${path}`,
111
- ``,
112
- `Open that file and replace the template with a few real sentences, for example:`,
113
- ` - What is broken or missing?`,
114
- ` - What will you change?`,
115
- ` - How will you know it worked?`,
116
- ``,
117
- `Then run: sdd next`,
118
- `Optional stages only: sdd skip <stage> -r "not needed"`
119
- ].join("\n");
120
- }
121
- return null;
122
- }
123
-
124
- // src/stage-gates.ts
125
- async function checkRequiredArtifacts(changePath2, stageId, artifacts) {
126
- const errors = [];
127
- for (const artifact of artifacts) {
128
- if (!artifact.required) continue;
129
- if (artifact.path.endsWith("/")) {
130
- const target2 = join(changePath2, artifact.path);
131
- if (!await pathExists(target2)) {
132
- errors.push(`Missing required directory: ${artifact.path} (stage ${stageId})`);
133
- }
134
- continue;
135
- }
136
- const target = join(changePath2, artifact.path);
137
- if (!await pathExists(target)) {
138
- errors.push(incompleteArtifactReason(artifact.path, null));
139
- continue;
140
- }
141
- const content = await readText(target);
142
- const reason = incompleteArtifactReason(artifact.path, content);
143
- if (reason) {
144
- errors.push(`${reason} (stage ${stageId})`);
145
- }
146
- }
147
- return errors;
148
- }
149
- async function incompletePriorStageErrors(ctx) {
150
- const errors = [];
151
- const stages = activeStages(ctx.workflow, ctx.meta);
152
- const currentIdx = stages.findIndex((s) => s.id === ctx.meta.stage);
153
- if (currentIdx <= 0) return errors;
154
- for (let i = 0; i < currentIdx; i++) {
155
- const stage = stages[i];
156
- if (isStageSkipped(ctx.meta, stage.id) || shouldAutoSkip(stage, ctx.meta)) {
157
- continue;
158
- }
159
- const required = stage.artifacts.filter((a) => a.required);
160
- if (!required.length && stage.optional) continue;
161
- const stageErrors = await checkRequiredArtifacts(
162
- ctx.path,
163
- stage.id,
164
- stage.artifacts
165
- );
166
- if (stageErrors.length) {
167
- errors.push(
168
- `Prior stage "${stage.id}" is not complete \u2014 finish it or \`sdd skip ${stage.id} -r "\u2026"\` if optional.`
169
- );
170
- errors.push(...stageErrors);
171
- }
172
- }
173
- return errors;
174
- }
175
- async function canLeaveStage(ctx, config) {
176
- const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
177
- const errors = [];
178
- const warnings = [];
179
- if (!stage) {
180
- return { ok: false, errors: [`Unknown stage: ${ctx.meta.stage}`], warnings };
181
- }
182
- errors.push(...await incompletePriorStageErrors(ctx));
183
- errors.push(
184
- ...await checkRequiredArtifacts(ctx.path, stage.id, stage.artifacts)
185
- );
186
- const declared = stage.gate?.type ?? "soft";
187
- let effectiveGate = declared;
188
- if (config.policy.gates === "hard" && declared === "soft") {
189
- effectiveGate = "hard";
190
- }
191
- const gateState = ctx.meta.gates?.[stage.id];
192
- if (effectiveGate === "hard") {
193
- if (!gateState || gateState.status !== "approved" && gateState.status !== "waived") {
194
- errors.push(
195
- `Hard gate on stage "${stage.id}" is not approved. Run \`sdd gate approve\` or complete the checklist.`
196
- );
197
- }
198
- } else if (effectiveGate === "soft" && stage.gate?.checklist?.length) {
199
- if (!gateState || gateState.status === "pending") {
200
- warnings.push(
201
- `Soft gate checklist not signed off on "${stage.id}" (proceeding allowed).`
202
- );
203
- }
204
- }
205
- const requiredCmds = stage.verify?.commands?.filter((c) => c.required) ?? [];
206
- if (requiredCmds.length) {
207
- const verifyOk = ctx.meta.verify_results?.[stage.id]?.ok === true;
208
- const humanOverride = gateState?.status === "waived" || gateState?.status === "approved";
209
- if (!verifyOk && !humanOverride) {
210
- errors.push(
211
- `Required local verify commands have not passed for "${stage.id}". Run \`sdd verify\` (or \`sdd gate approve/waive\` with a note).`
212
- );
213
- } else if (!verifyOk && humanOverride) {
214
- warnings.push(
215
- `Stage "${stage.id}" gate signed off without a successful required verify run.`
216
- );
217
- }
218
- }
219
- if (stage.optional || stage.kind === "clarify" || stage.kind === "brainstorm") {
220
- warnings.push(
221
- `Stage "${stage.id}" is optional (${stage.kind ?? "optional"}). Skip with: sdd skip ${stage.id} -r "not needed"`
222
- );
223
- }
224
- return { ok: errors.length === 0, errors, warnings };
225
- }
226
- async function fallbackIncompleteStageId(ctx) {
227
- const stages = activeStages(ctx.workflow, ctx.meta);
228
- for (const stage of stages) {
229
- if (stage.id === ctx.meta.stage) break;
230
- if (isStageSkipped(ctx.meta, stage.id) || shouldAutoSkip(stage, ctx.meta)) {
231
- continue;
232
- }
233
- const required = stage.artifacts.filter((a) => a.required);
234
- if (!required.length && stage.optional) continue;
235
- const errs = await checkRequiredArtifacts(ctx.path, stage.id, stage.artifacts);
236
- if (errs.length) return stage.id;
237
- }
238
- return null;
239
- }
240
- async function approveGate(projectRoot, config, changeId, stageId, note, status = "approved") {
241
- const ctx = await buildContext(projectRoot, config, changeId);
242
- const id = stageId ?? ctx.meta.stage;
243
- ctx.meta.gates = {
244
- ...ctx.meta.gates,
245
- [id]: { status, note, at: nowIso() }
246
- };
247
- await saveChangeMeta(projectRoot, config, ctx.meta);
248
- return buildContext(projectRoot, config, changeId);
249
- }
250
-
251
- // src/change.ts
252
- async function existingChangeIds(projectRoot, config) {
253
- const open = await listDirs(changesDir(projectRoot, config));
254
- const archived = await listDirs(archiveDir(projectRoot, config));
255
- return /* @__PURE__ */ new Set([...open, ...archived]);
256
- }
257
- async function createChange(input) {
258
- const { projectRoot, config, title, workflowName } = input;
259
- const workflow = await loadWorkflow(projectRoot, workflowName);
260
- if (!workflow.stages.length) {
261
- throw new Error(`Workflow ${workflowName} has no stages`);
262
- }
263
- const baseId = changeDirName(title);
264
- const id = uniqueChangeDirName(baseId, await existingChangeIds(projectRoot, config));
265
- const dir = changePath(projectRoot, config, id);
266
- await ensureDir(dir);
267
- let meta = ChangeMetaSchema.parse({
268
- id,
269
- title,
270
- workflow: workflowName,
271
- created: nowIso(),
272
- updated: nowIso(),
273
- status: "in_progress",
274
- stage: workflow.stages[0].id,
275
- domain: input.domain,
276
- branch: input.branch,
277
- flags: input.flags ?? {},
278
- overrides: {
279
- skip_stages: [],
280
- gates: {},
281
- extra_stages: []
282
- },
283
- skipped: [],
284
- gates: {}
285
- });
286
- const startId = firstActiveStageId(workflow, meta) ?? workflow.stages[0].id;
287
- meta = { ...meta, stage: startId };
288
- const firstStage = getStage(workflow, meta, startId);
289
- if (!firstStage) {
290
- throw new Error(`Workflow ${workflowName} has no active stages`);
291
- }
292
- await writeYaml(changeMetaPath(projectRoot, config, id), meta);
293
- await materializeStageArtifacts(projectRoot, config, id, workflow, meta, firstStage);
294
- await setActiveChange(projectRoot, config, id);
295
- try {
296
- const { refreshActiveAgentContext: refreshActiveAgentContext2 } = await import("./agents-R4RMNXOS.js");
297
- await refreshActiveAgentContext2(projectRoot);
298
- } catch {
299
- }
300
- return buildContext(projectRoot, config, id);
301
- }
302
- async function materializeStageArtifacts(projectRoot, config, changeId, workflow, meta, stage) {
303
- const created = [];
304
- const dir = changePath(projectRoot, config, changeId);
305
- const tdir = templatesDir(projectRoot);
306
- for (const artifact of stage.artifacts) {
307
- const target = join2(dir, artifact.path);
308
- if (await pathExists(target)) continue;
309
- if (artifact.path.endsWith("/")) {
310
- await ensureDir(target);
311
- created.push(artifact.path);
312
- continue;
313
- }
314
- let content = defaultArtifactContent(artifact.id, meta, stage, workflow);
315
- if (artifact.template) {
316
- const templatePath = join2(tdir, artifact.template.replace(/^templates\//, ""));
317
- const candidates = [
318
- join2(tdir, artifact.template),
319
- templatePath,
320
- join2(tdir, `${artifact.id}.md`)
321
- ];
322
- for (const c of candidates) {
323
- if (await pathExists(c)) {
324
- content = interpolate(await readText(c), meta, stage, workflow);
325
- break;
326
- }
327
- }
328
- } else {
329
- const fallback = join2(tdir, `${artifact.id}.md`);
330
- if (await pathExists(fallback)) {
331
- content = interpolate(await readText(fallback), meta, stage, workflow);
332
- }
333
- }
334
- await writeText(target, content);
335
- created.push(artifact.path);
336
- }
337
- return created;
338
- }
339
- function interpolate(template, meta, stage, workflow) {
340
- return template.replaceAll("{{title}}", meta.title).replaceAll("{{id}}", meta.id).replaceAll("{{workflow}}", workflow.name).replaceAll("{{stage}}", stage.id).replaceAll("{{stage_title}}", stage.title ?? stage.id).replaceAll("{{date}}", meta.created.slice(0, 10));
341
- }
342
- function defaultArtifactContent(artifactId, meta, stage, workflow) {
343
- return `# ${artifactId}
344
-
345
- > Change: **${meta.title}** (\`${meta.id}\`)
346
- > Workflow: \`${workflow.name}\` \xB7 Stage: \`${stage.id}\`
347
-
348
- ## Summary
349
-
350
- <!-- Write the intent / content for this artifact -->
351
-
352
- ## Notes
353
-
354
- -
355
- `;
356
- }
357
- async function advanceStage(projectRoot, config, changeId, opts) {
358
- let ctx = await buildContext(projectRoot, config, changeId);
359
- const warnings = [];
360
- if (ctx.meta.status === "completed") {
361
- throw new Error("Change is already completed.");
362
- }
363
- if (!opts?.force) {
364
- const fallback = await fallbackIncompleteStageId(ctx);
365
- if (fallback && fallback !== ctx.meta.stage) {
366
- const from2 = ctx.meta.stage;
367
- ctx.meta.stage = fallback;
368
- await saveChangeMeta(projectRoot, config, ctx.meta);
369
- const stage2 = getStage(ctx.workflow, ctx.meta, fallback);
370
- const artifactsCreated2 = await materializeStageArtifacts(
371
- projectRoot,
372
- config,
373
- changeId,
374
- ctx.workflow,
375
- ctx.meta,
376
- stage2
377
- );
378
- try {
379
- const { refreshActiveAgentContext: refreshActiveAgentContext2 } = await import("./agents-R4RMNXOS.js");
380
- await refreshActiveAgentContext2(projectRoot);
381
- } catch {
382
- }
383
- const updated2 = await buildContext(projectRoot, config, changeId);
384
- return {
385
- ctx: updated2,
386
- from: from2,
387
- to: fallback,
388
- completed: false,
389
- artifactsCreated: artifactsCreated2,
390
- warnings: [
391
- ...warnings,
392
- `Fell back to incomplete stage "${fallback}" (required work not done). Finish it before advancing.`
393
- ]
394
- };
395
- }
396
- }
397
- const check = await canLeaveStage(ctx, config);
398
- warnings.push(...check.warnings);
399
- if (!check.ok && !opts?.force) {
400
- throw new Error(check.errors.join("\n"));
401
- }
402
- const from = ctx.meta.stage;
403
- const next = nextStageId(ctx.workflow, ctx.meta);
404
- if (!next) {
405
- return {
406
- ctx,
407
- from,
408
- to: null,
409
- completed: false,
410
- artifactsCreated: [],
411
- warnings: [...warnings, "No further stages. Run `sdd complete` when ready."]
412
- };
413
- }
414
- ctx.meta.stage = next;
415
- await saveChangeMeta(projectRoot, config, ctx.meta);
416
- const stage = getStage(ctx.workflow, ctx.meta, next);
417
- const artifactsCreated = await materializeStageArtifacts(
418
- projectRoot,
419
- config,
420
- changeId,
421
- ctx.workflow,
422
- ctx.meta,
423
- stage
424
- );
425
- try {
426
- const { refreshActiveAgentContext: refreshActiveAgentContext2 } = await import("./agents-R4RMNXOS.js");
427
- await refreshActiveAgentContext2(projectRoot);
428
- } catch {
429
- }
430
- const updated = await buildContext(projectRoot, config, changeId);
431
- return {
432
- ctx: updated,
433
- from,
434
- to: next,
435
- completed: false,
436
- artifactsCreated,
437
- warnings
438
- };
439
- }
440
- async function skipStage(projectRoot, config, changeId, stageId, reason) {
441
- if (!config.per_change.allow_skip) {
442
- throw new Error("Skipping stages is disabled in .sdd/config.yaml (per_change.allow_skip).");
443
- }
444
- if (config.per_change.require_reason_on_skip && !reason.trim()) {
445
- throw new Error("A reason is required to skip a stage.");
446
- }
447
- const ctx = await buildContext(projectRoot, config, changeId);
448
- const stage = getStage(ctx.workflow, ctx.meta, stageId);
449
- if (!stage) {
450
- throw new Error(`Stage "${stageId}" not found in workflow ${ctx.meta.workflow}`);
451
- }
452
- if (!stage.skippable) {
453
- throw new Error(`Stage "${stageId}" is not skippable.`);
454
- }
455
- const skip_stages = new Set(ctx.meta.overrides.skip_stages ?? []);
456
- skip_stages.add(stageId);
457
- ctx.meta.overrides.skip_stages = [...skip_stages];
458
- ctx.meta.skipped = [
459
- ...ctx.meta.skipped ?? [],
460
- { stage: stageId, reason, at: nowIso() }
461
- ];
462
- if (ctx.meta.stage === stageId) {
463
- const next = nextStageId(ctx.workflow, ctx.meta);
464
- if (next) {
465
- ctx.meta.stage = next;
466
- const nextStage = getStage(ctx.workflow, ctx.meta, next);
467
- if (nextStage) {
468
- await materializeStageArtifacts(
469
- projectRoot,
470
- config,
471
- changeId,
472
- ctx.workflow,
473
- ctx.meta,
474
- nextStage
475
- );
476
- }
477
- }
478
- }
479
- await saveChangeMeta(projectRoot, config, ctx.meta);
480
- return buildContext(projectRoot, config, changeId);
481
- }
482
- async function switchWorkflow(projectRoot, config, changeId, workflowName, reason) {
483
- const ctx = await buildContext(projectRoot, config, changeId);
484
- const workflow = await loadWorkflow(projectRoot, workflowName);
485
- if (config.allowed_workflows?.length && !config.allowed_workflows.includes(workflowName)) {
486
- throw new Error(
487
- `Workflow "${workflowName}" is not in allowed_workflows: ${config.allowed_workflows.join(", ")}`
488
- );
489
- }
490
- ctx.meta.workflow = workflowName;
491
- const hasStage = workflow.stages.some((s) => s.id === ctx.meta.stage);
492
- if (!hasStage) {
493
- ctx.meta.stage = workflow.stages[0].id;
494
- }
495
- if (reason) {
496
- ctx.meta.flags = {
497
- ...ctx.meta.flags,
498
- workflow_switch_reason: reason
499
- };
500
- }
501
- await saveChangeMeta(projectRoot, config, ctx.meta);
502
- const stage = getStage(workflow, ctx.meta, ctx.meta.stage);
503
- if (stage) {
504
- await materializeStageArtifacts(projectRoot, config, changeId, workflow, ctx.meta, stage);
505
- }
506
- return buildContext(projectRoot, config, changeId);
507
- }
508
- async function completeChange(projectRoot, config, changeId) {
509
- const ctx = await buildContext(projectRoot, config, changeId);
510
- const next = nextStageId(ctx.workflow, ctx.meta);
511
- if (next) {
512
- const active = activeStages(ctx.workflow, ctx.meta);
513
- const last = active[active.length - 1];
514
- if (last && ctx.meta.stage !== last.id) {
515
- throw new Error(
516
- `Cannot complete: still on stage "${ctx.meta.stage}". Finish remaining stages or skip them. Next: ${next}`
517
- );
518
- }
519
- }
520
- const check = await canLeaveStage(ctx, config);
521
- if (!check.ok) {
522
- throw new Error(check.errors.join("\n"));
523
- }
524
- ctx.meta.status = "completed";
525
- ctx.meta.completed_at = nowIso();
526
- await saveChangeMeta(projectRoot, config, ctx.meta);
527
- await clearActiveIf(projectRoot, config, changeId);
528
- let archivedTo = null;
529
- const shouldArchive = config.persistence.archive_on_complete && ctx.workflow.on_complete?.archive !== false;
530
- if (shouldArchive) {
531
- const dest = join2(archiveDir(projectRoot, config), changeId);
532
- if (await pathExists(dest)) {
533
- throw new Error(`Archive already exists for change: ${changeId}`);
534
- }
535
- await ensureDir(archiveDir(projectRoot, config));
536
- await copyDir(ctx.path, dest);
537
- await removePath(ctx.path);
538
- archivedTo = dest;
539
- }
540
- if (archivedTo) {
541
- const meta = ChangeMetaSchema.parse(await readYaml(join2(archivedTo, "meta.yaml")));
542
- return {
543
- archivedTo,
544
- ctx: {
545
- id: changeId,
546
- path: archivedTo,
547
- meta,
548
- workflow: ctx.workflow,
549
- stages: ctx.stages,
550
- active: ctx.active
551
- }
552
- };
553
- }
554
- return { archivedTo, ctx: await buildContext(projectRoot, config, changeId) };
555
- }
556
-
557
116
  // src/init.ts
558
- import { join as join4 } from "pathe";
117
+ import { join as join2 } from "pathe";
559
118
 
560
119
  // src/defaults.ts
561
120
  import { existsSync } from "fs";
562
121
  import { fileURLToPath } from "url";
563
- import { dirname, join as join3 } from "pathe";
122
+ import { dirname, join } from "pathe";
564
123
  function defaultsRoot() {
565
124
  const here = dirname(fileURLToPath(import.meta.url));
566
125
  const candidates = [
567
- join3(here, "..", "defaults"),
126
+ join(here, "..", "defaults"),
568
127
  // npm package: dist/../defaults
569
- join3(here, "defaults"),
128
+ join(here, "defaults"),
570
129
  // same-dir defaults if present
571
- join3(here, "..", "..", "defaults")
130
+ join(here, "..", "..", "defaults")
572
131
  // src/ during some test runners
573
132
  ];
574
133
  for (const c of candidates) {
@@ -577,13 +136,13 @@ function defaultsRoot() {
577
136
  return candidates[0];
578
137
  }
579
138
  function defaultWorkflowsDir() {
580
- return join3(defaultsRoot(), "workflows");
139
+ return join(defaultsRoot(), "workflows");
581
140
  }
582
141
  function defaultTemplatesDir() {
583
- return join3(defaultsRoot(), "templates");
142
+ return join(defaultsRoot(), "templates");
584
143
  }
585
144
  function defaultMemoryDir() {
586
- return join3(defaultsRoot(), "memory");
145
+ return join(defaultsRoot(), "memory");
587
146
  }
588
147
 
589
148
  // src/init.ts
@@ -591,15 +150,15 @@ async function initProject(opts) {
591
150
  const { projectRoot, force = false } = opts;
592
151
  const created = [];
593
152
  const root = sddRoot(projectRoot);
594
- if (await pathExists(join4(root, "config.yaml")) && !force) {
153
+ if (await pathExists(join2(root, "config.yaml")) && !force) {
595
154
  throw new Error(
596
- `Already initialized (${join4(root, "config.yaml")}). Use --force to re-copy defaults.`
155
+ `Already initialized (${join2(root, "config.yaml")}). Use --force to re-copy defaults.`
597
156
  );
598
157
  }
599
158
  const config = defaultConfig();
600
159
  await ensureDir(root);
601
160
  await saveConfig(projectRoot, config);
602
- created.push(join4(root, "config.yaml"));
161
+ created.push(join2(root, "config.yaml"));
603
162
  const wfDest = workflowsDir(projectRoot);
604
163
  await ensureDir(wfDest);
605
164
  const wfSrc = defaultWorkflowsDir();
@@ -621,7 +180,7 @@ async function initProject(opts) {
621
180
  await copyDirSkipExisting(mSrc, mDest);
622
181
  } else {
623
182
  await writeTextIfMissing(
624
- join4(mDest, "index.md"),
183
+ join2(mDest, "index.md"),
625
184
  `# Documentation map
626
185
 
627
186
  Parent page for **stable** project docs. Start here, then open only what you need.
@@ -637,11 +196,11 @@ Active work: \`.sdd/active-context.md\` and \`changes/<id>/\`.
637
196
  `
638
197
  );
639
198
  await writeTextIfMissing(
640
- join4(mDest, "product.md"),
199
+ join2(mDest, "product.md"),
641
200
  "# Product\n\n<!-- What are you building? -->\n"
642
201
  );
643
202
  await writeTextIfMissing(
644
- join4(mDest, "constitution.md"),
203
+ join2(mDest, "constitution.md"),
645
204
  `# Constitution
646
205
 
647
206
  <!-- Non-negotiables agents must not violate. Broader style lives in conventions.md. -->
@@ -668,21 +227,21 @@ Active work: \`.sdd/active-context.md\` and \`changes/<id>/\`.
668
227
  `
669
228
  );
670
229
  await writeTextIfMissing(
671
- join4(mDest, "architecture.md"),
230
+ join2(mDest, "architecture.md"),
672
231
  "# Architecture\n\n<!-- High-level system shape -->\n"
673
232
  );
674
233
  await writeTextIfMissing(
675
- join4(mDest, "conventions.md"),
234
+ join2(mDest, "conventions.md"),
676
235
  "# Conventions\n\n<!-- Coding and process conventions -->\n"
677
236
  );
678
237
  }
679
- if (!await pathExists(join4(mDest, "constitution.md"))) {
680
- const fromDefault = join4(mSrc, "constitution.md");
238
+ if (!await pathExists(join2(mDest, "constitution.md"))) {
239
+ const fromDefault = join2(mSrc, "constitution.md");
681
240
  if (await pathExists(fromDefault)) {
682
241
  await copyDirSkipExisting(mSrc, mDest);
683
242
  } else {
684
243
  await writeTextIfMissing(
685
- join4(mDest, "constitution.md"),
244
+ join2(mDest, "constitution.md"),
686
245
  `# Constitution
687
246
 
688
247
  <!-- Non-negotiables agents must not violate. Broader style lives in conventions.md. -->
@@ -715,9 +274,9 @@ Active work: \`.sdd/active-context.md\` and \`changes/<id>/\`.
715
274
  await ensureDir(domainsDir(projectRoot, config));
716
275
  created.push(changesDir(projectRoot, config));
717
276
  created.push(domainsDir(projectRoot, config));
718
- await writeText(join4(changesDir(projectRoot, config), ".gitkeep"), "");
719
- await writeText(join4(domainsDir(projectRoot, config), ".gitkeep"), "");
720
- const sddReadme = join4(root, "README.md");
277
+ await writeText(join2(changesDir(projectRoot, config), ".gitkeep"), "");
278
+ await writeText(join2(domainsDir(projectRoot, config), ".gitkeep"), "");
279
+ const sddReadme = join2(root, "README.md");
721
280
  if (!await pathExists(sddReadme) || force) {
722
281
  await writeText(
723
282
  sddReadme,
@@ -764,7 +323,7 @@ sdd complete
764
323
 
765
324
  // src/verify.ts
766
325
  import { spawn } from "child_process";
767
- import { join as join5 } from "pathe";
326
+ import { join as join3 } from "pathe";
768
327
  function runCommand(command, cwd) {
769
328
  return new Promise((resolve) => {
770
329
  const child = spawn(command, {
@@ -836,7 +395,7 @@ async function runLocalVerify(projectRoot, config, changeId, opts) {
836
395
  throw new Error(`Unknown stage: ${ctx.meta.stage}`);
837
396
  }
838
397
  const evidenceDirRaw = stage.verify?.evidence_dir?.trim();
839
- const evidenceDir = evidenceDirRaw && evidenceDirRaw.length > 0 ? join5(ctx.path, evidenceDirRaw) : null;
398
+ const evidenceDir = evidenceDirRaw && evidenceDirRaw.length > 0 ? join3(ctx.path, evidenceDirRaw) : null;
840
399
  if (evidenceDir) {
841
400
  await ensureDir(evidenceDir);
842
401
  }
@@ -853,7 +412,7 @@ async function runLocalVerify(projectRoot, config, changeId, opts) {
853
412
  output: output.slice(0, 5e4)
854
413
  });
855
414
  if (evidenceDir) {
856
- const logPath = join5(evidenceDir, `${cmd.name.replace(/\s+/g, "-")}.log`);
415
+ const logPath = join3(evidenceDir, `${cmd.name.replace(/\s+/g, "-")}.log`);
857
416
  await writeText(
858
417
  logPath,
859
418
  `# ${cmd.name}
@@ -880,7 +439,7 @@ ${output}`
880
439
  ok = results.every((r) => r.exitCode === 0);
881
440
  }
882
441
  const checklist = stage.gate?.checklist ?? [];
883
- const summaryPath = join5(ctx.path, "local-test-results.md");
442
+ const summaryPath = join3(ctx.path, "local-test-results.md");
884
443
  const lines = buildSummaryLines(ctx.meta.title, stage.id, ok, results, checklist);
885
444
  const summaryBody = lines.join("\n");
886
445
  if (await pathExists(summaryPath)) {
@@ -913,7 +472,7 @@ ${output}`
913
472
  if (evidenceDir) {
914
473
  const stamp = nowIso();
915
474
  await writeText(
916
- join5(evidenceDir, `run-${stamp.replace(/[:.]/g, "-")}.md`),
475
+ join3(evidenceDir, `run-${stamp.replace(/[:.]/g, "-")}.md`),
917
476
  summaryBody
918
477
  );
919
478
  }
@@ -939,7 +498,7 @@ ${output}`
939
498
  }
940
499
 
941
500
  // src/code-context/index.ts
942
- import { extname as extname4, join as join8, normalize as normalize5 } from "pathe";
501
+ import { extname as extname4, join as join6, normalize as normalize5 } from "pathe";
943
502
 
944
503
  // src/code-context/adapters/registry.ts
945
504
  import { extname as extname2 } from "pathe";
@@ -1262,7 +821,7 @@ function formatJsonSummary(summary, gaps, slices, ok) {
1262
821
 
1263
822
  // src/code-context/focus.ts
1264
823
  import { promises as fs } from "fs";
1265
- import { isAbsolute, join as join6, normalize as normalize3, relative } from "pathe";
824
+ import { isAbsolute, join as join4, normalize as normalize3, relative } from "pathe";
1266
825
 
1267
826
  // src/code-context/ignore.ts
1268
827
  import { basename as basename2, extname as extname3, normalize as normalize2 } from "pathe";
@@ -1440,12 +999,12 @@ function extractPathLikeTokens(text) {
1440
999
  return found;
1441
1000
  }
1442
1001
  function toRepoRelative(projectRoot, p) {
1443
- const abs = isAbsolute(p) ? p : join6(projectRoot, p);
1002
+ const abs = isAbsolute(p) ? p : join4(projectRoot, p);
1444
1003
  const rel = relative(projectRoot, abs);
1445
1004
  return normalize3(rel).replace(/\\/g, "/");
1446
1005
  }
1447
1006
  function toAbsolute(projectRoot, p) {
1448
- return isAbsolute(p) ? p : join6(projectRoot, p);
1007
+ return isAbsolute(p) ? p : join4(projectRoot, p);
1449
1008
  }
1450
1009
  async function walkFiles(absDir, projectRoot, maxFiles, out, gaps) {
1451
1010
  if (out.length >= maxFiles) return;
@@ -1458,7 +1017,7 @@ async function walkFiles(absDir, projectRoot, maxFiles, out, gaps) {
1458
1017
  }
1459
1018
  for (const entry of entries) {
1460
1019
  if (out.length >= maxFiles) return;
1461
- const abs = join6(absDir, entry.name);
1020
+ const abs = join4(absDir, entry.name);
1462
1021
  const rel = toRepoRelative(projectRoot, abs);
1463
1022
  const kind = classifyPath(rel);
1464
1023
  if (kind === "secret" || kind === "ignored") continue;
@@ -1552,7 +1111,7 @@ async function resolveChangeMeta(projectRoot, changeId, gaps) {
1552
1111
  return null;
1553
1112
  }
1554
1113
  const cpath = changePath(projectRoot, config, id);
1555
- const metaPath = join6(cpath, "meta.yaml");
1114
+ const metaPath = join4(cpath, "meta.yaml");
1556
1115
  if (!await pathExists(metaPath)) {
1557
1116
  if (!await pathExists(cpath)) {
1558
1117
  gaps.push({
@@ -1585,7 +1144,7 @@ async function searchByKeywords(projectRoot, keywords, symbols, maxFiles) {
1585
1144
  const terms = [...keywords, ...symbols.map((s) => s.toLowerCase())];
1586
1145
  if (!terms.length) return out;
1587
1146
  for (const root of SOURCE_ROOTS) {
1588
- const abs = join6(projectRoot, root);
1147
+ const abs = join4(projectRoot, root);
1589
1148
  if (!await pathExists(abs)) continue;
1590
1149
  const files = [];
1591
1150
  await walkFiles(abs, projectRoot, maxFiles * 4, files, []);
@@ -1599,7 +1158,7 @@ async function searchByKeywords(projectRoot, keywords, symbols, maxFiles) {
1599
1158
  }
1600
1159
  if (symbols.length) {
1601
1160
  try {
1602
- const text = await readText(join6(projectRoot, rel));
1161
+ const text = await readText(join4(projectRoot, rel));
1603
1162
  if (symbols.some((s) => text.includes(s))) {
1604
1163
  out.push(rel);
1605
1164
  }
@@ -1635,7 +1194,7 @@ async function resolveFocus(request, options) {
1635
1194
  }
1636
1195
  notes.push(`Change signals from: ${change.id}`);
1637
1196
  for (const artifact of ["tasks.md", "feature.md", "stories.md", "lld.md"]) {
1638
- const body = await readBounded(join6(change.path, artifact), 8e3);
1197
+ const body = await readBounded(join4(change.path, artifact), 8e3);
1639
1198
  if (!body) continue;
1640
1199
  if (!request.paths?.length) {
1641
1200
  for (const p of extractPathLikeTokens(body)) {
@@ -1718,7 +1277,7 @@ async function resolveFocus(request, options) {
1718
1277
  }
1719
1278
 
1720
1279
  // src/code-context/graph.ts
1721
- import { dirname as dirname2, join as join7, normalize as normalize4 } from "pathe";
1280
+ import { dirname as dirname2, join as join5, normalize as normalize4 } from "pathe";
1722
1281
  var GraphLite = class {
1723
1282
  files = /* @__PURE__ */ new Map();
1724
1283
  edges = [];
@@ -1749,7 +1308,7 @@ var GraphLite = class {
1749
1308
  resolveImportTarget(fromFile, specifier) {
1750
1309
  if (!specifier.startsWith(".")) return void 0;
1751
1310
  const base = dirname2(fromFile);
1752
- const joined = normalize4(join7(base, specifier)).replace(/\\/g, "/");
1311
+ const joined = normalize4(join5(base, specifier)).replace(/\\/g, "/");
1753
1312
  const candidates = [
1754
1313
  joined,
1755
1314
  `${joined}.ts`,
@@ -2175,7 +1734,7 @@ async function generateCodeContext(request, deps) {
2175
1734
  });
2176
1735
  continue;
2177
1736
  }
2178
- const abs = join8(projectRoot, rel);
1737
+ const abs = join6(projectRoot, rel);
2179
1738
  if (!await pathExists(abs)) {
2180
1739
  gaps.push({
2181
1740
  code: "path_missing",
@@ -2264,7 +1823,7 @@ async function generateCodeContext(request, deps) {
2264
1823
  if (plan.changeId) {
2265
1824
  try {
2266
1825
  const { readYaml: readYaml2 } = await import("./fs-J6DIV6CX.js");
2267
- const metaPath = join8(
1826
+ const metaPath = join6(
2268
1827
  projectRoot,
2269
1828
  // best-effort: changes/<id>/meta.yaml via relative walk
2270
1829
  // Prefer using change path from resolve — re-read lightly
@@ -2272,10 +1831,10 @@ async function generateCodeContext(request, deps) {
2272
1831
  plan.changeId,
2273
1832
  "meta.yaml"
2274
1833
  );
2275
- const { loadConfig: loadConfig2 } = await import("./config-4QZMRSL3.js");
1834
+ const { loadConfig: loadConfig2 } = await import("./config-ZYEEIRXJ.js");
2276
1835
  const { changePath: changePath2 } = await import("./paths-64GA32IZ.js");
2277
1836
  const config = await loadConfig2(projectRoot);
2278
- const mp = join8(changePath2(projectRoot, config, plan.changeId), "meta.yaml");
1837
+ const mp = join6(changePath2(projectRoot, config, plan.changeId), "meta.yaml");
2279
1838
  if (await pathExists(mp)) {
2280
1839
  const meta = await readYaml2(mp);
2281
1840
  changeTitle = meta.title ?? plan.changeId;
@@ -2329,7 +1888,7 @@ async function generateCodeContext(request, deps) {
2329
1888
  }
2330
1889
 
2331
1890
  // src/refine.ts
2332
- import { join as join9 } from "pathe";
1891
+ import { join as join7 } from "pathe";
2333
1892
  function stageArtifactRefs(ctx, stageId) {
2334
1893
  const stage = getStage(ctx.workflow, ctx.meta, stageId);
2335
1894
  if (!stage) return [];
@@ -2337,7 +1896,7 @@ function stageArtifactRefs(ctx, stageId) {
2337
1896
  stageId,
2338
1897
  path: a.path,
2339
1898
  required: Boolean(a.required),
2340
- absPath: join9(ctx.path, a.path)
1899
+ absPath: join7(ctx.path, a.path)
2341
1900
  }));
2342
1901
  }
2343
1902
  function listMarkdown(refs) {
@@ -2382,11 +1941,11 @@ async function buildRefinePlan(opts) {
2382
1941
  laterArtifacts.push(...stageArtifactRefs(ctx, s.id));
2383
1942
  }
2384
1943
  const focusArtifacts = stageArtifactRefs(ctx, focusStageId);
2385
- const constitutionPath = join9(memoryDir(opts.projectRoot, opts.config), "constitution.md");
1944
+ const constitutionPath = join7(memoryDir(opts.projectRoot, opts.config), "constitution.md");
2386
1945
  const constitutionExists = await pathExists(constitutionPath);
2387
- const briefPath = join9(ctx.path, "refine-brief.md");
2388
- const reportPath = join9(ctx.path, "quality-report.md");
2389
- const openItemsPath = join9(ctx.path, "open-items.md");
1946
+ const briefPath = join7(ctx.path, "refine-brief.md");
1947
+ const reportPath = join7(ctx.path, "quality-report.md");
1948
+ const openItemsPath = join7(ctx.path, "open-items.md");
2390
1949
  const briefMarkdown = buildRefineBriefMarkdown({
2391
1950
  ctx,
2392
1951
  focusStageId,
@@ -2515,7 +2074,7 @@ Do **not** invent content for later stages that were not materialized yet. If a
2515
2074
  }
2516
2075
  async function writeRefineBrief(plan, projectRoot) {
2517
2076
  await writeText(plan.briefPath, plan.briefMarkdown);
2518
- const pointer = join9(sddRoot(projectRoot), "refine-brief.md");
2077
+ const pointer = join7(sddRoot(projectRoot), "refine-brief.md");
2519
2078
  await writeText(
2520
2079
  pointer,
2521
2080
  `# Refine brief pointer
@@ -2576,6 +2135,7 @@ export {
2576
2135
  ensureDir,
2577
2136
  fallbackAdapter,
2578
2137
  fallbackIncompleteStageId,
2138
+ findFeaturesBacklogPath,
2579
2139
  firstActiveStageId,
2580
2140
  formatJsonSummary,
2581
2141
  formatStatus,
@@ -2594,6 +2154,7 @@ export {
2594
2154
  isSecretPath,
2595
2155
  isStageSkipped,
2596
2156
  isSubstantiveArtifactContent,
2157
+ listBacklogFeatures,
2597
2158
  listChanges,
2598
2159
  listDirs,
2599
2160
  listFiles,
@@ -2608,8 +2169,10 @@ export {
2608
2169
  nowIso,
2609
2170
  optionForTarget,
2610
2171
  parseAgentTargets,
2172
+ parseFeaturesBacklog,
2611
2173
  parseIntegration,
2612
2174
  pathExists,
2175
+ promoteGreenfieldToMemory,
2613
2176
  readText,
2614
2177
  readYaml,
2615
2178
  recommendWorkflow,
@@ -2628,12 +2191,15 @@ export {
2628
2191
  shouldAutoSkip,
2629
2192
  skipStage,
2630
2193
  slugify,
2194
+ startFeatureFromBacklog,
2195
+ startGreenfield,
2631
2196
  switchWorkflow,
2632
2197
  templatesDir,
2633
2198
  todayIsoDate,
2634
2199
  tokenizeKeywords,
2635
2200
  typescriptAdapter,
2636
2201
  uniqueChangeDirName,
2202
+ updateFeatureStatus,
2637
2203
  workflowsDir,
2638
2204
  writeAgentHandoff,
2639
2205
  writeRefineBrief,