@applica-software-guru/sdd-core 1.0.0 → 1.3.3

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.
Files changed (58) hide show
  1. package/dist/agent/agent-defaults.d.ts +3 -0
  2. package/dist/agent/agent-defaults.d.ts.map +1 -0
  3. package/dist/agent/agent-defaults.js +13 -0
  4. package/dist/agent/agent-defaults.js.map +1 -0
  5. package/dist/agent/agent-runner.d.ts +9 -0
  6. package/dist/agent/agent-runner.d.ts.map +1 -0
  7. package/dist/agent/agent-runner.js +43 -0
  8. package/dist/agent/agent-runner.js.map +1 -0
  9. package/dist/index.d.ts +10 -5
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +11 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/prompt/apply-prompt-generator.d.ts +3 -0
  14. package/dist/prompt/apply-prompt-generator.d.ts.map +1 -0
  15. package/dist/prompt/apply-prompt-generator.js +48 -0
  16. package/dist/prompt/apply-prompt-generator.js.map +1 -0
  17. package/dist/prompt/prompt-generator.d.ts.map +1 -1
  18. package/dist/prompt/prompt-generator.js +7 -11
  19. package/dist/prompt/prompt-generator.js.map +1 -1
  20. package/dist/scaffold/init.d.ts +1 -1
  21. package/dist/scaffold/init.d.ts.map +1 -1
  22. package/dist/scaffold/init.js +10 -29
  23. package/dist/scaffold/init.js.map +1 -1
  24. package/dist/scaffold/skill-adapters.d.ts +39 -0
  25. package/dist/scaffold/skill-adapters.d.ts.map +1 -0
  26. package/dist/scaffold/skill-adapters.js +224 -0
  27. package/dist/scaffold/skill-adapters.js.map +1 -0
  28. package/dist/scaffold/templates.d.ts +5 -1
  29. package/dist/scaffold/templates.d.ts.map +1 -1
  30. package/dist/scaffold/templates.js +203 -55
  31. package/dist/scaffold/templates.js.map +1 -1
  32. package/dist/sdd.d.ts +7 -3
  33. package/dist/sdd.d.ts.map +1 -1
  34. package/dist/sdd.js +36 -18
  35. package/dist/sdd.js.map +1 -1
  36. package/dist/types.d.ts +2 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/agent/agent-defaults.ts +12 -0
  40. package/src/agent/agent-runner.ts +54 -0
  41. package/src/index.ts +17 -5
  42. package/src/prompt/apply-prompt-generator.ts +58 -0
  43. package/src/prompt/prompt-generator.ts +8 -18
  44. package/src/scaffold/init.ts +17 -38
  45. package/src/scaffold/skill-adapters.ts +322 -0
  46. package/src/scaffold/templates.ts +207 -54
  47. package/src/sdd.ts +57 -31
  48. package/src/types.ts +2 -0
  49. package/tests/apply.test.ts +119 -0
  50. package/tests/integration.test.ts +94 -51
  51. package/dist/delta/hasher.d.ts +0 -2
  52. package/dist/delta/hasher.d.ts.map +0 -1
  53. package/dist/delta/hasher.js +0 -8
  54. package/dist/delta/hasher.js.map +0 -1
  55. package/dist/lock/lock-manager.d.ts +0 -6
  56. package/dist/lock/lock-manager.d.ts.map +0 -1
  57. package/dist/lock/lock-manager.js +0 -39
  58. package/dist/lock/lock-manager.js.map +0 -1
@@ -17,13 +17,30 @@ export interface ProjectInfo {
17
17
  description: string;
18
18
  }
19
19
 
