@structured-vibe-coding/core 0.9.1 → 0.10.1

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,287 +1,30 @@
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 copyDirSkipExisting(src, dest) {
64
- await ensureDir(dest);
65
- const entries = await fs.readdir(src, { withFileTypes: true });
66
- for (const entry of entries) {
67
- const from = `${src}/${entry.name}`;
68
- const to = `${dest}/${entry.name}`;
69
- if (entry.isDirectory()) {
70
- await copyDirSkipExisting(from, to);
71
- } else if (!await pathExists(to)) {
72
- await ensureDir(dirname(to));
73
- await fs.copyFile(from, to);
74
- }
75
- }
76
- }
77
- async function writeTextIfMissing(path, content) {
78
- if (await pathExists(path)) return false;
79
- await writeText(path, content);
80
- return true;
81
- }
82
- async function removePath(path) {
83
- await fs.rm(path, { recursive: true, force: true });
84
- }
85
-
86
- // src/schemas.ts
87
- import { z } from "zod";
88
- var GateSchema = z.object({
89
- type: z.enum(["none", "soft", "hard"]).default("soft"),
90
- mode: z.enum(["none", "human_approval", "checklist"]).default("checklist"),
91
- checklist: z.array(z.string()).default([]),
92
- overridable: z.boolean().default(true)
93
- });
94
- var ArtifactDefSchema = z.object({
95
- id: z.string(),
96
- path: z.string(),
97
- template: z.string().optional(),
98
- required: z.boolean().default(true)
99
- });
100
- var VerifyCommandSchema = z.object({
101
- name: z.string(),
102
- run: z.string(),
103
- required: z.boolean().default(false)
104
- });
105
- var StageSchema = z.object({
106
- id: z.string(),
107
- title: z.string().optional(),
108
- summary: z.string().optional(),
109
- /**
110
- * standard — normal work stage
111
- * brainstorm — optional multi-approach exploration (before design)
112
- * clarify — optional Q&A before leaving a major stage
113
- */
114
- kind: z.enum(["standard", "brainstorm", "clarify"]).default("standard"),
115
- /** When true, stage is optional (skippable); agents may skip if not needed */
116
- optional: z.boolean().default(false),
117
- artifacts: z.array(ArtifactDefSchema).default([]),
118
- gate: GateSchema.default({}),
119
- skippable: z.boolean().default(true),
120
- skip_when: z.object({
121
- flags: z.array(z.string()).optional()
122
- }).optional(),
123
- verify: z.object({
124
- commands: z.array(VerifyCommandSchema).default([]),
125
- evidence_dir: z.string().optional()
126
- }).optional(),
127
- agent_context: z.object({
128
- include: z.array(z.string()).default([]),
129
- instructions: z.string().optional()
130
- }).optional()
131
- });
132
- var WorkflowSchema = z.object({
133
- name: z.string(),
134
- description: z.string().optional(),
135
- version: z.number().default(1),
136
- stages: z.array(StageSchema).min(1),
137
- recommendation: z.object({
138
- when: z.object({
139
- labels: z.array(z.string()).optional(),
140
- keywords: z.array(z.string()).optional(),
141
- max_complexity: z.enum(["low", "medium", "high"]).optional(),
142
- min_complexity: z.enum(["low", "medium", "high"]).optional()
143
- }).optional(),
144
- priority: z.number().default(0)
145
- }).optional(),
146
- on_complete: z.object({
147
- archive: z.boolean().default(false),
148
- domain_sync: z.enum(["never", "recommend", "require"]).default("never")
149
- }).optional()
150
- });
151
- var ConfigSchema = z.object({
152
- version: z.number().default(1),
153
- memory_path: z.string().default("memory"),
154
- changes_path: z.string().default("changes"),
155
- archive_path: z.string().default("archive"),
156
- domains_path: z.string().default("domains"),
157
- default_workflow: z.string().default("recommend"),
158
- allowed_workflows: z.array(z.string()).optional(),
159
- per_change: z.object({
160
- allow_skip: z.boolean().default(true),
161
- allow_gate_override: z.enum(["true", "false", "soft_only"]).default("soft_only"),
162
- allow_custom_stages: z.boolean().default(true),
163
- require_reason_on_skip: z.boolean().default(true)
164
- }).default({}),
165
- persistence: z.object({
166
- default: z.enum(["change_only", "domain_sync"]).default("change_only"),
167
- archive_on_complete: z.boolean().default(false),
168
- domain_sync: z.object({
169
- mode: z.enum(["never", "recommend", "require"]).default("never"),
170
- anchored_domains: z.array(z.string()).default([])
171
- }).default({})
172
- }).default({}),
173
- policy: z.object({
174
- gates: z.enum(["soft", "hard"]).default("soft")
175
- }).default({})
176
- });
177
- var ChangeMetaSchema = z.object({
178
- id: z.string(),
179
- title: z.string(),
180
- workflow: z.string(),
181
- created: z.string(),
182
- updated: z.string().optional(),
183
- status: z.enum(["in_progress", "blocked", "completed", "abandoned"]).default("in_progress"),
184
- stage: z.string(),
185
- branch: z.string().optional(),
186
- domain: z.string().optional(),
187
- flags: z.record(z.union([z.boolean(), z.string(), z.number()])).default({}),
188
- overrides: z.object({
189
- skip_stages: z.array(z.string()).default([]),
190
- gates: z.record(z.enum(["none", "soft", "hard"])).default({}),
191
- extra_stages: z.array(StageSchema).default([])
192
- }).default({}),
193
- skipped: z.array(
194
- z.object({
195
- stage: z.string(),
196
- reason: z.string(),
197
- at: z.string()
198
- })
199
- ).default([]),
200
- gates: z.record(
201
- z.object({
202
- status: z.enum(["pending", "approved", "failed", "waived"]),
203
- note: z.string().optional(),
204
- at: z.string().optional()
205
- })
206
- ).default({}),
207
- /** Results from `sdd verify` keyed by stage id */
208
- verify_results: z.record(
209
- z.object({
210
- ok: z.boolean(),
211
- at: z.string(),
212
- results: z.array(
213
- z.object({
214
- name: z.string(),
215
- exitCode: z.number().nullable()
216
- })
217
- ).default([])
218
- })
219
- ).default({}),
220
- pr: z.object({
221
- number: z.number().optional(),
222
- url: z.string().optional()
223
- }).optional(),
224
- completed_at: z.string().optional()
225
- });
226
-
227
- // src/paths.ts
228
- import { join } from "pathe";
229
- var SDD_DIR = ".sdd";
230
- function sddRoot(projectRoot) {
231
- return join(projectRoot, SDD_DIR);
232
- }
233
- function configPath(projectRoot) {
234
- return join(sddRoot(projectRoot), "config.yaml");
235
- }
236
- function workflowsDir(projectRoot) {
237
- return join(sddRoot(projectRoot), "workflows");
238
- }
239
- function templatesDir(projectRoot) {
240
- return join(sddRoot(projectRoot), "templates");
241
- }
242
- function memoryDir(projectRoot, config) {
243
- return join(projectRoot, config.memory_path);
244
- }
245
- function changesDir(projectRoot, config) {
246
- return join(projectRoot, config.changes_path);
247
- }
248
- function archiveDir(projectRoot, config) {
249
- return join(projectRoot, config.archive_path);
250
- }
251
- function domainsDir(projectRoot, config) {
252
- return join(projectRoot, config.domains_path);
253
- }
254
- function changePath(projectRoot, config, changeId) {
255
- return join(changesDir(projectRoot, config), changeId);
256
- }
257
- function changeMetaPath(projectRoot, config, changeId) {
258
- return join(changePath(projectRoot, config, changeId), "meta.yaml");
259
- }
260
- function activePointerPath(projectRoot, config) {
261
- return join(changesDir(projectRoot, config), ".active");
262
- }
1
+ import {
2
+ ChangeMetaSchema,
3
+ WorkflowSchema,
4
+ loadConfig
5
+ } from "./chunk-KZHNYGD6.js";
6
+ import {
7
+ activePointerPath,
8
+ changeMetaPath,
9
+ changePath,
10
+ changesDir,
11
+ sddRoot,
12
+ workflowsDir
13
+ } from "./chunk-7MJYPJMR.js";
14
+ import {
15
+ ensureDir,
16
+ listDirs,
17
+ listFiles,
18
+ pathExists,
19
+ readText,
20
+ readYaml,
21
+ removePath,
22
+ writeText,
23
+ writeYaml
24
+ } from "./chunk-EPFURATC.js";
263
25
 
