@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.
@@ -0,0 +1,1314 @@
1
+ // src/agents.ts
2
+ import { join as join4 } from "pathe";
3
+
4
+ // src/fs.ts
5
+ import { promises as fs } from "fs";
6
+ import { dirname } from "pathe";
7
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
8
+ async function pathExists(path) {
9
+ try {
10
+ await fs.access(path);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+ async function ensureDir(path) {
17
+ await fs.mkdir(path, { recursive: true });
18
+ }
19
+ async function readText(path) {
20
+ return fs.readFile(path, "utf8");
21
+ }
22
+ async function writeText(path, content) {
23
+ await ensureDir(dirname(path));
24
+ await fs.writeFile(path, content, "utf8");
25
+ }
26
+ async function readYaml(path) {
27
+ const raw = await readText(path);
28
+ return parseYaml(raw);
29
+ }
30
+ async function writeYaml(path, data) {
31
+ const content = stringifyYaml(data, {
32
+ lineWidth: 100,
33
+ defaultStringType: "PLAIN",
34
+ defaultKeyType: "PLAIN"
35
+ });
36
+ await writeText(path, content.endsWith("\n") ? content : `${content}
37
+ `);
38
+ }
39
+ async function listDirs(path) {
40
+ if (!await pathExists(path)) return [];
41
+ const entries = await fs.readdir(path, { withFileTypes: true });
42
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
43
+ }
44
+ async function listFiles(path) {
45
+ if (!await pathExists(path)) return [];
46
+ const entries = await fs.readdir(path, { withFileTypes: true });
47
+ return entries.filter((e) => e.isFile()).map((e) => e.name);
48
+ }
49
+ async function copyDir(src, dest) {
50
+ await ensureDir(dest);
51
+ const entries = await fs.readdir(src, { withFileTypes: true });
52
+ for (const entry of entries) {
53
+ const from = `${src}/${entry.name}`;
54
+ const to = `${dest}/${entry.name}`;
55
+ if (entry.isDirectory()) {
56
+ await copyDir(from, to);
57
+ } else {
58
+ await ensureDir(dirname(to));
59
+ await fs.copyFile(from, to);
60
+ }
61
+ }
62
+ }
63
+ async function removePath(path) {
64
+ await fs.rm(path, { recursive: true, force: true });
65
+ }
66
+
67
+ // src/schemas.ts
68
+ import { z } from "zod";
69
+ var GateSchema = z.object({
70
+ type: z.enum(["none", "soft", "hard"]).default("soft"),
71
+ mode: z.enum(["none", "human_approval", "checklist"]).default("checklist"),
72
+ checklist: z.array(z.string()).default([]),
73
+ overridable: z.boolean().default(true)
74
+ });
75
+ var ArtifactDefSchema = z.object({
76
+ id: z.string(),
77
+ path: z.string(),
78
+ template: z.string().optional(),
79
+ required: z.boolean().default(true)
80
+ });
81
+ var VerifyCommandSchema = z.object({
82
+ name: z.string(),
83
+ run: z.string(),
84
+ required: z.boolean().default(false)
85
+ });
86
+ var StageSchema = z.object({
87
+ id: z.string(),
88
+ title: z.string().optional(),
89
+ summary: z.string().optional(),
90
+ artifacts: z.array(ArtifactDefSchema).default([]),
91
+ gate: GateSchema.default({}),
92
+ skippable: z.boolean().default(true),
93
+ skip_when: z.object({
94
+ flags: z.array(z.string()).optional()
95
+ }).optional(),
96
+ verify: z.object({
97
+ commands: z.array(VerifyCommandSchema).default([]),
98
+ evidence_dir: z.string().optional()
99
+ }).optional(),
100
+ agent_context: z.object({
101
+ include: z.array(z.string()).default([]),
102
+ instructions: z.string().optional()
103
+ }).optional()
104
+ });
105
+ var WorkflowSchema = z.object({
106
+ name: z.string(),
107
+ description: z.string().optional(),
108
+ version: z.number().default(1),
109
+ stages: z.array(StageSchema).min(1),
110
+ recommendation: z.object({
111
+ when: z.object({
112
+ labels: z.array(z.string()).optional(),
113
+ keywords: z.array(z.string()).optional(),
114
+ max_complexity: z.enum(["low", "medium", "high"]).optional(),
115
+ min_complexity: z.enum(["low", "medium", "high"]).optional()
116
+ }).optional(),
117
+ priority: z.number().default(0)
118
+ }).optional(),
119
+ on_complete: z.object({
120
+ archive: z.boolean().default(true),
121
+ domain_sync: z.enum(["never", "recommend", "require"]).default("never")
122
+ }).optional()
123
+ });
124
+ var ConfigSchema = z.object({
125
+ version: z.number().default(1),
126
+ memory_path: z.string().default("memory"),
127
+ changes_path: z.string().default("changes"),
128
+ archive_path: z.string().default("archive"),
129
+ domains_path: z.string().default("domains"),
130
+ default_workflow: z.string().default("recommend"),
131
+ allowed_workflows: z.array(z.string()).optional(),
132
+ per_change: z.object({
133
+ allow_skip: z.boolean().default(true),
134
+ allow_gate_override: z.enum(["true", "false", "soft_only"]).default("soft_only"),
135
+ allow_custom_stages: z.boolean().default(true),
136
+ require_reason_on_skip: z.boolean().default(true)
137
+ }).default({}),
138
+ persistence: z.object({
139
+ default: z.enum(["change_only", "domain_sync"]).default("change_only"),
140
+ archive_on_complete: z.boolean().default(true),
141
+ domain_sync: z.object({
142
+ mode: z.enum(["never", "recommend", "require"]).default("never"),
143
+ anchored_domains: z.array(z.string()).default([])
144
+ }).default({})
145
+ }).default({}),
146
+ policy: z.object({
147
+ gates: z.enum(["soft", "hard"]).default("soft")
148
+ }).default({})
149
+ });
150
+ var ChangeMetaSchema = z.object({
151
+ id: z.string(),
152
+ title: z.string(),
153
+ workflow: z.string(),
154
+ created: z.string(),
155
+ updated: z.string().optional(),
156
+ status: z.enum(["in_progress", "blocked", "completed", "abandoned"]).default("in_progress"),
157
+ stage: z.string(),
158
+ branch: z.string().optional(),
159
+ domain: z.string().optional(),
160
+ flags: z.record(z.union([z.boolean(), z.string(), z.number()])).default({}),
161
+ overrides: z.object({
162
+ skip_stages: z.array(z.string()).default([]),
163
+ gates: z.record(z.enum(["none", "soft", "hard"])).default({}),
164
+ extra_stages: z.array(StageSchema).default([])
165
+ }).default({}),
166
+ skipped: z.array(
167
+ z.object({
168
+ stage: z.string(),
169
+ reason: z.string(),
170
+ at: z.string()
171
+ })
172
+ ).default([]),
173
+ gates: z.record(
174
+ z.object({
175
+ status: z.enum(["pending", "approved", "failed", "waived"]),
176
+ note: z.string().optional(),
177
+ at: z.string().optional()
178
+ })
179
+ ).default({}),
180
+ /** Results from `sdd verify` keyed by stage id */
181
+ verify_results: z.record(
182
+ z.object({
183
+ ok: z.boolean(),
184
+ at: z.string(),
185
+ results: z.array(
186
+ z.object({
187
+ name: z.string(),
188
+ exitCode: z.number().nullable()
189
+ })
190
+ ).default([])
191
+ })
192
+ ).default({}),
193
+ pr: z.object({
194
+ number: z.number().optional(),
195
+ url: z.string().optional()
196
+ }).optional(),
197
+ completed_at: z.string().optional()
198
+ });
199
+
200
+ // src/paths.ts
201
+ import { join } from "pathe";
202
+ var SDD_DIR = ".sdd";
203
+ function sddRoot(projectRoot) {
204
+ return join(projectRoot, SDD_DIR);
205
+ }
206
+ function configPath(projectRoot) {
207
+ return join(sddRoot(projectRoot), "config.yaml");
208
+ }
209
+ function workflowsDir(projectRoot) {
210
+ return join(sddRoot(projectRoot), "workflows");
211
+ }
212
+ function templatesDir(projectRoot) {
213
+ return join(sddRoot(projectRoot), "templates");
214
+ }
215
+ function memoryDir(projectRoot, config) {
216
+ return join(projectRoot, config.memory_path);
217
+ }
218
+ function changesDir(projectRoot, config) {
219
+ return join(projectRoot, config.changes_path);
220
+ }
221
+ function archiveDir(projectRoot, config) {
222
+ return join(projectRoot, config.archive_path);
223
+ }
224
+ function domainsDir(projectRoot, config) {
225
+ return join(projectRoot, config.domains_path);
226
+ }
227
+ function changePath(projectRoot, config, changeId) {
228
+ return join(changesDir(projectRoot, config), changeId);
229
+ }
230
+ function changeMetaPath(projectRoot, config, changeId) {
231
+ return join(changePath(projectRoot, config, changeId), "meta.yaml");
232
+ }
233
+ function activePointerPath(projectRoot, config) {
234
+ return join(changesDir(projectRoot, config), ".active");
235
+ }
236
+
237
+ // src/config.ts
238
+ async function isInitialized(projectRoot) {
239
+ return pathExists(configPath(projectRoot));
240
+ }
241
+ async function loadConfig(projectRoot) {
242
+ const path = configPath(projectRoot);
243
+ if (!await pathExists(path)) {
244
+ throw new Error(
245
+ `SDD is not initialized in ${projectRoot}. Run \`sdd init\` first.`
246
+ );
247
+ }
248
+ const raw = await readYaml(path);
249
+ return ConfigSchema.parse(raw);
250
+ }
251
+ async function saveConfig(projectRoot, config) {
252
+ await ensureDir(sddRoot(projectRoot));
253
+ await writeYaml(configPath(projectRoot), ConfigSchema.parse(config));
254
+ }
255
+ function defaultConfig() {
256
+ return ConfigSchema.parse({});
257
+ }
258
+ function projectPaths(projectRoot, config) {
259
+ return {
260
+ sdd: sddRoot(projectRoot),
261
+ config: configPath(projectRoot),
262
+ workflows: workflowsDir(projectRoot),
263
+ templates: templatesDir(projectRoot),
264
+ memory: memoryDir(projectRoot, config)
265
+ };
266
+ }
267
+
268
+ // src/change.ts
269
+ import { join as join3 } from "pathe";
270
+
271
+ // src/slug.ts
272
+ function slugify(input) {
273
+ return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
274
+ }
275
+ function todayIsoDate(d = /* @__PURE__ */ new Date()) {
276
+ return d.toISOString().slice(0, 10);
277
+ }
278
+ function nowIso(d = /* @__PURE__ */ new Date()) {
279
+ return d.toISOString();
280
+ }
281
+ function changeDirName(title, d = /* @__PURE__ */ new Date()) {
282
+ const slug = slugify(title) || "change";
283
+ return `${todayIsoDate(d)}-${slug}`;
284
+ }
285
+ function uniqueChangeDirName(base, taken) {
286
+ if (!taken.has(base)) return base;
287
+ let n = 2;
288
+ while (taken.has(`${base}-${n}`)) n += 1;
289
+ return `${base}-${n}`;
290
+ }
291
+
292
+ // src/workflow.ts
293
+ import { join as join2 } from "pathe";
294
+ async function listWorkflowNames(projectRoot) {
295
+ const dir = workflowsDir(projectRoot);
296
+ const files = await listFiles(dir);
297
+ return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => f.replace(/\.ya?ml$/, "")).sort();
298
+ }
299
+ async function loadWorkflow(projectRoot, name) {
300
+ const dir = workflowsDir(projectRoot);
301
+ const candidates = [`${name}.yaml`, `${name}.yml`];
302
+ for (const file of candidates) {
303
+ const path = join2(dir, file);
304
+ if (await pathExists(path)) {
305
+ const raw = await readYaml(path);
306
+ const workflow = WorkflowSchema.parse(raw);
307
+ if (workflow.name !== name) {
308
+ }
309
+ return workflow;
310
+ }
311
+ }
312
+ throw new Error(
313
+ `Workflow "${name}" not found in ${dir}. Available: ${(await listWorkflowNames(projectRoot)).join(", ") || "(none)"}`
314
+ );
315
+ }
316
+ function resolveStages(workflow, meta) {
317
+ const base = [...workflow.stages];
318
+ const extra = meta.overrides?.extra_stages ?? [];
319
+ const stages = [...base, ...extra];
320
+ return stages.map((stage) => {
321
+ const gateOverride = meta.overrides?.gates?.[stage.id];
322
+ if (!gateOverride) return stage;
323
+ return {
324
+ ...stage,
325
+ gate: {
326
+ ...stage.gate,
327
+ type: gateOverride
328
+ }
329
+ };
330
+ });
331
+ }
332
+ function isStageSkipped(meta, stageId) {
333
+ if (meta.overrides?.skip_stages?.includes(stageId)) return true;
334
+ if (meta.skipped?.some((s) => s.stage === stageId)) return true;
335
+ return false;
336
+ }
337
+ function shouldAutoSkip(stage, meta) {
338
+ const flags = stage.skip_when?.flags ?? [];
339
+ for (const flag of flags) {
340
+ if (meta.flags?.[flag] === true || meta.flags?.[flag] === "true") {
341
+ return true;
342
+ }
343
+ }
344
+ return false;
345
+ }
346
+ function activeStages(workflow, meta) {
347
+ return resolveStages(workflow, meta).filter((s) => {
348
+ if (isStageSkipped(meta, s.id)) return false;
349
+ if (shouldAutoSkip(s, meta)) return false;
350
+ return true;
351
+ });
352
+ }
353
+ function getStage(workflow, meta, stageId) {
354
+ return resolveStages(workflow, meta).find((s) => s.id === stageId);
355
+ }
356
+ function nextStageId(workflow, meta) {
357
+ const all = resolveStages(workflow, meta);
358
+ const activeIds = new Set(activeStages(workflow, meta).map((s) => s.id));
359
+ let start = all.findIndex((s) => s.id === meta.stage);
360
+ if (start === -1) start = -1;
361
+ for (let i = start + 1; i < all.length; i++) {
362
+ const id = all[i].id;
363
+ if (activeIds.has(id)) return id;
364
+ }
365
+ return null;
366
+ }
367
+ function previousStageId(workflow, meta) {
368
+ const all = resolveStages(workflow, meta);
369
+ const activeIds = new Set(activeStages(workflow, meta).map((s) => s.id));
370
+ const start = all.findIndex((s) => s.id === meta.stage);
371
+ if (start <= 0) return null;
372
+ for (let i = start - 1; i >= 0; i--) {
373
+ const id = all[i].id;
374
+ if (activeIds.has(id)) return id;
375
+ }
376
+ return null;
377
+ }
378
+ function firstActiveStageId(workflow, meta) {
379
+ return activeStages(workflow, meta)[0]?.id ?? null;
380
+ }
381
+ function inferComplexity(title, hints) {
382
+ const text = `${title} ${(hints?.keywords ?? []).join(" ")}`.toLowerCase();
383
+ const hotfixHints = ["typo", "hotfix", "nit", "rename", "bump", "chore", "readme", "docs"];
384
+ const highHints = [
385
+ "architecture",
386
+ "migration",
387
+ "auth",
388
+ "payment",
389
+ "billing",
390
+ "redesign",
391
+ "platform",
392
+ "arb",
393
+ "epic"
394
+ ];
395
+ if (hotfixHints.some((h) => text.includes(h))) return "low";
396
+ if (highHints.some((h) => text.includes(h))) return "high";
397
+ if (text.length > 80) return "medium";
398
+ return "medium";
399
+ }
400
+ async function recommendWorkflow(projectRoot, title, config, opts) {
401
+ const available = await listWorkflowNames(projectRoot);
402
+ const allowed = config.allowed_workflows?.length ? available.filter((n) => config.allowed_workflows.includes(n)) : available;
403
+ if (opts?.preferred) {
404
+ if (!available.includes(opts.preferred)) {
405
+ throw new Error(
406
+ `Workflow "${opts.preferred}" is not available. Available: ${available.join(", ") || "(none)"}`
407
+ );
408
+ }
409
+ if (config.allowed_workflows?.length && !config.allowed_workflows.includes(opts.preferred)) {
410
+ throw new Error(
411
+ `Workflow "${opts.preferred}" is not in allowed_workflows: ${config.allowed_workflows.join(", ")}`
412
+ );
413
+ }
414
+ return {
415
+ name: opts.preferred,
416
+ reason: "Explicitly selected",
417
+ alternatives: allowed.filter((n) => n !== opts.preferred)
418
+ };
419
+ }
420
+ if (config.default_workflow !== "recommend" && allowed.includes(config.default_workflow)) {
421
+ return {
422
+ name: config.default_workflow,
423
+ reason: `Repo default_workflow=${config.default_workflow}`,
424
+ alternatives: allowed.filter((n) => n !== config.default_workflow)
425
+ };
426
+ }
427
+ const complexity = opts?.complexity ?? inferComplexity(title);
428
+ const scored = [];
429
+ for (const name of allowed) {
430
+ try {
431
+ const wf = await loadWorkflow(projectRoot, name);
432
+ let score = wf.recommendation?.priority ?? 0;
433
+ let reason = `workflow priority ${score}`;
434
+ const when = wf.recommendation?.when;
435
+ if (complexity === "low" && name === "hotfix") {
436
+ score += 50;
437
+ reason = "Low complexity \u2192 hotfix";
438
+ } else if (complexity === "low" && name === "patch") {
439
+ score += 40;
440
+ reason = "Low complexity \u2192 patch";
441
+ } else if (complexity === "medium" && name === "feature") {
442
+ score += 45;
443
+ reason = "Medium complexity \u2192 feature";
444
+ } else if (complexity === "medium" && name === "patch") {
445
+ score += 30;
446
+ reason = "Medium complexity \u2192 patch";
447
+ } else if (complexity === "high" && name === "enterprise-feature") {
448
+ score += 50;
449
+ reason = "High complexity \u2192 enterprise-feature";
450
+ } else if (complexity === "high" && name === "feature") {
451
+ score += 35;
452
+ reason = "High complexity \u2192 feature";
453
+ }
454
+ const keywords = when?.keywords ?? [];
455
+ const titleLower = title.toLowerCase();
456
+ if (keywords.some((k) => titleLower.includes(k.toLowerCase()))) {
457
+ score += 20;
458
+ reason = `Keyword match for ${name}`;
459
+ }
460
+ scored.push({ name, score, reason });
461
+ } catch {
462
+ }
463
+ }
464
+ scored.sort((a, b) => b.score - a.score);
465
+ const best = scored[0];
466
+ if (!best) {
467
+ throw new Error("No workflows available. Run `sdd init` or add files under .sdd/workflows/");
468
+ }
469
+ return {
470
+ name: best.name,
471
+ reason: best.reason,
472
+ alternatives: scored.slice(1).map((s) => s.name)
473
+ };
474
+ }
475
+
476
+ // src/change.ts
477
+ async function existingChangeIds(projectRoot, config) {
478
+ const open = await listDirs(changesDir(projectRoot, config));
479
+ const archived = await listDirs(archiveDir(projectRoot, config));
480
+ return /* @__PURE__ */ new Set([...open, ...archived]);
481
+ }
482
+ async function createChange(input) {
483
+ const { projectRoot, config, title, workflowName } = input;
484
+ const workflow = await loadWorkflow(projectRoot, workflowName);
485
+ if (!workflow.stages.length) {
486
+ throw new Error(`Workflow ${workflowName} has no stages`);
487
+ }
488
+ const baseId = changeDirName(title);
489
+ const id = uniqueChangeDirName(baseId, await existingChangeIds(projectRoot, config));
490
+ const dir = changePath(projectRoot, config, id);
491
+ await ensureDir(dir);
492
+ let meta = ChangeMetaSchema.parse({
493
+ id,
494
+ title,
495
+ workflow: workflowName,
496
+ created: nowIso(),
497
+ updated: nowIso(),
498
+ status: "in_progress",
499
+ stage: workflow.stages[0].id,
500
+ domain: input.domain,
501
+ branch: input.branch,
502
+ flags: input.flags ?? {},
503
+ overrides: {
504
+ skip_stages: [],
505
+ gates: {},
506
+ extra_stages: []
507
+ },
508
+ skipped: [],
509
+ gates: {}
510
+ });
511
+ const startId = firstActiveStageId(workflow, meta) ?? workflow.stages[0].id;
512
+ meta = { ...meta, stage: startId };
513
+ const firstStage = getStage(workflow, meta, startId);
514
+ if (!firstStage) {
515
+ throw new Error(`Workflow ${workflowName} has no active stages`);
516
+ }
517
+ await writeYaml(changeMetaPath(projectRoot, config, id), meta);
518
+ await materializeStageArtifacts(projectRoot, config, id, workflow, meta, firstStage);
519
+ await setActiveChange(projectRoot, config, id);
520
+ try {
521
+ const { refreshActiveAgentContext: refreshActiveAgentContext2 } = await import("./agents-7PTSPJFQ.js");
522
+ await refreshActiveAgentContext2(projectRoot);
523
+ } catch {
524
+ }
525
+ return buildContext(projectRoot, config, id);
526
+ }
527
+ async function materializeStageArtifacts(projectRoot, config, changeId, workflow, meta, stage) {
528
+ const created = [];
529
+ const dir = changePath(projectRoot, config, changeId);
530
+ const tdir = templatesDir(projectRoot);
531
+ for (const artifact of stage.artifacts) {
532
+ const target = join3(dir, artifact.path);
533
+ if (await pathExists(target)) continue;
534
+ if (artifact.path.endsWith("/")) {
535
+ await ensureDir(target);
536
+ created.push(artifact.path);
537
+ continue;
538
+ }
539
+ let content = defaultArtifactContent(artifact.id, meta, stage, workflow);
540
+ if (artifact.template) {
541
+ const templatePath = join3(tdir, artifact.template.replace(/^templates\//, ""));
542
+ const candidates = [
543
+ join3(tdir, artifact.template),
544
+ templatePath,
545
+ join3(tdir, `${artifact.id}.md`)
546
+ ];
547
+ for (const c of candidates) {
548
+ if (await pathExists(c)) {
549
+ const raw = await readText(c);
550
+ content = interpolate(raw, meta, stage, workflow);
551
+ break;
552
+ }
553
+ }
554
+ } else {
555
+ const fallback = join3(tdir, `${artifact.id}.md`);
556
+ if (await pathExists(fallback)) {
557
+ content = interpolate(await readText(fallback), meta, stage, workflow);
558
+ }
559
+ }
560
+ await writeText(target, content);
561
+ created.push(artifact.path);
562
+ }
563
+ return created;
564
+ }
565
+ function interpolate(template, meta, stage, workflow) {
566
+ 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));
567
+ }
568
+ function defaultArtifactContent(artifactId, meta, stage, workflow) {
569
+ return `# ${artifactId}
570
+
571
+ > Change: **${meta.title}** (\`${meta.id}\`)
572
+ > Workflow: \`${workflow.name}\` \xB7 Stage: \`${stage.id}\`
573
+
574
+ ## Summary
575
+
576
+ <!-- Write the intent / content for this artifact -->
577
+
578
+ ## Notes
579
+
580
+ -
581
+ `;
582
+ }
583
+ async function loadChangeMeta(projectRoot, config, changeId) {
584
+ const path = changeMetaPath(projectRoot, config, changeId);
585
+ if (!await pathExists(path)) {
586
+ throw new Error(`Change not found: ${changeId}`);
587
+ }
588
+ return ChangeMetaSchema.parse(await readYaml(path));
589
+ }
590
+ async function saveChangeMeta(projectRoot, config, meta) {
591
+ meta.updated = nowIso();
592
+ await writeYaml(changeMetaPath(projectRoot, config, meta.id), ChangeMetaSchema.parse(meta));
593
+ }
594
+ async function buildContext(projectRoot, config, changeId) {
595
+ const meta = await loadChangeMeta(projectRoot, config, changeId);
596
+ const workflow = await loadWorkflow(projectRoot, meta.workflow);
597
+ return {
598
+ id: changeId,
599
+ path: changePath(projectRoot, config, changeId),
600
+ meta,
601
+ workflow,
602
+ stages: resolveStages(workflow, meta),
603
+ active: activeStages(workflow, meta)
604
+ };
605
+ }
606
+ async function listChanges(projectRoot, config) {
607
+ return listDirs(changesDir(projectRoot, config));
608
+ }
609
+ async function getActiveChangeId(projectRoot, config) {
610
+ const pointer = activePointerPath(projectRoot, config);
611
+ if (await pathExists(pointer)) {
612
+ const id = (await readText(pointer)).trim();
613
+ if (id && await pathExists(changePath(projectRoot, config, id))) {
614
+ return id;
615
+ }
616
+ if (id) {
617
+ await writeText(pointer, "");
618
+ }
619
+ }
620
+ const all = await listChanges(projectRoot, config);
621
+ const inProgress = [];
622
+ for (const id of all) {
623
+ try {
624
+ const meta = await loadChangeMeta(projectRoot, config, id);
625
+ if (meta.status === "in_progress" || meta.status === "blocked") {
626
+ inProgress.push({ id, updated: meta.updated ?? meta.created });
627
+ }
628
+ } catch {
629
+ }
630
+ }
631
+ if (!inProgress.length) return null;
632
+ inProgress.sort((a, b) => b.updated.localeCompare(a.updated));
633
+ return inProgress[0].id;
634
+ }
635
+ async function clearActiveIf(projectRoot, config, changeId) {
636
+ const pointer = activePointerPath(projectRoot, config);
637
+ if (!await pathExists(pointer)) return;
638
+ const id = (await readText(pointer)).trim();
639
+ if (id === changeId) {
640
+ await writeText(pointer, "");
641
+ }
642
+ }
643
+ async function setActiveChange(projectRoot, config, changeId) {
644
+ await ensureDir(changesDir(projectRoot, config));
645
+ await writeText(activePointerPath(projectRoot, config), `${changeId}
646
+ `);
647
+ }
648
+ async function resolveChangeId(projectRoot, config, changeId) {
649
+ if (changeId) return changeId;
650
+ const active = await getActiveChangeId(projectRoot, config);
651
+ if (!active) {
652
+ throw new Error("No active change. Pass --change <id> or run `sdd new`.");
653
+ }
654
+ return active;
655
+ }
656
+ async function canLeaveStage(ctx, config) {
657
+ const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
658
+ const errors = [];
659
+ const warnings = [];
660
+ if (!stage) {
661
+ return { ok: false, errors: [`Unknown stage: ${ctx.meta.stage}`], warnings };
662
+ }
663
+ for (const artifact of stage.artifacts) {
664
+ if (!artifact.required) continue;
665
+ const target = join3(ctx.path, artifact.path);
666
+ if (!await pathExists(target)) {
667
+ errors.push(`Missing required artifact: ${artifact.path}`);
668
+ continue;
669
+ }
670
+ }
671
+ const declared = stage.gate?.type ?? "soft";
672
+ let effectiveGate = declared;
673
+ if (config.policy.gates === "hard" && declared === "soft") {
674
+ effectiveGate = "hard";
675
+ }
676
+ const gateState = ctx.meta.gates?.[stage.id];
677
+ if (effectiveGate === "hard") {
678
+ if (!gateState || gateState.status !== "approved" && gateState.status !== "waived") {
679
+ errors.push(
680
+ `Hard gate on stage "${stage.id}" is not approved. Run \`sdd gate approve\` or complete the checklist.`
681
+ );
682
+ }
683
+ } else if (effectiveGate === "soft" && stage.gate?.checklist?.length) {
684
+ if (!gateState || gateState.status === "pending") {
685
+ warnings.push(
686
+ `Soft gate checklist not signed off on "${stage.id}" (proceeding allowed).`
687
+ );
688
+ }
689
+ }
690
+ const requiredCmds = stage.verify?.commands?.filter((c) => c.required) ?? [];
691
+ if (requiredCmds.length) {
692
+ const verifyOk = ctx.meta.verify_results?.[stage.id]?.ok === true;
693
+ const humanOverride = gateState?.status === "waived" || gateState?.status === "approved";
694
+ if (!verifyOk && !humanOverride) {
695
+ errors.push(
696
+ `Required local verify commands have not passed for "${stage.id}". Run \`sdd verify\` (or \`sdd gate approve/waive\` with a note).`
697
+ );
698
+ } else if (!verifyOk && humanOverride) {
699
+ warnings.push(
700
+ `Stage "${stage.id}" gate signed off without a successful required verify run.`
701
+ );
702
+ }
703
+ }
704
+ return { ok: errors.length === 0, errors, warnings };
705
+ }
706
+ async function advanceStage(projectRoot, config, changeId, opts) {
707
+ const ctx = await buildContext(projectRoot, config, changeId);
708
+ const warnings = [];
709
+ if (ctx.meta.status === "completed") {
710
+ throw new Error("Change is already completed.");
711
+ }
712
+ const check = await canLeaveStage(ctx, config);
713
+ warnings.push(...check.warnings);
714
+ if (!check.ok && !opts?.force) {
715
+ throw new Error(check.errors.join("\n"));
716
+ }
717
+ const from = ctx.meta.stage;
718
+ const next = nextStageId(ctx.workflow, ctx.meta);
719
+ if (!next) {
720
+ return {
721
+ ctx,
722
+ from,
723
+ to: null,
724
+ completed: false,
725
+ artifactsCreated: [],
726
+ warnings: [...warnings, "No further stages. Run `sdd complete` when ready."]
727
+ };
728
+ }
729
+ ctx.meta.stage = next;
730
+ await saveChangeMeta(projectRoot, config, ctx.meta);
731
+ const stage = getStage(ctx.workflow, ctx.meta, next);
732
+ const artifactsCreated = await materializeStageArtifacts(
733
+ projectRoot,
734
+ config,
735
+ changeId,
736
+ ctx.workflow,
737
+ ctx.meta,
738
+ stage
739
+ );
740
+ try {
741
+ const { refreshActiveAgentContext: refreshActiveAgentContext2 } = await import("./agents-7PTSPJFQ.js");
742
+ await refreshActiveAgentContext2(projectRoot);
743
+ } catch {
744
+ }
745
+ const updated = await buildContext(projectRoot, config, changeId);
746
+ return {
747
+ ctx: updated,
748
+ from,
749
+ to: next,
750
+ completed: false,
751
+ artifactsCreated,
752
+ warnings
753
+ };
754
+ }
755
+ async function skipStage(projectRoot, config, changeId, stageId, reason) {
756
+ if (!config.per_change.allow_skip) {
757
+ throw new Error("Skipping stages is disabled in .sdd/config.yaml (per_change.allow_skip).");
758
+ }
759
+ if (config.per_change.require_reason_on_skip && !reason.trim()) {
760
+ throw new Error("A reason is required to skip a stage.");
761
+ }
762
+ const ctx = await buildContext(projectRoot, config, changeId);
763
+ const stage = getStage(ctx.workflow, ctx.meta, stageId);
764
+ if (!stage) {
765
+ throw new Error(`Stage "${stageId}" not found in workflow ${ctx.meta.workflow}`);
766
+ }
767
+ if (!stage.skippable) {
768
+ throw new Error(`Stage "${stageId}" is not skippable.`);
769
+ }
770
+ const skip_stages = new Set(ctx.meta.overrides.skip_stages ?? []);
771
+ skip_stages.add(stageId);
772
+ ctx.meta.overrides.skip_stages = [...skip_stages];
773
+ ctx.meta.skipped = [
774
+ ...ctx.meta.skipped ?? [],
775
+ { stage: stageId, reason, at: nowIso() }
776
+ ];
777
+ if (ctx.meta.stage === stageId) {
778
+ const next = nextStageId(ctx.workflow, ctx.meta);
779
+ if (next) {
780
+ ctx.meta.stage = next;
781
+ const nextStage = getStage(ctx.workflow, ctx.meta, next);
782
+ if (nextStage) {
783
+ await materializeStageArtifacts(
784
+ projectRoot,
785
+ config,
786
+ changeId,
787
+ ctx.workflow,
788
+ ctx.meta,
789
+ nextStage
790
+ );
791
+ }
792
+ }
793
+ }
794
+ await saveChangeMeta(projectRoot, config, ctx.meta);
795
+ return buildContext(projectRoot, config, changeId);
796
+ }
797
+ async function switchWorkflow(projectRoot, config, changeId, workflowName, reason) {
798
+ const ctx = await buildContext(projectRoot, config, changeId);
799
+ const workflow = await loadWorkflow(projectRoot, workflowName);
800
+ if (config.allowed_workflows?.length && !config.allowed_workflows.includes(workflowName)) {
801
+ throw new Error(
802
+ `Workflow "${workflowName}" is not in allowed_workflows: ${config.allowed_workflows.join(", ")}`
803
+ );
804
+ }
805
+ ctx.meta.workflow = workflowName;
806
+ const hasStage = workflow.stages.some((s) => s.id === ctx.meta.stage);
807
+ if (!hasStage) {
808
+ ctx.meta.stage = workflow.stages[0].id;
809
+ }
810
+ if (reason) {
811
+ ctx.meta.flags = {
812
+ ...ctx.meta.flags,
813
+ workflow_switch_reason: reason
814
+ };
815
+ }
816
+ await saveChangeMeta(projectRoot, config, ctx.meta);
817
+ const stage = getStage(workflow, ctx.meta, ctx.meta.stage);
818
+ if (stage) {
819
+ await materializeStageArtifacts(projectRoot, config, changeId, workflow, ctx.meta, stage);
820
+ }
821
+ return buildContext(projectRoot, config, changeId);
822
+ }
823
+ async function approveGate(projectRoot, config, changeId, stageId, note, status = "approved") {
824
+ const ctx = await buildContext(projectRoot, config, changeId);
825
+ const id = stageId ?? ctx.meta.stage;
826
+ ctx.meta.gates = {
827
+ ...ctx.meta.gates,
828
+ [id]: { status, note, at: nowIso() }
829
+ };
830
+ await saveChangeMeta(projectRoot, config, ctx.meta);
831
+ return buildContext(projectRoot, config, changeId);
832
+ }
833
+ async function completeChange(projectRoot, config, changeId) {
834
+ const ctx = await buildContext(projectRoot, config, changeId);
835
+ const next = nextStageId(ctx.workflow, ctx.meta);
836
+ if (next) {
837
+ const active = activeStages(ctx.workflow, ctx.meta);
838
+ const last = active[active.length - 1];
839
+ if (last && ctx.meta.stage !== last.id) {
840
+ throw new Error(
841
+ `Cannot complete: still on stage "${ctx.meta.stage}". Finish remaining stages or skip them. Next: ${next}`
842
+ );
843
+ }
844
+ }
845
+ const check = await canLeaveStage(ctx, config);
846
+ if (!check.ok) {
847
+ throw new Error(check.errors.join("\n"));
848
+ }
849
+ ctx.meta.status = "completed";
850
+ ctx.meta.completed_at = nowIso();
851
+ await saveChangeMeta(projectRoot, config, ctx.meta);
852
+ await clearActiveIf(projectRoot, config, changeId);
853
+ let archivedTo = null;
854
+ const shouldArchive = config.persistence.archive_on_complete && ctx.workflow.on_complete?.archive !== false;
855
+ if (shouldArchive) {
856
+ const dest = join3(archiveDir(projectRoot, config), changeId);
857
+ if (await pathExists(dest)) {
858
+ throw new Error(`Archive already exists for change: ${changeId}`);
859
+ }
860
+ await ensureDir(archiveDir(projectRoot, config));
861
+ await copyDir(ctx.path, dest);
862
+ await removePath(ctx.path);
863
+ archivedTo = dest;
864
+ }
865
+ if (archivedTo) {
866
+ const meta = ChangeMetaSchema.parse(await readYaml(join3(archivedTo, "meta.yaml")));
867
+ return {
868
+ archivedTo,
869
+ ctx: {
870
+ id: changeId,
871
+ path: archivedTo,
872
+ meta,
873
+ workflow: ctx.workflow,
874
+ stages: ctx.stages,
875
+ active: ctx.active
876
+ }
877
+ };
878
+ }
879
+ return { archivedTo, ctx: await buildContext(projectRoot, config, changeId) };
880
+ }
881
+ function formatStatus(ctx) {
882
+ const lines = [];
883
+ lines.push(`Change: ${ctx.meta.title}`);
884
+ lines.push(`ID: ${ctx.id}`);
885
+ lines.push(`Status: ${ctx.meta.status}`);
886
+ lines.push(`Workflow: ${ctx.meta.workflow}`);
887
+ lines.push(`Path: ${ctx.path}`);
888
+ lines.push("");
889
+ lines.push("Stages:");
890
+ for (const stage of ctx.stages) {
891
+ const skipped = isStageSkipped(ctx.meta, stage.id) || shouldAutoSkip(stage, ctx.meta);
892
+ let mark = " ";
893
+ if (skipped) mark = "\xB7";
894
+ else if (stage.id === ctx.meta.stage) mark = "\u25CF";
895
+ else {
896
+ const order = ctx.active.map((s) => s.id);
897
+ const cur = order.indexOf(ctx.meta.stage);
898
+ const idx = order.indexOf(stage.id);
899
+ if (idx >= 0 && cur >= 0 && idx < cur) mark = "\u2713";
900
+ }
901
+ const gate = ctx.meta.gates?.[stage.id];
902
+ const gateInfo = gate ? ` [${gate.status}]` : "";
903
+ const skipInfo = skipped ? " (skipped)" : "";
904
+ lines.push(` [${mark}] ${stage.id}${stage.title ? ` \u2014 ${stage.title}` : ""}${gateInfo}${skipInfo}`);
905
+ }
906
+ if (ctx.meta.overrides?.skip_stages?.length) {
907
+ lines.push("");
908
+ lines.push(`Overrides: skip=${ctx.meta.overrides.skip_stages.join(", ")}`);
909
+ }
910
+ return lines.join("\n");
911
+ }
912
+ async function buildAgentPrompt(ctx, config, projectRoot) {
913
+ const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
914
+ const parts = [];
915
+ parts.push(`# SDD Agent Handoff`);
916
+ parts.push("");
917
+ parts.push(`You are implementing a change under Structured Vibe Coding (local SDD).`);
918
+ parts.push("");
919
+ parts.push(`## Change`);
920
+ parts.push(`- Title: ${ctx.meta.title}`);
921
+ parts.push(`- ID: ${ctx.id}`);
922
+ parts.push(`- Workflow: ${ctx.meta.workflow}`);
923
+ parts.push(`- Current stage: ${ctx.meta.stage}${stage?.title ? ` (${stage.title})` : ""}`);
924
+ parts.push(`- Change path: ${ctx.path}`);
925
+ parts.push("");
926
+ if (stage?.summary) {
927
+ parts.push(`## Stage goal`);
928
+ parts.push(stage.summary);
929
+ parts.push("");
930
+ }
931
+ if (stage?.agent_context?.instructions) {
932
+ parts.push(`## Instructions`);
933
+ parts.push(stage.agent_context.instructions);
934
+ parts.push("");
935
+ }
936
+ parts.push(`## Constraints`);
937
+ parts.push(`- Follow artifacts in the change directory as source of truth.`);
938
+ parts.push(`- Do not skip ARB/decision constraints if present.`);
939
+ parts.push(`- Local development only \u2014 verify on this machine.`);
940
+ parts.push(`- Prefer small, reviewable commits.`);
941
+ parts.push("");
942
+ parts.push(`## Artifacts to read`);
943
+ const memoryHint = join3(projectRoot, config.memory_path);
944
+ parts.push(`- Memory: ${memoryHint}/`);
945
+ for (const s of ctx.stages) {
946
+ for (const a of s.artifacts) {
947
+ parts.push(`- ${join3(ctx.path, a.path)}`);
948
+ }
949
+ }
950
+ parts.push("");
951
+ parts.push(`## Current stage artifacts`);
952
+ if (stage) {
953
+ for (const a of stage.artifacts) {
954
+ parts.push(`- ${a.path}${a.required ? " (required)" : ""}`);
955
+ }
956
+ }
957
+ parts.push("");
958
+ parts.push(`When done with this stage, the human will run \`sdd next\` or \`sdd verify\` / \`sdd complete\`.`);
959
+ return parts.join("\n");
960
+ }
961
+
962
+ // src/agents.ts
963
+ var ALL_AGENT_TARGETS = ["copilot", "claude-code", "intellij"];
964
+ async function installAgentIntegrations(opts) {
965
+ const targets = opts.targets?.length ? opts.targets : ALL_AGENT_TARGETS;
966
+ const created = [];
967
+ const skipped = [];
968
+ const root = opts.projectRoot;
969
+ const force = opts.force ?? false;
970
+ const write = async (rel, content) => {
971
+ const full = join4(root, rel);
972
+ if (await pathExists(full) && !force) {
973
+ skipped.push(rel);
974
+ return;
975
+ }
976
+ await writeText(full, content);
977
+ created.push(rel);
978
+ };
979
+ await refreshActiveAgentContext(root);
980
+ if (targets.includes("copilot")) {
981
+ await write(
982
+ ".github/copilot-instructions.md",
983
+ COPILOT_INSTRUCTIONS
984
+ );
985
+ await write(
986
+ ".github/agents/sdd.agent.md",
987
+ COPILOT_SDD_AGENT
988
+ );
989
+ await write(
990
+ ".github/instructions/sdd.instructions.md",
991
+ COPILOT_PATH_INSTRUCTIONS
992
+ );
993
+ }
994
+ if (targets.includes("claude-code")) {
995
+ await write(".claude/skills/sdd/SKILL.md", CLAUDE_SDD_SKILL);
996
+ await write(".claude/CLAUDE.md", CLAUDE_PROJECT_MD);
997
+ }
998
+ if (targets.includes("intellij")) {
999
+ await write(".idea/sdd-agent-notes.md", INTELLIJ_NOTES);
1000
+ await write("AGENTS.md", AGENTS_MD);
1001
+ } else if (targets.includes("copilot") || targets.includes("claude-code")) {
1002
+ await write("AGENTS.md", AGENTS_MD);
1003
+ }
1004
+ await write(
1005
+ join4(".sdd", "agents.json"),
1006
+ JSON.stringify(
1007
+ {
1008
+ version: 1,
1009
+ installed: targets,
1010
+ updated: (/* @__PURE__ */ new Date()).toISOString()
1011
+ },
1012
+ null,
1013
+ 2
1014
+ ) + "\n"
1015
+ );
1016
+ return { created, skipped, targets };
1017
+ }
1018
+ async function refreshActiveAgentContext(projectRoot) {
1019
+ const markerDir = sddRoot(projectRoot);
1020
+ if (!await pathExists(join4(markerDir, "config.yaml"))) {
1021
+ return null;
1022
+ }
1023
+ const config = await loadConfig(projectRoot);
1024
+ const activeId = await getActiveChangeId(projectRoot, config);
1025
+ const outPath = join4(markerDir, "active-context.md");
1026
+ if (!activeId) {
1027
+ const empty = `# SDD active context
1028
+
1029
+ _No active change._ Run \`sdd new "\u2026"\` or **SDD: New Change** in the IDE.
1030
+
1031
+ Then re-run \`sdd agents refresh\` (or advance a stage) to update this file.
1032
+ `;
1033
+ await writeText(outPath, empty);
1034
+ return outPath;
1035
+ }
1036
+ const ctx = await buildContext(projectRoot, config, activeId);
1037
+ const handoff = await buildAgentPrompt(ctx, config, projectRoot);
1038
+ const status = formatStatus(ctx);
1039
+ const body = `# SDD active context
1040
+
1041
+ > Auto-generated for GitHub Copilot, Claude Code, and IDE agents.
1042
+ > Change: **${ctx.meta.title}** \xB7 \`${ctx.id}\` \xB7 stage **${ctx.meta.stage}**
1043
+
1044
+ ## Status
1045
+
1046
+ \`\`\`
1047
+ ${status}
1048
+ \`\`\`
1049
+
1050
+ ${handoff}
1051
+ `;
1052
+ await writeText(outPath, body);
1053
+ return outPath;
1054
+ }
1055
+ var AGENTS_MD = `# Agent instructions \u2014 Structured Vibe Coding (SDD)
1056
+
1057
+ This repository uses **Spec-Driven Development** via the \`sdd\` tool (CLI / VS Code / IntelliJ).
1058
+
1059
+ ## Always
1060
+
1061
+ 1. Prefer the **active change pack** under \`changes/<id>/\` as the source of truth for the current task.
1062
+ 2. Read \`.sdd/active-context.md\` at the start of a coding session (refresh with \`sdd agents refresh\`).
1063
+ 3. Read \`memory/\` for product and architecture constraints.
1064
+ 4. Do **not** invent large features outside the current change intent.
1065
+ 5. Respect **ARB / gate decisions** (e.g. \`arb-decision.md\`) and workflow constraints.
1066
+ 6. When implementation is done, remind the human to run **local verify** (\`sdd verify\`) before \`sdd complete\`.
1067
+
1068
+ ## Workflows
1069
+
1070
+ Process is defined in \`.sdd/workflows/*.yaml\`. Stages produce markdown artifacts in the change folder. Do not skip hard gates.
1071
+
1072
+ ## Tools
1073
+
1074
+ | Surface | How |
1075
+ |---------|-----|
1076
+ | CLI | \`sdd status\`, \`sdd next\`, \`sdd agent\`, \`sdd verify\` |
1077
+ | VS Code / Cursor | Command Palette \u2192 **SDD:*** |
1078
+ | IntelliJ | Tools \u2192 **SDD** actions (runs CLI) |
1079
+ | GitHub Copilot | \`.github/copilot-instructions.md\` + agent \`sdd\` |
1080
+ | Claude Code | \`.claude/skills/sdd/SKILL.md\` |
1081
+
1082
+ ## Local only
1083
+
1084
+ Verification is **local development**. Do not assume CI replaced \`sdd verify\`.
1085
+ `;
1086
+ var COPILOT_INSTRUCTIONS = `<!-- Structured Vibe Coding (SDD) \u2014 GitHub Copilot instructions -->
1087
+
1088
+ # Project coding agent rules
1089
+
1090
+ This repo uses **Spec-Driven Development (SDD)** with the \`sdd\` CLI / IDE plugins.
1091
+
1092
+ ## Before writing code
1093
+
1094
+ 1. Open and follow \`.sdd/active-context.md\` if present.
1095
+ 2. Open the active change under \`changes/\` (see \`changes/.active\` or the latest in-progress folder).
1096
+ 3. Prefer artifacts for the **current stage** (intent/feature/design/tasks) over guessing.
1097
+ 4. Honor \`memory/product.md\`, \`memory/architecture.md\`, \`memory/conventions.md\`.
1098
+
1099
+ ## While coding
1100
+
1101
+ - Stay within the change scope (title + intent/feature).
1102
+ - Match patterns described in \`research.md\` / design when present.
1103
+ - Do not violate constraints in \`arb-decision.md\` or gate notes.
1104
+ - Prefer small, reviewable diffs.
1105
+
1106
+ ## When finishing
1107
+
1108
+ - Suggest \`sdd verify\` (local) and only then \`sdd complete\`.
1109
+ - Do not claim the change is complete without local verification when the workflow has a verify stage.
1110
+
1111
+ ## Commands the human may run
1112
+
1113
+ \`\`\`bash
1114
+ sdd status
1115
+ sdd next
1116
+ sdd agent # print handoff
1117
+ sdd agents refresh # refresh .sdd/active-context.md
1118
+ sdd verify
1119
+ sdd complete
1120
+ \`\`\`
1121
+ `;
1122
+ var COPILOT_SDD_AGENT = `---
1123
+ name: sdd
1124
+ description: Spec-Driven Development agent for this repo. Use when implementing the active sdd change, advancing stages, or following .sdd workflows.
1125
+ ---
1126
+
1127
+ # SDD agent (GitHub Copilot)
1128
+
1129
+ You are the **SDD implementer** for this workspace.
1130
+
1131
+ ## Mission
1132
+
1133
+ Implement only what the **active change pack** specifies. Specs and gates win over improvisation.
1134
+
1135
+ ## Required reads (in order)
1136
+
1137
+ 1. \`.sdd/active-context.md\`
1138
+ 2. \`changes/<active-id>/meta.yaml\` (workflow + stage)
1139
+ 3. Artifacts for the current stage and prior stages in that folder
1140
+ 4. \`memory/*.md\`
1141
+
1142
+ ## Behavior
1143
+
1144
+ - If there is no active change, tell the user to run \`sdd new "\u2026"\` or **SDD: New Change**.
1145
+ - If \`meta.yaml\` stage is not \`implement\`, help fill the current stage artifact instead of large code rewrites\u2014unless the user explicitly wants implementation early.
1146
+ - On implement stage: execute tasks from \`tasks.md\` / acceptance criteria; keep diffs scoped.
1147
+ - After code changes: remind them to run **local** \`sdd verify\`.
1148
+
1149
+ ## Out of scope
1150
+
1151
+ - Redesigning the whole product
1152
+ - Skipping hard gates
1153
+ - Inventing requirements not in the change pack
1154
+ `;
1155
+ var COPILOT_PATH_INSTRUCTIONS = `---
1156
+ applyTo: "changes/**,memory/**,.sdd/**"
1157
+ ---
1158
+
1159
+ # SDD paths
1160
+
1161
+ Files under \`changes/\`, \`memory/\`, and \`.sdd/\` drive Spec-Driven Development.
1162
+
1163
+ - Treat \`changes/**/meta.yaml\` as process state (do not invent fields).
1164
+ - Prefer editing stage artifacts over deleting workflow history.
1165
+ - When modifying code for an active change, keep alignment with that change's intent/design/tasks.
1166
+ `;
1167
+ var CLAUDE_SDD_SKILL = `---
1168
+ name: sdd
1169
+ description: Use Spec-Driven Development (sdd) for this repository \u2014 read active change, follow stages, implement within scope, local verify.
1170
+ ---
1171
+
1172
+ # Skill: Structured Vibe Coding (SDD)
1173
+
1174
+ ## When to use
1175
+
1176
+ - User asks to implement a feature/fix in this repo
1177
+ - User mentions sdd, change packs, stages, ARB, or local verify
1178
+ - You need the source of truth for the current task
1179
+
1180
+ ## Instructions
1181
+
1182
+ 1. Read \`.sdd/active-context.md\` (if missing, run \`sdd agents refresh\` via bash or ask the user).
1183
+ 2. Read \`changes/<id>/meta.yaml\` for \`workflow\` and \`stage\`.
1184
+ 3. Read relevant artifacts in that change folder (intent/feature/design/tasks/acceptance).
1185
+ 4. Read \`memory/\` for global constraints.
1186
+ 5. Implement **only** in-scope work for the current change.
1187
+ 6. Prefer project conventions in \`memory/conventions.md\`.
1188
+ 7. When done coding, instruct the user to run:
1189
+
1190
+ \`\`\`bash
1191
+ sdd verify
1192
+ sdd gate approve # if hard gate
1193
+ sdd next # or sdd complete on last stage
1194
+ \`\`\`
1195
+
1196
+ ## Bash helpers (if tools allow)
1197
+
1198
+ \`\`\`bash
1199
+ sdd status
1200
+ sdd agent
1201
+ sdd agents refresh
1202
+ \`\`\`
1203
+
1204
+ ## Do not
1205
+
1206
+ - Skip hard gates
1207
+ - Expand scope beyond the change title/intent
1208
+ - Delete \`.sdd\` or archive history casually
1209
+ `;
1210
+ var CLAUDE_PROJECT_MD = `# Claude Code \u2014 this repo uses SDD
1211
+
1212
+ Load the **sdd** skill under \`.claude/skills/sdd/\` for implementation work.
1213
+
1214
+ Always check \`.sdd/active-context.md\` and the active folder under \`changes/\` before large edits.
1215
+
1216
+ CLI: \`sdd\` (npm: \`@structured-vibe-coding/cli\`).
1217
+ `;
1218
+ var INTELLIJ_NOTES = `# SDD + IntelliJ / GitHub Copilot
1219
+
1220
+ This project uses Structured Vibe Coding (\`sdd\`).
1221
+
1222
+ ## IntelliJ plugin
1223
+
1224
+ Install the **Structured Vibe Coding (SDD)** plugin (or run the Gradle plugin from \`packages/intellij\`).
1225
+ Actions under **Tools \u2192 SDD** shell out to the \`sdd\` CLI \u2014 install the CLI on PATH:
1226
+
1227
+ \`\`\`bash
1228
+ npm i -g @structured-vibe-coding/cli
1229
+ \`\`\`
1230
+
1231
+ ## GitHub Copilot in IntelliJ
1232
+
1233
+ Copilot respects repository instructions:
1234
+
1235
+ - \`.github/copilot-instructions.md\`
1236
+ - \`AGENTS.md\`
1237
+ - \`.sdd/active-context.md\` (refresh often)
1238
+
1239
+ Use the SDD plugin action **Refresh agent context** after \`sdd new\` / stage changes.
1240
+ `;
1241
+
1242
+ export {
1243
+ GateSchema,
1244
+ ArtifactDefSchema,
1245
+ VerifyCommandSchema,
1246
+ StageSchema,
1247
+ WorkflowSchema,
1248
+ ConfigSchema,
1249
+ ChangeMetaSchema,
1250
+ SDD_DIR,
1251
+ sddRoot,
1252
+ configPath,
1253
+ workflowsDir,
1254
+ templatesDir,
1255
+ memoryDir,
1256
+ changesDir,
1257
+ archiveDir,
1258
+ domainsDir,
1259
+ changePath,
1260
+ changeMetaPath,
1261
+ activePointerPath,
1262
+ pathExists,
1263
+ ensureDir,
1264
+ readText,
1265
+ writeText,
1266
+ readYaml,
1267
+ writeYaml,
1268
+ listDirs,
1269
+ listFiles,
1270
+ copyDir,
1271
+ removePath,
1272
+ isInitialized,
1273
+ loadConfig,
1274
+ saveConfig,
1275
+ defaultConfig,
1276
+ projectPaths,
1277
+ listWorkflowNames,
1278
+ loadWorkflow,
1279
+ resolveStages,
1280
+ isStageSkipped,
1281
+ shouldAutoSkip,
1282
+ activeStages,
1283
+ getStage,
1284
+ nextStageId,
1285
+ previousStageId,
1286
+ firstActiveStageId,
1287
+ inferComplexity,
1288
+ recommendWorkflow,
1289
+ slugify,
1290
+ todayIsoDate,
1291
+ nowIso,
1292
+ changeDirName,
1293
+ uniqueChangeDirName,
1294
+ ALL_AGENT_TARGETS,
1295
+ installAgentIntegrations,
1296
+ refreshActiveAgentContext,
1297
+ createChange,
1298
+ loadChangeMeta,
1299
+ saveChangeMeta,
1300
+ buildContext,
1301
+ listChanges,
1302
+ getActiveChangeId,
1303
+ clearActiveIf,
1304
+ setActiveChange,
1305
+ resolveChangeId,
1306
+ canLeaveStage,
1307
+ advanceStage,
1308
+ skipStage,
1309
+ switchWorkflow,
1310
+ approveGate,
1311
+ completeChange,
1312
+ formatStatus,
1313
+ buildAgentPrompt
1314
+ };