20
- export const AGENT_MD_TEMPLATE = `# SDD Project
20
+ export const SKILL_MD_TEMPLATE = `---
21
+ name: sdd
22
+ description: >
23
+ Story Driven Development workflow. Use when working in a project
24
+ with .sdd/config.yaml, or when the user mentions SDD, sdd sync,
25
+ story driven development, or spec-driven development.
26
+ license: MIT
27
+ compatibility: Requires sdd CLI (npm i -g @applica-software-guru/sdd)
28
+ allowed-tools: Bash(sdd:*) Read Glob Grep
29
+ metadata:
30
+ author: applica-software-guru
31
+ version: "1.0"
32
+ ---
33
+
34
+ # SDD — Story Driven Development
21
35
 
22
- This project uses **Story Driven Development (SDD)**.
23
- Documentation drives implementation: read the docs first, then write code.
36
+ ## Detection
37
+
38
+ This project uses SDD if \`.sdd/config.yaml\` exists in the project root.
24
39
 
25
40
  ## Workflow
26
41
 
42
+ Follow this loop every time you work on an SDD project:
43
+
27
44
  1. Run \`sdd bug open\` — check if there are open bugs to fix first
28
45
  2. If there are open bugs, fix the code/docs, then run \`sdd mark-bug-resolved\`
29
46
  3. Run \`sdd cr pending\` — check if there are change requests to process
@@ -80,7 +97,142 @@ Delete the related code in \`code/\`, then run \`sdd mark-synced <file>\` (the d
80
97
  6. Respect all constraints in \`## Agent Notes\` sections (if present)
81
98
  7. Do not edit files inside \`.sdd/\` manually
82
99
 
83
- ## File format
100
+ ## Project structure
101
+
102
+ - \`product/\` — What to build (vision, users, features)
103
+ - \`system/\` — How to build it (entities, architecture, tech stack, interfaces)
104
+ - \`code/\` — All generated source code goes here
105
+ - \`change-requests/\` — Change requests to the documentation
106
+ - \`bugs/\` — Bug reports
107
+ - \`.sdd/\` — Project config and sync state (do not edit)
108
+
109
+ ## References
110
+
111
+ For detailed information on specific topics, see:
112
+
113
+ - [File format and status lifecycle](references/file-format.md)
114
+ - [Change Requests workflow](references/change-requests.md)
115
+ - [Bug workflow](references/bugs.md)
116
+ `;
117
+
118
+ export const SKILL_UI_MD_TEMPLATE = `---
119
+ name: sdd-ui
120
+ description: >
121
+ UI Component Editor workflow. Use when the user wants to implement a React component
122
+ from a screenshot in an SDD project, iterating visually with live preview.
123
+ license: MIT
124
+ compatibility: >
125
+ Requires sdd CLI (npm i -g @applica-software-guru/sdd).
126
+ Requires Playwright MCP configured in Claude Code
127
+ (e.g. @playwright/mcp or @executeautomation/playwright-mcp).
128
+ allowed-tools: Bash(sdd:*) Read Glob Grep Edit Write mcp__playwright__screenshot mcp__playwright__navigate mcp__playwright__click
129
+ metadata:
130
+ author: applica-software-guru
131
+ version: "1.1"
132
+ ---
133
+
134
+ # SDD UI — Visual Component Editor
135
+
136
+ ## Purpose
137
+
138
+ Use this workflow when implementing a React component from a screenshot reference in an SDD project.
139
+ The split-panel editor shows the spec screenshot on the left and the live component on the right,
140
+ so you can iterate visually until they match.
141
+
142
+ ## Prerequisites
143
+
144
+ - \`sdd\` CLI installed globally
145
+ - Playwright MCP configured in Claude Code settings
146
+ - e.g. \`@playwright/mcp\` or \`@executeautomation/playwright-mcp\`
147
+ - If not configured, inform the user and stop — visual feedback won't work without it
148
+
149
+ ## Workflow
150
+
151
+ ### Step 1 — Read the spec
152
+
153
+ Read the SDD feature file to understand what the component should look like and do.
154
+ Look for any screenshot paths referenced in the feature doc.
155
+
156
+ ### Step 2 — Launch the editor
157
+
158
+ \`\`\`bash
159
+ # Single screenshot — detached (recommended when run by an agent)
160
+ sdd ui launch-editor LoginForm \\
161
+ --screenshot product/features/auth/login.png \\
162
+ --detach
163
+
164
+ # Multiple screenshots (e.g. desktop + mobile)
165
+ sdd ui launch-editor LoginForm \\
166
+ --screenshot product/features/auth/login-desktop.png \\
167
+ --screenshot product/features/auth/login-mobile.png \\
168
+ --detach
169
+ \`\`\`
170
+
171
+ The command will:
172
+ - Scaffold \`code/components/LoginForm.tsx\` if it doesn't exist
173
+ - Print the exact component file path to edit
174
+ - Start the editor at \`http://localhost:5174\`
175
+
176
+ With \`--detach\` the process runs in background and the terminal is immediately free.
177
+ Without \`--detach\` it runs in foreground (use Ctrl+C to stop).
178
+
179
+ With multiple screenshots, the left panel shows a tab per screenshot.
180
+ With a single screenshot, no tab bar is shown.
181
+
182
+ ### Step 3 — Implement the component
183
+
184
+ Edit the file printed by \`sdd ui launch-editor\` (e.g. \`code/components/LoginForm.tsx\`).
185
+
186
+ Write a React component that matches the screenshot. Use standard HTML/CSS or inline styles —
187
+ no external UI library unless the project already uses one.
188
+
189
+ Vite HMR will update the right panel automatically on every save.
190
+
191
+ ### Step 4 — Visual check with Playwright
192
+
193
+ After each save, screenshot the live preview and compare it with the spec:
194
+
195
+ \`\`\`
196
+ mcp__playwright__navigate http://localhost:5174
197
+ mcp__playwright__screenshot
198
+ \`\`\`
199
+
200
+ The left panel already shows the spec screenshot for direct comparison.
201
+ Note differences in layout, spacing, typography, colors, and component structure.
202
+
203
+ ### Step 5 — Iterate
204
+
205
+ Edit component → Playwright screenshot → compare → repeat until the preview matches the spec.
206
+
207
+ ### Step 6 — Finalize
208
+
209
+ \`\`\`bash
210
+ sdd ui stop
211
+ sdd mark-synced product/features/auth/login.md
212
+ git add -A && git commit -m "sdd sync: implement LoginForm component"
213
+ \`\`\`
214
+
215
+ ## Notes
216
+
217
+ - The component file is permanent — it lives in \`code/components/\` and is part of your project
218
+ - Port \`5174\` by default (not \`5173\`) to avoid conflicts with the user's app dev server
219
+ - If the component needs props, scaffold it with hardcoded sample data for the preview
220
+
221
+ ## Troubleshooting
222
+
223
+ **Playwright MCP not configured:**
224
+ Stop and ask the user to add it to their Claude Code MCP settings before continuing.
225
+
226
+ **Component import fails in preview:**
227
+ Check that the component file has a valid default export and no TypeScript errors.
228
+
229
+ **Port already in use:**
230
+ \`sdd ui launch-editor LoginForm --screenshot login.png --port 5175\`
231
+ `;
232
+
233
+ export const FILE_FORMAT_REFERENCE = `# File Format and Status Lifecycle
234
+
235
+ ## YAML Frontmatter
84
236
 
85
237
  Every \`.md\` file in \`product/\` and \`system/\` must start with this YAML frontmatter:
86
238
 
@@ -94,13 +246,20 @@ version: "1.0"
94
246
  ---
95
247
  \`\`\`
96
248
 
97
- - **status**: one of:
98
- - \`new\` — new file, needs to be implemented
99
- - \`changed\`modified since last sync, code needs updating
100
- - \`deleted\`feature to be removed, agent should delete related code
101
- - \`synced\`already implemented, up to date
102
- - **version**: patch-bump on each edit (1.0 → 1.1 → 1.2)
103
- - **last-modified**: ISO 8601 datetime, updated on each edit
249
+ ## Status values
250
+
251
+ - **\`new\`**new file, needs to be implemented
252
+ - **\`changed\`**modified since last sync, code needs updating
253
+ - **\`deleted\`**feature to be removed, agent should delete related code
254
+ - **\`synced\`** already implemented, up to date
255
+
256
+ ## Version
257
+
258
+ Patch-bump on each edit: 1.0 → 1.1 → 1.2
259
+
260
+ ## Last-modified
261
+
262
+ ISO 8601 datetime, updated on each edit.
104
263
 
105
264
  ## How sync works
106
265
 
@@ -112,11 +271,37 @@ version: "1.0"
112
271
 
113
272
  This is why **committing after every mark-synced is mandatory** — the git history is what SDD uses to detect changes.
114
273
 
115
- ## Change Requests
274
+ ## UX and screenshots
275
+
276
+ When a feature has UX mockups or screenshots, place them next to the feature doc:
277
+
278
+ - **Simple feature** (no screenshots): \`product/features/auth.md\`
279
+ - **Feature with screenshots**: use a folder with \`index.md\`:
280
+
281
+ \`\`\`
282
+ product/features/auth/
283
+ index.md ← feature doc
284
+ login.png ← screenshot
285
+ register.png ← screenshot
286
+ \`\`\`
287
+
288
+ Reference images in the markdown with relative paths:
289
+
290
+ \`\`\`markdown
291
+ ## UX
292
+
293
+ ![Login screen](login.png)
294
+ ![Register screen](register.png)
295
+ \`\`\`
296
+
297
+ Both formats work — use a folder only when you have screenshots or multiple files for a feature.
298
+ `;
299
+
300
+ export const CHANGE_REQUESTS_REFERENCE = `# Change Requests
116
301
 
117
302
  Change Requests (CRs) are markdown files in \`change-requests/\` that describe modifications to the documentation.
118
303
 
119
- ### CR format
304
+ ## CR format
120
305
 
121
306
  \`\`\`yaml
122
307
  ---
@@ -129,24 +314,25 @@ created-at: "2025-01-01T00:00:00.000Z"
129
314
 
130
315
  - **status**: \`draft\` (pending) or \`applied\` (already processed)
131
316
 
132
- ### CR workflow
317
+ ## CR workflow
133
318
 
134
319
  1. Check for pending CRs: \`sdd cr pending\`
135
320
  2. Read each pending CR and apply the described changes to the documentation files (marking them as \`new\`, \`changed\`, or \`deleted\`)
136
321
  3. After applying a CR to the docs, mark it: \`sdd mark-cr-applied change-requests/CR-001.md\`
137
322
  4. Then run \`sdd sync\` to implement the code changes
138
323
 
139
- ### CR commands
324
+ ## CR commands
140
325
 
141
326
  - \`sdd cr list\` — See all change requests and their status
142
327
  - \`sdd cr pending\` — Show only draft CRs to process
143
328
  - \`sdd mark-cr-applied [files...]\` — Mark CRs as applied after updating the docs
329
+ `;
144
330
 