264
- // src/config.ts
265
- async function isInitialized(projectRoot) {
266
- return pathExists(configPath(projectRoot));
267
- }
268
- async function loadConfig(projectRoot) {
269
- const path = configPath(projectRoot);
270
- if (!await pathExists(path)) {
271
- throw new Error(
272
- `SDD is not initialized in ${projectRoot}. Run \`sdd init\` first.`
273
- );
274
- }
275
- const raw = await readYaml(path);
276
- return ConfigSchema.parse(raw);
277
- }
278
- async function saveConfig(projectRoot, config) {
279
- await ensureDir(sddRoot(projectRoot));
280
- await writeYaml(configPath(projectRoot), ConfigSchema.parse(config));
281
- }
282
- function defaultConfig() {
283
- return ConfigSchema.parse({});
284
- }
26
+ // src/agents.ts
27
+ import { join as join3 } from "pathe";
285
28
 
286
29
  // src/slug.ts
287
30
  function slugify(input) {
@@ -305,7 +48,7 @@ function uniqueChangeDirName(base, taken) {
305
48
  }
306
49
 
307
50
  // src/workflow.ts
308
- import { join as join2 } from "pathe";
51
+ import { join } from "pathe";
309
52
  async function listWorkflowNames(projectRoot) {
310
53
  const dir = workflowsDir(projectRoot);
311
54
  const files = await listFiles(dir);
@@ -315,7 +58,7 @@ async function loadWorkflow(projectRoot, name) {
315
58
  const dir = workflowsDir(projectRoot);
316
59
  const candidates = [`${name}.yaml`, `${name}.yml`];
317
60
  for (const file of candidates) {
318
- const path = join2(dir, file);
61
+ const path = join(dir, file);
319
62
  if (await pathExists(path)) {
320
63
  const raw = await readYaml(path);
321
64
  const workflow = WorkflowSchema.parse(raw);
@@ -553,7 +296,7 @@ async function resolveChangeId(projectRoot, config, changeId) {
553
296
  }
554
297
 
555
298
  // src/agent-handoff.ts
556
- import { join as join3 } from "pathe";
299
+ import { join as join2 } from "pathe";
557
300
  function formatStatus(ctx) {
558
301
  const lines = [];
559
302
  lines.push(`Change: ${ctx.meta.title}`);
@@ -621,12 +364,12 @@ async function buildAgentPrompt(ctx, config, projectRoot) {
621
364
  parts.push(`- Prefer small, reviewable commits.`);
622
365
  parts.push("");
623
366
  parts.push(`## Artifacts to read`);
624
- const memoryHint = join3(projectRoot, config.memory_path);
367
+ const memoryHint = join2(projectRoot, config.memory_path);
625
368
  parts.push(`- Documentation map (if present): ${memoryHint}/index.md`);
626
369
  parts.push(`- Memory: ${memoryHint}/`);
627
370
  for (const s of ctx.stages) {
628
371
  for (const a of s.artifacts) {
629
- parts.push(`- ${join3(ctx.path, a.path)}`);
372
+ parts.push(`- ${join2(ctx.path, a.path)}`);
630
373
  }
631
374
  }
632
375
  parts.push("");
@@ -637,6 +380,27 @@ async function buildAgentPrompt(ctx, config, projectRoot) {
637
380
  }
638
381
  }
639
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
+ }
640
404
  parts.push(
641
405
  `When done with this stage, the human will run \`sdd next\` or \`sdd verify\` / \`sdd complete\`.`
642
406
  );
@@ -732,7 +496,7 @@ async function removeOtherAgentHosts(projectRoot, keep) {
732
496
  for (const integ of AGENT_INTEGRATIONS) {
733
497
  if (integ.id === keep) continue;
734
498
  for (const rel of agentHostPaths(integ.id)) {
735
- const full = join4(projectRoot, rel);
499
+ const full = join3(projectRoot, rel);
736
500
  if (await pathExists(full)) {
737
501
  await removePath(full);
738
502
  removed.push(rel);
@@ -740,7 +504,7 @@ async function removeOtherAgentHosts(projectRoot, keep) {
740
504
  }
741
505
  }
742
506
  for (const rel of LEGACY_AGENT_PATHS) {
743
- const full = join4(projectRoot, rel);
507
+ const full = join3(projectRoot, rel);
744
508
  if (await pathExists(full)) {
745
509
  await removePath(full);
746
510
  removed.push(rel);
@@ -748,7 +512,7 @@ async function removeOtherAgentHosts(projectRoot, keep) {
748
512
  }
749
513
  for (const parent of [".claude", ".idea", ".grok"]) {
750
514
  if (keep === "grok" && parent === ".grok") continue;
751
- const full = join4(projectRoot, parent);
515
+ const full = join3(projectRoot, parent);
752
516
  if (!await pathExists(full)) continue;
753
517
  try {
754
518
  const { readdir } = await import("fs/promises");
@@ -803,7 +567,8 @@ var SDD_AGENT_ROLES = [
803
567
  roleRules: [
804
568
  "If stage is intent/feature/design/tasks/stories/research: act as planner (artifacts, little or no product code).",
805
569
  "If stage is implement: act as implementer (scoped code only).",
806
- "If stage is local_verify: act as reviewer (gaps vs acceptance; fixes only)."
570
+ "If stage is local_verify: act as reviewer (gaps vs acceptance; fixes only).",
571
+ "If kickoff/brief says refine or analyze: follow changes/<id>/refine-brief.md \u2014 stage-scoped refine; never edit constitution; do not sdd next."
807
572
  ]
808
573
  },
809
574
  {
@@ -842,7 +607,7 @@ async function installAgentIntegration(opts) {
842
607
  const force = opts.force ?? false;
843
608
  await removeOtherAgentHosts(root, opts.target);
844
609
  const write = async (rel, content) => {
845
- const full = join4(root, rel);
610
+ const full = join3(root, rel);
846
611
  if (await pathExists(full) && !force) {
847
612
  skipped.push(rel);
848
613
  return;
@@ -850,7 +615,7 @@ async function installAgentIntegration(opts) {
850
615
  await writeText(full, content);
851
616
  created.push(rel);
852
617
  };
853
- await write(join4(".sdd", "protocol.md"), PROTOCOL_MD);
618
+ await write(join3(".sdd", "protocol.md"), PROTOCOL_MD);
854
619
  await refreshActiveAgentContext(root);
855
620
  const roles = integ.rolesToInstall?.length ? SDD_AGENT_ROLES.filter((r) => integ.rolesToInstall.includes(r.id)) : SDD_AGENT_ROLES;
856
621
  for (const role of roles) {
@@ -858,7 +623,7 @@ async function installAgentIntegration(opts) {
858
623
  }
859
624
  await write("AGENTS.md", renderAgentsMd(integ));
860
625
  await write(
861
- join4(".sdd", "agents.json"),
626
+ join3(".sdd", "agents.json"),
862
627
  JSON.stringify(
863
628
  {
864
629
  version: 3,
@@ -926,7 +691,7 @@ Do not claim the change is complete without local verification when the workflow
926
691
  `;
927
692
  }
928
693
  async function loadInstalledAgent(projectRoot) {
929
- const path = join4(sddRoot(projectRoot), "agents.json");
694
+ const path = join3(sddRoot(projectRoot), "agents.json");
930
695
  if (!await pathExists(path)) return null;
931
696
  try {
932
697
  const raw = JSON.parse(await readText(path));
@@ -941,7 +706,7 @@ async function loadInstalledAgent(projectRoot) {
941
706
  async function writeAgentHandoff(projectRoot, config, changeId) {
942
707
  const ctx = await buildContext(projectRoot, config, changeId);
943
708
  const body = await buildAgentPrompt(ctx, config, projectRoot);
944
- const outPath = join4(sddRoot(projectRoot), "handoff.md");
709
+ const outPath = join3(sddRoot(projectRoot), "handoff.md");
945
710
  const header = `# SDD handoff (auto)
946
711
 
947
712
  > Generated for the AI coding agent configured at init.
@@ -957,6 +722,15 @@ Also read: \`.sdd/active-context.md\`, \`.sdd/protocol.md\`, \`memory/constituti
957
722
  }
958
723
  function agentKickoffMessage(opts) {
959
724
  const event = opts.event ?? "context updated";
725
+ if (opts.instructions?.trim()) {
726
+ return [
727
+ `SDD: ${event}.`,
728
+ `Title: ${opts.title}`,
729
+ `Change: ${opts.changeId}`,
730
+ `Stage: ${opts.stage}`,
731
+ opts.instructions.trim()
732
+ ].join(" ");
733
+ }
960
734
  return [
961
735
  `SDD: ${event}.`,
962
736
  `Title: ${opts.title}`,
@@ -969,16 +743,16 @@ function agentKickoffMessage(opts) {
969
743
  }
970
744
  async function refreshActiveAgentContext(projectRoot) {
971
745
  const markerDir = sddRoot(projectRoot);
972
- if (!await pathExists(join4(markerDir, "config.yaml"))) {
746
+ if (!await pathExists(join3(markerDir, "config.yaml"))) {
973
747
  return null;
974
748
  }
975
- const protocolPath = join4(markerDir, "protocol.md");
749
+ const protocolPath = join3(markerDir, "protocol.md");
976
750
  if (!await pathExists(protocolPath)) {
977
751
  await writeText(protocolPath, PROTOCOL_MD);
978
752
  }
979
753
  const config = await loadConfig(projectRoot);
980
754
  const activeId = await getActiveChangeId(projectRoot, config);
981
- const outPath = join4(markerDir, "active-context.md");
755
+ const outPath = join3(markerDir, "active-context.md");
982
756
  if (!activeId) {
983
757
  const empty = `# SDD active context
984
758
 
@@ -1101,41 +875,6 @@ Refresh context: \`sdd agents refresh\`.
1101
875
  }
1102
876
 
1103
877
  export {
1104
- GateSchema,
1105
- ArtifactDefSchema,
1106
- VerifyCommandSchema,
1107
- StageSchema,
1108
- WorkflowSchema,
1109
- ConfigSchema,
1110
- ChangeMetaSchema,
1111
- SDD_DIR,
1112
- sddRoot,
1113
- configPath,
1114
- workflowsDir,
1115
- templatesDir,
1116
- memoryDir,
1117
- changesDir,
1118
- archiveDir,
1119
- domainsDir,
1120
- changePath,
1121
- changeMetaPath,
1122
- activePointerPath,
1123
- pathExists,
1124
- ensureDir,
1125
- readText,
1126
- writeText,
1127
- readYaml,
1128
- writeYaml,
1129
- listDirs,
1130
- listFiles,
1131
- copyDir,
1132
- copyDirSkipExisting,
1133
- writeTextIfMissing,
1134
- removePath,
1135
- isInitialized,
1136
- loadConfig,
1137
- saveConfig,
1138
- defaultConfig,
1139
878
  listWorkflowNames,
1140
879
  loadWorkflow,
1141
880
  resolveStages,
@@ -0,0 +1,14 @@
1
+ import {
2
+ defaultConfig,
3
+ isInitialized,
4
+ loadConfig,
5
+ saveConfig
6
+ } from "./chunk-KZHNYGD6.js";
7
+ import "./chunk-7MJYPJMR.js";
8
+ import "./chunk-EPFURATC.js";
9
+ export {
10
+ defaultConfig,
11
+ isInitialized,
12
+ loadConfig,
13
+ saveConfig
14
+ };
@@ -0,0 +1,28 @@
1
+ import {
2
+ copyDir,
3
+ copyDirSkipExisting,
4
+ ensureDir,
5
+ listDirs,
6
+ listFiles,
7
+ pathExists,
8
+ readText,
9
+ readYaml,
10
+ removePath,
11
+ writeText,
12
+ writeTextIfMissing,
13
+ writeYaml
14
+ } from "./chunk-EPFURATC.js";
15
+ export {
16
+ copyDir,
17
+ copyDirSkipExisting,
18
+ ensureDir,
19
+ listDirs,
20
+ listFiles,
21
+ pathExists,
22
+ readText,
23
+ readYaml,
24
+ removePath,
25
+ writeText,
26
+ writeTextIfMissing,
27
+ writeYaml
28
+ };