@structured-vibe-coding/core 0.2.2 → 0.3.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,966 +1,92 @@
1
- // src/schemas.ts
2
- import { z } from "zod";
3
- var GateSchema = z.object({
4
- type: z.enum(["none", "soft", "hard"]).default("soft"),
5
- mode: z.enum(["none", "human_approval", "checklist"]).default("checklist"),
6
- checklist: z.array(z.string()).default([]),
7
- overridable: z.boolean().default(true)
8
- });
9
- var ArtifactDefSchema = z.object({
10
- id: z.string(),
11
- path: z.string(),
12
- template: z.string().optional(),
13
- required: z.boolean().default(true)
14
- });
15
- var VerifyCommandSchema = z.object({
16
- name: z.string(),
17
- run: z.string(),
18
- required: z.boolean().default(false)
19
- });
20
- var StageSchema = z.object({
21
- id: z.string(),
22
- title: z.string().optional(),
23
- summary: z.string().optional(),
24
- artifacts: z.array(ArtifactDefSchema).default([]),
25
- gate: GateSchema.default({}),
26
- skippable: z.boolean().default(true),
27
- skip_when: z.object({
28
- flags: z.array(z.string()).optional()
29
- }).optional(),
30
- verify: z.object({
31
- commands: z.array(VerifyCommandSchema).default([]),
32
- evidence_dir: z.string().optional()
33
- }).optional(),
34
- agent_context: z.object({
35
- include: z.array(z.string()).default([]),
36
- instructions: z.string().optional()
37
- }).optional()
38
- });
39
- var WorkflowSchema = z.object({
40
- name: z.string(),
41
- description: z.string().optional(),
42
- version: z.number().default(1),
43
- stages: z.array(StageSchema).min(1),
44
- recommendation: z.object({
45
- when: z.object({
46
- labels: z.array(z.string()).optional(),
47
- keywords: z.array(z.string()).optional(),
48
- max_complexity: z.enum(["low", "medium", "high"]).optional(),
49
- min_complexity: z.enum(["low", "medium", "high"]).optional()
50
- }).optional(),
51
- priority: z.number().default(0)
52
- }).optional(),
53
- on_complete: z.object({
54
- archive: z.boolean().default(true),
55
- domain_sync: z.enum(["never", "recommend", "require"]).default("never")
56
- }).optional()
57
- });
58
- var ConfigSchema = z.object({
59
- version: z.number().default(1),
60
- memory_path: z.string().default("memory"),
61
- changes_path: z.string().default("changes"),
62
- archive_path: z.string().default("archive"),
63
- domains_path: z.string().default("domains"),
64
- default_workflow: z.string().default("recommend"),
65
- allowed_workflows: z.array(z.string()).optional(),
66
- per_change: z.object({
67
- allow_skip: z.boolean().default(true),
68
- allow_gate_override: z.enum(["true", "false", "soft_only"]).default("soft_only"),
69
- allow_custom_stages: z.boolean().default(true),
70
- require_reason_on_skip: z.boolean().default(true)
71
- }).default({}),
72
- persistence: z.object({
73
- default: z.enum(["change_only", "domain_sync"]).default("change_only"),
74
- archive_on_complete: z.boolean().default(true),
75
- domain_sync: z.object({
76
- mode: z.enum(["never", "recommend", "require"]).default("never"),
77
- anchored_domains: z.array(z.string()).default([])
78
- }).default({})
79
- }).default({}),
80
- policy: z.object({
81
- gates: z.enum(["soft", "hard"]).default("soft")
82
- }).default({})
83
- });
84
- var ChangeMetaSchema = z.object({
85
- id: z.string(),
86
- title: z.string(),
87
- workflow: z.string(),
88
- created: z.string(),
89
- updated: z.string().optional(),
90
- status: z.enum(["in_progress", "blocked", "completed", "abandoned"]).default("in_progress"),
91
- stage: z.string(),
92
- branch: z.string().optional(),
93
- domain: z.string().optional(),
94
- flags: z.record(z.union([z.boolean(), z.string(), z.number()])).default({}),
95
- overrides: z.object({
96
- skip_stages: z.array(z.string()).default([]),
97
- gates: z.record(z.enum(["none", "soft", "hard"])).default({}),
98
- extra_stages: z.array(StageSchema).default([])
99
- }).default({}),
100
- skipped: z.array(
101
- z.object({
102
- stage: z.string(),
103
- reason: z.string(),
104
- at: z.string()
105
- })
106
- ).default([]),
107
- gates: z.record(
108
- z.object({
109
- status: z.enum(["pending", "approved", "failed", "waived"]),
110
- note: z.string().optional(),
111
- at: z.string().optional()
112
- })
113
- ).default({}),
114
- /** Results from `sdd verify` keyed by stage id */
115
- verify_results: z.record(
116
- z.object({
117
- ok: z.boolean(),
118
- at: z.string(),
119
- results: z.array(
120
- z.object({
121
- name: z.string(),
122
- exitCode: z.number().nullable()
123
- })
124
- ).default([])
125
- })
126
- ).default({}),
127
- pr: z.object({
128
- number: z.number().optional(),
129
- url: z.string().optional()
130
- }).optional(),
131
- completed_at: z.string().optional()
132
- });
133
-
134
- // src/paths.ts
135
- import { join } from "pathe";
136
- var SDD_DIR = ".sdd";
137
- function sddRoot(projectRoot) {
138
- return join(projectRoot, SDD_DIR);
139
- }
140
- function configPath(projectRoot) {
141
- return join(sddRoot(projectRoot), "config.yaml");
142
- }
143
- function workflowsDir(projectRoot) {
144
- return join(sddRoot(projectRoot), "workflows");
145
- }
146
- function templatesDir(projectRoot) {
147
- return join(sddRoot(projectRoot), "templates");
148
- }
149
- function memoryDir(projectRoot, config) {
150
- return join(projectRoot, config.memory_path);
151
- }
152
- function changesDir(projectRoot, config) {
153
- return join(projectRoot, config.changes_path);
154
- }
155
- function archiveDir(projectRoot, config) {
156
- return join(projectRoot, config.archive_path);
157
- }
158
- function domainsDir(projectRoot, config) {
159
- return join(projectRoot, config.domains_path);
160
- }
161
- function changePath(projectRoot, config, changeId) {
162
- return join(changesDir(projectRoot, config), changeId);
163
- }
164
- function changeMetaPath(projectRoot, config, changeId) {
165
- return join(changePath(projectRoot, config, changeId), "meta.yaml");
166
- }
167
- function activePointerPath(projectRoot, config) {
168
- return join(changesDir(projectRoot, config), ".active");
169
- }
170
-
171
- // src/fs.ts
172
- import { promises as fs } from "fs";
173
- import { dirname } from "pathe";
174
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
175
- async function pathExists(path) {
176
- try {
177
- await fs.access(path);
178
- return true;
179
- } catch {
180
- return false;
181
- }
182
- }
183
- async function ensureDir(path) {
184
- await fs.mkdir(path, { recursive: true });
185
- }
186
- async function readText(path) {
187
- return fs.readFile(path, "utf8");
188
- }
189
- async function writeText(path, content) {
190
- await ensureDir(dirname(path));
191
- await fs.writeFile(path, content, "utf8");
192
- }
193
- async function readYaml(path) {
194
- const raw = await readText(path);
195
- return parseYaml(raw);
196
- }
197
- async function writeYaml(path, data) {
198
- const content = stringifyYaml(data, {
199
- lineWidth: 100,
200
- defaultStringType: "PLAIN",
201
- defaultKeyType: "PLAIN"
202
- });
203
- await writeText(path, content.endsWith("\n") ? content : `${content}
204
- `);
205
- }
206
- async function listDirs(path) {
207
- if (!await pathExists(path)) return [];
208
- const entries = await fs.readdir(path, { withFileTypes: true });
209
- return entries.filter((e) => e.isDirectory()).map((e) => e.name);
210
- }
211
- async function listFiles(path) {
212
- if (!await pathExists(path)) return [];
213
- const entries = await fs.readdir(path, { withFileTypes: true });
214
- return entries.filter((e) => e.isFile()).map((e) => e.name);
215
- }
216
- async function copyDir(src, dest) {
217
- await ensureDir(dest);
218
- const entries = await fs.readdir(src, { withFileTypes: true });
219
- for (const entry of entries) {
220
- const from = `${src}/${entry.name}`;
221
- const to = `${dest}/${entry.name}`;
222
- if (entry.isDirectory()) {
223
- await copyDir(from, to);
224
- } else {
225
- await ensureDir(dirname(to));
226
- await fs.copyFile(from, to);
227
- }
228
- }
229
- }
230
- async function removePath(path) {
231
- await fs.rm(path, { recursive: true, force: true });
232
- }
233
-
234
- // src/config.ts
235
- async function isInitialized(projectRoot) {
236
- return pathExists(configPath(projectRoot));
237
- }
238
- async function loadConfig(projectRoot) {
239
- const path = configPath(projectRoot);
240
- if (!await pathExists(path)) {
241
- throw new Error(
242
- `SDD is not initialized in ${projectRoot}. Run \`sdd init\` first.`
243
- );
244
- }
245
- const raw = await readYaml(path);
246
- return ConfigSchema.parse(raw);
247
- }
248
- async function saveConfig(projectRoot, config) {
249
- await ensureDir(sddRoot(projectRoot));
250
- await writeYaml(configPath(projectRoot), ConfigSchema.parse(config));
251
- }
252
- function defaultConfig() {
253
- return ConfigSchema.parse({});
254
- }
255
- function projectPaths(projectRoot, config) {
256
- return {
257
- sdd: sddRoot(projectRoot),
258
- config: configPath(projectRoot),
259
- workflows: workflowsDir(projectRoot),
260
- templates: templatesDir(projectRoot),
261
- memory: memoryDir(projectRoot, config)
262
- };
263
- }
264
-
265
- // src/workflow.ts
266
- import { join as join2 } from "pathe";
267
- async function listWorkflowNames(projectRoot) {
268
- const dir = workflowsDir(projectRoot);
269
- const files = await listFiles(dir);
270
- return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => f.replace(/\.ya?ml$/, "")).sort();
271
- }
272
- async function loadWorkflow(projectRoot, name) {
273
- const dir = workflowsDir(projectRoot);
274
- const candidates = [`${name}.yaml`, `${name}.yml`];
275
- for (const file of candidates) {
276
- const path = join2(dir, file);
277
- if (await pathExists(path)) {
278
- const raw = await readYaml(path);
279
- const workflow = WorkflowSchema.parse(raw);
280
- if (workflow.name !== name) {
281
- }
282
- return workflow;
283
- }
284
- }
285
- throw new Error(
286
- `Workflow "${name}" not found in ${dir}. Available: ${(await listWorkflowNames(projectRoot)).join(", ") || "(none)"}`
287
- );
288
- }
289
- function resolveStages(workflow, meta) {
290
- const base = [...workflow.stages];
291
- const extra = meta.overrides?.extra_stages ?? [];
292
- const stages = [...base, ...extra];
293
- return stages.map((stage) => {
294
- const gateOverride = meta.overrides?.gates?.[stage.id];
295
- if (!gateOverride) return stage;
296
- return {
297
- ...stage,
298
- gate: {
299
- ...stage.gate,
300
- type: gateOverride
301
- }
302
- };
303
- });
304
- }
305
- function isStageSkipped(meta, stageId) {
306
- if (meta.overrides?.skip_stages?.includes(stageId)) return true;
307
- if (meta.skipped?.some((s) => s.stage === stageId)) return true;
308
- return false;
309
- }
310
- function shouldAutoSkip(stage, meta) {
311
- const flags = stage.skip_when?.flags ?? [];
312
- for (const flag of flags) {
313
- if (meta.flags?.[flag] === true || meta.flags?.[flag] === "true") {
314
- return true;
315
- }
316
- }
317
- return false;
318
- }
319
- function activeStages(workflow, meta) {
320
- return resolveStages(workflow, meta).filter((s) => {
321
- if (isStageSkipped(meta, s.id)) return false;
322
- if (shouldAutoSkip(s, meta)) return false;
323
- return true;
324
- });
325
- }
326
- function getStage(workflow, meta, stageId) {
327
- return resolveStages(workflow, meta).find((s) => s.id === stageId);
328
- }
329
- function nextStageId(workflow, meta) {
330
- const all = resolveStages(workflow, meta);
331
- const activeIds = new Set(activeStages(workflow, meta).map((s) => s.id));
332
- let start = all.findIndex((s) => s.id === meta.stage);
333
- if (start === -1) start = -1;
334
- for (let i = start + 1; i < all.length; i++) {
335
- const id = all[i].id;
336
- if (activeIds.has(id)) return id;
337
- }
338
- return null;
339
- }
340
- function previousStageId(workflow, meta) {
341
- const all = resolveStages(workflow, meta);
342
- const activeIds = new Set(activeStages(workflow, meta).map((s) => s.id));
343
- const start = all.findIndex((s) => s.id === meta.stage);
344
- if (start <= 0) return null;
345
- for (let i = start - 1; i >= 0; i--) {
346
- const id = all[i].id;
347
- if (activeIds.has(id)) return id;
348
- }
349
- return null;
350
- }
351
- function firstActiveStageId(workflow, meta) {
352
- return activeStages(workflow, meta)[0]?.id ?? null;
353
- }
354
- function inferComplexity(title, hints) {
355
- const text = `${title} ${(hints?.keywords ?? []).join(" ")}`.toLowerCase();
356
- const hotfixHints = ["typo", "hotfix", "nit", "rename", "bump", "chore", "readme", "docs"];
357
- const highHints = [
358
- "architecture",
359
- "migration",
360
- "auth",
361
- "payment",
362
- "billing",
363
- "redesign",
364
- "platform",
365
- "arb",
366
- "epic"
367
- ];
368
- if (hotfixHints.some((h) => text.includes(h))) return "low";
369
- if (highHints.some((h) => text.includes(h))) return "high";
370
- if (text.length > 80) return "medium";
371
- return "medium";
372
- }
373
- async function recommendWorkflow(projectRoot, title, config, opts) {
374
- const available = await listWorkflowNames(projectRoot);
375
- const allowed = config.allowed_workflows?.length ? available.filter((n) => config.allowed_workflows.includes(n)) : available;
376
- if (opts?.preferred) {
377
- if (!available.includes(opts.preferred)) {
378
- throw new Error(
379
- `Workflow "${opts.preferred}" is not available. Available: ${available.join(", ") || "(none)"}`
380
- );
381
- }
382
- if (config.allowed_workflows?.length && !config.allowed_workflows.includes(opts.preferred)) {
383
- throw new Error(
384
- `Workflow "${opts.preferred}" is not in allowed_workflows: ${config.allowed_workflows.join(", ")}`
385
- );
386
- }
387
- return {
388
- name: opts.preferred,
389
- reason: "Explicitly selected",
390
- alternatives: allowed.filter((n) => n !== opts.preferred)
391
- };
392
- }
393
- if (config.default_workflow !== "recommend" && allowed.includes(config.default_workflow)) {
394
- return {
395
- name: config.default_workflow,
396
- reason: `Repo default_workflow=${config.default_workflow}`,
397
- alternatives: allowed.filter((n) => n !== config.default_workflow)
398
- };
399
- }
400
- const complexity = opts?.complexity ?? inferComplexity(title);
401
- const scored = [];
402
- for (const name of allowed) {
403
- try {
404
- const wf = await loadWorkflow(projectRoot, name);
405
- let score = wf.recommendation?.priority ?? 0;
406
- let reason = `workflow priority ${score}`;
407
- const when = wf.recommendation?.when;
408
- if (complexity === "low" && name === "hotfix") {
409
- score += 50;
410
- reason = "Low complexity \u2192 hotfix";
411
- } else if (complexity === "low" && name === "patch") {
412
- score += 40;
413
- reason = "Low complexity \u2192 patch";
414
- } else if (complexity === "medium" && name === "feature") {
415
- score += 45;
416
- reason = "Medium complexity \u2192 feature";
417
- } else if (complexity === "medium" && name === "patch") {
418
- score += 30;
419
- reason = "Medium complexity \u2192 patch";
420
- } else if (complexity === "high" && name === "enterprise-feature") {
421
- score += 50;
422
- reason = "High complexity \u2192 enterprise-feature";
423
- } else if (complexity === "high" && name === "feature") {
424
- score += 35;
425
- reason = "High complexity \u2192 feature";
426
- }
427
- const keywords = when?.keywords ?? [];
428
- const titleLower = title.toLowerCase();
429
- if (keywords.some((k) => titleLower.includes(k.toLowerCase()))) {
430
- score += 20;
431
- reason = `Keyword match for ${name}`;
432
- }
433
- scored.push({ name, score, reason });
434
- } catch {
435
- }
436
- }
437
- scored.sort((a, b) => b.score - a.score);
438
- const best = scored[0];
439
- if (!best) {
440
- throw new Error("No workflows available. Run `sdd init` or add files under .sdd/workflows/");
441
- }
442
- return {
443
- name: best.name,
444
- reason: best.reason,
445
- alternatives: scored.slice(1).map((s) => s.name)
446
- };
447
- }
448
-
449
- // src/change.ts
450
- import { join as join3 } from "pathe";
451
-
452
- // src/slug.ts
453
- function slugify(input) {
454
- return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
455
- }
456
- function todayIsoDate(d = /* @__PURE__ */ new Date()) {
457
- return d.toISOString().slice(0, 10);
458
- }
459
- function nowIso(d = /* @__PURE__ */ new Date()) {
460
- return d.toISOString();
461
- }
462
- function changeDirName(title, d = /* @__PURE__ */ new Date()) {
463
- const slug = slugify(title) || "change";
464
- return `${todayIsoDate(d)}-${slug}`;
465
- }
466
- function uniqueChangeDirName(base, taken) {
467
- if (!taken.has(base)) return base;
468
- let n = 2;
469
- while (taken.has(`${base}-${n}`)) n += 1;
470
- return `${base}-${n}`;
471
- }
472
-
473
- // src/change.ts
474
- async function existingChangeIds(projectRoot, config) {
475
- const open = await listDirs(changesDir(projectRoot, config));
476
- const archived = await listDirs(archiveDir(projectRoot, config));
477
- return /* @__PURE__ */ new Set([...open, ...archived]);
478
- }
479
- async function createChange(input) {
480
- const { projectRoot, config, title, workflowName } = input;
481
- const workflow = await loadWorkflow(projectRoot, workflowName);
482
- if (!workflow.stages.length) {
483
- throw new Error(`Workflow ${workflowName} has no stages`);
484
- }
485
- const baseId = changeDirName(title);
486
- const id = uniqueChangeDirName(baseId, await existingChangeIds(projectRoot, config));
487
- const dir = changePath(projectRoot, config, id);
488
- await ensureDir(dir);
489
- let meta = ChangeMetaSchema.parse({
490
- id,
491
- title,
492
- workflow: workflowName,
493
- created: nowIso(),
494
- updated: nowIso(),
495
- status: "in_progress",
496
- stage: workflow.stages[0].id,
497
- domain: input.domain,
498
- branch: input.branch,
499
- flags: input.flags ?? {},
500
- overrides: {
501
- skip_stages: [],
502
- gates: {},
503
- extra_stages: []
504
- },
505
- skipped: [],
506
- gates: {}
507
- });
508
- const startId = firstActiveStageId(workflow, meta) ?? workflow.stages[0].id;
509
- meta = { ...meta, stage: startId };
510
- const firstStage = getStage(workflow, meta, startId);
511
- if (!firstStage) {
512
- throw new Error(`Workflow ${workflowName} has no active stages`);
513
- }
514
- await writeYaml(changeMetaPath(projectRoot, config, id), meta);
515
- await materializeStageArtifacts(projectRoot, config, id, workflow, meta, firstStage);
516
- await setActiveChange(projectRoot, config, id);
517
- return buildContext(projectRoot, config, id);
518
- }
519
- async function materializeStageArtifacts(projectRoot, config, changeId, workflow, meta, stage) {
520
- const created = [];
521
- const dir = changePath(projectRoot, config, changeId);
522
- const tdir = templatesDir(projectRoot);
523
- for (const artifact of stage.artifacts) {
524
- const target = join3(dir, artifact.path);
525
- if (await pathExists(target)) continue;
526
- if (artifact.path.endsWith("/")) {
527
- await ensureDir(target);
528
- created.push(artifact.path);
529
- continue;
530
- }
531
- let content = defaultArtifactContent(artifact.id, meta, stage, workflow);
532
- if (artifact.template) {
533
- const templatePath = join3(tdir, artifact.template.replace(/^templates\//, ""));
534
- const candidates = [
535
- join3(tdir, artifact.template),
536
- templatePath,
537
- join3(tdir, `${artifact.id}.md`)
538
- ];
539
- for (const c of candidates) {
540
- if (await pathExists(c)) {
541
- const raw = await readText(c);
542
- content = interpolate(raw, meta, stage, workflow);
543
- break;
544
- }
545
- }
546
- } else {
547
- const fallback = join3(tdir, `${artifact.id}.md`);
548
- if (await pathExists(fallback)) {
549
- content = interpolate(await readText(fallback), meta, stage, workflow);
550
- }
551
- }
552
- await writeText(target, content);
553
- created.push(artifact.path);
554
- }
555
- return created;
556
- }
557
- function interpolate(template, meta, stage, workflow) {
558
- 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));
559
- }
560
- function defaultArtifactContent(artifactId, meta, stage, workflow) {
561
- return `# ${artifactId}
562
-
563
- > Change: **${meta.title}** (\`${meta.id}\`)
564
- > Workflow: \`${workflow.name}\` \xB7 Stage: \`${stage.id}\`
565
-
566
- ## Summary
567
-
568
- <!-- Write the intent / content for this artifact -->
569
-
570
- ## Notes
571
-
572
- -
573
- `;
574
- }
575
- async function loadChangeMeta(projectRoot, config, changeId) {
576
- const path = changeMetaPath(projectRoot, config, changeId);
577
- if (!await pathExists(path)) {
578
- throw new Error(`Change not found: ${changeId}`);
579
- }
580
- return ChangeMetaSchema.parse(await readYaml(path));
581
- }
582
- async function saveChangeMeta(projectRoot, config, meta) {
583
- meta.updated = nowIso();
584
- await writeYaml(changeMetaPath(projectRoot, config, meta.id), ChangeMetaSchema.parse(meta));
585
- }
586
- async function buildContext(projectRoot, config, changeId) {
587
- const meta = await loadChangeMeta(projectRoot, config, changeId);
588
- const workflow = await loadWorkflow(projectRoot, meta.workflow);
589
- return {
590
- id: changeId,
591
- path: changePath(projectRoot, config, changeId),
592
- meta,
593
- workflow,
594
- stages: resolveStages(workflow, meta),
595
- active: activeStages(workflow, meta)
596
- };
597
- }
598
- async function listChanges(projectRoot, config) {
599
- return listDirs(changesDir(projectRoot, config));
600
- }
601
- async function getActiveChangeId(projectRoot, config) {
602
- const pointer = activePointerPath(projectRoot, config);
603
- if (await pathExists(pointer)) {
604
- const id = (await readText(pointer)).trim();
605
- if (id && await pathExists(changePath(projectRoot, config, id))) {
606
- return id;
607
- }
608
- if (id) {
609
- await writeText(pointer, "");
610
- }
611
- }
612
- const all = await listChanges(projectRoot, config);
613
- const inProgress = [];
614
- for (const id of all) {
615
- try {
616
- const meta = await loadChangeMeta(projectRoot, config, id);
617
- if (meta.status === "in_progress" || meta.status === "blocked") {
618
- inProgress.push({ id, updated: meta.updated ?? meta.created });
619
- }
620
- } catch {
621
- }
622
- }
623
- if (!inProgress.length) return null;
624
- inProgress.sort((a, b) => b.updated.localeCompare(a.updated));
625
- return inProgress[0].id;
626
- }
627
- async function clearActiveIf(projectRoot, config, changeId) {
628
- const pointer = activePointerPath(projectRoot, config);
629
- if (!await pathExists(pointer)) return;
630
- const id = (await readText(pointer)).trim();
631
- if (id === changeId) {
632
- await writeText(pointer, "");
633
- }
634
- }
635
- async function setActiveChange(projectRoot, config, changeId) {
636
- await ensureDir(changesDir(projectRoot, config));
637
- await writeText(activePointerPath(projectRoot, config), `${changeId}
638
- `);
639
- }
640
- async function resolveChangeId(projectRoot, config, changeId) {
641
- if (changeId) return changeId;
642
- const active = await getActiveChangeId(projectRoot, config);
643
- if (!active) {
644
- throw new Error("No active change. Pass --change <id> or run `sdd new`.");
645
- }
646
- return active;
647
- }
648
- async function canLeaveStage(ctx, config) {
649
- const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
650
- const errors = [];
651
- const warnings = [];
652
- if (!stage) {
653
- return { ok: false, errors: [`Unknown stage: ${ctx.meta.stage}`], warnings };
654
- }
655
- for (const artifact of stage.artifacts) {
656
- if (!artifact.required) continue;
657
- const target = join3(ctx.path, artifact.path);
658
- if (!await pathExists(target)) {
659
- errors.push(`Missing required artifact: ${artifact.path}`);
660
- continue;
661
- }
662
- }
663
- const declared = stage.gate?.type ?? "soft";
664
- let effectiveGate = declared;
665
- if (config.policy.gates === "hard" && declared === "soft") {
666
- effectiveGate = "hard";
667
- }
668
- const gateState = ctx.meta.gates?.[stage.id];
669
- if (effectiveGate === "hard") {
670
- if (!gateState || gateState.status !== "approved" && gateState.status !== "waived") {
671
- errors.push(
672
- `Hard gate on stage "${stage.id}" is not approved. Run \`sdd gate approve\` or complete the checklist.`
673
- );
674
- }
675
- } else if (effectiveGate === "soft" && stage.gate?.checklist?.length) {
676
- if (!gateState || gateState.status === "pending") {
677
- warnings.push(
678
- `Soft gate checklist not signed off on "${stage.id}" (proceeding allowed).`
679
- );
680
- }
681
- }
682
- const requiredCmds = stage.verify?.commands?.filter((c) => c.required) ?? [];
683
- if (requiredCmds.length) {
684
- const verifyOk = ctx.meta.verify_results?.[stage.id]?.ok === true;
685
- const humanOverride = gateState?.status === "waived" || gateState?.status === "approved";
686
- if (!verifyOk && !humanOverride) {
687
- errors.push(
688
- `Required local verify commands have not passed for "${stage.id}". Run \`sdd verify\` (or \`sdd gate approve/waive\` with a note).`
689
- );
690
- } else if (!verifyOk && humanOverride) {
691
- warnings.push(
692
- `Stage "${stage.id}" gate signed off without a successful required verify run.`
693
- );
694
- }
695
- }
696
- return { ok: errors.length === 0, errors, warnings };
697
- }
698
- async function advanceStage(projectRoot, config, changeId, opts) {
699
- const ctx = await buildContext(projectRoot, config, changeId);
700
- const warnings = [];
701
- if (ctx.meta.status === "completed") {
702
- throw new Error("Change is already completed.");
703
- }
704
- const check = await canLeaveStage(ctx, config);
705
- warnings.push(...check.warnings);
706
- if (!check.ok && !opts?.force) {
707
- throw new Error(check.errors.join("\n"));
708
- }
709
- const from = ctx.meta.stage;
710
- const next = nextStageId(ctx.workflow, ctx.meta);
711
- if (!next) {
712
- return {
713
- ctx,
714
- from,
715
- to: null,
716
- completed: false,
717
- artifactsCreated: [],
718
- warnings: [...warnings, "No further stages. Run `sdd complete` when ready."]
719
- };
720
- }
721
- ctx.meta.stage = next;
722
- await saveChangeMeta(projectRoot, config, ctx.meta);
723
- const stage = getStage(ctx.workflow, ctx.meta, next);
724
- const artifactsCreated = await materializeStageArtifacts(
725
- projectRoot,
726
- config,
727
- changeId,
728
- ctx.workflow,
729
- ctx.meta,
730
- stage
731
- );
732
- const updated = await buildContext(projectRoot, config, changeId);
733
- return {
734
- ctx: updated,
735
- from,
736
- to: next,
737
- completed: false,
738
- artifactsCreated,
739
- warnings
740
- };
741
- }
742
- async function skipStage(projectRoot, config, changeId, stageId, reason) {
743
- if (!config.per_change.allow_skip) {
744
- throw new Error("Skipping stages is disabled in .sdd/config.yaml (per_change.allow_skip).");
745
- }
746
- if (config.per_change.require_reason_on_skip && !reason.trim()) {
747
- throw new Error("A reason is required to skip a stage.");
748
- }
749
- const ctx = await buildContext(projectRoot, config, changeId);
750
- const stage = getStage(ctx.workflow, ctx.meta, stageId);
751
- if (!stage) {
752
- throw new Error(`Stage "${stageId}" not found in workflow ${ctx.meta.workflow}`);
753
- }
754
- if (!stage.skippable) {
755
- throw new Error(`Stage "${stageId}" is not skippable.`);
756
- }
757
- const skip_stages = new Set(ctx.meta.overrides.skip_stages ?? []);
758
- skip_stages.add(stageId);
759
- ctx.meta.overrides.skip_stages = [...skip_stages];
760
- ctx.meta.skipped = [
761
- ...ctx.meta.skipped ?? [],
762
- { stage: stageId, reason, at: nowIso() }
763
- ];
764
- if (ctx.meta.stage === stageId) {
765
- const next = nextStageId(ctx.workflow, ctx.meta);
766
- if (next) {
767
- ctx.meta.stage = next;
768
- const nextStage = getStage(ctx.workflow, ctx.meta, next);
769
- if (nextStage) {
770
- await materializeStageArtifacts(
771
- projectRoot,
772
- config,
773
- changeId,
774
- ctx.workflow,
775
- ctx.meta,
776
- nextStage
777
- );
778
- }
779
- }
780
- }
781
- await saveChangeMeta(projectRoot, config, ctx.meta);
782
- return buildContext(projectRoot, config, changeId);
783
- }
784
- async function switchWorkflow(projectRoot, config, changeId, workflowName, reason) {
785
- const ctx = await buildContext(projectRoot, config, changeId);
786
- const workflow = await loadWorkflow(projectRoot, workflowName);
787
- if (config.allowed_workflows?.length && !config.allowed_workflows.includes(workflowName)) {
788
- throw new Error(
789
- `Workflow "${workflowName}" is not in allowed_workflows: ${config.allowed_workflows.join(", ")}`
790
- );
791
- }
792
- ctx.meta.workflow = workflowName;
793
- const hasStage = workflow.stages.some((s) => s.id === ctx.meta.stage);
794
- if (!hasStage) {
795
- ctx.meta.stage = workflow.stages[0].id;
796
- }
797
- if (reason) {
798
- ctx.meta.flags = {
799
- ...ctx.meta.flags,
800
- workflow_switch_reason: reason
801
- };
802
- }
803
- await saveChangeMeta(projectRoot, config, ctx.meta);
804
- const stage = getStage(workflow, ctx.meta, ctx.meta.stage);
805
- if (stage) {
806
- await materializeStageArtifacts(projectRoot, config, changeId, workflow, ctx.meta, stage);
807
- }
808
- return buildContext(projectRoot, config, changeId);
809
- }
810
- async function approveGate(projectRoot, config, changeId, stageId, note, status = "approved") {
811
- const ctx = await buildContext(projectRoot, config, changeId);
812
- const id = stageId ?? ctx.meta.stage;
813
- ctx.meta.gates = {
814
- ...ctx.meta.gates,
815
- [id]: { status, note, at: nowIso() }
816
- };
817
- await saveChangeMeta(projectRoot, config, ctx.meta);
818
- return buildContext(projectRoot, config, changeId);
819
- }
820
- async function completeChange(projectRoot, config, changeId) {
821
- const ctx = await buildContext(projectRoot, config, changeId);
822
- const next = nextStageId(ctx.workflow, ctx.meta);
823
- if (next) {
824
- const active = activeStages(ctx.workflow, ctx.meta);
825
- const last = active[active.length - 1];
826
- if (last && ctx.meta.stage !== last.id) {
827
- throw new Error(
828
- `Cannot complete: still on stage "${ctx.meta.stage}". Finish remaining stages or skip them. Next: ${next}`
829
- );
830
- }
831
- }
832
- const check = await canLeaveStage(ctx, config);
833
- if (!check.ok) {
834
- throw new Error(check.errors.join("\n"));
835
- }
836
- ctx.meta.status = "completed";
837
- ctx.meta.completed_at = nowIso();
838
- await saveChangeMeta(projectRoot, config, ctx.meta);
839
- await clearActiveIf(projectRoot, config, changeId);
840
- let archivedTo = null;
841
- const shouldArchive = config.persistence.archive_on_complete && ctx.workflow.on_complete?.archive !== false;
842
- if (shouldArchive) {
843
- const dest = join3(archiveDir(projectRoot, config), changeId);
844
- if (await pathExists(dest)) {
845
- throw new Error(`Archive already exists for change: ${changeId}`);
846
- }
847
- await ensureDir(archiveDir(projectRoot, config));
848
- await copyDir(ctx.path, dest);
849
- await removePath(ctx.path);
850
- archivedTo = dest;
851
- }
852
- if (archivedTo) {
853
- const meta = ChangeMetaSchema.parse(await readYaml(join3(archivedTo, "meta.yaml")));
854
- return {
855
- archivedTo,
856
- ctx: {
857
- id: changeId,
858
- path: archivedTo,
859
- meta,
860
- workflow: ctx.workflow,
861
- stages: ctx.stages,
862
- active: ctx.active
863
- }
864
- };
865
- }
866
- return { archivedTo, ctx: await buildContext(projectRoot, config, changeId) };
867
- }
868
- function formatStatus(ctx) {
869
- const lines = [];
870
- lines.push(`Change: ${ctx.meta.title}`);
871
- lines.push(`ID: ${ctx.id}`);
872
- lines.push(`Status: ${ctx.meta.status}`);
873
- lines.push(`Workflow: ${ctx.meta.workflow}`);
874
- lines.push(`Path: ${ctx.path}`);
875
- lines.push("");
876
- lines.push("Stages:");
877
- for (const stage of ctx.stages) {
878
- const skipped = isStageSkipped(ctx.meta, stage.id) || shouldAutoSkip(stage, ctx.meta);
879
- let mark = " ";
880
- if (skipped) mark = "\xB7";
881
- else if (stage.id === ctx.meta.stage) mark = "\u25CF";
882
- else {
883
- const order = ctx.active.map((s) => s.id);
884
- const cur = order.indexOf(ctx.meta.stage);
885
- const idx = order.indexOf(stage.id);
886
- if (idx >= 0 && cur >= 0 && idx < cur) mark = "\u2713";
887
- }
888
- const gate = ctx.meta.gates?.[stage.id];
889
- const gateInfo = gate ? ` [${gate.status}]` : "";
890
- const skipInfo = skipped ? " (skipped)" : "";
891
- lines.push(` [${mark}] ${stage.id}${stage.title ? ` \u2014 ${stage.title}` : ""}${gateInfo}${skipInfo}`);
892
- }
893
- if (ctx.meta.overrides?.skip_stages?.length) {
894
- lines.push("");
895
- lines.push(`Overrides: skip=${ctx.meta.overrides.skip_stages.join(", ")}`);
896
- }
897
- return lines.join("\n");
898
- }
899
- async function buildAgentPrompt(ctx, config, projectRoot) {
900
- const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
901
- const parts = [];
902
- parts.push(`# SDD Agent Handoff`);
903
- parts.push("");
904
- parts.push(`You are implementing a change under Structured Vibe Coding (local SDD).`);
905
- parts.push("");
906
- parts.push(`## Change`);
907
- parts.push(`- Title: ${ctx.meta.title}`);
908
- parts.push(`- ID: ${ctx.id}`);
909
- parts.push(`- Workflow: ${ctx.meta.workflow}`);
910
- parts.push(`- Current stage: ${ctx.meta.stage}${stage?.title ? ` (${stage.title})` : ""}`);
911
- parts.push(`- Change path: ${ctx.path}`);
912
- parts.push("");
913
- if (stage?.summary) {
914
- parts.push(`## Stage goal`);
915
- parts.push(stage.summary);
916
- parts.push("");
917
- }
918
- if (stage?.agent_context?.instructions) {
919
- parts.push(`## Instructions`);
920
- parts.push(stage.agent_context.instructions);
921
- parts.push("");
922
- }
923
- parts.push(`## Constraints`);
924
- parts.push(`- Follow artifacts in the change directory as source of truth.`);
925
- parts.push(`- Do not skip ARB/decision constraints if present.`);
926
- parts.push(`- Local development only \u2014 verify on this machine.`);
927
- parts.push(`- Prefer small, reviewable commits.`);
928
- parts.push("");
929
- parts.push(`## Artifacts to read`);
930
- const memoryHint = join3(projectRoot, config.memory_path);
931
- parts.push(`- Memory: ${memoryHint}/`);
932
- for (const s of ctx.stages) {
933
- for (const a of s.artifacts) {
934
- parts.push(`- ${join3(ctx.path, a.path)}`);
935
- }
936
- }
937
- parts.push("");
938
- parts.push(`## Current stage artifacts`);
939
- if (stage) {
940
- for (const a of stage.artifacts) {
941
- parts.push(`- ${a.path}${a.required ? " (required)" : ""}`);
942
- }
943
- }
944
- parts.push("");
945
- parts.push(`When done with this stage, the human will run \`sdd next\` or \`sdd verify\` / \`sdd complete\`.`);
946
- return parts.join("\n");
947
- }
1
+ import {
2
+ ALL_AGENT_TARGETS,
3
+ ArtifactDefSchema,
4
+ ChangeMetaSchema,
5
+ ConfigSchema,
6
+ GateSchema,
7
+ SDD_DIR,
8
+ StageSchema,
9
+ VerifyCommandSchema,
10
+ WorkflowSchema,
11
+ activePointerPath,
12
+ activeStages,
13
+ advanceStage,
14
+ approveGate,
15
+ archiveDir,
16
+ buildAgentPrompt,
17
+ buildContext,
18
+ canLeaveStage,
19
+ changeDirName,
20
+ changeMetaPath,
21
+ changePath,
22
+ changesDir,
23
+ clearActiveIf,
24
+ completeChange,
25
+ configPath,
26
+ copyDir,
27
+ createChange,
28
+ defaultConfig,
29
+ domainsDir,
30
+ ensureDir,
31
+ firstActiveStageId,
32
+ formatStatus,
33
+ getActiveChangeId,
34
+ getStage,
35
+ inferComplexity,
36
+ installAgentIntegrations,
37
+ isInitialized,
38
+ isStageSkipped,
39
+ listChanges,
40
+ listDirs,
41
+ listFiles,
42
+ listWorkflowNames,
43
+ loadChangeMeta,
44
+ loadConfig,
45
+ loadWorkflow,
46
+ memoryDir,
47
+ nextStageId,
48
+ nowIso,
49
+ pathExists,
50
+ previousStageId,
51
+ projectPaths,
52
+ readText,
53
+ readYaml,
54
+ recommendWorkflow,
55
+ refreshActiveAgentContext,
56
+ removePath,
57
+ resolveChangeId,
58
+ resolveStages,
59
+ saveChangeMeta,
60
+ saveConfig,
61
+ sddRoot,
62
+ setActiveChange,
63
+ shouldAutoSkip,
64
+ skipStage,
65
+ slugify,
66
+ switchWorkflow,
67
+ templatesDir,
68
+ todayIsoDate,
69
+ uniqueChangeDirName,
70
+ workflowsDir,
71
+ writeText,
72
+ writeYaml
73
+ } from "./chunk-PT2BZ23N.js";
948
74
 