145
- ## Bugs
331
+ export const BUGS_REFERENCE = `# Bugs
146
332
 
147
333
  Bugs are markdown files in \`bugs/\` that describe problems found in the codebase.
148
334
 
149
- ### Bug format
335
+ ## Bug format
150
336
 
151
337
  \`\`\`yaml
152
338
  ---
@@ -159,54 +345,21 @@ created-at: "2025-01-01T00:00:00.000Z"
159
345
 
160
346
  - **status**: \`open\` (needs fixing) or \`resolved\` (already fixed)
161
347
 
162
- ### Bug workflow
348
+ ## Bug workflow
163
349
 
164
350
  1. Check for open bugs: \`sdd bug open\`
165
351
  2. Read each open bug and fix the code and/or documentation
166
352
  3. After fixing a bug, mark it: \`sdd mark-bug-resolved bugs/BUG-001.md\`
167
353
  4. Commit the fix
168
354
 
169
- ### Bug commands
355
+ ## Bug commands
170
356
 
171
357
  - \`sdd bug list\` — See all bugs and their status
172
358
  - \`sdd bug open\` — Show only open bugs to fix
173
359
  - \`sdd mark-bug-resolved [files...]\` — Mark bugs as resolved after fixing
174
-
175
- ## UX and screenshots
176
-
177
- When a feature has UX mockups or screenshots, place them next to the feature doc:
178
-
179
- - **Simple feature** (no screenshots): \`product/features/auth.md\`
180
- - **Feature with screenshots**: use a folder with \`index.md\`:
181
-
182
- \`\`\`
183
- product/features/auth/
184
- index.md ← feature doc
185
- login.png ← screenshot
186
- register.png ← screenshot
187
- \`\`\`
188
-
189
- Reference images in the markdown with relative paths:
190
-
191
- \`\`\`markdown
192
- ## UX
193
-
194
- ![Login screen](login.png)
195
- ![Register screen](register.png)
196
- \`\`\`
197
-
198
- Both formats work — use a folder only when you have screenshots or multiple files for a feature.
199
-
200
- ## Project structure
201
-
202
- - \`product/\` — What to build (vision, users, features)
203
- - \`system/\` — How to build it (entities, architecture, tech stack, interfaces)
204
- - \`code/\` — All generated source code goes here
205
- - \`change-requests/\` — Change requests to the documentation
206
- - \`bugs/\` — Bug reports
207
- - \`.sdd/\` — Project config and sync state (do not edit)
208
360
  `;
