@applica-software-guru/sdd-core 0.2.0 → 1.0.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.
- package/dist/agent/agent-defaults.d.ts +3 -0
- package/dist/agent/agent-defaults.d.ts.map +1 -0
- package/dist/agent/agent-defaults.js +13 -0
- package/dist/agent/agent-defaults.js.map +1 -0
- package/dist/agent/agent-runner.d.ts +9 -0
- package/dist/agent/agent-runner.d.ts.map +1 -0
- package/dist/agent/agent-runner.js +43 -0
- package/dist/agent/agent-runner.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/parser/bug-parser.d.ts +8 -0
- package/dist/parser/bug-parser.d.ts.map +1 -0
- package/dist/parser/bug-parser.js +45 -0
- package/dist/parser/bug-parser.js.map +1 -0
- package/dist/prompt/apply-prompt-generator.d.ts +3 -0
- package/dist/prompt/apply-prompt-generator.d.ts.map +1 -0
- package/dist/prompt/apply-prompt-generator.js +67 -0
- package/dist/prompt/apply-prompt-generator.js.map +1 -0
- package/dist/scaffold/init.js +1 -1
- package/dist/scaffold/init.js.map +1 -1
- package/dist/scaffold/templates.d.ts +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +55 -9
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/sdd.d.ts +5 -1
- package/dist/sdd.d.ts.map +1 -1
- package/dist/sdd.js +36 -0
- package/dist/sdd.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent/agent-defaults.ts +12 -0
- package/src/agent/agent-runner.ts +54 -0
- package/src/index.ts +7 -1
- package/src/parser/bug-parser.ts +41 -0
- package/src/prompt/apply-prompt-generator.ts +82 -0
- package/src/scaffold/init.ts +1 -1
- package/src/scaffold/templates.ts +55 -9
- package/src/sdd.ts +42 -1
- package/src/types.ts +17 -0
- package/tests/apply.test.ts +119 -0
- package/tests/bug.test.ts +173 -0
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEvE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,gBAAgB,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,KAAK;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,UAAU,GAAG,KAAK,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,KAAK,CAAC;QACX,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;QACjD,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEvE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,gBAAgB,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,KAAK;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,UAAU,GAAG,KAAK,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,KAAK,CAAC;QACX,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;QACjD,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,SAAS,CAAC;AAEtD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,mBAAmB,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,wBAAwB,CAAC;IACtC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,GAAG;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,cAAc,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const DEFAULT_AGENTS: Record<string, string> = {
|
|
2
|
+
claude: 'claude -p "$(cat $PROMPT_FILE)" --dangerously-skip-permissions --verbose',
|
|
3
|
+
codex: 'codex -q "$(cat $PROMPT_FILE)"',
|
|
4
|
+
opencode: 'opencode -p "$(cat $PROMPT_FILE)"',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function resolveAgentCommand(
|
|
8
|
+
name: string,
|
|
9
|
+
configAgents?: Record<string, string>
|
|
10
|
+
): string | undefined {
|
|
11
|
+
return configAgents?.[name] ?? DEFAULT_AGENTS[name];
|
|
12
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { writeFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import { resolveAgentCommand } from './agent-defaults.js';
|
|
7
|
+
|
|
8
|
+
export interface AgentRunnerOptions {
|
|
9
|
+
root: string;
|
|
10
|
+
prompt: string;
|
|
11
|
+
agent: string;
|
|
12
|
+
agents?: Record<string, string>;
|
|
13
|
+
onOutput?: (data: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function runAgent(options: AgentRunnerOptions): Promise<number> {
|
|
17
|
+
const { root, prompt, agent, agents, onOutput } = options;
|
|
18
|
+
|
|
19
|
+
const template = resolveAgentCommand(agent, agents);
|
|
20
|
+
if (!template) {
|
|
21
|
+
throw new Error(`Unknown agent "${agent}". Available: ${Object.keys(agents ?? {}).join(', ') || 'claude, codex, opencode'}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Write prompt to temp file (too large for CLI arg)
|
|
25
|
+
const tmpFile = join(tmpdir(), `sdd-prompt-${randomBytes(6).toString('hex')}.md`);
|
|
26
|
+
await writeFile(tmpFile, prompt, 'utf-8');
|
|
27
|
+
|
|
28
|
+
// Replace $PROMPT_FILE with the temp file path in the command template
|
|
29
|
+
const command = template.replace(/\$PROMPT_FILE/g, tmpFile);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const exitCode = await new Promise<number>((resolve, reject) => {
|
|
33
|
+
const child = spawn(command, {
|
|
34
|
+
cwd: root,
|
|
35
|
+
shell: true,
|
|
36
|
+
stdio: onOutput ? ['inherit', 'pipe', 'pipe'] : 'inherit',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (onOutput && child.stdout) {
|
|
40
|
+
child.stdout.on('data', (data: Buffer) => onOutput(data.toString()));
|
|
41
|
+
}
|
|
42
|
+
if (onOutput && child.stderr) {
|
|
43
|
+
child.stderr.on('data', (data: Buffer) => onOutput(data.toString()));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
child.on('error', reject);
|
|
47
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return exitCode;
|
|
51
|
+
} finally {
|
|
52
|
+
await unlink(tmpFile).catch(() => {});
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,13 @@ export type {
|
|
|
13
13
|
ChangeRequest,
|
|
14
14
|
ChangeRequestFrontmatter,
|
|
15
15
|
ChangeRequestStatus,
|
|
16
|
+
Bug,
|
|
17
|
+
BugFrontmatter,
|
|
18
|
+
BugStatus,
|
|
16
19
|
} from './types.js';
|
|
17
20
|
export { SDDError, LockFileNotFoundError, ParseError, ProjectNotInitializedError } from './errors.js';
|
|
18
21
|
export type { ProjectInfo } from './scaffold/templates.js';
|
|
19
|
-
export { isSDDProject, readConfig } from './config/config-manager.js';
|
|
22
|
+
export { isSDDProject, readConfig, writeConfig } from './config/config-manager.js';
|
|
23
|
+
export { runAgent } from './agent/agent-runner.js';
|
|
24
|
+
export type { AgentRunnerOptions } from './agent/agent-runner.js';
|
|
25
|
+
export { DEFAULT_AGENTS, resolveAgentCommand } from './agent/agent-defaults.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve, relative } from 'node:path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
import type { Bug, BugFrontmatter } from '../types.js';
|
|
6
|
+
import { ParseError } from '../errors.js';
|
|
7
|
+
|
|
8
|
+
export async function discoverBugFiles(root: string): Promise<string[]> {
|
|
9
|
+
const pattern = 'bugs/*.md';
|
|
10
|
+
const matches = await glob(pattern, { cwd: root, absolute: true });
|
|
11
|
+
return matches.sort();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseBugFile(filePath: string, content: string): { frontmatter: BugFrontmatter; body: string } {
|
|
15
|
+
try {
|
|
16
|
+
const { data, content: body } = matter(content);
|
|
17
|
+
const frontmatter: BugFrontmatter = {
|
|
18
|
+
title: data.title ?? '',
|
|
19
|
+
status: data.status ?? 'open',
|
|
20
|
+
author: data.author ?? '',
|
|
21
|
+
'created-at': data['created-at'] ?? '',
|
|
22
|
+
};
|
|
23
|
+
return { frontmatter, body };
|
|
24
|
+
} catch (err) {
|
|
25
|
+
throw new ParseError(filePath, (err as Error).message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function parseAllBugFiles(root: string): Promise<Bug[]> {
|
|
30
|
+
const paths = await discoverBugFiles(root);
|
|
31
|
+
const results: Bug[] = [];
|
|
32
|
+
|
|
33
|
+
for (const absPath of paths) {
|
|
34
|
+
const content = await readFile(absPath, 'utf-8');
|
|
35
|
+
const relPath = relative(root, absPath);
|
|
36
|
+
const { frontmatter, body } = parseBugFile(relPath, content);
|
|
37
|
+
results.push({ relativePath: relPath, frontmatter, body });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Bug, ChangeRequest, StoryFile } from '../types.js';
|
|
2
|
+
import { getFileDiff } from '../git/git.js';
|
|
3
|
+
|
|
4
|
+
export function generateApplyPrompt(
|
|
5
|
+
bugs: Bug[],
|
|
6
|
+
changeRequests: ChangeRequest[],
|
|
7
|
+
pendingFiles: StoryFile[],
|
|
8
|
+
root: string
|
|
9
|
+
): string | null {
|
|
10
|
+
if (bugs.length === 0 && changeRequests.length === 0 && pendingFiles.length === 0) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sections: string[] = [];
|
|
15
|
+
|
|
16
|
+
sections.push('# SDD Apply\n\nThis project uses Story Driven Development. Complete the tasks below in order.');
|
|
17
|
+
|
|
18
|
+
// Bugs section
|
|
19
|
+
if (bugs.length > 0) {
|
|
20
|
+
const lines = [`## Open Bugs (${bugs.length})\n`];
|
|
21
|
+
for (const bug of bugs) {
|
|
22
|
+
lines.push(`### \`${bug.relativePath}\` — ${bug.frontmatter.title}\n`);
|
|
23
|
+
lines.push(bug.body.trim());
|
|
24
|
+
lines.push('');
|
|
25
|
+
}
|
|
26
|
+
sections.push(lines.join('\n'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Change Requests section
|
|
30
|
+
if (changeRequests.length > 0) {
|
|
31
|
+
const lines = [`## Pending Change Requests (${changeRequests.length})\n`];
|
|
32
|
+
for (const cr of changeRequests) {
|
|
33
|
+
lines.push(`### \`${cr.relativePath}\` — ${cr.frontmatter.title}\n`);
|
|
34
|
+
lines.push(cr.body.trim());
|
|
35
|
+
lines.push('');
|
|
36
|
+
}
|
|
37
|
+
sections.push(lines.join('\n'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Pending Files section
|
|
41
|
+
if (pendingFiles.length > 0) {
|
|
42
|
+
const lines = [`## Pending Files (${pendingFiles.length})\n`];
|
|
43
|
+
for (const f of pendingFiles) {
|
|
44
|
+
lines.push(`- \`${f.relativePath}\` — **${f.frontmatter.status}**`);
|
|
45
|
+
}
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push('Read each file listed above before implementing.');
|
|
48
|
+
|
|
49
|
+
// Show git diff for changed files
|
|
50
|
+
const changed = pendingFiles.filter((f) => f.frontmatter.status === 'changed');
|
|
51
|
+
for (const f of changed) {
|
|
52
|
+
const diff = getFileDiff(root, f.relativePath);
|
|
53
|
+
if (diff) {
|
|
54
|
+
lines.push('');
|
|
55
|
+
lines.push(`### Changes in \`${f.relativePath}\`\n\n\`\`\`diff\n${diff}\n\`\`\``);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
sections.push(lines.join('\n'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Instructions
|
|
63
|
+
const instructions = ['## Instructions\n'];
|
|
64
|
+
|
|
65
|
+
if (bugs.length > 0) {
|
|
66
|
+
instructions.push('1. Fix each open bug, then run `sdd mark-bug-resolved <file>` and commit');
|
|
67
|
+
}
|
|
68
|
+
if (changeRequests.length > 0) {
|
|
69
|
+
instructions.push(`${bugs.length > 0 ? '2' : '1'}. Apply each CR to the documentation, then run \`sdd mark-cr-applied <file>\` and commit`);
|
|
70
|
+
}
|
|
71
|
+
if (pendingFiles.length > 0) {
|
|
72
|
+
const step = (bugs.length > 0 ? 1 : 0) + (changeRequests.length > 0 ? 1 : 0) + 1;
|
|
73
|
+
instructions.push(`${step}. Implement each pending file, then run \`sdd mark-synced <file>\` and commit`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lastStep = (bugs.length > 0 ? 1 : 0) + (changeRequests.length > 0 ? 1 : 0) + (pendingFiles.length > 0 ? 1 : 0) + 1;
|
|
77
|
+
instructions.push(`${lastStep}. After each mark-* command, immediately commit: \`git add -A && git commit -m "sdd: <description>"\``);
|
|
78
|
+
|
|
79
|
+
sections.push(instructions.join('\n'));
|
|
80
|
+
|
|
81
|
+
return sections.join('\n\n');
|
|
82
|
+
}
|
package/src/scaffold/init.ts
CHANGED
|
@@ -29,7 +29,7 @@ export async function initProject(root: string, info?: ProjectInfo): Promise<str
|
|
|
29
29
|
createdFiles.push('.sdd/config.yaml');
|
|
30
30
|
|
|
31
31
|
// Create directory structure
|
|
32
|
-
const dirs = ['product', 'product/features', 'system', 'code', 'change-requests'];
|
|
32
|
+
const dirs = ['product', 'product/features', 'system', 'code', 'change-requests', 'bugs'];
|
|
33
33
|
for (const dir of dirs) {
|
|
34
34
|
const absDir = resolve(root, dir);
|
|
35
35
|
if (!existsSync(absDir)) {
|
|
@@ -24,12 +24,14 @@ Documentation drives implementation: read the docs first, then write code.
|
|
|
24
24
|
|
|
25
25
|
## Workflow
|
|
26
26
|
|
|
27
|
-
1. Run \`sdd
|
|
28
|
-
2. If there are
|
|
29
|
-
3. Run \`sdd
|
|
30
|
-
4.
|
|
31
|
-
5.
|
|
32
|
-
6.
|
|
27
|
+
1. Run \`sdd bug open\` — check if there are open bugs to fix first
|
|
28
|
+
2. If there are open bugs, fix the code/docs, then run \`sdd mark-bug-resolved\`
|
|
29
|
+
3. Run \`sdd cr pending\` — check if there are change requests to process
|
|
30
|
+
4. If there are pending CRs, apply them to the docs, then run \`sdd mark-cr-applied\`
|
|
31
|
+
5. Run \`sdd sync\` to see what needs to be implemented
|
|
32
|
+
6. Read the documentation files listed in the sync output
|
|
33
|
+
7. Implement what each file describes, writing code inside \`code/\`
|
|
34
|
+
8. After implementing, mark files as synced:
|
|
33
35
|
|
|
34
36
|
\`\`\`
|
|
35
37
|
sdd mark-synced product/features/auth.md
|
|
@@ -41,7 +43,7 @@ Or mark all pending files at once:
|
|
|
41
43
|
sdd mark-synced
|
|
42
44
|
\`\`\`
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
9. **Commit immediately after mark-synced** — this is mandatory:
|
|
45
47
|
|
|
46
48
|
\`\`\`
|
|
47
49
|
git add -A && git commit -m "sdd sync: <brief description of what was implemented>"
|
|
@@ -58,17 +60,20 @@ Delete the related code in \`code/\`, then run \`sdd mark-synced <file>\` (the d
|
|
|
58
60
|
|
|
59
61
|
- \`sdd status\` — See all documentation files and their state (new/changed/deleted/synced)
|
|
60
62
|
- \`sdd diff\` — See what changed since last sync
|
|
61
|
-
- \`sdd sync\` — Get the sync prompt for pending files (
|
|
63
|
+
- \`sdd sync\` — Get the sync prompt for pending files (includes git diff for changed files)
|
|
62
64
|
- \`sdd validate\` — Check for broken references and issues
|
|
63
65
|
- \`sdd mark-synced [files...]\` — Mark specific files (or all) as synced
|
|
64
66
|
- \`sdd cr list\` — List all change requests with their status
|
|
65
67
|
- \`sdd cr pending\` — Show draft change requests to process
|
|
66
68
|
- \`sdd mark-cr-applied [files...]\` — Mark change requests as applied
|
|
69
|
+
- \`sdd bug list\` — List all bugs with their status
|
|
70
|
+
- \`sdd bug open\` — Show open bugs to fix
|
|
71
|
+
- \`sdd mark-bug-resolved [files...]\` — Mark bugs as resolved
|
|
67
72
|
|
|
68
73
|
## Rules
|
|
69
74
|
|
|
70
75
|
1. **Always commit after mark-synced** — run \`git add -A && git commit -m "sdd sync: ..."\` immediately after \`sdd mark-synced\`. Never leave synced files uncommitted.
|
|
71
|
-
2. Before running \`sdd sync\`, check for pending change requests with \`sdd cr pending\`
|
|
76
|
+
2. Before running \`sdd sync\`, check for open bugs with \`sdd bug open\` and pending change requests with \`sdd cr pending\`
|
|
72
77
|
3. If there are pending CRs, apply them to the docs first, then mark them with \`sdd mark-cr-applied\`
|
|
73
78
|
4. Only implement what the sync prompt asks for
|
|
74
79
|
5. All generated code goes inside \`code/\`
|
|
@@ -97,6 +102,16 @@ version: "1.0"
|
|
|
97
102
|
- **version**: patch-bump on each edit (1.0 → 1.1 → 1.2)
|
|
98
103
|
- **last-modified**: ISO 8601 datetime, updated on each edit
|
|
99
104
|
|
|
105
|
+
## How sync works
|
|
106
|
+
|
|
107
|
+
\`sdd sync\` generates a structured prompt for the agent based on pending files:
|
|
108
|
+
|
|
109
|
+
- **\`new\` files**: the agent reads the full documentation and implements it from scratch
|
|
110
|
+
- **\`changed\` files**: SDD uses \`git diff\` to compute what changed in the documentation since the last commit, and includes the diff in the sync prompt — this way the agent sees exactly what was modified and can update only the affected code
|
|
111
|
+
- **\`deleted\` files**: the agent removes the related code
|
|
112
|
+
|
|
113
|
+
This is why **committing after every mark-synced is mandatory** — the git history is what SDD uses to detect changes.
|
|
114
|
+
|
|
100
115
|
## Change Requests
|
|
101
116
|
|
|
102
117
|
Change Requests (CRs) are markdown files in \`change-requests/\` that describe modifications to the documentation.
|
|
@@ -127,6 +142,36 @@ created-at: "2025-01-01T00:00:00.000Z"
|
|
|
127
142
|
- \`sdd cr pending\` — Show only draft CRs to process
|
|
128
143
|
- \`sdd mark-cr-applied [files...]\` — Mark CRs as applied after updating the docs
|
|
129
144
|
|
|
145
|
+
## Bugs
|
|
146
|
+
|
|
147
|
+
Bugs are markdown files in \`bugs/\` that describe problems found in the codebase.
|
|
148
|
+
|
|
149
|
+
### Bug format
|
|
150
|
+
|
|
151
|
+
\`\`\`yaml
|
|
152
|
+
---
|
|
153
|
+
title: "Login fails with empty password"
|
|
154
|
+
status: open
|
|
155
|
+
author: "user"
|
|
156
|
+
created-at: "2025-01-01T00:00:00.000Z"
|
|
157
|
+
---
|
|
158
|
+
\`\`\`
|
|
159
|
+
|
|
160
|
+
- **status**: \`open\` (needs fixing) or \`resolved\` (already fixed)
|
|
161
|
+
|
|
162
|
+
### Bug workflow
|
|
163
|
+
|
|
164
|
+
1. Check for open bugs: \`sdd bug open\`
|
|
165
|
+
2. Read each open bug and fix the code and/or documentation
|
|
166
|
+
3. After fixing a bug, mark it: \`sdd mark-bug-resolved bugs/BUG-001.md\`
|
|
167
|
+
4. Commit the fix
|
|
168
|
+
|
|
169
|
+
### Bug commands
|
|
170
|
+
|
|
171
|
+
- \`sdd bug list\` — See all bugs and their status
|
|
172
|
+
- \`sdd bug open\` — Show only open bugs to fix
|
|
173
|
+
- \`sdd mark-bug-resolved [files...]\` — Mark bugs as resolved after fixing
|
|
174
|
+
|
|
130
175
|
## UX and screenshots
|
|
131
176
|
|
|
132
177
|
When a feature has UX mockups or screenshots, place them next to the feature doc:
|
|
@@ -158,6 +203,7 @@ Both formats work — use a folder only when you have screenshots or multiple fi
|
|
|
158
203
|
- \`system/\` — How to build it (entities, architecture, tech stack, interfaces)
|
|
159
204
|
- \`code/\` — All generated source code goes here
|
|
160
205
|
- \`change-requests/\` — Change requests to the documentation
|
|
206
|
+
- \`bugs/\` — Bug reports
|
|
161
207
|
- \`.sdd/\` — Project config and sync state (do not edit)
|
|
162
208
|
`;
|
|
163
209
|
|
package/src/sdd.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import type { StoryStatus, ValidationResult, SDDConfig, ChangeRequest } from './types.js';
|
|
3
|
+
import type { StoryStatus, ValidationResult, SDDConfig, ChangeRequest, Bug } from './types.js';
|
|
4
4
|
import { ProjectNotInitializedError } from './errors.js';
|
|
5
5
|
import { parseAllStoryFiles } from './parser/story-parser.js';
|
|
6
6
|
import { generatePrompt } from './prompt/prompt-generator.js';
|
|
7
|
+
import { generateApplyPrompt } from './prompt/apply-prompt-generator.js';
|
|
7
8
|
import { validate } from './validate/validator.js';
|
|
8
9
|
import { initProject } from './scaffold/init.js';
|
|
9
10
|
import { isSDDProject, readConfig, writeConfig } from './config/config-manager.js';
|
|
10
11
|
import { parseAllCRFiles } from './parser/cr-parser.js';
|
|
12
|
+
import { parseAllBugFiles } from './parser/bug-parser.js';
|
|
11
13
|
import type { ProjectInfo } from './scaffold/templates.js';
|
|
12
14
|
|
|
13
15
|
export class SDD {
|
|
@@ -51,6 +53,16 @@ export class SDD {
|
|
|
51
53
|
return generatePrompt(pending, this.root);
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
async applyPrompt(): Promise<string | null> {
|
|
57
|
+
this.ensureInitialized();
|
|
58
|
+
const [bugs, changeRequests, pendingFiles] = await Promise.all([
|
|
59
|
+
this.openBugs(),
|
|
60
|
+
this.pendingChangeRequests(),
|
|
61
|
+
this.pending(),
|
|
62
|
+
]);
|
|
63
|
+
return generateApplyPrompt(bugs, changeRequests, pendingFiles, this.root);
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
async validate(): Promise<ValidationResult> {
|
|
55
67
|
this.ensureInitialized();
|
|
56
68
|
const files = await parseAllStoryFiles(this.root);
|
|
@@ -115,6 +127,35 @@ export class SDD {
|
|
|
115
127
|
return marked;
|
|
116
128
|
}
|
|
117
129
|
|
|
130
|
+
async bugs(): Promise<Bug[]> {
|
|
131
|
+
this.ensureInitialized();
|
|
132
|
+
return parseAllBugFiles(this.root);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async openBugs(): Promise<Bug[]> {
|
|
136
|
+
const all = await this.bugs();
|
|
137
|
+
return all.filter((b) => b.frontmatter.status === 'open');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async markBugResolved(paths?: string[]): Promise<string[]> {
|
|
141
|
+
this.ensureInitialized();
|
|
142
|
+
const all = await this.bugs();
|
|
143
|
+
const marked: string[] = [];
|
|
144
|
+
|
|
145
|
+
for (const bug of all) {
|
|
146
|
+
if (bug.frontmatter.status === 'resolved') continue;
|
|
147
|
+
if (paths && paths.length > 0 && !paths.includes(bug.relativePath)) continue;
|
|
148
|
+
|
|
149
|
+
const absPath = resolve(this.root, bug.relativePath);
|
|
150
|
+
const content = await readFile(absPath, 'utf-8');
|
|
151
|
+
const updated = content.replace(/^status:\s*open/m, 'status: resolved');
|
|
152
|
+
await writeFile(absPath, updated, 'utf-8');
|
|
153
|
+
marked.push(bug.relativePath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return marked;
|
|
157
|
+
}
|
|
158
|
+
|
|
118
159
|
private ensureInitialized(): void {
|
|
119
160
|
if (!isSDDProject(this.root)) {
|
|
120
161
|
throw new ProjectNotInitializedError(this.root);
|
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';
|
|
@@ -74,3 +76,18 @@ export interface ChangeRequest {
|
|
|
74
76
|
frontmatter: ChangeRequestFrontmatter;
|
|
75
77
|
body: string;
|
|
76
78
|
}
|
|
79
|
+
|
|
80
|
+
export type BugStatus = 'open' | 'resolved';
|
|
81
|
+
|
|
82
|
+
export interface BugFrontmatter {
|
|
83
|
+
title: string;
|
|
84
|
+
status: BugStatus;
|
|
85
|
+
author: string;
|
|
86
|
+
'created-at': string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Bug {
|
|
90
|
+
relativePath: string;
|
|
91
|
+
frontmatter: BugFrontmatter;
|
|
92
|
+
body: string;
|
|
93
|
+
}
|
|
@@ -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
|
+
});
|