949
75
  // src/init.ts
950
- import { join as join5 } from "pathe";
76
+ import { join as join2 } from "pathe";
951
77
 
952
78
  // src/defaults.ts
953
79
  import { existsSync } from "fs";
954
80
  import { fileURLToPath } from "url";
955
- import { dirname as dirname2, join as join4 } from "pathe";
81
+ import { dirname, join } from "pathe";
956
82
  function defaultsRoot() {
957
- const here = dirname2(fileURLToPath(import.meta.url));
83
+ const here = dirname(fileURLToPath(import.meta.url));
958
84
  const candidates = [
959
- join4(here, "..", "defaults"),
85
+ join(here, "..", "defaults"),
960
86
  // npm package: dist/../defaults
961
- join4(here, "defaults"),
87
+ join(here, "defaults"),
962
88
  // extension bundle: dist/defaults
963
- join4(here, "..", "..", "defaults")
89
+ join(here, "..", "..", "defaults")
964
90
  // src/ during some test runners
965
91
  ];
966
92
  for (const c of candidates) {
@@ -969,13 +95,13 @@ function defaultsRoot() {
969
95
  return candidates[0];
970
96
  }
971
97
  function defaultWorkflowsDir() {
972
- return join4(defaultsRoot(), "workflows");
98
+ return join(defaultsRoot(), "workflows");
973
99
  }
974
100
  function defaultTemplatesDir() {
975
- return join4(defaultsRoot(), "templates");
101
+ return join(defaultsRoot(), "templates");
976
102
  }
977
103
  function defaultMemoryDir() {
978
- return join4(defaultsRoot(), "memory");
104
+ return join(defaultsRoot(), "memory");
979
105
  }
980
106
 
981
107
  // src/init.ts
@@ -983,15 +109,15 @@ async function initProject(opts) {
983
109
  const { projectRoot, force = false } = opts;
984
110
  const created = [];
985
111
  const root = sddRoot(projectRoot);
986
- if (await pathExists(join5(root, "config.yaml")) && !force) {
112
+ if (await pathExists(join2(root, "config.yaml")) && !force) {
987
113
  throw new Error(
988
- `Already initialized (${join5(root, "config.yaml")}). Use --force to re-copy defaults.`
114
+ `Already initialized (${join2(root, "config.yaml")}). Use --force to re-copy defaults.`
989
115
  );
990
116
  }
991
117
  const config = defaultConfig();
992
118
  await ensureDir(root);
993
119
  await saveConfig(projectRoot, config);
994
- created.push(join5(root, "config.yaml"));
120
+ created.push(join2(root, "config.yaml"));
995
121
  const wfDest = workflowsDir(projectRoot);
996
122
  await ensureDir(wfDest);
997
123
  const wfSrc = defaultWorkflowsDir();
@@ -1013,15 +139,15 @@ async function initProject(opts) {
1013
139
  await copyDir(mSrc, mDest);
1014
140
  } else {
1015
141
  await writeText(
1016
- join5(mDest, "product.md"),
142
+ join2(mDest, "product.md"),
1017
143
  "# Product\n\n<!-- What are you building? -->\n"
1018
144
  );
1019
145
  await writeText(
1020
- join5(mDest, "architecture.md"),
146
+ join2(mDest, "architecture.md"),
1021
147
  "# Architecture\n\n<!-- High-level system shape -->\n"
1022
148
  );
1023
149
  await writeText(
1024
- join5(mDest, "conventions.md"),
150
+ join2(mDest, "conventions.md"),
1025
151
  "# Conventions\n\n<!-- Coding and process conventions -->\n"
1026
152
  );
1027
153
  }
@@ -1032,10 +158,10 @@ async function initProject(opts) {
1032
158
  created.push(changesDir(projectRoot, config));
1033
159
  created.push(archiveDir(projectRoot, config));
1034
160
  created.push(domainsDir(projectRoot, config));
1035
- await writeText(join5(changesDir(projectRoot, config), ".gitkeep"), "");
1036
- await writeText(join5(archiveDir(projectRoot, config), ".gitkeep"), "");
1037
- await writeText(join5(domainsDir(projectRoot, config), ".gitkeep"), "");
1038
- const sddReadme = join5(root, "README.md");
161
+ await writeText(join2(changesDir(projectRoot, config), ".gitkeep"), "");
162
+ await writeText(join2(archiveDir(projectRoot, config), ".gitkeep"), "");
163
+ await writeText(join2(domainsDir(projectRoot, config), ".gitkeep"), "");
164
+ const sddReadme = join2(root, "README.md");
1039
165
  if (!await pathExists(sddReadme) || force) {
1040
166
  await writeText(
1041
167
  sddReadme,
@@ -1062,12 +188,23 @@ sdd complete
1062
188
  );
1063
189
  created.push(sddReadme);
1064
190
  }
1065
- return { created, config };
191
+ let agentsResult;
192
+ if (opts.agents !== false) {
193
+ const targets = Array.isArray(opts.agents) && opts.agents.length ? opts.agents : void 0;
194
+ const ag = await installAgentIntegrations({
195
+ projectRoot,
196
+ targets,
197
+ force
198
+ });
199
+ created.push(...ag.created);
200
+ agentsResult = { created: ag.created, skipped: ag.skipped };
201
+ }
202
+ return { created, config, agents: agentsResult };
1066
203
  }
1067
204
 
1068
205
  // src/verify.ts
1069
206
  import { spawn } from "child_process";
1070
- import { join as join6 } from "pathe";
207
+ import { join as join3 } from "pathe";
1071
208
  function runCommand(command, cwd) {
1072
209
  return new Promise((resolve) => {
1073
210
  const child = spawn(command, {
@@ -1096,7 +233,7 @@ async function runLocalVerify(projectRoot, config, changeId, opts) {
1096
233
  if (!stage) {
1097
234
  throw new Error(`Unknown stage: ${ctx.meta.stage}`);
1098
235
  }
1099
- const evidenceDir = stage.verify?.evidence_dir != null ? join6(ctx.path, stage.verify.evidence_dir) : join6(ctx.path, "evidence", "local");
236
+ const evidenceDir = stage.verify?.evidence_dir != null ? join3(ctx.path, stage.verify.evidence_dir) : join3(ctx.path, "evidence", "local");
1100
237
  await ensureDir(evidenceDir);
1101
238
  const results = [];
1102
239
  const runCommands = opts?.runCommands !== false;
@@ -1110,7 +247,7 @@ async function runLocalVerify(projectRoot, config, changeId, opts) {
1110
247
  exitCode,
1111
248
  output: output.slice(0, 5e4)
1112
249
  });
1113
- const logPath = join6(evidenceDir, `${cmd.name.replace(/\s+/g, "-")}.log`);
250
+ const logPath = join3(evidenceDir, `${cmd.name.replace(/\s+/g, "-")}.log`);
1114
251
  await writeText(
1115
252
  logPath,
1116
253
  `# ${cmd.name}
@@ -1136,7 +273,7 @@ ${output}`
1136
273
  ok = results.every((r) => r.exitCode === 0);
1137
274
  }
1138
275
  const checklist = stage.gate?.checklist ?? [];
1139
- const summaryPath = join6(ctx.path, "local-test-results.md");
276
+ const summaryPath = join3(ctx.path, "local-test-results.md");
1140
277
  const lines = [
1141
278
  `# Local verify results`,
1142
279
  ``,
@@ -1172,7 +309,7 @@ ${output}`
1172
309
  if (await pathExists(summaryPath)) {
1173
310
  const stamp = nowIso();
1174
311
  await writeText(
1175
- join6(evidenceDir, `run-${stamp.replace(/[:.]/g, "-")}.md`),
312
+ join3(evidenceDir, `run-${stamp.replace(/[:.]/g, "-")}.md`),
1176
313
  lines.join("\n")
1177
314
  );
1178
315
  } else {
@@ -1196,6 +333,7 @@ ${output}`
1196
333
  };
1197
334
  }
1198
335
  export {
336
+ ALL_AGENT_TARGETS,
1199
337
  ArtifactDefSchema,
1200
338
  ChangeMetaSchema,
1201
339
  ConfigSchema,
@@ -1234,6 +372,7 @@ export {
1234
372
  getStage,
1235
373
  inferComplexity,
1236
374
  initProject,
375
+ installAgentIntegrations,
1237
376
  isInitialized,
1238
377
  isStageSkipped,
1239
378
  listChanges,
@@ -1252,6 +391,7 @@ export {
1252
391
  readText,
1253
392
  readYaml,
1254
393
  recommendWorkflow,
394
+ refreshActiveAgentContext,
1255
395
  removePath,
1256
396
  resolveChangeId,
1257
397
  resolveStages,