209
361
 
210
- export const EMPTY_LOCK_TEMPLATE = () => `synced-at: "${new Date().toISOString()}"
362
+ export const EMPTY_LOCK_TEMPLATE =
363
+ () => `synced-at: "${new Date().toISOString()}"
211
364
  files: {}
212
365
  `;
package/src/sdd.ts CHANGED
@@ -1,15 +1,22 @@
1
- import { readFile, writeFile } from 'node:fs/promises';
2
- import { resolve } from 'node:path';
3
- import type { StoryStatus, ValidationResult, SDDConfig, ChangeRequest, Bug } from './types.js';
4
- import { ProjectNotInitializedError } from './errors.js';
5
- import { parseAllStoryFiles } from './parser/story-parser.js';
6
- import { generatePrompt } from './prompt/prompt-generator.js';
7
- import { validate } from './validate/validator.js';
8
- import { initProject } from './scaffold/init.js';
9
- import { isSDDProject, readConfig, writeConfig } from './config/config-manager.js';
10
- import { parseAllCRFiles } from './parser/cr-parser.js';
11
- import { parseAllBugFiles } from './parser/bug-parser.js';
12
- import type { ProjectInfo } from './scaffold/templates.js';
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import type { StoryStatus, ValidationResult, SDDConfig, ChangeRequest, Bug } from "./types.js";
4
+ import { ProjectNotInitializedError } from "./errors.js";
5
+ import { parseAllStoryFiles } from "./parser/story-parser.js";
6
+ import { generatePrompt } from "./prompt/prompt-generator.js";
7
+ import { generateApplyPrompt } from "./prompt/apply-prompt-generator.js";
8
+ import { validate } from "./validate/validator.js";
9
+ import { initProject } from "./scaffold/init.js";
10
+ import { isSDDProject, readConfig, writeConfig } from "./config/config-manager.js";
11
+ import { parseAllCRFiles } from "./parser/cr-parser.js";
12
+ import { parseAllBugFiles } from "./parser/bug-parser.js";
13
+ import type { ProjectInfo } from "./scaffold/templates.js";
14
+ import {
15
+ listSupportedAdapters,
16
+ syncSkillAdapters,
17
+ type SyncAdaptersOptions,
18
+ type SyncAdaptersResult,
19
+ } from "./scaffold/skill-adapters.js";
13
20
 
14
21
  export class SDD {
15
22
  private root: string;
@@ -22,6 +29,15 @@ export class SDD {
22
29
  return initProject(this.root, info);
23
30
  }
24
31
 
32
+ async syncAdapters(options?: SyncAdaptersOptions): Promise<SyncAdaptersResult> {
33
+ this.ensureInitialized();
34
+ return syncSkillAdapters(this.root, options);
35
+ }
36
+
37
+ supportedAdapters(): string[] {
38
+ return listSupportedAdapters();
39
+ }
40
+
25
41
  async config(): Promise<SDDConfig> {
26
42
  this.ensureInitialized();
27
43
  return readConfig(this.root);
@@ -36,15 +52,15 @@ export class SDD {
36
52
  relativePath: f.relativePath,
37
53
  status: f.frontmatter.status,
38
54
  version: f.frontmatter.version,
39
- lastModified: f.frontmatter['last-modified'],
55
+ lastModified: f.frontmatter["last-modified"],
40
56
  })),
41
57
  };
42
58
  }
43
59
 
44
- async pending(): Promise<import('./types.js').StoryFile[]> {
60
+ async pending(): Promise<import("./types.js").StoryFile[]> {
45
61
  this.ensureInitialized();
46
62
  const files = await parseAllStoryFiles(this.root);
47
- return files.filter((f) => f.frontmatter.status !== 'synced');
63
+ return files.filter((f) => f.frontmatter.status !== "synced");
48
64
  }
49
65
 
50
66
  async sync(): Promise<string> {
@@ -52,6 +68,16 @@ export class SDD {
52
68
  return generatePrompt(pending, this.root);
53
69
  }
54
70
 
71
+ async applyPrompt(): Promise<string | null> {
72
+ this.ensureInitialized();
73
+ const [bugs, changeRequests, pendingFiles] = await Promise.all([
74
+ this.openBugs(),
75
+ this.pendingChangeRequests(),
76
+ this.pending(),
77
+ ]);
78
+ return generateApplyPrompt(bugs, changeRequests, pendingFiles, this.root);
79
+ }
80
+
55
81
  async validate(): Promise<ValidationResult> {
56
82
  this.ensureInitialized();
57
83
  const files = await parseAllStoryFiles(this.root);
@@ -65,21 +91,21 @@ export class SDD {
65
91
 
66
92
  for (const file of files) {
67
93
  const { status } = file.frontmatter;
68
- if (status === 'synced') continue;
94
+ if (status === "synced") continue;
69
95
  if (paths && paths.length > 0 && !paths.includes(file.relativePath)) continue;
70
96
 
71
97
  const absPath = resolve(this.root, file.relativePath);
72
98
 
73
- if (status === 'deleted') {
99
+ if (status === "deleted") {
74
100
  // File marked for deletion — remove it
75
- const { unlink } = await import('node:fs/promises');
101
+ const { unlink } = await import("node:fs/promises");
76
102
  await unlink(absPath);
77
103
  marked.push(`${file.relativePath} (removed)`);
78
104
  } else {
79
105
  // new or changed → synced
80
- const content = await readFile(absPath, 'utf-8');
81
- const updated = content.replace(/^status:\s*(new|changed)/m, 'status: synced');
82
- await writeFile(absPath, updated, 'utf-8');
106
+ const content = await readFile(absPath, "utf-8");
107
+ const updated = content.replace(/^status:\s*(new|changed)/m, "status: synced");
108
+ await writeFile(absPath, updated, "utf-8");
83
109
  marked.push(file.relativePath);
84
110
  }
85
111
  }
@@ -94,7 +120,7 @@ export class SDD {
94
120
 
95
121
  async pendingChangeRequests(): Promise<ChangeRequest[]> {
96
122
  const all = await this.changeRequests();
97
- return all.filter((cr) => cr.frontmatter.status === 'draft');
123
+ return all.filter((cr) => cr.frontmatter.status === "draft");
98
124
  }
99
125
 
100
126
  async markCRApplied(paths?: string[]): Promise<string[]> {
@@ -103,13 +129,13 @@ export class SDD {
103
129
  const marked: string[] = [];
104
130
 
105
131
  for (const cr of all) {
106
- if (cr.frontmatter.status === 'applied') continue;
132
+ if (cr.frontmatter.status === "applied") continue;
107
133
  if (paths && paths.length > 0 && !paths.includes(cr.relativePath)) continue;
108
134
 
109
135
  const absPath = resolve(this.root, cr.relativePath);
110
- const content = await readFile(absPath, 'utf-8');
111
- const updated = content.replace(/^status:\s*draft/m, 'status: applied');
112
- await writeFile(absPath, updated, 'utf-8');
136
+ const content = await readFile(absPath, "utf-8");
137
+ const updated = content.replace(/^status:\s*draft/m, "status: applied");
138
+ await writeFile(absPath, updated, "utf-8");
113
139
  marked.push(cr.relativePath);
114
140
  }
115
141
 
@@ -123,7 +149,7 @@ export class SDD {
123
149
 
124
150
  async openBugs(): Promise<Bug[]> {
125
151
  const all = await this.bugs();
126
- return all.filter((b) => b.frontmatter.status === 'open');
152
+ return all.filter((b) => b.frontmatter.status === "open");
127
153
  }
128
154
 
129
155
  async markBugResolved(paths?: string[]): Promise<string[]> {
@@ -132,13 +158,13 @@ export class SDD {
132
158
  const marked: string[] = [];
133
159
 
134
160
  for (const bug of all) {
135
- if (bug.frontmatter.status === 'resolved') continue;
161
+ if (bug.frontmatter.status === "resolved") continue;
136
162
  if (paths && paths.length > 0 && !paths.includes(bug.relativePath)) continue;
137
163
 
138
164
  const absPath = resolve(this.root, bug.relativePath);
139
- const content = await readFile(absPath, 'utf-8');
140
- const updated = content.replace(/^status:\s*open/m, 'status: resolved');
141
- await writeFile(absPath, updated, 'utf-8');
165
+ const content = await readFile(absPath, "utf-8");
166
+ const updated = content.replace(/^status:\s*open/m, "status: resolved");
167
+ await writeFile(absPath, updated, "utf-8");
142
168
  marked.push(bug.relativePath);
143
169
  }
144
170
 
package/src/types.ts CHANGED
@@ -58,6 +58,8 @@ export interface StoryStatus {
58
58
  export interface SDDConfig {
59
59
  description: string;
60
60
  'last-sync-commit'?: string;
61
+ agent?: string;
62
+ agents?: Record<string, string>;
61
63
  }
62
64
 
63
65
  export type ChangeRequestStatus = 'draft' | 'applied';
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateApplyPrompt } from '../src/prompt/apply-prompt-generator.js';
3
+ import { resolveAgentCommand, DEFAULT_AGENTS } from '../src/agent/agent-defaults.js';
4
+ import type { Bug, ChangeRequest, StoryFile } from '../src/types.js';
5
+
6
+ function makeBug(overrides: Partial<Bug> = {}): Bug {
7
+ return {
8
+ relativePath: 'bugs/BUG-001.md',
9
+ frontmatter: {
10
+ title: 'Login button broken',
11
+ status: 'open',
12
+ author: 'test',
13
+ 'created-at': '2024-01-01T00:00:00.000Z',
14
+ },
15
+ body: '## Description\n\nThe login button does not work.',
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ function makeCR(overrides: Partial<ChangeRequest> = {}): ChangeRequest {
21
+ return {
22
+ relativePath: 'change-requests/CR-001.md',
23
+ frontmatter: {
24
+ title: 'Add dark mode',
25
+ status: 'draft',
26
+ author: 'test',
27
+ 'created-at': '2024-01-01T00:00:00.000Z',
28
+ },
29
+ body: '## Changes\n\nAdd dark mode support.',
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ function makeFile(overrides: Partial<StoryFile> = {}): StoryFile {
35
+ return {
36
+ relativePath: 'product/features/auth.md',
37
+ frontmatter: {
38
+ title: 'Auth',
39
+ status: 'new',
40
+ author: 'test',
41
+ 'last-modified': '2024-01-01T00:00:00.000Z',
42
+ version: '1.0',
43
+ },
44
+ body: '# Auth Feature',
45
+ pendingItems: [],
46
+ agentNotes: null,
47
+ crossRefs: [],
48
+ hash: 'abc',
49
+ ...overrides,
50
+ };
51
+ }
52
+
53
+ describe('generateApplyPrompt', () => {
54
+ it('returns null when nothing to do', () => {
55
+ const result = generateApplyPrompt([], [], [], '/tmp');
56
+ expect(result).toBeNull();
57
+ });
58
+
59
+ it('generates prompt with only bugs', () => {
60
+ const prompt = generateApplyPrompt([makeBug()], [], [], '/tmp');
61
+ expect(prompt).not.toBeNull();
62
+ expect(prompt).toContain('# SDD Apply');
63
+ expect(prompt).toContain('Open Bugs (1)');
64
+ expect(prompt).toContain('Login button broken');
65
+ expect(prompt).toContain('bugs/BUG-001.md');
66
+ expect(prompt).not.toContain('Pending Change Requests');
67
+ expect(prompt).not.toContain('Pending Files');
68
+ });
69
+
70
+ it('generates prompt with only CRs', () => {
71
+ const prompt = generateApplyPrompt([], [makeCR()], [], '/tmp');
72
+ expect(prompt).not.toBeNull();
73
+ expect(prompt).toContain('Pending Change Requests (1)');
74
+ expect(prompt).toContain('Add dark mode');
75
+ expect(prompt).not.toContain('Open Bugs');
76
+ expect(prompt).not.toContain('Pending Files');
77
+ });
78
+
79
+ it('generates prompt with only pending files', () => {
80
+ const prompt = generateApplyPrompt([], [], [makeFile()], '/tmp');
81
+ expect(prompt).not.toBeNull();
82
+ expect(prompt).toContain('Pending Files (1)');
83
+ expect(prompt).toContain('product/features/auth.md');
84
+ expect(prompt).toContain('**new**');
85
+ expect(prompt).not.toContain('Open Bugs');
86
+ expect(prompt).not.toContain('Pending Change Requests');
87
+ });
88
+
89
+ it('generates prompt with all three', () => {
90
+ const prompt = generateApplyPrompt([makeBug()], [makeCR()], [makeFile()], '/tmp');
91
+ expect(prompt).not.toBeNull();
92
+ expect(prompt).toContain('Open Bugs (1)');
93
+ expect(prompt).toContain('Pending Change Requests (1)');
94
+ expect(prompt).toContain('Pending Files (1)');
95
+ expect(prompt).toContain('## Instructions');
96
+ });
97
+ });
98
+
99
+ describe('resolveAgentCommand', () => {
100
+ it('resolves built-in agent', () => {
101
+ const cmd = resolveAgentCommand('claude');
102
+ expect(cmd).toBe(DEFAULT_AGENTS.claude);
103
+ });
104
+
105
+ it('resolves config agent over built-in', () => {
106
+ const cmd = resolveAgentCommand('claude', { claude: 'my-claude $PROMPT' });
107
+ expect(cmd).toBe('my-claude $PROMPT');
108
+ });
109
+
110
+ it('resolves custom agent from config', () => {
111
+ const cmd = resolveAgentCommand('my-agent', { 'my-agent': 'my-agent-cmd $PROMPT' });
112
+ expect(cmd).toBe('my-agent-cmd $PROMPT');
113
+ });
114
+
115
+ it('returns undefined for unknown agent', () => {
116
+ const cmd = resolveAgentCommand('unknown');
117
+ expect(cmd).toBeUndefined();
118
+ });
119
+ });