@structured-vibe-coding/core 0.12.2 → 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.
@@ -1,413 +1,26 @@
1
1
  import {
2
- ChangeMetaSchema,
3
- WorkflowSchema,
4
2
  loadConfig
5
- } from "./chunk-KZHNYGD6.js";
3
+ } from "./chunk-OJFMJHNL.js";
6
4
  import {
7
- activePointerPath,
8
- changeMetaPath,
9
- changePath,
10
- changesDir,
11
- sddRoot,
12
- workflowsDir
5
+ buildAgentPrompt,
6
+ formatStatus
7
+ } from "./chunk-C725DWSB.js";
8
+ import {
9
+ buildContext,
10
+ getActiveChangeId
11
+ } from "./chunk-A752UVPF.js";
12
+ import {
13
+ sddRoot
13
14
  } from "./chunk-7MJYPJMR.js";
14
15
  import {
15
- ensureDir,
16
- listDirs,
17
- listFiles,
18
16
  pathExists,
19
17
  readText,
20
- readYaml,
21
18
  removePath,
22
- writeText,
23
- writeYaml
19
+ writeText
24
20
  } from "./chunk-EPFURATC.js";
25
21
 
26
22
  // src/agents.ts
27
- import { join as join3 } from "pathe";
28
-
29
- // src/slug.ts
30
- function slugify(input) {
31
- return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
32
- }
33
- function todayIsoDate(d = /* @__PURE__ */ new Date()) {
34
- return d.toISOString().slice(0, 10);
35
- }
36
- function nowIso(d = /* @__PURE__ */ new Date()) {
37
- return d.toISOString();
38
- }
39
- function changeDirName(title, d = /* @__PURE__ */ new Date()) {
40
- const slug = slugify(title) || "change";
41
- return `${todayIsoDate(d)}-${slug}`;
42
- }
43
- function uniqueChangeDirName(base, taken) {
44
- if (!taken.has(base)) return base;
45
- let n = 2;
46
- while (taken.has(`${base}-${n}`)) n += 1;
47
- return `${base}-${n}`;
48
- }
49
-
50
- // src/workflow.ts
51
23
  import { join } from "pathe";
