draht-claude 2026.4.23
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/.claude-plugin/plugin.json +21 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE +22 -0
- package/README.md +199 -0
- package/agents/architect.md +45 -0
- package/agents/debugger.md +57 -0
- package/agents/git-committer.md +52 -0
- package/agents/implementer.md +35 -0
- package/agents/reviewer.md +57 -0
- package/agents/security-auditor.md +109 -0
- package/agents/verifier.md +44 -0
- package/bin/draht-tools.cjs +1067 -0
- package/cli.mjs +348 -0
- package/commands/atomic-commit.md +61 -0
- package/commands/discuss-phase.md +54 -0
- package/commands/execute-phase.md +111 -0
- package/commands/fix.md +50 -0
- package/commands/init-project.md +65 -0
- package/commands/map-codebase.md +52 -0
- package/commands/new-project.md +73 -0
- package/commands/next-milestone.md +49 -0
- package/commands/orchestrate.md +58 -0
- package/commands/pause-work.md +38 -0
- package/commands/plan-phase.md +107 -0
- package/commands/progress.md +30 -0
- package/commands/quick.md +50 -0
- package/commands/resume-work.md +35 -0
- package/commands/review.md +55 -0
- package/commands/verify-work.md +72 -0
- package/hooks/hooks.json +26 -0
- package/package.json +50 -0
- package/scripts/gsd-post-phase.cjs +133 -0
- package/scripts/gsd-post-task.cjs +165 -0
- package/scripts/gsd-pre-execute.cjs +146 -0
- package/scripts/gsd-quality-gate.cjs +252 -0
- package/scripts/prompt-context.cjs +36 -0
- package/scripts/session-start.cjs +52 -0
- package/skills/ddd-workflow/SKILL.md +108 -0
- package/skills/gsd-workflow/SKILL.md +111 -0
- package/skills/tdd-workflow/SKILL.md +115 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Draht Quality Gate Hook
|
|
6
|
+
* Runs after task completion to enforce quality standards.
|
|
7
|
+
* Called by the build agent after each verify step.
|
|
8
|
+
*
|
|
9
|
+
* Usage: node gsd-quality-gate.js [--strict]
|
|
10
|
+
* Exit 0 = quality OK, Exit 1 = quality issues
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { execSync } = require("node:child_process");
|
|
14
|
+
const fs = require("node:fs");
|
|
15
|
+
const path = require("node:path");
|
|
16
|
+
|
|
17
|
+
// ── Toolchain detection — mirrors src/gsd/hook-utils.ts ──────────────────────
|
|
18
|
+
function detectToolchain(cwd) {
|
|
19
|
+
if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) {
|
|
20
|
+
return { pm: "bun", testCmd: "bun test", coverageCmd: "bun test --coverage", lintCmd: "bunx biome check ." };
|
|
21
|
+
}
|
|
22
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
|
|
23
|
+
return { pm: "pnpm", testCmd: "pnpm test", coverageCmd: "pnpm run test:coverage", lintCmd: "pnpm run lint" };
|
|
24
|
+
}
|
|
25
|
+
if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
|
|
26
|
+
return { pm: "yarn", testCmd: "yarn test", coverageCmd: "yarn run test:coverage", lintCmd: "yarn run lint" };
|
|
27
|
+
}
|
|
28
|
+
return { pm: "npm", testCmd: "npm test", coverageCmd: "npm run test:coverage", lintCmd: "npm run lint" };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readHookConfig(cwd) {
|
|
32
|
+
const defaults = { coverageThreshold: 80, tddMode: "advisory", qualityGateStrict: false };
|
|
33
|
+
const configPath = path.join(cwd, ".planning", "config.json");
|
|
34
|
+
if (!fs.existsSync(configPath)) return defaults;
|
|
35
|
+
try {
|
|
36
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
37
|
+
const h = raw.hooks || {};
|
|
38
|
+
return {
|
|
39
|
+
coverageThreshold: typeof h.coverageThreshold === "number" ? h.coverageThreshold : defaults.coverageThreshold,
|
|
40
|
+
tddMode: h.tddMode === "strict" || h.tddMode === "advisory" ? h.tddMode : defaults.tddMode,
|
|
41
|
+
qualityGateStrict: typeof h.qualityGateStrict === "boolean" ? h.qualityGateStrict : defaults.qualityGateStrict,
|
|
42
|
+
};
|
|
43
|
+
} catch { return defaults; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Inline domain validator — mirrors src/gsd/domain-validator.ts
|
|
47
|
+
function extractGlossaryTerms(content) {
|
|
48
|
+
const terms = new Set();
|
|
49
|
+
const sectionMatch = content.match(/## Ubiquitous Language([\s\S]*?)(?:\n## |$)/);
|
|
50
|
+
const section = sectionMatch ? sectionMatch[1] : content;
|
|
51
|
+
for (const m of section.matchAll(/\*\*([A-Z][a-zA-Z0-9]+)\*\*/g)) terms.add(m[1]);
|
|
52
|
+
for (const m of section.matchAll(/^[-*]\s+([A-Z][a-zA-Z0-9]+)\s*:/gm)) terms.add(m[1]);
|
|
53
|
+
for (const m of section.matchAll(/\|\s*([A-Z][a-zA-Z0-9]+)\s*\|/g)) terms.add(m[1]);
|
|
54
|
+
return terms;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadDomainContent(cwd) {
|
|
58
|
+
const modelPath = path.join(cwd, ".planning", "DOMAIN-MODEL.md");
|
|
59
|
+
if (fs.existsSync(modelPath)) return fs.readFileSync(modelPath, "utf-8");
|
|
60
|
+
const domainPath = path.join(cwd, ".planning", "DOMAIN.md");
|
|
61
|
+
if (fs.existsSync(domainPath)) return fs.readFileSync(domainPath, "utf-8");
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
66
|
+
const cwd = process.cwd();
|
|
67
|
+
const toolchain = detectToolchain(cwd);
|
|
68
|
+
const hookConfig = readHookConfig(cwd);
|
|
69
|
+
const strict = process.argv.includes("--strict") || hookConfig.qualityGateStrict;
|
|
70
|
+
const issues = [];
|
|
71
|
+
|
|
72
|
+
// 1. TypeScript check
|
|
73
|
+
try {
|
|
74
|
+
const tsCmd = toolchain.pm === "bun" ? "bun run tsgo --noEmit 2>&1" : "npx tsc --noEmit 2>&1";
|
|
75
|
+
execSync(tsCmd, { timeout: 60000, encoding: "utf-8", cwd });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const output = error.stdout || error.stderr || "";
|
|
78
|
+
const errorCount = (output.match(/error TS/g) || []).length;
|
|
79
|
+
if (errorCount > 0) {
|
|
80
|
+
issues.push({ severity: strict ? "error" : "warning", message: `${errorCount} TypeScript error(s)`, details: output.slice(0, 500) });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 2. Lint check (if biome.json exists use biome, else use toolchain lint)
|
|
85
|
+
if (fs.existsSync(path.join(cwd, "biome.json"))) {
|
|
86
|
+
try {
|
|
87
|
+
execSync(`${toolchain.lintCmd} --error-on-warnings 2>&1`, { timeout: 30000, encoding: "utf-8", cwd });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const output = error.stdout || error.stderr || "";
|
|
90
|
+
issues.push({ severity: strict ? "error" : "warning", message: "Lint issues", details: output.slice(0, 500) });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. Run tests
|
|
95
|
+
try {
|
|
96
|
+
const testOutput = execSync(`${toolchain.testCmd} 2>&1`, { timeout: 120000, encoding: "utf-8", cwd });
|
|
97
|
+
const failMatch = testOutput.match(/(\d+) fail/);
|
|
98
|
+
if (failMatch && parseInt(failMatch[1], 10) > 0) {
|
|
99
|
+
issues.push({ severity: strict ? "error" : "warning", message: `${failMatch[1]} test(s) failing` });
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const output = error.stdout || error.stderr || "";
|
|
103
|
+
const failMatch = output.match(/(\d+) fail/);
|
|
104
|
+
if (failMatch && parseInt(failMatch[1], 10) > 0) {
|
|
105
|
+
issues.push({ severity: strict ? "error" : "warning", message: `${failMatch[1]} test(s) failing` });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 4. Check for console.log in source files (not tests)
|
|
110
|
+
try {
|
|
111
|
+
const result = execSync(
|
|
112
|
+
"grep -rn 'console\\.log' src/ --include='*.ts' --include='*.tsx' 2>/dev/null | grep -v '// debug' | head -5",
|
|
113
|
+
{ encoding: "utf-8", cwd }
|
|
114
|
+
).trim();
|
|
115
|
+
if (result) {
|
|
116
|
+
issues.push({ severity: "warning", message: "console.log found in source", details: result });
|
|
117
|
+
}
|
|
118
|
+
} catch { /* grep returns 1 when no match — that's fine */ }
|
|
119
|
+
|
|
120
|
+
// 5. Domain glossary compliance (checks DOMAIN-MODEL.md, falls back to DOMAIN.md)
|
|
121
|
+
const domainContent = loadDomainContent(cwd);
|
|
122
|
+
if (domainContent) {
|
|
123
|
+
try {
|
|
124
|
+
const glossaryTerms = extractGlossaryTerms(domainContent);
|
|
125
|
+
const changedFiles = execSync(
|
|
126
|
+
"git diff --cached --name-only 2>/dev/null || git diff --name-only HEAD~1",
|
|
127
|
+
{ encoding: "utf-8", cwd }
|
|
128
|
+
).trim().split("\n").filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
129
|
+
|
|
130
|
+
const unknownTerms = [];
|
|
131
|
+
for (const file of changedFiles) {
|
|
132
|
+
if (!fs.existsSync(path.join(cwd, file))) continue;
|
|
133
|
+
const src = fs.readFileSync(path.join(cwd, file), "utf-8");
|
|
134
|
+
const declarations = [...src.matchAll(/(?:class|interface|type|enum)\s+([A-Z][a-zA-Z0-9]+)/g)].map((m) => m[1]);
|
|
135
|
+
for (const term of declarations) {
|
|
136
|
+
if (!glossaryTerms.has(term)) unknownTerms.push(`${file}: ${term}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (unknownTerms.length > 0) {
|
|
140
|
+
issues.push({
|
|
141
|
+
severity: hookConfig.tddMode === "strict" ? "error" : "warning",
|
|
142
|
+
message: `${unknownTerms.length} PascalCase type(s) not in domain glossary (DOMAIN-MODEL.md)`,
|
|
143
|
+
details: unknownTerms.slice(0, 5).join(", "),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
} catch { /* ignore */ }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 6. Bounded context boundary check — flag suspicious cross-directory imports
|
|
150
|
+
try {
|
|
151
|
+
const changedSrcFiles = execSync(
|
|
152
|
+
"git diff --cached --name-only 2>/dev/null || git diff --name-only HEAD~1",
|
|
153
|
+
{ encoding: "utf-8", cwd }
|
|
154
|
+
).trim().split("\n").filter((f) => /^src\/[^/]+\//.test(f) && (f.endsWith(".ts") || f.endsWith(".tsx")));
|
|
155
|
+
|
|
156
|
+
const crossContextImports = [];
|
|
157
|
+
for (const file of changedSrcFiles) {
|
|
158
|
+
if (!fs.existsSync(path.join(cwd, file))) continue;
|
|
159
|
+
const ownContext = file.split("/")[1];
|
|
160
|
+
const src = fs.readFileSync(path.join(cwd, file), "utf-8");
|
|
161
|
+
const imports = [...src.matchAll(/from\s+['"](\.\.\/.+?)['"]/g)].map((m) => m[1]);
|
|
162
|
+
for (const imp of imports) {
|
|
163
|
+
const resolved = path.normalize(path.join(path.dirname(file), imp));
|
|
164
|
+
const parts = resolved.split(path.sep);
|
|
165
|
+
const srcIdx = parts.indexOf("src");
|
|
166
|
+
if (srcIdx !== -1 && parts[srcIdx + 1] && parts[srcIdx + 1] !== ownContext) {
|
|
167
|
+
crossContextImports.push(`${file} → ${parts.slice(srcIdx).join("/")}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (crossContextImports.length > 0) {
|
|
172
|
+
issues.push({
|
|
173
|
+
severity: "warning",
|
|
174
|
+
message: `${crossContextImports.length} suspicious cross-context import(s) detected`,
|
|
175
|
+
details: crossContextImports.slice(0, 3).join("; "),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} catch { /* ignore */ }
|
|
179
|
+
|
|
180
|
+
// 7. TDD health — check test-to-source file ratio
|
|
181
|
+
try {
|
|
182
|
+
const allSrc = execSync(
|
|
183
|
+
"find src -name '*.ts' -not -name '*.test.ts' -not -name '*.spec.ts' 2>/dev/null | wc -l",
|
|
184
|
+
{ encoding: "utf-8", cwd }
|
|
185
|
+
).trim();
|
|
186
|
+
const allTests = execSync(
|
|
187
|
+
"find src -name '*.test.ts' -o -name '*.spec.ts' 2>/dev/null | wc -l",
|
|
188
|
+
{ encoding: "utf-8", cwd }
|
|
189
|
+
).trim();
|
|
190
|
+
const srcCount = parseInt(allSrc, 10) || 0;
|
|
191
|
+
const testCount = parseInt(allTests, 10) || 0;
|
|
192
|
+
if (srcCount > 0) {
|
|
193
|
+
const ratio = testCount / srcCount;
|
|
194
|
+
if (ratio < 0.3) {
|
|
195
|
+
issues.push({
|
|
196
|
+
severity: "warning",
|
|
197
|
+
message: `TDD health: test-to-source ratio is ${(ratio * 100).toFixed(0)}% (${testCount} tests / ${srcCount} sources) — target ≥ 30%`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch { /* ignore — src/ may not exist */ }
|
|
202
|
+
|
|
203
|
+
// 8. Check for TODO/FIXME/HACK comments in changed files
|
|
204
|
+
try {
|
|
205
|
+
const diff = execSync(
|
|
206
|
+
"git diff --cached --name-only 2>/dev/null || git diff --name-only HEAD~1",
|
|
207
|
+
{ encoding: "utf-8", cwd }
|
|
208
|
+
).trim();
|
|
209
|
+
if (diff) {
|
|
210
|
+
const files = diff.split("\n").filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
try {
|
|
213
|
+
const content = fs.readFileSync(path.join(cwd, file), "utf-8");
|
|
214
|
+
const todos = content.match(/\/\/\s*(TODO|FIXME|HACK|XXX):/gi) || [];
|
|
215
|
+
if (todos.length > 0) {
|
|
216
|
+
issues.push({ severity: "info", message: `${file}: ${todos.length} TODO/FIXME comment(s)` });
|
|
217
|
+
}
|
|
218
|
+
} catch { /* file may not exist */ }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch { /* ignore */ }
|
|
222
|
+
|
|
223
|
+
// Output
|
|
224
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
225
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
226
|
+
const infos = issues.filter((i) => i.severity === "info");
|
|
227
|
+
|
|
228
|
+
if (errors.length > 0) {
|
|
229
|
+
console.log(`\n❌ Quality Gate FAILED (${errors.length} error(s)):`);
|
|
230
|
+
for (const e of errors) {
|
|
231
|
+
console.log(` ❌ ${e.message}`);
|
|
232
|
+
if (e.details) console.log(` ${e.details.split("\n")[0]}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (warnings.length > 0) {
|
|
237
|
+
console.log(`\n⚠️ ${warnings.length} warning(s):`);
|
|
238
|
+
for (const w of warnings) {
|
|
239
|
+
console.log(` ⚠️ ${w.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (infos.length > 0) {
|
|
244
|
+
console.log(`\nℹ️ ${infos.length} note(s):`);
|
|
245
|
+
for (const i of infos) console.log(` ℹ️ ${i.message}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
249
|
+
console.log("✅ Quality gate passed");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* UserPromptSubmit Hook
|
|
6
|
+
* Injects minimal draht planning context before each user prompt is sent to the model.
|
|
7
|
+
* Only activates in projects with .planning/ and adds at most a short reminder line.
|
|
8
|
+
* Keep output tiny — this runs on every prompt.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require("node:fs");
|
|
12
|
+
const path = require("node:path");
|
|
13
|
+
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const PLANNING = path.join(cwd, ".planning");
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(PLANNING)) {
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const statePath = path.join(PLANNING, "STATE.md");
|
|
22
|
+
if (!fs.existsSync(statePath)) {
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const state = fs.readFileSync(statePath, "utf-8");
|
|
28
|
+
const phaseMatch = state.match(/## Current Phase: (.+)/);
|
|
29
|
+
const statusMatch = state.match(/## Status: (.+)/);
|
|
30
|
+
if (phaseMatch && statusMatch) {
|
|
31
|
+
// Print a single-line reminder. Claude Code prepends stdout to the prompt context.
|
|
32
|
+
console.log(`[draht] ${phaseMatch[1].trim()} — ${statusMatch[1].trim()}`);
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
|
|
36
|
+
process.exit(0);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session Start Hook
|
|
6
|
+
* Surfaces draht planning state when a Claude Code session starts in a project.
|
|
7
|
+
* - Reports current phase and task from .planning/STATE.md
|
|
8
|
+
* - Flags CONTINUE-HERE.md if the previous session was paused
|
|
9
|
+
* - Silent in projects without .planning/
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const PLANNING = path.join(cwd, ".planning");
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(PLANNING)) {
|
|
19
|
+
// No draht planning — silent
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const lines = [];
|
|
24
|
+
|
|
25
|
+
// STATE.md — current phase + status
|
|
26
|
+
const statePath = path.join(PLANNING, "STATE.md");
|
|
27
|
+
if (fs.existsSync(statePath)) {
|
|
28
|
+
try {
|
|
29
|
+
const state = fs.readFileSync(statePath, "utf-8");
|
|
30
|
+
const phaseMatch = state.match(/## Current Phase: (.+)/);
|
|
31
|
+
const statusMatch = state.match(/## Status: (.+)/);
|
|
32
|
+
const activityMatch = state.match(/## Last Activity: (.+)/);
|
|
33
|
+
if (phaseMatch) lines.push(`Phase: ${phaseMatch[1].trim()}`);
|
|
34
|
+
if (statusMatch) lines.push(`Status: ${statusMatch[1].trim()}`);
|
|
35
|
+
if (activityMatch) lines.push(`Last activity: ${activityMatch[1].trim()}`);
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// CONTINUE-HERE.md — resume marker
|
|
40
|
+
const continuePath = path.join(PLANNING, "CONTINUE-HERE.md");
|
|
41
|
+
if (fs.existsSync(continuePath)) {
|
|
42
|
+
lines.push("");
|
|
43
|
+
lines.push("CONTINUE-HERE.md present — the previous session was paused.");
|
|
44
|
+
lines.push("Run /resume-work to continue, or read .planning/CONTINUE-HERE.md for the handoff.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (lines.length > 0) {
|
|
48
|
+
console.log("━ Draht planning state ━");
|
|
49
|
+
for (const line of lines) console.log(line);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
process.exit(0);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ddd-workflow
|
|
3
|
+
description: Domain-driven design discipline — bounded contexts, ubiquitous language, aggregates, domain events, context maps, and how the .planning/DOMAIN.md file drives code structure and naming. Use whenever the user is modelling a new domain, extracting domain concepts from existing code, deciding where code should live, or naming things.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DDD Workflow
|
|
7
|
+
|
|
8
|
+
Draht embeds domain-driven design into project initialization, planning, and execution. The `.planning/DOMAIN.md` file is the single source of truth for domain concepts.
|
|
9
|
+
|
|
10
|
+
## .planning/DOMAIN.md Structure
|
|
11
|
+
|
|
12
|
+
```markdown
|
|
13
|
+
## Bounded Contexts
|
|
14
|
+
- **Billing** — everything about invoices, payments, subscriptions
|
|
15
|
+
- **Catalog** — products, pricing, availability
|
|
16
|
+
- **Fulfillment** — order processing, shipping, returns
|
|
17
|
+
|
|
18
|
+
## Ubiquitous Language
|
|
19
|
+
- **Invoice** — a document requesting payment for delivered goods or services
|
|
20
|
+
- **Order** — a customer's request to purchase goods, before fulfillment
|
|
21
|
+
- **Line Item** — a single row on an invoice or order
|
|
22
|
+
- **SKU** — a unique identifier for a product variant in the catalog
|
|
23
|
+
|
|
24
|
+
## Context Map
|
|
25
|
+
- Billing ← Catalog (downstream — billing reads product info)
|
|
26
|
+
- Fulfillment ← Billing (downstream — fulfillment needs invoice status)
|
|
27
|
+
- Shared kernel: Money, TaxRate (used by Billing and Fulfillment)
|
|
28
|
+
|
|
29
|
+
## Aggregates
|
|
30
|
+
### Billing
|
|
31
|
+
- Invoice (root) — LineItem, Payment
|
|
32
|
+
- Subscription (root) — BillingCycle
|
|
33
|
+
|
|
34
|
+
### Catalog
|
|
35
|
+
- Product (root) — Variant, Price
|
|
36
|
+
|
|
37
|
+
## Domain Events
|
|
38
|
+
- `InvoiceIssued` — Billing → Fulfillment, Notification
|
|
39
|
+
- `PaymentReceived` — Billing → Notification
|
|
40
|
+
- `OrderShipped` — Fulfillment → Notification, Customer
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## The Five Rules
|
|
44
|
+
|
|
45
|
+
### 1. Bounded contexts shape the code
|
|
46
|
+
- File/module structure mirrors bounded contexts: `src/billing/`, `src/catalog/`, `src/fulfillment/`
|
|
47
|
+
- Each context owns its aggregates, value objects, services, and domain events
|
|
48
|
+
- Cross-context imports are suspicious — prefer domain events or ACL adapters
|
|
49
|
+
|
|
50
|
+
### 2. Code uses the ubiquitous language
|
|
51
|
+
- Class names, method names, variable names must match the glossary
|
|
52
|
+
- If you need a new term, update `DOMAIN.md` **first**, then write the code
|
|
53
|
+
- Never invent terms in code that aren't in the glossary
|
|
54
|
+
|
|
55
|
+
### 3. Aggregates enforce invariants
|
|
56
|
+
- Each aggregate has one root entity
|
|
57
|
+
- All writes go through the root — never modify child entities directly from outside
|
|
58
|
+
- Aggregate boundaries align with transaction boundaries
|
|
59
|
+
- Aggregates reference each other by ID, not by reference
|
|
60
|
+
|
|
61
|
+
### 4. Domain events cross context boundaries
|
|
62
|
+
- Upstream context publishes an event (`InvoiceIssued`)
|
|
63
|
+
- Downstream contexts subscribe and react (Notification sends email, Fulfillment releases order)
|
|
64
|
+
- No direct function call from Billing into Fulfillment — always via event
|
|
65
|
+
|
|
66
|
+
### 5. Shared kernel is explicit
|
|
67
|
+
- If two contexts must share a type (e.g. `Money`, `TaxRate`), put it in `src/shared/` and document it in the Context Map
|
|
68
|
+
- Shared kernel changes are high-cost — they affect multiple contexts
|
|
69
|
+
- Prefer duplication over coupling when in doubt
|
|
70
|
+
|
|
71
|
+
## The Post-Phase Domain Health Check
|
|
72
|
+
|
|
73
|
+
The `gsd-post-phase.cjs` hook checks `DOMAIN.md` after each phase:
|
|
74
|
+
- Is `## Bounded Contexts` section present?
|
|
75
|
+
- Is `## Ubiquitous Language` section present?
|
|
76
|
+
- Count of unique PascalCase terms (proxy for glossary size)
|
|
77
|
+
|
|
78
|
+
The `gsd-quality-gate.cjs` script also runs a domain validator that compares identifiers in code against the glossary and flags unknown terms.
|
|
79
|
+
|
|
80
|
+
## Extracting Domain from Existing Code
|
|
81
|
+
|
|
82
|
+
When running `/init-project` or `/map-codebase` on a codebase that wasn't built with DDD:
|
|
83
|
+
|
|
84
|
+
1. List top-level `src/` subdirectories — candidates for bounded contexts
|
|
85
|
+
2. Scan PascalCase class / interface / type names — candidates for entities and value objects
|
|
86
|
+
3. Scan repeated nouns in function names — candidates for domain concepts
|
|
87
|
+
4. Look for cross-directory imports — candidates for context coupling to fix
|
|
88
|
+
5. Write `DOMAIN.md` with what you found + what should exist
|
|
89
|
+
6. Use subsequent phases to refactor toward the target model
|
|
90
|
+
|
|
91
|
+
## Anti-patterns
|
|
92
|
+
|
|
93
|
+
**Anemic domain model** — entities that are just data bags with no behaviour. Push logic into the entities.
|
|
94
|
+
|
|
95
|
+
**Scattered aggregates** — one aggregate's logic spread across multiple contexts. Consolidate or introduce an ACL.
|
|
96
|
+
|
|
97
|
+
**Terminology drift** — the same concept called different things in different files. Fix in `DOMAIN.md` first, rename code second.
|
|
98
|
+
|
|
99
|
+
**Shared database** — multiple contexts writing to the same tables without explicit shared-kernel agreement. Break the coupling.
|
|
100
|
+
|
|
101
|
+
**Direct cross-context imports** — `import { ... } from '../billing/...'` in `src/fulfillment/`. Use domain events or ACL adapters.
|
|
102
|
+
|
|
103
|
+
## When to Update DOMAIN.md
|
|
104
|
+
|
|
105
|
+
- Before writing code that introduces a new term → add it to the glossary first
|
|
106
|
+
- During `/discuss-phase` when gray areas reveal missing concepts
|
|
107
|
+
- After `/verify-work` when the reviewer agent flags domain language drift
|
|
108
|
+
- Whenever a refactor reveals that existing names don't match reality
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gsd-workflow
|
|
3
|
+
description: Draht's Get Shit Done workflow — how to use /new-project, /discuss-phase, /plan-phase, /execute-phase, /verify-work, /next-milestone, /pause-work, /resume-work, /progress, /fix, /quick and the .planning/ directory structure to drive a project from idea to shipping. Use when the user asks how to plan work, structure a project, set up milestones, track progress, or wants to start using draht's workflow.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GSD (Get Shit Done) Workflow
|
|
7
|
+
|
|
8
|
+
Draht's GSD workflow is a milestone → phase → plan → task hierarchy that lives in `.planning/` and is driven by slash commands + hooks.
|
|
9
|
+
|
|
10
|
+
## Directory Structure
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
.planning/
|
|
14
|
+
├── PROJECT.md # what are we building
|
|
15
|
+
├── REQUIREMENTS.md # v1 / v2 / out-of-scope
|
|
16
|
+
├── ROADMAP.md # phases grouped into milestones
|
|
17
|
+
├── DOMAIN.md # bounded contexts + ubiquitous language (DDD)
|
|
18
|
+
├── TEST-STRATEGY.md # test framework, levels, coverage
|
|
19
|
+
├── STATE.md # current phase, status, last activity
|
|
20
|
+
├── CONTINUE-HERE.md # handoff doc (only when paused)
|
|
21
|
+
├── execution-log.jsonl # append-only task execution log
|
|
22
|
+
├── phases/
|
|
23
|
+
│ └── 01-phase-slug/
|
|
24
|
+
│ ├── 01-01-PLAN.md
|
|
25
|
+
│ ├── 01-01-SUMMARY.md
|
|
26
|
+
│ └── 01-02-PLAN.md
|
|
27
|
+
└── phase-N-report.md # generated by post-phase hook
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## The Cycle
|
|
31
|
+
|
|
32
|
+
### Project initialization (once)
|
|
33
|
+
- **`/new-project`** — greenfield: questioning → domain model → requirements → roadmap
|
|
34
|
+
- **`/init-project`** — existing codebase: map → extract domain → questioning → roadmap
|
|
35
|
+
- **`/map-codebase`** — standalone codebase analysis
|
|
36
|
+
|
|
37
|
+
### Per-phase cycle (fresh session between each step)
|
|
38
|
+
1. **`/discuss-phase N`** — capture decisions, gray areas, domain terms
|
|
39
|
+
2. **`/plan-phase N`** — create atomic execution plans (parallel via architect subagents)
|
|
40
|
+
3. **`/execute-phase N`** — TDD red→green→refactor (parallel via implementer subagents)
|
|
41
|
+
4. **`/verify-work N`** — parallel verifier + security-auditor + reviewer, produce UAT report
|
|
42
|
+
|
|
43
|
+
Start a fresh session (`/clear`) between steps. Each command assumes a clean context.
|
|
44
|
+
|
|
45
|
+
### Milestone transition
|
|
46
|
+
- **`/next-milestone`** — only after ALL phases in the current milestone are `complete`
|
|
47
|
+
|
|
48
|
+
### Session continuity
|
|
49
|
+
- **`/pause-work`** — create `CONTINUE-HERE.md` with in-progress state
|
|
50
|
+
- **`/resume-work`** — read handoff, verify state, continue
|
|
51
|
+
- **`/progress`** — show current position in the roadmap
|
|
52
|
+
|
|
53
|
+
### Ad-hoc
|
|
54
|
+
- **`/quick`** — small task with tracking but without full phase ceremony
|
|
55
|
+
- **`/fix`** — bug fix with TDD discipline (reproducing test first)
|
|
56
|
+
- **`/review`** — parallel code review + security audit
|
|
57
|
+
- **`/atomic-commit`** — analyze diff, split into atomic conventional commits
|
|
58
|
+
|
|
59
|
+
## Task Format (XML inside PLAN.md files)
|
|
60
|
+
|
|
61
|
+
```xml
|
|
62
|
+
<task type="auto">
|
|
63
|
+
<n>Task name</n>
|
|
64
|
+
<context>Bounded context</context>
|
|
65
|
+
<domain>Aggregates/entities touched</domain>
|
|
66
|
+
<files>affected files</files>
|
|
67
|
+
<test>RED phase — write failing tests first</test>
|
|
68
|
+
<action>GREEN phase — minimal impl to pass tests</action>
|
|
69
|
+
<refactor>REFACTOR phase — improve without breaking tests</refactor>
|
|
70
|
+
<verify>How to verify (tests pass + manual check)</verify>
|
|
71
|
+
<done>What "done" looks like as assertions</done>
|
|
72
|
+
</task>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Task types: `auto`, `checkpoint:human-verify`, `checkpoint:decision`.
|
|
76
|
+
|
|
77
|
+
## Hooks
|
|
78
|
+
|
|
79
|
+
The plugin ships workflow hooks under `${CLAUDE_PLUGIN_ROOT}/scripts/`:
|
|
80
|
+
|
|
81
|
+
- `gsd-pre-execute.cjs <phase>` — preconditions before execution (DOMAIN.md, plans, uncommitted changes)
|
|
82
|
+
- `gsd-post-task.cjs <phase> <plan> <task> <status> [commit]` — record result + type check + tests + TDD cycle check
|
|
83
|
+
- `gsd-post-phase.cjs <phase>` — generate phase report, update ROADMAP status
|
|
84
|
+
- `gsd-quality-gate.cjs [--strict]` — lint + typecheck + test + coverage against `.planning/config.json` threshold
|
|
85
|
+
|
|
86
|
+
These are invoked from inside commands (not as Claude Code lifecycle hooks).
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
`.planning/config.json` (optional):
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"hooks": {
|
|
95
|
+
"coverageThreshold": 80,
|
|
96
|
+
"tddMode": "advisory",
|
|
97
|
+
"qualityGateStrict": false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `tddMode: "strict"` — post-task hook aborts on green: commit without preceding red:
|
|
103
|
+
- `tddMode: "advisory"` — logs a warning instead
|
|
104
|
+
- `qualityGateStrict: true` — fail the gate on any lint/type/test/coverage miss
|
|
105
|
+
|
|
106
|
+
## Key Rules
|
|
107
|
+
|
|
108
|
+
- One phase at a time, one cycle step per session
|
|
109
|
+
- `/next-milestone` ONLY after every phase in the current milestone is verified
|
|
110
|
+
- Fix plans include a reproducing test before any implementation
|
|
111
|
+
- Never skip verification — it's the only thing that marks a phase complete
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tdd-workflow
|
|
3
|
+
description: Test-driven development discipline — red→green→refactor cycle, commit conventions (red:, green:, refactor:), TDD cycle violations, reproducing tests before fixes, and how to write tests that actually drive design. Use whenever the user is writing code that has testable behaviour, fixing bugs, or asks about TDD.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TDD Workflow
|
|
7
|
+
|
|
8
|
+
Draht enforces strict test-driven development through commit conventions, post-task hooks, and the plan task format.
|
|
9
|
+
|
|
10
|
+
## The Cycle
|
|
11
|
+
|
|
12
|
+
### RED — Write a failing test
|
|
13
|
+
1. Write a test that describes the behaviour you want
|
|
14
|
+
2. Run the test runner — it MUST fail for the right reason (not a syntax error, not a missing import)
|
|
15
|
+
3. Commit with prefix `red:`
|
|
16
|
+
```
|
|
17
|
+
git add <test-files>
|
|
18
|
+
git commit -m "red: <what the test proves>"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### GREEN — Make it pass with the smallest possible change
|
|
22
|
+
1. Write the minimum implementation that makes the failing test pass
|
|
23
|
+
2. Run the test — confirm it passes
|
|
24
|
+
3. Run the full test suite — confirm no regressions
|
|
25
|
+
4. Commit with prefix `green:`
|
|
26
|
+
```
|
|
27
|
+
git add <impl-files>
|
|
28
|
+
git commit -m "green: <task name>"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### REFACTOR — Improve structure while staying green
|
|
32
|
+
1. Tests must stay green after every change — run them often
|
|
33
|
+
2. Extract value objects, push logic into domain layer, remove duplication
|
|
34
|
+
3. Keep to the ubiquitous language from `.planning/DOMAIN.md`
|
|
35
|
+
4. Commit with prefix `refactor:`
|
|
36
|
+
```
|
|
37
|
+
git add <files>
|
|
38
|
+
git commit -m "refactor: <what was improved>"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Rules
|
|
42
|
+
|
|
43
|
+
1. **Never write implementation before a failing test.** If you find yourself writing code that doesn't have a failing test waiting for it, stop and write the test first.
|
|
44
|
+
|
|
45
|
+
2. **A test that passes on first run is suspect.** It means you're not testing what you think. Make it fail by breaking the implementation temporarily — does it fail? If not, the test is useless.
|
|
46
|
+
|
|
47
|
+
3. **One red → one green → optional refactor.** Keep cycles small. A red commit with 20 failing tests is too big.
|
|
48
|
+
|
|
49
|
+
4. **Test behaviour, not implementation.** Write tests against the public API. Tests that mock everything are tests of the mocks.
|
|
50
|
+
|
|
51
|
+
5. **Domain tests use domain language.** Class names, test names, fixture names must match `.planning/DOMAIN.md` if it exists. Domain tests read like specs.
|
|
52
|
+
|
|
53
|
+
6. **Fix bugs with a reproducing test first.** No exceptions. The test must fail before the fix, pass after.
|
|
54
|
+
|
|
55
|
+
## TDD Cycle Violations
|
|
56
|
+
|
|
57
|
+
The post-task hook (`gsd-post-task.cjs`) checks commit history for cycle violations:
|
|
58
|
+
|
|
59
|
+
- A `green:` commit with no preceding `red:` commit for the same task → violation
|
|
60
|
+
- In `strict` mode: the hook aborts execution
|
|
61
|
+
- In `advisory` mode: the hook logs a warning to `.planning/execution-log.jsonl`
|
|
62
|
+
|
|
63
|
+
Set the mode in `.planning/config.json`:
|
|
64
|
+
```json
|
|
65
|
+
{ "hooks": { "tddMode": "strict" } }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## When to Skip the Cycle
|
|
69
|
+
|
|
70
|
+
Only skip TDD for:
|
|
71
|
+
- Pure configuration (tsconfig, biome, prettier)
|
|
72
|
+
- Documentation-only changes
|
|
73
|
+
- Generated code (auto-generated clients, schema bindings)
|
|
74
|
+
- Mechanical refactors with no behaviour change (e.g., rename)
|
|
75
|
+
|
|
76
|
+
Never skip for:
|
|
77
|
+
- Bug fixes
|
|
78
|
+
- New features
|
|
79
|
+
- Changes to domain logic
|
|
80
|
+
- Changes to APIs
|
|
81
|
+
|
|
82
|
+
## The Plan Task Format Drives TDD
|
|
83
|
+
|
|
84
|
+
Plan tasks use `<test>`, `<action>`, `<refactor>` sections precisely to force the cycle:
|
|
85
|
+
|
|
86
|
+
```xml
|
|
87
|
+
<task type="auto">
|
|
88
|
+
<n>Add user authentication</n>
|
|
89
|
+
<test>
|
|
90
|
+
RED phase: Write failing tests FIRST.
|
|
91
|
+
- test/auth.test.ts: valid credentials → returns session token
|
|
92
|
+
- test/auth.test.ts: invalid password → throws UnauthorizedError
|
|
93
|
+
- test/auth.test.ts: expired token → returns null
|
|
94
|
+
</test>
|
|
95
|
+
<action>
|
|
96
|
+
GREEN phase: Minimal implementation.
|
|
97
|
+
- src/auth/login.ts: verify password hash, return token
|
|
98
|
+
- src/auth/session.ts: read/write session store
|
|
99
|
+
</action>
|
|
100
|
+
<refactor>
|
|
101
|
+
Extract password verification into domain layer. Keep session IO at the boundary.
|
|
102
|
+
</refactor>
|
|
103
|
+
</task>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
When executing, the implementer subagent follows this order strictly. Commits get the `red:` / `green:` / `refactor:` prefixes automatically.
|
|
107
|
+
|
|
108
|
+
## Coverage Goals
|
|
109
|
+
|
|
110
|
+
`.planning/config.json` sets the threshold:
|
|
111
|
+
```json
|
|
112
|
+
{ "hooks": { "coverageThreshold": 80 } }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The `gsd-quality-gate.cjs` script enforces this at verification time. Coverage is a floor, not a target — aim for the meaningful paths.
|