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,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Acceptance testing of completed phase work (parallel verifier, security-auditor, and reviewer subagents)
|
|
3
|
+
argument-hint: "<phase-number>"
|
|
4
|
+
allowed-tools: Bash, Read, Write, Edit, Task
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /verify-work
|
|
8
|
+
|
|
9
|
+
Walk through acceptance testing of completed phase work, using subagents for parallel verification.
|
|
10
|
+
|
|
11
|
+
Phase: $1
|
|
12
|
+
|
|
13
|
+
> **Tool note**: Invoke `draht-tools <subcommand>` as `node "${CLAUDE_PLUGIN_ROOT}/bin/draht-tools.cjs" <subcommand>`. For subagents, use the **Task tool** and dispatch multiple in parallel (single assistant turn = multiple Task tool calls).
|
|
14
|
+
|
|
15
|
+
## Atomic Reasoning
|
|
16
|
+
|
|
17
|
+
Before verifying, decompose phase acceptance into atomic reasoning units:
|
|
18
|
+
|
|
19
|
+
**For each deliverable:**
|
|
20
|
+
1. **State the logical component** — What was this deliverable meant to produce? What user value does it provide?
|
|
21
|
+
2. **Validate independence** — Can this deliverable be tested independently? What are its dependencies?
|
|
22
|
+
3. **Verify correctness** — What tests prove it works? What edge cases must pass? What security concerns exist?
|
|
23
|
+
|
|
24
|
+
**Synthesize verification strategy:**
|
|
25
|
+
- Group parallel verification tasks (test suite, security audit, code review, domain compliance)
|
|
26
|
+
- Map each deliverable to specific test scenarios and acceptance criteria
|
|
27
|
+
- Identify critical vs optional checks
|
|
28
|
+
- Plan fix strategies for potential failures
|
|
29
|
+
|
|
30
|
+
## Steps
|
|
31
|
+
1. Run `draht-tools extract-deliverables $1` to get testable items
|
|
32
|
+
|
|
33
|
+
2. **Run parallel verification via the Task tool**:
|
|
34
|
+
Dispatch these three subagents in parallel (single assistant turn, three Task tool calls):
|
|
35
|
+
|
|
36
|
+
- **Task tool** with `subagent_type: "verifier"` and prompt:
|
|
37
|
+
"Run the full test suite for this project. Check package.json for the test command. Record pass/fail counts. Then run any available lint and typecheck commands (e.g. npm run check, npm run lint, npx tsc --noEmit). Report all results with error details."
|
|
38
|
+
|
|
39
|
+
- **Task tool** with `subagent_type: "security-auditor"` and prompt:
|
|
40
|
+
"Audit the recent code changes (use git log and git diff to find them). Check for injection risks, auth bypasses, secrets in code, unsafe patterns. Report findings by severity."
|
|
41
|
+
|
|
42
|
+
- **Task tool** with `subagent_type: "reviewer"` and prompt:
|
|
43
|
+
"Review the recent code changes (use git log and git diff). Check domain language compliance against `.planning/DOMAIN.md` if it exists — scan for identifiers not in the glossary and cross-context boundary violations. Report findings."
|
|
44
|
+
|
|
45
|
+
3. Run the quality gate check:
|
|
46
|
+
```bash
|
|
47
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/gsd-quality-gate.cjs"
|
|
48
|
+
```
|
|
49
|
+
4. Collect results from all subagents and the quality gate
|
|
50
|
+
5. Walk the user through each deliverable one at a time, incorporating findings from the parallel checks
|
|
51
|
+
6. Record results (pass/fail/partially/skip)
|
|
52
|
+
7. For failures: diagnose and create fix plans via `draht-tools create-fix-plan $1 P`
|
|
53
|
+
- Fix plans MUST include a reproducing test that demonstrates the failure before any implementation
|
|
54
|
+
8. Write UAT report: `draht-tools write-uat $1`
|
|
55
|
+
- Report must include: test health summary (pass/fail/coverage), security audit results, domain model status (any glossary violations), deliverable results
|
|
56
|
+
9. If all passed: mark phase complete.
|
|
57
|
+
- If more phases remain in the milestone: tell the user to start a fresh session and run `/discuss-phase N+1`.
|
|
58
|
+
- If ALL phases in the milestone are complete: tell the user to start a fresh session and run `/next-milestone`.
|
|
59
|
+
10. If failures: route to `/execute-phase $1 --gaps-only`
|
|
60
|
+
|
|
61
|
+
## Workflow
|
|
62
|
+
This is the last step in the per-phase cycle:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
/discuss-phase N → /plan-phase N → /execute-phase N → /verify-work N
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
After verify-work passes:
|
|
69
|
+
- More phases remaining → `/discuss-phase N+1`
|
|
70
|
+
- ALL phases in milestone verified → `/next-milestone`
|
|
71
|
+
|
|
72
|
+
`/next-milestone` is ONLY for generating new phases after every phase in the current milestone is complete.
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.cjs\""
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"UserPromptSubmit": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/prompt-context.cjs\""
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "draht-claude",
|
|
3
|
+
"version": "2026.4.23",
|
|
4
|
+
"description": "Draht GSD, multi-agent, TDD and DDD workflows as a Claude Code plugin. Install with `npx draht-claude install`.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"draht-claude": "./cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
".claude-plugin",
|
|
11
|
+
"bin",
|
|
12
|
+
"scripts",
|
|
13
|
+
"hooks",
|
|
14
|
+
"commands",
|
|
15
|
+
"agents",
|
|
16
|
+
"skills",
|
|
17
|
+
"cli.mjs",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node cli.mjs --help > /dev/null && node scripts/gsd-pre-execute.cjs --self-check 2>/dev/null || true"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"claude-code",
|
|
27
|
+
"claude-code-plugin",
|
|
28
|
+
"draht",
|
|
29
|
+
"gsd",
|
|
30
|
+
"planning",
|
|
31
|
+
"tdd",
|
|
32
|
+
"ddd",
|
|
33
|
+
"multi-agent",
|
|
34
|
+
"workflow",
|
|
35
|
+
"installer"
|
|
36
|
+
],
|
|
37
|
+
"author": "Mario Zechner",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/draht-dev/draht.git",
|
|
45
|
+
"directory": "packages/draht-claude"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=20.6.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Draht Post-Phase Hook
|
|
6
|
+
* Runs after phase completion to generate reports and update state.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node gsd-post-phase.js <phase-number>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require("node:fs");
|
|
12
|
+
const path = require("node:path");
|
|
13
|
+
|
|
14
|
+
const phaseNum = process.argv[2];
|
|
15
|
+
if (!phaseNum) {
|
|
16
|
+
console.error("Usage: gsd-post-phase.js <phase-number>");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PLANNING = ".planning";
|
|
21
|
+
const LOG_FILE = path.join(PLANNING, "execution-log.jsonl");
|
|
22
|
+
|
|
23
|
+
// 1. Read execution log for this phase
|
|
24
|
+
let entries = [];
|
|
25
|
+
if (fs.existsSync(LOG_FILE)) {
|
|
26
|
+
entries = fs.readFileSync(LOG_FILE, "utf-8")
|
|
27
|
+
.split("\n")
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.map((l) => JSON.parse(l))
|
|
30
|
+
.filter((e) => e.phase === parseInt(phaseNum, 10));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const passed = entries.filter((e) => e.status === "pass").length;
|
|
34
|
+
const failed = entries.filter((e) => e.status === "fail").length;
|
|
35
|
+
const skipped = entries.filter((e) => e.status === "skip").length;
|
|
36
|
+
const warnings = entries.filter((e) => e.warning).length;
|
|
37
|
+
|
|
38
|
+
// 2. Compute TDD and domain health metrics
|
|
39
|
+
const tddViolations = entries.filter((e) => e.status === "tdd-violation").length;
|
|
40
|
+
|
|
41
|
+
// TDD commit counts: derive from git log scoped to commits since phase start
|
|
42
|
+
// (use the earliest entry timestamp as the lower bound)
|
|
43
|
+
let tddSummary = "⚠️ No TDD commit data in log";
|
|
44
|
+
try {
|
|
45
|
+
const { execSync } = require("node:child_process");
|
|
46
|
+
// Scope to commits that follow TDD naming convention for this phase
|
|
47
|
+
const gitLog = execSync(
|
|
48
|
+
`git log --format=%s --grep="^[0-9]" -E --extended-regexp 2>/dev/null || git log --format=%s 2>/dev/null`,
|
|
49
|
+
{ encoding: "utf-8" }
|
|
50
|
+
).trim();
|
|
51
|
+
const lines = gitLog.split("\n");
|
|
52
|
+
const redCount = lines.filter((l) => /^red:/i.test(l)).length;
|
|
53
|
+
const greenCount = lines.filter((l) => /^green:/i.test(l)).length;
|
|
54
|
+
const refactorCount = lines.filter((l) => /^refactor:/i.test(l)).length;
|
|
55
|
+
if (redCount + greenCount + refactorCount > 0) {
|
|
56
|
+
tddSummary = `🔴 Red: ${redCount} 🟢 Green: ${greenCount} 🔵 Refactor: ${refactorCount}`;
|
|
57
|
+
} else {
|
|
58
|
+
tddSummary = "⚠️ No red:/green:/refactor: commits found — TDD cycle may not have been followed";
|
|
59
|
+
}
|
|
60
|
+
} catch { /* ignore */ }
|
|
61
|
+
|
|
62
|
+
// Domain health: check DOMAIN.md presence and glossary size
|
|
63
|
+
let domainSummary = "⚠️ .planning/DOMAIN.md not found";
|
|
64
|
+
const domainPath = path.join(PLANNING, "DOMAIN.md");
|
|
65
|
+
if (fs.existsSync(domainPath)) {
|
|
66
|
+
const domainContent = fs.readFileSync(domainPath, "utf-8");
|
|
67
|
+
const termMatches = [...domainContent.matchAll(/\b([A-Z][a-zA-Z0-9]+)\b/g)];
|
|
68
|
+
const termCount = new Set(termMatches.map((m) => m[1])).size;
|
|
69
|
+
const hasContexts = domainContent.includes("## Bounded Contexts");
|
|
70
|
+
const hasGlossary = domainContent.includes("## Ubiquitous Language");
|
|
71
|
+
domainSummary = `${hasContexts ? "✅" : "❌"} Bounded Contexts ${hasGlossary ? "✅" : "❌"} Ubiquitous Language 📖 ~${termCount} terms`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// TEST-STRATEGY health
|
|
75
|
+
let testStrategySummary = "⚠️ .planning/TEST-STRATEGY.md not found";
|
|
76
|
+
if (fs.existsSync(path.join(PLANNING, "TEST-STRATEGY.md"))) {
|
|
77
|
+
testStrategySummary = "✅ TEST-STRATEGY.md present";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Generate phase report
|
|
81
|
+
const reportPath = path.join(PLANNING, `phase-${phaseNum}-report.md`);
|
|
82
|
+
const report = `# Phase ${phaseNum} Execution Report
|
|
83
|
+
|
|
84
|
+
Generated: ${new Date().toISOString().replace("T", " ").slice(0, 19)}
|
|
85
|
+
|
|
86
|
+
## Task Results
|
|
87
|
+
- ✅ Passed: ${passed}
|
|
88
|
+
- ❌ Failed: ${failed}
|
|
89
|
+
- ⏭️ Skipped: ${skipped}
|
|
90
|
+
- ⚠️ Warnings: ${warnings}
|
|
91
|
+
|
|
92
|
+
## Execution Log
|
|
93
|
+
| Timestamp | Plan | Task | Status | Commit |
|
|
94
|
+
|-----------|------|------|--------|--------|
|
|
95
|
+
${entries.map((e) => `| ${e.timestamp.slice(0, 19)} | ${e.plan} | ${e.task} | ${e.status} | ${e.commit || "-"} |`).join("\n")}
|
|
96
|
+
|
|
97
|
+
## Quality Gate
|
|
98
|
+
${failed === 0 ? "✅ All tasks passed — ready for verification" : `❌ ${failed} failure(s) — fix plans may be needed`}
|
|
99
|
+
${warnings > 0 ? `⚠️ ${warnings} task(s) introduced type errors` : "✅ No type errors introduced"}
|
|
100
|
+
|
|
101
|
+
## TDD Health
|
|
102
|
+
${tddSummary}
|
|
103
|
+
${tddViolations > 0 ? `❌ ${tddViolations} TDD cycle violation(s) recorded (green: without red:)` : "✅ No TDD cycle violations recorded"}
|
|
104
|
+
|
|
105
|
+
## Domain Model Health
|
|
106
|
+
${domainSummary}
|
|
107
|
+
${testStrategySummary}
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
fs.writeFileSync(reportPath, report);
|
|
111
|
+
console.log(`Phase ${phaseNum} report: ${reportPath}`);
|
|
112
|
+
|
|
113
|
+
// 4. Update ROADMAP.md status
|
|
114
|
+
const roadmapPath = path.join(PLANNING, "ROADMAP.md");
|
|
115
|
+
if (fs.existsSync(roadmapPath)) {
|
|
116
|
+
let roadmap = fs.readFileSync(roadmapPath, "utf-8");
|
|
117
|
+
const newStatus = failed === 0 ? "complete" : "needs-fixes";
|
|
118
|
+
const regex = new RegExp(`(## Phase ${phaseNum}:.+?)— \`\\w+\``, "m");
|
|
119
|
+
roadmap = roadmap.replace(regex, `$1— \`${newStatus}\``);
|
|
120
|
+
fs.writeFileSync(roadmapPath, roadmap);
|
|
121
|
+
console.log(`ROADMAP.md: Phase ${phaseNum} → ${newStatus}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 5. Summary
|
|
125
|
+
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
126
|
+
console.log(` Draht ► PHASE ${phaseNum} ${failed === 0 ? "COMPLETE ✅" : "NEEDS FIXES ❌"}`);
|
|
127
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
128
|
+
console.log(`\n${passed} passed, ${failed} failed, ${skipped} skipped`);
|
|
129
|
+
if (failed === 0) {
|
|
130
|
+
console.log(`\nNext: gsd-verify-work ${phaseNum}`);
|
|
131
|
+
} else {
|
|
132
|
+
console.log(`\nNext: gsd-execute-phase ${phaseNum} --gaps-only`);
|
|
133
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Draht Post-Task Hook
|
|
6
|
+
* Runs after each task execution to validate and record results.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node gsd-post-task.js <phase> <plan> <task-num> <status> [commit-hash]
|
|
9
|
+
* Status: pass | fail | skip
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
const { execSync } = require("node:child_process");
|
|
15
|
+
|
|
16
|
+
const [phaseNum, planNum, taskNum, status, commitHash] = process.argv.slice(2);
|
|
17
|
+
if (!phaseNum || !planNum || !taskNum || !status) {
|
|
18
|
+
console.error("Usage: gsd-post-task.js <phase> <plan> <task-num> <status> [commit-hash]");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Toolchain detection — mirrors src/gsd/hook-utils.ts ──────────────────────
|
|
23
|
+
function detectToolchain(cwd) {
|
|
24
|
+
if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) {
|
|
25
|
+
return { pm: "bun", testCmd: "bun test", coverageCmd: "bun test --coverage", lintCmd: "bunx biome check ." };
|
|
26
|
+
}
|
|
27
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
|
|
28
|
+
return { pm: "pnpm", testCmd: "pnpm test", coverageCmd: "pnpm run test:coverage", lintCmd: "pnpm run lint" };
|
|
29
|
+
}
|
|
30
|
+
if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
|
|
31
|
+
return { pm: "yarn", testCmd: "yarn test", coverageCmd: "yarn run test:coverage", lintCmd: "yarn run lint" };
|
|
32
|
+
}
|
|
33
|
+
return { pm: "npm", testCmd: "npm test", coverageCmd: "npm run test:coverage", lintCmd: "npm run lint" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readHookConfig(cwd) {
|
|
37
|
+
const defaults = { coverageThreshold: 80, tddMode: "advisory", qualityGateStrict: false };
|
|
38
|
+
const configPath = path.join(cwd, ".planning", "config.json");
|
|
39
|
+
if (!fs.existsSync(configPath)) return defaults;
|
|
40
|
+
try {
|
|
41
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
42
|
+
const h = raw.hooks || {};
|
|
43
|
+
return {
|
|
44
|
+
coverageThreshold: typeof h.coverageThreshold === "number" ? h.coverageThreshold : defaults.coverageThreshold,
|
|
45
|
+
tddMode: h.tddMode === "strict" || h.tddMode === "advisory" ? h.tddMode : defaults.tddMode,
|
|
46
|
+
qualityGateStrict: typeof h.qualityGateStrict === "boolean" ? h.qualityGateStrict : defaults.qualityGateStrict,
|
|
47
|
+
};
|
|
48
|
+
} catch { return defaults; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const PLANNING = ".planning";
|
|
52
|
+
const LOG_FILE = path.join(PLANNING, "execution-log.jsonl");
|
|
53
|
+
const cwd = process.cwd();
|
|
54
|
+
const toolchain = detectToolchain(cwd);
|
|
55
|
+
const hookConfig = readHookConfig(cwd);
|
|
56
|
+
|
|
57
|
+
// 0. TDD cycle compliance check
|
|
58
|
+
if (commitHash) {
|
|
59
|
+
try {
|
|
60
|
+
const currentMsg = execSync(`git log --format=%s -n 1 ${commitHash} 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
61
|
+
if (/^green:/i.test(currentMsg)) {
|
|
62
|
+
const taskPrefix = `${phaseNum}-${planNum}-${taskNum}`;
|
|
63
|
+
const recentMsgs = execSync(`git log --format=%s -n 50 ${commitHash}~1 2>/dev/null`, { encoding: "utf-8" })
|
|
64
|
+
.trim()
|
|
65
|
+
.split("\n")
|
|
66
|
+
.filter((m) => m.includes(taskPrefix) || /^(red|green|refactor):/i.test(m));
|
|
67
|
+
const prevTaskMsg = recentMsgs.find((m) => /^(red|green|refactor):/i.test(m) && m.includes(taskPrefix));
|
|
68
|
+
if (!prevTaskMsg || !/^red:/i.test(prevTaskMsg)) {
|
|
69
|
+
const violation = `TDD violation: "green:" commit without preceding "red:" for task ${phaseNum}-${planNum}-${taskNum}`;
|
|
70
|
+
if (hookConfig.tddMode === "strict") {
|
|
71
|
+
console.error(`❌ ${violation}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
} else {
|
|
74
|
+
console.log(`⚠️ ${violation}`);
|
|
75
|
+
}
|
|
76
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify({
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
phase: parseInt(phaseNum, 10),
|
|
79
|
+
plan: parseInt(planNum, 10),
|
|
80
|
+
task: parseInt(taskNum, 10),
|
|
81
|
+
status: "tdd-violation",
|
|
82
|
+
warning: "green: commit without preceding red: commit",
|
|
83
|
+
commit: commitHash,
|
|
84
|
+
}) + "\n");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Not a git repo or commit not found — ignore
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 1. Append to execution log
|
|
93
|
+
const entry = {
|
|
94
|
+
timestamp: new Date().toISOString(),
|
|
95
|
+
phase: parseInt(phaseNum, 10),
|
|
96
|
+
plan: parseInt(planNum, 10),
|
|
97
|
+
task: parseInt(taskNum, 10),
|
|
98
|
+
status,
|
|
99
|
+
commit: commitHash || null,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + "\n");
|
|
103
|
+
|
|
104
|
+
// 2. Run type check and tests if status is pass
|
|
105
|
+
if (status === "pass") {
|
|
106
|
+
// Type check
|
|
107
|
+
try {
|
|
108
|
+
const tsCmd = toolchain.pm === "bun" ? "bun run tsgo --noEmit 2>&1" : "npx tsc --noEmit 2>&1";
|
|
109
|
+
execSync(tsCmd, { timeout: 30000, encoding: "utf-8", cwd });
|
|
110
|
+
// Run tests
|
|
111
|
+
try {
|
|
112
|
+
const testOutput = execSync(`${toolchain.testCmd} 2>&1`, { timeout: 60000, encoding: "utf-8", cwd });
|
|
113
|
+
const testMatch = testOutput.match(/(\d+) pass/);
|
|
114
|
+
const testCount = testMatch ? testMatch[1] : "?";
|
|
115
|
+
console.log(`✅ Task ${phaseNum}-${planNum}-${taskNum}: passed + types clean + ${testCount} tests pass`);
|
|
116
|
+
} catch (testErr) {
|
|
117
|
+
const testOut = testErr.stdout || testErr.stderr || "";
|
|
118
|
+
const failMatch = testOut.match(/(\d+) fail/);
|
|
119
|
+
if (failMatch) {
|
|
120
|
+
console.log(`⚠️ Task ${phaseNum}-${planNum}-${taskNum}: passed + types clean but ${failMatch[1]} test(s) failed`);
|
|
121
|
+
} else {
|
|
122
|
+
console.log(`✅ Task ${phaseNum}-${planNum}-${taskNum}: passed + types clean (no test runner found)`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const output = error.stdout || "";
|
|
127
|
+
const errorCount = (output.match(/error TS/g) || []).length;
|
|
128
|
+
if (errorCount > 0) {
|
|
129
|
+
console.log(`⚠️ Task ${phaseNum}-${planNum}-${taskNum}: passed but ${errorCount} type error(s) introduced`);
|
|
130
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify({
|
|
131
|
+
...entry,
|
|
132
|
+
warning: `${errorCount} type errors introduced`,
|
|
133
|
+
}) + "\n");
|
|
134
|
+
} else {
|
|
135
|
+
console.log(`✅ Task ${phaseNum}-${planNum}-${taskNum}: passed`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} else if (status === "fail") {
|
|
139
|
+
console.log(`❌ Task ${phaseNum}-${planNum}-${taskNum}: FAILED`);
|
|
140
|
+
|
|
141
|
+
// Check if we should create a fix plan
|
|
142
|
+
const logContent = fs.readFileSync(LOG_FILE, "utf-8");
|
|
143
|
+
const taskFailures = logContent.split("\n")
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.map((l) => JSON.parse(l))
|
|
146
|
+
.filter((e) => e.phase === parseInt(phaseNum, 10) && e.plan === parseInt(planNum, 10) && e.task === parseInt(taskNum, 10) && e.status === "fail");
|
|
147
|
+
|
|
148
|
+
if (taskFailures.length >= 3) {
|
|
149
|
+
console.log(`\n⚠️ Task has failed 3+ times. Consider creating a fix plan:`);
|
|
150
|
+
console.log(` draht create-fix-plan ${phaseNum} ${planNum} "Task ${taskNum} failed repeatedly"`);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
console.log(`⏭️ Task ${phaseNum}-${planNum}-${taskNum}: skipped`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. Update STATE.md last activity
|
|
157
|
+
const statePath = path.join(PLANNING, "STATE.md");
|
|
158
|
+
if (fs.existsSync(statePath)) {
|
|
159
|
+
let state = fs.readFileSync(statePath, "utf-8");
|
|
160
|
+
state = state.replace(
|
|
161
|
+
/## Last Activity:.*/,
|
|
162
|
+
`## Last Activity: ${new Date().toISOString().replace("T", " ").slice(0, 19)}`
|
|
163
|
+
);
|
|
164
|
+
fs.writeFileSync(statePath, state);
|
|
165
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Draht Pre-Execute Hook
|
|
6
|
+
* Runs before gsd-execute-phase to validate preconditions.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node gsd-pre-execute.js <phase-number>
|
|
9
|
+
* Exit 0 = proceed, Exit 1 = abort
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
const { execSync } = require("node:child_process");
|
|
15
|
+
|
|
16
|
+
const phaseNum = process.argv[2];
|
|
17
|
+
if (!phaseNum) {
|
|
18
|
+
console.error("Usage: gsd-pre-execute.js <phase-number>");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PLANNING = ".planning";
|
|
23
|
+
const errors = [];
|
|
24
|
+
const warnings = [];
|
|
25
|
+
|
|
26
|
+
// 1. Check .planning/ exists
|
|
27
|
+
if (!fs.existsSync(PLANNING)) {
|
|
28
|
+
errors.push("No .planning/ directory found. Run gsd-new-project first.");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Check STATE.md exists
|
|
32
|
+
if (!fs.existsSync(path.join(PLANNING, "STATE.md"))) {
|
|
33
|
+
errors.push("No STATE.md found. Project not initialized.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 3. Check ROADMAP.md exists and phase is defined
|
|
37
|
+
const roadmapPath = path.join(PLANNING, "ROADMAP.md");
|
|
38
|
+
if (fs.existsSync(roadmapPath)) {
|
|
39
|
+
const roadmap = fs.readFileSync(roadmapPath, "utf-8");
|
|
40
|
+
if (!roadmap.includes(`Phase ${phaseNum}`)) {
|
|
41
|
+
errors.push(`Phase ${phaseNum} not found in ROADMAP.md`);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
errors.push("No ROADMAP.md found.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 4. Check phase directory exists with plans
|
|
48
|
+
const phasesDir = path.join(PLANNING, "phases");
|
|
49
|
+
if (fs.existsSync(phasesDir)) {
|
|
50
|
+
const entries = fs.readdirSync(phasesDir);
|
|
51
|
+
const phaseDir = entries.find((e) => e.startsWith(String(phaseNum).padStart(2, "0") + "-"));
|
|
52
|
+
if (!phaseDir) {
|
|
53
|
+
errors.push(`No phase directory found for phase ${phaseNum}. Run gsd-plan-phase first.`);
|
|
54
|
+
} else {
|
|
55
|
+
const phaseFiles = fs.readdirSync(path.join(phasesDir, phaseDir));
|
|
56
|
+
const plans = phaseFiles.filter((f) => f.endsWith("-PLAN.md") && !f.includes("FIX"));
|
|
57
|
+
if (plans.length === 0) {
|
|
58
|
+
errors.push(`No plans found in phase ${phaseNum}. Run gsd-plan-phase first.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check plans have required elements
|
|
62
|
+
for (const planFile of plans) {
|
|
63
|
+
const content = fs.readFileSync(path.join(phasesDir, phaseDir, planFile), "utf-8");
|
|
64
|
+
if (!content.includes("<task")) {
|
|
65
|
+
warnings.push(`${planFile}: No <task> elements — plan may be incomplete`);
|
|
66
|
+
}
|
|
67
|
+
if (!content.includes("<verify>")) {
|
|
68
|
+
warnings.push(`${planFile}: Missing <verify> steps — tasks won't be verifiable`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 5. Check git status
|
|
75
|
+
try {
|
|
76
|
+
const status = execSync("git status --porcelain", { encoding: "utf-8" }).trim();
|
|
77
|
+
if (status) {
|
|
78
|
+
const lines = status.split("\n").length;
|
|
79
|
+
warnings.push(`${lines} uncommitted file(s) — consider committing first`);
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
warnings.push("Not a git repository — no commit tracking");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 6. Check for CONTINUE-HERE.md (unfinished session)
|
|
86
|
+
if (fs.existsSync(path.join(PLANNING, "CONTINUE-HERE.md"))) {
|
|
87
|
+
warnings.push("CONTINUE-HERE.md exists — previous session may be unfinished. Consider gsd-resume-work.");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 7. Check DOMAIN.md exists and has required sections
|
|
91
|
+
const domainPath = path.join(PLANNING, "DOMAIN.md");
|
|
92
|
+
if (!fs.existsSync(domainPath)) {
|
|
93
|
+
warnings.push("No .planning/DOMAIN.md found — domain model not documented. Run /new-project or /map-codebase to create it.");
|
|
94
|
+
} else {
|
|
95
|
+
const domainContent = fs.readFileSync(domainPath, "utf-8");
|
|
96
|
+
if (!domainContent.includes("## Bounded Contexts")) {
|
|
97
|
+
errors.push("DOMAIN.md is missing '## Bounded Contexts' section.");
|
|
98
|
+
}
|
|
99
|
+
if (!domainContent.includes("## Ubiquitous Language")) {
|
|
100
|
+
errors.push("DOMAIN.md is missing '## Ubiquitous Language' section.");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 8. Check TEST-STRATEGY.md exists
|
|
105
|
+
if (!fs.existsSync(path.join(PLANNING, "TEST-STRATEGY.md"))) {
|
|
106
|
+
warnings.push("No .planning/TEST-STRATEGY.md found — test strategy not documented. Run /new-project or /map-codebase to create it.");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 9. Check all plans have non-empty <test> sections
|
|
110
|
+
if (fs.existsSync(phasesDir)) {
|
|
111
|
+
const entries2 = fs.readdirSync(phasesDir);
|
|
112
|
+
const phaseDir2 = entries2.find((e) => e.startsWith(String(phaseNum).padStart(2, "0") + "-"));
|
|
113
|
+
if (phaseDir2) {
|
|
114
|
+
const phaseFiles2 = fs.readdirSync(path.join(phasesDir, phaseDir2));
|
|
115
|
+
const plans2 = phaseFiles2.filter((f) => f.endsWith("-PLAN.md") && !f.includes("FIX"));
|
|
116
|
+
for (const planFile of plans2) {
|
|
117
|
+
const content = fs.readFileSync(path.join(phasesDir, phaseDir2, planFile), "utf-8");
|
|
118
|
+
// Extract all <test>...</test> blocks and check they are non-empty
|
|
119
|
+
const testMatches = [...content.matchAll(/<test>([\s\S]*?)<\/test>/g)];
|
|
120
|
+
if (testMatches.length === 0) {
|
|
121
|
+
errors.push(`${planFile}: Missing <test> sections — TDD requires tests for every task`);
|
|
122
|
+
} else {
|
|
123
|
+
for (const m of testMatches) {
|
|
124
|
+
if (!m[1].trim()) {
|
|
125
|
+
errors.push(`${planFile}: Empty <test> section found — fill in test cases before executing`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Output
|
|
134
|
+
if (warnings.length > 0) {
|
|
135
|
+
console.log(`⚠️ ${warnings.length} warning(s):`);
|
|
136
|
+
for (const w of warnings) console.log(` - ${w}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (errors.length > 0) {
|
|
140
|
+
console.log(`\n❌ ${errors.length} error(s) — cannot proceed:`);
|
|
141
|
+
for (const e of errors) console.log(` - ${e}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`✅ Pre-execute checks passed for phase ${phaseNum}`);
|
|
146
|
+
process.exit(0);
|