52
- async function listWorkflowNames(projectRoot) {
53
- const dir = workflowsDir(projectRoot);
54
- const files = await listFiles(dir);
55
- return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => f.replace(/\.ya?ml$/, "")).sort();
56
- }
57
- async function loadWorkflow(projectRoot, name) {
58
- const dir = workflowsDir(projectRoot);
59
- const candidates = [`${name}.yaml`, `${name}.yml`];
60
- for (const file of candidates) {
61
- const path = join(dir, file);
62
- if (await pathExists(path)) {
63
- const raw = await readYaml(path);
64
- const workflow = WorkflowSchema.parse(raw);
65
- if (workflow.name !== name) {
66
- }
67
- return workflow;
68
- }
69
- }
70
- throw new Error(
71
- `Workflow "${name}" not found in ${dir}. Available: ${(await listWorkflowNames(projectRoot)).join(", ") || "(none)"}`
72
- );
73
- }
74
- function resolveStages(workflow, meta) {
75
- const base = [...workflow.stages];
76
- const extra = meta.overrides?.extra_stages ?? [];
77
- const stages = [...base, ...extra];
78
- return stages.map((stage) => {
79
- const gateOverride = meta.overrides?.gates?.[stage.id];
80
- if (!gateOverride) return stage;
81
- return {
82
- ...stage,
83
- gate: {
84
- ...stage.gate,
85
- type: gateOverride
86
- }
87
- };
88
- });
89
- }
90
- function isStageSkipped(meta, stageId) {
91
- if (meta.overrides?.skip_stages?.includes(stageId)) return true;
92
- if (meta.skipped?.some((s) => s.stage === stageId)) return true;
93
- return false;
94
- }
95
- function shouldAutoSkip(stage, meta) {
96
- const flags = stage.skip_when?.flags ?? [];
97
- for (const flag of flags) {
98
- if (meta.flags?.[flag] === true || meta.flags?.[flag] === "true") {
99
- return true;
100
- }
101
- }
102
- return false;
103
- }
104
- function activeStages(workflow, meta) {
105
- return resolveStages(workflow, meta).filter((s) => {
106
- if (isStageSkipped(meta, s.id)) return false;
107
- if (shouldAutoSkip(s, meta)) return false;
108
- return true;
109
- });
110
- }
111
- function getStage(workflow, meta, stageId) {
112
- return resolveStages(workflow, meta).find((s) => s.id === stageId);
113
- }
114
- function nextStageId(workflow, meta) {
115
- const all = resolveStages(workflow, meta);
116
- const activeIds = new Set(activeStages(workflow, meta).map((s) => s.id));
117
- let start = all.findIndex((s) => s.id === meta.stage);
118
- if (start === -1) start = -1;
119
- for (let i = start + 1; i < all.length; i++) {
120
- const id = all[i].id;
121
- if (activeIds.has(id)) return id;
122
- }
123
- return null;
124
- }
125
- function firstActiveStageId(workflow, meta) {
126
- return activeStages(workflow, meta)[0]?.id ?? null;
127
- }
128
- function inferComplexity(title, hints) {
129
- const text = `${title} ${(hints?.keywords ?? []).join(" ")}`.toLowerCase();
130
- const hotfixHints = ["typo", "hotfix", "nit", "rename", "bump", "chore", "readme", "docs"];
131
- const highHints = [
132
- "architecture",
133
- "migration",
134
- "auth",
135
- "payment",
136
- "billing",
137
- "redesign",
138
- "platform",
139
- "arb",
140
- "epic"
141
- ];
142
- if (hotfixHints.some((h) => text.includes(h))) return "low";
143
- if (highHints.some((h) => text.includes(h))) return "high";
144
- if (text.length > 80) return "medium";
145
- return "medium";
146
- }
147
- async function recommendWorkflow(projectRoot, title, config, opts) {
148
- const available = await listWorkflowNames(projectRoot);
149
- const allowed = config.allowed_workflows?.length ? available.filter((n) => config.allowed_workflows.includes(n)) : available;
150
- if (opts?.preferred) {
151
- if (!available.includes(opts.preferred)) {
152
- throw new Error(
153
- `Workflow "${opts.preferred}" is not available. Available: ${available.join(", ") || "(none)"}`
154
- );
155
- }
156
- if (config.allowed_workflows?.length && !config.allowed_workflows.includes(opts.preferred)) {
157
- throw new Error(
158
- `Workflow "${opts.preferred}" is not in allowed_workflows: ${config.allowed_workflows.join(", ")}`
159
- );
160
- }
161
- return {
162
- name: opts.preferred,
163
- reason: "Explicitly selected",
164
- alternatives: allowed.filter((n) => n !== opts.preferred)
165
- };
166
- }
167
- if (config.default_workflow !== "recommend" && allowed.includes(config.default_workflow)) {
168
- return {
169
- name: config.default_workflow,
170
- reason: `Repo default_workflow=${config.default_workflow}`,
171
- alternatives: allowed.filter((n) => n !== config.default_workflow)
172
- };
173
- }
174
- const complexity = opts?.complexity ?? inferComplexity(title);
175
- const scored = [];
176
- for (const name of allowed) {
177
- try {
178
- const wf = await loadWorkflow(projectRoot, name);
179
- let score = wf.recommendation?.priority ?? 0;
180
- let reason = `workflow priority ${score}`;
181
- const when = wf.recommendation?.when;
182
- if (complexity === "low" && name === "hotfix") {
183
- score += 50;
184
- reason = "Low complexity \u2192 hotfix";
185
- } else if (complexity === "low" && name === "patch") {
186
- score += 40;
187
- reason = "Low complexity \u2192 patch";
188
- } else if (complexity === "medium" && name === "feature") {
189
- score += 45;
190
- reason = "Medium complexity \u2192 feature";
191
- } else if (complexity === "medium" && name === "patch") {
192
- score += 30;
193
- reason = "Medium complexity \u2192 patch";
194
- } else if (complexity === "high" && name === "enterprise-feature") {
195
- score += 50;
196
- reason = "High complexity \u2192 enterprise-feature";
197
- } else if (complexity === "high" && name === "feature") {
198
- score += 35;
199
- reason = "High complexity \u2192 feature";
200
- }
201
- const keywords = when?.keywords ?? [];
202
- const titleLower = title.toLowerCase();
203
- if (keywords.some((k) => titleLower.includes(k.toLowerCase()))) {
204
- score += 20;
205
- reason = `Keyword match for ${name}`;
206
- }
207
- scored.push({ name, score, reason });
208
- } catch {
209
- }
210
- }
211
- scored.sort((a, b) => b.score - a.score);
212
- const best = scored[0];
213
- if (!best) {
214
- throw new Error("No workflows available. Run `sdd init` or add files under .sdd/workflows/");
215
- }
216
- return {
217
- name: best.name,
218
- reason: best.reason,
219
- alternatives: scored.slice(1).map((s) => s.name)
220
- };
221
- }
222
-
223
- // src/change-context.ts
224
- async function loadChangeMeta(projectRoot, config, changeId) {
225
- const path = changeMetaPath(projectRoot, config, changeId);
226
- if (!await pathExists(path)) {
227
- throw new Error(`Change not found: ${changeId}`);
228
- }
229
- return ChangeMetaSchema.parse(await readYaml(path));
230
- }
231
- async function saveChangeMeta(projectRoot, config, meta) {
232
- meta.updated = nowIso();
233
- await writeYaml(changeMetaPath(projectRoot, config, meta.id), ChangeMetaSchema.parse(meta));
234
- }
235
- async function buildContext(projectRoot, config, changeId) {
236
- const meta = await loadChangeMeta(projectRoot, config, changeId);
237
- const workflow = await loadWorkflow(projectRoot, meta.workflow);
238
- return {
239
- id: changeId,
240
- path: changePath(projectRoot, config, changeId),
241
- meta,
242
- workflow,
243
- stages: resolveStages(workflow, meta),
244
- active: activeStages(workflow, meta)
245
- };
246
- }
247
- async function listChanges(projectRoot, config) {
248
- return listDirs(changesDir(projectRoot, config));
249
- }
250
- async function getActiveChangeId(projectRoot, config) {
251
- const pointer = activePointerPath(projectRoot, config);
252
- if (await pathExists(pointer)) {
253
- const id = (await readText(pointer)).trim();
254
- if (id && await pathExists(changePath(projectRoot, config, id))) {
255
- return id;
256
- }
257
- if (id) {
258
- await writeText(pointer, "");
259
- }
260
- }
261
- const all = await listChanges(projectRoot, config);
262
- const inProgress = [];
263
- for (const id of all) {
264
- try {
265
- const meta = await loadChangeMeta(projectRoot, config, id);
266
- if (meta.status === "in_progress" || meta.status === "blocked") {
267
- inProgress.push({ id, updated: meta.updated ?? meta.created });
268
- }
269
- } catch {
270
- }
271
- }
272
- if (!inProgress.length) return null;
273
- inProgress.sort((a, b) => b.updated.localeCompare(a.updated));
274
- return inProgress[0].id;
275
- }
276
- async function clearActiveIf(projectRoot, config, changeId) {
277
- const pointer = activePointerPath(projectRoot, config);
278
- if (!await pathExists(pointer)) return;
279
- const id = (await readText(pointer)).trim();
280
- if (id === changeId) {
281
- await writeText(pointer, "");
282
- }
283
- }
284
- async function setActiveChange(projectRoot, config, changeId) {
285
- await ensureDir(changesDir(projectRoot, config));
286
- await writeText(activePointerPath(projectRoot, config), `${changeId}
287
- `);
288
- }
289
- async function resolveChangeId(projectRoot, config, changeId) {
290
- if (changeId) return changeId;
291
- const active = await getActiveChangeId(projectRoot, config);
292
- if (!active) {
293
- throw new Error("No active change. Pass --change <id> or run `sdd new`.");
294
- }
295
- return active;
296
- }
297
-
298
- // src/agent-handoff.ts
299
- import { join as join2 } from "pathe";
300
- function formatStatus(ctx) {
301
- const lines = [];
302
- lines.push(`Change: ${ctx.meta.title}`);
303
- lines.push(`ID: ${ctx.id}`);
304
- lines.push(`Status: ${ctx.meta.status}`);
305
- lines.push(`Workflow: ${ctx.meta.workflow}`);
306
- lines.push(`Path: ${ctx.path}`);
307
- lines.push("");
308
- lines.push("Stages:");
309
- for (const stage of ctx.stages) {
310
- const skipped = isStageSkipped(ctx.meta, stage.id) || shouldAutoSkip(stage, ctx.meta);
311
- let mark = " ";
312
- if (skipped) mark = "\xB7";
313
- else if (stage.id === ctx.meta.stage) mark = "\u25CF";
314
- else {
315
- const order = ctx.active.map((s) => s.id);
316
- const cur = order.indexOf(ctx.meta.stage);
317
- const idx = order.indexOf(stage.id);
318
- if (idx >= 0 && cur >= 0 && idx < cur) mark = "\u2713";
319
- }
320
- const gate = ctx.meta.gates?.[stage.id];
321
- const gateInfo = gate ? ` [${gate.status}]` : "";
322
- const skipInfo = skipped ? " (skipped)" : "";
323
- lines.push(
324
- ` [${mark}] ${stage.id}${stage.title ? ` \u2014 ${stage.title}` : ""}${gateInfo}${skipInfo}`
325
- );
326
- }
327
- if (ctx.meta.overrides?.skip_stages?.length) {
328
- lines.push("");
329
- lines.push(`Overrides: skip=${ctx.meta.overrides.skip_stages.join(", ")}`);
330
- }
331
- return lines.join("\n");
332
- }
333
- async function buildAgentPrompt(ctx, config, projectRoot) {
334
- const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
335
- const parts = [];
336
- parts.push(`# SDD Agent Handoff`);
337
- parts.push("");
338
- parts.push(`You are implementing a change under Structured Vibe Coding (local SDD).`);
339
- parts.push("");
340
- parts.push(`## Change`);
341
- parts.push(`- Title: ${ctx.meta.title}`);
342
- parts.push(`- ID: ${ctx.id}`);
343
- parts.push(`- Workflow: ${ctx.meta.workflow}`);
344
- parts.push(
345
- `- Current stage: ${ctx.meta.stage}${stage?.title ? ` (${stage.title})` : ""}`
346
- );
347
- parts.push(`- Change path: ${ctx.path}`);
348
- parts.push("");
349
- if (stage?.summary) {
350
- parts.push(`## Stage goal`);
351
- parts.push(stage.summary);
352
- parts.push("");
353
- }
354
- if (stage?.agent_context?.instructions) {
355
- parts.push(`## Instructions`);
356
- parts.push(stage.agent_context.instructions);
357
- parts.push("");
358
- }
359
- parts.push(`## Constraints`);
360
- parts.push(`- Follow artifacts in the change directory as source of truth.`);
361
- parts.push(`- Honor \`memory/constitution.md\` when present (non-negotiables).`);
362
- parts.push(`- Do not skip ARB/decision constraints if present.`);
363
- parts.push(`- Local development only \u2014 verify on this machine.`);
364
- parts.push(`- Prefer small, reviewable commits.`);
365
- parts.push("");
366
- parts.push(`## Artifacts to read`);
367
- const memoryHint = join2(projectRoot, config.memory_path);
368
- parts.push(`- Documentation map (if present): ${memoryHint}/index.md`);
369
- parts.push(`- Memory: ${memoryHint}/`);
370
- for (const s of ctx.stages) {
371
- for (const a of s.artifacts) {
372
- parts.push(`- ${join2(ctx.path, a.path)}`);
373
- }
374
- }
375
- parts.push("");
376
- parts.push(`## Current stage artifacts`);
377
- if (stage) {
378
- for (const a of stage.artifacts) {
379
- parts.push(`- ${a.path}${a.required ? " (required)" : ""}`);
380
- }
381
- }
382
- parts.push("");
383
- const stageId = ctx.meta.stage;
384
- if (stageId === "implement" || stageId === "local_verify") {
385
- parts.push(`## Code context (product code)`);
386
- parts.push("");
387
- parts.push(
388
- `Process context above is not enough for product code structure.`
389
- );
390
- parts.push(
391
- `When you need symbols, callers, or package-boundary context:`
392
- );
393
- parts.push(
394
- `1. Run: \`sdd context\` (optional: \`--path \u2026\`, \`--symbol \u2026\`, \`--out change\`)`
395
- );
396
- parts.push(
397
- `2. If present, read: \`changes/${ctx.id}/code-context.md\` (regenerate if stale)`
398
- );
399
- parts.push(
400
- `Do not dump unbounded source into the prompt; prefer the sliced output.`
401
- );
402
- parts.push("");
403
- }
404
- parts.push(
405
- `When done with this stage, the human will run \`sdd next\` or \`sdd verify\` / \`sdd complete\`.`
406
- );
407
- return parts.join("\n");
408
- }
409
-
410
- // src/agents.ts
411
24
  var AGENT_INTEGRATIONS = [
412
25
  {
413
26
  id: "copilot",
@@ -512,7 +125,7 @@ async function removeOtherAgentHosts(projectRoot, keep) {
512
125
  for (const integ of AGENT_INTEGRATIONS) {
513
126
  if (integ.id === keep) continue;
514
127
  for (const rel of agentHostPaths(integ.id)) {
515
- const full = join3(projectRoot, rel);
128
+ const full = join(projectRoot, rel);
516
129
  if (await pathExists(full)) {
517
130
  await removePath(full);
518
131
  removed.push(rel);
@@ -520,7 +133,7 @@ async function removeOtherAgentHosts(projectRoot, keep) {
520
133
  }
521
134
  }
522
135
  for (const rel of LEGACY_AGENT_PATHS) {
523
- const full = join3(projectRoot, rel);
136
+ const full = join(projectRoot, rel);
524
137
  if (await pathExists(full)) {
525
138
  await removePath(full);
526
139
  removed.push(rel);
@@ -529,7 +142,7 @@ async function removeOtherAgentHosts(projectRoot, keep) {
529
142
  for (const parent of [".claude", ".idea", ".grok", ".ollama"]) {
530
143
  if (keep === "grok" && parent === ".grok") continue;
531
144
  if (keep === "ollama" && parent === ".ollama") continue;
532
- const full = join3(projectRoot, parent);
145
+ const full = join(projectRoot, parent);
533
146
  if (!await pathExists(full)) continue;
534
147
  try {
535
148
  const { readdir } = await import("fs/promises");
@@ -624,7 +237,7 @@ async function installAgentIntegration(opts) {
624
237
  const force = opts.force ?? false;
625
238
  await removeOtherAgentHosts(root, opts.target);
626
239
  const write = async (rel, content) => {
627
- const full = join3(root, rel);
240
+ const full = join(root, rel);
628
241
  if (await pathExists(full) && !force) {
629
242
  skipped.push(rel);
630
243
  return;
@@ -632,7 +245,7 @@ async function installAgentIntegration(opts) {
632
245
  await writeText(full, content);
633
246
  created.push(rel);
634
247
  };
635
- await write(join3(".sdd", "protocol.md"), PROTOCOL_MD);
248
+ await write(join(".sdd", "protocol.md"), PROTOCOL_MD);
636
249
  await refreshActiveAgentContext(root);
637
250
  const roles = integ.rolesToInstall?.length ? SDD_AGENT_ROLES.filter((r) => integ.rolesToInstall.includes(r.id)) : SDD_AGENT_ROLES;
638
251
  for (const role of roles) {
@@ -640,7 +253,7 @@ async function installAgentIntegration(opts) {
640
253
  }
641
254
  await write("AGENTS.md", renderAgentsMd(integ));
642
255
  await write(
643
- join3(".sdd", "agents.json"),
256
+ join(".sdd", "agents.json"),
644
257
  JSON.stringify(
645
258
  {
646
259
  version: 3,
@@ -708,7 +321,7 @@ Do not claim the change is complete without local verification when the workflow
708
321
  `;
709
322
  }
710
323
  async function loadInstalledAgent(projectRoot) {
711
- const path = join3(sddRoot(projectRoot), "agents.json");
324
+ const path = join(sddRoot(projectRoot), "agents.json");
712
325
  if (!await pathExists(path)) return null;
713
326
  try {
714
327
  const raw = JSON.parse(await readText(path));
@@ -723,7 +336,7 @@ async function loadInstalledAgent(projectRoot) {
723
336
  async function writeAgentHandoff(projectRoot, config, changeId) {
724
337
  const ctx = await buildContext(projectRoot, config, changeId);
725
338
  const body = await buildAgentPrompt(ctx, config, projectRoot);
726
- const outPath = join3(sddRoot(projectRoot), "handoff.md");
339
+ const outPath = join(sddRoot(projectRoot), "handoff.md");
727
340
  const header = `# SDD handoff (auto)
728
341
 
729
342
  > Generated for the AI coding agent configured at init.
@@ -760,16 +373,16 @@ function agentKickoffMessage(opts) {
760
373
  }
761
374
  async function refreshActiveAgentContext(projectRoot) {
762
375
  const markerDir = sddRoot(projectRoot);
763
- if (!await pathExists(join3(markerDir, "config.yaml"))) {
376
+ if (!await pathExists(join(markerDir, "config.yaml"))) {
764
377
  return null;
765
378
  }
766
- const protocolPath = join3(markerDir, "protocol.md");
379
+ const protocolPath = join(markerDir, "protocol.md");
767
380
  if (!await pathExists(protocolPath)) {
768
381
  await writeText(protocolPath, PROTOCOL_MD);
769
382
  }
770
383
  const config = await loadConfig(projectRoot);
771
384
  const activeId = await getActiveChangeId(projectRoot, config);
772
- const outPath = join3(markerDir, "active-context.md");
385
+ const outPath = join(markerDir, "active-context.md");
773
386
  if (!activeId) {
774
387
  const empty = `# SDD active context
775
388
 
@@ -892,32 +505,6 @@ Refresh context: \`sdd agents refresh\`.
892
505
  }
893
506
 
894
507
  export {
895
- listWorkflowNames,
896
- loadWorkflow,
897
- resolveStages,
898
- isStageSkipped,
899
- shouldAutoSkip,
900
- activeStages,
901
- getStage,
902
- nextStageId,
903
- firstActiveStageId,
904
- inferComplexity,
905
- recommendWorkflow,
906
- slugify,
907
- todayIsoDate,
908
- nowIso,
909
- changeDirName,
910
- uniqueChangeDirName,
911
- loadChangeMeta,
912
- saveChangeMeta,
913
- buildContext,
914
- listChanges,
915
- getActiveChangeId,
916
- clearActiveIf,
917
- setActiveChange,
918
- resolveChangeId,
919
- formatStatus,
920
- buildAgentPrompt,
921
508
  AGENT_INTEGRATIONS,
922
509
  ALL_AGENT_TARGETS,
923
510
  DEFAULT_INIT_INTEGRATION,